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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Android Biometric API: Getting Started
25 mins
- Getting Started
- Introducing The Android Biometric API
- Checking Device Capabilities
- Implementing the Biometric Login
- Building BiometricPrompt
- Preparing PromptInfo
- Initializing BiometricPrompt
- Displaying BiometricPrompt
- Creating CryptographyUtil
- Cryptography 101: Cipher, Keystore and SecretKey
- Working With the Keystore
- Generating the SecretKey
- Encrypting and Decrypting Your Secrets
- Encrypting Plaintext to Ciphertext
- Handling Callbacks
- Decrypting Ciphertext to Plaintext
- Implementing Your Building Blocks
- Where to Go From Here?
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!