diff --git a/app/src/main/java/com/example/biometricpropmpts/MainActivity.kt b/app/src/main/java/com/example/biometricpropmpts/MainActivity.kt index 8d1f0bb..0456a57 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,20 @@ 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 - val TAG = "MyBiometricMain" + private lateinit var keyguardLauncher: ActivityResultLauncher + private val TAG = "MyBiometricApp" + private var isEnrollClicked = false + private val viewModel: MainViewModel by viewModels() - @RequiresApi(Build.VERSION_CODES.R) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() @@ -91,39 +85,60 @@ class MainActivity : FragmentActivity() { } } - @RequiresApi(Build.VERSION_CODES.R) + override fun onRestart() { + super.onRestart() + viewModel.isBiometricEnrolled = isBiometricEnrolled() + } + + 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 -> - val viewModel: MainViewModel by viewModels() + Scaffold( + modifier = Modifier + .fillMaxSize() + .background( + brush = Brush.verticalGradient(listOf(Color(0xFFA87FFB), Color(0xFF7B42F6))) + ), + containerColor = Color.Transparent + ) { innerPadding -> + + enrollLauncher = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + if (result.resultCode == RESULT_OK) + showToastMessage(this, R.string.biometric_enrolled) + else + showToastMessage(this, R.string.enrollment_canceled) + } - enrollLauncher = - rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + 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,96 +155,136 @@ 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 -> { + Log.d(TAG, "App can authenticate using biometrics.") onSuccessful() - Log.d("MY_APP_TAG", "App can authenticate using biometrics.") } 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(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 + startActivity(Intent(Settings.ACTION_SECURITY_SETTINGS)) + Log.e(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..a3df17a 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 = "MyBiometricApp" + 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,161 @@ 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, "Encrypt 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 -> - val decryptedString = String(decrypted, Charset.defaultCharset()) - updateUiState { - copy(decryptedPassword = decryptedString) + 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, "Decrypt Exception Authenticate: $exception") } - Log.d(tag, "Decrypted Password: $decryptedString") - } + } + } ?: run { + 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) { + 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, "Decrypt 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..11b0ab9 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-encrypt + Device doesn\'t have secure lock screen \ No newline at end of file