feat(security, settings): Überarbeitung der Verschlüsselungs-Sicherheit und -Logik
Dieses Commit behebt kritische Sicherheitslücken und verbessert die Logik sowie die Benutzerfreundlichkeit der Verschlüsselungsfunktionen.
WICHTIGSTE ÄNDERUNGEN:
1. **Sicherheitslücken geschlossen:**
* Das Ändern oder Entfernen des Verschlüsselungspassworts erfordert nun eine zwingende biometrische Authentifizierung.
* Auch das Deaktivieren der Verschlüsselung ist jetzt durch eine Biometrie-Abfrage geschützt, um unbefugtes Löschen des Schlüssels zu verhindern.
* Dies schließt die Lücke, bei der eine Person mit Zugriff auf das entsperrte Gerät die Daten hätte kompromittieren können.
2. **Biometrie-Logik korrigiert:**
* Die Funktion 'Biometrisches Entsperren' funktioniert nun wie vorgesehen. Bei Aktivierung wird der Hauptschlüssel beim App-Start einmalig per Biometrie geladen.
* Dieser Schlüssel wird für die gesamte Sitzung wiederverwendet, sodass gesperrte Elemente ohne wiederholte Abfragen geöffnet werden können.
* Die Funktion kann über einen neuen Schalter in den Einstellungen jederzeit an- und ausgeschaltet werden.
3. **Verbesserungen an der UI:**
* Die missverständliche Bezeichnung 'Syncronisationsordner verschlüsseln' wurde zu 'Datenverschlüsselung' korrigiert.
4. **Kompatibilität des Entschlüsselungs-Skripts:**
* Das externe Skript decrypt.py wurde aktualisiert, um die neue, verschachtelte Verschlüsselungsstruktur zu verstehen und eine vollständige 'Tiefenentschlüsselung' der exportierten JSON-Dateien durchzuführen.
This commit is contained in:
@@ -384,28 +384,72 @@ fun AppShell(
|
||||
var showPasswordDialog by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
val onEncryptionToggle: (Boolean) -> Unit = { enabled ->
|
||||
isEncryptionEnabled = enabled
|
||||
sharedPrefs.edit {
|
||||
putBoolean("encryption_enabled", enabled)
|
||||
}
|
||||
if (!enabled) {
|
||||
// If encryption is disabled, remove the key
|
||||
keyManager.deleteKey()
|
||||
hasEncryptionPassword = false
|
||||
if (!enabled && hasEncryptionPassword) {
|
||||
// If turning off encryption, require auth to delete the key
|
||||
val activity = context.findActivity() as FragmentActivity
|
||||
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")
|
||||
}
|
||||
},
|
||||
onFailed = {},
|
||||
onError = { _, _ -> }
|
||||
)
|
||||
} else if (enabled) {
|
||||
isEncryptionEnabled = true
|
||||
sharedPrefs.edit {
|
||||
putBoolean("encryption_enabled", true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val onSetEncryptionPassword: () -> Unit = { showPasswordDialog = true }
|
||||
|
||||
val onRemoveEncryption: () -> Unit = {
|
||||
keyManager.deleteKey()
|
||||
hasEncryptionPassword = false
|
||||
isEncryptionEnabled = false // Also disable the toggle
|
||||
sharedPrefs.edit {
|
||||
putBoolean("encryption_enabled", false)
|
||||
val onSetEncryptionPassword: () -> Unit = {
|
||||
if (hasEncryptionPassword) {
|
||||
val activity = context.findActivity() as FragmentActivity
|
||||
biometricAuthenticator.promptBiometricAuth(
|
||||
title = context.getString(R.string.change_encryption_password),
|
||||
subtitle = context.getString(R.string.confirm_to_change_password),
|
||||
negativeButtonText = context.getString(R.string.cancel),
|
||||
fragmentActivity = activity,
|
||||
onSuccess = { showPasswordDialog = true },
|
||||
onFailed = { },
|
||||
onError = { _, _ -> }
|
||||
)
|
||||
} else {
|
||||
showPasswordDialog = true
|
||||
}
|
||||
}
|
||||
|
||||
val onRemoveEncryption: () -> Unit = {
|
||||
val activity = context.findActivity() as FragmentActivity
|
||||
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")
|
||||
}
|
||||
},
|
||||
onFailed = {},
|
||||
onError = { _, _ -> }
|
||||
)
|
||||
}
|
||||
|
||||
val performAutomaticExport: suspend (Context, SecretKey?) -> Unit = remember(notesViewModel, shoppingListsViewModel, recipesViewModel, syncFolderUri) {
|
||||
{ context, secretKey ->
|
||||
syncFolderUri?.let { uri ->
|
||||
@@ -572,7 +616,8 @@ fun AppShell(
|
||||
val observer = LifecycleEventObserver { _, event ->
|
||||
when (event) {
|
||||
Lifecycle.Event.ON_START -> {
|
||||
if (isEncryptionEnabled && hasEncryptionPassword && !isDecryptionAttempted) {
|
||||
val isBiometricUnlockEnabled = sharedPrefs.getBoolean("biometric_unlock_enabled", false)
|
||||
if (isEncryptionEnabled && hasEncryptionPassword && isBiometricUnlockEnabled && !isDecryptionAttempted) {
|
||||
scope.launch {
|
||||
try {
|
||||
val cipher = keyManager.getDecryptionCipher()
|
||||
@@ -1535,38 +1580,55 @@ fun AppShell(
|
||||
secretKey = secretKey,
|
||||
fileEncryptor = fileEncryptor,
|
||||
onUnlockClick = {
|
||||
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_list),
|
||||
subtitle = "",
|
||||
negativeButtonText = context.getString(R.string.cancel),
|
||||
fragmentActivity = activity,
|
||||
crypto = crypto,
|
||||
onSuccess = { result ->
|
||||
result.cryptoObject?.cipher?.let { authenticatedCipher ->
|
||||
scope.launch {
|
||||
val key = keyManager.getSecretKeyFromAuthenticatedCipher(authenticatedCipher)
|
||||
val listWithItems = shoppingListsViewModel.getShoppingListWithItemsStream(selectedListId!!).firstOrNull()
|
||||
if (listWithItems != null && listWithItems.shoppingList.encryptedItems != null) {
|
||||
try {
|
||||
val decryptedItemsJson = String(fileEncryptor.decrypt(Base64.getDecoder().decode(listWithItems.shoppingList.encryptedItems), key), Charsets.UTF_8)
|
||||
val decryptedItems = Json.decodeFromString<List<ShoppingListItem>>(decryptedItemsJson)
|
||||
shoppingListsViewModel.unlockList(selectedListId!!, decryptedItems)
|
||||
} catch (e: Exception) {
|
||||
Log.e("MainActivity", "Failed to decrypt list items.", e)
|
||||
scope.launch {
|
||||
if (secretKey != null) {
|
||||
// Key already exists, decrypt directly
|
||||
val listWithItems = shoppingListsViewModel.getShoppingListWithItemsStream(selectedListId!!).firstOrNull()
|
||||
if (listWithItems != null && listWithItems.shoppingList.encryptedItems != null) {
|
||||
try {
|
||||
val decryptedItemsJson = String(fileEncryptor.decrypt(Base64.getDecoder().decode(listWithItems.shoppingList.encryptedItems), secretKey!!), Charsets.UTF_8)
|
||||
val decryptedItems = Json.decodeFromString<List<ShoppingListItem>>(decryptedItemsJson)
|
||||
shoppingListsViewModel.unlockList(selectedListId!!, decryptedItems)
|
||||
} catch (e: Exception) {
|
||||
Log.e("MainActivity", "Failed to decrypt list items with session key.", 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_list),
|
||||
subtitle = "",
|
||||
negativeButtonText = context.getString(R.string.cancel),
|
||||
fragmentActivity = activity,
|
||||
crypto = crypto,
|
||||
onSuccess = { result ->
|
||||
result.cryptoObject?.cipher?.let { authenticatedCipher ->
|
||||
scope.launch {
|
||||
val key = keyManager.getSecretKeyFromAuthenticatedCipher(authenticatedCipher)
|
||||
val listWithItems = shoppingListsViewModel.getShoppingListWithItemsStream(selectedListId!!).firstOrNull()
|
||||
if (listWithItems != null && listWithItems.shoppingList.encryptedItems != null) {
|
||||
try {
|
||||
val decryptedItemsJson = String(fileEncryptor.decrypt(Base64.getDecoder().decode(listWithItems.shoppingList.encryptedItems), key), Charsets.UTF_8)
|
||||
val decryptedItems = Json.decodeFromString<List<ShoppingListItem>>(decryptedItemsJson)
|
||||
shoppingListsViewModel.unlockList(selectedListId!!, decryptedItems)
|
||||
} catch (e: Exception) {
|
||||
Log.e("MainActivity", "Failed to decrypt list items.", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
onFailed = {},
|
||||
onError = { _, _ -> }
|
||||
)
|
||||
},
|
||||
onFailed = {},
|
||||
onError = { _, _ -> }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
is Screen.NoteDetail -> {
|
||||
@@ -1574,48 +1636,76 @@ fun AppShell(
|
||||
noteId = selectedNoteId,
|
||||
viewModel = notesViewModel,
|
||||
onUnlockClick = {
|
||||
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
|
||||
}
|
||||
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), key), Charsets.UTF_8)
|
||||
} catch (e: Exception) {
|
||||
Log.w("MainActivity", "Could not decrypt note title, assuming it's plaintext.")
|
||||
noteToDecrypt.title // Fallback to plaintext
|
||||
}
|
||||
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)
|
||||
android.widget.Toast.makeText(context, "Decryption failed.", android.widget.Toast.LENGTH_LONG).show()
|
||||
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 = { _, _ -> }
|
||||
)
|
||||
},
|
||||
onFailed = {},
|
||||
onError = { _, _ -> }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
is Screen.Recipes -> {
|
||||
@@ -1636,48 +1726,76 @@ fun AppShell(
|
||||
recipeId = selectedRecipeId,
|
||||
viewModel = recipesViewModel,
|
||||
onUnlockClick = {
|
||||
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
|
||||
}
|
||||
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), key), Charsets.UTF_8)
|
||||
} catch (e: Exception) {
|
||||
Log.w("MainActivity", "Could not decrypt recipe title, assuming it's plaintext.")
|
||||
recipeToDecrypt.title // Fallback to plaintext
|
||||
}
|
||||
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)
|
||||
android.widget.Toast.makeText(context, "Decryption failed.", android.widget.Toast.LENGTH_LONG).show()
|
||||
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 = { _, _ -> }
|
||||
)
|
||||
},
|
||||
onFailed = {},
|
||||
onError = { _, _ -> }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
is Screen.About -> {
|
||||
@@ -1694,6 +1812,8 @@ fun AppShell(
|
||||
onSetEncryptionPassword = onSetEncryptionPassword,
|
||||
onRemoveEncryption = onRemoveEncryption,
|
||||
hasEncryptionPassword = hasEncryptionPassword,
|
||||
biometricAuthenticator = biometricAuthenticator,
|
||||
sharedPrefs = sharedPrefs,
|
||||
onResetShoppingListsTitle = {
|
||||
sharedPrefs.edit {
|
||||
remove("shopping_lists_title")
|
||||
@@ -1726,7 +1846,8 @@ fun AppShell(
|
||||
hasEncryptionPassword = true
|
||||
showPasswordDialog = false
|
||||
},
|
||||
keyManager = keyManager
|
||||
keyManager = keyManager,
|
||||
sharedPrefs = sharedPrefs
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,8 @@ import kotlinx.coroutines.launch
|
||||
fun EncryptionPasswordDialog(
|
||||
onDismiss: () -> Unit,
|
||||
onPasswordSet: () -> Unit,
|
||||
keyManager: KeyManager
|
||||
keyManager: KeyManager,
|
||||
sharedPrefs: android.content.SharedPreferences
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
@@ -108,6 +109,7 @@ fun EncryptionPasswordDialog(
|
||||
scope.launch {
|
||||
try {
|
||||
keyManager.generateAndStoreKey(password, useBiometrics)
|
||||
sharedPrefs.edit().putBoolean("biometric_unlock_enabled", useBiometrics).apply()
|
||||
onPasswordSet()
|
||||
Toast.makeText(context, R.string.encryption_password_set_success, Toast.LENGTH_SHORT).show()
|
||||
} catch (e: Exception) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package de.lxtools.noteshop.ui
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
@@ -38,7 +39,6 @@ import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
@@ -47,6 +47,8 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.core.content.edit
|
||||
import de.lxtools.noteshop.BiometricAuthenticator
|
||||
import de.lxtools.noteshop.Screen
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class, ExperimentalMaterial3Api::class)
|
||||
@@ -61,6 +63,8 @@ fun SettingsScreen(
|
||||
onSetEncryptionPassword: () -> Unit,
|
||||
onRemoveEncryption: () -> Unit,
|
||||
hasEncryptionPassword: Boolean,
|
||||
biometricAuthenticator: BiometricAuthenticator,
|
||||
sharedPrefs: SharedPreferences,
|
||||
onResetShoppingListsTitle: () -> Unit,
|
||||
onResetRecipesTitle: () -> Unit,
|
||||
currentStartScreen: String,
|
||||
@@ -74,6 +78,8 @@ fun SettingsScreen(
|
||||
Screen.Recipes.route to stringResource(R.string.start_screen_recipes)
|
||||
)
|
||||
val currentStartScreenName = startScreenOptions[currentStartScreen] ?: stringResource(R.string.start_screen_last)
|
||||
var isBiometricUnlockEnabled by remember { mutableStateOf(sharedPrefs.getBoolean("biometric_unlock_enabled", false)) }
|
||||
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
@@ -140,125 +146,157 @@ fun SettingsScreen(
|
||||
Text(text = stringResource(R.string.reset_shopping_lists_name))
|
||||
}
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Button(onClick = onResetRecipesTitle, modifier = Modifier.fillMaxWidth()) {
|
||||
Text(text = stringResource(R.string.reset_recipes_name))
|
||||
}
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
HorizontalDivider()
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.security),
|
||||
style = MaterialTheme.typography.titleLarge
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
// Biometric check
|
||||
val context = LocalContext.current
|
||||
val biometricManager = BiometricManager.from(context)
|
||||
val canUseBiometrics = remember {
|
||||
biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG) == BiometricManager.BIOMETRIC_SUCCESS
|
||||
}
|
||||
Button(onClick = onResetRecipesTitle, modifier = Modifier.fillMaxWidth()) {
|
||||
Text(text = stringResource(R.string.reset_recipes_name))
|
||||
}
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
HorizontalDivider()
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(enabled = canUseBiometrics) { if (canUseBiometrics) onAppLockChange(!isAppLockEnabled) }
|
||||
.padding(vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.app_lock),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.alpha(if (canUseBiometrics) 1f else 0.5f)
|
||||
)
|
||||
Switch(
|
||||
checked = isAppLockEnabled,
|
||||
onCheckedChange = onAppLockChange,
|
||||
enabled = canUseBiometrics
|
||||
)
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { onEncryptionToggle(!isEncryptionEnabled) }
|
||||
.padding(vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.sync_folder_encryption),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
modifier = Modifier.weight(1f) // Allow text to take available space
|
||||
)
|
||||
Switch(
|
||||
checked = isEncryptionEnabled,
|
||||
onCheckedChange = onEncryptionToggle
|
||||
)
|
||||
}
|
||||
|
||||
if (isEncryptionEnabled) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Button(onClick = onSetEncryptionPassword, modifier = Modifier.fillMaxWidth()) {
|
||||
Text(text = stringResource(if (hasEncryptionPassword) R.string.change_encryption_password else R.string.set_encryption_password))
|
||||
}
|
||||
if (hasEncryptionPassword) { Spacer(modifier = Modifier.height(8.dp))
|
||||
Button(onClick = onRemoveEncryption, modifier = Modifier.fillMaxWidth()) {
|
||||
Text(text = stringResource(R.string.remove_encryption))
|
||||
} }
|
||||
}
|
||||
}
|
||||
Text(
|
||||
text = stringResource(R.string.security),
|
||||
style = MaterialTheme.typography.titleLarge
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
// Biometric check
|
||||
val context = LocalContext.current
|
||||
val biometricManager = BiometricManager.from(context)
|
||||
val canUseBiometrics = remember {
|
||||
biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG) == BiometricManager.BIOMETRIC_SUCCESS
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
private fun ThemeChooserItem(
|
||||
theme: ColorTheme,
|
||||
isSelected: Boolean,
|
||||
onClick: () -> Unit
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(enabled = canUseBiometrics) { if (canUseBiometrics) onAppLockChange(!isAppLockEnabled) }
|
||||
.padding(vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
val themeColor = when (theme) {
|
||||
ColorTheme.STANDARD -> BluePrimary
|
||||
ColorTheme.GREEN -> GreenPrimary
|
||||
ColorTheme.VIOLET -> VioletPrimary
|
||||
ColorTheme.YELLOW -> YellowPrimary
|
||||
ColorTheme.ORANGE -> OrangePrimary
|
||||
ColorTheme.RED -> RedPrimary
|
||||
}
|
||||
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier.clickable(onClick = onClick).padding(4.dp)
|
||||
) {
|
||||
Box(
|
||||
Text(
|
||||
text = stringResource(R.string.app_lock),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.alpha(if (canUseBiometrics) 1f else 0.5f)
|
||||
)
|
||||
Switch(
|
||||
checked = isAppLockEnabled,
|
||||
onCheckedChange = onAppLockChange,
|
||||
enabled = canUseBiometrics
|
||||
)
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { onEncryptionToggle(!isEncryptionEnabled) }
|
||||
.padding(vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.data_encryption),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
modifier = Modifier.weight(1f) // Allow text to take available space
|
||||
)
|
||||
Switch(
|
||||
checked = isEncryptionEnabled,
|
||||
onCheckedChange = onEncryptionToggle
|
||||
)
|
||||
}
|
||||
|
||||
if (isEncryptionEnabled) {
|
||||
if (hasEncryptionPassword && canUseBiometrics) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.size(48.dp) // Reduced size
|
||||
.clip(CircleShape)
|
||||
.background(themeColor)
|
||||
.then(
|
||||
if (isSelected) {
|
||||
Modifier.border(2.dp, MaterialTheme.colorScheme.onBackground, CircleShape)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
val newValue = !isBiometricUnlockEnabled
|
||||
sharedPrefs.edit { putBoolean("biometric_unlock_enabled", newValue) }
|
||||
isBiometricUnlockEnabled = newValue
|
||||
}
|
||||
.padding(vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
if (isSelected) {
|
||||
val checkmarkColor = if (theme == ColorTheme.YELLOW) Color.Black else Color.White
|
||||
Icon(
|
||||
imageVector = Icons.Default.Check,
|
||||
contentDescription = stringResource(R.string.selected),
|
||||
tint = checkmarkColor,
|
||||
modifier = Modifier.size(24.dp) // Reduced size
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = stringResource(R.string.biometric_unlock),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
Switch(
|
||||
checked = isBiometricUnlockEnabled,
|
||||
onCheckedChange = {
|
||||
val newValue = it
|
||||
sharedPrefs.edit { putBoolean("biometric_unlock_enabled", newValue) }
|
||||
isBiometricUnlockEnabled = newValue
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Button(onClick = onSetEncryptionPassword, modifier = Modifier.fillMaxWidth()) {
|
||||
Text(text = stringResource(if (hasEncryptionPassword) R.string.change_encryption_password else R.string.set_encryption_password))
|
||||
}
|
||||
if (hasEncryptionPassword) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(text = stringResource(id = theme.titleRes), style = MaterialTheme.typography.bodyMedium)
|
||||
Button(onClick = onRemoveEncryption, modifier = Modifier.fillMaxWidth()) {
|
||||
Text(text = stringResource(R.string.remove_encryption))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
private fun ThemeChooserItem(
|
||||
theme: ColorTheme,
|
||||
isSelected: Boolean,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
val themeColor = when (theme) {
|
||||
ColorTheme.STANDARD -> BluePrimary
|
||||
ColorTheme.GREEN -> GreenPrimary
|
||||
ColorTheme.VIOLET -> VioletPrimary
|
||||
ColorTheme.YELLOW -> YellowPrimary
|
||||
ColorTheme.ORANGE -> OrangePrimary
|
||||
ColorTheme.RED -> RedPrimary
|
||||
}
|
||||
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier
|
||||
.clickable(onClick = onClick)
|
||||
.padding(4.dp)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(48.dp) // Reduced size
|
||||
.clip(CircleShape)
|
||||
.background(themeColor)
|
||||
.then(
|
||||
if (isSelected) {
|
||||
Modifier.border(2.dp, MaterialTheme.colorScheme.onBackground, CircleShape)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
if (isSelected) {
|
||||
val checkmarkColor = if (theme == ColorTheme.YELLOW) Color.Black else Color.White
|
||||
Icon(
|
||||
imageVector = Icons.Default.Check,
|
||||
contentDescription = stringResource(R.string.selected),
|
||||
tint = checkmarkColor,
|
||||
modifier = Modifier.size(24.dp) // Reduced size
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(text = stringResource(id = theme.titleRes), style = MaterialTheme.typography.bodyMedium)
|
||||
}
|
||||
}
|
||||
@@ -225,7 +225,7 @@
|
||||
<string name="lock_note">Notiz sperren</string>
|
||||
<string name="unlock_note">Sperre entfernen</string>
|
||||
|
||||
<string name="sync_folder_encryption">Synchronisationsordner verschlüsseln</string>
|
||||
<string name="data_encryption">Datenverschlüsselung</string>
|
||||
<string name="set_encryption_password">Verschlüsselungspasswort festlegen</string>
|
||||
<string name="change_encryption_password">Verschlüsselungspasswort ändern</string>
|
||||
<string name="remove_encryption">Verschlüsselung entfernen</string>
|
||||
@@ -262,4 +262,8 @@
|
||||
<string name="color_theme_yellow">Gelb</string>
|
||||
<string name="color_theme_orange">Orange</string>
|
||||
<string name="color_theme_red">Rot</string>
|
||||
<string name="confirm_to_disable_encryption">Bestätigen, um die Verschlüsselung zu deaktivieren und zu entfernen</string>
|
||||
<string name="confirm_to_change_password">Bestätigen, um das Verschlüsselungspasswort zu ändern</string>
|
||||
<string name="confirm_to_remove_encryption">Bestätigen, um die Verschlüsselung dauerhaft zu entfernen</string>
|
||||
<string name="biometric_unlock">Biometrisches Entsperren</string>
|
||||
</resources>
|
||||
@@ -225,7 +225,7 @@
|
||||
<string name="lock_note">Lock note</string>
|
||||
<string name="unlock_note">Remove lock</string>
|
||||
|
||||
<string name="sync_folder_encryption">Sync Folder Encryption</string>
|
||||
<string name="data_encryption">Data Encryption</string>
|
||||
<string name="set_encryption_password">Set Encryption Password</string>
|
||||
<string name="change_encryption_password">Change Encryption Password</string>
|
||||
<string name="remove_encryption">Remove Encryption</string>
|
||||
@@ -264,4 +264,8 @@
|
||||
<string name="color_theme_red">Red</string>
|
||||
<string name="error_corrupted_note">Note data is corrupted and cannot be decrypted.</string>
|
||||
<string name="error_corrupted_recipe">Recipe data is corrupted and cannot be decrypted.</string>
|
||||
<string name="confirm_to_disable_encryption">Confirm to disable and remove encryption</string>
|
||||
<string name="confirm_to_change_password">Confirm to change encryption password</string>
|
||||
<string name="confirm_to_remove_encryption">Confirm to permanently remove encryption</string>
|
||||
<string name="biometric_unlock">Biometric Unlock</string>
|
||||
</resources>
|
||||
132
decrypt.py
132
decrypt.py
@@ -1,5 +1,6 @@
|
||||
import base64
|
||||
import argparse
|
||||
import json
|
||||
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
||||
@@ -11,60 +12,123 @@ ITERATIONS = 10000
|
||||
KEY_LENGTH_BYTES = 32 # 256 bits
|
||||
IV_LENGTH_BYTES = 12
|
||||
|
||||
def derive_key(password, salt):
|
||||
"""Leitet den Schlüssel aus dem Passwort und dem Salt ab."""
|
||||
kdf = PBKDF2HMAC(
|
||||
algorithm=hashes.SHA256(),
|
||||
length=KEY_LENGTH_BYTES,
|
||||
salt=salt,
|
||||
iterations=ITERATIONS
|
||||
)
|
||||
return kdf.derive(password.encode('utf-8'))
|
||||
|
||||
def decrypt_payload(encrypted_payload, key):
|
||||
"""
|
||||
Entschlüsselt eine einzelne Datennutzlast (IV + Chiffretext).
|
||||
Gibt die entschlüsselten Bytes oder None bei einem Fehler zurück.
|
||||
"""
|
||||
if len(encrypted_payload) < IV_LENGTH_BYTES:
|
||||
return None
|
||||
|
||||
iv = encrypted_payload[:IV_LENGTH_BYTES]
|
||||
ciphertext = encrypted_payload[IV_LENGTH_BYTES:]
|
||||
|
||||
aesgcm = AESGCM(key)
|
||||
try:
|
||||
return aesgcm.decrypt(iv, ciphertext, None)
|
||||
except InvalidTag:
|
||||
return None
|
||||
|
||||
def process_decrypted_json(json_data, key):
|
||||
"""
|
||||
Durchläuft die JSON-Struktur und entschlüsselt verschachtelte Felder.
|
||||
"""
|
||||
if isinstance(json_data, list):
|
||||
for item in json_data:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
|
||||
# Notizen und Rezepte: Entschlüsselt das 'content'-Feld
|
||||
if 'content' in item and isinstance(item['content'], str):
|
||||
try:
|
||||
encrypted_content_bytes = base64.b64decode(item['content'])
|
||||
decrypted_content_bytes = decrypt_payload(encrypted_content_bytes, key)
|
||||
if decrypted_content_bytes is not None:
|
||||
item['content'] = decrypted_content_bytes.decode('utf-8')
|
||||
print(f" - Inhalt für '{item.get('title', 'Unbekannt')}' entschlüsselt.")
|
||||
except (ValueError, TypeError, base64.binascii.Error):
|
||||
# Inhalt ist kein Base64, also wahrscheinlich Klartext. Ignorieren.
|
||||
pass
|
||||
|
||||
# Einkaufslisten: Entschlüsselt 'encryptedItems'
|
||||
if 'shoppingList' in item and 'items' in item: # Struktur von ShoppingListWithItems
|
||||
shopping_list = item['shoppingList']
|
||||
if shopping_list.get('encryptedItems') and isinstance(shopping_list['encryptedItems'], str):
|
||||
try:
|
||||
encrypted_items_bytes = base64.b64decode(shopping_list['encryptedItems'])
|
||||
decrypted_items_json_bytes = decrypt_payload(encrypted_items_bytes, key)
|
||||
if decrypted_items_json_bytes is not None:
|
||||
# Die entschlüsselte Nutzlast ist ein weiterer JSON-String (die Artikelliste)
|
||||
item['items'] = json.loads(decrypted_items_json_bytes.decode('utf-8'))
|
||||
shopping_list['encryptedItems'] = None
|
||||
print(f" - Artikel für Einkaufsliste '{shopping_list.get('name', 'Unbekannt')}' entschlüsselt.")
|
||||
except (ValueError, TypeError, base64.binascii.Error):
|
||||
pass
|
||||
|
||||
return json_data
|
||||
|
||||
def decrypt_file(input_file, output_file, password):
|
||||
"""
|
||||
Decrypts a file encrypted by the Noteshop Android app.
|
||||
Entschlüsselt eine von der Noteshop-App exportierte Datei und führt eine
|
||||
tiefe Entschlüsselung für verschachtelte JSON-Inhalte durch.
|
||||
"""
|
||||
try:
|
||||
# 1. Salt dekodieren
|
||||
# 1. Schlüssel ableiten
|
||||
salt = base64.b64decode(SALT_B64)
|
||||
|
||||
# 2. Schlüssel aus dem Passwort ableiten (PBKDF2)
|
||||
kdf = PBKDF2HMAC(
|
||||
algorithm=hashes.SHA256(),
|
||||
length=KEY_LENGTH_BYTES,
|
||||
salt=salt,
|
||||
iterations=ITERATIONS
|
||||
)
|
||||
key = kdf.derive(password.encode('utf-8'))
|
||||
key = derive_key(password, salt)
|
||||
print("Schlüssel erfolgreich abgeleitet.")
|
||||
|
||||
# 3. Verschlüsselte Datei lesen
|
||||
# 2. Hauptdatei lesen und entschlüsseln
|
||||
with open(input_file, 'rb') as f:
|
||||
encrypted_data_with_iv = f.read()
|
||||
encrypted_file_bytes = f.read()
|
||||
|
||||
# 4. IV und verschlüsselte Daten trennen
|
||||
if len(encrypted_data_with_iv) < IV_LENGTH_BYTES:
|
||||
print("Fehler: Die verschlüsselte Datei ist zu kurz.")
|
||||
decrypted_data_bytes = decrypt_payload(encrypted_file_bytes, key)
|
||||
|
||||
if decrypted_data_bytes is None:
|
||||
print("Fehler: Entschlüsselung der Hauptdatei fehlgeschlagen. Das Passwort ist höchstwahrscheinlich falsch oder die Datei ist beschädigt.")
|
||||
return
|
||||
|
||||
iv = encrypted_data_with_iv[:IV_LENGTH_BYTES]
|
||||
ciphertext = encrypted_data_with_iv[IV_LENGTH_BYTES:]
|
||||
print(f"IV gefunden ({len(iv)} bytes), Chiffretext ({len(ciphertext)} bytes).")
|
||||
# 3. Versuchen, als JSON zu verarbeiten für die tiefe Entschlüsselung
|
||||
try:
|
||||
decrypted_json_str = decrypted_data_bytes.decode('utf-8')
|
||||
json_data = json.loads(decrypted_json_str)
|
||||
print("Hauptdatei erfolgreich entschlüsselt. Führe Tiefenentschlüsselung für JSON-Inhalt durch...")
|
||||
|
||||
# 5. Daten mit AES/GCM entschlüsseln
|
||||
aesgcm = AESGCM(key)
|
||||
|
||||
print("Entschlüsselung wird versucht...")
|
||||
decrypted_data = aesgcm.decrypt(iv, ciphertext, None)
|
||||
# 4. Tiefe Entschlüsselung durchführen
|
||||
fully_decrypted_data = process_decrypted_json(json_data, key)
|
||||
|
||||
# 6. Entschlüsselte Daten in die Ausgabedatei schreiben
|
||||
with open(output_file, 'wb') as f:
|
||||
f.write(decrypted_data)
|
||||
# 5. Vollständig entschlüsseltes JSON in die Ausgabedatei schreiben
|
||||
with open(output_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(fully_decrypted_data, f, ensure_ascii=False, indent=4)
|
||||
|
||||
print(f"Erfolg! Datei wurde erfolgreich tiefenentschlüsselt nach '{output_file}'.")
|
||||
|
||||
print(f"Erfolg! Datei wurde erfolgreich nach '{output_file}' entschlüsselt.")
|
||||
except (json.JSONDecodeError, UnicodeDecodeError):
|
||||
# Es war kein JSON, also die rohen entschlüsselten Daten schreiben (für Abwärtskompatibilität)
|
||||
print("Warnung: Entschlüsselter Inhalt ist kein JSON. Schreibe rohe entschlüsselte Daten.")
|
||||
with open(output_file, 'wb') as f:
|
||||
f.write(decrypted_data_bytes)
|
||||
print(f"Erfolg! Datei wurde (flach) entschlüsselt nach '{output_file}'.")
|
||||
|
||||
except InvalidTag:
|
||||
print("Fehler: Entschlüsselung fehlgeschlagen. Das Passwort ist höchstwahrscheinlich falsch oder die Datei ist beschädigt.")
|
||||
except Exception as e:
|
||||
print(f"Ein unerwarteter Fehler ist aufgetreten: {e}")
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser(description="Entschlüsselt Noteshop-Dateien.")
|
||||
parser.add_argument('-i', '--input', required=True, help="Pfad zur verschlüsselten Eingabedatei.")
|
||||
parser.add_argument('-o', '--output', required=True, help="Pfad zur entschlüsselten Ausgabedatei.")
|
||||
parser = argparse.ArgumentParser(description="Entschlüsselt Noteshop-Backup-Dateien (.json).")
|
||||
parser.add_argument('-i', '--input', required=True, help="Pfad zur verschlüsselten Eingabedatei (z.B. notes.json).")
|
||||
parser.add_argument('-o', '--output', required=True, help="Pfad zur vollständig entschlüsselten Ausgabedatei.")
|
||||
parser.add_argument('-p', '--password', required=True, help="Das Verschlüsselungspasswort.")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
decrypt_file(args.input, args.output, args.password)
|
||||
decrypt_file(args.input, args.output, args.password)
|
||||
Reference in New Issue
Block a user