Android Development
Technology, Information and Media
Article

Biometric authentication in Android II: The cryptographic twist

by
Subir Chakraborty
October 18, 2022
5
min(s)
Remember about the ‘ANARCHIC’ future we discussed recently? You know, where we spoke about developers, app aesthetics, and app security.

Well, this would prevent you from even imagining that alternate universe (or what if its multiverse scenario?) Okay, enough jokes 😛

So, how?

As promised (we didn't solemnly swear, but still 😛), we’re back with the sequel to the biometric authentication in Android.

And this time, cryptography joins the bandwagon of enhancing app security. 

But first, what is cryptography? It is a technique used to secure sensitive data with algorithms that helps ensure the authenticity of data, which cannot be accessed without a secret key. This helps in preventing unauthorized users from reading the encrypted data.

So now, since you know what’s cryptography, let's get our hands dirty with the AndroidX Biometric API and cryptography.

AndroidX Biometric API and Cryptography

As we mentioned in the first article of this series, the AndroidX Biometric API is a comprehensive solution for handling biometric authentication in Android applications. This API authenticates users with the help of biometrics and device credentials.

Another key aspect of this API is that it includes an exciting feature known as CryptoObject, which can be used to incorporate cryptography in the authentication flow. The AndroidX Biometric API supports cryptographic solutions like Signature, Cipher, and Mac. Here, we’ll be focusing on Cipher.

After successful authentication with the biometric prompt, we can further enhance app security with a cryptographic operation. How, you ask?

By using a SecretKey object to perform encryption and decryption while authenticating with the Cipher object. 

So, let’s see how biometric authentication and cryptography work together in an Android framework. 

Cryptography and key management

As mentioned above, a cryptography-based authentication can be carried out using a Cipher object that enables the encryption and decryption of data. To use Cipher, we have to generate a SecretKey. This key can be used by authorized personnel to protect the data. 

In the Android ecosystem, these keys are stored in the KeyStore. This does the job of storing these keys at secure locations. 

In case, an app requests the SecretKey, the KeyStore provides an alias of the key, instead of the actual SecretKey itself. Only the KeyStore is capable of providing an alias of the SecretKey, which helps in boosting reliability and security. 

When the app wants to encrypt data, the KeyStore does that using the alias while providing us with the ciphertext, which is basically the encrypted data. The same also takes place when the app wants to decrypt the encrypted data. 

Here’s how it takes place:


fun generateSecretKey(keyGenParams: KeyGenParameterSpec) {
    val keyGenerator = KeyGenerator.getInstance(
            KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore")
    keyGenerator.init(keyGenParams)
    keyGenerator.generateKey()
}

fun getSecretKey(): SecretKey {
    val keyStore = KeyStore.getInstance("AndroidKeyStore")

    // keyStore must be loaded before accessing it.
    keyStore.load(null)
    return keyStore.getKey(KEY_NAME, null) as SecretKey
}

fun getCipher(): Cipher {
    return Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/"
            + KeyProperties.BLOCK_MODE_CBC + "/"
            + KeyProperties.ENCRYPTION_PADDING_PKCS7)
} 

Authenticating using only biometric credentials

If an app uses a SecretKey that needs biometric credentials to unlock, users have to authenticate their biometric credentials every time before the app accesses the SecretKey.

Here’s how to enable this with a few steps.

Step 1

Generate a key using the KeyGenParameterSpec configuration, we use a builder for that:


fun getKeyGenParams(): KeyGenParameterSpec {
return KeyGenParameterSpec.Builder(
        KEY_NAME,
        KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
        .setBlockModes(KeyProperties.BLOCK_MODE_CBC)
        .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
        .setUserAuthenticationRequired(true)
        // Invalidate the keys if the user has registered a new biometric
        // credential, such as a new fingerprint.
        .setInvalidatedByBiometricEnrollment(true)
        .build()
}

Step 2

Start the biometric authentication workflow using a cipher:


fun authenticateToEncrypt() {
    val cipher = getCipher()
    val secretKey = getSecretKey()
    cipher.init(Cipher.ENCRYPT_MODE, secretKey)
    biometricPrompt.authenticate(promptInfo,
            BiometricPrompt.CryptoObject(cipher))
}

Step 3

 Use the SecretKey to encrypt the sensitive data inside biometric authentication callbacks:


override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
    val cipher = result.cryptoObject?.cipher
    val plainText = "Some data to encrypt"
    val encryptedInfo: ByteArray =cipher?.doFinal(
      plaintext.toByteArray(Charset.defaultCharset())
    )
    Log.d("TAG", "Encrypted information: " +  Arrays.toString(encryptedInfo))
}

Authenticating using biometric or lock screen credentials

If an app uses a SecretKey that needs biometric or lock screen credentials (password, pattern, or PIN) to unlock, then a validity time period must be specified. This validity allows the app to perform multiple cryptographic operations without re-authenticating the user. 

To use this type of key, we have to add a fallback to non-biometric credentials. Hence, we cannot pass an instance of CryptoObject in the authenticate() function of the BiometricPrompt.

Here’s how this type of authentication takes place.

Step 1

Generate a key using the KeyGenParameterSpec configuration:


val keyGenParams = KeyGenParameterSpec.Builder(
            KEY_NAME,
            KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
            .setBlockModes(KeyProperties.BLOCK_MODE_CBC)
            .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
            .setUserAuthenticationRequired(true)
            .setUserAuthenticationParameters(
                 VALIDITY_DURATION_IN_SECONDS, ALLOWED_AUTHENTICATORS
         )
        .build()

Step 2

Encrypt the sensitive information within a time span of VALIDITY_DURATION_SECONDS after the user authenticates successfully:


fun encryptSensitiveData() {
    val cipher = getCipher()
    val secretKey = getSecretKey()
    try {
        cipher.init(Cipher.ENCRYPT_MODE, secretKey)
        val encryptedInfo: ByteArray = cipher.doFinal(
                plaintext-string.toByteArray(Charset.defaultCharset()))
        Log.d("TAG", "Encrypted information: " + Arrays.toString(encryptedInfo))
    } catch (e: InvalidKeyException) {
        Log.e("TAG", "Key is invalid.")
    } catch (e: UserNotAuthenticatedException) {
        Log.d("TAG", "The key's validity timed out.")
        biometricPrompt.authenticate(promptInfo)
    }

To add the aforementioned callback, we can mention that the app supports device credentials by including DEVICE_CREDENTIAL into the set of values passed for setAllowedAuthenticators().

Authenticating using auth-per-use keys

Users also have the option to integrate auth-per-keys within the instance of the BiometricPrompt. Auth-per-keys are secret keys that are used to perform a single cryptographic operation. For example, if you have to carry out 5 cryptographic operations, you’ll need 5 auth-per-keys. 

Due to its immense security benefits, auth-per-keys offer exciting use cases such as making large payments or updating transaction statement records. 

To incorporate an auth-per-key within the BiometricPrompt, we can generate the following KeyGenParameterSpec configuration:


val authPerUseKeyGenParams =
        KeyGenParameterSpec.Builder("myKeystoreAlias", key-purpose)
    // Accept either a device credential or a biometric credential.
    // To accept only one type of credential, include only that type as the second argument.
    .setUserAuthenticationParameters(0 /* duration */,
            KeyProperties.AUTH_BIOMETRIC_STRONG or
            KeyProperties.AUTH_DEVICE_CREDENTIAL)
    .build()

Authenticating without explicit user interaction

In some cases, users might not need to confirm the authentication process. Here, the users can interact with the app more quickly after re-authenticating using face or fingerprint recognition. 

To implement this, pass false into the setConfirmationRequired() function in PromptInfo Builder.


fun getBiometricPromptInfo(
    title: String = "Biometric login",
    subtitle: String = "Log in using your biometric credential",
    negativeButtonText = "Use account password",
isConfirmationRequired = false
): BiometricPrompt.PromptInfo {

  val builder = BiometricPrompt.PromptInfo.Builder()
      .setTitle(title)
      .setSubtitle(subtitle)
      .setNegativeButtonText(negativeButtonText)
      .setConfirmationRequired(isConfirmationRequired)
  return builder.build()
}

And that’s how you can build a passwordless secure solution for authentication using BiometricPrompt and cryptography. 

With passwordless solutions getting all the attention in recent times, stay tuned for our take on this exciting and potentially game-changing trend!

Subir Chakraborty

Subir Chakraborty works at Mutual Mobile as a Senior Engineer I.

More by this author