Skip to content

Comments

Add PIN attempt limiting with PBKDF2 key stretching#135

Open
wksantiago wants to merge 3 commits intomainfrom
PIN-attempt-limiting
Open

Add PIN attempt limiting with PBKDF2 key stretching#135
wksantiago wants to merge 3 commits intomainfrom
PIN-attempt-limiting

Conversation

@wksantiago
Copy link
Contributor

@wksantiago wksantiago commented Jan 23, 2026

Summary

  • Add PBKDF2 key stretching (100k iterations) for PIN-derived encryption key
  • Implement progressive delay schedule after failed attempts (15s/1min/15min)
  • Device bricks and wipes all secrets after 21 failed attempts
  • Support SE-based attempt tracking with NVS fallback
  • HMAC-protected state prevents counter tampering attacks

Test plan

  • Run test_pin_attempt_limit native tests
  • Verify progressive delays after failed PIN attempts
  • Confirm device bricks after 21 consecutive failures
  • Test SE persistence when secure element is available
  • Verify HMAC detects tampered state (on-device test with real mbedtls)

Closes #68

Summary by CodeRabbit

  • New Features

    • PIN rate limiting now persists across restarts and exposes APIs to query attempts, max attempts, remaining delay, and bricked status.
    • Devices auto-brick after maximum failed PIN attempts and can trigger device wipe on persistent failure.
  • Security Improvements

    • PIN state integrity is protected with HMAC-backed validation and stronger key derivation/salt handling.
    • Added a constant-time memory comparison helper.
  • Tests

    • Added comprehensive native tests for PIN attempt limits, persistence, bricking, delay scheduling, and tamper detection.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Jan 23, 2026

Warning

Rate limit exceeded

@wksantiago has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 5 minutes and 22 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

Walkthrough

Introduces persistent PIN attempt limiting with per-device PBKDF2/HKDF key derivation, HMAC-backed PIN state stored in secure element or NVS, exponential backoff delays, device bricking after max failures, and new runtime/test accessors and native tests.

Changes

Cohort / File(s) Summary
Constant-Time Comparison Utilities
components/crypto_asm/include/crypto_asm.h, test/native/mocks/crypto_asm.h
Adds static inline int secure_memcmp(const void *a, const void *b, size_t len) delegating to ct_compare() to provide a constant-time memory comparison wrapper.
Storage Crypto Core Implementation
main/storage_crypto.c
Replaces in-memory rate-limiter with persistent pin_state_t, per-device salt handling, PBKDF2-HMAC-SHA256 + HKDF flow for storage key, HMAC-based state integrity, SE/NVS backend abstraction, bricking and device-wipe logic, delay computation, load/save lifecycle, and several UNIT_TEST hooks.
Storage Crypto Public API
main/storage_crypto.h
Exports runtime query functions: storage_crypto_get_attempts, storage_crypto_get_max_attempts, storage_crypto_get_delay_remaining, storage_crypto_is_bricked and UNIT_TEST hooks: storage_crypto_reset_rate_limit, storage_crypto_set_attempts_for_test, storage_crypto_set_bricked_for_test.
Storage Crypto Mock Accessors
test/native/mocks/storage_crypto.h
Implements mock accessors and in-memory mock state (mock_pin_attempts, mock_is_bricked) to mirror new runtime queries for tests.
PIN Attempt Limit Test Suite
test/native/test_pin_attempt_limit.c
New native test binary exercising initialization, delay schedule, attempt tracking, bricking, SE persistence, PBKDF2/HMAC flows, tamper detection, and edge cases (13+ tests, mock SE and lightweight test harness).
Test Build Configuration
test/native/CMakeLists.txt
Adds conditional test_pin_attempt_limit target compiled/linked when MBEDCRYPTO_LIB is present; otherwise emits a skip message.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant App as Application
    participant SC as StorageCrypto
    participant SE as SecureElement/NVS
    participant Crypto as PBKDF2/HMAC

    User->>App: Enter PIN
    App->>SC: storage_crypto_init()/verify attempt
    SC->>SE: load pin_state (attempts,salt,hmac)
    SE-->>SC: pin_state
    SC->>Crypto: compute_state_hmac(state, device_id)
    Crypto-->>SC: hmac_value
    SC->>SC: validate HMAC == stored_hmac
    alt HMAC invalid
        SC->>App: error (tamper)
    else HMAC valid
        SC->>SC: if bricked -> return BRICKED
        SC->>SC: compute delay based on attempts
        alt delay > 0
            SC->>App: indicate delay remaining
        else
            App->>SC: perform PIN check (derive key)
            SC->>Crypto: PBKDF2/HKDF derive storage key
            Crypto-->>SC: derived_key
            alt PIN correct
                SC->>SC: reset attempts, save state
                SC->>SE: write pin_state (attempts=0,hmac)
                SE-->>SC: ack
                SC->>App: success
            else PIN incorrect
                SC->>SC: increment attempts
                alt attempts >= max
                    SC->>SC: set bricked, wipe device
                    SC->>SE: save bricked state
                    SE-->>SC: ack
                    SC->>App: device locked
                else
                    SC->>SE: save updated state
                    SE-->>SC: ack
                    SC->>App: retry info
                end
            end
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested reviewers

  • kwsantiago

