Fix: Prevent data loss when disabling encryption
Resolves a critical bug that caused permanent data loss of locked items when encryption was disabled. The previous implementation deleted the encryption key without decrypting the data first. This commit introduces a safe-offboarding process: - When disabling encryption, the user is now authenticated to retrieve the secret key. - New `decryptAllLockedItems` functions in the ViewModels are called to decrypt all locked notes, recipes, and shopping lists across the app. - Only after successful decryption is the key permanently deleted. Additionally, this commit removes the last remnants of the old session-based unlocking logic and standardizes the auto-relock-on-exit behavior across all detail screens for a consistent user experience.
This commit is contained in:
@@ -403,20 +403,45 @@ fun AppShell(
|
||||
val onEncryptionToggle: (Boolean) -> Unit = { enabled ->
|
||||
if (!enabled) { // Turning OFF
|
||||
if (hasEncryptionPassword) {
|
||||
// Has a password, so require auth to delete it
|
||||
// Has a password, so require auth to decrypt all and delete the key
|
||||
val activity = context.findActivity() as FragmentActivity
|
||||
val cipher = keyManager.getDecryptionCipher()
|
||||
if (cipher == null) {
|
||||
// Fallback for safety, though this state should be unlikely if hasEncryptionPassword is true
|
||||
keyManager.deleteKey()
|
||||
hasEncryptionPassword = false
|
||||
isEncryptionEnabled = false
|
||||
sharedPrefs.edit {
|
||||
putBoolean("encryption_enabled", false)
|
||||
remove("biometric_unlock_enabled")
|
||||
}
|
||||
}
|
||||
val crypto = BiometricPrompt.CryptoObject(cipher!!)
|
||||
|
||||
biometricAuthenticator.promptBiometricAuth(
|
||||
title = context.getString(R.string.remove_encryption),
|
||||
subtitle = context.getString(R.string.confirm_to_disable_encryption),
|
||||
negativeButtonText = context.getString(R.string.cancel),
|
||||
fragmentActivity = activity,
|
||||
onSuccess = {
|
||||
keyManager.deleteKey()
|
||||
hasEncryptionPassword = false
|
||||
isEncryptionEnabled = false
|
||||
sharedPrefs.edit {
|
||||
putBoolean("encryption_enabled", false)
|
||||
remove("biometric_unlock_enabled")
|
||||
crypto = crypto,
|
||||
onSuccess = { result ->
|
||||
result.cryptoObject?.cipher?.let { authenticatedCipher ->
|
||||
scope.launch {
|
||||
val key = keyManager.getSecretKeyFromAuthenticatedCipher(authenticatedCipher)
|
||||
// Decrypt all items before deleting the key
|
||||
notesViewModel.decryptAllLockedItems(key, fileEncryptor)
|
||||
shoppingListsViewModel.decryptAllLockedItems(key, fileEncryptor)
|
||||
recipesViewModel.decryptAllLockedItems(key, fileEncryptor)
|
||||
|
||||
// Now, safely delete the key and disable the feature
|
||||
keyManager.deleteKey()
|
||||
hasEncryptionPassword = false
|
||||
isEncryptionEnabled = false
|
||||
sharedPrefs.edit {
|
||||
putBoolean("encryption_enabled", false)
|
||||
remove("biometric_unlock_enabled")
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
onFailed = {},
|
||||
@@ -456,18 +481,42 @@ fun AppShell(
|
||||
|
||||
val onRemoveEncryption: () -> Unit = {
|
||||
val activity = context.findActivity() as FragmentActivity
|
||||
val cipher = keyManager.getDecryptionCipher()
|
||||
if (cipher == null) {
|
||||
// This can happen if no key is set. In this case, just disable the feature.
|
||||
hasEncryptionPassword = false
|
||||
isEncryptionEnabled = false
|
||||
sharedPrefs.edit {
|
||||
putBoolean("encryption_enabled", false)
|
||||
remove("biometric_unlock_enabled")
|
||||
}
|
||||
}
|
||||
val crypto = BiometricPrompt.CryptoObject(cipher!!)
|
||||
|
||||
biometricAuthenticator.promptBiometricAuth(
|
||||
title = context.getString(R.string.remove_encryption),
|
||||
subtitle = context.getString(R.string.confirm_to_remove_encryption),
|
||||
negativeButtonText = context.getString(R.string.cancel),
|
||||
fragmentActivity = activity,
|
||||
onSuccess = {
|
||||
keyManager.deleteKey()
|
||||
hasEncryptionPassword = false
|
||||
isEncryptionEnabled = false
|
||||
sharedPrefs.edit {
|
||||
putBoolean("encryption_enabled", false)
|
||||
remove("biometric_unlock_enabled")
|
||||
crypto = crypto,
|
||||
onSuccess = { result ->
|
||||
result.cryptoObject?.cipher?.let { authenticatedCipher ->
|
||||
scope.launch {
|
||||
val key = keyManager.getSecretKeyFromAuthenticatedCipher(authenticatedCipher)
|
||||
// Decrypt all items in all ViewModels
|
||||
notesViewModel.decryptAllLockedItems(key, fileEncryptor)
|
||||
shoppingListsViewModel.decryptAllLockedItems(key, fileEncryptor)
|
||||
recipesViewModel.decryptAllLockedItems(key, fileEncryptor)
|
||||
|
||||
// Now it's safe to delete the key
|
||||
keyManager.deleteKey()
|
||||
hasEncryptionPassword = false
|
||||
isEncryptionEnabled = false
|
||||
sharedPrefs.edit {
|
||||
putBoolean("encryption_enabled", false)
|
||||
remove("biometric_unlock_enabled")
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
onFailed = {},
|
||||
@@ -1311,7 +1360,6 @@ fun AppShell(
|
||||
)
|
||||
noteDetails.let { noteDetails ->
|
||||
val isNoteLocked = noteDetails.isLocked
|
||||
val isNoteSessionUnlocked = noteDetails.id in notesViewModel.unlockedNoteIds.collectAsState().value
|
||||
|
||||
DropdownMenuItem(
|
||||
text = { Text(if (isNoteLocked) stringResource(R.string.unlock_note) else stringResource(R.string.lock_note)) },
|
||||
@@ -1387,7 +1435,6 @@ fun AppShell(
|
||||
)
|
||||
recipeDetails.let { recipeDetails ->
|
||||
val isRecipeLocked = recipeDetails.isLocked
|
||||
val isRecipeSessionUnlocked = recipeDetails.id in recipesViewModel.unlockedRecipeIds.collectAsState().value
|
||||
|
||||
DropdownMenuItem(
|
||||
text = { Text(if (isRecipeLocked) dynamicRecipeStrings.unlockItem else dynamicRecipeStrings.lockItem) },
|
||||
@@ -1650,77 +1697,30 @@ fun AppShell(
|
||||
NoteDetailScreen(
|
||||
noteId = selectedNoteId,
|
||||
viewModel = notesViewModel,
|
||||
secretKey = secretKey,
|
||||
fileEncryptor = fileEncryptor,
|
||||
onUnlockClick = {
|
||||
scope.launch {
|
||||
if (secretKey != null) {
|
||||
// Key already exists, decrypt directly
|
||||
val noteToDecrypt = notesViewModel.noteDetails.value
|
||||
try {
|
||||
val decryptedContent = try {
|
||||
String(fileEncryptor.decrypt(Base64.getDecoder().decode(noteToDecrypt.content), secretKey!!), Charsets.UTF_8)
|
||||
} catch (e: Exception) {
|
||||
Log.w("MainActivity", "Could not decrypt note content, assuming it's plaintext.")
|
||||
noteToDecrypt.content
|
||||
}
|
||||
|
||||
val decryptedTitle = try {
|
||||
String(fileEncryptor.decrypt(Base64.getDecoder().decode(noteToDecrypt.title), secretKey!!), Charsets.UTF_8)
|
||||
} catch (e: Exception) {
|
||||
Log.w("MainActivity", "Could not decrypt note title, assuming it's plaintext.")
|
||||
noteToDecrypt.title
|
||||
}
|
||||
|
||||
notesViewModel.setDecryptedDetails(decryptedTitle, decryptedContent)
|
||||
selectedNoteId?.let { notesViewModel.unlockNote(it) }
|
||||
} catch (e: Exception) {
|
||||
Log.e("MainActivity", "An unexpected error occurred during note decryption.", e)
|
||||
}
|
||||
} else {
|
||||
// No session key, prompt for authentication
|
||||
val cipher = keyManager.getDecryptionCipher()
|
||||
if (cipher != null) {
|
||||
val crypto = BiometricPrompt.CryptoObject(cipher)
|
||||
val activity = context.findActivity() as FragmentActivity
|
||||
biometricAuthenticator.promptBiometricAuth(
|
||||
title = context.getString(R.string.unlock_note),
|
||||
subtitle = "",
|
||||
negativeButtonText = context.getString(R.string.cancel),
|
||||
fragmentActivity = activity,
|
||||
crypto = crypto,
|
||||
onSuccess = { result ->
|
||||
result.cryptoObject?.cipher?.let { authenticatedCipher ->
|
||||
val key = keyManager.getSecretKeyFromAuthenticatedCipher(authenticatedCipher)
|
||||
val noteToDecrypt = notesViewModel.noteDetails.value
|
||||
try {
|
||||
val decryptedContent = try {
|
||||
String(fileEncryptor.decrypt(Base64.getDecoder().decode(noteToDecrypt.content), key), Charsets.UTF_8)
|
||||
} catch (e: Exception) {
|
||||
Log.w("MainActivity", "Could not decrypt note content, assuming it's plaintext.")
|
||||
noteToDecrypt.content // Fallback to plaintext
|
||||
}
|
||||
|
||||
val decryptedTitle = try {
|
||||
String(fileEncryptor.decrypt(Base64.getDecoder().decode(noteToDecrypt.title), key), Charsets.UTF_8)
|
||||
} catch (e: Exception) {
|
||||
Log.w("MainActivity", "Could not decrypt note title, assuming it's plaintext.")
|
||||
noteToDecrypt.title // Fallback to plaintext
|
||||
}
|
||||
|
||||
notesViewModel.setDecryptedDetails(decryptedTitle, decryptedContent)
|
||||
selectedNoteId?.let { notesViewModel.unlockNote(it) }
|
||||
} catch (e: Exception) {
|
||||
Log.e("MainActivity", "An unexpected error occurred during note decryption.", e)
|
||||
android.widget.Toast.makeText(context, "Decryption failed.", android.widget.Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
},
|
||||
onFailed = {},
|
||||
onError = { _, _ -> }
|
||||
)
|
||||
}
|
||||
val activity = context as FragmentActivity
|
||||
val cipher = keyManager.getDecryptionCipher()
|
||||
if (cipher != null) {
|
||||
val crypto = BiometricPrompt.CryptoObject(cipher)
|
||||
biometricAuthenticator.promptBiometricAuth(
|
||||
title = context.getString(R.string.unlock_note),
|
||||
subtitle = "",
|
||||
negativeButtonText = context.getString(R.string.cancel),
|
||||
fragmentActivity = activity,
|
||||
crypto = crypto,
|
||||
onSuccess = { result ->
|
||||
result.cryptoObject?.cipher?.let { authenticatedCipher ->
|
||||
val key = keyManager.getSecretKeyFromAuthenticatedCipher(authenticatedCipher)
|
||||
selectedNoteId?.let { notesViewModel.toggleNoteLock(it, key, fileEncryptor) }
|
||||
}
|
||||
},
|
||||
onFailed = {},
|
||||
onError = { _, _ -> }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
is Screen.Recipes -> {
|
||||
@@ -1740,77 +1740,30 @@ fun AppShell(
|
||||
de.lxtools.noteshop.ui.recipes.RecipeDetailScreen(
|
||||
recipeId = selectedRecipeId,
|
||||
viewModel = recipesViewModel,
|
||||
secretKey = secretKey,
|
||||
fileEncryptor = fileEncryptor,
|
||||
onUnlockClick = {
|
||||
scope.launch {
|
||||
if (secretKey != null) {
|
||||
// Key already exists, decrypt directly
|
||||
val recipeToDecrypt = recipesViewModel.recipeDetails.value
|
||||
try {
|
||||
val decryptedContent = try {
|
||||
String(fileEncryptor.decrypt(Base64.getDecoder().decode(recipeToDecrypt.content), secretKey!!), Charsets.UTF_8)
|
||||
} catch (e: Exception) {
|
||||
Log.w("MainActivity", "Could not decrypt recipe content, assuming it's plaintext.")
|
||||
recipeToDecrypt.content
|
||||
}
|
||||
|
||||
val decryptedTitle = try {
|
||||
String(fileEncryptor.decrypt(Base64.getDecoder().decode(recipeToDecrypt.title), secretKey!!), Charsets.UTF_8)
|
||||
} catch (e: Exception) {
|
||||
Log.w("MainActivity", "Could not decrypt recipe title, assuming it's plaintext.")
|
||||
recipeToDecrypt.title
|
||||
}
|
||||
|
||||
recipesViewModel.setDecryptedDetails(decryptedTitle, decryptedContent)
|
||||
selectedRecipeId?.let { recipesViewModel.unlockRecipe(it) }
|
||||
} catch (e: Exception) {
|
||||
Log.e("MainActivity", "An unexpected error occurred during recipe decryption.", e)
|
||||
}
|
||||
} else {
|
||||
// No session key, prompt for authentication
|
||||
val cipher = keyManager.getDecryptionCipher()
|
||||
if (cipher != null) {
|
||||
val crypto = BiometricPrompt.CryptoObject(cipher)
|
||||
val activity = context.findActivity() as FragmentActivity
|
||||
biometricAuthenticator.promptBiometricAuth(
|
||||
title = context.getString(R.string.unlock_recipe),
|
||||
subtitle = "",
|
||||
negativeButtonText = context.getString(R.string.cancel),
|
||||
fragmentActivity = activity,
|
||||
crypto = crypto,
|
||||
onSuccess = { result ->
|
||||
result.cryptoObject?.cipher?.let { authenticatedCipher ->
|
||||
val key = keyManager.getSecretKeyFromAuthenticatedCipher(authenticatedCipher)
|
||||
val recipeToDecrypt = recipesViewModel.recipeDetails.value
|
||||
try {
|
||||
val decryptedContent = try {
|
||||
String(fileEncryptor.decrypt(Base64.getDecoder().decode(recipeToDecrypt.content), key), Charsets.UTF_8)
|
||||
} catch (e: Exception) {
|
||||
Log.w("MainActivity", "Could not decrypt recipe content, assuming it's plaintext.")
|
||||
recipeToDecrypt.content // Fallback to plaintext
|
||||
}
|
||||
|
||||
val decryptedTitle = try {
|
||||
String(fileEncryptor.decrypt(Base64.getDecoder().decode(recipeToDecrypt.title), key), Charsets.UTF_8)
|
||||
} catch (e: Exception) {
|
||||
Log.w("MainActivity", "Could not decrypt recipe title, assuming it's plaintext.")
|
||||
recipeToDecrypt.title // Fallback to plaintext
|
||||
}
|
||||
|
||||
recipesViewModel.setDecryptedDetails(decryptedTitle, decryptedContent)
|
||||
selectedRecipeId?.let { recipesViewModel.unlockRecipe(it) }
|
||||
} catch (e: Exception) {
|
||||
Log.e("MainActivity", "An unexpected error occurred during recipe decryption.", e)
|
||||
android.widget.Toast.makeText(context, "Decryption failed.", android.widget.Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
},
|
||||
onFailed = {},
|
||||
onError = { _, _ -> }
|
||||
)
|
||||
}
|
||||
val activity = context as FragmentActivity
|
||||
val cipher = keyManager.getDecryptionCipher()
|
||||
if (cipher != null) {
|
||||
val crypto = BiometricPrompt.CryptoObject(cipher)
|
||||
biometricAuthenticator.promptBiometricAuth(
|
||||
title = context.getString(R.string.unlock_recipe),
|
||||
subtitle = "",
|
||||
negativeButtonText = context.getString(R.string.cancel),
|
||||
fragmentActivity = activity,
|
||||
crypto = crypto,
|
||||
onSuccess = { result ->
|
||||
result.cryptoObject?.cipher?.let { authenticatedCipher ->
|
||||
val key = keyManager.getSecretKeyFromAuthenticatedCipher(authenticatedCipher)
|
||||
selectedRecipeId?.let { recipesViewModel.toggleRecipeLock(it, key, fileEncryptor) }
|
||||
}
|
||||
},
|
||||
onFailed = {},
|
||||
onError = { _, _ -> }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
is Screen.About -> {
|
||||
|
||||
@@ -29,17 +29,42 @@ import androidx.biometric.BiometricManager
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import de.lxtools.noteshop.security.FileEncryptor
|
||||
import javax.crypto.SecretKey
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun NoteDetailScreen(
|
||||
noteId: Int?,
|
||||
viewModel: NotesViewModel,
|
||||
modifier: Modifier = Modifier,
|
||||
onUnlockClick: () -> Unit
|
||||
onUnlockClick: () -> Unit,
|
||||
secretKey: SecretKey?,
|
||||
fileEncryptor: FileEncryptor
|
||||
) {
|
||||
|
||||
val noteDetails by viewModel.noteDetails.collectAsState()
|
||||
val unlockedNoteIds by viewModel.unlockedNoteIds.collectAsState()
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
var wasLockedInitially by remember { mutableStateOf<Boolean?>(null) }
|
||||
|
||||
LaunchedEffect(noteDetails) {
|
||||
if (wasLockedInitially == null) { // This ensures we only set it once
|
||||
wasLockedInitially = noteDetails.isLocked
|
||||
}
|
||||
}
|
||||
|
||||
DisposableEffect(noteId) {
|
||||
onDispose {
|
||||
if (wasLockedInitially == true) {
|
||||
viewModel.toggleNoteLock(noteId ?: 0, secretKey, fileEncryptor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(key1 = noteId) {
|
||||
if (noteId != null) {
|
||||
@@ -50,7 +75,7 @@ fun NoteDetailScreen(
|
||||
}
|
||||
}
|
||||
|
||||
if (noteDetails.isLocked && noteDetails.id !in unlockedNoteIds) {
|
||||
if (noteDetails.isLocked) {
|
||||
LockedScreenPlaceholder(onUnlockClick = onUnlockClick)
|
||||
} else {
|
||||
Column(
|
||||
|
||||
@@ -38,13 +38,6 @@ class NotesViewModel(private val noteshopRepository: NoteshopRepository) : ViewM
|
||||
_searchQuery.value = query
|
||||
}
|
||||
|
||||
private val _unlockedNoteIds = MutableStateFlow<Set<Int>>(emptySet())
|
||||
val unlockedNoteIds: StateFlow<Set<Int>> = _unlockedNoteIds.asStateFlow()
|
||||
|
||||
fun unlockNote(noteId: Int) {
|
||||
_unlockedNoteIds.value = _unlockedNoteIds.value + noteId
|
||||
}
|
||||
|
||||
fun toggleNoteLock(noteId: Int, secretKey: SecretKey?, fileEncryptor: FileEncryptor) {
|
||||
viewModelScope.launch(kotlinx.coroutines.Dispatchers.IO) {
|
||||
val note = noteshopRepository.getNoteStream(noteId).firstOrNull()
|
||||
@@ -60,7 +53,6 @@ class NotesViewModel(private val noteshopRepository: NoteshopRepository) : ViewM
|
||||
noteToUpdate = noteToUpdate.copy(
|
||||
content = encryptedContent
|
||||
)
|
||||
_unlockedNoteIds.value = _unlockedNoteIds.value - noteId
|
||||
} catch (e: Exception) {
|
||||
Log.e("NotesViewModel", "Failed to encrypt note content on lock.", e)
|
||||
return@launch // Don't update if encryption fails
|
||||
@@ -248,6 +240,15 @@ class NotesViewModel(private val noteshopRepository: NoteshopRepository) : ViewM
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun decryptAllLockedItems(secretKey: SecretKey, fileEncryptor: FileEncryptor) {
|
||||
val allNotes = noteshopRepository.getAllNotesStream().firstOrNull()
|
||||
allNotes?.forEach { note ->
|
||||
if (note.isLocked) {
|
||||
toggleNoteLock(note.id, secretKey, fileEncryptor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TIMEOUT_MILLIS = 5_000L
|
||||
}
|
||||
|
||||
@@ -38,18 +38,42 @@ import de.lxtools.noteshop.R
|
||||
import dev.jeziellago.compose.markdowntext.MarkdownText
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import de.lxtools.noteshop.security.FileEncryptor
|
||||
import javax.crypto.SecretKey
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun RecipeDetailScreen(
|
||||
recipeId: Int?,
|
||||
viewModel: RecipesViewModel,
|
||||
modifier: Modifier = Modifier,
|
||||
onUnlockClick: () -> Unit
|
||||
onUnlockClick: () -> Unit,
|
||||
secretKey: SecretKey?,
|
||||
fileEncryptor: FileEncryptor
|
||||
) {
|
||||
|
||||
val recipeDetails by viewModel.recipeDetails.collectAsState()
|
||||
val unlockedRecipeIds by viewModel.unlockedRecipeIds.collectAsState()
|
||||
var isEditMode by remember { mutableStateOf(false) }
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
var wasLockedInitially by remember { mutableStateOf<Boolean?>(null) }
|
||||
|
||||
LaunchedEffect(recipeDetails) {
|
||||
if (wasLockedInitially == null) { // This ensures we only set it once
|
||||
wasLockedInitially = recipeDetails.isLocked
|
||||
}
|
||||
}
|
||||
|
||||
DisposableEffect(recipeId) {
|
||||
onDispose {
|
||||
if (wasLockedInitially == true) {
|
||||
viewModel.toggleRecipeLock(recipeId ?: 0, secretKey, fileEncryptor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(key1 = recipeId) {
|
||||
if (recipeId != null) {
|
||||
@@ -60,7 +84,7 @@ fun RecipeDetailScreen(
|
||||
}
|
||||
}
|
||||
|
||||
if (recipeDetails.isLocked && recipeDetails.id !in unlockedRecipeIds) {
|
||||
if (recipeDetails.isLocked) {
|
||||
LockedScreenPlaceholder(onUnlockClick = onUnlockClick)
|
||||
} else {
|
||||
Column(
|
||||
|
||||
@@ -38,13 +38,6 @@ class RecipesViewModel(private val noteshopRepository: NoteshopRepository) : Vie
|
||||
_searchQuery.value = query
|
||||
}
|
||||
|
||||
private val _unlockedRecipeIds = MutableStateFlow<Set<Int>>(emptySet())
|
||||
val unlockedRecipeIds: StateFlow<Set<Int>> = _unlockedRecipeIds.asStateFlow()
|
||||
|
||||
fun unlockRecipe(recipeId: Int) {
|
||||
_unlockedRecipeIds.value = _unlockedRecipeIds.value + recipeId
|
||||
}
|
||||
|
||||
fun toggleRecipeLock(recipeId: Int, secretKey: SecretKey?, fileEncryptor: FileEncryptor) {
|
||||
viewModelScope.launch(kotlinx.coroutines.Dispatchers.IO) {
|
||||
val recipe = noteshopRepository.getRecipeStream(recipeId).firstOrNull()
|
||||
@@ -60,7 +53,6 @@ class RecipesViewModel(private val noteshopRepository: NoteshopRepository) : Vie
|
||||
recipeToUpdate = recipeToUpdate.copy(
|
||||
content = encryptedContent
|
||||
)
|
||||
_unlockedRecipeIds.value = _unlockedRecipeIds.value - recipeId
|
||||
} catch (e: Exception) {
|
||||
Log.e("RecipesViewModel", "Failed to encrypt recipe content on lock.", e)
|
||||
return@launch // Don't update if encryption fails
|
||||
@@ -251,6 +243,15 @@ class RecipesViewModel(private val noteshopRepository: NoteshopRepository) : Vie
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun decryptAllLockedItems(secretKey: SecretKey, fileEncryptor: FileEncryptor) {
|
||||
val allRecipes = noteshopRepository.getAllRecipesStream().firstOrNull()
|
||||
allRecipes?.forEach { recipe ->
|
||||
if (recipe.isLocked) {
|
||||
toggleRecipeLock(recipe.id, secretKey, fileEncryptor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TIMEOUT_MILLIS = 5_000L
|
||||
}
|
||||
|
||||
@@ -420,6 +420,15 @@ class ShoppingListsViewModel(private val noteshopRepository: NoteshopRepository)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun decryptAllLockedItems(secretKey: SecretKey, fileEncryptor: FileEncryptor) {
|
||||
val allLists = noteshopRepository.getAllShoppingListsWithItemsStream().firstOrNull()
|
||||
allLists?.forEach { listWithItems ->
|
||||
if (listWithItems.shoppingList.isLocked) {
|
||||
toggleListLock(listWithItems.shoppingList.id, secretKey, fileEncryptor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TIMEOUT_MILLIS = 5_000L
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user