Illustration for Securing Android Applications with Biometrics

In the early days of Android, implementing fingerprint authentication was a fragmented nightmare. We had the FingerprintManager, but it was deprecated, inconsistent across manufacturers, and didn't support face unlock or iris scanning.

Fast forward to 2025, and the androidx.biometric library has standardized everything into a unified, secure API. But simply showing a biometric prompt isn't enough. To truly secure sensitive data—like banking information or personal notes—you need to leverage the power of the Android Keystore system in conjunction with biometrics.

In this post, we'll explore how to implement robust biometric authentication that doesn't just check who the user is, but cryptographically guarantees it.

The BiometricPrompt API

The core of modern Android biometrics is the BiometricPrompt. It handles the UI consistency across devices and supports various modalities (fingerprint, face, iris).

Here is the basic setup:

val executor = ContextCompat.getMainExecutor(this)
val biometricPrompt = BiometricPrompt(this, executor,
    object : BiometricPrompt.AuthenticationCallback() {
        override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
            super.onAuthenticationSucceeded(result)
            // Authentication successful!
            unlockSecretData(result.cryptoObject)
        }

        override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
            super.onAuthenticationError(errorCode, errString)
            // Handle error (e.g., user canceled, too many attempts)
        }
    })

val promptInfo = BiometricPrompt.PromptInfo.Builder()
    .setTitle("Unlock Secure Vault")
    .setSubtitle("Log in using your biometric credential")
    .setNegativeButtonText("Use Account Password")
    .build()

The Critical Piece: CryptoObject

If you only use the code above, you are trusting the OS to say "Yes, this is the user." But a compromised device (or a rooted one with a hooked framework) could potentially bypass this check.

To prevent this, we use a CryptoObject. The idea is simple:

  1. Generate a cryptographic key in the Android Keystore.
  2. Configure this key so it requires user authentication to be used.
  3. Pass a Cipher initialized with this key to the BiometricPrompt.
  4. The Keystore will only unlock the key if the biometric hardware confirms the user's identity.

1. Generating the Key

val keyGenerator = KeyGenerator.getInstance(
    KeyProperties.KEY_ALGORITHM_AES, 
    "AndroidKeyStore"
)

val keyGenParameterSpec = KeyGenParameterSpec.Builder(
    "my_secret_key",
    KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
)
.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
.setUserAuthenticationRequired(true) // <--- The magic line
.setUserAuthenticationParameters(0, KeyProperties.AUTH_BIOMETRIC_STRONG)
.build()

keyGenerator.init(keyGenParameterSpec)
keyGenerator.generateKey()

2. Authenticating with the Cipher

val cipher = Cipher.getInstance("AES/CBC/PKCS7Padding")
val secretKey = keyStore.getKey("my_secret_key", null) as SecretKey
cipher.init(Cipher.ENCRYPT_MODE, secretKey)

val cryptoObject = BiometricPrompt.CryptoObject(cipher)
biometricPrompt.authenticate(promptInfo, cryptoObject)

Now, inside onAuthenticationSucceeded, you can use result.cryptoObject?.cipher to actually encrypt or decrypt data. If the biometric check was bypassed, the cipher wouldn't be unlocked, and you wouldn't be able to access the data.

Strong vs. Weak Biometrics

Not all biometrics are created equal. Android classifies them into Class 3 (Strong) and Class 2 (Weak).

When configuring your key, using .setUserAuthenticationParameters(0, KeyProperties.AUTH_BIOMETRIC_STRONG) ensures that only secure hardware can unlock your key.

Conclusion

Biometric authentication is a powerful tool for UX and security, but it must be implemented correctly. By binding your authentication flow to a hardware-backed key in the Keystore, you ensure that your app's security is tied to the physical user, not just a boolean flag in your code.