Passkey KEK derivation: deriveBits vs deriveKey

Umaasa ang passkey login ng PayCal sa isang key encryption key (KEK) na dini-derive sa loob ng Web Worker gamit ang WebCrypto API. Noong Mayo 2026 binago namin ang derivation mula deriveKey() papuntang deriveBits() + importKey() para maiwasan ang known hang path sa Safari / WebKit. Ipinapaliwanag ng article na ito ang pagbabago, bakit cryptographically identical ang dalawang path, at paano namin ito na-verify.

Summary

Change date Mayo 18, 2026
File changed html/js/calendar/crypto-worker.js
Function affected derivePasskeyKEK()
Algorithm HKDF-SHA-256 → AES-GCM-256 (unchanged)
Reason Iwasan ang known deriveKey() Worker hang path sa Safari / WebKit
Cryptographic impact Wala. Parehong key material ang ginagawa ng dalawang path.
Verification Kinukumpirma ng parity test tests/crypto/hkdf-equivalence.mjs ang identical decrypt behavior

Background: Passkey KEK architecture

Pinoprotektahan ng PayCal ang per-user data gamit ang data encryption key (DEK) na naka-store nang encrypted, hindi kailanman plain text. Para ma-decrypt ito sa login time, nagde-derive ang PayCal ng key encryption key (KEK) mula sa passkey credential ng user.

Tumatakbo ang derivation sa loob ng Web Worker, isang sandboxed background thread na humahawak ng cryptographic operations nang hiwalay sa main browser thread. Tinatawag ng Worker ang WebCrypto API ng browser (crypto.subtle) para gawin ang HKDF key derivation step.

Ang input sa derivation ay ang stable credentialId mula sa WebAuthn passkey credential, hindi ang signature. Non-deterministic ang ECDSA signatures, kaya ang KEK na derived mula sa signature ay mag-iiba sa bawat login at mabibigo sa pag-unwrap ng DEK. Ang paggamit ng credentialId ang gumagarantiya na parehong KEK ang ginagawa tuwing nag-a-authenticate ang user gamit ang parehong device.

Ang HKDF parameters ay:

  • Algorithm: HKDF-SHA-256
  • IKM: UTF-8 encoding ng credentialId
  • Salt: Per-user random 32-byte value na naka-store server-side
  • Info: Fixed domain string paycal-passkey-kek
  • Output: 256-bit AES-GCM key (non-extractable)

WebCrypto API: dalawang path papunta sa parehong key

May dalawang paraan ang WebCrypto API para gumawa ng derived key mula sa HKDF input:

crypto.subtle.deriveKey(algorithm, baseKey, derivedKeyType, extractable, usages)
Gumagawa ng HKDF derivation at direktang nagbabalik ng CryptoKey object. Ito ang mas compact na call: isinasama ng caller ang output key algorithm, gaya ng AES-GCM-256, sa isang step.
crypto.subtle.deriveBits(algorithm, baseKey, length) + crypto.subtle.importKey(format, keyData, algorithm, extractable, usages)
Gumagawa ng HKDF derivation at raw bytes muna ang ibinabalik. Pagkatapos, ini-import ng caller ang bytes bilang CryptoKey gamit ang pangalawang importKey call. Two-step equivalent ito: identical ang resulting key object sa type, length, extractability, at permitted usages.

Parehong HKDF-SHA-256 function ang ginagamit ng dalawang path, parehong IKM, salt, at info parameters ang kino-consume, at parehong 256 bits ng key material ang ginagawa. Ang kaibahan lang ay kung paano pinoproseso internally ng Web Worker runtime ang call.

Ang problema: observed deriveKey() hang sa Safari / WebKit Web Workers

May operationally observed deriveKey() hang path sa Safari / WebKit kapag ang call ay ginawa mula sa loob ng Web Worker. Maaaring makaranas ang Safari users ng login flow na bigla na lang tumitigil, walang error, walang timeout, at walang visible feedback, habang hinihintay matapos ng Worker ang derivation.

