Zum Inhalt

Cryptomator Hub – Client- und Server-Spezifikation (Clean-Room-Beschreibung)

Stand: 2026-10-13. Grundlage für Phase F (Krywerk als Hub-Client) und für einen eigenen, Hub-kompatiblen Server in Rust. Diese Datei beschreibt ausschließlich Verhalten, Formate, Endpunkte, Algorithmen und Parameter. Sie enthält bewusst keinen kopierten Quellcode (Hub ist AGPL-3.0, Cryptomator-Desktop GPL-3.0). Wer den Server implementiert, soll ausschließlich mit diesem Dokument arbeiten.

Markierungen: - (ungeprüft) – nicht im Code verifiziert, nur plausibel oder aus Doku/Namen abgeleitet. - (beobachtet) – am lokalen Test-Stack (Hub 2.1.2, Keycloak 26.7.5) per HTTP nachgeprüft.

0. Quellen und Versionen

Kürzel Repository Stand
HUB cryptomator/hub, Tag 2.1.2 (Commit 2887647a) – entspricht dem Test-Stack (/api/version → hubVersion 2.1.2) maßgeblich
HUB-dev cryptomator/hub, Branch develop (Commit d7e72f38, Version 2.2.0-SNAPSHOT, apiLevel 5, UVF) nur für Abschnitt 13/14
DESK cryptomator/cryptomator, develop (Commit 6c03aa9c, 1.20.0-SNAPSHOT; Hub-Teil identisch mit 1.19.x ab 1.19.1 (ungeprüft im Detail)) maßgeblich für Client
T2C coffeelibs/tiny-oauth2-client Tag 0.8.1 (vom Desktop benutzte OAuth-Bibliothek) OIDC-Details
CL / CFS cryptomator/cryptolib (0af261a), cryptomator/cryptofs (9eb7e59) Masterkey, vault.cryptomator
IOS / AND cryptomator/ios (047f538), cryptomator/android (0d12781) nur überflogen

Pfadabkürzungen: HUB-API = backend/src/main/java/org/cryptomator/hub/api/, HUB-FE = frontend/src/, DESK-HUB = src/main/java/org/cryptomator/ui/keyloading/hub/.

1. Überblick und Glossar

Hub ist ein Schlüsselverteilungsdienst: Er speichert nur verschlüsselte Schlüssel (JWEs) und Zugriffsrechte. Alle Kryptographie (Masterkey erzeugen, für Nutzer verschlüsseln, entschlüsseln) findet in Clients statt (Browser-Web-UI, Desktop, Mobile). Der Server signiert für Format-8-Tresore nichts.

Begriff Bedeutung
Vault Key / Masterkey 64 Byte Zufall = Format-8-Masterkey: Bytes 0–31 = Verschlüsselungsschlüssel (encKey), Bytes 32–63 = MAC-Schlüssel (macKey) (CL Masterkey-Konstruktor; HUB-FE common/crypto.ts VaultKeys.hashDirectoryId bestätigt die Reihenfolge). Gleichzeitig HMAC-Schlüssel für die Signatur von vault.cryptomator.
User Key Pair Pro Nutzer zwei P-384-Schlüsselpaare: ECDH (zum Empfangen von Vault Keys) und ECDSA (Web-of-Trust-Signaturen, seit Hub 1.4). Öffentliche Teile liegen im Klartext auf dem Server, private nur verschlüsselt. (HUB-FE crypto.ts UserKeys)
Device Key Pair P-384-ECDH-Schlüsselpaar pro Gerät (Desktop, Mobile, Browser). Der private User-ECDH-Key wird je Gerät mit ECDH-ES auf den Device Key verschlüsselt.
Account Key / Setup Code Geheimnis des Nutzers (Web-UI erzeugt eine zufällige UUID als String, HUB-FE components/InitialSetup.vue fetchData). Schützt die privaten User Keys per PBES2. In der UI „Account Key“, im API-Feld setupCode.
Recovery Key Masterkey + 2 Byte Prüfsumme, als 44 Wörter kodiert (Abschnitt 11). Nur für Wiederherstellung durch den Owner.
Admin Password (legacy) Hub 1.0–1.2: Vault-Masterkey per PBKDF2-Passwort auf dem Server gespeichert; nur noch für „Claim Ownership“-Migration (Abschnitt 12).
Vault ID UUID (zufällig vom Ersteller erzeugt). Steht als jti im Payload und als letztes Pfadsegment der kid von vault.cryptomator.
Hub ID UUID der Hub-Instanz (settings.hub_id, GET /api/settings → hubId); bindet die Lizenz. Für Clients irrelevant.
apiLevel Ganzzahl in GET /api/config; Clients wählen danach den Unlock-Ablauf (Abschnitt 9). Hub 2.1.2 liefert 4 (HUB-API ConfigResource.getConfig; beobachtet).
Access Token (Hub-Sprech) Nicht das OAuth-Token, sondern der pro (Nutzer, Vault) gespeicherte JWE mit dem Vault Key. Tabelle access_token, Spalte vault_masterkey.
Bearer Token OAuth2/OIDC Access Token von Keycloak, für alle authentifizierten API-Aufrufe.

Schlüsselhierarchie (Format 8):

Setup Code ──PBES2──► privateKeys-JWE (auf Server) ──► User-ECDH-Privkey (+ ECDSA-Privkey)
Device-Privkey ──ECDH-ES──► userPrivateKey-JWE (pro Gerät, auf Server) ──► User-ECDH-Privkey
User-ECDH-Privkey ──ECDH-ES──► Access-Token-JWE (pro Nutzer+Vault, auf Server) ──► 64-Byte-Masterkey
Masterkey ──HMAC-SHA256──► Signatur von vault.cryptomator (liegt nur im Tresor, nicht auf dem Server)

2. vault.cryptomator eines Hub-Tresors

Erzeugt im Browser (HUB-FE common/vaultconfig.ts VaultConfig.create, common/crypto.ts VaultKeys.createVaultConfig). JWT in Compact Serialization, alle drei Teile base64url ohne Padding.

Header (JSON, Feldreihenfolge wie erzeugt: kid, typ, alg, hub):

Feld Wert
kid "hub+" + absoluteApiBaseUrl + "vaults/" + vaultId, z. B. hub+https://hub.example.com/api/vaults/3f1c… (absoluteApiBaseUrl endet auf /api/)
typ "jwt" (klein geschrieben – anders als bei normalen Format-8-Tresoren)
alg "HS256"
hub Objekt HubConfig, siehe unten

hub-Objekt (HUB-FE crypto.ts Interface VaultConfigHeaderHub; DESK-HUB HubConfig):

Feld Inhalt bei Erzeugung Desktop-Nutzung
clientId keycloakClientIdCryptomator aus /api/config (Standard "cryptomator") OAuth client_id
authEndpoint keycloakAuthEndpoint aus /api/config Authorization Endpoint
tokenEndpoint keycloakTokenEndpoint aus /api/config Token Endpoint
authSuccessUrl <origin>/app/unlock-success?vault=<vaultId> Browser-Redirect nach Login; Desktop hängt &device=<deviceId> an
authErrorUrl <origin>/app/unlock-error?vault=<vaultId> dito bei Fehler
apiBaseUrl <origin>/api/ Basis aller API-Aufrufe (nullable; fehlt in Hub-1.0-Tresoren)
devicesResourceUrl <origin>/api/devices/ (veraltet) Fallback: API-Basis = devicesResourceUrl + "/.." normalisiert

Unbekannte Felder werden vom Desktop ignoriert. Payload: jti = Vault-ID (UUID-String), format = 8, cipherCombo = "SIV_GCM", shorteningThreshold = 220. Kein iat/exp.

Signatur: HMAC-SHA256 über b64url(header) "." b64url(payload) mit dem vollen 64-Byte-Masterkey als HMAC-Schlüssel (identisch zu normalen Format-8-Tresoren). Der Server kennt den Masterkey nie und kann die Datei weder erzeugen noch prüfen. Desktop/cryptofs akzeptieren HS256/HS384/HS512 (CFS VaultConfig.UnverifiedVaultConfig.initAlgorithm).

Parsing im Desktop (CFS VaultConfig.decode, getKeyId, getHeader; DESK-HUB HubKeyLoadingStrategy, HubKeyLoadingModule.provideHubConfig, ReceiveKeyController.extractVaultId): 1. JWT ungeprüft dekodieren; kid als URI parsen. 2. Key-Loading-Strategie nach URI-Schema: hub+http und hub+https → Hub-Strategie (Präfix hub+ wird geprüft). Andere Schemata (masterkeyfile:) → normale Passwort-Strategie. 3. Vault-ID = Teil des URI-Pfads nach dem letzten /. Host/Pfad der kid werden sonst nicht benutzt; alle URLs kommen aus dem Header-Objekt hub. 4. Masterkey per Hub holen (Abschnitt 6), danach erst die HMAC-Signatur prüfen. Folge: Der Header ist bis dahin nicht authentisch – deshalb die Host-Vertrauensprüfung (Abschnitt 6.1, Sicherheitsfix GHSA-34rf-rwr3-7g43, Desktop 1.19.1).

ZIP-Vorlage (HUB-FE vaultconfig.ts VaultConfig.exportTemplate; Download "<Tresorname>.zip"): vault.cryptomator (obiges JWT) und ein leerer Ordner d/<XX>/<YYYY…30> für das Stammverzeichnis (dirIdHash("") wie Format 8). Kein masterkey.cryptomator, keine dirid.c9r. Der Nutzer entpackt das ZIP in den Cloud-Speicher.

3. Kryptographische Formate

3.1 Kurven und Schlüsselkodierungen

Objekt Kurve Kodierung Transport
User ECDH Public Key P-384 SPKI DER Base64 Standard mit Padding (ecdhPublicKey, legacy publicKey)
User ECDSA Public Key P-384 SPKI DER Base64 Standard (ecdsaPublicKey)
Device Public Key P-384 SPKI DER Base64 Standard (publicKey im Device). Hub-1.x-Legacy: base64url ohne Padding
Private Keys (in JWE-Payloads) P-384 PKCS#8 DER Base64 Standard
Masterkey (in JWE-Payload) – 64 Byte roh Base64 Standard
JWE/JWS/JWT-Teile – – base64url ohne Padding

