Skip to content

feat: invalidate biometric keys when biometric is changed on ios#79

Merged
sbaiahmed1 merged 3 commits into
sbaiahmed1:mainfrom
omar0404:main
Mar 25, 2026
Merged

feat: invalidate biometric keys when biometric is changed on ios#79
sbaiahmed1 merged 3 commits into
sbaiahmed1:mainfrom
omar0404:main

Conversation

@omar0404

@omar0404 omar0404 commented Mar 17, 2026

Copy link
Copy Markdown

#77

Summary by CodeRabbit

  • New Features

    • Detects biometric enrollment/current-set changes and marks affected biometric keys as inaccessible.
    • New explicit error for "biometry current set changed" requiring re‑enrollment.
  • Improvements

    • Optional stronger binding of keys to the current biometric set for enhanced security.
    • Clears or persists biometric state appropriately when creating, deleting or verifying keys; respects device‑credential fallback.
  • Documentation

    • Added iOS migration guidance and updated key/parameter docs for new mappings and behavior.

@coderabbitai

coderabbitai Bot commented Mar 17, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Adds per-key biometric domain-state persistence and change detection on iOS; integrates these checks into key creation, deletion, integrity validation, and signature verification; and updates access-control creation to optionally use .biometryCurrentSet when biometricStrength == "strong".

Changes

Cohort / File(s) Summary
Domain State Management & Key Flows
ios/ReactNativeBiometrics.swift
Adds private helpers to compute per-key UserDefaults storage keys, persist/clear base64 LAContext.evaluatedPolicyDomainState, and detect enrollment/current-set changes. Integrates domain-state logic into createKeys, deleteKeys, validateKeyIntegrity, and verifyKeySignatureWithEncoding to short-circuit with a specific error when changed.
Error Handling
ios/ReactNativeBiometricsError.swift
Adds .biometryCurrentSetChanged enum case and maps it in errorInfo to BIOMETRY_CURRENT_SET_CHANGED with an enrollment re-enrollment message.
Biometric Access Control & Simulator Mapping
ios/Utils.swift
Extends createBiometricAccessControl signature to accept useBiometryCurrentSet and selects .biometryCurrentSet when appropriate; updates simulator deriveLAPolicy(from:) to recognize .biometryCurrentSet alongside .biometryAny.
Docs / Migration Guidance
README.md, docs/CRYPTOGRAPHIC_KEYS.md
Documents biometricStrength → iOS access-control mapping (strong.biometryCurrentSet, default → .biometryAny), allowDeviceCredentials.userPresence behavior, and adds migration guidance that keys are bound at creation and .biometryCurrentSet keys are invalidated on enrollment changes.

Sequence Diagram(s)

sequenceDiagram
    participant App
    participant RNBiometrics as RNBiometrics (iOS)
    participant LA as LocalAuthentication (LAContext)
    participant Keychain
    participant Storage as UserDefaults

    App->>RNBiometrics: createKeys(alias, biometricStrength, allowDeviceCredentials)
    RNBiometrics->>LA: evaluatePolicy / get evaluatedPolicyDomainState
    LA-->>RNBiometrics: domainState (base64)
    RNBiometrics->>RNBiometrics: compute storage key for alias
    RNBiometrics->>Keychain: create SecKey with SecAccessControl(useBiometryCurrentSet?)
    Keychain-->>RNBiometrics: key created
    alt allowDeviceCredentials == true
      RNBiometrics->>Storage: clear stored domain state for alias
    else
      RNBiometrics->>Storage: persist domainState for alias
    end

    App->>RNBiometrics: verifyKeySignature(alias, data)
    RNBiometrics->>Storage: read stored domainState for alias
    RNBiometrics->>LA: obtain current domainState
    RNBiometrics->>RNBiometrics: compare stored vs current
    alt domain state changed
      RNBiometrics-->>App: { success: false, error: BIOMETRY_CURRENT_SET_CHANGED }
    else
      RNBiometrics->>Keychain: locate SecKey and attempt signing/verification
      Keychain-->>RNBiometrics: sign/verify result
      RNBiometrics-->>App: { success: true/false, details }
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

