UIKit Apprentice, Second Edition – Now Updated!

Learn iOS and Swift from scratch. Build four powerful apps—with support for iPad and Dark Mode. Publish apps to the App Store.

Home Android & Kotlin Tutorials

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.

4.6/5 5 Ratings

Version

  • Kotlin 1.4, Android 10.0, Android Studio 4.1

What’s the most commonly used feature of a smartphone, apart from making phone calls or texting? Is it taking photos? File sharing? Listening to music or watching videos? Actually, it’s a feature many people use countless times a day without even thinking about it: biometric authentication!

Biometric authentication allows you to quickly unlock your device with your fingerprint or face, confirming it’s really you who’s using the device. In this tutorial, you’ll learn how to use it to create an app named iCrypt using the Android Biometric API.

This app will securely store your messages, so only you can unlock them using your biometric signature. You’ll learn how to:

  1. Integrate the AndroidX Biometric Library.
  2. Authenticate a user through a biometric prompt.
  3. Encrypt and decrypt sensitive information using biometric credentials.
  4. Properly handle successful or failed authentication.
Note: This tutorial requires a device that has biometric authentication capability, either fingerprint or Android face recognition. The examples and sample code here will focus on the fingerprint authentication process.

Getting Started

Download the materials using the Download Materials button at the top or the bottom of this tutorial. Extract and open the starter project in Android Studio 4.0 or later.

Before you build and run, you need to include a dependency for the AndroidX Biometric Library, which is a one-stop user authentication solution for Android developers.

Open the app-level build.gradle file and add the following line to the end of dependencies {...}:

def biometricLibraryVersion = "1.0.1"
implementation "androidx.biometric:biometric:$biometricLibraryVersion"

In this code, you specify the version of the library to use and include it as a dependency for compilation. Click Sync Now at the top-right corner of the Android Studio to sync your project, and you’re all set! Build and run and you’ll see the login screen:

iCrypt Login Screen

Before proceeding, take a closer look at the library you just added to your code.

Introducing The Android Biometric API

AndroidX Biometric Library allows developers to:

  • Check if the device supports biometric authentication.
  • Display a standardized biometric prompt for fingerprint or facial recognition.
  • Detect successful or failed authentication attempts with simple callbacks.
  • Provide the option to use a device’s PIN/pattern/password instead of biometric credentials.

Here’s how it works under the hood with different Android versions and vendors:

Android Biometric Architecture chart

Note: If you’re interested in learning more about Android Biometric Architecture, check out the official documentation.

You’ll start by checking if a device can use biometric authentication.

Checking Device Capabilities

Since Android Biometric APIs are relatively new, not all devices have biometric capability. So your first step is to check if your user’s device is capable of biometric authentication. This only takes a few simple steps.

Create a new object file named BiometricUtil inside the util package. In Android Studio, select the util package, then click File ▸ New ▸ Kotlin File/Class. Next, select Object and enter the name. This will be your helper class for managing the biometric authentication process.

Now, add this function to check the user’s hardware capability:

fun hasBiometricCapability(context: Context): Int {
  val biometricManager = BiometricManager.from(context)
  return biometricManager.canAuthenticate()
}

Next, add the follow imports at the top of the file:

import android.content.Context
import androidx.biometric.BiometricManager

The code above creates a BiometricManager from the app context and calls canAuthenticate() to check whether the hardware is capable of biometric authentication.

However, this doesn’t guarantee that the user is ready to use biometric authentication. They may have the necessary hardware for facial recognition or reading fingerprints on the device, but you can only call a BiometricPrompt if you’ve registered your fingerprint or face in the device’s Security Settings.

When you run canAuthenticate(), it returns one of three different results:

  1. BIOMETRIC_SUCCESS: The device is ready to use a biometric prompt, meaning the hardware is available and the user has enrolled their biometric data.
  2. BIOMETRIC_ERROR_NONE_ENROLLED: The device supports biometrics, but the user hasn’t enrolled either their fingerprints or their face.
  3. BIOMETRIC_ERROR_NO_HARDWARE: The device hardware has no biometric authentication capability.

