fix(security): Implement persistent secret key for reliable re-locking

- Introduced a `SecretKeySaver` to persistently store the encryption `secretKey` across configuration changes using `rememberSaveable`.
- Ensured that the `secretKey` is updated in the `AppShell`'s state upon successful biometric authentication for unlocking notes, lists, and recipes.
- This addresses the bug where locked items could be bypassed due to the `secretKey` being lost on screen rotation or other configuration changes, leading to failed re-locking.
- This also resolves the issue where the unlock button for locked items would only work after an app restart.
This commit is contained in:
2025-11-02 09:45:06 +01:00
parent e8a392aafc
commit 416f1e37aa

View File

@@ -79,6 +79,8 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.setValue
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.SaverScope
import androidx.compose.ui.Alignment
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.compose.ui.Modifier
@@ -125,6 +127,8 @@ import de.lxtools.noteshop.ui.EncryptionPasswordDialog
import de.lxtools.noteshop.ui.GuidedTourScreen
import java.io.FileOutputStream
import javax.crypto.SecretKey
import java.util.Base64
import javax.crypto.spec.SecretKeySpec
// Data class to hold the dynamic strings
data class DynamicListStrings(
@@ -203,6 +207,18 @@ fun Context.findActivity(): Activity {
throw IllegalStateException("no activity")
}
object SecretKeySaver : Saver<SecretKey?, String> {
override fun restore(value: String): SecretKey? {
if (value == "null") return null
val decodedKey = Base64.getDecoder().decode(value)
return SecretKeySpec(decodedKey, 0, decodedKey.size, "AES")
}
override fun SaverScope.save(value: SecretKey?): String {
return value?.encoded?.let { Base64.getEncoder().encodeToString(it) } ?: "null"
}
}
class BiometricAuthenticator(private val context: Context) {
private lateinit var promptInfo: BiometricPrompt.PromptInfo
@@ -712,7 +728,7 @@ fun AppShell(
}
val lifecycleOwner = LocalLifecycleOwner.current
var secretKey by remember { mutableStateOf<SecretKey?>(null) }
var secretKey by rememberSaveable(stateSaver = SecretKeySaver) { mutableStateOf(null) }
var isDecryptionAttempted by rememberSaveable { mutableStateOf(false) }
DisposableEffect(lifecycleOwner, context, scope) {
@@ -1779,6 +1795,7 @@ fun AppShell(
keyManager.getSecretKeyFromAuthenticatedCipher(
authenticatedCipher
)
secretKey = key
shoppingListsViewModel.toggleListLock(
selectedListId!!,
key,
@@ -1818,6 +1835,7 @@ fun AppShell(
keyManager.getSecretKeyFromAuthenticatedCipher(
authenticatedCipher
)
secretKey = key
selectedNoteId?.let {
notesViewModel.toggleNoteLock(
it,
@@ -1872,6 +1890,7 @@ fun AppShell(
keyManager.getSecretKeyFromAuthenticatedCipher(
authenticatedCipher
)
secretKey = key
selectedRecipeId?.let {
recipesViewModel.toggleRecipeLock(
it,