Search code examples
androidencryptionfingerprint

KeyPermanentlyInvalidatedException not thrown when a new fingerprint is added


I have CipherUtils class which is responsible for creating the cipher which is taken from the google dialog sample


import android.annotation.TargetApi
import android.app.Application
import android.app.KeyguardManager
import android.content.SharedPreferences
import android.hardware.fingerprint.FingerprintManager
import android.os.Build
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyPermanentlyInvalidatedException
import android.security.keystore.KeyProperties
import androidx.core.content.ContextCompat.getSystemService
import androidx.core.content.edit
import java.io.IOException
import java.security.*
import java.security.cert.CertificateException
import javax.crypto.Cipher
import javax.crypto.KeyGenerator
import javax.crypto.NoSuchPaddingException
import javax.crypto.SecretKey
import javax.inject.Inject


class CipherUtils @Inject constructor(
    val application: Application,
    val sharedPreferences: SharedPreferences
) {
    internal val DEFAULT_KEY_NAME = "ubit_key"
    private val KEY = "fingerInvalid"

    private var mKeyStore: KeyStore? = null
    private var mKeyGenerator: KeyGenerator? = null
    private var defaultCipher: Cipher

    init {

        try {
            mKeyStore = KeyStore.getInstance("AndroidKeyStore")
        } catch (e: KeyStoreException) {
            throw RuntimeException("Failed to get an instance of KeyStore", e)
        }

        try {
            mKeyGenerator = KeyGenerator
                .getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore")
        } catch (e: NoSuchAlgorithmException) {
            throw RuntimeException("Failed to get an instance of KeyGenerator", e)
        } catch (e: NoSuchProviderException) {
            throw RuntimeException("Failed to get an instance of KeyGenerator", e)
        }

        try {
            defaultCipher = Cipher.getInstance(
                KeyProperties.KEY_ALGORITHM_AES + "/"
                        + KeyProperties.BLOCK_MODE_CBC + "/"
                        + KeyProperties.ENCRYPTION_PADDING_PKCS7
            )
        } catch (e: NoSuchAlgorithmException) {
            throw RuntimeException("Failed to get an instance of Cipher", e)
        } catch (e: NoSuchPaddingException) {
            throw RuntimeException("Failed to get an instance of Cipher", e)
        }

        init()
    }

    @TargetApi(Build.VERSION_CODES.M)
    private fun init() {
        val keyguardManager = getSystemService(application,KeyguardManager::class.java)
        val fingerprintManager = getSystemService(application,FingerprintManager::class.java)

        if (!keyguardManager!!.isKeyguardSecure()) {
            // Show a message that the user hasn't set up a fingerprint or lock screen.
            return
        }

        // Now the protection level of USE_FINGERPRINT permission is normal instead of dangerous.
        // See http://developer.android.com/reference/android/Manifest.permission.html#USE_FINGERPRINT
        // The line below prevents the false positive inspection from Android Studio
        // noinspection ResourceType
        if (!fingerprintManager!!.hasEnrolledFingerprints()) {
            // This happens when no fingerprints are registered.

            return
        }
        createKey(DEFAULT_KEY_NAME, true)
    }

    /**
     * Creates a symmetric key in the Android Key Store which can only be used after the user has
     * authenticated with fingerprint.
     *
     * @param keyName the name of the key to be created
     * @param invalidatedByBiometricEnrollment if `false` is passed, the created key will not
     * be invalidated even if a new fingerprint is enrolled.
     * The default value is `true`, so passing
     * `true` doesn't change the behavior
     * (the key will be invalidated if a new fingerprint is
     * enrolled.). Note that this parameter is only valid if
     * the app works on Android N developer preview.
     */
    @TargetApi(Build.VERSION_CODES.M)
    fun createKey(keyName: String, invalidatedByBiometricEnrollment: Boolean) {
        // The enrolling flow for fingerprint. This is where you ask the user to set up fingerprint
        // for your flow. Use of keys is necessary if you need to know if the set of
        // enrolled fingerprints has changed.
        try {
            mKeyStore?.load(null)
            // Set the alias of the entry in Android KeyStore where the key will appear
            // and the constrains (purposes) in the constructor of the Builder

            val builder = KeyGenParameterSpec.Builder(
                keyName,
                KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
            )
                .setBlockModes(KeyProperties.BLOCK_MODE_CBC)
                // Require the user to authenticate with a fingerprint to authorize every use
                // of the key
                .setUserAuthenticationRequired(true)
                .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)

            // This is a workaround to avoid crashes on devices whose API level is < 24
            // because KeyGenParameterSpec.Builder#setInvalidatedByBiometricEnrollment is only
            // visible on API level +24.
            // Ideally there should be a compat library for KeyGenParameterSpec.Builder but
            // which isn't available yet.
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                builder.setInvalidatedByBiometricEnrollment(invalidatedByBiometricEnrollment)
            }
            mKeyGenerator?.init(builder.build())
            mKeyGenerator?.generateKey()
        } catch (e: NoSuchAlgorithmException) {
            throw RuntimeException(e)
        } catch (e: InvalidAlgorithmParameterException) {
            throw RuntimeException(e)
        } catch (e: CertificateException) {
            throw RuntimeException(e)
        } catch (e: IOException) {
            throw RuntimeException(e)
        }

    }

    /**
     * Initialize the [Cipher] instance with the created key in the
     * [.createKey] method.
     *
     * @param keyName the key name to init the cipher
     * @return `true` if initialization is successful, `false` if the lock screen has
     * been disabled or reset after the key was generated, or if a fingerprint got enrolled after
     * the key was generated.
     */
    @TargetApi(Build.VERSION_CODES.M)
    private fun initCipher(cipher: Cipher, keyName: String): Boolean {
        try {
            mKeyStore?.load(null)
            val key = mKeyStore?.getKey(keyName, null) as SecretKey
            cipher.init(Cipher.ENCRYPT_MODE, key)
            return true
        } catch (e: KeyPermanentlyInvalidatedException) {
            return false
        } catch (e: KeyStoreException) {
            throw RuntimeException("Failed to init Cipher", e)
        } catch (e: CertificateException) {
            throw RuntimeException("Failed to init Cipher", e)
        } catch (e: UnrecoverableKeyException) {
            throw RuntimeException("Failed to init Cipher", e)
        } catch (e: IOException) {
            throw RuntimeException("Failed to init Cipher", e)
        } catch (e: NoSuchAlgorithmException) {
            throw RuntimeException("Failed to init Cipher", e)
        } catch (e: InvalidKeyException) {
            throw RuntimeException("Failed to init Cipher", e)
        }

    }

}

When a new fingerprint is enrolled, with running initCipher, I expect to get KeyPermanentlyInvalidatedException but it returns true. what am I missing and how to fix this to get to know when a new fingerprint is added?


Solution

  • I searched a lot and found The answer in a comment in issues

    This sample creates a new key in onCreate, so if you launch the sample app after you add a new fingerprint, KeyPermanentlyInvalidatedException is not thrown because at the time the key is created, a set of fingerprints include the new created one.

    If you want to test that KeyPermanentlyInvalidatedException, please try to add a new fingerprint while keeping the app open.

    But I want this feature to work when app closes. So added a few lines of code before creating a new key, I check if there is a key before and don't create it if it exists.

    /**
     * returns current saved key
     */
    private fun getCurrentKey(keyName: String): Key? {
        keyStore?.load(null)
        return keyStore?.getKey(keyName, null)
    }
    

    and replace this with the create key part of the code

    /**
     * Only if the key is not created, Create a new key
     */
    private fun createKeyIfNotExists() {
        if (getCurrentKey(DEFAULT_KEY_NAME) == null) {
            createKey(DEFAULT_KEY_NAME, true)
        }
    }