Skip to content

fix(ios): gate hot-key signing behind Face ID via .userPresence on device#299

Merged
WiktorStarczewski merged 3 commits into
mainfrom
ios-hotkey-user-presence
Jun 27, 2026
Merged

fix(ios): gate hot-key signing behind Face ID via .userPresence on device#299
WiktorStarczewski merged 3 commits into
mainfrom
ios-hotkey-user-presence

Conversation

@WiktorStarczewski

@WiktorStarczewski WiktorStarczewski commented Jun 27, 2026

Copy link
Copy Markdown
Collaborator

What

Adds .userPresence to the iOS Secure Enclave hot-key access control so user-initiated hot signing (and hot-key reveal) require Face ID / Touch ID on real devices — closing an iOS-only gap where these operations signed silently.

Also bundles a small Xcode-26 build fix (see end) that the iOS build needs to compile at all.

Why (.userPresence)

The SE hot key in HotKeyPlugin.swift was created with .privateKeyUsage only. .privateKeyUsage is a usage permission — by itself it does not prompt for authentication (only .userPresence / .biometryAny / .biometryCurrentSet / .devicePasscode do). The in-code comment claiming ".privateKeyUsage triggers Face ID" was simply wrong.

Consequences:

  • iOS: SecKeyCreateDecryptedData unwrapped the secret with no prompt → user-initiated Guardian claims/sends signed silently, despite the design intent of a "tap-to-confirm biometric on mobile".
  • Android: already gated — HotKeyPlugin.kt uses setUserAuthenticationRequired(true) + StrongBox and unwraps inside a BiometricPrompt. The two platforms had diverged.

The change

generateHotKey now builds the access-control flags as:

var accessFlags: SecAccessControlCreateFlags = [.privateKeyUsage]
#if !targetEnvironment(simulator)
accessFlags.insert(.userPresence)
#endif
  • .userPresence, not .biometryCurrentSet — both require the user to be present, but .biometryCurrentSet invalidates the key on biometric re-enrollment (bricks the hot key until re-activation). .userPresence survives re-enrollment and falls back to the device passcode.
  • Device-only (#if !targetEnvironment(simulator)) — mirrors the existing kSecAttrTokenIDSecureEnclave simulator guard. On the simulator the host SE is unavailable and biometrics aren't reliably enrolled, so we keep .privateKeyUsage-only there; this keeps the iOS E2E harness's silent signing working.

Also corrects the two now-inaccurate code comments.

Scope / caveats

  • Background auto-consume is unaffected — it's cold-signed in WASM (buildColdMultisigService) and never touches the hot key, so silent background claims keep working.
  • Existing hot keys keep their prior (ungated) behavior until the key is re-activated/rotated — the access control is fixed at key-creation time. A forced migration is out of scope (and a product decision); the existing replace-hot-key / re-activation flows naturally upgrade a key when used.
  • The gate runs on device builds only. CI builds iOS for the simulator destination, so the #if !simulator branch isn't compiled/exercised in CI — it must be verified on a physical device.

Also bundled: Xcode-26 iOS build fix

main's iOS build is currently broken under Xcode 26 (the macos-26 CI runner): two foundKey as? SecKey downcasts (signWithHotKey line ~246, revealHotKey line ~352) are no-ops for CoreFoundation types — they always succeed — which Xcode 26 rejects as a hard error. Replaced each with a CFGetTypeID(foundKey) == SecKeyGetTypeID() guard + force-cast (the correct defensive downcast).

This fix is byte-identical to the one already on #248, so the two won't conflict when both land. Bundling it here lets this PR's iOS build go green and unbreaks main's iOS release build independently of #248's timeline.

Verification

  • New SE access-control code typechecks against both SDKs — xcrun --sdk iphoneos swiftc -typecheck (compiles the .userPresence branch) and --sdk iphonesimulator both exit 0.
  • build-mobile.yml (iOS Simulator, macos-26) on this branch: the first run revealed the pre-existing as? SecKey break (Android passed); re-run after the bundled fix confirms the full iOS project compiles.
  • Manual device check (Face ID prompt on a user-initiated claim) still pending — flagged because CI can't cover the device path.

Android already enforces the biometric gate; no Android change.

@WiktorStarczewski WiktorStarczewski merged commit 85c2776 into main Jun 27, 2026
18 checks passed
@WiktorStarczewski WiktorStarczewski deleted the ios-hotkey-user-presence branch June 27, 2026 11:48
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.

1 participant