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 3 of 4 of this article. Click here to view the first page.

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