From cd830269a998a285fe4096343514c82b0fb5834f Mon Sep 17 00:00:00 2001 From: M3DZIK Date: Sun, 29 Oct 2023 14:49:21 +0100 Subject: [PATCH] Check if biometric is available --- .../android/ui/screens/auth/Unlock.kt | 54 +++++---- .../ui/screens/settings/SettingsSecurity.kt | 114 ++++++++++-------- .../librepass/android/utils/Biometric.kt | 50 +++++--- 3 files changed, 124 insertions(+), 94 deletions(-) diff --git a/app/src/main/java/dev/medzik/librepass/android/ui/screens/auth/Unlock.kt b/app/src/main/java/dev/medzik/librepass/android/ui/screens/auth/Unlock.kt index ba5f58f8..db20837b 100644 --- a/app/src/main/java/dev/medzik/librepass/android/ui/screens/auth/Unlock.kt +++ b/app/src/main/java/dev/medzik/librepass/android/ui/screens/auth/Unlock.kt @@ -32,6 +32,7 @@ import dev.medzik.librepass.android.ui.components.TextInputField import dev.medzik.librepass.android.utils.KeyAlias import dev.medzik.librepass.android.utils.SecretStore import dev.medzik.librepass.android.utils.UserSecrets +import dev.medzik.librepass.android.utils.checkIfBiometricAvailable import dev.medzik.librepass.android.utils.showBiometricPrompt import dev.medzik.librepass.client.utils.Cryptography import dev.medzik.librepass.client.utils.Cryptography.computePasswordHash @@ -59,16 +60,18 @@ fun UnlockScreen(navController: NavController) { loading = true // compute base password hash - val passwordHash = computePasswordHash( - password = password, - email = credentials.email, - argon2Function = Argon2( - 32, - credentials.parallelism, - credentials.memory, - credentials.iterations + val passwordHash = + computePasswordHash( + password = password, + email = credentials.email, + argon2Function = + Argon2( + 32, + credentials.parallelism, + credentials.memory, + credentials.iterations + ) ) - ) val publicKey = X25519.publicFromPrivate(passwordHash.hash) @@ -106,11 +109,12 @@ fun UnlockScreen(navController: NavController) { fun showBiometric() { showBiometricPrompt( context = context, - cipher = KeyStore.initForDecryption( - alias = KeyAlias.BiometricPrivateKey, - initializationVector = Hex.decode(credentials.biometricProtectedPrivateKeyIV!!), - deviceAuthentication = true - ), + cipher = + KeyStore.initForDecryption( + alias = KeyAlias.BiometricPrivateKey, + initializationVector = Hex.decode(credentials.biometricProtectedPrivateKeyIV!!), + deviceAuthentication = true + ), onAuthenticationSucceeded = { cipher -> val privateKey = KeyStore.decrypt(cipher, credentials.biometricProtectedPrivateKey!!) @@ -135,7 +139,7 @@ fun UnlockScreen(navController: NavController) { } LaunchedEffect(scope) { - if (credentials.biometricEnabled) showBiometric() + if (credentials.biometricEnabled && checkIfBiometricAvailable(context)) showBiometric() } TextInputField( @@ -150,21 +154,23 @@ fun UnlockScreen(navController: NavController) { loading = loading, onClick = { onUnlock(password) }, enabled = password.isNotEmpty(), - modifier = Modifier - .fillMaxWidth() - .padding(top = 8.dp) - .padding(horizontal = 80.dp) + modifier = + Modifier + .fillMaxWidth() + .padding(top = 8.dp) + .padding(horizontal = 80.dp) ) { Text(stringResource(R.string.Button_Unlock)) } - if (credentials.biometricEnabled) { + if (credentials.biometricEnabled && checkIfBiometricAvailable(context)) { OutlinedButton( onClick = { showBiometric() }, - modifier = Modifier - .fillMaxWidth() - .padding(top = 8.dp) - .padding(horizontal = 80.dp) + modifier = + Modifier + .fillMaxWidth() + .padding(top = 8.dp) + .padding(horizontal = 80.dp) ) { Text(stringResource(R.string.Button_UseBiometric)) } diff --git a/app/src/main/java/dev/medzik/librepass/android/ui/screens/settings/SettingsSecurity.kt b/app/src/main/java/dev/medzik/librepass/android/ui/screens/settings/SettingsSecurity.kt index 626ab30b..3f928627 100644 --- a/app/src/main/java/dev/medzik/librepass/android/ui/screens/settings/SettingsSecurity.kt +++ b/app/src/main/java/dev/medzik/librepass/android/ui/screens/settings/SettingsSecurity.kt @@ -34,6 +34,7 @@ import dev.medzik.librepass.android.utils.SecretStore.readKey import dev.medzik.librepass.android.utils.SecretStore.writeKey import dev.medzik.librepass.android.utils.StoreKey import dev.medzik.librepass.android.utils.VaultTimeoutValues +import dev.medzik.librepass.android.utils.checkIfBiometricAvailable import dev.medzik.librepass.android.utils.showBiometricPrompt import kotlinx.coroutines.launch @@ -68,15 +69,17 @@ fun SettingsSecurityScreen() { showBiometricPrompt( context = context as MainActivity, - cipher = KeyStore.initForEncryption( - KeyAlias.BiometricPrivateKey, - deviceAuthentication = true - ), + cipher = + KeyStore.initForEncryption( + KeyAlias.BiometricPrivateKey, + deviceAuthentication = true + ), onAuthenticationSucceeded = { cipher -> - val encryptedData = KeyStore.encrypt( - cipher = cipher, - clearBytes = Hex.decode(userSecrets.privateKey) - ) + val encryptedData = + KeyStore.encrypt( + cipher = cipher, + clearBytes = Hex.decode(userSecrets.privateKey) + ) biometricEnabled = true @@ -98,55 +101,63 @@ fun SettingsSecurityScreen() { fun getVaultTimeoutTranslation(value: VaultTimeoutValues): String { return when (value) { VaultTimeoutValues.INSTANT -> stringResource(R.string.Settings_Vault_Timeout_Instant) - VaultTimeoutValues.ONE_MINUTE -> pluralStringResource( - R.plurals.Time_Minutes, - 1, - 1 - ) - - VaultTimeoutValues.FIVE_MINUTES -> pluralStringResource( - R.plurals.Time_Minutes, - 5, - 5 - ) - - VaultTimeoutValues.FIFTEEN_MINUTES -> pluralStringResource( - R.plurals.Time_Minutes, - 15, - 15 - ) - - VaultTimeoutValues.THIRTY_MINUTES -> pluralStringResource( - R.plurals.Time_Minutes, - 30, - 30 - ) - - VaultTimeoutValues.ONE_HOUR -> pluralStringResource( - R.plurals.Time_Hours, - 1, - 1 - ) + VaultTimeoutValues.ONE_MINUTE -> + pluralStringResource( + R.plurals.Time_Minutes, + 1, + 1 + ) + + VaultTimeoutValues.FIVE_MINUTES -> + pluralStringResource( + R.plurals.Time_Minutes, + 5, + 5 + ) + + VaultTimeoutValues.FIFTEEN_MINUTES -> + pluralStringResource( + R.plurals.Time_Minutes, + 15, + 15 + ) + + VaultTimeoutValues.THIRTY_MINUTES -> + pluralStringResource( + R.plurals.Time_Minutes, + 30, + 30 + ) + + VaultTimeoutValues.ONE_HOUR -> + pluralStringResource( + R.plurals.Time_Hours, + 1, + 1 + ) VaultTimeoutValues.NEVER -> stringResource(R.string.Settings_Vault_Timeout_Never) } } - SwitcherPreference( - title = stringResource(R.string.Settings_BiometricUnlock), - icon = { Icon(Icons.Default.Fingerprint, contentDescription = null) }, - checked = biometricEnabled, - onCheckedChange = { biometricHandler() } - ) + if (checkIfBiometricAvailable(context)) { + SwitcherPreference( + title = stringResource(R.string.Settings_BiometricUnlock), + icon = { Icon(Icons.Default.Fingerprint, contentDescription = null) }, + checked = biometricEnabled, + onCheckedChange = { biometricHandler() } + ) + } PropertyPreference( title = stringResource(R.string.Settings_Vault_Timeout_Modal_Title), icon = { Icon(Icons.Default.Timer, contentDescription = null) }, - currentValue = getVaultTimeoutTranslation( - VaultTimeoutValues.fromSeconds( - vaultTimeout - ) - ), + currentValue = + getVaultTimeoutTranslation( + VaultTimeoutValues.fromSeconds( + vaultTimeout + ) + ), onClick = { timerDialogState.show() }, ) @@ -161,9 +172,10 @@ fun SettingsSecurityScreen() { ) { Text( text = getVaultTimeoutTranslation(it), - modifier = Modifier - .padding(vertical = 12.dp) - .fillMaxWidth() + modifier = + Modifier + .padding(vertical = 12.dp) + .fillMaxWidth() ) } } diff --git a/app/src/main/java/dev/medzik/librepass/android/utils/Biometric.kt b/app/src/main/java/dev/medzik/librepass/android/utils/Biometric.kt index 7c630da6..8d5932de 100644 --- a/app/src/main/java/dev/medzik/librepass/android/utils/Biometric.kt +++ b/app/src/main/java/dev/medzik/librepass/android/utils/Biometric.kt @@ -1,5 +1,7 @@ package dev.medzik.librepass.android.utils +import android.content.Context +import androidx.biometric.BiometricManager import androidx.biometric.BiometricPrompt import androidx.fragment.app.FragmentActivity import dev.medzik.librepass.android.R @@ -11,25 +13,35 @@ fun showBiometricPrompt( onAuthenticationSucceeded: (Cipher) -> Unit, onAuthenticationFailed: () -> Unit ) { - val promptInfo = BiometricPrompt.PromptInfo.Builder() - .setTitle(context.getString(R.string.BiometricUnlock_Title)) - .setSubtitle(context.getString(R.string.BiometricUnlock_Subtitle)) - .setNegativeButtonText(context.getString(R.string.BiometricUnlock_Button_UsePassword)) - .build() - - val biometricPrompt = BiometricPrompt( - context, - object : BiometricPrompt.AuthenticationCallback() { - override fun onAuthenticationError(errorCode: Int, errString: CharSequence) = - onAuthenticationFailed() - - override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) = - onAuthenticationSucceeded(result.cryptoObject?.cipher!!) - - override fun onAuthenticationFailed() = - onAuthenticationFailed() - } - ) + val promptInfo = + BiometricPrompt.PromptInfo.Builder() + .setTitle(context.getString(R.string.BiometricUnlock_Title)) + .setSubtitle(context.getString(R.string.BiometricUnlock_Subtitle)) + .setNegativeButtonText(context.getString(R.string.BiometricUnlock_Button_UsePassword)) + .build() + + val biometricPrompt = + BiometricPrompt( + context, + object : BiometricPrompt.AuthenticationCallback() { + override fun onAuthenticationError( + errorCode: Int, + errString: CharSequence + ) = onAuthenticationFailed() + + override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) = + onAuthenticationSucceeded(result.cryptoObject?.cipher!!) + + override fun onAuthenticationFailed() = onAuthenticationFailed() + } + ) biometricPrompt.authenticate(promptInfo, BiometricPrompt.CryptoObject(cipher)) } + +fun checkIfBiometricAvailable(context: Context): Boolean { + val status = BiometricManager.from(context).canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK) + + // return true when available + return status == BiometricManager.BIOMETRIC_SUCCESS +}