Zum Inhalt

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

  1. 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.
  2. 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 von SHA-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) plus jti = 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.cryptomator lesen, Masterkey aus dem Schlüssel, Signatur + jti prüfen, Tresor mit dem Masterkey öffnen (wie nach dem Entsperren über den Server; krywerk-mount --recovery-key).
  • In Passwort-Tresor umwandeln: masterkey.cryptomator mit neuem Passwort (scrypt wie kw-format) schreiben, vault.cryptomator neu signieren mit kid = masterkeyfile:masterkey.cryptomator, jti = bisherige Tresor-ID, sonst gleiche Felder (Format 8, SIV_GCM, Kürzungsgrenze 220). Die alte Datei bleibt als vault.cryptomator.hub-<UTC-Zeit> liegen (zurück: umbenennen). Danach öffnet jede Cryptomator-App den Ordner mit dem Passwort. Reihenfolge absturzsicher: erst masterkey.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}, Rolle create-vaults; existiert die ID schon und man ist nicht OWNER → Fehler), vault.cryptomator mit neuem hub-Kopf (Endpunkte aus /api/config des Servers) neu signieren (alte Datei als vault.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üfen krywerkShare == 1, vaultId = erwartete ID, k = erwartete Schwelle, y 64 Byte.
  • Ablage: je Gremiumsmitglied die Nutzlast als JWE ECDH-ES + A256GCM (wie Access Tokens, kw-jose Variante A) an dessen User-ECDH-Schlüssel (ecdhPublicKey aus users/me bzw. 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 nur verifier = 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) an processPublicKey.
  • Abschluss: Antragsteller entschlüsselt processPrivateKey, damit alle eingereichten Anteile, prüft sie (gleiche vaultId/k, verschiedene x), rekonstruiert den Masterkey, prüft ihn gegen vault.cryptomator (falls der Ordner lokal vorliegt) und gegen den Prüfwert (eigene Berechnung), und sendet proof plus 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 (oder KRYWERK_RECOVERY_KEY fü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.