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?
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!