#!/usr/bin/env python3 """Oeffnet eine EasyKeys-Tresordatei OHNE den Code der App. pip install argon2-cffi cryptography python3 werkzeug/tresor_nachbau.py tresor.pwm # fragt nach dem Passwort python3 werkzeug/tresor_nachbau.py tresor.pwm --json # gibt den Klartext aus Der Zweck ist der Nachweis, nicht die Bedienung: Diese Datei kennt nur die Formatbeschreibung aus `tresor/Format.kt` und zwei fremde Bibliotheken (die Argon2-Referenzimplementierung und OpenSSL). Oeffnet sie eine Datei der App, dann ist die Datei genau das, was die Beschreibung sagt - Argon2id und AES-256-GCM, kein zweiter Schluessel, kein eigenes Verfahren. Gilt genauso fuer eine Sicherung (sie IST die Tresordatei) und fuer eine Geraetedatei `easykeys-*.ekv` (dort mit dem Abgleichspasswort). Offset Laenge Inhalt (Zahlen Big-Endian) 0 8 "PWMGRVLT" 8 2 Formatversion (1) 10 1 KDF (1 = Argon2id, Version 1.3) 11 4 Argon2 Speicher in KiB 15 4 Argon2 Durchgaenge 19 1 Argon2 Spuren 20 16 Salz 36 12 Nonce 48 n AES-256-GCM(JSON, UTF-8), 16 Byte Tag angehaengt; AAD = Byte 0..47 Das Passwort geht als UTF-8 in die Ableitung, heraus kommen 32 Byte Schluessel. """ import getpass import json import struct import sys from argon2.low_level import Type, hash_secret_raw from cryptography.exceptions import InvalidTag from cryptography.hazmat.primitives.ciphers.aead import AESGCM KOPF = 48 MAX_SPEICHER_KIB = 512 * 1024 # dieselbe Wache wie in der App: Der Kopf ist MAX_DURCHGAENGE = 100 # vor der Ableitung noch nicht authentifiziert. def oeffnen(daten, passwort): """Liefert (Kopfangaben, Klartext-Bytes). Wirft ValueError mit dem Grund.""" if len(daten) < KOPF + 16 or daten[:8] != b"PWMGRVLT": raise ValueError("keine EasyKeys-Datei") version, kdf, speicher, durchgaenge, spuren = struct.unpack(">HBIIB", daten[8:20]) if version != 1 or kdf != 1: raise ValueError("unbekannte Version %d / KDF %d" % (version, kdf)) if not (8 <= speicher <= MAX_SPEICHER_KIB and 1 <= durchgaenge <= MAX_DURCHGAENGE and spuren >= 1): raise ValueError("KDF-Werte ausserhalb der Grenzen") salz, nonce = daten[20:36], daten[36:48] schluessel = hash_secret_raw( secret=passwort.encode("utf-8"), salt=salz, time_cost=durchgaenge, memory_cost=speicher, parallelism=spuren, hash_len=32, type=Type.ID, version=19) try: klar = AESGCM(schluessel).decrypt(nonce, daten[KOPF:], daten[:KOPF]) except InvalidTag: raise ValueError("falsches Passwort oder veraenderte Datei") return {"speicherKiB": speicher, "durchgaenge": durchgaenge, "spuren": spuren}, klar def main(): if len(sys.argv) < 2: raise SystemExit(__doc__) daten = open(sys.argv[1], "rb").read() passwort = sys.argv[3] if len(sys.argv) > 3 and sys.argv[2] == "--passwort" \ else getpass.getpass("Passwort: ") try: kopf, klar = oeffnen(daten, passwort) except ValueError as e: raise SystemExit("nicht geoeffnet: %s" % e) tresor = json.loads(klar.decode("utf-8")) if "--json" in sys.argv: print(json.dumps(tresor, ensure_ascii=False, indent=2)) return print("Argon2id %(speicherKiB)d KiB, %(durchgaenge)d Durchgaenge, %(spuren)d Spur(en); " "AES-256-GCM" % kopf) print("%d Eintraege, %d im Papierkorb" % (len(tresor.get("eintraege", [])), len(tresor.get("papierkorb", [])))) if __name__ == "__main__": main()