Poem

🐇 I hopped through LAContext, saved each state,

Keys tied to whiskers, snug as fate,
If prints change, I curtsey and pause,
Ask for new keys — such are my laws,
A rabbit's watch over your secret gate.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 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 functionality to invalidate biometric keys when biometric enrollment changes on iOS, which is the core feature implemented across all modified files.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@ios/ReactNativeBiometrics.swift`:
- Around line 61-78: hasBiometricDomainStateChanged currently returns false when
LAContext.canEvaluatePolicy fails (biometrics unavailable/unenrolled), which
hides the change; update hasBiometricDomainStateChanged (and its use of
LAContext.canEvaluatePolicy and evaluatedPolicyDomainState) to treat a failed
canEvaluatePolicy as a domain-state change (return true) or propagate/translate
it into the biometryCurrentSetChanged condition so callers will re-enroll;
specifically, after creating LAContext() and calling
context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error:
&error), if it returns false or context.evaluatedPolicyDomainState is nil,
return true (or surface the equivalent error) instead of false, keeping
biometricDomainStateStorageKey lookup and storedDomainState comparison intact.

In `@ios/Utils.swift`:
- Around line 165-172: Changing the SecAccessControlCreateFlags from
.biometryAny to .biometryCurrentSet is a breaking change for existing keys;
update the code and release to handle migration by either (1) attempting to
access existing keys by trying both policies when reading keys (check code paths
that use authConstraint and the allowDeviceCredentialsFallback decision), (2)
providing a migration path to recreate/re-enroll keys created with .biometryAny,
or (3) gating the new behavior behind a flag and documenting it; also add clear
release notes/migration guidance stating that keys created prior to this change
remain bound to .biometryAny and will be invalidated by biometric enrollment
changes when using .biometryCurrentSet.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8063956a-6c18-41cc-a8ee-74adf256f39a

📥 Commits

Reviewing files that changed from the base of the PR and between 9aee70a and 6366a66.

📒 Files selected for processing (3)
  • ios/ReactNativeBiometrics.swift
  • ios/ReactNativeBiometricsError.swift
  • ios/Utils.swift

Comment thread ios/ReactNativeBiometrics.swift
Comment thread ios/Utils.swift Outdated
@sbaiahmed1

Copy link
Copy Markdown
Owner

@omar0404 can you please resolve coderabbit threads ?

@omar0404

Copy link
Copy Markdown
Author

@omar0404 can you please resolve coderabbit threads ?

done , waiting for the second review

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
ios/ReactNativeBiometrics.swift (1)

565-577: ⚠️ Potential issue | 🟡 Minor

Clear the cached domain state only after deletion is confirmed.

The errSecSuccess branch removes the cached state before the follow-up lookup verifies that the key is actually gone. If that verification fails, the key remains but future enrollment-change checks for that alias are silently disabled.

Suggested fix
     case errSecSuccess:
