Samenvatting
| Wijzigingsdatum | 18 mei 2026 |
| Gewijzigd bestand | html/js/calendar/crypto-worker.js |
| Betrokken functie | derivePasskeyKEK() |
| Algoritme | HKDF-SHA-256 → AES-GCM-256 (ongewijzigd) |
| Reden | Bekend deriveKey()-Worker-hangpad in Safari / WebKit vermijden |
| Cryptografische impact | Geen. Beide paden produceren hetzelfde sleutelmateriaal. |
| Verificatie | Pariteitstest tests/crypto/hkdf-equivalence.mjs bevestigt identiek decryptgedrag |
Achtergrond: passkey-KEK-architectuur
PayCal beschermt gegevens per gebruiker met een data encryption key (DEK) die versleuteld wordt opgeslagen, nooit als platte tekst. Om die bij het inloggen te ontsleutelen, leidt PayCal een key encryption key (KEK) af uit de passkey-credential van de gebruiker.
De derivatie draait in een Web Worker, een gesandboxte achtergrondthread die cryptografische operaties gescheiden van de hoofdthread van de browser uitvoert. De Worker roept de WebCrypto API van de browser (crypto.subtle) aan om de HKDF-derivatiestap uit te voeren.
De invoer voor de derivatie is de stabiele credentialId uit de WebAuthn-passkey-credential, niet de handtekening. ECDSA-handtekeningen zijn niet deterministisch; een KEK die uit een handtekening wordt afgeleid zou bij elke login verschillen en de DEK niet kunnen unwrapen. Gebruik van de credentialId garandeert dat dezelfde KEK wordt geproduceerd telkens wanneer de gebruiker met hetzelfde apparaat authenticeert.
De HKDF-parameters zijn:
- Algoritme: HKDF-SHA-256
- IKM: UTF-8-codering van
credentialId - Salt: willekeurige 32-byte waarde per gebruiker, server-side opgeslagen
- Info: vaste domeinstring
paycal-passkey-kek - Output: 256-bit AES-GCM-sleutel (niet extraheerbaar)
De WebCrypto API: twee paden naar dezelfde sleutel
De WebCrypto API biedt twee manieren om uit HKDF-invoer een afgeleide sleutel te produceren:
crypto.subtle.deriveKey(algorithm, baseKey, derivedKeyType, extractable, usages)- Voert HKDF-derivatie uit en geeft direct een
CryptoKey-object terug. Dit is de compactere call: de caller specificeert het output-key-algoritme, zoals AES-GCM-256, in één stap. crypto.subtle.deriveBits(algorithm, baseKey, length)+crypto.subtle.importKey(format, keyData, algorithm, extractable, usages)- Voert HKDF-derivatie uit en geeft eerst ruwe bytes terug. De caller importeert die bytes daarna als
CryptoKeymet een tweedeimportKey-call. Dit is een tweestaps equivalent: het resulterende sleutelobject is identiek in type, lengte, extraheerbaarheid en toegestane usages.
Beide paden roepen dezelfde HKDF-SHA-256-functie aan, gebruiken dezelfde IKM-, salt- en info-parameters en produceren dezelfde 256 bits sleutelmateriaal. Het enige verschil is hoe de Web Worker-runtime de call intern verwerkt.
Het probleem: geobserveerde deriveKey()-hang in Safari / WebKit Web Workers
Er is een operationeel geobserveerd deriveKey()-hangpad in Safari / WebKit wanneer de call vanuit een Web Worker wordt gedaan. Safari-gebruikers konden een loginflow ervaren die simpelweg stopte, zonder fout, timeout of zichtbaar feedback, terwijl werd gewacht tot de Worker de derivatie afrondde.
De deriveBits()-call, gebruikt om dezelfde output te produceren, vertoont deze hang niet. Het probleem is specifiek voor het deriveKey()-codepad binnen de Worker-uitvoeringscontext; het raakt het onderliggende HKDF-algoritme zelf niet.
deriveKey() en deriveBits() zijn beide first-class WebCrypto APIs, gedocumenteerd door Apple en gespecificeerd door W3C. Hun semantiek is identiek voor deze use case. De hang is een runtime-regressie, geen ontwerpverschil tussen de twee calls.
Voor deze exacte combinatie van Worker + HKDF + deriveKey() is geen canoniek openbaar Apple-bugticket geïndexeerd. Dit past bij een klasse Safari/WebKit edge-case crypto-regressies die operationeel worden ontdekt, in productie worden omzeild en niet formeel in een openbaar ticket worden gevolgd. Onze engineeringbeslissing is het problematische callpad volledig te vermijden, ongeacht WebKit-versie, in plaats van browserstatus runtime te detecteren.
Wat we hebben gewijzigd
De functie derivePasskeyKEK() in html/js/calendar/crypto-worker.js is bijgewerkt om deriveBits() + importKey() als enige derivatiepad te gebruiken. De deriveKey()-call is volledig verwijderd.
De nieuwe derivatiereeks is:
- IKM-bytes als HKDF-basissleutel importeren via
importKey('raw', ikmMaterial, 'HKDF', false, ['deriveBits']) - 256 ruwe bits afleiden via
deriveBits(hkdfParams, ikm, 256) - Die bits als AES-GCM-256-sleutel importeren via
importKey('raw', keyBits, { name: 'AES-GCM', length: 256 }, false, ['encrypt', 'decrypt'])
De HKDF-parameters (SHA-256-hash, salt per gebruiker, vaste info-string paycal-passkey-kek) zijn ongewijzigd. Het output-sleuteltype, de lengte en de niet-extraheerbare vlag zijn ongewijzigd. Bestaande wrapped DEKs van gebruikers die eerder het oude codepad gebruikten blijven volledig compatibel: geen re-wrapping of herinschrijving nodig.
Bewijs van cryptografische equivalentie
We stellen equivalentie niet alleen op basis van redenering. Het bestand tests/crypto/hkdf-equivalence.mjs bevat een pariteitstest die:
- Een sleutel afleidt via Pad A (
deriveKey()) met dezelfde HKDF-parameters - Een sleutel afleidt via Pad B (
deriveBits()+importKey()) met dezelfde HKDF-parameters - Een testpayload versleutelt met de Pad A-sleutel
- Die ciphertext ontsleutelt met de Pad B-sleutel
- Controleert dat de ontsleutelde bytes exact overeenkomen met de oorspronkelijke plaintext
- Dit omgekeerd herhaalt: versleutelen met Pad B, ontsleutelen met Pad A
Als beide ontsleutelingen slagen en dezelfde bytes produceren, zijn de sleutels operationeel identiek. De test slaagt in Node.js 18+ en wordt in ons SOC 2 false-positive adjudication ledger genoemd als bewijsitem CRYPTO-001.
Wat niet is veranderd
- Het sleutelderivatiealgoritme blijft HKDF-SHA-256
- De outputsleutel blijft 256-bit AES-GCM
- De outputsleutel blijft niet extraheerbaar uit de Worker-context
- De IKM-bron blijft de stabiele
credentialId(niet de ECDSA-handtekening) - De HKDF-info-string blijft
paycal-passkey-kek - Salts per gebruiker zijn ongewijzigd
- Geen gebruikersactie, herinschrijving of credentialrotatie is nodig
- De data encryption key (DEK) die onder de oude KEK is gewrapped blijft volledig compatibel met de nieuwe KEK: de bytes zijn dezelfde sleutel
Waarom we dit publiceren
Deze wijziging heeft geen invloed op de beveiliging. Ze beïnvloedt wel hoe onze code zich gedraagt op een specifieke browserengine en raakt het meest beveiligingsgevoelige deel van onze client-side cryptografiestack.
We publiceren dit omdat ons transparantiebeleid vereist dat elke wijziging in sleutelderivatielogica, zelfs een gedragsneutrale wijziging, openbaar wordt gedocumenteerd met technische onderbouwing en verificatiebewijs voor gebruikers en auditors.
Cryptografische wijzigingen zouden nooit onzichtbaar moeten zijn, zelfs wanneer ze bewust inert zijn.
Referenties
-
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.