Exciting News! Our blog has a new Home!🚀
Background
With the increasing reliance on smartphones for various activities, securing access to sensitive information has become paramount. Traditional methods like passwords or PINs are often cumbersome and prone to security breaches.
Biometric authentication addresses these concerns by leveraging unique biological traits such as fingerprints, facial features, or iris patterns for identity verification.
Android devices have built-in support for biometric authentication, making it accessible for developers to integrate into their applications seamlessly.
What we’ll implement in this blog?
The full source code is available on GitHub.
Introduction
Biometric authentication has become a cornerstone of security in modern mobile applications, offering users a convenient and secure way to access sensitive information.
In this blog post, we will explore how to implement biometric authentication using Jetpack Compose, the modern UI toolkit for Android, coupled with AES encryption for added security.
Why Cryptographic Solution is Necessary
While biometric authentication offers enhanced security, it’s crucial to augment it with cryptographic solutions like AES encryption for robust protection of sensitive data.
Here’s why cryptographic solutions are essential when working with biometric authentication:
Data Protection
Key Management
Compliance Requirements
Defense against Attacks
Types of authenticators that your app can support
BIOMETRIC_STRONG: Authentication using a Class 3 biometric, as defined on the Android compatibility definition page.
BIOMETRIC_WEAK: Authentication using a Class 2 biometric, as defined on the Android compatibility definition page.
DEVICE_CREDENTIAL: Authentication using a screen lock credential – the user's PIN, pattern, or password.
Types of Authentication Supported
When implementing biometric authentication in an app, it’s essential to support various biometric modalities to cater to different devices and user preferences.
Android provides support for the following biometric authentication types:
Fingerprint Authentication: Utilizes the unique patterns of a user’s fingerprints for authentication.
Face Authentication: Verifies the user’s identity by analyzing facial features captured by the device’s camera.
Iris Authentication: Scans the unique patterns in the user’s iris for authentication.
By supporting multiple authentication types, developers can ensure compatibility with a wide range of devices and accommodate users with various preferences and accessibility needs.
In this blog post, we will focus on implementing fingerprint authentication using Jetpack Compose and AES encryption.
We will walk through the process of integrating biometric authentication into an Android application and securing sensitive data using AES encryption.
So let’s begin with the implementation…
The source code is available on GitHub.
Implementing Biometric Authentication
1. Add Biometric Authentication Dependencies
To get started with biometric authentication in your Android application, you need to add the following dependencies to your app-level build.gradle file:
dependencies {
implementation "androidx.biometric:biometric:1.2.0"
}
These dependencies provide the necessary APIs to interact with the biometric hardware on Android devices and authenticate users using their biometric data.
2. Create CryptoManager for AES Encryption
CryptoManager will manage the encryption and decryption of sensitive data using AES encryption. The CryptoManager class will handle key generation, encryption, and decryption operations.
// Interface defining cryptographic operations
interface CryptoManager {
// Initialize encryption cipher
fun initEncryptionCipher(keyName: String): Cipher
// Initialize decryption cipher
fun initDecryptionCipher(keyName: String, initializationVector: ByteArray): Cipher
// Encrypt plaintext
fun encrypt(plaintext: String, cipher: Cipher): EncryptedData
// Decrypt ciphertext
fun decrypt(ciphertext: ByteArray, cipher: Cipher): String
// Save encrypted data to SharedPreferences
fun saveToPrefs(
encryptedData: EncryptedData,
context: Context,
filename: String,
mode: Int,
prefKey: String
)
// Retrieve encrypted data from SharedPreferences
fun getFromPrefs(
context: Context,
filename: String,
mode: Int,
prefKey: String
): EncryptedData?
}
// Factory function to create CryptoManager instance
fun CryptoManager(): CryptoManager = CryptoManagerImpl()
// Implementation of CryptoManager interface
class CryptoManagerImpl : CryptoManager {
// Encryption transformation algorithm
private val ENCRYPTION_TRANSFORMATION = "AES/GCM/NoPadding"
// Android KeyStore provider
private val ANDROID_KEYSTORE = "AndroidKeyStore"
// Key alias for the secret key
private val KEY_ALIAS = "MyKeyAlias"
// KeyStore instance
private val keyStore: KeyStore = KeyStore.getInstance(ANDROID_KEYSTORE)
init {
// Load the KeyStore
keyStore.load(null)
// If key alias doesn't exist, create a new secret key
if (!keyStore.containsAlias(KEY_ALIAS)) {
createSecretKey()
}
}
// Initialize encryption cipher
override fun initEncryptionCipher(keyName: String): Cipher {
val cipher = Cipher.getInstance(ENCRYPTION_TRANSFORMATION)
cipher.init(Cipher.ENCRYPT_MODE, getSecretKey())
return cipher
}
// Initialize decryption cipher
override fun initDecryptionCipher(keyName: String, initializationVector: ByteArray): Cipher {
val cipher = Cipher.getInstance(ENCRYPTION_TRANSFORMATION)
val spec = GCMParameterSpec(128, initializationVector)
cipher.init(Cipher.DECRYPT_MODE, getSecretKey(), spec)
return cipher
}
// Encrypt plaintext
override fun encrypt(plaintext: String, cipher: Cipher): EncryptedData {
val encryptedBytes = cipher.doFinal(plaintext.toByteArray(Charset.forName("UTF-8")))
return EncryptedData(encryptedBytes, cipher.iv)
}
// Decrypt ciphertext
override fun decrypt(ciphertext: ByteArray, cipher: Cipher): String {
val decryptedBytes = cipher.doFinal(ciphertext)
return String(decryptedBytes, Charset.forName("UTF-8"))
}
// Save encrypted data to SharedPreferences
override fun saveToPrefs(
encryptedData: EncryptedData,
context: Context,
filename: String,
mode: Int,
prefKey: String
) {
val json = Gson().toJson(encryptedData)
with(context.getSharedPreferences(filename, mode).edit()) {
putString(prefKey, json)
apply()
}
}
// Retrieve encrypted data from SharedPreferences
override fun getFromPrefs(
context: Context,
filename: String,
mode: Int,
prefKey: String
): EncryptedData? {
val json = context.getSharedPreferences(filename, mode).getString(prefKey, null)
return Gson().fromJson(json, EncryptedData::class.java)
}
// Create a new secret key
private fun createSecretKey() {
val keyGenParams = KeyGenParameterSpec.Builder(
KEY_ALIAS,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
).apply {
setBlockModes(KeyProperties.BLOCK_MODE_GCM)
WesetEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
setUserAuthenticationRequired(true)
}.build()
val keyGenerator =
KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEYSTORE)
keyGenerator.init(keyGenParams)
keyGenerator.generateKey()
}
// Retrieve the secret key from KeyStore
private fun getSecretKey(): SecretKey {
return keyStore.getKey(KEY_ALIAS, null) as SecretKey
}
}
// Data class to hold encrypted data
data class EncryptedData(val ciphertext: ByteArray, val initializationVector: ByteArray) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as EncryptedData
if (!ciphertext.contentEquals(other.ciphertext)) return false
return initializationVector.contentEquals(other.initializationVector)
}
override fun hashCode(): Int {
var result = ciphertext.contentHashCode()
result = 31 * result + initializationVector.contentHashCode()
return result
}
}
3. Create BiometricHelper that will handle biometric authentication operations
BiometricHelper is a versatile utility object designed to simplify the integration of biometric authentication features into Android applications. This helper class encapsulates complex biometric API interactions, providing developers with a clean and intuitive interface to perform common biometric authentication tasks.
object BiometricHelper {
...
}
Now, in BiometricHelper , we will add below functions with specific functionalities:
- Biometric Availability Check:
The first step in implementing biometric authentication is to check whether the device supports biometric authentication.
BiometricHelper offers a convenient method, isBiometricAvailable(), which performs this check and returns a boolean value indicating the availability of biometric authentication on the device.
// Check if biometric authentication is available on the device
fun isBiometricAvailable(context: Context): Boolean {
val biometricManager = BiometricManager.from(context)
return when (biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG or BiometricManager.Authenticators.BIOMETRIC_WEAK)) {
BiometricManager.BIOMETRIC_SUCCESS -> true
else -> {
Log.e("TAG", "Biometric authentication not available")
false
}
}
}
- BiometricPrompt Integration:
BiometricHelper seamlessly integrates with the BiometricPrompt API, which serves as the primary interface for biometric authentication on Android devices.
It provides a method, getBiometricPrompt(), to create a BiometricPrompt instance with a predefined callback, simplifying the setup process and handling of authentication events.
// Retrieve a BiometricPrompt instance with a predefined callback
private fun getBiometricPrompt(
context: FragmentActivity,
onAuthSucceed: (BiometricPrompt.AuthenticationResult) -> Unit
): BiometricPrompt {
val biometricPrompt =
BiometricPrompt(
context,
ContextCompat.getMainExecutor(context),
object : BiometricPrompt.AuthenticationCallback() {
// Handle successful authentication
override fun onAuthenticationSucceeded(
result: BiometricPrompt.AuthenticationResult
) {
Log.e("TAG", "Authentication Succeeded: ${result.cryptoObject}")
// Execute custom action on successful authentication
onAuthSucceed(result)
}
// Handle authentication errors
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
Log.e("TAG", "onAuthenticationError")
}
// Handle authentication failures
override fun onAuthenticationFailed() {
Log.e("TAG", "onAuthenticationFailed")
}
}
)
return biometricPrompt
}
- Customizable Prompt Info:
BiometricHelper facilitates the creation of BiometricPrompt.PromptInfo objects with customizable display text, allowing developers to tailor the authentication prompt to match the look and feel of their app.
The getPromptInfo() method generates a PromptInfo object with a customized title, subtitle, description, and negative button text.
// Create BiometricPrompt.PromptInfo with customized display text
private fun getPromptInfo(context: FragmentActivity): BiometricPrompt.PromptInfo {
return BiometricPrompt.PromptInfo.Builder()
.setTitle(context.getString(R.string.biometric_prompt_title_text))
.setSubtitle(context.getString(R.string.biometric_prompt_subtitle_text))
.setDescription(context.getString(R.string.biometric_prompt_description_text))
.setConfirmationRequired(false)
.setNegativeButtonText(
context.getString(R.string.biometric_prompt_use_password_instead_text)
)
.build()
}
- User Biometrics Registration:
Registering user biometrics involves encrypting sensitive data, such as authentication tokens, using cryptographic techniques.
BiometricHelper offers a registerUserBiometrics() method, which encrypts a randomly generated token and stores it securely using the CryptoManager, an accompanying cryptographic utility class.
// Register user biometrics by encrypting a randomly generated token
fun registerUserBiometrics(
context: FragmentActivity,
onSuccess: (authResult: BiometricPrompt.AuthenticationResult) -> Unit = {}
) {
val cryptoManager = CryptoManager()
val cipher = cryptoManager.initEncryptionCipher(SECRET_KEY)
val biometricPrompt =
getBiometricPrompt(context) { authResult ->
authResult.cryptoObject?.cipher?.let { cipher ->
// Dummy token for now(in production app, generate a unique and genuine token
// for each user registration or consider using token received from authentication server)
val token = UUID.randomUUID().toString()
val encryptedToken = cryptoManager.encrypt(token, cipher)
cryptoManager.saveToPrefs(
encryptedToken,
context,
ENCRYPTED_FILE_NAME,
Context.MODE_PRIVATE,
PREF_BIOMETRIC
)
// Execute custom action on successful registration
onSuccess(authResult)
}
}
biometricPrompt.authenticate(getPromptInfo(context), BiometricPrompt.CryptoObject(cipher))
}
- User Authentication:
authenticateUser() function handles the authentication process using biometrics. It decrypts the stored token using the CryptoManager and initiates the biometric authentication flow.
Upon successful authentication, the decrypted token is retrieved, enabling the app to grant access to the user.
// Authenticate user using biometrics by decrypting stored token
fun authenticateUser(context: FragmentActivity, onSuccess: (plainText: String) -> Unit) {
val cryptoManager = CryptoManager()
val encryptedData =
cryptoManager.getFromPrefs(
context,
ENCRYPTED_FILE_NAME,
Context.MODE_PRIVATE,
PREF_BIOMETRIC
)
encryptedData?.let { data ->
val cipher = cryptoManager.initDecryptionCipher(SECRET_KEY, data.initializationVector)
val biometricPrompt =
getBiometricPrompt(context) { authResult ->
authResult.cryptoObject?.cipher?.let { cipher ->
val plainText = cryptoManager.decrypt(data.ciphertext, cipher)
// Execute custom action on successful authentication
onSuccess(plainText)
}
}
val promptInfo = getPromptInfo(context)
biometricPrompt.authenticate(promptInfo, BiometricPrompt.CryptoObject(cipher))
}
}
So, our BiometricHelper
is ready and we now just have to call these
functions at the required places to manage user authentication.
This post only has implementation until
BiometricHelper
, to read the complete guide including proper function usages, please visit our full blog.The post is originally published on canopas.com.
I encourage you to share your thoughts in the comments section below. Your input not only enriches our content but also fuels our motivation to create more valuable and informative articles for you.
Follow Canopas to get updates on interesting articles!