"🐰
I dug a hole and found a key,
Salted, stretched, and HMAC'd for me.
Attempts now slow with every try,
Bricks stand guard against the sly.
Hop on—this vault is safe and free!"

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: adding PIN attempt limiting with PBKDF2 key stretching, which aligns with the core objectives.
Linked Issues check ✅ Passed The PR meets all linked issue #68 requirements: stores attempt counter persistently [storage_crypto.c], implements exponential backoff delays [get_delay_ms function], adds device lockout after 21 failures [bricked state logic], uses PBKDF2-HMAC-SHA256 key stretching [salt-based derivation], and includes comprehensive testing.
Out of Scope Changes check ✅ Passed All changes are tightly scoped to PIN attempt limiting: crypto_asm.h adds secure_memcmp helper for constant-time comparison, storage_crypto.c/h implements the core attempt-limiting and key-stretching logic, CMakeLists adds the test, and test files validate the implementation.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@wksantiago wksantiago requested a review from kwsantiago January 23, 2026 14:02
@wksantiago wksantiago self-assigned this Jan 23, 2026
@wksantiago
Copy link
Contributor Author

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Jan 23, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@main/storage_crypto.c`:
- Around line 343-368: The rate-limit can be bypassed after reboot because
storage_crypto_check_rate_limit reads pin_state.last_failure_time (set from a
monotonic timer) but compares it to get_time_ms which resets on reboot; change
the logic in storage_crypto_check_rate_limit and the pin state handling so you
persist a lockout_deadline (absolute expiry) instead of last_failure_time or
detect clock/monotonic resets: on failure set pin_state.lockout_deadline =
get_time_ms() + get_delay_ms(...), persist it, and in
storage_crypto_check_rate_limit compare current time to lockout_deadline (treat
missing/earlier current time than last stored time as still locked until
deadline) ensuring functions load_pin_state, get_delay_ms, and get_time_ms are
used consistently to compute and validate the absolute deadline.
- Around line 79-112: compute_state_hmac currently uses the device_id as the
HMAC key; replace that with a true secret key from a secure source (secure
element, eFuse, or flash-encryption-derived key). Modify compute_state_hmac to
accept or retrieve a secret key (e.g., a 32-byte key argument or call a secure
retrieval function like secure_element_get_key()), use that secret to build
key_padded/ipad/opad instead of device_id, and ensure the secret is never logged
and is wiped with secure_memzero after use; update callers to provide the secret
or ensure the function retrieves it from the secure element.
- Around line 305-319: The backoff function get_delay_ms currently returns
UINT32_MAX for attempts > 12, which prematurely triggers ERR_PIN_BRICKED before
PIN_MAX_ATTEMPTS (21); change the final condition so attempts
13–(PIN_MAX_ATTEMPTS-1) return the 15-minute backoff and only return UINT32_MAX
when attempts >= PIN_MAX_ATTEMPTS. Concretely, keep the existing branches for
<=3, <=6, <=9, then replace the <=12 branch with a branch that returns
15*60*1000 for attempts <= (PIN_MAX_ATTEMPTS - 1) (or explicitly for attempts <=
20) and return UINT32_MAX only when attempts >= PIN_MAX_ATTEMPTS; reference
get_delay_ms and the PIN_MAX_ATTEMPTS constant when making this change.
🧹 Nitpick comments (1)
test/native/CMakeLists.txt (1)

169-180: Consider using MbedTLS::mbedcrypto when available for consistency.

Lines 95-117 use find_package(MbedTLS) and the MbedTLS::mbedcrypto target when available. This new target only uses find_library, which could fail on systems where mbedtls is found via package config but not in standard library paths. For consistency:

♻️ Suggested improvement for consistent mbedtls handling
-find_library(MBEDCRYPTO_LIB mbedcrypto)
-if(MBEDCRYPTO_LIB)
+if(MbedTLS_FOUND OR MBEDCRYPTO_LIB)
     add_executable(test_pin_attempt_limit test_pin_attempt_limit.c ${MAIN_DIR}/random_utils.c ${MAIN_DIR}/hw_entropy.c ${MAIN_DIR}/error_codes.c)
     target_include_directories(test_pin_attempt_limit PRIVATE
         ${CMAKE_CURRENT_SOURCE_DIR}/mocks
         ${MAIN_DIR}
     )
     target_compile_definitions(test_pin_attempt_limit PRIVATE NATIVE_TEST=1 UNIT_TEST=1 MOCK_MBEDTLS=1)
-    target_link_libraries(test_pin_attempt_limit ${MBEDCRYPTO_LIB})
+    if(MbedTLS_FOUND)
+        target_link_libraries(test_pin_attempt_limit MbedTLS::mbedcrypto)
+    else()
+        target_link_libraries(test_pin_attempt_limit ${MBEDCRYPTO_LIB})
+    endif()
 else()
     message(STATUS "Skipping test_pin_attempt_limit (mbedtls not found)")
 endif()

@wksantiago wksantiago removed the request for review from kwsantiago January 24, 2026 13:58
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@main/storage_crypto.c`:
- Around line 50-55: The macro block defining NVS_NAMESPACE and NVS_KEY_*
constants violates clang-format style; run clang-format on main/storage_crypto.c
or reformat the block so the define names and string literals follow the
project's alignment rules (e.g. consistent spacing/tabs between macro names and
values) for NVS_NAMESPACE, NVS_KEY_FAILURES, NVS_KEY_LOCKOUT, NVS_KEY_BRICKED,
NVS_KEY_SALT and NVS_KEY_HMAC so the static analysis/CICD checks pass.
- Around line 153-192: compute_state_hmac currently swallows HMAC-key retrieval
failures by zeroing hmac_out and returning void; change its signature to return
an int (0 on success, non-zero on failure), propagate the error from
get_hmac_secret_key to the caller instead of writing a zero HMAC, and ensure
callers (e.g., the state load/save functions that call compute_state_hmac) check
the return value and abort/skip accepting or writing state when the HMAC cannot
be computed; keep the existing secure_memzero calls for secret_key,
ipad/opad/inner_hash on both success and failure paths and ensure hmac_out is
not used when compute_state_hmac returns an error.
- Around line 429-433: The lockout deadline overflows because get_time_ms(),
pin_state.lockout_deadline, and local now are 32-bit; change get_time_ms() to
return uint64_t, change pin_state.lockout_deadline to uint64_t, update any local
uint32_t now variables to uint64_t, and replace NVS calls
nvs_get_u32()/nvs_set_u32() with nvs_get_u64()/nvs_set_u64() where the deadline
is persisted (ensure reading/writing uses the 64-bit key). Update any related
comparisons and assignments in the functions that reference get_time_ms() and
pin_state.lockout_deadline (e.g., the lockout check and setting code) so all
time math uses 64-bit to prevent wrap-around.
♻️ Duplicate comments (1)
main/storage_crypto.c (1)

83-149: Fallback HMAC key is still predictable when SE is absent.
The non‑SE path hashes device_id/serial, which isn’t secret, so an attacker can recompute the HMAC and tamper with the NVS state. Please use a true secret source (e.g., SE-stored key, flash‑encryption/eFuse/NVS‑encryption derived key) or treat state as untrusted in this mode.

@wksantiago wksantiago requested a review from kwsantiago January 24, 2026 14:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add PIN attempt limiting and key stretching

1 participant