From f11d7567ce5ddf2549e9550f58602aead54c4da4 Mon Sep 17 00:00:00 2001 From: atina Date: Wed, 2 Jul 2025 17:13:02 +0330 Subject: [PATCH 1/2] Authentication has been done for API below 30 --- .../example/biometricpropmpts/MainActivity.kt | 200 ++++++++++------ .../biometricpropmpts/MainViewModel.kt | 216 +++++++++++++----- app/src/main/res/values/strings.xml | 7 + 3 files changed, 300 insertions(+), 123 deletions(-) diff --git a/app/src/main/java/com/example/biometricpropmpts/MainActivity.kt b/app/src/main/java/com/example/biometricpropmpts/MainActivity.kt index 8d1f0bb..e77dccc 100644 --- a/app/src/main/java/com/example/biometricpropmpts/MainActivity.kt +++ b/app/src/main/java/com/example/biometricpropmpts/MainActivity.kt @@ -1,19 +1,20 @@ package com.example.biometricpropmpts +import android.app.KeyguardManager +import android.content.Context import android.content.Intent import android.os.Build import android.os.Bundle import android.provider.Settings import android.util.Log -import android.view.WindowInsets import android.widget.Toast +import com.example.biometricpropmpts.R import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels -import androidx.annotation.RequiresApi import androidx.biometric.BiometricManager import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL @@ -22,15 +23,12 @@ import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Lock @@ -38,24 +36,20 @@ import androidx.compose.material.icons.filled.Person import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextFieldDefaults import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.PasswordVisualTransformation @@ -67,20 +61,19 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.core.content.ContextCompat -import androidx.core.view.WindowCompat import androidx.fragment.app.FragmentActivity import com.example.biometricpropmpts.ui.theme.BiometricPropmptsTheme import dagger.hilt.android.AndroidEntryPoint import java.nio.charset.Charset import javax.crypto.Cipher -import kotlin.reflect.KFunction1 @AndroidEntryPoint class MainActivity : FragmentActivity() { private lateinit var enrollLauncher: ActivityResultLauncher + private lateinit var keyguardLauncher: ActivityResultLauncher val TAG = "MyBiometricMain" + var isEnrollClicked = false - @RequiresApi(Build.VERSION_CODES.R) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() @@ -91,39 +84,61 @@ class MainActivity : FragmentActivity() { } } - @RequiresApi(Build.VERSION_CODES.R) + private fun isBiometricEnrolled(): Boolean { + val biometricManager = BiometricManager.from(this) + val canAuthenticate = biometricManager.canAuthenticate(BIOMETRIC_STRONG) + return canAuthenticate == BiometricManager.BIOMETRIC_SUCCESS + } + @Composable private fun EnrollBiometric() { - Scaffold( modifier = Modifier - .fillMaxSize() - .background( - brush = Brush.verticalGradient( - listOf(Color(0xFFA87FFB), Color(0xFF7B42F6)) - ) - ), containerColor = Color.Transparent) { innerPadding -> + Scaffold( + modifier = Modifier + .fillMaxSize() + .background( + brush = Brush.verticalGradient(listOf(Color(0xFFA87FFB), Color(0xFF7B42F6))) + ), + containerColor = Color.Transparent + ) { innerPadding -> + val viewModel: MainViewModel by viewModels() + viewModel.isBiometricEnrolled = isBiometricEnrolled() enrollLauncher = + rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + if (result.resultCode == RESULT_OK) + showToastMessage(this, R.string.biometric_enrolled) + else + showToastMessage(this, R.string.enrollment_canceled) + + } + + keyguardLauncher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> if (result.resultCode == RESULT_OK) { - Toast.makeText(this, "Biometric enrolled!", Toast.LENGTH_SHORT).show() - } else { - Toast.makeText(this, "Enrollment canceled.", Toast.LENGTH_SHORT).show() - } + if (isEnrollClicked) { + viewModel.encrypt(null) + isEnrollClicked = false + } else + viewModel.decrypt(null) + + } else + showToastMessage(this, R.string.enrollment_canceled) } StylishLoginScreen( modifier = Modifier.padding(innerPadding), viewModel = viewModel, onEnrollClick = { - checkBiometricAvailability(onSuccessful = { - viewModel.encrypt(::authenticate) - }) + isEnrollClicked = true + checkBiometricAvailability( + onSuccessful = { viewModel.encrypt(::authenticate) } + ) }, onShowDecryptionClick = { - checkBiometricAvailability(onSuccessful = { - viewModel.decrypt(::authenticate) - }) + checkBiometricAvailability( + onSuccessful = { viewModel.decrypt(::authenticate) } + ) } ) } @@ -140,59 +155,61 @@ class MainActivity : FragmentActivity() { val biometricPrompt = BiometricPrompt( this, executor, object : BiometricPrompt.AuthenticationCallback() { + override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { super.onAuthenticationError(errorCode, errString) - Toast.makeText( - applicationContext, - "Authentication error: $errString", Toast.LENGTH_SHORT - ).show() + showToastMessage(this@MainActivity, R.string.failed_authentication, errString.toString()) } override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { super.onAuthenticationSucceeded(result) onSucceedEncrypt?.let { val encryptedPassword: ByteArray? = - result.cryptoObject?.cipher?.doFinal( - uiState.password.text.toByteArray( - Charset.defaultCharset() - ) - ) - - Toast.makeText( - applicationContext, - "Authentication Succeed", - Toast.LENGTH_SHORT - ) - .show() + result.cryptoObject?.cipher?.doFinal(uiState.password.text.toByteArray(Charset.defaultCharset())) + + showToastMessage(this@MainActivity, R.string.succeed_authentication) if (encryptedPassword != null) onSucceedEncrypt(encryptedPassword) } - onSucceedDecrypt?.let { - onSucceedDecrypt(result) - } + + onSucceedDecrypt?.let { onSucceedDecrypt(result) } } - }) + } + + ) + + val promptInfo = BiometricPrompt.PromptInfo.Builder() .setTitle("Biometric login for my app") .setSubtitle("Log in using your biometric credential") - .setAllowedAuthenticators(BIOMETRIC_STRONG or DEVICE_CREDENTIAL) - .build() + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) + promptInfo.setAllowedAuthenticators(BIOMETRIC_STRONG or DEVICE_CREDENTIAL) + + else + promptInfo.setNegativeButtonText("Cancel") // Mandatory for API < 30 + biometricPrompt.authenticate( - promptInfo, + promptInfo.build(), BiometricPrompt.CryptoObject(cipher) ) } - @RequiresApi(Build.VERSION_CODES.R) - fun checkBiometricAvailability(onSuccessful: () -> Unit) { + private fun checkBiometricAvailability(onSuccessful: () -> Unit) { val biometricManager = BiometricManager.from(this) - when (biometricManager.canAuthenticate(BIOMETRIC_STRONG or DEVICE_CREDENTIAL)) { + val result = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) + biometricManager.canAuthenticate(BIOMETRIC_STRONG or DEVICE_CREDENTIAL) + else + biometricManager.canAuthenticate() // ⚠️ Deprecated, but correct for API < 30 + + + when (result) { BiometricManager.BIOMETRIC_SUCCESS -> { - onSuccessful() Log.d("MY_APP_TAG", "App can authenticate using biometrics.") + onSuccessful() } BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> @@ -201,35 +218,73 @@ class MainActivity : FragmentActivity() { BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE -> Log.e("MY_APP_TAG", "Biometric features are currently unavailable.") + BiometricManager.BIOMETRIC_ERROR_UNSUPPORTED -> { + Log.e("MY_APP_TAG", "Biometric features are incompatible with the current Android version.") + showDeviceCredentialPrompt() + } + BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> { // Prompts the user to create credentials that your app accepts. - val enrollIntent = Intent(Settings.ACTION_BIOMETRIC_ENROLL).apply { - putExtra( - Settings.EXTRA_BIOMETRIC_AUTHENTICATORS_ALLOWED, - BIOMETRIC_STRONG or DEVICE_CREDENTIAL - ) - } - enrollLauncher.launch(enrollIntent) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + val enrollIntent = Intent(Settings.ACTION_BIOMETRIC_ENROLL).apply { + putExtra( + Settings.EXTRA_BIOMETRIC_AUTHENTICATORS_ALLOWED, + BIOMETRIC_STRONG or DEVICE_CREDENTIAL + ) + } + + enrollLauncher.launch(enrollIntent) + } else + showDeviceCredentialPrompt() } + } + } + + @Suppress("DEPRECATION") + // KeyguardManager.createConfirmDeviceCredentialIntent(...) was deprecated in API 33 because Google now prefers BiometricPrompt, and + // there is no replacement for this on API 23–29 + private fun showDeviceCredentialPrompt() { + val keyguardManager = getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager + val intent = keyguardManager.createConfirmDeviceCredentialIntent( + "Authentication Required", + "Use your screen lock to continue" + ) + intent?.let { + keyguardLauncher.launch(it) + } ?: run { + // Device doesn't have secure lock screen + showToastMessage(this@MainActivity, R.string.no_lockScreen) + Log.e("MY_APP_TAG", "Device doesn't have secure lock screen") } } } + @Composable fun StylishLoginScreen( modifier: Modifier = Modifier, viewModel: MainViewModel, onEnrollClick: () -> Unit, - onShowDecryptionClick: () -> Unit + onShowDecryptionClick: () -> Unit, ) { val uiState = viewModel.uiState.collectAsState() - Log.d("StylishLoginScreen: ","Recompose") + val context = LocalContext.current + + Log.d("StylishLoginScreen: ", "Recompose") Box( modifier = modifier - .fillMaxSize().padding(16.dp), + .fillMaxSize() + .padding(16.dp), contentAlignment = Alignment.Center ) { + + LaunchedEffect(Unit) { + viewModel.feedback.collect { messageResId -> + showToastMessage(context, messageResId) + } + } + Column( modifier = Modifier .fillMaxWidth() @@ -320,7 +375,7 @@ fun StylishLoginScreen( } @Composable -private fun DecryptedPassword(uiState: LoginUIState) { +fun DecryptedPassword(uiState: LoginUIState) { Text( text = "Decrypted Password:", color = Color(0xFFFFFFFF), @@ -344,7 +399,7 @@ private fun DecryptedPassword(uiState: LoginUIState) { } @Composable -private fun StyledButton(text: String, painterId: Int, colorId: Long, onClick: () -> Unit) { +fun StyledButton(text: String, painterId: Int, colorId: Long, onClick: () -> Unit) { Button( onClick = onClick, modifier = Modifier @@ -413,6 +468,9 @@ fun TextFieldWithIcon( } } +private fun showToastMessage(context: Context, messageResId: Int, extraMessage: String? = null) { + Toast.makeText(context, context.getString(messageResId, extraMessage), Toast.LENGTH_SHORT).show() +} @Preview(showBackground = true) @Composable diff --git a/app/src/main/java/com/example/biometricpropmpts/MainViewModel.kt b/app/src/main/java/com/example/biometricpropmpts/MainViewModel.kt index 0b5ff25..0dd5145 100644 --- a/app/src/main/java/com/example/biometricpropmpts/MainViewModel.kt +++ b/app/src/main/java/com/example/biometricpropmpts/MainViewModel.kt @@ -2,9 +2,10 @@ package com.example.biometricpropmpts import android.os.Build import android.security.keystore.KeyGenParameterSpec +import android.security.keystore.KeyPermanentlyInvalidatedException import android.security.keystore.KeyProperties +import android.security.keystore.UserNotAuthenticatedException import android.util.Log -import androidx.annotation.RequiresApi import androidx.biometric.BiometricPrompt import androidx.compose.ui.text.input.TextFieldValue import androidx.lifecycle.ViewModel @@ -12,25 +13,27 @@ import androidx.lifecycle.viewModelScope import com.example.biometricpropmpts.data.repository.UserPreferencesRepository import com.google.protobuf.ByteString import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import java.nio.charset.Charset +import java.security.InvalidKeyException import java.security.KeyStore import javax.crypto.Cipher import javax.crypto.KeyGenerator import javax.crypto.SecretKey import javax.crypto.spec.GCMParameterSpec -import javax.crypto.spec.IvParameterSpec import javax.inject.Inject data class LoginUIState( val username: TextFieldValue = TextFieldValue(""), val password: TextFieldValue = TextFieldValue(""), - val decryptedPassword: String = "" + val decryptedPassword: String = "", ) @HiltViewModel @@ -38,29 +41,39 @@ class MainViewModel @Inject constructor(private val userPreferencesRepository: U ViewModel() { private val KEY_PROVIDER = "AndroidKeyStore" private val KEY_ALIAS = "biometricSecretKey" - val pass = "ThisIsTheBioPass" + private val pass = "ThisIsTheBioPass" + private val tag = "MainViewModel" + var isBiometricEnrolled = false + private val _uiState = MutableStateFlow(LoginUIState()) val uiState: StateFlow = _uiState.asStateFlow() - private val tag = "MainViewModel" - @RequiresApi(Build.VERSION_CODES.R) - private val keyGenParameterSpec = KeyGenParameterSpec.Builder( - KEY_ALIAS, - KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT - ).setBlockModes(KeyProperties.BLOCK_MODE_GCM) - .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) - .setUserAuthenticationRequired(true) - .setInvalidatedByBiometricEnrollment(true) - .setUserAuthenticationParameters( - 0, - KeyProperties.AUTH_BIOMETRIC_STRONG or KeyProperties.AUTH_DEVICE_CREDENTIAL - ) - .build() + private val _feedback = MutableSharedFlow() + val feedback = _feedback.asSharedFlow() + + + private fun keyGenParameterSpec(): KeyGenParameterSpec = + KeyGenParameterSpec.Builder( + KEY_ALIAS, + KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT + ).apply { + setBlockModes(KeyProperties.BLOCK_MODE_GCM) + setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) + setUserAuthenticationRequired(true) + setInvalidatedByBiometricEnrollment(true) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) + setUserAuthenticationParameters( + 0, + KeyProperties.AUTH_BIOMETRIC_STRONG or KeyProperties.AUTH_DEVICE_CREDENTIAL + ) + else if (!isBiometricEnrolled) + setUserAuthenticationValidityDurationSeconds(5) // Required for pre-API 30 support + + }.build() private fun generateSecretKey(keyGenParameterSpec: KeyGenParameterSpec) { - val keyGenerator = KeyGenerator.getInstance( - KeyProperties.KEY_ALGORITHM_AES, KEY_PROVIDER - ) + val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, KEY_PROVIDER) keyGenerator.init(keyGenParameterSpec) keyGenerator.generateKey() @@ -92,58 +105,157 @@ class MainViewModel @Inject constructor(private val userPreferencesRepository: U } } - @RequiresApi(Build.VERSION_CODES.R) - fun encrypt(authenticate: (LoginUIState, Cipher, ((encryptedPassword: ByteArray) -> Unit)?, ((decryptedPassword: BiometricPrompt.AuthenticationResult) -> Unit)?) -> Unit) { - if (getSecretKey() == null) - generateSecretKey(keyGenParameterSpec = keyGenParameterSpec) - val cipher = getCipher() - getSecretKey()?.let { secretKey -> - cipher.init(Cipher.ENCRYPT_MODE, secretKey) - authenticate(uiState.value, cipher, { encryptedPassword -> + private fun ensureEncryptionValidSecretKey(cipher: Cipher) { + try { + val key = getSecretKey() + cipher.init(Cipher.ENCRYPT_MODE, key) + } catch (exception: Exception) { + when (exception) { + is KeyPermanentlyInvalidatedException, is UserNotAuthenticatedException, is InvalidKeyException -> { + deleteKey() + generateSecretKey(keyGenParameterSpec()) + cipher.init(Cipher.ENCRYPT_MODE, getSecretKey()) + } + + else -> { + updateFeedback(R.string.something_wrong) + Log.e(tag, "Exception: $exception") + throw exception + } + } + } + } + + fun encrypt( + authenticate: (( + LoginUIState, + Cipher, + ((encryptedPassword: ByteArray) -> Unit)?, + ((decryptedPassword: BiometricPrompt.AuthenticationResult) -> Unit)? + ) -> Unit)? + ) { + try { + if (getSecretKey() == null) + generateSecretKey(keyGenParameterSpec = keyGenParameterSpec()) + val cipher = getCipher() + ensureEncryptionValidSecretKey(cipher) + authenticate?.let { + authenticate( + uiState.value, + cipher, + { encryptedPassword -> + login( + uiState.value.username.text.toByteArray(Charset.defaultCharset()), + encryptedPassword, + cipher.iv + ) + }, + null + ) + } ?: run { + val encryptedPassword = + cipher.doFinal(uiState.value.password.text.toByteArray(Charset.defaultCharset())) login( - uiState.value.username.text.toByteArray( - Charset.defaultCharset() - ), encryptedPassword, + uiState.value.username.text.toByteArray(Charset.defaultCharset()), + encryptedPassword, cipher.iv ) - }, null) + } + } catch (exception: Exception) { + Log.e(tag, "Exception: $exception") } } - - @RequiresApi(Build.VERSION_CODES.R) - fun decrypt(authenticate: (LoginUIState, Cipher, ((encryptedPassword: ByteArray) -> Unit)?, ((decryptedPassword: BiometricPrompt.AuthenticationResult) -> Unit)?) -> Unit) { + fun decrypt( + authenticate: (( + LoginUIState, + Cipher, + ((encryptedPassword: ByteArray) -> Unit)?, + ((decryptedPassword: BiometricPrompt.AuthenticationResult) -> Unit)? + ) -> Unit)? + ) { if (getSecretKey() == null) - generateSecretKey(keyGenParameterSpec) + generateSecretKey(keyGenParameterSpec()) val cipher = getCipher() getSecretKey()?.let { secretKey -> viewModelScope.launch { - try{ - val data = userPreferencesRepository.userPreferencesFlow.first() - cipher.init(Cipher.DECRYPT_MODE, secretKey, data.iv.toByteArray()?.let { GCMParameterSpec(128,it) }) - authenticate( - uiState.value, - cipher, - null - ) { result: BiometricPrompt.AuthenticationResult -> - - val decryptedPassword = data.password.toByteArray() - result.cryptoObject?.cipher?.doFinal(decryptedPassword)?.let { decrypted -> + try { + val data = userPreferencesRepository.userPreferencesFlow.first() + val decryptedPassword = data.password.toByteArray() + + cipher.init( + Cipher.DECRYPT_MODE, + secretKey, + data.iv.toByteArray()?.let { GCMParameterSpec(128, it) }) + + authenticate?.let { authenticate -> + authenticate( + uiState.value, + cipher, + null, + ) { result: BiometricPrompt.AuthenticationResult -> + + try { + result.cryptoObject?.cipher?.doFinal(decryptedPassword) + ?.let { decrypted -> + val decryptedString = + String(decrypted, Charset.defaultCharset()) + updateUiState { copy(decryptedPassword = decryptedString) } + } + } catch (exception: Exception) { + Log.e(tag, "Exception: $exception") + } + + } + } ?: run { + cipher.doFinal(decryptedPassword)?.let { decrypted -> val decryptedString = String(decrypted, Charset.defaultCharset()) updateUiState { copy(decryptedPassword = decryptedString) } - Log.d(tag, "Decrypted Password: $decryptedString") + } + } + } catch (exception: Exception) { + when (exception) { + is KeyPermanentlyInvalidatedException, is UserNotAuthenticatedException -> { + updateFeedback(R.string.key_mismatch) + deleteKey() + /* When the user tries to decrypt with another key + * We must delete the key. + * Also delete the stored encrypted data (since it’s now useless) + * Prompt the user to create new credentials + */ } - + else -> { + updateFeedback(R.string.something_wrong) + Log.e(tag, "Exception: $exception") + } } - }catch (exception: Exception){ - Log.d(tag, "Exception: $exception") } } } } + private fun deleteKey() { + try { + val keyStore = KeyStore.getInstance(KEY_PROVIDER).apply { load(null) } + + if (keyStore.containsAlias(KEY_ALIAS)) + keyStore.deleteEntry(KEY_ALIAS) + else + Log.w(tag, "Key '$KEY_ALIAS' not found in Keystore.") + + } catch (e: Exception) { + Log.e(tag, "Failed to delete key '$KEY_ALIAS': ${e.message}") + } + } + + + private fun updateFeedback(messageResId: Int) { + viewModelScope.launch { + _feedback.emit(messageResId) + } + } } \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 55a5ded..2a3f1a2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2,4 +2,11 @@ BiometricPropmpts Password Username + Biometric enrolled! + Enrollment canceled + Authentication Succeed + Authentication error: %s + Something wend wrong, Please try again + The data cannot be decrypted with a different key; please re-enroll + Device doesn\'t have secure lock screen \ No newline at end of file From 7822524f22fbb76fc1e1fea454823a1f018533c3 Mon Sep 17 00:00:00 2001 From: atina Date: Mon, 21 Jul 2025 12:41:58 +0330 Subject: [PATCH 2/2] The problem with the authentication prompt not appearing after switching methods without closing the application has been fixed. If the user has not enrolled an authentication method, send them to settings. --- .../example/biometricpropmpts/MainActivity.kt | 32 +++++++++---------- .../biometricpropmpts/MainViewModel.kt | 26 ++++++++------- app/src/main/res/values/strings.xml | 2 +- 3 files changed, 32 insertions(+), 28 deletions(-) diff --git a/app/src/main/java/com/example/biometricpropmpts/MainActivity.kt b/app/src/main/java/com/example/biometricpropmpts/MainActivity.kt index e77dccc..0456a57 100644 --- a/app/src/main/java/com/example/biometricpropmpts/MainActivity.kt +++ b/app/src/main/java/com/example/biometricpropmpts/MainActivity.kt @@ -71,8 +71,9 @@ import javax.crypto.Cipher class MainActivity : FragmentActivity() { private lateinit var enrollLauncher: ActivityResultLauncher private lateinit var keyguardLauncher: ActivityResultLauncher - val TAG = "MyBiometricMain" - var isEnrollClicked = false + private val TAG = "MyBiometricApp" + private var isEnrollClicked = false + private val viewModel: MainViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -84,6 +85,11 @@ class MainActivity : FragmentActivity() { } } + override fun onRestart() { + super.onRestart() + viewModel.isBiometricEnrolled = isBiometricEnrolled() + } + private fun isBiometricEnrolled(): Boolean { val biometricManager = BiometricManager.from(this) val canAuthenticate = biometricManager.canAuthenticate(BIOMETRIC_STRONG) @@ -101,20 +107,14 @@ class MainActivity : FragmentActivity() { containerColor = Color.Transparent ) { innerPadding -> - val viewModel: MainViewModel by viewModels() - viewModel.isBiometricEnrolled = isBiometricEnrolled() - - enrollLauncher = - rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + enrollLauncher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> if (result.resultCode == RESULT_OK) showToastMessage(this, R.string.biometric_enrolled) else showToastMessage(this, R.string.enrollment_canceled) - } - keyguardLauncher = - rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + keyguardLauncher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> if (result.resultCode == RESULT_OK) { if (isEnrollClicked) { viewModel.encrypt(null) @@ -208,18 +208,18 @@ class MainActivity : FragmentActivity() { when (result) { BiometricManager.BIOMETRIC_SUCCESS -> { - Log.d("MY_APP_TAG", "App can authenticate using biometrics.") + Log.d(TAG, "App can authenticate using biometrics.") onSuccessful() } BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> - Log.e("MY_APP_TAG", "No biometric features available on this device.") + Log.e(TAG, "No biometric features available on this device.") BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE -> - Log.e("MY_APP_TAG", "Biometric features are currently unavailable.") + Log.e(TAG, "Biometric features are currently unavailable.") BiometricManager.BIOMETRIC_ERROR_UNSUPPORTED -> { - Log.e("MY_APP_TAG", "Biometric features are incompatible with the current Android version.") + Log.e(TAG, "Biometric features are incompatible with the current Android version.") showDeviceCredentialPrompt() } @@ -254,8 +254,8 @@ class MainActivity : FragmentActivity() { keyguardLauncher.launch(it) } ?: run { // Device doesn't have secure lock screen - showToastMessage(this@MainActivity, R.string.no_lockScreen) - Log.e("MY_APP_TAG", "Device doesn't have secure lock screen") + startActivity(Intent(Settings.ACTION_SECURITY_SETTINGS)) + Log.e(TAG, "Device doesn't have secure lock screen") } } } diff --git a/app/src/main/java/com/example/biometricpropmpts/MainViewModel.kt b/app/src/main/java/com/example/biometricpropmpts/MainViewModel.kt index 0dd5145..a3df17a 100644 --- a/app/src/main/java/com/example/biometricpropmpts/MainViewModel.kt +++ b/app/src/main/java/com/example/biometricpropmpts/MainViewModel.kt @@ -42,7 +42,7 @@ class MainViewModel @Inject constructor(private val userPreferencesRepository: U private val KEY_PROVIDER = "AndroidKeyStore" private val KEY_ALIAS = "biometricSecretKey" private val pass = "ThisIsTheBioPass" - private val tag = "MainViewModel" + private val TAG = "MyBiometricApp" var isBiometricEnrolled = false private val _uiState = MutableStateFlow(LoginUIState()) @@ -119,7 +119,7 @@ class MainViewModel @Inject constructor(private val userPreferencesRepository: U else -> { updateFeedback(R.string.something_wrong) - Log.e(tag, "Exception: $exception") + Log.e(TAG, "Exception: $exception") throw exception } } @@ -162,7 +162,7 @@ class MainViewModel @Inject constructor(private val userPreferencesRepository: U ) } } catch (exception: Exception) { - Log.e(tag, "Exception: $exception") + Log.e(TAG, "Encrypt Exception: $exception") } } @@ -203,16 +203,20 @@ class MainViewModel @Inject constructor(private val userPreferencesRepository: U updateUiState { copy(decryptedPassword = decryptedString) } } } catch (exception: Exception) { - Log.e(tag, "Exception: $exception") + Log.e(TAG, "Decrypt Exception Authenticate: $exception") } } } ?: run { - cipher.doFinal(decryptedPassword)?.let { decrypted -> - val decryptedString = String(decrypted, Charset.defaultCharset()) - updateUiState { - copy(decryptedPassword = decryptedString) + try { + cipher.doFinal(decryptedPassword)?.let { decrypted -> + val decryptedString = String(decrypted, Charset.defaultCharset()) + updateUiState { + copy(decryptedPassword = decryptedString) + } } + } catch (exception: Exception) { + Log.e(TAG, "Decrypt Exception Run Block: $exception") } } } catch (exception: Exception) { @@ -229,7 +233,7 @@ class MainViewModel @Inject constructor(private val userPreferencesRepository: U else -> { updateFeedback(R.string.something_wrong) - Log.e(tag, "Exception: $exception") + Log.e(TAG, "Decrypt Exception: $exception") } } } @@ -245,10 +249,10 @@ class MainViewModel @Inject constructor(private val userPreferencesRepository: U if (keyStore.containsAlias(KEY_ALIAS)) keyStore.deleteEntry(KEY_ALIAS) else - Log.w(tag, "Key '$KEY_ALIAS' not found in Keystore.") + Log.w(TAG, "Key '$KEY_ALIAS' not found in Keystore.") } catch (e: Exception) { - Log.e(tag, "Failed to delete key '$KEY_ALIAS': ${e.message}") + Log.e(TAG, "Failed to delete key '$KEY_ALIAS': ${e.message}") } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2a3f1a2..11b0ab9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -7,6 +7,6 @@ Authentication Succeed Authentication error: %s Something wend wrong, Please try again - The data cannot be decrypted with a different key; please re-enroll + The data cannot be decrypted with a different key; please re-encrypt Device doesn\'t have secure lock screen \ No newline at end of file