I am running into an issue where I want to invalidate a SecretKey when a user adds a new fingerprint to their Android device. I can generate the key, pull up the biometric prompt with no issues. When I add a new fingerprint, the key is not throwing an exception.. it still allows the prompt to appear.
I can remove all fingerprints from the device, and the KeyPermanentlyInvalidatedException is thrown.
I am using the BiometricPrompt AndroidX library with my minSDK set to 28. I have been following this guide: https://developer.android.com/training/sign-in/biometric-auth#crypto
My code is below - I would be very appreciative if someone would be willing to help me with this issue.
package com.example.dogtime
import android.os.Bundle
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyPermanentlyInvalidatedException
import android.security.keystore.KeyProperties
import android.widget.Button
import androidx.appcompat.app.AppCompatActivity
import androidx.biometric.BiometricPrompt
import androidx.core.content.ContextCompat
import butterknife.ButterKnife
import java.security.KeyStore
import javax.crypto.Cipher
import javax.crypto.KeyGenerator
import javax.crypto.SecretKey
class MainActivity : AppCompatActivity() {
private lateinit var biometricPrompt: BiometricPrompt
private lateinit var biometricPromptInfo: BiometricPrompt.PromptInfo
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ButterKnife.bind(this)
setContentView(R.layout.activity_main)
// Set up prompt
biometricPrompt = BiometricPrompt(
this,
ContextCompat.getMainExecutor(baseContext),
object : BiometricPrompt.AuthenticationCallback() {
// no-op
}
)
// Prompt info
biometricPromptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle("Title")
.setNegativeButtonText("Test")
.build()
// Generates key
findViewById<Button>(R.id.count_btn).setOnClickListener {
generateKey()
}
// Shows biometric prompt
findViewById<Button>(R.id.random_btn).setOnClickListener {
getCrypto()?.let { it1 -> biometricPrompt.authenticate(biometricPromptInfo, it1) }
?: run {
// Key is invalidated - this is never triggered
}
}
}
private fun generateKey() {
val keyGenerator = KeyGenerator
.getInstance(KeyProperties
.KEY_ALGORITHM_AES, "AndroidKeyStore")
keyGenerator.init(getKeyGen())
keyGenerator.generateKey()
}
private fun getCipher() : Cipher {
return Cipher.getInstance(
KeyProperties.KEY_ALGORITHM_AES + "/"
+ KeyProperties.BLOCK_MODE_CBC + "/"
+ KeyProperties.ENCRYPTION_PADDING_PKCS7
)
}
private fun getKey() : SecretKey {
val keyStore = KeyStore.getInstance("AndroidKeyStore")
keyStore.load(null)
return keyStore.getKey("key6", null) as SecretKey
}
private fun getKeyGen() : KeyGenParameterSpec {
return KeyGenParameterSpec.Builder(
"key6",
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
).setBlockModes(KeyProperties.BLOCK_MODE_CBC)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
.setUserAuthenticationRequired(true)
.setInvalidatedByBiometricEnrollment(true)
.build()
}
private fun getCrypto() : BiometricPrompt.CryptoObject? {
return try {
val cipher = getCipher()
val key = getKey()
cipher.init(Cipher.ENCRYPT_MODE, key)
BiometricPrompt.CryptoObject(cipher)
} catch(e: KeyPermanentlyInvalidatedException) {
val keyStore = KeyStore.getInstance("AndroidKeyStore")
keyStore.load(null)
keyStore.deleteEntry("key6")
null
}
}
}
(Answering my own question). I have gotten this to work with the above solution on the following device specs.
Previously, I was testing this with an emulator, and the keys would not be invalidated. I am not sure why, but emulator testing could not efficiently test this scenario.