Biometric Encryption and Decryption with AndroidX.Biometric library: Enhancing Security and User Experience
Introduction
In today’s digital landscape, security and privacy are of paramount importance. Android developers face the challenge of implementing robust security measures while maintaining a seamless user experience. One technology that addresses these concerns is biometric encryption and decryption, enabled by the AndroidX.Biometric library. This powerful library empowers developers to leverage biometric authentication methods for encrypting and decrypting sensitive data, providing a secure and user-friendly solution. In this article, we explore the capabilities and benefits of biometric encryption and decryption using the AndroidX.Biometric library and delve into practical implementation considerations.
Understanding Biometric Encryption and Decryption
Biometric encryption and decryption involve utilizing the unique biometric traits of individuals, such as fingerprints or facial features, to secure and access sensitive data. By incorporating biometric authentication into the encryption and decryption process, developers can establish a strong and convenient security mechanism. The AndroidX.Biometric library serves as a bridge between biometric hardware and application development, facilitating the seamless integration of biometric authentication
The Power of AndroidX.Biometric Library
The AndroidX.Biometric library provides a standardized and simplified API for integrating biometric authentication into Android applications. It offers support for various biometric modalities, including fingerprint recognition, facial recognition, and iris scanning, depending on the capabilities of the device. This library abstracts the underlying biometric hardware complexities, enabling developers to focus on utilizing biometrics for encryption and decryption without worrying about device-specific implementations.
Before we dive in
Before we get into the the library implementation, I would like to throw a small light about the some basic concepts of cryptography which will help you out in understanding the concepts much clearer down the line.
Cipher: A cipher is a way of converting information into a secret code. It takes the original message and transforms it using specific rules or a secret key to make it unreadable to unauthorized people. The same rules or key are used to reverse the process and decrypt the message back to its original form.
Initialization Vector (IV): An IV is like a random starting point used in encryption to add extra randomness and make the encrypted data more secure. It is combined with the encryption key to set up the encryption process. Even if you encrypt the same information multiple times, using different IVs ensures that the resulting encrypted data will be different each time.
AES Encryption: AES is a widely used method for encrypting information. It works by dividing the data into fixed-size blocks and applying various mathematical operations to scramble the information according to a secret key. AES is known for its strong security, efficiency, and widespread usage. It’s commonly used to protect sensitive information on Android devices and in other applications that require secure communication and data protection.
Setting up authentication callback
executor = ContextCompat.getMainExecutor(this)
biometricPrompt = BiometricPrompt(this, executor, object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationError(
errorCode: Int, errString: CharSequence
) {
super.onAuthenticationError(errorCode, errString)
}
override fun onAuthenticationSucceeded(
result: BiometricPrompt.AuthenticationResult
) {
super.onAuthenticationSucceeded(result)
// the result contains cipher object which can be used for encryption.
}
override fun onAuthenticationFailed() {
super.onAuthenticationFailed()
}
})
Adding a biometric callback serves multiple purposes, and one of them is to facilitate cryptographic encryption or decryption operations based on the biometric authentication request. Let’s explore an example of how the cipher object can be used in this context
override fun onAuthenticationSucceeded(
result: BiometricPrompt.AuthenticationResult
) {
super.onAuthenticationSucceeded(result)
val cryptoObject = requireNotNull(result.cryptoObject)
val cipher = requireNotNull(cryptoObject.cipher)
processedByteArray = cipher.doFinal(rawByteArray)
}
Now, you might wonder how the function cipher.doFinal
operates in both encryption and decryption modes. Let me explain. The Cipher
can be initialized in two modes: encryption and decryption. If we initialize the Cipher
in encryption mode, then cipher.doFinal
will carry out encryption. On the other hand, if we initialize the Cipher
in decryption mode, cipher.doFinal
will perform the decryption process.
The other advantage that I wanted to point out is the initialization vector (IV) byte array that the cipher
provides us. Here is how you can access them from the result object.
override fun onAuthenticationSucceeded(
result: BiometricPrompt.AuthenticationResult
) {
super.onAuthenticationSucceeded(result)
val cryptoObject = requireNotNull(result.cryptoObject)
val cipher = requireNotNull(cryptoObject.cipher)
// This a very important when we do encryption and decryption.
val initializationVector = cipher.iv
}
We will get to understand the usage of IV better while we progress.
Setting up Biometric Prompt Info
promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle("Biometric login for my app")
.setSubtitle("Log in using your biometric credential")
.setNegativeButtonText("Use account password")
.build()
The prompt-info is pretty much self explanatory, we are mentioning a bunch of text here to be shown when we display the popup.
Getting to an understanding about the cryptography parameters
It is important to decide which type of cryptography algorithm, block mode and encryption padding that we should use for the biometric authentication.
Here are the ones that I am going to use for our example
private val KEY_SIZE = 2048
private val ANDROID_KEYSTORE = "AndroidKeyStore"
private val ENCRYPTION_BLOCK_MODE = KeyProperties.BLOCK_MODE_CBC
private val ENCRYPTION_PADDING = KeyProperties.ENCRYPTION_PADDING_PKCS7
private val ENCRYPTION_ALGORITHM = KeyProperties.KEY_ALGORITHM_AES
Here are a few things to note. We have decided to go with KEY_ALGORITHM_AES
because we don't require an asymmetric cryptography approach. Implementing RSA
would be an overkill for our purposes. Therefore, we can opt for the strongest symmetric algorithm available to fulfill our requirements.
Creating the Cipher object
private fun getCipher(): Cipher {
val transformation = (ENCRYPTION_ALGORITHM + "/"
+ ENCRYPTION_BLOCK_MODE + "/"
+ ENCRYPTION_PADDING)
return Cipher.getInstance(transformation)
}
private fun getOrCreateSecretKey(keyName: String): SecretKey {
// If Secretkey was previously created for that keyName, then grab and return it.
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 }
// if you reach here, then a new SecretKey must be generated for that keyName
val keyGenParams = KeyGenParameterSpec.Builder(
keyName,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(ENCRYPTION_BLOCK_MODE)
.setEncryptionPaddings(ENCRYPTION_PADDING)
.setUserAuthenticationRequired(true)
.setKeySize(KEY_SIZE)
.setInvalidatedByBiometricEnrollment(true)
.build()
val keyGenerator = KeyGenerator.getInstance(
ENCRYPTION_ALGORITHM,
ANDROID_KEYSTORE
)
keyGenerator.init(keyGenParams)
return keyGenerator.generateKey()
}
The first function, getCipher()
, returns a Cipher
object used for encryption and decryption operations. It generates a transformation string by combining the encryption algorithm, block mode, and padding. The Cipher
instance is then obtained using the transformation string through Cipher.getInstance()
.
The second function, getOrCreateSecretKey(keyName: String)
, retrieves or creates a secret key based on the provided keyName
. It first loads the KeyStore
instance using the ANDROID_KEYSTORE
type. If a previously created secret key exists for the given keyName
, it is retrieved and returned.
If no existing secret key is found, a new one is generated using the KeyGenParameterSpec.Builder
. The builder sets various parameters such as the key name, purposes (encryption and decryption), block modes, encryption paddings, user authentication requirement, key size, and invalidated by biometric enrollment flag. These parameters configure the key generation process.
Finally, a KeyGenerator
instance is obtained using the specified encryption algorithm and ANDROID_KEYSTORE
type. It is Initialized with the generated key generation parameters, and the secret key is generated using keyGenerator.generateKey()
. The newly created secret key is then returned.
Initializing the cipher object in encryption mode
fun getInitializedCipherForEncryption(keyName: String): Cipher {
val cipher = getCipher()
val secretKey = getOrCreateSecretKey(keyName)
cipher.init(Cipher.ENCRYPT_MODE, secretKey)
return cipher
}
Upon successful authentication of biometrics with an encryption mode cipher, it is crucial to acknowledge that the onAuthenticationSuccess
callback will receive the Initialization Vector (IV) object. This IV object needs to be securely stored in local storage to enable decryption operations later. While the IV is not considered a sensitive item, it is still advisable to apply a basic level of security. It's important to remember that the IV is not the key used for decryption, so ensuring minimal protection for the IV would suffice.
Initializing the cipher object in decryption mode
fun getInitializedCipherForDecryption(
keyName: String,
initializationVector: ByteArray
): Cipher {
val cipher = getCipher()
val secretKey = getOrCreateSecretKey(keyName)
cipher.init(Cipher.DECRYPT_MODE, secretKey, IvParameterSpec(initializationVector))
return cipher
}
The IV, which is stored in the device storage during the encryption process, is utilized when initializing the decryption cipher object. It serves as a crucial component for the decryption operation, allowing the cipher to properly reconstruct the original data.
Launching the biometric auth
val cpr = getInitializedCipherForEncryption(superSecireAndroidKey)
// or
val cpr = getInitializedCipherForDecryption(
keyName = superSecireAndroidKey,
initializationVector = iv // From the local storage.
)
// Biometric popup will be shown in this step.
biometricPrompt.authenticate(promptInfo, BiometricPrompt.CryptoObject(cpr))
This step involves launching the biometric popup based on the mode specified in the cipher. After initiating the call, we will start listening to the biometric callback that was created in the initial step of this process. This callback will enable us to continue with the encryption or decryption operation based on the biometric authentication outcome.
POC
Here is a small POC implementation where I’ll encrypting and decrypting the string Joseph's super secure password
using biometrics.
You may notice a brief black screen in between during the biometric prompt. This occurs because the system automatically blocks screen captures and recordings when the biometric prompt is displayed. This security measure ensures that sensitive biometric information remains protected and cannot be captured or recorded without authorization.
Conclusion
Biometric encryption and decryption using the AndroidX.Biometric library present a robust solution for enhancing security and user experience in Android applications. By integrating biometric authentication methods, developers can establish a secure means of encrypting and decrypting sensitive data, all while providing a seamless and convenient user experience. As the digital landscape evolves, leveraging the power of biometrics in Android development becomes increasingly essential, empowering users with enhanced security and privacy in the palm of their hands.