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
Subscribe to my newsletter
Read articles from Enoch Rathod directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Enoch Rathod
Enoch Rathod
๐ Android Developer | Crafting Code & Connections | Java ๐ Kotlin ๐ Compose| Passionate about innovation & collaboration | Let's connect & learn together!