Zusammenfassung
| Änderungsdatum | 18. Mai 2026 |
| Geänderte Datei | html/js/calendar/crypto-worker.js |
| Betroffene Funktion | derivePasskeyKEK() |
| Algorithmus | HKDF-SHA-256 → AES-GCM-256 (unverändert) |
| Grund | Bekannten deriveKey()-Worker-Hang-Pfad in Safari / WebKit vermeiden |
| Kryptografische Auswirkung | Keine. Beide Pfade erzeugen dasselbe Schlüsselmaterial. |
| Verifikation | Paritätstest tests/crypto/hkdf-equivalence.mjs bestätigt identisches Entschlüsselungsverhalten |
Hintergrund: Passkey-KEK-Architektur
PayCal schützt nutzerbezogene Daten mit einem data encryption key (DEK), der verschlüsselt gespeichert wird, nie im Klartext. Um ihn beim Login zu entschlüsseln, leitet PayCal einen key encryption key (KEK) aus dem Passkey-Credential des Nutzers ab.
Die Ableitung läuft in einem Web Worker, einem sandboxed Hintergrund-Thread, der kryptografische Operationen getrennt vom Haupt-Browser-Thread ausführt. Der Worker ruft die WebCrypto API des Browsers (crypto.subtle) auf, um den HKDF-Ableitungsschritt auszuführen.
Eingabe für die Ableitung ist die stabile credentialId aus dem WebAuthn-Passkey-Credential, nicht die Signatur. ECDSA-Signaturen sind nicht deterministisch; jeder aus einer Signatur abgeleitete KEK wäre bei jedem Login anders und könnte den DEK nicht entpacken. Die Nutzung der credentialId garantiert, dass bei jeder Authentifizierung mit demselben Gerät derselbe KEK entsteht.
Die HKDF-Parameter sind:
- Algorithmus: HKDF-SHA-256
- IKM: UTF-8-Encoding von
credentialId - Salt: Pro Nutzer zufälliger 32-Byte-Wert, serverseitig gespeichert
- Info: Fester Domain-String
paycal-passkey-kek - Output: 256-Bit-AES-GCM-Schlüssel (nicht extrahierbar)
Die WebCrypto API: Zwei Pfade zum selben Schlüssel
Die WebCrypto API stellt zwei Wege bereit, um aus einer HKDF-Eingabe einen abgeleiteten Schlüssel zu erzeugen:
crypto.subtle.deriveKey(algorithm, baseKey, derivedKeyType, extractable, usages)- Führt die HKDF-Ableitung aus und gibt direkt ein
CryptoKey-Objekt zurück. Dies ist der kompaktere Aufruf: Der Aufrufer gibt den Ausgabeschlüsselalgorithmus, etwa AES-GCM-256, in einem Schritt an. crypto.subtle.deriveBits(algorithm, baseKey, length)+crypto.subtle.importKey(format, keyData, algorithm, extractable, usages)- Führt die HKDF-Ableitung aus und gibt zuerst rohe Bytes zurück. Danach importiert der Aufrufer diese Bytes mit einem zweiten
importKey-Aufruf alsCryptoKey. Das ist zweistufig äquivalent: Der resultierende Schlüssel ist in Typ, Länge, Extrahierbarkeit und erlaubter Nutzung identisch.
Beide Pfade rufen dieselbe HKDF-SHA-256-Funktion auf, verwenden dieselben IKM-, Salt- und Info-Parameter und erzeugen dieselben 256 Bit Schlüsselmaterial. Der einzige Unterschied ist, wie die Web-Worker-Laufzeit den Aufruf intern verarbeitet.
Das Problem: Beobachteter deriveKey()-Hang in Safari / WebKit Web Workers
In Safari / WebKit gibt es einen operativ beobachteten deriveKey()-Hang-Pfad, wenn der Aufruf aus einem Web Worker erfolgt. Safari-Nutzer konnten einen Login-Flow erleben, der einfach stoppte: kein Fehler, kein Timeout und kein sichtbares Feedback, während auf den Abschluss der Ableitung gewartet wurde.
Der deriveBits()-Aufruf, der dieselbe Ausgabe erzeugt, zeigt diesen Hang nicht. Das Problem liegt spezifisch im deriveKey()-Codepfad innerhalb des Worker-Ausführungskontexts; der zugrunde liegende HKDF-Algorithmus selbst ist nicht betroffen.
deriveKey() und deriveBits() sind beide erstklassige WebCrypto APIs, von Apple dokumentiert und durch das W3C spezifiziert. Für diesen Anwendungsfall sind ihre Semantiken identisch. Der Hang ist eine Runtime-Regression, kein Designunterschied zwischen den beiden Aufrufen.
Für diese exakte Kombination aus Worker + HKDF + deriveKey() ist kein kanonisches Apple-Bugticket öffentlich indexiert. Das passt zu einer Klasse von Safari/WebKit-Edge-Case-Krypto-Regressionen, die operativ entdeckt, in Produktion umgangen und nicht öffentlich sichtbar formal verfolgt werden. Unsere technische Entscheidung ist, den problematischen Aufrufpfad unabhängig von der WebKit-Version vollständig zu vermeiden, statt Browserzustand zur Laufzeit zu erkennen.
Was wir geändert haben
Die Funktion derivePasskeyKEK() in html/js/calendar/crypto-worker.js wurde so aktualisiert, dass deriveBits() + importKey() der einzige Ableitungspfad ist. Der deriveKey()-Aufruf wurde vollständig entfernt.
Die neue Ableitungssequenz ist:
- IKM-Bytes als HKDF-Basisschlüssel über
importKey('raw', ikmMaterial, 'HKDF', false, ['deriveBits'])importieren - 256 rohe Bits über
deriveBits(hkdfParams, ikm, 256)ableiten - Diese Bits als AES-GCM-256-Schlüssel über
importKey('raw', keyBits, { name: 'AES-GCM', length: 256 }, false, ['encrypt', 'decrypt'])importieren
Die HKDF-Parameter (SHA-256-Hash, pro Nutzer Salt, fester Info-String paycal-passkey-kek) sind unverändert. Schlüsseltyp, Länge und Nicht-Extrahierbarkeit der Ausgabe sind unverändert. Bestehende wrapped DEKs von Nutzern, die zuvor den alten Codepfad verwendet haben, bleiben vollständig kompatibel: kein Re-Wrapping und keine erneute Registrierung erforderlich.
Nachweis kryptografischer Äquivalenz
Wir behaupten Äquivalenz nicht nur durch Argumentation. Die Datei tests/crypto/hkdf-equivalence.mjs enthält einen Paritätstest, der:
- Einen Schlüssel über Pfad A (
deriveKey()) mit denselben HKDF-Parametern ableitet - Einen Schlüssel über Pfad B (
deriveBits()+importKey()) mit denselben HKDF-Parametern ableitet - Eine Test-Payload mit dem Pfad-A-Schlüssel verschlüsselt
- Diesen Ciphertext mit dem Pfad-B-Schlüssel entschlüsselt
- Prüft, dass die entschlüsselten Bytes exakt dem ursprünglichen Plaintext entsprechen
- Den Ablauf umkehrt: mit Pfad B verschlüsseln, mit Pfad A entschlüsseln
Wenn beide Entschlüsselungen gelingen und dieselben Bytes erzeugen, sind die Schlüssel operativ identisch. Der Test besteht in Node.js 18+ und ist in unserem SOC 2 false-positive adjudication ledger als Evidenzobjekt CRYPTO-001 referenziert.
Was sich nicht geändert hat
- Der Schlüsselableitungsalgorithmus bleibt HKDF-SHA-256
- Der Ausgabeschlüssel bleibt 256-Bit-AES-GCM
- Der Ausgabeschlüssel bleibt aus dem Worker-Kontext nicht extrahierbar
- Die IKM-Quelle bleibt die stabile
credentialId(nicht die ECDSA-Signatur) - Der HKDF-Info-String bleibt
paycal-passkey-kek - Pro-Nutzer-Salts bleiben unverändert
- Keine Nutzeraktion, erneute Registrierung oder Credential-Rotation ist nötig
- Der data encryption key (DEK), der unter dem alten KEK gewrappt wurde, ist vollständig mit dem neuen KEK kompatibel: Die Bytes sind derselbe Schlüssel
Warum wir das veröffentlichen
Diese Änderung wirkt sich nicht auf die Sicherheit aus. Sie beeinflusst aber, wie unser Code auf einer bestimmten Browser-Engine läuft, und berührt den sicherheitssensibelsten Teil unseres clientseitigen Kryptografie-Stacks.
Wir veröffentlichen sie, weil unsere Transparenzrichtlinie verlangt, dass jede Änderung an Schlüsselableitungslogik, auch eine verhaltensneutrale, öffentlich dokumentiert wird, inklusive technischer Begründung und Verifikationsevidenz für Nutzer und Auditoren.
Kryptografische Änderungen sollten nie unsichtbar sein, selbst wenn sie absichtlich inert sind.
Referenzen
-
WebKit Blog — Update on Web Cryptography
webkit.org/blog/7790/update-on-web-cryptography/
Official Apple/WebKit announcement adding HKDF and related WebCrypto improvements. Authoritative baseline for HKDF support in WebKit. -
Apple Developer — SubtleCrypto Documentation
developer.apple.com/documentation/webkitjs/subtlecrypto
Confirms bothderiveBits()andderiveKey()are first-class APIs in WebKit. Relevant to the architectural justification for usingderiveBits(). -
MDN — SubtleCrypto.deriveBits()
developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/deriveBits
W3C-aligned specification reference forderiveBits(), including Worker support. Confirms this call is a valid and fully supported path in Worker contexts. -
MDN — SubtleCrypto.deriveKey()
developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/deriveKey
W3C-aligned specification reference forderiveKey(). Comparing this with thederiveBits()spec confirms both are specified to work in Workers; the observed Safari hang is a runtime regression, not a spec divergence. -
Stack Overflow — HKDF output length and
deriveKeyvsderiveBitssemantics
stackoverflow.com/questions/79365928/how-to-specify-desired-hkdf-output-length-with-crypto-subtle-derivekey
Practical community discussion showing why developers intentionally preferderiveBits()for deterministic control and cross-browser compatibility when deriving HKDF keys.