-      clearStoredBiometricDomainState(for: keyTag)
       ReactNativeBiometricDebug.debugLog("Key with tag '\(keyTag)' deleted successfully")
       
       // Verify deletion
       let verifyStatus = SecItemCopyMatching(query as CFDictionary, nil)
       if verifyStatus == errSecItemNotFound {
+        clearStoredBiometricDomainState(for: keyTag)
         ReactNativeBiometricDebug.debugLog("Keys deleted and verified successfully")
         resolve(["success": true])
       } else {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ios/ReactNativeBiometrics.swift` around lines 565 - 577, The code currently
calls clearStoredBiometricDomainState(for: keyTag) immediately when deleteStatus
== errSecSuccess before verifying the key removal; change the flow in the switch
branch so that you only call clearStoredBiometricDomainState(for: keyTag) after
the follow-up verification (the SecItemCopyMatching call stored in verifyStatus)
confirms the key is gone (verifyStatus == errSecItemNotFound) and you
resolve(["success": true]); if verification fails, do not clear the cached state
and instead run the existing error path (handleError(.keyDeletionFailed, reject:
reject)) so enrollment-change checks remain intact for that alias.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@ios/ReactNativeBiometrics.swift`:
- Around line 441-443: The branch currently computes biometricStrengthValue and
useBiometryCurrentSet but still stores domain state for all non-fallback keys;
change the domain-state storage logic so it only records domain state when
useBiometryCurrentSet is true (i.e., only for .biometryCurrentSet keys) to avoid
blocking .biometryAny/default/weak keys after enrollment changes; apply the same
conditional fix in the other occurrence around the biometric domain-state
storage lines (the block referenced at 523–527) and ensure
hasBiometricDomainStateChanged checks remain compatible with this restricted
storage behavior.
- Around line 69-75: The guard conflates all canEvaluatePolicy failures with
enrollment changes; modify the logic around LAContext.canEvaluatePolicy and
evaluatedPolicyDomainState so you inspect the NSError (from canEvaluatePolicy)
and only treat LAError.biometryNotEnrolled as an enrollment change (return false
or trigger re-enroll), while treating LAError.biometryLockout as a temporary
state (return true / do not trigger re-enroll). Concretely, call
context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error:
&error), then if it returns false check (error as? LAError)?.code ==
.biometryNotEnrolled -> handle as enrollment-changed, else if .biometryLockout
-> treat as temporary and return success, and if evaluatedPolicyDomainState is
nil then treat that as an enrollment change.

---

Outside diff comments:
In `@ios/ReactNativeBiometrics.swift`:
- Around line 565-577: The code currently calls
clearStoredBiometricDomainState(for: keyTag) immediately when deleteStatus ==
errSecSuccess before verifying the key removal; change the flow in the switch
branch so that you only call clearStoredBiometricDomainState(for: keyTag) after
the follow-up verification (the SecItemCopyMatching call stored in verifyStatus)
confirms the key is gone (verifyStatus == errSecItemNotFound) and you
resolve(["success": true]); if verification fails, do not clear the cached state
and instead run the existing error path (handleError(.keyDeletionFailed, reject:
reject)) so enrollment-change checks remain intact for that alias.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e0942dd8-1499-44e2-b27f-fa221aa0532b

📥 Commits

Reviewing files that changed from the base of the PR and between 6366a66 and d8acf65.

📒 Files selected for processing (4)
  • README.md
  • docs/CRYPTOGRAPHIC_KEYS.md
  • ios/ReactNativeBiometrics.swift
  • ios/Utils.swift
✅ Files skipped from review due to trivial changes (2)
  • docs/CRYPTOGRAPHIC_KEYS.md
  • README.md

Comment thread ios/ReactNativeBiometrics.swift
Comment thread ios/ReactNativeBiometrics.swift
@sbaiahmed1

Copy link
Copy Markdown
Owner

Thank, and sorry for the delay it was Holidays for us
can you please rebase so we can merge ?

@sbaiahmed1

Copy link
Copy Markdown
Owner

can you please use rebase (thats what we use in the branch)

Screenshot 2026-03-25 at 11 17 48

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@README.md`:
- Around line 830-838: Remove the duplicated `failIfExists` documentation line
so the parameter is documented only once; keep the iOS-specific clarifications
for `biometricStrength` and `allowDeviceCredentials` intact and ensure the
single `failIfExists` entry (using `failIfExists` and `KEY_ALREADY_EXISTS`)
remains in the section, deleting the redundant duplicate line to avoid
repetition.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6b43a44b-9040-4b3d-9b0b-6bd20714001a

📥 Commits

Reviewing files that changed from the base of the PR and between 9b6708d and 568dd79.

📒 Files selected for processing (5)
  • README.md
  • docs/CRYPTOGRAPHIC_KEYS.md
  • ios/ReactNativeBiometrics.swift
  • ios/ReactNativeBiometricsError.swift
  • ios/Utils.swift
✅ Files skipped from review due to trivial changes (2)
  • docs/CRYPTOGRAPHIC_KEYS.md
  • ios/ReactNativeBiometrics.swift

Comment thread README.md
@sbaiahmed1
sbaiahmed1 merged commit 25a0080 into sbaiahmed1:main Mar 25, 2026
6 checks passed
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.

3 participants