Fix(encryption): Resolve startup password unlock issues
- Fixed `Unresolved reference` error for startup password dialog variables by correctly declaring them in `AppShell.kt`. - Added missing German translations for encryption-related strings in `strings.xml`. - Corrected `onUnlockEncryption` logic in `AppShell.kt` to properly handle password-based decryption, addressing the "Unlock failed" error. - Ensured the app closes when the `StartupPasswordDialog` is cancelled in `AppDialogs.kt`. - Prevented automatic biometric binding when setting an encryption password in `EncryptionPasswordDialog.kt`, allowing users to choose password-only encryption. - Adjusted biometric prompt behavior in `AppShell.kt` to allow multiple fingerprint attempts before falling back to the password dialog.
This commit is contained in:
@@ -78,6 +78,8 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.FileOutputStream
|
||||
import javax.crypto.SecretKey
|
||||
import android.util.Base64
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -95,6 +97,10 @@ fun AppShell(
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
var showStartupPasswordDialog by rememberSaveable { mutableStateOf(false) }
|
||||
var startupUnlockErrorMessage by rememberSaveable { mutableStateOf<String?>(null) }
|
||||
|
||||
var syncFolderUriString by rememberSaveable {
|
||||
mutableStateOf(
|
||||
sharedPrefs.getString(
|
||||
@@ -518,44 +524,46 @@ fun AppShell(
|
||||
Lifecycle.Event.ON_START -> {
|
||||
val isBiometricUnlockEnabled =
|
||||
sharedPrefs.getBoolean("biometric_unlock_enabled", false)
|
||||
if (isEncryptionEnabled && hasEncryptionPassword && isBiometricUnlockEnabled && !isDecryptionAttempted) {
|
||||
scope.launch {
|
||||
try {
|
||||
val cipher = keyManager.getDecryptionCipher()
|
||||
if (cipher != null) {
|
||||
val crypto =
|
||||
androidx.biometric.BiometricPrompt.CryptoObject(cipher)
|
||||
val activity = context.findActivity() as FragmentActivity
|
||||
biometricAuthenticator.promptBiometricAuth(
|
||||
title = context.getString(R.string.unlock_app),
|
||||
subtitle = context.getString(R.string.confirm_to_unlock),
|
||||
fragmentActivity = activity,
|
||||
crypto = crypto,
|
||||
onSuccess = { result ->
|
||||
result.cryptoObject?.cipher?.let { authenticatedCipher ->
|
||||
scope.launch {
|
||||
secretKey =
|
||||
keyManager.getSecretKeyFromAuthenticatedCipher(
|
||||
authenticatedCipher
|
||||
)
|
||||
isDecryptionAttempted = true
|
||||
if (isEncryptionEnabled && hasEncryptionPassword && !isDecryptionAttempted) {
|
||||
if (isBiometricUnlockEnabled) {
|
||||
scope.launch {
|
||||
try {
|
||||
val cipher = keyManager.getDecryptionCipher()
|
||||
if (cipher != null) {
|
||||
val crypto =
|
||||
androidx.biometric.BiometricPrompt.CryptoObject(cipher)
|
||||
val activity = context.findActivity() as FragmentActivity
|
||||
biometricAuthenticator.promptBiometricAuth(
|
||||
title = context.getString(R.string.unlock_app),
|
||||
subtitle = context.getString(R.string.confirm_to_unlock),
|
||||
fragmentActivity = activity,
|
||||
crypto = crypto,
|
||||
onSuccess = { result ->
|
||||
result.cryptoObject?.cipher?.let { authenticatedCipher ->
|
||||
scope.launch {
|
||||
secretKey =
|
||||
keyManager.getSecretKeyFromAuthenticatedCipher(
|
||||
authenticatedCipher
|
||||
)
|
||||
isDecryptionAttempted = true
|
||||
}
|
||||
}
|
||||
},
|
||||
onFailed = {},
|
||||
onError = { _, _ ->
|
||||
showStartupPasswordDialog = true
|
||||
}
|
||||
},
|
||||
onFailed = {
|
||||
isDecryptionAttempted = true
|
||||
}, // Proceed without key
|
||||
onError = { _, _ ->
|
||||
isDecryptionAttempted = true
|
||||
} // Proceed without key
|
||||
)
|
||||
} else {
|
||||
isDecryptionAttempted = true // No key/cipher, proceed
|
||||
)
|
||||
} else {
|
||||
isDecryptionAttempted = true // No key/cipher, proceed
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("AppShell", "Error during decryption prompt: ${e.message}")
|
||||
isDecryptionAttempted = true // Proceed without key
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("AppShell", "Error during decryption prompt: ${e.message}")
|
||||
isDecryptionAttempted = true // Proceed without key
|
||||
}
|
||||
} else {
|
||||
showStartupPasswordDialog = true
|
||||
}
|
||||
} else {
|
||||
isDecryptionAttempted = true // No encryption, proceed
|
||||
@@ -654,6 +662,9 @@ fun AppShell(
|
||||
var itemToUnlockId by rememberSaveable { mutableStateOf<Int?>(null) }
|
||||
var itemToUnlockType by rememberSaveable { mutableStateOf<Screen?>(null) }
|
||||
|
||||
|
||||
|
||||
|
||||
var showChooseLockMethodDialog by rememberSaveable { mutableStateOf(false) }
|
||||
var itemToLockId: Int? by rememberSaveable { mutableStateOf(null) }
|
||||
var itemToLockType: LockableItemType? by rememberSaveable { mutableStateOf(null) }
|
||||
@@ -1310,5 +1321,46 @@ fun AppShell(
|
||||
},
|
||||
canUseBiometrics = canUseBiometrics,
|
||||
itemToLockType = itemToLockType,
|
||||
showStartupPasswordDialog = showStartupPasswordDialog,
|
||||
onShowStartupPasswordDialogChange = { showStartupPasswordDialog = it },
|
||||
onUnlockEncryption = { password ->
|
||||
scope.launch {
|
||||
try {
|
||||
val decryptionCipher = keyManager.getDecryptionCipher()
|
||||
if (decryptionCipher == null) {
|
||||
startupUnlockErrorMessage = context.getString(R.string.unlock_failed)
|
||||
return@launch
|
||||
}
|
||||
|
||||
val sharedPrefs = context.getSharedPreferences("encryption_prefs", Context.MODE_PRIVATE)
|
||||
val encryptedDerivedKeyString = sharedPrefs.getString("encrypted_derived_key", null)
|
||||
if (encryptedDerivedKeyString == null) {
|
||||
startupUnlockErrorMessage = context.getString(R.string.unlock_failed)
|
||||
return@launch
|
||||
}
|
||||
val encryptedDerivedKey = Base64.decode(encryptedDerivedKeyString, Base64.DEFAULT)
|
||||
|
||||
val decryptedStoredKeyBytes = decryptionCipher.doFinal(encryptedDerivedKey)
|
||||
|
||||
val currentPbeKey = keyManager.derivePbeKey(password.toCharArray())
|
||||
|
||||
Log.d("AppShell_Unlock", "Decrypted Stored Key: ${decryptedStoredKeyBytes.joinToString()}")
|
||||
Log.d("AppShell_Unlock", "Current PBE Key: ${currentPbeKey.encoded.joinToString()}")
|
||||
|
||||
if (decryptedStoredKeyBytes.contentEquals(currentPbeKey.encoded)) {
|
||||
secretKey = SecretKeySpec(decryptedStoredKeyBytes, "AES")
|
||||
isDecryptionAttempted = true
|
||||
showStartupPasswordDialog = false
|
||||
startupUnlockErrorMessage = null
|
||||
} else {
|
||||
startupUnlockErrorMessage = context.getString(R.string.incorrect_password)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("AppShell", "Failed to unlock encryption with password", e)
|
||||
startupUnlockErrorMessage = context.getString(R.string.unlock_failed)
|
||||
}
|
||||
}
|
||||
},
|
||||
startupUnlockErrorMessage = startupUnlockErrorMessage
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -97,7 +97,7 @@ fun EncryptionPasswordDialog(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
try {
|
||||
val useBiometrics = biometricAuthenticator.isBiometricAuthAvailable()
|
||||
val useBiometrics = false
|
||||
val pbeKey = keyManager.derivePbeKey(password.toCharArray())
|
||||
|
||||
val finalTask = { authorizedCipher: javax.crypto.Cipher ->
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
package de.lxtools.noteshop.ui
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Visibility
|
||||
import androidx.compose.material.icons.filled.VisibilityOff
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.unit.dp
|
||||
import de.lxtools.noteshop.R
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun StartupPasswordDialog(
|
||||
onDismiss: () -> Unit,
|
||||
onUnlock: (String) -> Unit,
|
||||
errorMessage: String? = null
|
||||
) {
|
||||
var password by remember { mutableStateOf("") }
|
||||
var showPassword by remember { mutableStateOf(false) }
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text(text = stringResource(R.string.unlock_encryption)) },
|
||||
text = {
|
||||
Column {
|
||||
Text(text = stringResource(R.string.enter_password_to_unlock_encryption))
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
OutlinedTextField(
|
||||
value = password,
|
||||
onValueChange = { password = it },
|
||||
label = { Text(stringResource(R.string.password)) },
|
||||
visualTransformation = if (showPassword) VisualTransformation.None else PasswordVisualTransformation(),
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
|
||||
trailingIcon = {
|
||||
val image = if (showPassword) Icons.Filled.Visibility else Icons.Filled.VisibilityOff
|
||||
val description = if (showPassword) "Hide password" else "Show password"
|
||||
IconButton(onClick = { showPassword = !showPassword }) {
|
||||
Icon(imageVector = image, description)
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
errorMessage?.let {
|
||||
Text(text = it, color = androidx.compose.material3.MaterialTheme.colorScheme.error)
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
Button(
|
||||
onClick = { onUnlock(password) },
|
||||
enabled = password.isNotBlank()
|
||||
) {
|
||||
Text(stringResource(R.string.unlock))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text(stringResource(R.string.cancel))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -30,7 +30,10 @@ import de.lxtools.noteshop.ui.ChooseLockMethodDialog
|
||||
import de.lxtools.noteshop.ui.LockableItemType
|
||||
import de.lxtools.noteshop.ui.LockMethod
|
||||
import de.lxtools.noteshop.ui.shopping.ShoppingListsViewModel
|
||||
import de.lxtools.noteshop.ui.StartupPasswordDialog
|
||||
import kotlinx.coroutines.launch
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import de.lxtools.noteshop.findActivity
|
||||
|
||||
@Composable
|
||||
fun AppDialogs(
|
||||
@@ -99,6 +102,10 @@ fun AppDialogs(
|
||||
onConfirmLockMethod: (LockMethod) -> Unit,
|
||||
canUseBiometrics: Boolean,
|
||||
itemToLockType: de.lxtools.noteshop.ui.LockableItemType?,
|
||||
showStartupPasswordDialog: Boolean,
|
||||
onShowStartupPasswordDialogChange: (Boolean) -> Unit,
|
||||
onUnlockEncryption: (String) -> Unit,
|
||||
startupUnlockErrorMessage: String?
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val dynamicRecipeStrings = getDynamicRecipeStrings(recipesTitle)
|
||||
@@ -280,4 +287,16 @@ fun AppDialogs(
|
||||
canUseBiometrics = canUseBiometrics
|
||||
)
|
||||
}
|
||||
|
||||
if (showStartupPasswordDialog) {
|
||||
val context = LocalContext.current
|
||||
StartupPasswordDialog(
|
||||
onDismiss = {
|
||||
onShowStartupPasswordDialogChange(false)
|
||||
context.findActivity().finish()
|
||||
},
|
||||
onUnlock = onUnlockEncryption,
|
||||
errorMessage = startupUnlockErrorMessage
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -237,6 +237,8 @@
|
||||
<string name="unlock_item">Element entsperren</string>
|
||||
<string name="enter_password_to_unlock">Geben Sie das Passwort ein, um dieses Element zu entsperren.</string>
|
||||
<string name="incorrect_password">Falsches Passwort.</string>
|
||||
<string name="unlock_encryption">Verschlüsselung entsperren</string>
|
||||
<string name="enter_password_to_unlock_encryption">Geben Sie das Passwort ein, um die Datenverschlüsselung zu entsperren.</string>
|
||||
|
||||
<string name="data_encryption">Datenverschlüsselung</string>
|
||||
<string name="set_encryption_password">Verschlüsselungspasswort festlegen</string>
|
||||
|
||||
@@ -238,6 +238,8 @@
|
||||
<string name="unlock_item">Unlock Item</string>
|
||||
<string name="enter_password_to_unlock">Enter password to unlock this item.</string>
|
||||
<string name="incorrect_password">Incorrect password.</string>
|
||||
<string name="unlock_encryption">Unlock Encryption</string>
|
||||
<string name="enter_password_to_unlock_encryption">Enter password to unlock data encryption.</string>
|
||||
|
||||
<string name="data_encryption">Data Encryption</string>
|
||||
<string name="set_encryption_password">Set Encryption Password</string>
|
||||
|
||||
Reference in New Issue
Block a user