Biometric authentication
BiometricPrompt (androidx.biometric) is the unified API for fingerprint/face auth: it shows the system dialog and hides sensor differences.
Key concepts:
- Authenticator classes:
BIOMETRIC_STRONG(Class 3 — allowed for cryptographic operations),BIOMETRIC_WEAK(Class 2),DEVICE_CREDENTIAL(PIN/pattern fallback). - canAuthenticate() — checks biometrics exist/are enrolled/available; route to enrollment accordingly.
- CryptoObject — binding biometrics to cryptography: a Keystore key created with
setUserAuthenticationRequired(true); the cipher works only after successful biometrics.
The critical distinction — two levels:
- Weak integration:
onAuthenticationSucceeded→ justif (success) unlock()— hookable with Frida and bypassable. - Strong integration: the success callback hands you the CryptoObject's cipher → real data (the refresh token) is decrypted with it — without biometrics the data stays sealed, making hooks useless.
In banking: biometrics for login + re-confirmation for critical operations (large transfers, limit changes) — step-up auth.
// Keystore açarı: yalnız auth-dan sonra istifadə oluna bilər
private fun buildKey() {
val spec = KeyGenParameterSpec.Builder(
"biometric_key",
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setUserAuthenticationRequired(true) // biometrika şərti
.setInvalidatedByBiometricEnrollment(true) // yeni barmaq → açar ləğv
.build()
KeyGenerator.getInstance("AES", "AndroidKeyStore")
.apply { init(spec) }.generateKey()
}
val prompt = BiometricPrompt(this, executor,
object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
val cipher = result.cryptoObject?.cipher ?: return
val token = String(cipher.doFinal(encryptedRefreshToken)) // real decrypt
proceedWithLogin(token)
}
})
prompt.authenticate(
BiometricPrompt.PromptInfo.Builder()
.setTitle("Bank hesabınıza giriş")
.setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
.setNegativeButtonText("PIN ilə daxil ol")
.build(),
BiometricPrompt.CryptoObject(cipher)
)Strong integration via CryptoObject
Interview question: "What should happen when a new fingerprint is enrolled?" The banking answer: with setInvalidatedByBiometricEnrollment(true) the key auto-invalidates → the user signs in again with full credentials → biometrics get re-enabled. The reason: an attacker who adds their own finger to the device must not get in. It is a UX-security trade-off — non-critical apps skip it, a bank must not.
🛠 Practice task
Compare the two levels of BiometricPrompt:
- Start simple:
onAuthenticationSucceeded→unlock(). Test it with an enrolled fingerprint on the emulator. - Then go strong: create a Keystore key with
setUserAuthenticationRequired(true), encrypt a string with it, and make decryption possible only through theCryptoObject. - Enroll a new fingerprint on the emulator and watch the key invalidate thanks to
setInvalidatedByBiometricEnrollment.
Done when: you can state in one sentence what changes for an attacker between the two.
📚 Sources and documentation
- Biometric authenticationofficialdeveloper.android.com
- androidx.biometric referenceofficialdeveloper.android.com