Securing data with BiometricPrompt
Tags: android, kotlin
Published: 2019-05-19 19:27:20

Before Android 9 Pie, your options to implement a fingerprint scanning dialog were:

* Build your own from scratch
* Copy Google's [sample implementation](https://github.com/googlesamples/android-FingerprintDialog)
* Use some unofficial library

Unfortunately neither of those options are great. Unless of course you have really specific UI requirements and **need** to build your own. But for most applications it's enough to have a generic dialog and it'd be great to have an official one.

Luckily, since Android 9 Pie, Google has introduced the BiometricPrompt that addresses this issue. Hopefully this post will help you integrate it.

In all fairness, there are some great existing blog posts on the matter. But kudos aside, the ones I came across lack some things, so I'm hoping this post will fill the gaps.

[Mark Ellison's post](https://blog.stylingandroid.com/biometrics-biometricprompt/) is great but his example only works on Android 9 Pie and doesn't address encrypting data. [Natig Babayev's post](https://medium.com/mindorks/fingerprint-authentication-using-biometricprompt-compat-1466365b4795) also shines, but it doesn't show how it could be encapsulated and also doesn't address encryption.

As always I welcome, and actually seek feedback, so feel free to comment critically.

## The basics

Since most Android device manufacturers either struggle, or deliberately prevent, to upgrade their devices to newer Android versions, we're still forced to support older Android versions.

Luckily Google is doing the heavy lifting with their **AndroidX** libraries (formerly known as Support Library)

So first of all you'll need to add an **additional dependency** to make use of the Biometric Prompt on older Android versions.

```groovy
implementation 'androidx.biometric:biometric:1.0.0-alpha04'
```

### The basic components

The main class involved in the biometric prompt is, what a surprise, called `androidx.biometric.BiometricPrompt`. You can create an instance using its only public constructor:

```java
/**
 * ...
 * 
 * @param fragmentActivity A reference to the client's activity.
 * @param executor An executor to handle callback events.
 * @param callback An object to receive authentication events.
 */
public BiometricPrompt(@NonNull FragmentActivity fragmentActivity,
                       @NonNull Executor executor, 
                       @NonNull AuthenticationCallback callback)
```

The third parameter: `AuthenticationCallback` is an abstract static class with three non-abstract methods, so technically we won't have to override any of them.

So for the sake of a simple "hello-world" prompt we can pass an anonymous object without overriding anything.

```kotlin
BiometricPrompt(fragmentActivity, executor, 
    object : BiometricPrompt.AuthenticationCallback() {})
```

Secondly we need to describe what the prompt looks like. That's where the `BiometricPrompt.PromptInfo` comes into play. It conveniently provides a builder class:

```kotlin
val cancelString = activity.getString(android.R.string.cancel)
BiometricPrompt.PromptInfo.Builder()
    .setTitle("Prompt Title") // required
    .setSubtitle("Prompt Subtitle")
    .setDescription("Prompt Description: lorem ipsum")
    .setNegativeButtonText(cancelString) // required
    .build()
```

That's all that can be configured. It should be quite self-explanatory. Only the *Title* and *NegativeButtonText* is required.

To launch the prompt, all we have to do is call the authenticate method.

```kotlin
val biometricPrompt = BiometricPrompt(...)
val promptInfo = BiometricPrompt.PromptInfo.Builder()...

biometricPrompt.authenticate(promptInfo)
```

<figure>
  <img src="https://thepracticaldev.s3.amazonaws.com/i/lz4dk5997d7fr2s22nql.jpg">
  <figcaption>left: BiometricPrompt on Android 7 Nougat / right: BiometricPrompt on Android 9 Pie</figcaption>
</figure>

***

## Encryption

In my opinion encrypting data is at the heart of using biometric authentication in the first place. So now that we have a working prompt we should look into it.

**⚠️ *Important Disclaimer* ⚠️ I'm not an in depth security expert! The code and specifically the chosen encryption parameters, are based on Google's previously mentioned [sample](https://github.com/googlesamples/android-FingerprintDialog) and merely adapted to the new BiometricPrompt.**

### Key & Cipher

To encrypt data we need a **SecretKey** and a **Cipher**.

```kotlin
fun createKey(): SecretKey {
  val algorithm = KeyProperties.KEY_ALGORITHM_AES
  val provider = "AndroidKeyStore"
  val keyGenerator = KeyGenerator.getInstance(algorithm, provider)
  
  val keyName = "MY_KEY"
  val purposes = KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
  val keyGenParameterSpec = KeyGenParameterSpec.Builder(keyName, purposes)
          .setBlockModes(BLOCK_MODE)
          .setEncryptionPaddings(PADDING)
          .setUserAuthenticationRequired(true)
          .build()

  keyGenerator.init(keyGenParameterSpec)
  return keyGenerator.generateKey()
}

fun getEncryptCipher(key: Key): Cipher {
  val algorithm = KeyProperties.KEY_ALGORITHM_AES
  val blockMode = KeyProperties.BLOCK_MODE_CBC
  val padding = KeyProperties.ENCRYPTION_PADDING_PKCS7
  return Cipher.getInstance("$algorithm/$blockMode/$padding").apply { 
    init(Cipher.ENCRYPT_MODE, key) 
  }
}
```

The main take away here is the `setUserAuthenticationRequired(true)` method call during key creation that:

> Sets whether this key is authorized to be used only if the user has been authenticated.

> By default, the key is authorized to be used regardless of whether the user has been authenticated.

This basically means: to use the key you have to provide a form of authentication, i.e. your fingerprint. For further explanation refer to the [Android documentation](https://developer.android.com/reference/android/security/keystore/KeyGenParameterSpec.Builder#setUserAuthenticationRequired(boolean)) 🔍

Since no key length is specified, Android uses a default key length: for AES this is set to 128 bits. For more information on cryptography on Android refer to the [documentation](https://developer.android.com/guide/topics/security/cryptography) 🔍

### Using the cipher to encrypt

To encrypt data using the cipher just call its `doFinal(byte[] input)` method. For decryption later on, we need to keep hold of the cipher's initialization vector.

```kotlin
val clearTextData: ByteArray
val encryptedData: ByteArray = cipher.doFinal(clearTextData)

// save encrypted data & init vector: i.e. in shared preferences
val dat = Base64.encodeToString(encryptedData, Base64.DEFAULT)
val iv  = Base64.encodeToString(cipher.iv, Base64.DEFAULT)
sharedPreferences.edit {
    putString(ENCRYPTED_DATA, dat)
    putString(INITIALIZATION_VECTOR, iv)
}
```

## Decryption

To decrypt the encrypted data you'll need

* previously created SecretKey
* previously used initialization vector
* decryption cipher

```kotlin
fun getKey(): Key? {
  val keyStore: KeyStore = KeyStore.getInstance("AndroidKeyStore").apply { 
    load(null) 
  }
  return keyStore.getKey("MY_KEY", null)
}

fun getDecryptCipher(key: Key, iv: ByteArray): Cipher {
  val algorithm = KeyProperties.KEY_ALGORITHM_AES
  val blockMode = KeyProperties.BLOCK_MODE_CBC
  val padding = KeyProperties.ENCRYPTION_PADDING_PKCS7
  return Cipher.getInstance("$algorithm/$blockMode/$padding").apply { 
    init(Cipher.DECRYPT_MODE, key, IvParameterSpec(iv)) 
  }
}
```

### Using the cipher to decrypt

This is quite straight forward and resembles the encryption call

```kotlin
val encryptedData: ByteArray = getEncryptedData()
val clearTextData: ByteArray = cipher.doFinal(encryptedData)
```

***

## "Tink" different

Like I mentioned earlier I'm not an in depth security expert. For people like me (and others) Google created [Tink](https://github.com/google/tink) with the goal of making encryption simpler and harder to misuse.

With Tink's Android flavor `com.google.crypto.tink:tink-android:1.2.2` the whole encryption/decryption breaks down to this simpler and easier to understand code:

```kotlin
Config.register(TinkConfig.LATEST)

val keysetHandle = AndroidKeysetManager.Builder()
        .withSharedPref(activity, TINK_KEYSET_NAME, null)
        .withKeyTemplate(AeadKeyTemplates.AES256_GCM)
        .withMasterKeyUri(MASTER_KEY_URI)
        .build().keysetHandle

val aead = AeadFactory.getPrimitive(keysetHandle)
val encrypted = aead.encrypt("SECRET!".toByteArray(), null)
val cleartext = aead.decrypt(encrypted, null)

```

A lot of internals we've seen before are now hidden: Cipher, initialization vector, encryption parameters etc.

Unforunately the previously discussed flag `setUserAuthenticationRequired ` is not set, and it's currently not possible to pass this AEAD (Authenticated Encryption with Associated Data) to the BiometricPrompt for "authentication".

I created an [issue on Github](https://github.com/google/tink/issues/211) for this. When there's news on the matter I'll write a follow-up post. 

***

## Tying things together

Now that we have a working prompt and understand how to encrypt data, we can tie the two together. Just pass the cipher required to en-/decrypt to the BiometricPrompt:

```kotlin
val promptCrypto = BiometricPrompt.CryptoObject(cipher)
biometricPrompt.authenticate(promptInfo, promptCrypto)
```

The CryptoObject you pass in, will be "unlocked" (remember the flag we set when creating the key) and passed on to the `onAuthenticationSucceeded` callback method of the previously seen `AuthenticationCallback`

```kotlin
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
    super.onAuthenticationSucceeded(result)
    result.cryptoObject?.cipher?.let { cipher ->
        // encrypt or decrypt data with cipher
    }
}
```

### Handling errors

Besides the succeeded callback method, there are two other callback methods for failed attempts:

`onAuthenticationFailed` *"Called when a biometric is valid but not recognized"*. Probably not something you need to address because there's already a default animation implemented in the prompt.

`onAuthenticationError` *"Called when an unrecoverable error has been encountered and the operation is complete. No further actions will be made on this object"*

The error callback method provides `int errCode, CharSequence errString` that can be used to handle different errors and display feedback. For instance

```kotlin
override fun onAuthenticationError(errorCode: Int, 
                                   errorString: CharSequence) {
    super.onAuthenticationError(errorCode, errString)
    when (errorCode) {
        ...
        BiometricPrompt.ERROR_TIMEOUT -> // user fell asleep ;-)
        ...
    }
}
```

Refer to the [Android documentation to see all available error codes](https://developer.android.com/reference/android/hardware/biometrics/BiometricPrompt.html#BIOMETRIC_ERROR_CANCELED).

### Build for convenience

No even though there are lots of different errors that can be handled using the biometric prompt, I'd argue that in most cases you only want to deal with two cases. Either encryption/decryption works 👍 or it doesn't 👎

So why not implement a supporting class that provides convenience for the two cases and encapsulates everything we previously discussed. API Suggestion:

```kotlin
fun encryptPrompt(data: ByteArray,
                  failedAction: () -> Unit,
                  successAction: () -> Unit)

fun decryptPrompt(failedAction: () -> Unit,
                  successAction: (ByteArray) -> Unit)
```

So in the `succeeded` and `error` callbacks of `AuthenticationCallback` we can do the following:

#### Encrypt case

```kotlin
fun onAuthenticationSucceeded(result: AuthenticationResult) {
    super.onAuthenticationSucceeded(result)
    result.cryptoObject?.cipher?.let { resultCipher ->
        val iv = resultCipher.iv
        val encryptedData = resultCipher.doFinal(data)
        saveEncryptedData(encryptedData, iv)
        activity.runOnUiThread { successAction() }
    }
}

fun onAuthenticationError(errCode: Int, errStr: CharSequence) {
    super.onAuthenticationError(errCode, errStr)
    Log.d(TAG, "Authentication error. $errStr ($errCode)")
    activity.runOnUiThread { failedAction() }
}
```

#### Decrypt case

```kotlin
fun onAuthenticationSucceeded(result: AuthenticationResult) {
    super.onAuthenticationSucceeded(result)
    result.cryptoObject?.cipher?.let { cipher ->
        val encrypted = getEncryptedData()
        val decryptedData = cipher.doFinal(encrypted)
        activity.runOnUiThread { successAction(decryptedData) }
    }
}

fun onAuthenticationError(errCode: Int, errStr: CharSequence) {
    super.onAuthenticationError(errCode, errStr)
    Log.d(TAG, "Authentication error. $errStr ($errCode)")
    activity.runOnUiThread { failedAction() }
}
```

### Usage

This should enable you to conveniently use the biometric prompt:

```kotlin
val promptManager = BiometricPromptManager(fragmentActivity)
promptManager.encryptPrompt(
    data = secureText.toByteArray(),
    failedAction = { showToast("encrypt failed") },
    successAction = { showToast("encrypt success") }
)

promptManager.decryptPrompt(
    failedAction = { showToast("decrypt failed") },
    successAction = { showToast("decrypt success: $it") }
)
```

### Alternatives

Of course if you require a more detailed error handling or don't like this style you could also

* use custom response codes and/or do everything in one callback method
* add different callback actions (for different errors)
* pass error code to failedAction

***

## Issues

Unfortunately, I came across the following issue when using the AndroidX biometric prompt in a Fragment on Android 8 and earlier.

```
java.lang.IllegalStateException: FragmentManager is already executing transactions
```

According to others on Stackoverflow the issue seems to have been introduced with the newest alpha04 version

* <https://stackoverflow.com/questions/55683300/android-crashed-after-updating-androidx-biometric-to-1-0-0-alpha04>
* <https://stackoverflow.com/questions/55934108/fragmentmanager-is-already-executing-transactions-when-executing-biometricprompt>

To "resolve" the issue: you can artificially delay the call with a Handler , or revert to version alpha03.

```kotlin
Handler().postDelayed({
        // launch biometric prompt for Fragment 
}, 0)
```

***

The complete code can be found on Github:

{% github stravag/android-sample-biometric-prompt %}

Happy coding!

***

![](https://thepracticaldev.s3.amazonaws.com/i/iu22ph3ksl2bsooujzi7.gif)