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!
Top comments (0)