Server-Validierung nur per Regex (HUB validation/*): OnlyBase64Chars = Zeichen [+/A-Za-z0-9] gefolgt von beliebig vielen =; ValidJWE = fünf durch Punkte getrennte base64url-Teile (2. und 3. und 5. dürfen leer sein); ValidId = Zeichen aus [-_.a-zA-Z0-9] (auch leer); ValidJWS analog dreiteilig (ungeprüft im Detail).

3.2 JWE-Variante A: ECDH-ES + A256GCM (Direct Key Agreement)

Verwendung: Vault Key → Nutzer, User Key → Gerät, Setup Code → Nutzer selbst, Legacy Vault Key → Gerät. (HUB-FE common/jwe.ts JWEBuilder.ecdhEs, ECDH_ES.deriveContentKey, ConcatKDF.kdf; DESK-HUB JWEHelper.encryptKey über Nimbus JOSE.)

  • Protected Header: alg = "ECDH-ES", enc = "A256GCM", epk = ephemerer P-384-Public-Key als JWK (kty "EC", crv "P-384", x, y; WebCrypto-Exporte enthalten zusätzlich ext/key_ops – Leser müssen unbekannte JWK-Felder tolerieren (ungeprüft, aber funktioniert offensichtlich mit Nimbus)). Web-UI setzt apu und apv auf base64url des leeren Strings (also ""); Desktop (Nimbus) lässt sie weg. Fehlend und leer sind kryptographisch gleichwertig.
  • Encrypted Key: leer (zweiter Teil der Compact Serialization ist leer).
  • Z = ECDH(ephemer, Empfänger) = x-Koordinate, 48 Byte.
  • CEK = ConcatKDF-SHA-256 (NIST SP 800-56A, RFC 7518 §4.6.2): CEK = SHA256( be32(1) ‖ Z ‖ OtherInfo )[0..32] mit OtherInfo = be32(len("A256GCM")) ‖ "A256GCM" ‖ be32(len(apu)) ‖ apu ‖ be32(len(apv)) ‖ apv ‖ be32(256) (apu/apv = base64url-dekodierte Bytes, hier jeweils Länge 0). Eine Runde genügt (256 Bit).
  • Inhalt: AES-256-GCM, IV 12 Byte zufällig, Tag 128 Bit, AAD = ASCII-Bytes des base64url-kodierten Protected Headers. Klartext = UTF-8-JSON.

3.3 JWE-Variante B: PBES2-HS512+A256KW + A256GCM

Verwendung: private User Keys unter dem Setup Code (privateKeys). (HUB-FE jwe.ts JWEBuilder.pbes2, PBES2.deriveWrappingKey; DESK-HUB JWEHelper.decryptUserKey(jwe, setupCode).)

  • Header: alg = "PBES2-HS512+A256KW", enc = "A256GCM", p2s = base64url(16 Zufallsbytes), p2c = 1 000 000 (Standard der Web-UI, PBES2.DEFAULT_ITERATION_COUNT), apu/apv = "".
  • KEK = PBKDF2-HMAC-SHA512(Passwort = UTF-8(Setup Code), Salt = UTF-8("PBES2-HS512+A256KW") ‖ 0x00 ‖ p2s-Bytes, Iterationen = p2c, Länge 32 Byte).
  • CEK = 32 Byte zufällig; Encrypted Key = AES Key Wrap (RFC 3394) des CEK mit KEK → 40 Byte.
  • Inhalt wie oben (AES-256-GCM, AAD = Header-b64url).
  • Falscher Setup Code ⇒ Key-Unwrap scheitert (Integritätsprüfung von AES-KW). Desktop zeigt dann „ungültiger Setup Code“ (DESK-HUB RegisterDeviceController.setupFailed).

3.4 Payload-Formen

JWE Variante Payload-JSON
Access Token (Vault Key für Nutzer) A {"key": "<Base64 der 64 Masterkey-Bytes>"}
privateKeys (User Keys unter Setup Code) B {"key": "<B64 PKCS8 ECDH>", "ecdhPrivateKey": "<B64 PKCS8 ECDH>", "ecdsaPrivateKey": "<B64 PKCS8 ECDSA>"} – key ist redundant „für Abwärtskompatibilität“
userPrivateKey eines Geräts (von Web-UI erzeugt) A wie privateKeys (drei Felder)
userPrivateKey eines Geräts (vom Desktop erzeugt) A nur {"key": "<B64 PKCS8 ECDH>"} (DESK-HUB JWEHelper.encryptUserKey)
setupCode (Setup Code für den Nutzer selbst) A, Empfänger = eigener User-ECDH-Key {"setupCode": "<Setup Code>"}
Legacy Access Token (Hub 1.x, an Gerät) A, Empfänger = Device Key {"key": "<Base64 Masterkey>"}

Wichtig für Kompatibilität: Desktop (und laut Feldnamen auch iOS/Android) liest bei User Keys ausschließlich das Feld key (DESK-HUB JWEHelper.readKey mit Feldname key). Jeder Erzeuger von privateKeys/userPrivateKey muss daher key befüllen. Browser lesen ecdhPrivateKey und fallen auf key zurück (HUB-FE UserKeys.createFromJwe). Base64 hier ist Standard-Alphabet mit Padding (Java Base64.getDecoder würde base64url ablehnen).

4. OIDC-Login des Desktop-Clients

(DESK-HUB AuthFlowTask.call, AuthFlowController; T2C AuthorizationCodeGrant, PKCE, http/RedirectTarget, util/RandomUtil.)

  • Flow: Authorization Code mit PKCE, öffentlicher Client ohne Secret.
  • client_id = hub.clientId aus dem Vault-Header (Standard cryptomator).
  • Scopes: keine (scope-Parameter wird weggelassen). Keycloak liefert dann Standard-Scopes.
  • PKCE: code_verifier = base64url (ohne Padding) von 33 Zufallsbytes (44 Zeichen; angefragt „43“, auf Vielfaches von 3 Bytes aufgerundet); code_challenge = base64url(SHA-256(ASCII(verifier))), code_challenge_method = S256.
  • state: base64url von 12 Zufallsbytes (16 Zeichen); wird bei Rückkehr exakt verglichen.
  • Redirect: http://127.0.0.1:<Port>/<Pfad> – Host ist die Loopback-IP (nicht localhost, RFC 8252 §8.3), Port vom Betriebssystem zugewiesen (Port 0), Pfad = / + 16 zufällige base64url-Zeichen. Der lokale Server akzeptiert nur exakt diesen Pfad; prüft state; liest code oder error.
  • Antwort an den Browser: HTTP-Redirect auf authSuccessUrl + "&device=" + deviceId bzw. authErrorUrl + "&device=" + deviceId (der Hub zeigt dort eine „Sie können zurück zur App“-Seite).
  • Authorization-URL: bestehende Query des authEndpoint bleibt erhalten, ergänzt um response_type=code, client_id, state, code_challenge, code_challenge_method, redirect_uri.
  • Token Request: POST an tokenEndpoint, form-encoded: grant_type=authorization_code, client_id, code_verifier, code, redirect_uri. Timeout 10 s. Erwartet HTTP 200; verwendet nur access_token aus der JSON-Antwort. Refresh Token wird nicht genutzt; jedes Entsperren loggt neu ein (Browser-SSO-Session macht das meist unsichtbar).
  • Keycloak-Client cryptomator (HUB backend/src/main/resources/cryptomator-realm.json, deploy/compose/prod/compose.yaml): publicClient: true, Attribut pkce.code.challenge.method = S256, redirectUris: http://127.0.0.1/*, org.cryptomator.ios:/hub/auth, org.cryptomator.android:/hub/auth. Keycloak erlaubt bei Loopback-IP beliebige Ports (RFC 8252) (ungeprüft für andere IdPs).
  • Mobile: iOS AppAuth mit Redirect org.cryptomator.ios:/hub/auth, scopes: nil (IOS CryptomatorHubAuthenticator+HubAuthenticating.swift); Android org.cryptomator.android:/hub/auth (AND CryptoConstants.HUB_REDIRECT_URL).
  • Alle API-Aufrufe: Header Authorization: Bearer <access_token>.

5. Geräteschlüssel und Geräteregistrierung (Desktop)

Erzeugung/Speicherung (DESK common/settings/DeviceKey.loadOrCreate): Ein P-384-Schlüsselpaar pro Installation (nicht pro Hub/Tresor), gespeichert als PKCS#12-Datei (cryptomator.p12Path, z. B. ~/.config/Cryptomator/key.p12, macOS ~/Library/Application Support/Cryptomator/key.p12). Das PKCS#12-Passwort ist eine zufällige UUID im System-Schlüsselbund unter dem Eintrag cryptomator-device-p12. Fehlt Datei oder Passwort, wird neu erzeugt (⇒ neues Gerät). Ohne Schlüsselbund-Unterstützung bricht der Desktop mit „kein Schlüsselbund“ ab.

Device ID (DESK-HUB HubKeyLoadingModule.provideDeviceId; identisch im Browser HUB-FE BrowserKeys.id): deviceId = HEX_UPPER( SHA-256( SPKI-DER(devicePublicKey) ) ) → 64 Zeichen.

Registrierung (DESK-HUB RegisterDeviceController.register), ausgelöst durch 404 bei GET /api/devices/{deviceId}: 1. UI fragt Gerätename (Vorbelegung: Hostname) und Setup Code (Account Key). 2. GET /api/users/me (Bearer). Erwartet 200. Liest publicKey (= User-ECDH-SPKI, Base64) und privateKey (= privateKeys-JWE) – die Legacy-Feldnamen. 3. Best-Effort-Migration von Hub-1.x-Geräten (Abschnitt 9.2): GET /api/devices/{deviceId}/legacy-access-tokens. 4. privateKey-JWE mit Setup Code entschlüsseln (Variante B) → User-ECDH-Privkey (PKCS#8 aus Feld key). 5. User-Privkey per Variante A für den eigenen Device-Public-Key verschlüsseln, Payload {"key": …}. 6. PUT /api/devices/{deviceId}, Content-Type: application/json, Body: {"id": deviceId, "name": <Name>, "publicKey": <Base64 SPKI Device>, "type": "DESKTOP", "userPrivateKey": <JWE>, "creationTime": <ISO-8601 Instant, z. B. "2026-10-13T12:00:00.123Z">}. 7. 201 → Erfolgsseite, danach automatisch erneuter Schlüsselempfang (DESK-HUB RegisterSuccessController.complete). 409 → „Gerät existiert bereits (bei anderem Nutzer)“. Sonst Fehler. Falscher Setup Code → Feld als ungültig markiert.

iOS/Android registrieren identisch mit type: "MOBILE" (IOS CryptomatorHubAuthenticator.registerDevice; AND HubRepositoryImpl). Browser: type: "BROWSER", Device Key im IndexedDB unter der User-ID.

6. Unlock-Ablauf des Desktops (apiLevel ≥ 1)

6.1 Host-Vertrauensprüfung (vor jedem Netzwerkzugriff)

(DESK-HUB CheckHostTrustController.initialize, isConsistentHubConfig.) „Authority“ = schema://host[:port]. 1. Konsistenz: Authority von apiBaseUrl (bzw. aus devicesResourceUrl abgeleitet), devicesResourceUrl, authSuccessUrl, authErrorUrl müssen gleich der API-Authority sein; tokenEndpoint muss die Authority des authEndpoint haben. Sonst Abbruch („nicht vertrauenswürdiger Host“). 2. API- und Auth-Host enden beide auf .cryptomator.cloud und kein HTTP → vertraut. 3. Beide Authorities in System-Property cryptomator.hub.allowedHosts (kommagetrennt) → vertraut. 4. HTTP (nicht HTTPS) und nicht beide Hosts exakt localhost → abgelehnt. 5. cryptomator.hub.enableTrustOnFirstUse=true (in allen offiziellen Installern gesetzt, DESK dist/*) und beide Authorities bereits in den Einstellungen trustedHosts → vertraut; sonst Nutzer fragen (TOFU) und bei Zustimmung speichern. 6. Sonst ablehnen.

Konsequenz für einen eigenen Server: HTTPS (außer localhost), Erfolgs-/Fehlerseiten auf demselben Origin wie die API, Token- und Auth-Endpoint auf demselben Origin; Nutzer bestätigen den Host beim ersten Mal.

6.2 Anfragen der Reihe nach

(DESK-HUB ReceiveKeyController; HTTP/1.1, Timeout je 10 s; URLs = API-Basis + relativer Pfad.)

# Anfrage Header Erwartung / Reaktion
0 OIDC-Login (Abschnitt 4) – Bearer Token
1 GET users/me?withDevices=false Bearer 200 → Feld name wird als Dateisystem-Besitzername gemerkt. Jeder andere Status ⇒ Fehler
2 GET config ohne Auth 200; apiLevel ≥ 1 → Schritt 3, sonst Legacy (9.1)
3 GET devices/{deviceId} Bearer 200 → userPrivateKey (JWE) und name lesen. 404 → Registrierung (Abschnitt 5). Sonst Fehler
4 GET vaults/{vaultId}/access-token Bearer, Hub-Device-ID: <deviceId> siehe Tabelle 6.3
5 lokal – userPrivateKey mit Device-Privkey entschlüsseln (A) → User-Privkey; Access-Token mit User-Privkey entschlüsseln (A) → key → 64-Byte-Masterkey; danach vault.cryptomator-HMAC prüfen

Mobile Apps fragen zuerst access-token, danach devices/{id} (IOS CryptomatorHubAuthenticator), Ergebnis identisch. iOS verlangt apiLevel ≥ 2 (Konstante minimumHubVersion = 2), Android ≥ 1 (AND UnlockHubVault.HUB_MINIMUM_VERSION).

6.3 Statuscodes von GET /api/vaults/{vaultId}/access-token

(HUB-API VaultResource.unlock; Reihenfolge der Prüfungen auf dem Server, soweit bestimmbar.)

Status Bedingung (Server) Desktop-Reaktion
401 Kein/ungültiges Bearer Token Fehler
403 Nutzer hat effektiv weder Rolle MEMBER noch OWNER; auch bei unbekannter Vault-ID (Filter-Standard „FORBIDDEN bei fehlendem Vault“, beobachtet) „Gerät/Nutzer nicht autorisiert“ (UnauthorizedDeviceController)
402 Lizenz abgelaufen (ActiveLicense-Filter) oder Lizenz noch nicht eingerichtet (globaler Setup-Filter) oder mehr sitzplatzbelegende Nutzer mit Access Token als Lizenz-Sitze „Lizenz ungültig/überschritten“ (InvalidLicenseController)
410 Vault archiviert und Query evenIfArchived nicht true Fenster schließen, Dialog „Tresor archiviert“
449 Nutzer ist (effektiv) Mitglied, hat aber noch keinen ecdhPublicKey (Account nicht eingerichtet) (beobachtet; Nichtmitglied ohne Account → 403) „Account einrichten“; Button öffnet <Web-App>/profile mit Web-App = API-Basis + ../app/
403 Mitglied, aber noch kein Access Token für (Nutzer, Vault) hinterlegt (Text „Access to this device not granted“) wie 403 oben
200 Access Token vorhanden; Body = JWE als text/plain entsperren

Server-Seiteneffekte bei 200: Audit-Event VaultKeyRetrieved (Ergebnis SUCCESS/UNAUTHORIZED, IP, Device-ID); wenn Hub-Device-ID zu einem Gerät des Nutzers gehört: lastAccessTime und lastIpAddress des Geräts aktualisieren (unbekannte IDs werden ignoriert). Zusätzliche Antwort-Header bei vorhandener Lizenz: Hub-iOS-License (JWT) plus Hub-Subscription-State: ACTIVE, sowie Hub-Android-License (JWT) (Abschnitt 15). Die Filter-Reihenfolge zwischen 402 (Lizenz) und 403 (Rolle) ist nicht festgelegt (beide gleiche Priorität) (ungeprüft). 404 ist dokumentiert, wird aber praktisch nicht erreicht.

7. Datenmodell des Servers (Hub 2.1.2)

(HUB entities/*, Flyway-Migrationen backend/src/main/resources/org/cryptomator/hub/flyway/V*.sql.)

Entität Felder Bemerkungen
Authority id (String, PK = Keycloak-ID), type (USER/GROUP), name, pictureUrl? Oberklasse für User und Group
User + email?, firstName?, lastName?, language?, enabled (bool), ecdhPublicKey?, ecdsaPublicKey?, privateKeys? (JWE B), setupCode? (JWE A) ecdhPublicKey == null ⇔ Account nicht eingerichtet
Group Mitglieder = Authorities (User oder Gruppen; verschachtelt)
EffectiveGroupMembership (group_id, member_id, intermediate_group_ids[]) Transitive Hülle; Tabelle, bei Sync gepflegt
Device id (PK, 64 Hex), owner (User), name, type (BROWSER/DESKTOP/MOBILE), publickey, userPrivateKeys (JWE A), creationTime, lastAccessTime?, lastIpAddress?
Vault id (UUID), name, description?, creationTime, archived, requiredEmergencyKeyShares (int), emergencyKeyShares (Map UserID→JWE), Legacy: salt?, iterations?, masterkey?, authPublicKey?, authPrivateKey? Server speichert kein vault.cryptomator
VaultAccess (vault_id, authority_id) PK, role (OWNER/MEMBER) Direkte Mitgliedschaft von User oder Gruppe
EffectiveVaultAccess (View) (vault_id, authority_id, role) Vereinigung: direkte VaultAccess-Einträge ∪ (VaultAccess einer Gruppe × alle effektiven Mitglieder dieser Gruppe). Ein Nutzer kann mehrere Rollen effektiv haben
AccessToken (user_id, vault_id) PK, vault_masterkey (JWE A) Nur gültig, solange auch EffectiveVaultAccess besteht (Unlock-Abfrage joint beide). Entfernen eines Mitglieds löscht den Token nicht
Settings (eine Zeile) hubId, licenseKey?, wotMaxDepth, wotIdVerifyLen, enableEmergencyAccess, defaultRequiredEmergencyKeyShares, defaultMinMembers, allowChoosingEmergencyCouncil, emergencyCouncilMemberIds
WotEntry / EffectiveWot wot: (user_id = signiert, signer_id) PK, signature (JWS, opak); effective_wot (View): (trusting_user_id, trusted_user_id, signature_chain[]) Web of Trust, Abschnitt 16a; keine Löschung bei Reset
LegacyDevice / LegacyAccessToken Hub-1.x-Geräte und pro-Gerät-Tokens veraltet
EmergencyRecoveryProcess Notfall-Wiederherstellung Abschnitt 16
AuditEvent + Unterarten siehe Abschnitt 16

Benutzer-Synchronisation: Nutzer und Gruppen stammen aus Keycloak. (1) HUB keycloak/KeycloakAuthorityPuller.sync läuft periodisch (hub.keycloak.syncer-period, dev 1 min), liest alle Keycloak-Nutzer/-Gruppen über einen Service-Account-Client (cryptomatorhub-system, Client Credentials) und gleicht Tabellen inkl. Gruppenmitgliedschaften ab (anlegen, aktualisieren, löschen). (2) Die Web-UI ruft nach dem Login PUT /api/users/me ohne Body auf (HUB-FE router/index.ts, Query sync_me), wodurch der Nutzer sofort angelegt wird (Name aus dem Token, picture, email). Ein Nutzer ohne DB-Zeile bekommt bei GET /users/me 404 – der Desktop scheitert dann mit „unerwarteter Antwort“. Empfehlung für den eigenen Server: Nutzer beim ersten gültigen Token automatisch anlegen.

Seat-Zählung (nur für Lizenz, HUB entities/EffectiveVaultAccess Named Queries): „sitzplatzbelegend“ = aktivierter Nutzer mit effektivem Zugriff auf mindestens einen nicht archivierten Vault. Ohne Lizenzlogik entfällt das vollständig.

8. Authentifizierung und Autorisierung auf dem Server

  • Quarkus OIDC als service (nur Bearer-Prüfung), quarkus.oidc.client-id=cryptomatorhub, Auth-Server = Keycloak-Realm (HUB application.properties). Signaturprüfung über JWKS des Realms, Issuer aus der Discovery (Quarkus-Standard, ungeprüft im Detail). Audience wird nicht geprüft (beobachtet: Token mit aud=account, azp=krywerk-e2e wird akzeptiert). Tokens des Clients cryptomator funktionieren daher.
  • Rollen = Keycloak-Realm-Rollen aus Claim realm_access.roles (Quarkus-Keycloak-Standard; Realm-Mapper realm_access.roles in cryptomator-realm.json) (Mapping-Pfad ungeprüft, Verhalten beobachtet). Rollen (HUB keycloak/RealmRole): user (alle normalen Endpunkte), create-vaults (neuen Vault anlegen), admin (Verwaltung). Fehlende Rolle → 403; fehlendes Token → 401. (beobachtet 2026-10-13, Black Box) Enthält das Access Token einen Claim groups, nimmt Hub 2.1.2 diesen statt realm_access.roles als Rollenliste: Nach Hinzufügen eines Keycloak-Mappers „Group Membership“ mit Claim groups bekamen Nutzer mit Gruppe 403 auf GET /users/me, Nutzer ohne Gruppe (Claim fehlt) weiter
  • Ein eigener Gruppen-Claim für Dritte (z. B. Krywerk Server) muss im selben Realm daher anders heißen. Keycloak lässt den Gruppen-Claim bei Nutzern ohne Gruppe ganz weg (beobachtet).
  • Identität: sub = User-ID; Anzeigename = Quarkus-Principal-Name (typisch preferred_username, ungeprüft).
  • Vault-Rollen-Filter (HUB filters/VaultRoleFilter.filter): Für Methoden mit verlangter Vault-Rolle: Vault existiert → (a) Bypass, wenn der Nutzer eine angegebene Realm-Rolle hat (z. B. admin bei GET /vaults/{id}), (b) Bypass für Notfall-Rat-Mitglieder, wo erlaubt, (c) sonst muss eine der effektiven Rollen des Nutzers in der verlangten Menge liegen, sonst 403. Vault existiert nicht → je nach Endpunkt 403 (Standard), 404, durchlassen oder „Realm-Rolle verlangt“ (create-vaults beim Anlegen).
  • Lizenz-Filter: (1) global (HUB filters/LicenseSetupFilter): solange keine Lizenz installiert ist, 402 für alle Endpunkte außer GET /config, GET/PUT /users/me, Lizenz-Trial/-Refresh, Billing. (2) @ActiveLicense (filters/ActiveLicenseFilter): 402 bei abgelaufener Lizenz (access-token, legacy keys, addUser, addGroup). Ein eigener Server ohne Lizenzmodell gibt nie 402 zurück.
  • Server-eigene Kryptographie: keine für Format 8. Er verifiziert nur (a) Lizenz-JWTs (ES512, fest eingebauter Public Key + x5c-Kette, HUB license/LicenseVerifierProducer, X5cCheckingJWTVerifier) und (b) Claim-Ownership-Beweise (ES384, Abschnitt 12). JWEs werden nur per Regex auf Form geprüft und unverändert gespeichert/ausgeliefert.

9. API-Level, Hub 1.x vs. 2.x, Legacy-Geräte

apiLevel-Bedeutung (Kommentar in HUB-dev ConfigResource): 0 = initial (Hub 1.0–1.2); 1 = neuer Unlock-Flow über /devices/{id} mit User Keys (Hub 1.3.0); 2 = fehlender Response-Header in 1.3.0 nachgereicht; 3 = HTTP 402 bei überschrittenen Sitzen (1.3.4); 4 = Hub 1.4.0 (ECDSA-Key/WoT, gilt bis 2.1.x); 5 = UVF (/vaults/{id}/uvf/*, Access Tokens können UVF-Member-Key enthalten; erst HUB-dev). Für einen Format-8-Server ist 4 die richtige Angabe.

9.1 Legacy-Unlock (apiLevel 0, Hub 1.0–1.2)

(DESK-HUB ReceiveKeyController.requestLegacyAccessToken, LegacyRegisterDeviceController; HUB-API VaultResource.legacyUnlock.) - GET vaults/{vaultId}/keys/{deviceId} (Bearer). 200 → Body = JWE (Variante A) mit Masterkey direkt an den Device Key verschlüsselt ({"key": …}); 402/403/410 wie oben; 404 → Legacy-Registrierung. - Legacy-Registrierung: PUT <devicesResourceUrl><deviceId>, Body {"id", "name", "publicKey"} mit publicKey = base64url ohne Padding des SPKI; erwartet 201 oder 409. Danach musste ein Vault-Owner im Browser dem Gerät Zugriff gewähren. - Hub 2.1.2 bedient keys/{deviceId} noch aus legacy_access_token: 404 bei unbekanntem Vault (beobachtet), 403 wenn für (Vault, Gerät, Nutzer) kein Legacy-Token existiert.

9.2 Migration von Legacy-Geräten (beim Registrieren unter Hub ≥ 1.3)

(DESK-HUB RegisterDeviceController.migrateLegacyDevices; HUB-API DeviceResource.getLegacyAccessTokens, UsersResource.updateMyAccessTokens.) 1. GET devices/{deviceId}/legacy-access-tokens → 200 mit JSON-Objekt { "<vaultId>": "<JWE an Device>", … } (beobachtet: {}). Nicht-200 oder leer ⇒ überspringen. 2. Jeden JWE mit Device-Privkey entschlüsseln, Masterkey neu für den User-ECDH-Public-Key verschlüsseln. 3. POST users/me/access-tokens mit { "<vaultId>": "<neuer JWE>", … } → 200 (unbekannte Vaults werden übersprungen). Fehler werden nur geloggt. 4. Beim PUT /devices/{id} löscht der Server ein gleichnamiges Legacy-Gerät.

10. REST-API-Referenz (Hub 2.1.2, Präfix /api)

Konventionen: JSON (UTF-8); Zeitstempel ISO-8601-Instant mit Millisekunden (…T…Z); null-Felder werden in Antworten meist weggelassen (JsonInclude NON_NULL, beobachtet bei users/me). Rolle „user“ bedeutet Realm-Rolle user. „Owner“ = effektive Vault-Rolle OWNER (über Gruppe genügt).

10.1 Config, Version, Settings

Methode + Pfad Auth Antwort
GET /config keine 200 ConfigDto (unten)
GET /version keine (beobachtet) {"hubVersion": "2.1.2", "keycloakVersion": "26.7.5"}
GET /settings user {"hubId", "wotMaxDepth" (0–9, Standard 3), "wotIdVerifyLen" (≥ 0, Standard 2), "enableEmergencyAccess", "defaultRequiredEmergencyKeyShares", "defaultMinMembers", "allowChoosingEmergencyCouncil", "emergencyCouncilMemberIds": [..]}
PUT /settings admin Body wie oben → 204; 400 ungültig

ConfigDto (HUB-API ConfigResource.ConfigDto): keycloakUrl, keycloakRealm, keycloakClientIdHub (cryptomatorhub, Web-UI), keycloakClientIdCryptomator (cryptomator, Apps), keycloakAuthEndpoint, keycloakTokenEndpoint (öffentliche URLs, interne Realm-URL-Präfixe werden ersetzt), serverTime, apiLevel (4), entitlements {seats, showTrialHint, auditLogRetentionDays, emergencyAccessEnabled, keycloakAccessEnabled, iosLicense?, androidLicense?, desktopLicense?}, billingUrl, licenseApiUrl, licenseSetupRequired. Von Clients tatsächlich genutzt: Desktop/iOS/Android nur apiLevel. Die Web-UI nutzt keycloakClientIdCryptomator, keycloakAuthEndpoint, keycloakTokenEndpoint zum Bauen des Vault-Headers sowie Keycloak-URL/Realm/Hub-Client-ID für ihren eigenen Login. Die Apps beziehen alle URLs aus dem Vault-Header, nie aus /config.

10.2 Nutzer

Methode + Pfad Auth Request Antwort / Status
GET /users/me?withDevices=<bool> user (auch im Setup-Modus) – 200 UserDto; devices nur bei withDevices=true, sonst []; 404 wenn keine DB-Zeile
PUT /users/me user (auch im Setup-Modus) optional UserDto 201. Legt Nutzer an falls nötig; setzt immer name/pictureUrl/email aus dem Token. Mit Body: übernimmt setupCode, ecdhPublicKey, ecdsaPublicKey, privateKeys, language; aktualisiert vorhandene Geräte aus devices (nur type, name, publicKey, userPrivateKey; legt keine an, löscht keine). Audit UserSetupCodeChanged / UserKeysChanged bei Änderung
POST /users/me/access-tokens user { "<vaultId>": "<JWE>" } 200; speichert eigene Access Tokens (unbekannte Vaults übersprungen) – keine Rollenprüfung!
POST /users/me/reset user – 204; löscht User Keys, Setup Code, alle Geräte, alle Access Tokens, Notfall-Anteile des Nutzers
GET /users/me-with-legacy-devices-and-access user – UserDto mit Legacy-Geräten (veraltet)
PUT /users/trusted/{userId} user Body = kompakte JWS (ES384) als text/plain 204; speichert/überschreibt die Signatur (aufrufender Nutzer → {userId}) ohne jede Prüfung; Audit SignedWotId. Details 16a.2
GET /users/trusted, GET /users/trusted/{userId} user – [{ "trustedUserId", "signatureChain": [JWS…] }] bzw. einzelnes Objekt; je Ziel nur die kürzeste Kette (≤ wotMaxDepth+1 Signaturen), Reihenfolge vom Aufrufer zum Ziel; 404 ohne ausreichend kurze Kette. Details 16a.2
GET /users, GET/PUT/DELETE /users/{id}, PUT /users/{id}/enabled, POST /users admin – Nutzerverwaltung (schreibt auch nach Keycloak; für Clients irrelevant)

UserDto (HUB-API UserDto) – Ausgabe: id, type = "USER", name, pictureUrl?, email, firstName?, lastName?, language?, enabled, devices [DeviceDto], ecdhPublicKey?, publicKey? (Alias von ecdhPublicKey), ecdsaPublicKey?, privateKeys?, privateKey? (Alias von privateKeys), setupCode?. Eingabe akzeptiert jeweils neuen oder alten Namen (neuer gewinnt). Die Aliase publicKey/privateKey sind Pflicht: Desktop, iOS und Android lesen nur diese (DESK-HUB RegisterDeviceController.UserDto, IOS UserDto, AND HubRepositoryImpl). Öffentliche Kurzform (justPublicInfo, z. B. in users-requiring-access-grant): ohne privateKeys, setupCode, Geräte.

10.3 Geräte

Methode + Pfad Auth Request Antwort / Status
PUT /devices/{deviceId} user DeviceDto (name nicht leer, ohne HTML; publicKey Base64; userPrivateKey JWE; type optional, Standard DESKTOP) 201 (auch bei Update eines eigenen Geräts: dann name, publicKey, userPrivateKey überschrieben; creationTime serverseitig). 409 bei DB-Konflikt (ID gehört anderem Nutzer). Löscht gleichnamiges Legacy-Gerät. Audit DeviceRegistered
GET /devices/{deviceId} user – 200 DeviceDto; 404 wenn nicht vorhanden oder fremd
DELETE /devices/{deviceId} user – 204; 404 wenn nicht vorhanden/fremd. Audit DeviceRemoved
GET /devices/{deviceId}/legacy-access-tokens user – 200 {vaultId: JWE}
DELETE /devices/{deviceId}/legacy-device user – 204/404
GET /devices?ids=…, GET /devices/legacy-devices?ids=…, GET /devices/has-legacy-devices admin – Listen / bool

DeviceDto: id, name, type (BROWSER|DESKTOP|MOBILE), publicKey, userPrivateKey (Singular, historisch), owner (User-ID), creationTime, lastIpAddress?, lastAccessTime?, legacyDevice (bool). Geräteliste des Nutzers: GET /users/me?withDevices=true.

10.4 Vaults

Methode + Pfad Auth / Vault-Rolle Request Antwort / Status
PUT /vaults/{vaultId} user; existiert → OWNER (oder Notfall-Rat); neu → Realm-Rolle create-vaults VaultDto (id, name (nicht leer), description?, archived (ignoriert), requiredEmergencyKeyShares (≥0), emergencyKeyShares (Map), creationTime ignoriert) 201 + VaultDto bei Neuanlage (Ersteller wird automatisch OWNER; Audit VaultCreated, VaultMemberAdded), 200 bei Update (nur Name, Beschreibung, Notfall-Anteile). 402 Sitze. 403 fehlende Rolle (beobachtet 201 mit create-vaults)
GET /vaults/{vaultId} MEMBER/OWNER oder Realm admin; fehlend → 404 – VaultDto
DELETE /vaults/{vaultId} OWNER – 204 (beobachtet); kaskadiert Zugriffe/Tokens (ungeprüft im Detail)
PUT /vaults/{vaultId}/archived OWNER oder admin; fehlend → 404 Body text/plain true/false 200 VaultDto; 402 beim Entarchivieren über Sitzlimit
GET /vaults/accessible?role=<OWNER\|MEMBER> user – [VaultDto] mit effektivem Zugriff (optional nach Rolle gefiltert)
GET /vaults/recoverable user – Vaults, in deren Notfall-Rat der Nutzer ist
GET /vaults/some?ids=…, GET /vaults/all admin – [VaultDto]
GET /vaults/{vaultId}/members OWNER (oder Rat) – [MemberDto] direkte Mitglieder; deaktivierte Nutzer ausgeblendet
PUT /vaults/{vaultId}/members OWNER (oder Rat) { "<authorityId>": "OWNER"\|"MEMBER", … } (nicht leer) 204; ersetzt die gesamte direkte Mitgliederliste (unbekannte IDs ignoriert; keine gültigen → 400); 402 Sitze; 404 Vault fehlt
PUT /vaults/{vaultId}/users/{userId}?role=MEMBER\|OWNER OWNER (oder Rat) – 201 neu, 200 Rolle geändert; 404 Nutzer unbekannt; 402
PUT /vaults/{vaultId}/groups/{groupId}?role=… OWNER – 201/200; 404 Gruppe; 402
DELETE /vaults/{vaultId}/authority/{authorityId} OWNER (oder Rat) – 204; 404 wenn keine direkte Mitgliedschaft. Access Token bleibt gespeichert, ist aber ohne Mitgliedschaft wirkungslos
GET /vaults/{vaultId}/users-requiring-access-grant OWNER (oder Rat) – [UserDto (öffentlich)]: aktivierte Nutzer mit effektivem Zugriff, eingerichtetem ecdhPublicKey und ohne Access Token
POST /vaults/{vaultId}/access-tokens OWNER (oder Rat) { "<userId>": "<JWE>", … } (nicht leer) 200; legt an/überschreibt; 404 unbekannter Nutzer; 402 Sitze. Audit VaultAccessGranted. Server prüft nicht, ob der Nutzer Mitglied ist
GET /vaults/{vaultId}/access-token?evenIfArchived=<bool> MEMBER/OWNER Header Hub-Device-ID optional siehe 6.3
GET /vaults/{vaultId}/keys/{deviceId} user – Legacy (9.1)
POST /vaults/{vaultId}/claim-ownership user form proof=<JWS> Legacy (12)

VaultDto: id, name, creationTime, description?, archived, requiredEmergencyKeyShares, emergencyKeyShares (Map UserID→JWE), Legacy masterkey?, iterations?, salt?, authPublicKey?, authPrivateKey?. MemberDto: id, type (USER|GROUP), name, pictureUrl?, ecdhPublicKey?, ecdsaPublicKey?, vaultRole (beobachtet: {"id","type":"USER","name","vaultRole":"OWNER"}), memberSize? (Gruppen).

10.5 Authorities und Gruppen

Methode + Pfad Auth Antwort
GET /authorities/search?query=<Text>&withMemberSize=<bool> user [AuthorityDto] (Nutzer und Gruppen nach Namen; deaktivierte Nutzer ausgeblendet). Nutzer erscheinen in UserDto-Form, Gruppen {id, type:"GROUP", name, memberSize?} (beobachtet)
GET /authorities?ids=… user [AuthorityDto]
GET /groups, GET /groups/{id}, GET /groups/{id}/effective-members, POST /groups, PUT/DELETE /groups/{id}, POST/DELETE /groups/{id}/members/{userId} admin Gruppenverwaltung (Hub 2.x verwaltet Keycloak-Gruppen)

10.6 Lizenz, Billing, Audit, Notfall

GET /license/user-info (user) → {licensedSeats, usedSeats, expiresAt, gracePeriodEndsAt}; PUT /license/trial, POST /license/refresh, GET /billing, PUT /billing/token (admin) – nur für das Hub-Lizenzmodell. GET /auditlog (admin, Parameter Start-/Enddatum, Reihenfolge, Seitengröße 1–100, Typ). /emergency-access/* siehe Abschnitt 16.

11. Tresor-Erstellung über die Web-UI (Format 8)

(HUB-FE components/CreateVault.vue initialize, createVault, downloadVaultTemplate; common/crypto.ts.) 1. Browser erzeugt 64 Byte Masterkey (VaultKeys.create) und zeigt sofort den Recovery Key an. 2. Optional: Notfall-Anteile erzeugen (Secret Sharing des Masterkeys, JWE pro Ratsmitglied) (Details 16). 3. vaultId = zufällige UUID (crypto.randomUUID). 4. vault.cryptomator bauen und signieren (Abschnitt 2), Stammordner-Hash berechnen. 5. Owner-Access-Token = JWE A mit {"key": Base64(Masterkey)} an den eigenen ecdhPublicKey. 6. PUT /api/vaults/{vaultId} mit {id, name, creationTime, description, archived:false, requiredEmergencyKeyShares, emergencyKeyShares} → Server macht Ersteller zum OWNER. 7. POST /api/vaults/{vaultId}/access-tokens mit { "<eigene User-ID>": <JWE> }. 8. Nutzer lädt das ZIP herunter. Das vault.cryptomator wird nicht hochgeladen.

Mitglieder hinzufügen und Zugriff gewähren (HUB-FE components/VaultDetails.vue, GrantPermissionDialog.giveUsersAccess): Owner fügt Nutzer/Gruppen hinzu (PUT …/users/{id} bzw. …/groups/{id} bzw. PUT …/members). Danach liefert users-requiring-access-grant die Nutzer, die einen Token brauchen. Owner holt seinen eigenen Token (GET …/access-token?evenIfArchived=true mit Hub-Device-ID des Browsers), entschlüsselt den Masterkey, verschlüsselt ihn je Nutzer an dessen ecdhPublicKey und schickt alle per POST …/access-tokens. Nutzer ohne eingerichteten Account (ecdhPublicKey fehlt) werden übersprungen und später nachgeholt. Optional prüft die Web-UI vorher die Web-of-Trust-Kette (ES384-JWS mit Payload aus ecdhPublicKey/ecdsaPublicKey; iss = Signierer, sub = signierter Nutzer, iat stehen im JWS-Header; HUB-FE common/wot.ts) und zeigt ein Vertrauens-Schild; die Prüfung ist rein informativ und blockiert die Vergabe nicht (Abschnitt 16a).

Recovery Key (HUB-FE VaultKeys.createRecoveryKey/recover, common/util.ts WordEncoder, Wortliste common/4096words_en.ts): crc = CRC32(masterkey) (Standard-CRC-32); Daten = 64 Byte Masterkey ‖ crc & 0xFF ‖ (crc >> 8) & 0xFF = 66 Byte. Je 3 Byte → zwei 12-Bit-Indizes (i1 = b1·16 + (b2 >> 4), i2 = (b2 & 0x0F)·256 + b3) in die 4096-Wörter-Liste → 44 Wörter, durch Leerzeichen getrennt (Trennzeichen ungeprüft). Wiederherstellung: dekodieren, Länge 66 und Prüfsumme prüfen. Verwendung: (a) „Tresor wiederherstellen“ beim Erstellen – erzeugt mit demselben Masterkey einen neuen Hub-Vault mit neuer ID (neues vault.cryptomator nötig); (b) Owner-Dialog „Recover“ – Masterkey aus Recovery Key, Access Token für sich selbst neu hochladen (RecoverVaultDialog).

Kann ein Drittclient (Krywerk) Tresore über die API anlegen? Ja. Der Server sieht nie Klartextschlüssel und kein vault.cryptomator. Krywerk braucht: Realm-Rolle create-vaults, eingerichteten Account (eigener ecdhPublicKey), die Werte keycloakClientIdCryptomator/keycloakAuthEndpoint/ keycloakTokenEndpoint aus /api/config und die öffentliche Basis-URL für den Header. Dann Schritte 1–8 lokal nachbilden. Das Format der Erfolgs-/Fehler-URLs (/app/unlock-success?vault=…) muss zum Server passen.

12. Legacy: Admin-Passwort und Claim Ownership

Hub 1.0–1.2 speicherte pro Vault: salt (Base64), iterations, masterkey = Base64(Nonce 12 Byte ‖ AES-GCM-Wrap des Masterkeys), authPublicKey (SPKI ECDSA P-384), authPrivateKey (Base64, Nonce ‖ GCM-Wrap PKCS#8). KEK = PBKDF2-HMAC-SHA256(Admin-Passwort, salt, iterations, 256 Bit) (HUB-FE VaultKeys.decryptWithAdminPassword). Übernahme: Browser entpackt mit dem Passwort, signiert ein ES384-JWT mit Claims sub = eigene User-ID, vaultId (klein geschrieben), nbf, exp, sendet es als Formfeld proof an POST /vaults/{id}/claim-ownership. Server prüft mit authPublicKey (30 s Toleranz), macht den Nutzer zum OWNER und löscht alle Legacy-Felder (HUB-API VaultResource.claimOwnership). 400 falscher Beweis, 404 Vault fehlt, 409 bereits übernommen. Für Krywerk und den eigenen Server optional.

13. Universal Vault Format (UVF) – nur Erkennung

UVF gibt es erst in HUB-dev (apiLevel 5). Erkennung: Im Tresor liegt vault.uvf statt (bzw. ohne) vault.cryptomator. vault.uvf ist ein JWE in JSON-Serialisierung mit crit: ["uvf.spec.version"], uvf.spec.version: 1 und org.cryptomator.hub.canonical = <apiBase>/vaults/<id>/uvf/vault.uvf; Empfänger org.cryptomator.hub.memberkey (A256KW) und org.cryptomator.hub.recoverykey.<JWK-Thumbprint> (ECDH-ES) (HUB-dev common/universalVaultFormat.ts). Server-Endpunkte /vaults/{id}/uvf/vault.uvf und /uvf/jwks.json. Krywerk unterstützt UVF vorerst nicht: bei vault.uvf mit klarer Meldung abbrechen; der eigene Server meldet apiLevel 4.

14. Unterschiede, die ein Client kennen muss (Kurzfassung)

  • apiLevel 0: pro-Gerät-Tokens (keys/{deviceId}), keine User Keys. ≥1: User Keys + /devices/{id} + access-token. ≥3: 402 bei Sitzüberschreitung. 4: ECDSA-User-Key, WoT. 5: UVF.
  • Feldnamen: neue (ecdhPublicKey, privateKeys) und alte (publicKey, privateKey) parallel.
  • userPrivateKey-Payload hat je nach Erzeuger 1 oder 3 Felder; immer key lesen.
  • Hub 2.x verwaltet Nutzer/Gruppen über Keycloak-Admin-API; für den Unlock irrelevant.

15. Lizenzdurchsetzung (neutral beschrieben)

  • Server-seitig: Lizenz = ES512-JWT von api.cryptomator.org, geprüft gegen eingebauten Public Key plus Zertifikatskette (CN „Hub License Issuer CA (Prod)“), gebunden an die Hub ID; Entitlements bestimmen Sitze, Audit-Aufbewahrung, Notfallzugriff. Durchsetzung nur über HTTP 402 (Abschnitt 8).
  • Desktop: prüft keine Hub-Lizenz; reagiert nur auf 402 mit einem Hinweisdialog. (Die Desktop-Supporter-Zertifikate (common/LicenseChecker) sind davon unabhängig.)
  • iOS: wertet Header Hub-iOS-License aus – ein ES512-JWT (aud „Cryptomator for iOS“, beobachtet im Test-Stack), das die App selbst gegen einen eingebauten Schlüssel prüft; fehlt er, Fallback auf Hub-Subscription-State (ACTIVE/sonst inaktiv). Inaktiv bedeutet vermutlich eingeschränkte Funktionen (z. B. nur Lesen) (ungeprüft). Android analog mit Hub-Android-License (AND HubRepositoryImpl.resolveSubscriptionState). desktopLicense (aud „Cryptomator“) wird in /config geliefert, aber im Desktop-Code nicht ausgewertet (beobachtet: nirgends referenziert).
  • Für den eigenen Server: keine 402, keine Lizenz-Header. Mobile Apps behandeln Hub-Tresore dann als „ohne aktives Abo“ – es werden keine Lizenzen nachgebildet.

16. Audit-Log und Notfallzugriff (Kurzüberblick)

Audit-Ereignisse (HUB entities/events/*): DeviceRegistered, DeviceRemoved, VaultCreated, VaultUpdated, VaultMemberAdded/Removed/Updated, VaultAccessGranted, VaultKeyRetrieved (mit Ergebnis, IP, Device-ID), VaultOwnershipClaimed, UserKeysChange, UserSetupCodeChange, UserAccountReset, SignedWotId, SettingWotUpdate, EmergencyAccessSetup, EmergencyAccessSettingsUpdated, EmergencyAccessRecovery Started/Approved/Completed/Aborted. Abruf GET /auditlog (admin, lizenzabhängig). Für Kompatibilität nicht nötig; Device-lastAccessTime/lastIpAddress aber schon (UI-Anzeige).

Notfallzugriff: Masterkey wird bei der Erstellung in n Anteile für Ratsmitglieder geteilt (emergencyKeyShares = UserID→JWE, Schwelle requiredEmergencyKeyShares). Wiederherstellung über Prozesse PUT /emergency-access/{processId} (Felder id, vaultId, type, details, requiredKeyShares ≥2, processPublicKey, recoveredKeyShares {UserID: {processPrivateKey JWE, unrecoveredKeyShare JWE, …}}), POST …/recovered-key-shares, DELETE …/complete, DELETE …/abort, GET /emergency-access, GET /emergency-access/{vaultId}. Ratsmitglieder dürfen dann Owner-Endpunkte nutzen (Bypass in 8). Algorithmus der Anteilsbildung nicht untersucht (ungeprüft) – für einen minimalen Server weglassen.

16a. Web of Trust (Hub ≥ 1.4)

Zweck: Ein Owner soll vor dem Verteilen eines Vault Keys erkennen können, ob der vom Server gelieferte ecdhPublicKey eines Mitglieds wirklich diesem Mitglied gehört (Schutz gegen einen Server, der fremde Schlüssel unterschiebt). Dazu signieren Nutzer gegenseitig die öffentlichen Schlüssel anderer Nutzer, nachdem sie deren Fingerprint außerhalb von Hub abgeglichen haben. Der Server speichert die Signaturen opak und berechnet nur Ketten; jede kryptographische Prüfung geschieht im Client. Der Cryptomator-Desktop nutzt WoT nicht – weder die Endpunkte noch den ECDSA-Schlüssel (DESK develop: keine Treffer für die Endpunkte oder ecdsaPublicKey). iOS/Android wurden dafür nicht geprüft (ungeprüft, vermutlich ebenso). WoT ist reine Web-UI-Funktion.

16a.1 Signaturformat („A vertraut B“)

(HUB-FE common/wot.ts sign, common/jwt.ts)

  • Kompakte JWS-Serialisierung B64URL(Header) "." B64URL(Payload) "." B64URL(Signatur), alle drei Teile Base64url ohne Padding. Header und Payload sind UTF-8-JSON (so, wie JSON.stringify sie erzeugt: ohne Leerzeichen).
  • Algorithmus ES384: ECDSA über P-384 mit SHA-384 über die ASCII-Bytes von B64URL(Header) "." B64URL(Payload); Signatur im JOSE-Format r ‖ s (je 48 Byte, zusammen 96 Byte, kein DER).
  • Signierschlüssel: der ECDSA-Privatschlüssel von A (aus A's privateKeys-JWE, Feld ecdsaPrivateKey, im Browser über den Geräteschlüssel des Browsers entschlüsselt).
  • Header – enthält die Metadaten (nicht der Payload!), Feldreihenfolge beim Erzeugen: alg = "ES384", typ = "JWT", b64 = true (JSON-Boolean), iss = Hub-User-ID von A (Signierer), sub = Hub-User-ID von B (signierter Nutzer), iat = Unix-Zeit in Sekunden (Ganzzahl). Kein kid, kein jwk, kein crit. b64: true ist der RFC-7797-Standardfall und ändert nichts an der Kodierung.
  • Payload – genau zwei Felder in dieser Reihenfolge: ecdhPublicKey, ecdsaPublicKey. Werte sind die unveränderten Strings aus B's UserDto (Base64 Standard mit Padding über SPKI-DER, siehe 3.1). Es werden keine Claims in den Payload geschrieben.
  • Der Client signiert nur, wenn B beide Schlüssel besitzt; sonst Fehler.

Beispiel (erfundene, gekürzte Werte):

Header:  {"alg":"ES384","typ":"JWT","b64":true,"iss":"3f2a…-alice","sub":"9c41…-bob","iat":1760000000}
Payload: {"ecdhPublicKey":"MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE…","ecdsaPublicKey":"MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE…"}
JWS:     eyJhbGciOiJFUzM4NCIsInR5cCI6IkpXVCIsImI2NCI6dHJ1ZSwiaXNzIjoi….eyJlY2RoUHVibGljS2V5Ijoi….<128 Zeichen Signatur>

Hinweise für eine eigene Implementierung: Beim Prüfen immer über die empfangenen kodierten Teile rechnen, nie neu serialisieren. Beim Erzeugen die obige Feldreihenfolge einhalten, damit die Web-UI und Audit-Ansicht identische Ergebnisse sehen (Pflicht ist sie nicht, da die Web-UI nur die Signatur und den Payload prüft).

16a.2 Serverseite

Tabelle wot (HUB entities/WotEntry, Migration V16__WoT.sql): Primärschlüssel (user_id = signierter Nutzer, signer_id = Signierer), Spalte signature (Text, die JWS unverändert). Beide IDs sind Fremdschlüssel auf die Nutzertabelle mit Löschkaskade (Nutzer gelöscht → seine Einträge als Signierer und als Signierter verschwinden). Pro Paar (Signierer, Signierter) gibt es höchstens eine Signatur; ein erneutes Signieren überschreibt die alte.

PUT /api/users/trusted/{userId} (HUB-API UsersResource, Rolle user): - Request-Body: die kompakte JWS als Klartext, Content-Type: text/plain (kein JSON, keine Anführungszeichen). Body darf nicht fehlen. - Signierer = sub des Bearer Tokens (aufrufender Nutzer), Signierter = {userId} aus dem Pfad. - Keine Validierung: Der Server parst die JWS nicht, prüft weder Signatur noch iss/sub/Payload und auch nicht, ob {userId} Schlüssel hat. Beliebiger Text wird gespeichert (die Server-Tests speichern z. B. Freitext). Selbstsignatur ({userId} = eigener Nutzer) wird nicht abgewiesen. - Antwort 204 ohne Body. Fehlerfälle nicht ausdrücklich behandelt: aufrufender Nutzer ohne DB-Zeile bzw. unbekannte {userId} (Fremdschlüssel) führen zu einem Serverfehler (vermutlich 500, ungeprüft). - Audit-Ereignis SignedWotId mit userId (signiert), signerId, signerKey = der zum Zeitpunkt des Aufrufs gespeicherte ecdsaPublicKey des Signierers, signature = die JWS. - Die Web-UI ruft danach sofort GET /users/trusted/{userId} auf, um die neue Kette zu erhalten.

Ketten-Berechnung (View effective_wot, HUB entities/EffectiveWot): rekursive Suche über wot: - Start: jeder Eintrag (Signierer S → Signierter U) ist eine Kette der Tiefe 0 mit der Signaturliste [sig(S→U)] und der Signiererliste [S]. - Erweiterung: Kette (T … → U, Tiefe d) wird um einen Eintrag (U → V) verlängert, falls d < wotMaxDepth und V nicht in der bisherigen Signiererliste vorkommt (Schleifenschutz). Neue Signaturliste = alte Liste + sig(U→V), Signiererliste + U, Tiefe d+1. - Ergebnis je Paar (vertrauender Nutzer T = erster Signierer, vertrauter Nutzer V): nur die kürzeste Kette (geringste Tiefe). Bei mehreren gleich kurzen Ketten ist die Auswahl nicht festgelegt. - Kettenlänge = Tiefe + 1, also höchstens wotMaxDepth + 1 Signaturen. wotMaxDepth = 0 heißt: nur direkte Signaturen. - Reihenfolge der signatureChain: vom anfragenden Nutzer zum Ziel. Element 0 ist von T signiert (über den ersten Zwischennutzer bzw. direkt über das Ziel), das letzte Element signiert die Schlüssel des Ziels. - Eine Selbstsignatur (T → T) erscheint als Paar (T, T) mit Kette der Länge 1; ansonsten taucht T nie als eigenes Ziel auf.

GET /api/users/trusted (Rolle user, kein Caching): 200, JSON-Array aller Paare mit T = aufrufender Nutzer: [{"trustedUserId": "<id>", "signatureChain": ["<JWS>", …]}, …]. Leeres Array, wenn keine.

GET /api/users/trusted/{userId} (Rolle user, kein Caching): 200 mit einem Objekt {"trustedUserId", "signatureChain"} für T = aufrufender Nutzer, V = {userId}; 404, wenn keine Kette mit höchstens wotMaxDepth + 1 Signaturen existiert (auch für unbekannte IDs).

Beispiel: A signiert B, B signiert C, wotMaxDepth = 3. A erhält [{"trustedUserId":"B","signatureChain":["<A→B>"]},{"trustedUserId":"C","signatureChain":["<A→B>","<B→C>"]}].

16a.3 Prüfung im Client (Web-UI)

(HUB-FE common/wot.ts verify, components/TrustDetails.vue)

Eingaben: signatureChain (vom Server), der Ziel-Nutzer mit seinen aktuellen ecdhPublicKey und ecdsaPublicKey (aus der jeweiligen Nutzer-/Mitgliederliste) und der eigene ECDSA-Public-Key. Den eigenen Schlüssel nimmt die Web-UI aus dem lokal entschlüsselten Schlüsselpaar (nicht aus dem vom Server gelieferten ecdsaPublicKey – sonst könnte der Server die Prüfung aushebeln).

Algorithmus: 1. vertrauter_schlüssel := eigener ECDSA-Public-Key. 2. Für jedes Element jws_i der Kette in Reihenfolge: a. Header dekodieren; alg muss ES384 sein, sonst Fehler. b. ES384-Signatur von jws_i mit vertrauter_schlüssel prüfen; ungültig → Fehler. c. Payload dekodieren (JSON mit ecdhPublicKey, ecdsaPublicKey). d. Ist jws_i nicht das letzte Element: vertrauter_schlüssel := der im Payload stehende ecdsaPublicKey (Base64 → SPKI-DER → P-384-ECDSA-Schlüssel) und weiter mit dem nächsten Element. (Der Schlüssel des Zwischennutzers kommt also aus der signierten Nutzlast, nicht vom Server.) e. Ist jws_i das letzte Element: Beide Payload-Felder müssen als Strings exakt gleich den aktuellen Schlüsseln des Ziel-Nutzers sein; sonst Fehler. 3. Kein Fehler → Kette gültig.

Nicht geprüft werden: iss, sub, typ, b64, iat (weder Alter noch Zukunft), Kettenlänge gegen wotMaxDepth (das begrenzt nur der Server) und ob die Signierer der Zwischenglieder zu den Nutzer-IDs passen. Eine leere Kette wird nicht gesondert behandelt (der Server liefert keine). Empfehlung für Krywerk: zusätzlich sub des letzten Glieds = Ziel-ID und iss von Glied 0 = eigene ID prüfen – das ist strenger als Hub, aber mit Hub-erzeugten Signaturen verträglich.

Vertrauensstufe (nur Anzeige, TrustDetails): - Ziel = man selbst → Stufe 0, angezeigt als „Trust Level 100 %“. - Gültige Kette der Länge n → Stufe n, angezeigt als Prozentwert 1/n (100 %, 50 %, 33 %, …), grünes Schild („Verified“). - Keine Kette (404 bzw. nicht in der Liste), Ziel ohne beide Schlüssel, oder Prüfung fehlgeschlagen → Stufe −1 „Unverified“, rotes Schild. - Hat das Ziel noch keinen Account eingerichtet, erscheint der Hinweis, dass der Nutzer nicht eingerichtet ist; Signieren ist dann nicht möglich. Für alle anderen (außer sich selbst) gibt es die Schaltfläche „Check Identity…“, auch wenn bereits eine Kette besteht (direktes Signieren verkürzt die Kette auf 1).

16a.4 Fingerprint und Identitätsprüfung

(HUB-FE common/wot.ts computeFingerprint, common/crypto.ts JWK-Thumbprint, components/UserkeyFingerprint.vue, components/SignUserKeysDialog.vue)

Fingerprint eines Nutzers: 1. Für ecdhPublicKey und ecdsaPublicKey je den JWK-Thumbprint nach RFC 7638 bilden: SHA-256 über die UTF-8-Bytes von {"crv":"P-384","kty":"EC","x":"<x>","y":"<y>"} (genau diese Reihenfolge, keine Leerzeichen), x/y = 48-Byte-Big-Endian-Koordinaten in Base64url ohne Padding. Beide Schlüssel ergeben trotz verschiedener Verwendung denselben Thumbprint-Aufbau (kein alg/use enthalten). 2. Thumbprint(ECDH) ‖ Thumbprint(ECDSA) = 64 Byte. 3. SHA-256 darüber → 32 Byte → 64 Hex-Zeichen in Großbuchstaben. 4. Anzeige: Blöcke zu 8 Zeichen, durch je ein Leerzeichen getrennt (8 Blöcke).

Der eigene Fingerprint steht im Nutzerprofil („User Key Fingerprint“); er wird über einen anderen Kanal an den Prüfenden weitergegeben.

Dialog „Verify {Name}'s Identity“ (A prüft B): - A tippt die ersten wotIdVerifyLen Zeichen von B's Fingerprint ein. Sobald die Eingabe mindestens wotIdVerifyLen Zeichen lang ist und der erwartete Fingerprint (ohne Leerzeichen, Großbuchstaben) mit der Eingabe beginnt (Vergleich exakt, d. h. groß geschrieben und ohne Leerzeichen), füllt die UI den vollständigen formatierten Fingerprint ein. Bestätigen ist erst möglich, wenn die Eingabe ohne Leerzeichen dem vollständigen Fingerprint entspricht (also nach dem Autovervollständigen oder bei vollständiger manueller Eingabe). - wotIdVerifyLen = 0: Feld ist vorbefüllt und schreibgeschützt; A bestätigt nur noch. - Ohne geladene Einstellung gilt im Dialog vorübergehend 64 (alle Zeichen). - Bestätigen → Signatur nach 16a.1 erzeugen → PUT /users/trusted/{B} → GET /users/trusted/{B} → Anzeige aktualisieren.

16a.5 Einstellungen

(Settings, HUB-API SettingsResource, HUB-FE components/AdminSettings.vue)

Feld Standard Bereich Server Bereich Admin-UI Bedeutung
wotMaxDepth 3 0–9 (Validierung + DB-Constraint) 0–9 Maximale Zahl zusätzlicher Glieder; Ketten haben höchstens wotMaxDepth + 1 Signaturen
wotIdVerifyLen 2 ≥ 0 (keine Obergrenze) 0–64 Zahl der Fingerprint-Zeichen, die beim Prüfen eingetippt werden müssen

Lesen über GET /settings (jeder Nutzer), Ändern über PUT /settings (admin, kompletter Body). Ändert sich einer der beiden Werte, entsteht das Audit-Ereignis SettingWotUpdate (updatedBy, wotMaxDepth, wotIdVerifyLen). Eine Änderung von wotMaxDepth wirkt sofort auf alle Abfragen, da die Ketten bei jeder Abfrage neu berechnet werden.

16a.6 Verwendung in der Web-UI

  • Vault-Details (Mitgliederliste), Zugriff gewähren (GrantPermissionDialog, Liste der Nutzer, die einen Token bekommen) und Auswahl des Notfall-Rats (Admin-Einstellungen Notfallzugriff, Einrichtung/Dialog Notfallzugriff über MultiUserSelectInputGroup): neben jedem Nutzer (nicht bei Gruppen) das Schild aus 16a.3 mit Popover und „Check Identity…“. Die Liste der Ketten kommt jeweils aus GET /users/trusted.
  • Rein informativ: Hub blockiert nichts. Der Owner kann Zugriff auch an „Unverified“-Nutzer gewähren, Notfall-Rat-Mitglieder können ungeprüft sein. Es gibt keine zusätzliche Warnung außer dem roten Schild.
  • Audit-Log (AuditLogDetailsSignedWotId): prüft die gespeicherte JWS mit dem im Ereignis gespeicherten signerKey und klassifiziert: „noch gültig“ (Signierer-Schlüssel unverändert und Fingerprint der signierten Schlüssel = aktueller Fingerprint des Ziels), „Schlüssel des Signierers geändert“, „Schlüssel des Ziels geändert“ oder „ungültig“ (Signatur fehlerhaft).

16a.7 Schlüsselwechsel und Konto-Reset

  • POST /users/me/reset und neue Schlüssel über PUT /users/me löschen keine wot-Einträge – weder die vom Nutzer erstellten noch die über ihn. Es gibt keine serverseitige Invalidierung.
  • Faktische Wirkung durch die Client-Prüfung:
  • Signaturen über den Nutzer passen nicht mehr zu seinen neuen Schlüsseln (Schritt 2e) → er wird für alle anderen „Unverified“, bis ihn jemand neu signiert (überschreibt die alte Zeile).
  • Ketten, die der Nutzer selbst als Erster signiert hat, scheitern für ihn in Schritt 2b, weil sein neuer ECDSA-Schlüssel die alten Signaturen nicht bestätigt → er muss andere neu signieren.
  • Glieder, die der Nutzer als Zwischenglied mit seinem alten Schlüssel signiert hat, bleiben für Dritte gültig, solange das vorherige Glied seinen alten ecdsaPublicKey enthält (der Prüfschlüssel kommt aus der Nutzlast). Signiert er danach neu, ersetzt die neue Signatur (neuer Schlüssel) die alte Zeile, und Ketten über ihn scheitern bis zur Neusignierung seiner Schlüssel durch den Vorgänger.
  • Da der Server nur die kürzeste Kette liefert, kann eine ungültige kurze Kette eine gültige längere verdecken; die Web-UI fragt keine Alternativen ab.

16a.8 Umsetzung in Krywerk (Empfehlung)

  • Server: Tabelle wie oben, PUT speichert opak (optional zusätzlich Signatur gegen den aktuellen ecdsaPublicKey des Aufrufers prüfen und 400 liefern – verträglich mit der Web-UI, aber strenger als Hub); Ketten per Breitensuche mit Schleifenschutz und Tiefenlimit, kürzeste Kette je Ziel, Reihenfolge vom Anfragenden zum Ziel; 404 bei fehlender Kette; Audit optional.
  • Client: Signieren nach 16a.1 (ES384, Header mit iss/sub/iat, Payload = exakte Schlüsselstrings), Prüfen nach 16a.3 mit dem lokal gehaltenen eigenen ECDSA-Public-Key, Fingerprint nach 16a.4 (Testvektor: Fingerprint aus der Profilseite der Web-UI mit eigener Berechnung vergleichen).

17. Minimaler kompatibler Server

17a. Offizieller Desktop entsperrt und registriert Geräte

Neben einem OIDC-Provider (öffentlicher Client mit PKCE S256, Redirect http://127.0.0.1:*/*, Realm-Rollen im Token oder eigenes Rollenmodell) braucht es:

