Wiederherstellungsschlüssel und Notfallzugriff für Team-Ordner (F9)¶
Eigenes Verfahren von Krywerk (Entscheidung 2026-10-14): nicht kompatibel mit dem Notfallzugriff oder den
Recovery Keys des Cryptomator Hub; funktioniert mit Krywerk Server und Krywerk-Clients (kw-hub-client, krywerk-hub,
krywerk-mount, Krywerk-App). Offizielle Cryptomator-Apps sind davon nicht betroffen (sie sehen nur normale Hub-Tresore).
Kein Hub-/Cryptomator-Quellcode als Grundlage; nur eigene Festlegungen und öffentliche Standards.
Begriffe in der Oberfläche: Wiederherstellungsschlüssel (Recovery Key), Notfallzugriff (Emergency Access), Notfallgremium (Council), Notfallfreigabe (Approval), Notfallantrag (Recovery Process).
1. Ziele und Bedrohungsmodell¶
- Wiederherstellungsschlüssel: Der 64-Byte-Masterkey eines Team-Ordners als abtippbarer/druckbarer Text. Wer ihn hat, kann den Team-Ordner ohne Server öffnen, in einen Passwort-Tresor umwandeln oder bei einem (neuen) Server neu registrieren. Für den Totalverlust des Servers und als versiegelte Hinterlegung bei der Leitung.
- Notfallzugriff (k von n, Vier-Augen-Prinzip): Ein Notfallgremium aus n Personen; mindestens k ≥ 2 müssen zustimmen, damit eine Person (Antragsteller, selbst im Gremium) Zugriff als „verwaltet“ auf einen Team-Ordner bekommt – z. B. wenn alle Verwaltenden ausgeschieden sind.
Annahmen: Der Server ist ehrlich-aber-neugierig (sieht nur verschlüsselte Daten, setzt Regeln durch). Einzelne Gremiumsmitglieder dürfen allein nichts erreichen (k ≥ 2). Ein bösartiger Server kann Zugriffsrechte ohnehin verfälschen (wie im Hub-Protokoll) – er kann aber ohne k Anteile keinen Masterkey rekonstruieren.
2. Wiederherstellungsschlüssel – Textformat¶
- Bytes:
0x01(Version) ‖ Masterkey (64 B: encKey ‖ macKey) ‖ Prüfsumme (5 B) = 70 Byte. Prüfsumme = die ersten 5 Byte vonSHA-256(0x01 ‖ Masterkey). - Kodierung: Crockford-Base32 (Alphabet
0123456789ABCDEFGHJKMNPQRSTVWXYZ, MSB zuerst, ohne Padding) → 70 × 8 = 560 Bit = 112 Zeichen, in 28 Gruppen à 4 Zeichen, getrennt durch-; Anzeige in 7 Zeilen à 4 Gruppen. - Einlesen (tolerant): Großbuchstaben; Leerraum, Zeilenumbrüche,
-,_,.ignorieren;O→0,I/L→1; andere Zeichen = Fehler „ungültiges Zeichen“. Länge ≠ 112 → Fehler mit Anzahl gefundener Zeichen. Version ≠ 1 → „unbekannte Version“. Prüfsumme falsch → „Tippfehler“ (ohne Position). - Ob der Schlüssel zum Team-Ordner gehört, zeigt die Signaturprüfung von
vault.cryptomator(HS256 mit dem Masterkey) plusjti= Tresor-ID; Fehlschlag → „gehört nicht zu diesem Team-Ordner“. - Der Schlüssel wird nie gespeichert, geloggt oder an den Server geschickt. Anzeige einmalig beim Anlegen; Verwaltende können ihn später erneut erzeugen (sie haben den Masterkey).
Beispielaufbau (kein echter Schlüssel): 0G4R-…-7Q2M (28 Gruppen).
3. Wiederherstellen ohne Server (Client, lokal)¶
- Öffnen:
vault.cryptomatorlesen, Masterkey aus dem Schlüssel, Signatur +jtiprüfen, Tresor mit dem Masterkey öffnen (wie nach dem Entsperren über den Server;krywerk-mount --recovery-key). - In Passwort-Tresor umwandeln:
masterkey.cryptomatormit neuem Passwort (scrypt wiekw-format) schreiben,vault.cryptomatorneu signieren mitkid = masterkeyfile:masterkey.cryptomator,jti= bisherige Tresor-ID, sonst gleiche Felder (Format 8, SIV_GCM, Kürzungsgrenze 220). Die alte Datei bleibt alsvault.cryptomator.hub-<UTC-Zeit>liegen (zurück: umbenennen). Danach öffnet jede Cryptomator-App den Ordner mit dem Passwort. Reihenfolge absturzsicher: erstmasterkey.cryptomator(temporär + umbenennen), dann alte Konfiguration sichern, dann neue Konfiguration schreiben. - Bei einem Server neu registrieren („Server verloren“): angemeldete Sitzung beim (neuen) Server; Tresor mit
derselben ID anlegen (
PUT /api/vaults/{id}, Rollecreate-vaults; existiert die ID schon und man ist nicht OWNER → Fehler),vault.cryptomatormit neuemhub-Kopf (Endpunkte aus/api/configdes Servers) neu signieren (alte Datei alsvault.cryptomator.hub-<Zeit>), eigenes Access Token anlegen. Mitglieder müssen danach neu aufgenommen und freigegeben werden.
4. Notfallzugriff – Kryptographie¶
- Shamir Secret Sharing über GF(2⁸) (Reduktionspolynom x⁸+x⁴+x³+x+1 = 0x11B), byteweise über die 64 Byte des Masterkeys; Polynom vom Grad k−1 je Byte mit zufälligen Koeffizienten (OS-Zufall); Anteil i hat x = i (1…n), y = 64 Byte. Rekonstruktion: Lagrange-Interpolation an x = 0 mit genau k (oder mehr) verschiedenen x. Grenzen: 2 ≤ k ≤ n ≤ 32.
- Anteil-Nutzlast (JSON, UTF-8):
{"krywerkShare":1,"vaultId":"<id>","x":<1..n>,"k":<k>,"y":"<Base64 std, 64 B>"}. Leser prüfenkrywerkShare == 1,vaultId= erwartete ID,k= erwartete Schwelle,y64 Byte. - Ablage: je Gremiumsmitglied die Nutzlast als JWE
ECDH-ES+A256GCM(wie Access Tokens,kw-joseVariante A) an dessen User-ECDH-Schlüssel (ecdhPublicKeyaususers/mebzw. Suche). Mitglieder ohne eingerichtetes Konto können nicht ins Gremium. - Prüfwert (damit der Server nur echte Wiederherstellungen abschließt):
proof = Base64url-ohne-Padding( HMAC-SHA256( key = macKey (Masterkey-Bytes 32..64), msg = UTF-8("krywerk-notfall-v1:" ‖ vaultId) ) ); der Server speichert nurverifier = Base64url-ohne-Padding( SHA-256( UTF-8(proof) ) ). - Antrag: Der Antragsteller erzeugt ein P-384-Schlüsselpaar für den Antrag (process key). Öffentlicher Teil:
SPKI Base64 (std)
processPublicKey; privater Teil als JWE-Nutzlast{"key":"<PKCS#8 Base64 std>"}, verschlüsselt (ECDH-ES) an den eigenen User-ECDH-Schlüssel des Antragstellers →processPrivateKey(überlebt so App-Neustarts/Gerätewechsel). - Zustimmung: Ein Gremiumsmitglied entschlüsselt seinen Anteil (eigener User-Schlüssel), prüft
vaultId/k, und verschlüsselt dieselbe Anteil-Nutzlast neu (ECDH-ES) anprocessPublicKey. - Abschluss: Antragsteller entschlüsselt
processPrivateKey, damit alle eingereichten Anteile, prüft sie (gleichevaultId/k, verschiedenex), rekonstruiert den Masterkey, prüft ihn gegenvault.cryptomator(falls der Ordner lokal vorliegt) und gegen den Prüfwert (eigene Berechnung), und sendetproofplus ein Access Token (Masterkey an den eigenen User-ECDH-Schlüssel).
5. Server-API (Krywerk-eigen, Präfix /api/krywerk)¶
Alle Endpunkte verlangen ein gültiges Token mit der normalen Nutzerrolle (wie die Hub-Endpunkte). Antworten JSON;
Zeitstempel ISO-8601 UTC mit Millisekunden. Fehler: 400 (Form), 403 (keine Berechtigung), 404 (Tresor/Antrag
unbekannt – auch für Fremde, die nicht berechtigt sind, den Antrag zu sehen: 404 statt 403, wenn sie weder
OWNER noch Gremium sind), 409 (Zustandskonflikt). Hub-Felder requiredEmergencyKeyShares/emergencyKeyShares
bleiben unberührt (immer 0 / {}); die Krywerk-Daten liegen in eigenen Tabellen.
| Methode + Pfad | Wer | Verhalten |
|---|---|---|
PUT /api/krywerk/vaults/{vaultId}/emergency |
OWNER des Tresors | Body {"requiredShares":k,"shares":{"<userId>":"<JWE>",…},"verifier":"<b64url>"}. Prüft: Tresor existiert (sonst 404), k ≥ max(2, emergency.min_required_shares), k ≤ n ≤ 32, alle userIds sind Nutzer mit ecdhPublicKey (sonst 400 mit Liste), JWEs fünfteilig, verifier Base64url 43 Zeichen. Ersetzt eine vorhandene Einrichtung vollständig; offene Anträge dieses Tresors werden abgebrochen. Ereignis SETUP. → 200 Status-Objekt (s. u.) |
DELETE /api/krywerk/vaults/{vaultId}/emergency |
OWNER | Einrichtung entfernen (Anteile, Prüfwert), offene Anträge abbrechen, Ereignis REMOVED → 204 |
GET /api/krywerk/vaults/{vaultId}/emergency |
OWNER oder Gremium | Status-Objekt: {"vaultId","configured":bool,"requiredShares":k,"council":[{"id","name","email"}],"myShare":"<JWE>"? (nur Gremium),"processes":[Antrag-Kurzform…]} (offene und die letzten 20 abgeschlossenen/abgebrochenen) |
GET /api/krywerk/emergency |
jeder | Übersicht für den Aufrufer: {"council":[{"vaultId","vaultName","requiredShares","councilSize","openProcesses":[Antrag-Kurzform…]}],"owned":[{"vaultId","vaultName","configured":bool,"openProcesses":[…]}]} – Tresore, in deren Gremium er sitzt, und Tresore, die er verwaltet |
POST /api/krywerk/emergency/processes |
Gremium des Tresors | Body {"vaultId","reason","processPublicKey","processPrivateKey"}; reason 1–1000 Zeichen ohne Steuerzeichen außer Zeilenumbruch; nur ein offener Antrag je Tresor (sonst 409); Tresor ohne Einrichtung → 409. Ereignis STARTED → 201 Antrag (Langform) |
GET /api/krywerk/emergency/processes/{processId} |
Gremium oder OWNER des Tresors | Antrag (Langform); nur für den Antragsteller zusätzlich processPrivateKey und recoveredShares {userId: JWE} |
POST /api/krywerk/emergency/processes/{processId}/approvals |
Gremium | Body {"share":"<JWE>"}; nur offen (sonst 409), je Person einmal (409). Ereignis APPROVED → 200 Antrag |
POST /api/krywerk/emergency/processes/{processId}/complete |
Antragsteller | Body {"proof","accessToken"}; offen (409), Zustimmungen ≥ k (409), SHA-256(proof) = verifier (sonst 403, Ereignis PROOF_FAILED). Danach: Antragsteller wird OWNER des Tresors (direkter Eintrag, vorhandene Rolle wird OWNER), Access Token gespeichert/ersetzt, Status COMPLETED, Ereignis COMPLETED → 200 Antrag |
DELETE /api/krywerk/emergency/processes/{processId} |
Antragsteller, Gremium oder OWNER | offen → ABORTED, Ereignis ABORTED → 200 Antrag; nicht offen → 409 |
Antrag-Kurzform: {"id","vaultId","vaultName","requester":{"id","name"},"reason","requiredShares","approvals":<Anzahl>,"status":"OPEN|COMPLETED|ABORTED|EXPIRED","creationTime","completionTime"?}.
Langform: zusätzlich "processPublicKey", "approvedBy":[{"id","name","time"}], "events":[{"time","actor":{"id","name"}?,"action"}]
(Aktionen: SETUP, REMOVED, STARTED, APPROVED, PROOF_FAILED, COMPLETED, ABORTED, EXPIRED; nur die
Ereignisse des Antrags bzw. beim Status-Objekt die des Tresors).
Ablauf: offene Anträge älter als emergency.process_max_age_days (Standard 14) gelten als EXPIRED
(beim Lesen bzw. vor jeder Änderung prüfen und speichern, Ereignis EXPIRED).
Löschkaskaden: Tresor löschen → Einrichtung, Anträge, Ereignisse weg. Konto zurücksetzen (users/me/reset) →
eigene Anteile bleiben gespeichert (sind dann nicht mehr entschlüsselbar; k von n trägt das).
Konfiguration (Server): emergency.min_required_shares (KRYWERK_SERVER_EMERGENCY_MIN_SHARES, Standard 2,
Werte < 2 werden als 2 behandelt), emergency.process_max_age_days (KRYWERK_SERVER_EMERGENCY_MAX_AGE_DAYS, Standard 14).
6. Client (kw-hub-client, krywerk-hub, krywerk-mount, Krywerk-App)¶
kw_hub_client::recovery: Kodierung/Dekodierung Wiederherstellungsschlüssel, Öffnen, Umwandeln, Neu registrieren.kw_hub_client::shamir: Aufteilen/Zusammensetzen (Tests: alle k-Teilmengen rekonstruieren, k−1 Anteile verraten nichts Prüfbares, doppelte x abgelehnt).HubSession:recovery_key(vault),emergency_status(vault_id),emergency_setup(vault, k, council),emergency_remove(vault_id),emergency_overview(),emergency_start(vault_id, reason),emergency_approve(process_id),emergency_complete(process_id, vault: Option<&VaultRef>),emergency_abort(process_id).krywerk-hub:wiederherstellungsschluessel <ordner>,wiederherstellen <ordner> [--passwort-tresor | --server URL],notfall einrichten <ordner> --schwelle K <wer…>,notfall status [<ordner>],notfall entfernen <ordner>,notfall beantragen <ordner> --grund TEXT,notfall zustimmen <antrag-id>,notfall abschliessen <antrag-id> [<ordner>],notfall abbrechen <antrag-id>. Wiederherstellungsschlüssel verdeckt einlesen (oderKRYWERK_RECOVERY_KEYfür Tests); Anzeige nur auf stdout, nie in Protokollen.krywerk-mount <team-ordner> --recovery-key: einhängen ohne Server.- Krywerk-App: beim Anlegen eines Team-Ordners Wiederherstellungsschlüssel einmalig anzeigen (Kopieren, Speichern, Drucken, „Ich habe ihn sicher aufbewahrt“ wie beim Freischaltcode); „Verwalten“: Wiederherstellungsschlüssel anzeigen, Notfallzugriff einrichten/entfernen; Karte „Notfallzugriff“: Anträge sehen, zustimmen, abschließen; offene Anträge zählen im Infobereich.