fix(android): Use CryptoObject to prevent UserNotAuthenticatedException#788
fix(android): Use CryptoObject to prevent UserNotAuthenticatedException#788grndcherokee wants to merge 4 commits intooblador:masterfrom
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
This commit adds patches previously maintained separately by Klarna: 1. Skip StrongBox on slow OEM implementations (Motorola, Xiaomi) - Add SLOW_STRONGBOX_MANUFACTURERS set - Add isSlowStrongBoxManufacturer() function - Skip StrongBox key generation for these manufacturers 2. Enhanced error reporting with stack traces - Add generateUserInfo() helper to attach stack traces - Include stack traces in all promise.reject calls - Helps with debugging keychain errors in production These changes are additive and don't affect the CryptoObject fix.
When installing from GitHub (not npm), the prepare script doesn't run automatically, causing TypeScript type definitions to be missing. This commit includes the pre-built lib folder to ensure the package works correctly when installed directly from GitHub.
|
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. |
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>
|
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 |
|
|
||
| var secretKey: Key? = null | ||
| val supportsSecureHardware = DeviceAvailability.isStrongboxAvailable(applicationContext) | ||
| val slowStrongBox = DeviceAvailability.isSlowStrongBoxManufacturer() |
There was a problem hiding this comment.
Why do we need this and how do we know they are slow?
| // Prepare cipher for CryptoObject binding to prevent timing issues | ||
| // between biometric authentication and crypto operations. | ||
| val cipher = try { | ||
| val c = getCachedInstance() |
There was a problem hiding this comment.
Why in try catch? I don't think this function can throw.
| @@ -0,0 +1,115 @@ | |||
| "use strict"; | |||
There was a problem hiding this comment.
Please remove the lib files, clean up this PR.
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:AUTHENTICATE_WITH_CRYPTO_OBJECTconstantgetAuthenticateWithCryptoObject()helperResultHandlerProvider.kt: Accept and passauthenticateWithCryptoObjectparameterResultHandlerInteractiveBiometric.kt:authenticateWithCryptoObjectconstructor parameterCryptoObjectinauthenticateWithPrompt()AuthenticationResult.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.Migration Notes
For apps experiencing
CryptoFailedExceptionwith "User not authenticated" on Samsung devices:Note: When enabling this option, Face Unlock may stop working on some devices. Only enable if you're experiencing the race condition issue and fingerprint authentication is acceptable for your use case.