Endpunkt Pflichtverhalten
GET /api/config (ohne Auth) 200, JSON mit mindestens apiLevel: 4 (Desktop braucht ≥1, iOS ≥2)
GET /api/users/me (+ Query withDevices tolerieren) 200 mit name, publicKey, privateKey (Aliase!), setupCode, id; Nutzer bei Bedarf automatisch anlegen
GET /api/devices/{id} 200 {name, userPrivateKey, …} für eigenes Gerät, sonst 404
PUT /api/devices/{id} Body speichern, 201; 409 wenn die ID einem anderen Nutzer gehört
GET /api/vaults/{id}/access-token 403/410/449/200 gemäß 6.3; Hub-Device-ID für lastAccess auswerten
GET /api/devices/{id}/legacy-access-tokens optional; 200 {} oder 404 (Desktop überspringt)
<origin>/app/unlock-success, <origin>/app/unlock-error, <origin>/app/profile beliebige HTML-Seiten auf demselben Origin wie die API

17b. Krywerk statt Web-UI (Account, Tresore, Mitglieder)

Zweck Endpunkte
Account einrichten / Setup Code ändern PUT /api/users/me mit ecdhPublicKey, ecdsaPublicKey, privateKeys (B, Felder key+ecdhPrivateKey+ecdsaPrivateKey), setupCode (A an sich selbst)
Account zurücksetzen POST /api/users/me/reset
Geräte auflisten/entfernen GET /api/users/me?withDevices=true, DELETE /api/devices/{id}
Tresor anlegen PUT /api/vaults/{id} (Rolle create-vaults), POST /api/vaults/{id}/access-tokens
Tresore auflisten/ansehen GET /api/vaults/accessible, GET /api/vaults/{id}
Mitglieder GET /api/authorities/search, GET /api/vaults/{id}/members, PUT /api/vaults/{id}/users/{uid}?role=, PUT /api/vaults/{id}/groups/{gid}?role=, DELETE /api/vaults/{id}/authority/{aid} (alternativ PUT /api/vaults/{id}/members)
Zugriff gewähren GET /api/vaults/{id}/users-requiring-access-grant, GET /api/vaults/{id}/access-token?evenIfArchived=true, POST /api/vaults/{id}/access-tokens
Archivieren / Löschen PUT /api/vaults/{id}/archived, DELETE /api/vaults/{id}

