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.
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:
- Integrate the AndroidX Biometric Library.
- Authenticate a user through a biometric prompt.
- Encrypt and decrypt sensitive information using biometric credentials.
- Properly handle successful or failed authentication.
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:
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:
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:
- BIOMETRIC_SUCCESS: The device is ready to use a biometric prompt, meaning the hardware is available and the user has enrolled their biometric data.
- BIOMETRIC_ERROR_NONE_ENROLLED: The device supports biometrics, but the user hasn’t enrolled either their fingerprints or their face.
- 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.
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:
- Set PromptInfo with your desired message and configuration.
- 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:
- It creates an
executor
to handle the callback events. - It creates the
callback
object to receive authentication events on Success, Failed or Error status with the appropriate result or error messages. - Finally, it constructs a biometric prompt using the
activity
,executor
andcallback
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:
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 namedPreferenceUtil
. 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:
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:
-
keyName
, in this function, is your alias. It looks forkeyName
inKeyStore
and returns the associated SecretKey. - If no SecretKey exists for this
keyName
, you createparamsBuilder
for encryption and decryption, applying the constants you defined earlier, such asENCRYPTION_BLOCK_MODE
andKEY_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. - It prepares
keyGenerator
using the configuration fromparamsBuilder
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:
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:
- Creates
CryptoObject
for the biometric prompt by callingCryptographyUtil.getInitializedCipherForEncryption()
fromCryptographyUtil
. - Displays the biometric prompt using
showBiometricPrompt()
, passing theactivity
reference,listener
, to handle callback actions andCryptoObject
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:
Now, tap on any secret message from the list. This opens DecryptionActivity, which refuses to display your secret message unless you authenticate yourself.
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!
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:
Congratulations, and welcome to the world of passwordless authentication!
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.
Comments