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 source code is available on github.
We are what we repeatedly do. Excellence, then, is not an act, but a habit. Try out Justly and start building your habits today!
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.
Biometric authentication offers several advantages over traditional methods like passwords or PINs.
Firstly, it provides a seamless user experience, eliminating the need for users to remember complex passwords.
Secondly, it enhances security by utilizing unique biological traits such as fingerprints or facial features.
Lastly, biometric authentication reduces the risk of unauthorized access, as these biological traits are inherently difficult to replicate.
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:
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:
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.
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.
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
}
}
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:
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
}
}
}
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
}
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 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()
}
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))
}
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 this functions at required places to manage user authentication.
Let’s assume you have designed sign-up screen and integrated authorization service e.g., Firebase authentication, then on successful authorization, you will receive a token and we will use that token to register user biometrics.
If you want to checkout how to authenticate user using firebase authentication then you can checkout this blog.
So, upon successful account creation, it will be an opportune moment to prompt users to register their biometrics for future authentication.
We’ll leverage the BiometricHelper’s registerUserBiometrics() function to seamlessly integrate this process into our SignUp screen.
@Composable
fun SignUpScreen() {
// Obtain the current FragmentActivity context
val context = LocalContext.current as FragmentActivity
// Check if biometric authentication is available on the device
val isBiometricAvailable = remember { BiometricHelper.isBiometricAvailable(context) }
// Retrieve the ViewModel for the SignUpScreen
val viewModel = hiltViewModel<SignUpScreenViewModel>()
// Collect the current state from the ViewModel
val state by viewModel.state.collectAsState()
// Mutable state to control the visibility of the biometric enable dialog
var showBiometricEnableDialog by remember { mutableStateOf(false) }
// LaunchedEffect to trigger the biometric enable dialog upon successful sign-up
LaunchedEffect(key1 = state) {
if (state is SignUpState.SUCCESS) {
showBiometricEnableDialog = true
}
}
// Display the biometric enable dialog if necessary
if (showBiometricEnableDialog) {
if (isBiometricAvailable) {
EnableBiometricDialog(
onEnable = {
// Register user biometrics upon enabling
BiometricHelper.registerUserBiometrics(context) {
showBiometricEnableDialog = false
// Update ViewModel to indicate biometric enabled
viewModel.setBiometricEnabled(true)
// Navigate to the home activity
context.startHomeActivity()
}
},
// Navigate to the home activity if user don't want to register biometric authentication
{
context.startHomeActivity()
}
)
} else {
// Navigate to the home activity if biometric not available
context.startHomeActivity()
}
}
}
// Extension function to start the HomeActivity and finish the current activity
fun Activity.startHomeActivity() {
val homeIntent = Intent(this, HomeActivity::class.java)
this.startActivity(homeIntent)
this.finish()
}
Explanation:
showBiometricEnableDialog
, to determine whether to display the dialog for enabling biometric authentication.isBiometricAvailable
), we show a dialog prompting the user to enable biometric authentication.registerUserBiometrics()
function is called, registering the user's biometrics securely.From here, we have successfully enabled biometric authentication and now we will show biometric prompt on sign-in screen.
Here, we will check if biometric authentication is registered or not. If its registered then we will show biometric-prompt, so that users have delightful experience while signing in.
We’ll leverage the BiometricHelper’s authenticateUser() function to seamlessly integrate this process into our SignIn screen.
@Composable
fun SignInScreen(navController: NavHostController) {
// Obtain the current FragmentActivity context
val context = LocalContext.current as FragmentActivity
// Retrieve the ViewModel for the SignInScreen
val viewModel = hiltViewModel<SignInScreenViewModel>()
// Collect the current state from the ViewModel
val state by viewModel.state.collectAsState()
// Check if biometric authentication is available on the device
val isBiometricAvailable = remember { BiometricHelper.isBiometricAvailable(context) }
// Determine whether to show the biometric prompt
val showBiometricPrompt by viewModel.showBiometricPrompt.collectAsState()
// Mutable state to control the visibility of the biometric icon
val showBiometricIcon = remember { mutableStateOf(false) }
// LaunchedEffect to navigate to the home activity upon successful sign-in
LaunchedEffect(key1 = state) {
if (state is SignInState.Success) {
context.navigateToHomeActivity()
}
}
// LaunchedEffect to check if biometric login is enabled
LaunchedEffect(key1 = isBiometricAvailable) {
if (isBiometricAvailable) {
// Check in preference data store if user enabled biometric or not
viewModel.checkIfBiometricLoginEnabled()
}
}
// LaunchedEffect to handle biometric authentication
LaunchedEffect(key1 = showBiometricPrompt) {
if (showBiometricPrompt) {
BiometricHelper.authenticateUser(context,
onSuccess = { plainText ->
// This will be the same token which we used while registering user
viewModel.setToken(plainText)
context.navigateToHomeActivity()
})
} else {
// Check if biometric authentication is registered
val cryptoManager = CryptoManager()
val encryptedData = cryptoManager.getFromPrefs(
context,
ENCRYPTED_FILE_NAME,
Context.MODE_PRIVATE,
PREF_BIOMETRIC
)
encryptedData?.let {
showBiometricIcon.value = true
}
}
}
}
Explanation:
This setup ensures that users are provided with a seamless and secure authentication experience on the Sign-In screen, with support for biometric authentication if available and enabled.
That’s it, we’ve effectively implemented biometric authentication, culminating in a robust security solution. With this setup, users can enjoy a seamless and secure authentication experience.
In conclusion, we’ve successfully implemented biometric authentication using Jetpack Compose and AES encryption, ensuring both security and user convenience in Android applications.
By adopting biometric authentication, we provide users with a seamless and robust authentication method, reducing reliance on passwords and enhancing overall security.
Integrating AES encryption adds an extra layer of protection to sensitive user data, bolstering security measures.
As we conclude, it’s clear that embracing biometric authentication aligns with evolving security standards and user expectations, promising a safer and more user-friendly authentication experience.
Get started today
Let's build the next
big thing!
Let's improve your business's digital strategy and implement robust mobile apps to achieve your business objectives. Schedule Your Free Consultation Now.
Get Free ConsultationGet started today
Let's build the next big thing!
Let's improve your business's digital strategy and implement robust mobile apps to achieve your business objectives. Schedule Your Free Consultation Now.
Get Free Consultation