To ensure the device is ready to use a biometric prompt, add the following function:

fun isBiometricReady(context: Context) =
      hasBiometricCapability(context) == BiometricManager.BIOMETRIC_SUCCESS

This returns true only if the device has biometric hardware capability and the user has enrolled their biometrics.

Implementing the Biometric Login

You want to show an option for the user to log in with biometrics when they are ready, so open LoginActivity inside the ui package and replace showBiometricLoginOption() with the code below:

fun showBiometricLoginOption() {
  buttonBiometricsLogin.visibility =
      if (BiometricUtil.isBiometricReady(this)) View.VISIBLE
      else View.GONE
}

This adds the following in the imports section on top:

import com.raywenderlich.icrypt.util.BiometricUtil
import kotlinx.android.synthetic.main.activity_login.*

The function above is self-explanatory: It shows the button to log in with biometrics if the device supports biometric login and the user has set their fingerprint or facial ID. Otherwise, it’ll hide the button and the user can’t access the biometric login feature.

Run the app again. If you’ve enrolled your biometrics, you’ll see a new button, USE BIOMETRICS TO LOGIN, appear below the original login button.

Login Screen with Biometric Login button

Building BiometricPrompt

If you’re eager to use biometrics to log in and eliminate the need to type your password, there’s good news. You’re only two steps away from displaying a biometric prompt to ease your login process.

Here’s what you need to do first:

  1. Set PromptInfo with your desired message and configuration.
  2. Initialize the biometric prompt with the calling activity and callback handlers.

Preparing PromptInfo

Open BiometricUtil.kt again and append this function:

fun setBiometricPromptInfo(
    title: String,
    subtitle: String,
    description: String,
    allowDeviceCredential: Boolean
): BiometricPrompt.PromptInfo {
  val builder = BiometricPrompt.PromptInfo.Builder()
      .setTitle(title)
      .setSubtitle(subtitle)
      .setDescription(description)

  // Use Device Credentials if allowed, otherwise show Cancel Button
  builder.apply {
    if (allowDeviceCredential) setDeviceCredentialAllowed(true)
    else setNegativeButtonText("Cancel")
  }

  return builder.build()
}

Next, include this import above the code you just added:

import androidx.biometric.BiometricPrompt

The code above uses a builder class, BiometricPrompt.PromptInfo.Builder, to create the dialog and populate it with the title, subtitle and description — pretty simple!

Are you wondering what allowDeviceCredential is doing here? It allows you to configure fallback options to skip biometric authentication. For example, this lets you offer the option to use the device’s existing passcode/pattern or show a Cancel button when the biometric prompt displays. BiometricPrompt.PromptInfo.Builder offers you both options with some built-in functions.

Setting allowDeviceCredential to true applies setDeviceCredentialAllowed(true) to the dialog builder to create a special button that launches your device’s PIN, passcode or pattern lock screen as an alternative method of user authentication.

The default value for allowDeviceCredential is false. In that case, the biometric prompt shows a Negative or Cancel button. setNegativeButtonText("Cancel") just sets the button text to Cancel. You’re free to set any text, though, like, “Go Away”. :]

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!

Encrypting and Decrypting Your Secrets

Build and run again and log in with your fingerprint. You’ll see an empty screen with a floating action button at the bottom-right corner. Tap the button; it’ll navigate to EncryptionActivity, which looks like this:

iCrypt message entry screen

Try inputting some text and tap ENCRYPT MESSAGE at the bottom of the screen — but nothing happens! Your next task is to encrypt the text you just entered and store it somewhere safe.

Encrypting Plaintext to Ciphertext

You need a cipher to execute encryption and decryption with a SecretKey, remember? Add getCipher() inside CryptographyUtil.kt. That gives you a cipher instance:

fun getCipher(): Cipher {
  val transformation = "$ENCRYPTION_ALGORITHM/$ENCRYPTION_BLOCK_MODE/$ENCRYPTION_PADDING"

  return Cipher.getInstance(transformation)
}

Here, transformation defines the encryption algorithm with additional information, following Java’s Standard Cipher Algorithm Names documentation.

Now, prepare the cipher instance providing the SecretKey you need for encryption:

