How to Implement Biometric Authentication with Jetpack Compose and AES Encryption

·

8 min read

How to Implement Biometric Authentication with Jetpack Compose and AES Encryption

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?

Biometric authentication

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:

  1. Data Protection

  2. Key Management

  3. Compliance Requirements

  4. Defense against Attacks

Types of authenticators that your app can support

  1. BIOMETRIC_STRONG: Authentication using a Class 3 biometric, as defined on the Android compatibility definition page.

  2. BIOMETRIC_WEAK: Authentication using a Class 2 biometric, as defined on the Android compatibility definition page.

  3. 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:

  1. Fingerprint Authentication: Utilizes the unique patterns of a user’s fingerprints for authentication.

  2. Face Authentication: Verifies the user’s identity by analyzing facial features captured by the device’s camera.

  3. 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!

Did you find this article valuable?

Support Canopas's blog by becoming a sponsor. Any amount is appreciated!