Android Biometric API: Getting Started

Learn how to implement biometric authentication in your Android app by using the Android Biometric API to create an app that securely stores messages. By Zahidur Rahman Faisal.

4.6 (8) · 1 Review

Download materials
Save for later
Share
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

Initializing BiometricPrompt

Next, you’ll initialize the biometric prompt and handle the callbacks with a listener from the calling activity. initBiometricPrompt() does the job. Add the following code to BiometricUtil.kt:

fun initBiometricPrompt(
    activity: AppCompatActivity,
    listener: BiometricAuthListener
): BiometricPrompt {
  // 1
  val executor = ContextCompat.getMainExecutor(activity)

  // 2
  val callback = object : BiometricPrompt.AuthenticationCallback() {
    override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
      super.onAuthenticationError(errorCode, errString)
      listener.onBiometricAuthenticationError(errorCode, errString.toString())
    }

    override fun onAuthenticationFailed() {
      super.onAuthenticationFailed()
      Log.w(this.javaClass.simpleName, "Authentication failed for an unknown reason")
    }

    override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
      super.onAuthenticationSucceeded(result)
      listener.onBiometricAuthenticationSuccess(result)
    }
  }

  // 3
  return BiometricPrompt(activity, executor, callback)
}

Now, you need to add the following imports to the the import section at the top:

import android.util.Log
import androidx.core.content.ContextCompat
import androidx.appcompat.app.AppCompatActivity
import com.raywenderlich.icrypt.common.BiometricAuthListener

The function above does three things:

  1. It creates an executor to handle the callback events.
  2. It creates the callback object to receive authentication events on Success, Failed or Error status with the appropriate result or error messages.
  3. Finally, it constructs a biometric prompt using the activity, executor and callback references. These three parameters are passed on to the UI level to display the prompt and to handle success or failed authentication.

Displaying BiometricPrompt

Now, you need to perform the steps above to display the biometric prompt. Add one more function in BiometricUtil.kt to tie them all together:

fun showBiometricPrompt(
    title: String = "Biometric Authentication",
    subtitle: String = "Enter biometric credentials to proceed.",
    description: String = "Input your Fingerprint or FaceID to ensure it's you!",
    activity: AppCompatActivity,
    listener: BiometricAuthListener,
    cryptoObject: BiometricPrompt.CryptoObject? = null,
    allowDeviceCredential: Boolean = false
) {
  // 1
  val promptInfo = setBiometricPromptInfo(
      title,
      subtitle,
      description,
      allowDeviceCredential
  )

  // 2
  val biometricPrompt = initBiometricPrompt(activity, listener)

  // 3
  biometricPrompt.apply {
    if (cryptoObject == null) authenticate(promptInfo)
    else authenticate(promptInfo, cryptoObject)
  }
}

The first two statements in this function are obvious — they’re just performing setBiometricPromptInfo() and initBiometricPrompt() with the supplied parameters, as mentioned earlier. PromptInfo will use parameter defaults for title, subtitle and description if you don’t pass anything explicitly.

However, the third statement is a bit cryptic. The biometric prompt uses CryptoObject, if available, along with PromptInfo to authenticate.

But what’s CryptoObject?

Before jumping into that, look at BiometricPrompt. Simply replace onClickBiometrics() in LoginActivity.kt with the code below:

fun onClickBiometrics(view: View) {
  BiometricUtil.showBiometricPrompt(
      activity = this,
      listener = this,
      cryptoObject = null,
      allowDeviceCredential = true
  )
}

Here, you call showBiometricPrompt() when the user taps USE BIOMETRICS TO LOGIN.

Now, run the app and use biometrics to log in. You’ll see something like this:

Biometric Authentication Prompt

Note: The standard biometric prompt UI varies depending on your device.

Creating CryptographyUtil

Now that the biometric prompt is ready, your next goal is to leverage it to encrypt and decrypt your secrets. Here’s where CryptoObject comes into play!

Cryptography 101: Cipher, Keystore and SecretKey

