Skip to content

fix(android): Use CryptoObject to prevent UserNotAuthenticatedException#788

Open
grndcherokee wants to merge 3 commits intooblador:masterfrom
grndcherokee:fix/crypto-object-binding
Open

fix(android): Use CryptoObject to prevent UserNotAuthenticatedException#788
grndcherokee wants to merge 3 commits intooblador:masterfrom
grndcherokee:fix/crypto-object-binding

Conversation

@grndcherokee
Copy link

Summary

Fix race condition between BiometricPrompt.onAuthenticationSucceeded and subsequent crypto operations that causes CryptoFailedException: User not authenticated on 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:

  1. BiometricPrompt.onAuthenticationSucceeded callback firing
  2. The actual crypto operation (cipher.doFinal()) executing

On some devices, particularly Samsung Galaxy S24/S25 with fingerprint authentication, this window can expire before the crypto operation runs, causing UserNotAuthenticatedException wrapped in CryptoFailedException.

The root cause is that the current implementation calls prompt.authenticate(promptInfo) without a CryptoObject, then attempts crypto operations in the success callback. This decouples the authentication from the crypto operation, creating a race condition.

Solution

Use BiometricPrompt.CryptoObject to atomically bind the biometric authentication to the crypto operation:

  1. Pre-initialize the Cipher before calling BiometricPrompt.authenticate()
  2. Pass it as BiometricPrompt.CryptoObject(cipher) to authenticate()
  3. Use the authenticated cipher from AuthenticationResult.cryptoObject in onAuthenticationSucceeded

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 for pre-initialized ciphers
  • CipherStorageKeystoreRsaEcb.kt: Initialize cipher before biometric prompt for decrypt operations
  • CipherStorageKeystoreAesGcm.kt: Initialize cipher before biometric prompt for encrypt/decrypt operations
  • ResultHandlerInteractiveBiometric.kt:
    • Use CryptoObject when calling BiometricPrompt.authenticate()
    • Use authenticated cipher from AuthenticationResult.cryptoObject in success callback
    • Add helper methods for crypto operations with pre-authenticated cipher

Backwards Compatibility

  • Falls back to original behavior if cipher initialization fails
  • No changes to key generation or storage format
  • Existing credentials remain valid and accessible
  • No changes required for consuming applications

Testing

  • Covered by existing E2E tests in accessControlTest.spec.js which test biometric save/load flows
  • The fix maintains backwards compatibility - if cipher initialization fails, it falls back to the original behavior
  • Tested manually on Samsung S24/S25 devices where the issue was reported

Related Issues

Fixes timing-related UserNotAuthenticatedException errors reported on Samsung devices.

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.
@tiagomelilo
Copy link

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.

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