Developing Fingerprint Biometric Authorization in Android App
On Android biometrics went through long path: FingerprintManager (deprecated API 28), BiometricPrompt (appeared in API 28, worked properly in 29–30), and finally stable androidx.biometric:biometric library version 1.2+. If app still uses FingerprintManager — it's technical debt that will explode on API 34 target.
What breaks most often
BiometricPrompt requires passing FragmentActivity or Fragment. Developers sometimes try calling it from ViewModel or Repository — get IllegalStateException at runtime. Prompt lives in UI layer, period.
Second stone — CryptoObject. Many implementations call BiometricPrompt.authenticate() without CryptoObject, meaning they check only biometric presence, but don't tie it to cryptographic operation. This is "weak" biometrics: attacker with root access can theoretically fake authentication result by injecting SUCCESS into AuthenticationCallback. Correct path — Class 3 (Strong) biometrics with CryptoObject.
Third — Android fragmentation. On MIUI 12–13 BiometricManager.canAuthenticate(BIOMETRIC_STRONG) returns BIOMETRIC_ERROR_NONE_ENROLLED even with registered fingerprints due to Xiaomi customization. Have to add fallback check via FingerprintManagerCompat for such cases.
Correct implementation with CryptoObject
Essence: generate key in Android Keystore tied to biometrics. On authentication Cipher initialized with this key and passed to CryptoObject. If biometrics passed successfully — cipher unlocked and can encrypt/decrypt data.
val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore")
keyGenerator.init(
KeyGenParameterSpec.Builder(KEY_NAME, KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
.setUserAuthenticationRequired(true)
.setInvalidatedByBiometricEnrollment(true)
.build()
)
keyGenerator.generateKey()
setInvalidatedByBiometricEnrollment(true) — key invalidated on adding new fingerprint. Without this flag old key remains working after user changes biometrics.
After key generation:
val cipher = Cipher.getInstance("${KeyProperties.KEY_ALGORITHM_AES}/${KeyProperties.BLOCK_MODE_CBC}/${KeyProperties.ENCRYPTION_PADDING_PKCS7}")
val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
val secretKey = keyStore.getKey(KEY_NAME, null) as SecretKey
cipher.init(Cipher.ENCRYPT_MODE, secretKey)
val cryptoObject = BiometricPrompt.CryptoObject(cipher)
Then pass cryptoObject to biometricPrompt.authenticate(promptInfo, cryptoObject).
Callback must be handled completely
object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
val cipher = result.cryptoObject?.cipher ?: return
// decrypt token from EncryptedSharedPreferences
}
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
when (errorCode) {
BiometricPrompt.ERROR_LOCKOUT -> showFallback()
BiometricPrompt.ERROR_LOCKOUT_PERMANENT -> showPermanentLockout()
BiometricPrompt.ERROR_NEGATIVE_BUTTON -> showPinAuth()
BiometricPrompt.ERROR_USER_CANCELED -> { /* do nothing */ }
}
}
override fun onAuthenticationFailed() {
// attempt failed, but limit not exhausted — BiometricPrompt itself shows error
}
}
onAuthenticationFailed — not final error. System itself updates prompt UI. Don't hide prompt and don't show your own errors in this callback.
Token storage
Use EncryptedSharedPreferences from androidx.security:security-crypto. Encrypt token via cipher from successful CryptoObject, save ciphertext + salt + IV to EncryptedSharedPreferences. Next time authorizing: deploy biometrics in DECRYPT_MODE with saved IV → get plaintext token.
Stages and timeframe
Check minimum API level (our target — API 23+, BiometricPrompt works from API 28 via androidx.biometric) → KeyStore key and CryptoObject-flow implementation → custom prompt UI with texts → handle all error codes → test on real devices (Samsung Galaxy, Xiaomi, Pixel) → unit test coverage via mock BiometricPrompt.
Timeframe — 3–6 business days. On Xiaomi and devices with custom firmware add time for separate compatibility check.







