fix(android): Use CryptoObject to prevent UserNotAuthenticatedException - #788
fix(android): Use CryptoObject to prevent UserNotAuthenticatedException#788grndcherokee wants to merge 4 commits into
Conversation
Fix race condition between BiometricPrompt.onAuthenticationSucceeded and subsequent crypto operations that causes "User not authenticated" errors on some Android devices (particularly Samsung S24/S25). When setUserAuthenticationRequired(true) is set on a KeyStore key with a validity duration, there's a timing window between when the callback fires and when the crypto operation executes. On some devices, this window can expire before the crypto runs. The fix uses BiometricPrompt.CryptoObject to atomically bind the biometric authentication to the crypto operation: 1. Pre-initialize Cipher before calling BiometricPrompt.authenticate() 2. Pass it as BiometricPrompt.CryptoObject(cipher) 3. Use authenticated cipher from AuthenticationResult.cryptoObject This ensures the crypto operation is cryptographically bound to the biometric authentication, eliminating timing-related failures. Changes: - ResultHandler.kt: Add optional cipher field to CryptoContext - CipherStorageKeystoreRsaEcb.kt: Init cipher for decrypt - CipherStorageKeystoreAesGcm.kt: Init cipher for encrypt/decrypt - ResultHandlerInteractiveBiometric.kt: Use CryptoObject for auth
|
Hi @grndcherokee. I'm using your version on my project. On my package.json i'm using "react-native-keychain": "git+https://github.com/grndcherokee/react-native-keychain.git#fix/crypto-object-binding", instead of "react-native-keychain": "10.0.0". But, now, on my android device I'm only able to log in using fingerprint biometrics. The error message "authenticate to retrieve secret" appear and only fingerprint biometrics is acessible for log in. |
|
Hi! Thanks for trying out the fork and reporting this issue. This is actually expected behavior when using Good news: I've just pushed an update that makes this behavior configurable via a new To restore Face Unlock support, you can now explicitly set it to await Keychain.getGenericPassword({
authenticateWithCryptoObject: false, // Allow all biometric types including Face Unlock
});If you want the stronger security that prevents the "User not authenticated" race condition (but limits to fingerprint), use: await Keychain.getGenericPassword({
authenticateWithCryptoObject: true, // Class 3 biometrics only (fingerprint)
});Please update to the latest commit on the |
Add `authenticateWithCryptoObject` option to SetOptions and GetOptions to allow developers to opt-in to CryptoObject-bound biometric authentication. When enabled: - Biometric authentication is atomically bound to the crypto operation - Prevents race conditions causing "User not authenticated" errors - Only Class 3 (Strong) biometrics allowed (typically fingerprint) - Face Unlock may not work on devices with Class 2 classification When disabled (default): - Standard authentication flow is used - All biometric types supported including Face Unlock - Maintains backward compatibility with existing implementations This makes the security enhancement opt-in to avoid breaking changes for users who rely on Face Unlock or other Class 2 biometrics. Co-authored-by: Cursor <cursoragent@cursor.com>
fd4cacb to
7246541
Compare
|
Hi @DorianMazur! I addressed the feedback from the previous review and pushed the requested changes. |
… enabled Skip the cipher pre-initialization in UserNotAuthenticatedException catch blocks when authenticateWithCryptoObject is false (the default). The extra cipher.init() calls add latency before the biometric prompt appears, causing test-android (35) to fail after the turbo module merge enabled new arch. Made-with: Cursor
|
Pushed a fix for the API 35 test failure ( Root cause: The cipher pre-initialization in the Fix: Added a Could someone approve the workflow run so CI can verify? |
|
Hi @DorianMazur 👋 Just a gentle nudge — when you get a chance, could you finish your review on this PR? I've addressed all the previous feedback and the latest fix (de25b97) resolves the API 35 test failure. Happy to make any further changes if needed. Thanks! |
|
Hey @oblador / @DorianMazur / @vonovak / @pcoltau 👋 Just a gentle nudge - Would you be OK if we merge this? |
|
Hey @DorianMazur — just following up on this one. I know you've been busy with the turbo module work and other PRs, so I appreciate your time. Quick summary of where this stands: All 7 CI checks are passing |
|
Hey @oblador / @DorianMazur / @vonovak / @pcoltau 👋 Just a gentle nudge - Would you be OK if we merge this? |
1 similar comment
|
Hey @oblador / @DorianMazur / @vonovak / @pcoltau 👋 Just a gentle nudge - Would you be OK if we merge this? |
|
Hey @oblador / @DorianMazur / @vonovak / @pcoltau 👋 Just a gentle nudge - Would you be OK if we merge this? |
Summary
Fix race condition between
BiometricPrompt.onAuthenticationSucceededand subsequent crypto operations that causesCryptoFailedException: User not authenticatedon some Android devices (particularly Samsung S24/S25).Problem
When
setUserAuthenticationRequired(true)is set on a KeyStore key with a validity duration (e.g., 5 seconds), there's a timing window between:BiometricPrompt.onAuthenticationSucceededcallback firingcipher.doFinal()) executingOn some devices, particularly Samsung Galaxy S24/S25 with fingerprint authentication, this window can expire before the crypto operation runs, causing
UserNotAuthenticatedExceptionwrapped inCryptoFailedException.The root cause is that the current implementation calls
prompt.authenticate(promptInfo)without aCryptoObject, then attempts crypto operations in the success callback. This decouples the authentication from the crypto operation, creating a race condition.Solution
Use
BiometricPrompt.CryptoObjectto atomically bind the biometric authentication to the crypto operation, controlled via a new opt-in option:Cipherbefore callingBiometricPrompt.authenticate()BiometricPrompt.CryptoObject(cipher)toauthenticate()AuthenticationResult.cryptoObjectinonAuthenticationSucceededThis ensures the crypto operation is cryptographically bound to the biometric authentication, eliminating timing-related failures.
New Option:
authenticateWithCryptoObjectA new optional parameter has been added to
SetOptionsandGetOptions:When enabled (
true):When disabled (
false, default):Changes
types.ts: AddauthenticateWithCryptoObject?: booleantoSetOptionsandGetOptionsKeychainModule.kt: Add constant, helper, and pass option through decrypt/encrypt call chainResultHandlerProvider.kt: Accept and passauthenticateWithCryptoObjectparameterResultHandlerInteractiveBiometric.kt: Conditionally useCryptoObjectinauthenticateWithPrompt()and use authenticated cipher fromAuthenticationResult.cryptoObjectin success callbackResultHandlerInteractiveBiometricManualRetry.kt: Pass through the parameterResultHandler.kt: Add optionalcipherfield toCryptoContextCipherStorageKeystoreRsaEcb.kt: Initialize cipher before biometric promptCipherStorageKeystoreAesGcm.kt: Initialize cipher before biometric promptBackwards Compatibility
false, preserving existing behaviorTesting
accessControlTest.spec.jswhich test biometric save/load flowsRelated Issues
Fixes timing-related
UserNotAuthenticatedExceptionerrors reported on Samsung devices.