fun getInitializedCipherForEncryption(): Cipher {
  val cipher = getCipher()
  val secretKey = getOrCreateSecretKey(YOUR_SECRET_KEY_NAME)
  cipher.init(Cipher.ENCRYPT_MODE, secretKey)

  return cipher
}

Note that the SecretKey is generated only once — when you use it for the first time. If cipher requires it later, it’ll use the same SecretKey, executing getOrCreateSecretKey() to unlock your secrets.

You’re now ready to encrypt and hide your secrets! Add this convenient function, right after getInitializedCipherForEncryption():

fun encryptData(plaintext: String, cipher: Cipher): EncryptedMessage {
  val ciphertext = cipher
    .doFinal(plaintext.toByteArray(Charset.forName("UTF-8")))
  return EncryptedMessage(ciphertext, cipher.iv)
}

This function converts plaintext to ciphertext. After you pass your plaintext and cipher through this function, the cipher does its magic to encrypt the plaintext, then returns EncryptedMessage.

EncryptedMessage is a data class inside the common package. It consists of your cipherText, initializationVector for the cipher and savedAt property, which keeps the timestamp of the moment you created an EncryptedMessage.

The encryption action happens within EncryptionActivity.

Open EncryptionActivity.kt and add this function at the bottom of the class:

private fun showBiometricPromptToEncrypt() {
  // 1
  val cryptoObject = BiometricPrompt.CryptoObject(
    CryptographyUtil.getInitializedCipherForEncryption()
  )
  // 2
  BiometricUtil.showBiometricPrompt(
    activity = this,
    listener = this,
    cryptoObject = cryptoObject
  )
}

The function above performs two simple tasks. It:

  1. Creates CryptoObject for the biometric prompt by calling CryptographyUtil.getInitializedCipherForEncryption() from CryptographyUtil.
  2. Displays the biometric prompt using showBiometricPrompt(), passing the activity reference, listener, to handle callback actions and CryptoObject as the cipher.

Next, replace onClickEncryptMessage() with:

fun onClickEncryptMessage(view: View) {
  val message = textInputMessage.editText?.text.toString().trim()
  if (!TextUtils.isEmpty(message)) {
    showBiometricPromptToEncrypt()
  }
}

This simply displays the biometric prompt upon tapping the button when you input any message to encrypt.

With the ability to encrypt in place, it’s time to combine encrypting with the biometric authentication.

Handling Callbacks

Now, the final step — you need to encrypt and save your message, which can only happen if biometric authentication is successful. Find onBiometricAuthenticationSuccess(), which is already implemented in EncryptionActivity for your convenience. Insert the code below inside that function:

result.cryptoObject?.cipher?.let {
  val message = textInputMessage.editText?.text.toString().trim()
  if (!TextUtils.isEmpty(message)) {
    encryptAndSave(message, it)
    confirmInput()
  }
}

This takes the cipher from the result on a successful callback, uses it to encrypt your message and then saves it. Then it shows a confirmation alert when complete.

The actual encryption and storage of the message happens inside encryptAndSave(). Create it at the end of EncryptionActivity.kt as follows:

private fun encryptAndSave(plainTextMessage: String, cipher: Cipher) {
  val encryptedMessage = CryptographyUtil.encryptData(plainTextMessage, cipher)

  PreferenceUtil.storeEncryptedMessage(
    applicationContext,
    prefKey = encryptedMessage.savedAt.toString(),
    encryptedMessage = encryptedMessage
  )
}

Here, you’re converting your plainTextMessage to EncryptedMessage with the help of the cipher and storing it in the SharedPreference with the savedAt timestamp as its preference-key.

Build and run again, navigate to EncryptionActivity and tap ENCRYPT MESSAGE after inputting some text. The biometric prompt appears. Authenticate with your fingerprint and voila — you encrypted your first message!

Go back and you’ll see a list of your encrypted messages, sorted with the latest on top:

iCrypt message list

Now, tap on any secret message from the list. This opens DecryptionActivity, which refuses to display your secret message unless you authenticate yourself.

iCrypt Decrypt Message Screen