CryptoObject is just a cipher, an object that helps with data encryption and decryption. The cipher knows how to use a SecretKey to encrypt your data. Anyone who has the SecretKey can decrypt anything encrypted with the same cipher.

Android keeps SecretKeys in a secure system called the Keystore. The purpose of the Android Keystore is to keep the key material outside of the Android operating system entirely, in a secure location known as the Trusted Execution Environment (TEE) or the Strongbox. The Android Keystore keeps the SecretKey as closely restricted as possible, ensuring that the app, the Android userspace and even the Linux kernel don’t have access to it.

Working With the Keystore

BiometricPrompt doesn’t know how to get a SecretKey, or even where the Keystore is. BiometricPrompt just acts as a gatekeeper to verify your authenticity as the owner of the data. It then asks for help from the cipher to obtain the SecretKey, use it for encryption or decryption, then return the data.

Consider the cipher as a middleman, like The Keymaker from the renowned sci-fi movie, “The Matrix”, who only knows how to open the door to your Keystore.

So, when you’re doing encryption or decryption in the app using BiometricPrompt, what happens end-to-end is:

  • The app asks the user to authenticate themselves through a biometric prompt.
  • Upon successful authentication, the Android Keystore generates a cipher and tags it with a specific SecretKey.
  • The cipher performs encryption on the plaintext and returns a ciphertext and an initialization vector (IV). Together, they’re known as EncryptedMessage, which you’ll see later in this tutorial.
  • You store EncryptedMessage in local storage using a utility class named PreferenceUtil. You’ll decrypt and display it later.
  • During decryption, you authenticate again through the BiometricPrompt. This ensures you’re using the same cipher and SecretKey when you use your fingerprint or face as a signature to decrypt your EncryptedMessage.
  • The cipher then uses the initialization vector to perform decryption on the ciphertext, then returns it as plaintext to display in the app.

The overall process looks like this:

Android Keystore interacting with Android Userspace via CryptoObject

Note: AndroidX Biometric API supports cipher, MAC and signature for cryptographic operations. You’ll use cipher as the standard here.

Now that you know the steps, it’s time to turn them into code!

Generating the SecretKey

Create a new object named CryptographyUtil inside util and define the constants below inside it:

private const val ANDROID_KEYSTORE = "AndroidKeyStore"
private const val YOUR_SECRET_KEY_NAME = "Y0UR$3CR3TK3YN@M3"
private const val KEY_SIZE = 128
private const val ENCRYPTION_BLOCK_MODE = KeyProperties.BLOCK_MODE_GCM
private const val ENCRYPTION_PADDING = KeyProperties.ENCRYPTION_PADDING_NONE
private const val ENCRYPTION_ALGORITHM = KeyProperties.KEY_ALGORITHM_AES

Next, hold your cursor over KeyProperties and press Option-Return on Mac or Control-Alt-O on Windows to import the class. Do the same for the rest of your imports. You’ll need those constants later when you add more functions for encryption/decryption.

Then, generate the SecretKey using the Android Keystore:

fun getOrCreateSecretKey(keyName: String): SecretKey {
  // 1
  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 }

  // 2
  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)
  }

  // 3
  val keyGenParams = paramsBuilder.build()
  val keyGenerator = KeyGenerator.getInstance(
      KeyProperties.KEY_ALGORITHM_AES,
      ANDROID_KEYSTORE
  )
  keyGenerator.init(keyGenParams)

  return keyGenerator.generateKey()
}

The name of the function is self-explanatory. It executes the steps below:

  1. keyName, in this function, is your alias. It looks for keyName in KeyStore and returns the associated SecretKey.
  2. If no SecretKey exists for this keyName, you create paramsBuilder for encryption and decryption, applying the constants you defined earlier, such as ENCRYPTION_BLOCK_MODE and KEY_SIZE. In the same block, setUserAuthenticationRequired(true) ensures that the user is only authorized to use the key if they authenticated themselves using the password/PIN/pattern or biometric.
  3. It prepares keyGenerator using the configuration from paramsBuilder and returns the generated SecretKey.

With the SecretKey generated, you can now encrypt some secrets!