Nicht nötig: Lizenz, Billing, Audit-Abruf, WoT (optional, Abschnitt 16a), Notfallzugriff, Legacy-Endpunkte, Admin-Verwaltung (Nutzer/Gruppen können direkt aus dem IdP synchronisiert werden).

Serverseitige Invarianten: (1) Ersteller wird OWNER; (2) effektiver Zugriff = direkt ∪ über (verschachtelte) Gruppen; (3) Access Token zählt nur bei effektivem Zugriff; (4) 449 (nur bei effektiver Mitgliedschaft, sonst 403) vor Token-Prüfung, wenn ecdhPublicKey fehlt; (5) archivierte Vaults → 410 (außer evenIfArchived=true), bleiben aber für Owner verwaltbar; (6) Geräte gehören genau einem Nutzer, fremde Geräte sind „404“; (7) alle Schlüsselfelder opak speichern, nur Form validieren; (8) Owner-only-Endpunkte prüfen die effektive Rolle.

18. Hinweise für die Rust-Implementierung

Primitive (alle reine Rust-Crates, kein OpenSSL nötig):

Zweck Vorschlag
P-384 ECDH, Schlüsselgenerierung, SPKI/PKCS#8-DER p384 (Features ecdh, ecdsa, pkcs8, pem), elliptic-curve (jwk für epk)
ES384 (WoT, Claim-Proof) p384::ecdsa (JWS-Signatur im Format r‖s, 96 Byte)
Concat KDF selbst implementieren (eine SHA-256-Runde, sha2) – trivial und gut testbar
AES-256-GCM aes-gcm
PBES2 pbkdf2 + hmac + sha2 (SHA-512), aes-kw (RFC 3394)
Base64 base64 mit STANDARD (Schlüssel/Payloads) und URL_SAFE_NO_PAD (JOSE)
HS256 vault.cryptomator bereits in Krywerk vorhanden (hmac/sha2)
OIDC-Client (Desktop-Flow) oauth2 (PKCE, Code-Austausch) + kleiner Loopback-Server auf 127.0.0.1:0 (tokio/hyper oder blockierend tiny_http); Browser öffnen mit open/webbrowser
Token-Prüfung (Server) jsonwebtoken mit JWKS-Cache (RS256/ES256 je nach IdP) oder openidconnect
Recovery Key crc32fast; Wortliste nicht aus dem Hub-Repo kopieren, sondern als Daten mit Lizenzklärung beziehen (offene Frage)
Device-Key-Speicher keyring (System-Schlüsselbund) für Passwort/Schlüssel, Datei mit eigenem Format (kein PKCS#12 nötig)
Geheimnisse zeroize/secrecy für Masterkey, Privkeys, Setup Code, Tokens

Fertige JOSE-Crates (josekit braucht OpenSSL; jose-jwe-Unterstützung für ECDH-ES/PBES2 lückenhaft) – eigene schmale Implementierung der zwei Varianten ist vermutlich robuster. Testvektoren: Hub-JWEs aus dem Test-Stack erzeugen und gegen Krywerk entschlüsseln (und umgekehrt mit dem offiziellen Desktop).

Sicherheit: Bearer Tokens, Setup Code, Masterkey, private Schlüssel und entschlüsselte Payloads niemals loggen (auch nicht in Debug-Ausgaben oder Fehlermeldungen; HTTP-Clients ohne Header-Logging). Host- Vertrauensprüfung wie 6.1 übernehmen (Header vor Masterkey-Erhalt nicht authentisch). Nach Erhalt des Masterkeys die vault.cryptomator-Signatur zwingend prüfen und jti mit der Vault-ID aus der kid vergleichen (der Desktop tut Letzteres nicht explizit, ungeprüft). p2c beim Entschlüsseln nach oben begrenzen (DoS), aber ≥ 1 000 000 zulassen. Nur HTTPS außer für localhost.

19. Offene Fragen / Unsicherheiten

  1. Genaues Quarkus-Mapping von Rollen (realm_access.roles vs. groups) und Issuer-Prüfung – Verhalten beobachtet, Konfiguration nicht vollständig nachvollzogen. Audience-Prüfung findet nicht statt. Ein vorhandener Claim groups ersetzt die Rollen (beobachtet, siehe §8).
  2. Filter-Reihenfolge 402 vs. 403 bei access-token; im Test-Stack (mit Lizenz) kam für fremde Vault-ID 403.
  3. Ob Nimbus (Desktop) eine Obergrenze für p2c hat (ungeprüft); Hub nutzt 1 000 000.
  4. iOS/Android nur überflogen: genaue Bedeutung von „inaktivem Abo“, vollständige Statuscode-Behandlung.
  5. Lizenz/Bezugsquelle der 4096-Wörter-Liste für Recovery Keys (Datei liegt im AGPL-Repo) – vor Übernahme klären.
  6. Exakte Löschkaskaden beim DELETE /vaults/{id} (Access Tokens, Notfall-Anteile).
  7. Ob Mitglied-Entfernung den Access Token löschen sollte (Hub tut es nicht; Schlüssel ist ohnehin kompromittiert, Rotation gibt es in Format 8 nicht).
  8. Recovery-Key-Trennzeichen und Normalisierung (Groß/Klein, Mehrfach-Leerzeichen) beim Dekodieren.
  9. Notfallzugriff (Secret-Sharing-Verfahren) nicht untersucht.
  10. Hub ≥ 2.2 (UVF, apiLevel 5) noch unveröffentlicht; Verhalten älterer Desktop-Versionen gegenüber einem Server mit apiLevel 5 nicht geprüft.
  11. WoT: Statuscode von PUT /users/trusted/{userId} bei unbekannter Ziel-ID bzw. Aufrufer ohne DB-Zeile (vermutlich 500, nicht beobachtet); Auswahl unter gleich kurzen Ketten ist nicht deterministisch.

Hinweis zum Test-Stack: Beim Prüfen wurde versehentlich ein Test-Vault (11111111-…) über PUT /api/vaults/{id} angelegt und sofort per DELETE wieder entfernt (204). Inzwischen ist im Test-Stack eine (Trial-)Lizenz aktiv (licenseSetupRequired: false, 5 Sitze).