Biometric Authentication with Cryptographic in Jetpack Compose

Biometric Authentication with Cryptographic in Jetpack Compose

One method of protecting sensitive information or premium content within your app is to request biometric authentication, such as using face recognition or fingerprint recognition.

If we talk about authentication, android has divided it into 3 classes.

Class 1(DEVICE_CREDENTIAL): this includes authentication using a screen lock credential – the user's PIN, pattern, or password.

Class 2(BIOMETRIC_WEAK): It is the same as the class 1 type but if we include any extra two-step authentication with it then it is considered as class 2 authentication

Class 3(BIOMETRIC_STRONG): In this, we use authentication using biometric and face recognition

Add below dependency to your app gradle file:

dependencies {
    // Java language implementation
    implementation("androidx.biometric:biometric:1.1.0")

    // Kotlin
    implementation("androidx.biometric:biometric:1.2.0-alpha05")

    // Appcompat 
    implementation(libs.androidx.appcompat)
}

Create a class named BiometricPromptManager:

  • biometric prompt is a fragment which pops.

  • we can show fragments in AppCompatActivity but by default in compose there is ComponentActicity. so here we will include AppCompatActivity as a parameter of the class.

class BiometricPromptManager(private val activity: AppCompatActivity) {
}

Change Theme in Themes.xml File

as we are changing ComponentActivity to AppCompatActivity. We have to change the theme to the Appcompat theme in the Themes.xml File

<resources>
    <style name="Theme.BiometricAuth" parent="Theme.AppCompat.NoActionBar" />
</resources>

Create a Function named showBiometricPrompt inside the class BiometricPromptManager:

  • add two parameters to function -> title and description

  • create a reference for the biometric manager

  • create authenticators and declare the type of authentication

class BiometricPromptManager(private val activity: AppCompatActivity) {

fun showBiometricPrompt(title: String, description: String){
      // Reference of biometric manager
        val manager = BiometricManager.from(activity)

        //there are multiple ways to authenticate so here authenticators are used
        //1. BIOMETRIC_STRONG -> finger print and face recognition
        //2. DEVICE_CREDENTIAL -> the user's PIN, pattern, or password.
        val authenticators =
            if (Build.VERSION.SDK_INT >= 30) BIOMETRIC_STRONG or DEVICE_CREDENTIAL else BIOMETRIC_STRONG

        // we can construct the prompt that how it's look like using promptInfo.Builder
        val promptInfo = PromptInfo.Builder()
            .setTitle(title)
            .setDescription(description)
            .setAllowedAuthenticators(authenticators)
    }
}

Create an Enum class for getting different types of errors:

    sealed interface BiometricResult {
        data object HardwareUnavailable : BiometricResult
        data object FeatureUnavailable : BiometricResult
        data class AuthenticationError(val error: String) : BiometricResult
        data object AuthenticationFailed : BiometricResult
        data object AuthenticationSuccess : BiometricResult
        data object AuthenticationNotSet : BiometricResult
    }

create a channel for showing data and updating UI as per the result:

 // Channel for showing result.
      private val resultChannel = Channel<BiometricResult>()
      val promptResult = resultChannel.receiveAsFlow()

check if biometrics is available on the device or not:

   // Checking wheather the device can provide functionallity of authentication or not
        when (manager.canAuthenticate(authenticators)) {
            BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE -> {
                resultChannel.trySend(BiometricResult.HardwareUnavailable)
                return
            }

            BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> {
                resultChannel.trySend(BiometricResult.FeatureUnavailable)
                return
            }

            BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> {
                resultChannel.trySend(BiometricResult.AuthenticationNotSet)
                return
            }
            else -> Unit
        }

Now, Define actual prompt with callback:

   // Actual prompt with callback 
        val prompt = BiometricPrompt(
            activity,
            object : BiometricPrompt.AuthenticationCallback() {
                override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
                    super.onAuthenticationError(errorCode, errString)
                    resultChannel.trySend(BiometricResult.AuthenticationError(errString.toString()))
                }

                override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
                    super.onAuthenticationSucceeded(result)
                    resultChannel.trySend(BiometricResult.AuthenticationSuccess)
                }

                override fun onAuthenticationFailed() {
                    super.onAuthenticationFailed()
                    resultChannel.trySend(BiometricResult.AuthenticationFailed)
                }
            }
        )
     prompt.authenticate(promptInfo.build())

Now in MainActivity declare the promptManager with the lazy keyword

 private val promptManager by lazy {
        BiometricPromptManager(this)
    }

create the basic UI. add button and on click of button show authentication screen

Column(
    modifier = Modifier
        .fillMaxSize(),
    verticalArrangement = Arrangement.Center,
    horizontalAlignment = Alignment.CenterHorizontally
) {
    Button(onClick = {
        promptManager.showBiometricPrompt(
            title = "Sample prompt",
            description = "Sample prompt description"
        )
    }) {
        Text(text = "Authenticate")
    }
}

