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.
134 lines
6.0 KiB
Python
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) |