Don’t worry, you’ll learn how to unlock it soon…

Decrypting Ciphertext to Plaintext

Only you can see your secret message by authenticating with your biometrics, but you need a cipher with the proper configuration to convert ciphertext back to plaintext. To obtain this cipher, open CryptographyUtil.kt again and add the function below:

fun getInitializedCipherForDecryption(
      initializationVector: ByteArray? = null
  ): Cipher {
  val cipher = getCipher()
  val secretKey = getOrCreateSecretKey(YOUR_SECRET_KEY_NAME)
  cipher.init(
    Cipher.DECRYPT_MODE,
    secretKey,
    GCMParameterSpec(KEY_SIZE, initializationVector)
  )

  return cipher
}

Here, you’re passing initializationVector from EncryptedMessage. Note that you need initializationVector to retrieve the same set of parameters you used for encryption so you can revert the encryption. Then, you initialize the cipher again, this time in decryption mode with required specs and the SecretKey from your Keystore.

After that, write a function to execute the decryption with your cipher:

fun decryptData(ciphertext: ByteArray, cipher: Cipher): String {
  val plaintext = cipher.doFinal(ciphertext)
  return String(plaintext, Charset.forName("UTF-8"))
}

The function above is a mirror image of your previously added encryptData(). You’ll pass ciphertext and cipher as arguments here, then the cipher will work its magic again and return a plain string, decrypting the ciphertext.

Implementing Your Building Blocks

Now that you’ve constructed the building blocks, it’s time to play with them!

Android character playing with building blocks

You need to prompt for biometric authentication once again to unlock your secrets upon tapping DECRYPT MESSAGE.

Open DecryptionActivity, add showBiometricPromptToDecrypt() at the bottom of the class and call it inside onClickDecryptMessage(). Your code will look like this:

fun onClickDecryptMessage(view: View) {
  showBiometricPromptToDecrypt()
}

private fun showBiometricPromptToDecrypt() {
  encryptedMessage?.initializationVector?.let { it ->
    val cryptoObject = BiometricPrompt.CryptoObject(
       CryptographyUtil.getInitializedCipherForDecryption(it)
    )

    BiometricUtil.showBiometricPrompt(
      activity = this,
      listener = this,
      cryptoObject = cryptoObject
   )
  }
}

showBiometricPromptToDecrypt() looks for the initializationVector from your EncryptedMessage in this class. It then calls getInitializedCipherForDecryption() from CryptographyUtil to prepare a cipher for decryption, providing the specs from initializationVector. CryptoObject holds the cipher and BiometricPrompt acts as a gatekeeper here — you get the cipher only upon successful authentication, and only then can you unlock your secret!

The rest of your task is easy. Insert the code below inside onBiometricAuthenticationSuccess():

result.cryptoObject?.cipher?.let {
  decryptAndDisplay(it)
}

This takes the cipher from the authentication result and uses it to decrypt and display your message.

Now, define decryptAndDisplay():

private fun decryptAndDisplay(cipher: Cipher) {
  encryptedMessage?.cipherText?.let { it ->
    val decryptedMessage = CryptographyUtil.decryptData(it, cipher)
    textViewMessage.text = decryptedMessage
  }
}

The steps are simple here: You’re asking for help from CryptographyUtil.decryptData(), providing your ciphertext and the cipher. It performs a decryption operation and returns decryptedMessage in plaintext. decryptedMessage then displays in your TextView.

Build and run now, and try unlocking one of your secret messages with a biometric prompt. You’ll be amazed at how painless, yet secure, the process is!

The final screen reveals your secret message at the center of the screen, like this:

Decrypted Message on Screen

Congratulations, and welcome to the world of passwordless authentication!

Android celebrating with confetti

Where to Go From Here?

Check out the final project by clicking the Download Materials button at the top or bottom of this tutorial.

Don’t end your journey with biometrics and cryptography here; enhance your knowledge with these useful resources:

I hope you enjoyed creating a passwordless app and learning about the biometric authentication process. If you have any questions or comments, join the forum discussion below.

Average Rating

4.6/5

Add a rating for this content

5 ratings

More like this

Contributors

Comments