Back to Notes

Android Security: Biometric Authentication & Keystore Mastery

By Davide Agostini

Android Security: Biometric Authentication & Keystore Mastery

Securing your app goes beyond simple password protection. In this guide, we'll explore how to implement biometric authentication and use Android's Keystore for secure key management.

Why Biometrics Matter

Biometric authentication provides:

  • Convenience: No passwords to remember
  • Security: Hard to forge fingerprints/faces
  • UX: Fast authentication flow

Setting Up Biometric Authentication

First, add the dependency:

// build.gradle.kts
dependencies {
    implementation("androidx.biometric:biometric:1.1.0")
}

❌ BAD: No BiometricPrompt, Direct Hardware Access

// DON'T do this - insecure and deprecated
class InsecureLoginActivity : Activity() {
    fun authenticate() {
        // This approach is outdated and unsafe
        val fingerprintManager = getSystemService(FingerprintManager::class.java)
        // Direct hardware access without proper validation
        fingerprintManager.authenticate(null, null, 0, null, null)
    }
}

✅ GOOD: Using BiometricPrompt Properly

// Recommended approach with BiometricPrompt
class SecureLoginActivity : AppCompatActivity() {
 
    private lateinit var executor: Executor
    private lateinit var biometricPrompt: BiometricPrompt
 
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
 
        executor = ContextCompat.getMainExecutor(this)
 
        // Create BiometricPrompt with callback handling
        biometricPrompt = BiometricPrompt(this, executor,
            object : BiometricPrompt.AuthenticationCallback() {
                override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
                    super.onAuthenticationError(errorCode, errString)
                    // Handle errors gracefully
                    showMessage("Authentication error: $errString")
                }
 
                override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
                    super.onAuthenticationSucceeded(result)
                    // Success! Grant access
                    navigateToHome()
                }
 
                override fun onAuthenticationFailed() {
                    super.onAuthenticationFailed()
                    // Don't reveal whether biometric was valid or not
                    showMessage("Authentication failed")
                }
            })
    }
 
    fun showBiometricPrompt() {
        val promptInfo = BiometricPrompt.PromptInfo.Builder()
            .setTitle("Authenticate")
            .setSubtitle("Use your biometric credential")
            .setNegativeButtonText("Use password")
            .setAllowedAuthenticators(Authenticators.BIOMETRIC_STRONG)
            .build()
 
        biometricPrompt.authenticate(promptInfo)
    }
}

Using Keystore for Secure Key Storage

The Android Keystore system allows you to store cryptographic keys in a hardware-backed secure container.

❌ BAD: Storing Secrets in SharedPreferences

// DON'T store sensitive data like this
class InsecureStorage {
    val prefs = context.getSharedPreferences("secure_data", Context.MODE_PRIVATE)
 
    fun saveApiKey(apiKey: String) {
        // Plain text storage - extremely dangerous!
        prefs.edit().putString("api_key", apiKey).apply()
    }
 
    fun getApiKey(): String? {
        // Anyone with root can read this
        return prefs.getString("api_key", null)
    }
}

✅ GOOD: Using EncryptedSharedPreferences with Keystore

// Recommended: Encrypted storage backed by Keystore
class SecureStorage(private val context: Context) {
 
    private val masterKey = MasterKey.Builder(context)
        .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
        .build()
 
    private val prefs = EncryptedSharedPreferences.create(
        context,
        "secure_prefs",
        masterKey,
        EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
        EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
    )
 
    fun saveApiKey(apiKey: String) {
        // Encrypted automatically using Keystore-backed key
        prefs.edit().putString("api_key", apiKey).apply()
    }
 
    fun getApiKey(): String? {
        return prefs.getString("api_key", null)
    }
 
    // Generate a cryptographic key in Keystore
    fun generateKey(alias: String): SecretKey {
        val keyGenerator = KeyGenerator.getInstance(
            KeyProperties.KEY_ALGORITHM_AES,
            "AndroidKeyStore"
        )
 
        val keyGenSpec = KeyGenParameterSpec.Builder(
            alias,
            KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
        )
            .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
            .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
            .setKeySize(256)
            .setUserAuthenticationRequired(true) // Require biometric auth!
            .build()
 
        keyGenerator.init(keyGenSpec)
        return keyGenerator.generateKey()
    }
}

Using the Keystore Key for Encryption

class EncryptionManager(private val context: Context) {
 
    private val keyStore = KeyStore.getInstance("AndroidKeyStore").apply {
        load(null)
    }
 
    fun encryptData(alias: String, plaintext: ByteArray): ByteArray {
        val key = keyStore.getKey(alias, null) as SecretKey
        val cipher = Cipher.getInstance("AES/GCM/NoPadding")
        cipher.init(Cipher.ENCRYPT_MODE, key)
 
        val iv = cipher.iv
        val ciphertext = cipher.doFinal(plaintext)
 
        // Combine IV and ciphertext
        return iv + ciphertext
    }
 
    fun decryptData(alias: String, encrypted: ByteArray): ByteArray {
        val key = keyStore.getKey(alias, null) as SecretKey
 
        // Extract IV from beginning of data
        val iv = encrypted.sliceArray(0 until 12)
        val ciphertext = encrypted.sliceArray(12 until encrypted.size)
 
        val cipher = Cipher.getInstance("AES/GCM/NoPadding")
        val spec = GCMParameterSpec(128, iv)
        cipher.init(Cipher.DECRYPT_MODE, key, spec)
 
        return cipher.doFinal(ciphertext)
    }
}

Best Practices Summary

| Practice | Recommendation | |----------|----------------| | Biometric UI | Use BiometricPrompt, not deprecated APIs | | Fallback | Always provide password fallback | | Error handling | Don't reveal sensitive info in error messages | | Key storage | Use EncryptedSharedPreferences | | Cryptographic keys | Store in hardware-backed Keystore when possible | | Authentication | Require biometric auth for key usage |

Key Takeaway: Combine biometric authentication with Keystore-backed encryption for maximum security. Use EncryptedSharedPreferences for sensitive data and always require user authentication for key operations. Never store secrets in plain text!