collect the flow as a state to observe the result

val biometricResult by promptManager.promptResult.collectAsState(
    initial = null
)

   biometricResult?.let { result ->
        Text(
            text = when (result) {
                is BiometricPromptManager.BiometricResult.AuthenticationError -> {
                    result.error
                }

                BiometricPromptManager.BiometricResult.AuthenticationFailed -> {
                    "Authentication failed"
                }

                BiometricPromptManager.BiometricResult.AuthenticationNotSet -> {
                    "Authentication not set"
                }

                BiometricPromptManager.BiometricResult.AuthenticationSuccess -> {
                    "Authentication success"
                }

                BiometricPromptManager.BiometricResult.FeatureUnavailable -> {
                    "Feature unavailable"
                }

                BiometricPromptManager.BiometricResult.HardwareUnavailable -> {
                    "Hardware unavailable"
                }
            }
        )
    }

If authentication is Not set result is shown. then we have to give the user an option to set its authentication pattern or PIN for that launch activity for the result.

 val enrollLauncher = rememberLauncherForActivityResult(
                        contract = ActivityResultContracts.StartActivityForResult(),
                        onResult = {
                            println("Activity result: $it")
                        }
                    )

LaunchedEffect(biometricResult) {
    if (biometricResult is BiometricPromptManager.BiometricResult.AuthenticationNotSet) {
        if (Build.VERSION.SDK_INT >= 30) {
            val enrollIntent = Intent(Settings.ACTION_BIOMETRIC_ENROLL).apply {
                putExtra(
                    Settings.EXTRA_BIOMETRIC_AUTHENTICATORS_ALLOWED,
                    BIOMETRIC_STRONG or DEVICE_CREDENTIAL
                )
            }
            enrollLauncher.launch(enrollIntent)
        }
    }
}

Now if want to add additional security to authentication then we can do it by cryptographic.

Create a file called CryptographyUtils that creates a function generateSecretKey:

  • here we are generating a secret key using the AES algorithm.
fun generateSecretKey(): SecretKey {
    val keyGenerator = KeyGenerator.getInstance(
        KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore"
    )
    keyGenerator.init(
        KeyGenParameterSpec.Builder(
            "hafdhkkhsfdhasga",
            KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
        )
            .setBlockModes(KeyProperties.BLOCK_MODE_CBC)
            .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
            .setUserAuthenticationRequired(true)
            .setInvalidatedByBiometricEnrollment(true)
            .build()
    )
    return keyGenerator.generateKey()
}

Now we will create a cipher so that we can use this secret key.

fun getCipher(secretKey: SecretKey): Cipher {
    val cipher = Cipher.getInstance(
        KeyProperties.KEY_ALGORITHM_AES + "/"
                + KeyProperties.BLOCK_MODE_CBC + "/"
                + KeyProperties.ENCRYPTION_PADDING_PKCS7
    )
    cipher.init(Cipher.ENCRYPT_MODE, secretKey)
    return cipher
}

Now we will call the authentication using cryptography

  • here we have generated a secret key

  • using cipher to use the secret key

  • created a crypto object using Cipher

  • passed the cryptoObject in the prompt. authenticate parameter. so now encryption and decryption will happen on its own when every user authenticates

val secretKey = generateSecretKey()
val cipher = getCipher(secretKey)
val cryptoObject = BiometricPrompt.CryptoObject(cipher)
prompt.authenticate(promptInfo.build(), cryptoObject)

Full code of BiometricPromptManager:

class BiometricPromptManager(private val activity: AppCompatActivity) {
      // Channel for showing result.
      private val resultChannel = Channel<BiometricResult>()
      val promptResult = resultChannel.receiveAsFlow()