Ang deriveBits() call, na ginagamit para gumawa ng parehong output, ay hindi nagpapakita ng hang na ito. Specific ang issue sa deriveKey() code path sa loob ng Worker execution context; hindi nito naaapektuhan ang underlying HKDF algorithm.

Parehong first-class WebCrypto APIs ang deriveKey() at deriveBits(), documented ng Apple at specified ng W3C. Identical ang semantics nila para sa use case na ito. Runtime regression ang hang, hindi design difference ng dalawang call.

Walang canonical public Apple bug ticket na indexed para sa exact Worker + HKDF + deriveKey() combination na ito. Tugma ito sa class ng Safari/WebKit edge-case crypto regressions na nadidiskubre operationally, nira-round around sa production, at hindi formally tracked sa public ticket. Engineering decision namin na iwasan ang problematic call path nang buo, anuman ang WebKit version, sa halip na mag-detect ng browser state at runtime.

Ano ang binago namin

In-update ang derivePasskeyKEK() function sa html/js/calendar/crypto-worker.js para gamitin ang deriveBits() + importKey() bilang sole derivation path. Tuluyang inalis ang deriveKey() call.

Ang bagong derivation sequence ay:

  1. I-import ang IKM bytes bilang HKDF base key gamit ang importKey('raw', ikmMaterial, 'HKDF', false, ['deriveBits'])
  2. Mag-derive ng 256 raw bits gamit ang deriveBits(hkdfParams, ikm, 256)
  3. I-import ang bits na iyon bilang AES-GCM-256 key gamit ang importKey('raw', keyBits, { name: 'AES-GCM', length: 256 }, false, ['encrypt', 'decrypt'])

Hindi nagbago ang HKDF parameters (SHA-256 hash, per-user salt, fixed info string paycal-passkey-kek). Hindi nagbago ang output key type, length, at non-extractable flag. Fully compatible ang existing wrapped DEKs ng users na dating gumamit ng old code path: walang re-wrapping o re-enrollment na kailangan.

Cryptographic equivalence proof

Hindi lang reasoning ang basehan namin sa equivalence. Ang file tests/crypto/hkdf-equivalence.mjs ay may parity test na:

  1. Nagde-derive ng key via Path A (deriveKey()) gamit ang parehong HKDF parameters
  2. Nagde-derive ng key via Path B (deriveBits() + importKey()) gamit ang parehong HKDF parameters
  3. Nag-e-encrypt ng test payload gamit ang Path A key
  4. Nagde-decrypt ng ciphertext gamit ang Path B key
  5. Ina-assert na exact match ang decrypted bytes sa original plaintext
  6. Inuulit nang pabaliktad: encrypt with Path B, decrypt with Path A

Kung parehong successful ang decryptions at parehong bytes ang lumabas, operationally identical ang keys. Passing ang test sa Node.js 18+ at referenced ito sa aming SOC 2 false-positive adjudication ledger bilang evidence item CRYPTO-001.

Ano ang hindi nagbago

  • HKDF-SHA-256 pa rin ang key derivation algorithm
  • 256-bit AES-GCM pa rin ang output key
  • Non-extractable pa rin ang output key mula sa Worker context
  • Stable credentialId pa rin ang IKM source (hindi ECDSA signature)
  • paycal-passkey-kek pa rin ang HKDF info string
  • Unchanged ang per-user salts
  • Walang user action, re-enrollment, o credential rotation na kailangan
  • Fully compatible ang data encryption key (DEK) na wrapped sa lumang KEK sa bagong KEK: parehong key ang bytes

Bakit namin ito ipinapublish

Hindi naaapektuhan ng pagbabagong ito ang security. Pero naaapektuhan nito kung paano kumikilos ang code namin sa isang specific browser engine, at hinahawakan nito ang pinaka-security-sensitive na bahagi ng client-side cryptography stack namin.

Ipinapublish namin ito dahil kailangan ng transparency policy namin na anumang pagbabago sa key derivation logic, kahit behaviorally neutral, ay publicly documented kasama ang technical reasoning at verification evidence para mareview ng users at auditors.

Hindi dapat maging invisible ang cryptographic changes, kahit intentionally inert ang mga ito.

Mga reference