Im trying to implement biometric authorization in Android app. I followed android official documentation and it was worked everything fine until today, so when I removed finger print and add new one, now its throwing exception, I tried to put try catch in getOrCreateSecretKey but it was same :(
android.security.keystore.KeyPermanentlyInvalidatedException: Key permanently invalidated
private class CryptographyManagerImpl : CryptographyManager {
private val KEY_SIZE = 256
private val ANDROID_KEYSTORE = "AndroidKeyStore"
private val ENCRYPTION_BLOCK_MODE = KeyProperties.BLOCK_MODE_GCM
private val ENCRYPTION_PADDING = KeyProperties.ENCRYPTION_PADDING_NONE
private val ENCRYPTION_ALGORITHM = KeyProperties.KEY_ALGORITHM_AES
override fun getInitializedCipherForEncryption(keyName: String): Cipher {
val cipher = getCipher()
val secretKey = getOrCreateSecretKey(keyName)
cipher.init(Cipher.ENCRYPT_MODE, secretKey)
return cipher
}
override fun getInitializedCipherForDecryption(
keyName: String,
initializationVector: ByteArray
): Cipher {
val cipher = getCipher()
val secretKey = getOrCreateSecretKey(keyName)
cipher.init(Cipher.DECRYPT_MODE, secretKey, GCMParameterSpec(128, initializationVector))
return cipher
}
override fun encryptData(plaintext: String, cipher: Cipher): CiphertextWrapper {
val ciphertext = cipher.doFinal(plaintext.toByteArray(Charset.forName("UTF-8")))
return CiphertextWrapper(ciphertext, cipher.iv)
}
override fun decryptData(ciphertext: ByteArray, cipher: Cipher): String {
val plaintext = cipher.doFinal(ciphertext)
return String(plaintext, Charset.forName("UTF-8"))
}
private fun getCipher(): Cipher {
val transformation = "$ENCRYPTION_ALGORITHM/$ENCRYPTION_BLOCK_MODE/$ENCRYPTION_PADDING"
return Cipher.getInstance(transformation)
}
private fun getOrCreateSecretKey(keyName: String): SecretKey {
// If Secretkey was previously created for that keyName, then grab and return it.
val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE)
keyStore.load(null) // Keystore must be loaded before it can be accessed
keyStore.getKey(keyName, null)?.let { return it as SecretKey }
// if you reach here, then a new SecretKey must be generated for that keyName
val paramsBuilder = KeyGenParameterSpec.Builder(
keyName,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
)
paramsBuilder.apply {
setBlockModes(ENCRYPTION_BLOCK_MODE)
setEncryptionPaddings(ENCRYPTION_PADDING)
setKeySize(KEY_SIZE)
setUserAuthenticationRequired(true)
}
val keyGenParams = paramsBuilder.build()
val keyGenerator = KeyGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_AES,
ANDROID_KEYSTORE
)
keyGenerator.init(keyGenParams)
return keyGenerator.generateKey()
}
Exception
2021-10-19 17:28:05.252 6613-6613/com.samplebet E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.samplebet, PID: 6613
java.lang.RuntimeException: java.lang.reflect.InvocationTargetException
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:612)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1130)
Caused by: java.lang.reflect.InvocationTargetException
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:602)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1130)
Caused by: android.security.keystore.KeyPermanentlyInvalidatedException: Key permanently invalidated
at android.security.KeyStore.getInvalidKeyException(KeyStore.java:1533)
at android.security.KeyStore.getInvalidKeyException(KeyStore.java:1548)
at android.security.keystore.KeyStoreCryptoOperationUtils.getInvalidKeyExceptionForInit(KeyStoreCryptoOperationUtils.java:54)
at android.security.keystore.KeyStoreCryptoOperationUtils.getExceptionForCipherInit(KeyStoreCryptoOperationUtils.java:89)
at android.security.keystore.AndroidKeyStoreCipherSpiBase.ensureKeystoreOperationInitialized(AndroidKeyStoreCipherSpiBase.java:265)
at android.security.keystore.AndroidKeyStoreCipherSpiBase.engineInit(AndroidKeyStoreCipherSpiBase.java:148)
at javax.crypto.Cipher.tryTransformWithProvider(Cipher.java:2980)
at javax.crypto.Cipher.tryCombinations(Cipher.java:2891)
at javax.crypto.Cipher$SpiAndProviderUpdater.updateAndGetSpiAndProvider(Cipher.java:2796)
at javax.crypto.Cipher.chooseProvider(Cipher.java:773)
at javax.crypto.Cipher.init(Cipher.java:1288)
at javax.crypto.Cipher.init(Cipher.java:1223)
at com.samplebet.biometric.CryptographyManagerImpl.getInitializedCipherForDecryption(CryptographyManager.kt:98)
at com.samplebet.auth.AuthActivity.showBiometricPromptForDecryption(AuthActivity.kt:120)
at com.samplebet.auth.AuthActivity.onCreate$lambda-5(AuthActivity.kt:87)
at com.samplebet.auth.AuthActivity.$r8$lambda$xxAGKHOwnj6tq1hGqTWaney7IFw(Unknown Source:0)
at com.samplebet.auth.AuthActivity$$ExternalSyntheticLambda0.onClick(Unknown Source:2)
at android.view.View.performClick(View.java:8160)
at android.widget.TextView.performClick(TextView.java:16222)
at com.google.android.material.button.MaterialButton.performClick(MaterialButton.java:1119)
at android.view.View.performClickInternal(View.java:8137)
at android.view.View.access$3700(View.java:888)
at android.view.View$PerformClick.run(View.java:30236)
at android.os.Handler.handleCallback(Handler.java:938)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:246)
This happens because in your key generator parameters you include the setUserAuthenticationRequired(true) which invalidates the key when all biometrics are removed or a new one is added on the device.
This means that when you try to decrypt using such a key after you have removed a fingerprint and add another you will get the KeyPermanentlyInvalidatedException thrown when you call cipher.init.
Catching the exception, deleting the invalid key and generating a new one is part of the solution because with the new key you will not be able to decrypt your encrypted(with the previous key) data anymore. For this you will have to have a fallback flow where you ask the user for the data to be encrypted again with the new key.
As final notes:
You could use the setInvalidatedByBiometricEnrollment method (available from API 24) when generating new keys and set it to false which will narrow down a little bit the cases where the key will be invalidated but is probably considered a little bit less secure.
Setting the setUserAuthenticationRequired to false when generating the key (which would also resolve your issue) is not recommended.