     fun showBiometricPrompt(title: String, description: String) {
        // Reference of biometric manager
        val manager = BiometricManager.from(activity)

        //there are multiple ways to authenticate so here authenticators are used
        //1. BIOMETRIC_STRONG -> finger print and face recognition
        //2. DEVICE_CREDENTIAL -> the user's PIN, pattern, or password.
        val authenticators =
            if (Build.VERSION.SDK_INT >= 30) BIOMETRIC_STRONG or DEVICE_CREDENTIAL else BIOMETRIC_STRONG

        // we can construct the prompt that how it's look like using promptInfo.Builder
        val promptInfo = PromptInfo.Builder()
            .setTitle(title)
            .setDescription(description)
            .setAllowedAuthenticators(authenticators)

        // Checking wheather the device can provide functionallity of authentication or not
        when (manager.canAuthenticate(authenticators)) {
            BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE -> {
                resultChannel.trySend(BiometricResult.HardwareUnavailable)
                return
            }

            BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> {
                resultChannel.trySend(BiometricResult.FeatureUnavailable)
                return
            }

            BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> {
                resultChannel.trySend(BiometricResult.AuthenticationNotSet)
                return
            }

            else -> Unit
        }

        // Actual prompt with callback 
        val prompt = BiometricPrompt(
            activity,
            object : BiometricPrompt.AuthenticationCallback() {
                override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
                    super.onAuthenticationError(errorCode, errString)
                    resultChannel.trySend(BiometricResult.AuthenticationError(errString.toString()))
                }

                override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
                    super.onAuthenticationSucceeded(result)
                    resultChannel.trySend(BiometricResult.AuthenticationSuccess)
                }

                override fun onAuthenticationFailed() {
                    super.onAuthenticationFailed()
                    resultChannel.trySend(BiometricResult.AuthenticationFailed)
                }
            }
        )

        val secretKey = generateSecretKey()
        val cipher = getCipher(secretKey)
        val cryptoObject = BiometricPrompt.CryptoObject(cipher)
        prompt.authenticate(promptInfo.build(), cryptoObject)
    }

    sealed interface BiometricResult {
        data object HardwareUnavailable : BiometricResult
        data object FeatureUnavailable : BiometricResult
        data class AuthenticationError(val error: String) : BiometricResult
        data object AuthenticationFailed : BiometricResult
        data object AuthenticationSuccess : BiometricResult
        data object AuthenticationNotSet : BiometricResult
    }
}

Full Code of Main Activity:

class MainActivity : AppCompatActivity() {

    private val promptManager by lazy {
        BiometricPromptManager(this)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            BiometricAuthTheme {
                // A surface container using the 'background' color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {

                    val biometricResult by promptManager.promptResult.collectAsState(
                        initial = null
                    )

                    val enrollLauncher = rememberLauncherForActivityResult(
                        contract = ActivityResultContracts.StartActivityForResult(),
                        onResult = {
                            println("Activity result: $it")
                        }
                    )

                    LaunchedEffect(biometricResult) {
                        if (biometricResult is BiometricPromptManager.BiometricResult.AuthenticationNotSet) {
                            if (Build.VERSION.SDK_INT >= 30) {
                                val enrollIntent = Intent(Settings.ACTION_BIOMETRIC_ENROLL).apply {
                                    putExtra(
                                        Settings.EXTRA_BIOMETRIC_AUTHENTICATORS_ALLOWED,
                                        BIOMETRIC_STRONG or DEVICE_CREDENTIAL
                                    )
                                }
                                enrollLauncher.launch(enrollIntent)
                            }
                        }
                    }

                    Column(
                        modifier = Modifier
                            .fillMaxSize(),
                        verticalArrangement = Arrangement.Center,
                        horizontalAlignment = Alignment.CenterHorizontally
                    ) {
                        Button(onClick = {
                            promptManager.showBiometricPrompt(
                                title = "Sample prompt",
                                description = "Sample prompt description"
                            )
                        }) {
                            Text(text = "Authenticate")
                        }

                        biometricResult?.let { result ->
                            Text(
                                text = when (result) {
                                    is BiometricPromptManager.BiometricResult.AuthenticationError -> {
                                        result.error
                                    }

                                    BiometricPromptManager.BiometricResult.AuthenticationFailed -> {
                                        "Authentication failed"
                                    }

                                    BiometricPromptManager.BiometricResult.AuthenticationNotSet -> {
                                        "Authentication not set"
                                    }

                                    BiometricPromptManager.BiometricResult.AuthenticationSuccess -> {
                                        "Authentication success"
                                    }

                                    BiometricPromptManager.BiometricResult.FeatureUnavailable -> {
                                        "Feature unavailable"
                                    }

                                    BiometricPromptManager.BiometricResult.HardwareUnavailable -> {
                                        "Hardware unavailable"
                                    }
                                }
                            )
                        }
                    }
                }
            }
        }
    }
}

Full Code of CryptoGraphyUtils:

fun generateSecretKey(): SecretKey {
    val keyGenerator = KeyGenerator.getInstance(
        KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore"
    )
    keyGenerator.init(
        KeyGenParameterSpec.Builder(
            "hafdhkkhsfdhasga",
            KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
        )
            .setBlockModes(KeyProperties.BLOCK_MODE_CBC)
            .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
            .setUserAuthenticationRequired(true)
            .setInvalidatedByBiometricEnrollment(true)
            .build()
    )
    return keyGenerator.generateKey()
}

fun getCipher(secretKey: SecretKey): Cipher {
    val cipher = Cipher.getInstance(
        KeyProperties.KEY_ALGORITHM_AES + "/"
                + KeyProperties.BLOCK_MODE_CBC + "/"
                + KeyProperties.ENCRYPTION_PADDING_PKCS7
    )
    cipher.init(Cipher.ENCRYPT_MODE, secretKey)
    return cipher
}

This is how we can add Biometricauthentication to project.

Source Code:- https://github.com/enochrathod98/Biometric-Auth