feat(security): Enhance password protection and fix lock state bugs
This commit introduces several improvements to the password protection feature for notes, recipes, and shopping lists. - Password confirmation and visibility: When setting a password, the user is now prompted to enter it twice for confirmation. A visibility toggle has been added to both the set password and unlock dialogs to improve user experience. - Differentiated unlock methods: The app now distinguishes between password and biometric protection. If an item is locked with a password, the app will directly prompt for the password instead of attempting biometric authentication first. - Fixed disappearing lock icon: A bug was fixed where the lock icon would disappear after an item was unlocked and then edited. The lock state is now correctly preserved.
This commit is contained in:
@@ -620,12 +620,20 @@ fun AppShell(
|
||||
sharedPrefs.edit { putBoolean("first_launch_completed", true) }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var selectedListId: Int? by rememberSaveable { mutableStateOf(null) }
|
||||
var selectedNoteId: Int? by rememberSaveable { mutableStateOf(null) }
|
||||
var selectedRecipeId: Int? by rememberSaveable { mutableStateOf(null) }
|
||||
|
||||
var itemWasInitiallyLocked by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(selectedNoteId, selectedListId, selectedRecipeId) {
|
||||
if (selectedNoteId == null && selectedListId == null && selectedRecipeId == null) {
|
||||
itemWasInitiallyLocked = false
|
||||
}
|
||||
}
|
||||
|
||||
var showListDialog by rememberSaveable { mutableStateOf(false) }
|
||||
val listDetails by shoppingListsViewModel.listDetails.collectAsState()
|
||||
|
||||
@@ -652,34 +660,41 @@ fun AppShell(
|
||||
|
||||
|
||||
val onUnlockItem: (Int, Screen, Int) -> Unit = { id, type, protectionType ->
|
||||
if (protectionType == 1 || protectionType == 2) { // Password or Biometric protected
|
||||
biometricAuthenticator.promptBiometricAuth(
|
||||
title = context.getString(R.string.unlock_item),
|
||||
subtitle = "",
|
||||
fragmentActivity = context.findActivity() as FragmentActivity,
|
||||
onSuccess = { _ ->
|
||||
when (type) {
|
||||
is Screen.ShoppingListDetail -> selectedListId = id
|
||||
is Screen.NoteDetail -> selectedNoteId = id
|
||||
is Screen.RecipeDetail -> selectedRecipeId = id
|
||||
else -> {}
|
||||
when (protectionType) {
|
||||
1 -> { // Password protected
|
||||
itemToUnlockId = id
|
||||
itemToUnlockType = type
|
||||
showUnlockPasswordDialog = true
|
||||
}
|
||||
2 -> { // Biometric protected
|
||||
biometricAuthenticator.promptBiometricAuth(
|
||||
title = context.getString(R.string.unlock_item),
|
||||
subtitle = "",
|
||||
fragmentActivity = context.findActivity() as FragmentActivity,
|
||||
onSuccess = { _ ->
|
||||
when (type) {
|
||||
is Screen.ShoppingListDetail -> selectedListId = id
|
||||
is Screen.NoteDetail -> selectedNoteId = id
|
||||
is Screen.RecipeDetail -> selectedRecipeId = id
|
||||
else -> {}
|
||||
}
|
||||
itemWasInitiallyLocked = true
|
||||
currentScreen = type
|
||||
},
|
||||
onFailed = {
|
||||
// Biometric failed, offer password as fallback if desired
|
||||
itemToUnlockId = id
|
||||
itemToUnlockType = type
|
||||
showUnlockPasswordDialog = true
|
||||
},
|
||||
onError = { _, _ ->
|
||||
// Biometric error, offer password as fallback if desired
|
||||
itemToUnlockId = id
|
||||
itemToUnlockType = type
|
||||
showUnlockPasswordDialog = true
|
||||
}
|
||||
itemWasInitiallyLocked = true
|
||||
currentScreen = type
|
||||
},
|
||||
onFailed = {
|
||||
// Biometric failed, offer password as fallback if desired
|
||||
itemToUnlockId = id
|
||||
itemToUnlockType = type
|
||||
showUnlockPasswordDialog = true
|
||||
},
|
||||
onError = { _, _ ->
|
||||
// Biometric error, offer password as fallback if desired
|
||||
itemToUnlockId = id
|
||||
itemToUnlockType = type
|
||||
showUnlockPasswordDialog = true
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1180,8 +1195,8 @@ fun AppShell(
|
||||
|
||||
when (itemToUnlockType) {
|
||||
Screen.NoteDetail -> {
|
||||
val note = notesViewModel.noteDetails.value
|
||||
if (note.id == itemToUnlockId && note.protectionHash == hashedPassword) {
|
||||
val note = notesViewModel.uiState.value.noteList.find { it.id == itemToUnlockId }
|
||||
if (note != null && note.protectionHash == hashedPassword) {
|
||||
isPasswordCorrect = true
|
||||
selectedNoteId = itemToUnlockId
|
||||
}
|
||||
|
||||
@@ -4,9 +4,15 @@ 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
|
||||
@@ -17,7 +23,9 @@ 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
|
||||
|
||||
@@ -29,6 +37,7 @@ fun UnlockPasswordDialog(
|
||||
errorMessage: String? = null
|
||||
) {
|
||||
var password by remember { mutableStateOf("") }
|
||||
var showPassword by remember { mutableStateOf(false) }
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
@@ -41,11 +50,19 @@ fun UnlockPasswordDialog(
|
||||
value = password,
|
||||
onValueChange = { password = it },
|
||||
label = { Text(stringResource(R.string.password)) },
|
||||
visualTransformation = PasswordVisualTransformation(),
|
||||
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 { message ->
|
||||
Text(text = message, color = androidx.compose.material3.MaterialTheme.colorScheme.error)
|
||||
errorMessage?.let {
|
||||
Text(text = it, color = androidx.compose.material3.MaterialTheme.colorScheme.error)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -71,7 +71,8 @@ class NotesViewModel(private val noteshopRepository: NoteshopRepository) : ViewM
|
||||
content = note.content,
|
||||
displayOrder = note.displayOrder,
|
||||
protectionHash = note.protectionHash,
|
||||
protectionType = note.protectionType
|
||||
protectionType = note.protectionType,
|
||||
lockMethod = note.lockMethod
|
||||
)
|
||||
}
|
||||
|
||||
@@ -224,7 +225,8 @@ data class NoteDetails(
|
||||
val content: String = "",
|
||||
val displayOrder: Int = 0,
|
||||
val protectionHash: String = "",
|
||||
val protectionType: Int = 0
|
||||
val protectionType: Int = 0,
|
||||
val lockMethod: Int = 0
|
||||
) {
|
||||
fun toNote(): Note = Note(
|
||||
id = id,
|
||||
@@ -232,7 +234,8 @@ data class NoteDetails(
|
||||
content = content,
|
||||
displayOrder = displayOrder,
|
||||
protectionHash = protectionHash,
|
||||
protectionType = protectionType
|
||||
protectionType = protectionType,
|
||||
lockMethod = lockMethod
|
||||
)
|
||||
|
||||
fun isValid(): Boolean {
|
||||
|
||||
@@ -4,9 +4,15 @@ 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
|
||||
@@ -17,7 +23,9 @@ 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
|
||||
|
||||
@@ -28,6 +36,11 @@ fun SetPasswordDialog(
|
||||
onSetPassword: (String) -> Unit
|
||||
) {
|
||||
var password by remember { mutableStateOf("") }
|
||||
var confirmPassword by remember { mutableStateOf("") }
|
||||
var showPassword by remember { mutableStateOf(false) }
|
||||
var showConfirmPassword by remember { mutableStateOf(false) }
|
||||
|
||||
val isNewPasswordValid = password.isNotBlank() && password == confirmPassword
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
@@ -40,7 +53,31 @@ fun SetPasswordDialog(
|
||||
value = password,
|
||||
onValueChange = { password = it },
|
||||
label = { Text(stringResource(R.string.password)) },
|
||||
visualTransformation = PasswordVisualTransformation(),
|
||||
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()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
OutlinedTextField(
|
||||
value = confirmPassword,
|
||||
onValueChange = { confirmPassword = it },
|
||||
label = { Text(stringResource(R.string.confirm_password)) },
|
||||
visualTransformation = if (showConfirmPassword) VisualTransformation.None else PasswordVisualTransformation(),
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
|
||||
trailingIcon = {
|
||||
val image = if (showConfirmPassword) Icons.Filled.Visibility else Icons.Filled.VisibilityOff
|
||||
val description = if (showConfirmPassword) "Hide password" else "Show password"
|
||||
IconButton(onClick = { showConfirmPassword = !showConfirmPassword }) {
|
||||
Icon(imageVector = image, description)
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
@@ -48,7 +85,7 @@ fun SetPasswordDialog(
|
||||
confirmButton = {
|
||||
Button(
|
||||
onClick = { onSetPassword(password) },
|
||||
enabled = password.isNotBlank()
|
||||
enabled = isNewPasswordValid
|
||||
) {
|
||||
Text(stringResource(R.string.save))
|
||||
}
|
||||
|
||||
@@ -68,7 +68,8 @@ class RecipesViewModel(private val noteshopRepository: NoteshopRepository) : Vie
|
||||
content = recipe.content,
|
||||
displayOrder = recipe.displayOrder,
|
||||
protectionHash = recipe.protectionHash,
|
||||
protectionType = recipe.protectionType
|
||||
protectionType = recipe.protectionType,
|
||||
lockMethod = recipe.lockMethod
|
||||
)
|
||||
}
|
||||
|
||||
@@ -222,7 +223,8 @@ data class RecipeDetails(
|
||||
val content: String = "",
|
||||
val displayOrder: Int = 0,
|
||||
val protectionHash: String = "",
|
||||
val protectionType: Int = 0
|
||||
val protectionType: Int = 0,
|
||||
val lockMethod: Int = 0
|
||||
) {
|
||||
fun toRecipe(): Recipe = Recipe(
|
||||
id = id,
|
||||
@@ -230,7 +232,8 @@ data class RecipeDetails(
|
||||
content = content,
|
||||
displayOrder = displayOrder,
|
||||
protectionHash = protectionHash,
|
||||
protectionType = protectionType
|
||||
protectionType = protectionType,
|
||||
lockMethod = lockMethod
|
||||
)
|
||||
|
||||
fun isValid(): Boolean {
|
||||
|
||||
@@ -114,7 +114,8 @@ class ShoppingListsViewModel(private val noteshopRepository: NoteshopRepository,
|
||||
name = list.name,
|
||||
displayOrder = list.displayOrder,
|
||||
protectionHash = list.protectionHash,
|
||||
protectionType = list.protectionType
|
||||
protectionType = list.protectionType,
|
||||
lockMethod = list.lockMethod
|
||||
)
|
||||
}
|
||||
|
||||
@@ -139,7 +140,8 @@ class ShoppingListsViewModel(private val noteshopRepository: NoteshopRepository,
|
||||
name = currentList.name,
|
||||
displayOrder = currentList.displayOrder,
|
||||
protectionHash = currentList.protectionHash,
|
||||
protectionType = currentList.protectionType
|
||||
protectionType = currentList.protectionType,
|
||||
lockMethod = currentList.lockMethod
|
||||
) ?: currentList
|
||||
noteshopRepository.updateShoppingList(listToUpdate)
|
||||
}
|
||||
@@ -609,14 +611,16 @@ data class ShoppingListDetails(
|
||||
val name: String = "",
|
||||
val displayOrder: Int = 0,
|
||||
val protectionHash: String = "",
|
||||
val protectionType: Int = 0
|
||||
val protectionType: Int = 0,
|
||||
val lockMethod: Int = 0
|
||||
) {
|
||||
fun toShoppingList(): ShoppingList = ShoppingList(
|
||||
id = id,
|
||||
name = name,
|
||||
displayOrder = displayOrder,
|
||||
protectionHash = protectionHash,
|
||||
protectionType = protectionType
|
||||
protectionType = protectionType,
|
||||
lockMethod = lockMethod
|
||||
)
|
||||
|
||||
fun isValid(): Boolean {
|
||||
|
||||
Reference in New Issue
Block a user