Search code examples
androidandroid-jetpackandroid-biometricencrypted-shared-preferenceandroid-jetpack-security

EncryptedSharedPreferences isUserAuthenticationRequired not working properly


I am using EncryptedSharedPreferences to store encrypted data.

val biometricManager = BiometricManager.from(this)
val hasFingerprint = biometricManager.canAuthenticate() == BiometricManager.BIOMETRIC_SUCCESS

val advanceSpec = KeyGenParameterSpec.Builder(
    "master_key",
    KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
).apply {
    setBlockModes(KeyProperties.BLOCK_MODE_GCM)
    setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
    setKeySize(256)
    if(hasFingerprint){
        setUserAuthenticationRequired(true)
        setUserAuthenticationValidityDurationSeconds(1)
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N){
            setInvalidatedByBiometricEnrollment(false)
        }
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.P){
            setIsStrongBoxBacked(true)
            setUserConfirmationRequired(true)
        }
    }
}.build()

val masterKey = MasterKeys.getOrCreate(advanceSpec)
val preferences = EncryptedSharedPreferences.create(
    "TestPreferences",
    masterKey,
    applicationContext,
    EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
    EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)

Actually, I was missing the BiometricPrompt part. I thought that calling setUserAuthenticationRequired(true) would automatically handle authenticating user. but we have to show the BiometricPrompt ourselves. isUserAuthenticationRequired only ensures that the key would only be activated when a user is authorized.

val biometricPrompt = BiometricPrompt(
            activity,
            ContextCompat.getMainExecutor(activity),
            object: BiometricPrompt.AuthenticationCallback() {
                override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
                    super.onAuthenticationSucceeded(result)
                    createSharedPreferences()
                }
            }
        )

        biometricPrompt.authenticate(promptInfo)

But, there is a problem. It only throws the UserNotAuthenticatedException while creating the EncryptedSharedPreferences. After that we can perform read and write operations for as long as we need. It isn't taking into consideration the setUserAuthenticationValidityDurationSeconds(1).


Solution

  • I found the reason why it does not require authentication post 1 second. It is because, once the encrypted shared preference is intiated, it loads the keys used to encrypt and decrypt the data in the memory and then those are used to access the data from the files. You can read the code inside EncryptedSharedPreference class. This is pretty evident.

    KeysetHandle daeadKeysetHandle = new AndroidKeysetManager.Builder()
                    .withKeyTemplate(prefKeyEncryptionScheme.getKeyTemplate())
                    .withSharedPref(context, KEY_KEYSET_ALIAS, fileName)
                    .withMasterKeyUri(KEYSTORE_PATH_URI + masterKeyAlias)
                    .build().getKeysetHandle();
            KeysetHandle aeadKeysetHandle = new AndroidKeysetManager.Builder()
                    .withKeyTemplate(prefValueEncryptionScheme.getKeyTemplate())
                    .withSharedPref(context, VALUE_KEYSET_ALIAS, fileName)
                    .withMasterKeyUri(KEYSTORE_PATH_URI + masterKeyAlias)
                    .build().getKeysetHandle();
    
            DeterministicAead daead = daeadKeysetHandle.getPrimitive(DeterministicAead.class);
            Aead aead = aeadKeysetHandle.getPrimitive(Aead.class);
    
            return new EncryptedSharedPreferences(fileName, masterKeyAlias,
                    context.getSharedPreferences(fileName, Context.MODE_PRIVATE), aead, daead);