Files
noteshop/decrypt.py
Désiré Werner Menrath 6ba7f16e89 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.
2025-10-19 15:03:03 +02:00

134 lines
6.0 KiB
Python

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
from cryptography.exceptions import InvalidTag
# --- Parameter aus der Android-App ---
SALT_B64 = "/y2B/g/y2B/g/y2B/g=="
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):
"""
Entschlüsselt eine von der Noteshop-App exportierte Datei und führt eine
tiefe Entschlüsselung für verschachtelte JSON-Inhalte durch.
"""
try:
# 1. Schlüssel ableiten
salt = base64.b64decode(SALT_B64)
key = derive_key(password, salt)
print("Schlüssel erfolgreich abgeleitet.")
# 2. Hauptdatei lesen und entschlüsseln
with open(input_file, 'rb') as f:
encrypted_file_bytes = f.read()
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
# 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...")
# 4. Tiefe Entschlüsselung durchführen
fully_decrypted_data = process_decrypted_json(json_data, key)
# 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}'.")
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 Exception as e:
print(f"Ein unerwarteter Fehler ist aufgetreten: {e}")
if __name__ == '__main__':
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)