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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user