सारांश
| Change date | 18 मई 2026 |
| File changed | html/js/calendar/crypto-worker.js |
| Function affected | derivePasskeyKEK() |
| Algorithm | HKDF-SHA-256 → AES-GCM-256 (unchanged) |
| Reason | Safari / WebKit में known deriveKey() Worker hang path से बचना |
| Cryptographic impact | कोई नहीं। दोनों paths वही key material बनाते हैं। |
| Verification | Parity test tests/crypto/hkdf-equivalence.mjs identical decrypt behavior confirm करता है |
Background: Passkey KEK architecture
PayCal per-user data को data encryption key (DEK) से protect करता है, जो encrypted रूप में stored रहती है, plaintext में कभी नहीं। Login time पर उसे decrypt करने के लिए PayCal user के passkey credential से key encryption key (KEK) derive करता है।
Derivation एक Web Worker के अंदर चलती है, जो sandboxed background thread है और cryptographic operations को main browser thread से अलग handle करता है। Worker browser की WebCrypto API (crypto.subtle) को call करके HKDF key derivation step चलाता है।
Derivation input WebAuthn passkey credential की stable credentialId है, signature नहीं। ECDSA signatures non-deterministic होते हैं, इसलिए signature से derive हुई KEK हर login पर बदलती और DEK unwrap fail करती। credentialId का उपयोग same device से हर authentication पर same KEK बनाना guarantee करता है।
HKDF parameters ये हैं:
- Algorithm: HKDF-SHA-256
- IKM:
credentialIdकी UTF-8 encoding - Salt: प्रति user random 32-byte value, server-side stored
- Info: fixed domain string
paycal-passkey-kek - Output: 256-bit AES-GCM key (non-extractable)
WebCrypto API: एक ही key तक दो paths
WebCrypto API HKDF input से derived key बनाने के दो तरीके देता है:
crypto.subtle.deriveKey(algorithm, baseKey, derivedKeyType, extractable, usages)- HKDF derivation चलाता है और सीधे
CryptoKeyobject लौटाता है। यह compact call है: caller output key algorithm, जैसे AES-GCM-256, एक step में देता है। crypto.subtle.deriveBits(algorithm, baseKey, length)+crypto.subtle.importKey(format, keyData, algorithm, extractable, usages)- HKDF derivation चलाता है और पहले raw bytes लौटाता है। फिर caller उन bytes को दूसरे
importKeycall सेCryptoKeyके रूप में import करता है। यह two-step equivalent है: resulting key object type, length, extractability और permitted usages में identical है।
दोनों paths वही HKDF-SHA-256 function invoke करते हैं, वही IKM, salt और info parameters consume करते हैं, और वही 256 bits key material produce करते हैं। फर्क केवल यह है कि Web Worker runtime call को internally कैसे process करता है।
समस्या: Safari / WebKit Web Workers में observed deriveKey() hang
Safari / WebKit में Web Worker के अंदर call होने पर deriveKey() का operationally observed hang path है। Safari users को ऐसा login flow मिल सकता था जो बस रुक जाता था: कोई error नहीं, timeout नहीं, visible feedback नहीं, जबकि Worker derivation complete करने का इंतजार कर रहा होता था।
deriveBits() call, जो वही output produce करता है, यह hang नहीं दिखाता। Issue Worker execution context में deriveKey() code path तक सीमित है; underlying HKDF algorithm प्रभावित नहीं है।
deriveKey() और deriveBits() दोनों first-class WebCrypto APIs हैं, Apple द्वारा documented और W3C द्वारा specified। इस use case के लिए semantics identical हैं। Hang runtime regression है, दोनों calls के बीच design difference नहीं।
इस exact Worker + HKDF + deriveKey() combination के लिए कोई canonical public Apple bug ticket indexed नहीं है। यह Safari/WebKit edge-case crypto regressions की उस class जैसा है जो operationally discovered होती है, production में worked around होती है, और public ticket में formally tracked नहीं होती। हमारी engineering decision है कि browser state runtime पर detect करने की बजाय problematic call path को पूरी तरह avoid किया जाए।
हमने क्या बदला
html/js/calendar/crypto-worker.js में derivePasskeyKEK() function को update किया गया ताकि deriveBits() + importKey() ही sole derivation path रहे। deriveKey() call पूरी तरह हटा दिया गया।
नई derivation sequence है:
- IKM bytes को HKDF base key के रूप में
importKey('raw', ikmMaterial, 'HKDF', false, ['deriveBits'])से import करना deriveBits(hkdfParams, ikm, 256)से 256 raw bits derive करना- इन bits को AES-GCM-256 key के रूप में
importKey('raw', keyBits, { name: 'AES-GCM', length: 256 }, false, ['encrypt', 'decrypt'])से import करना
HKDF parameters (SHA-256 hash, per-user salt, fixed info string paycal-passkey-kek) unchanged हैं। Output key type, length और non-extractable flag unchanged हैं। पुराने code path से stored existing wrapped DEKs fully compatible हैं: no re-wrapping or re-enrollment required.
Cryptographic equivalence proof
हम equivalence केवल reasoning से assert नहीं करते। File tests/crypto/hkdf-equivalence.mjs में parity test है जो:
- Same HKDF parameters से Path A (
deriveKey()) द्वारा key derive करता है - Same HKDF parameters से Path B (
deriveBits()+importKey()) द्वारा key derive करता है - Path A key से test payload encrypt करता है
- Path B key से उस ciphertext को decrypt करता है
- Assert करता है कि decrypted bytes original plaintext से exactly match करते हैं
- Reverse दोहराता है: Path B से encrypt, Path A से decrypt
अगर दोनों decryptions succeed करते हैं और same bytes produce करते हैं, keys operationally identical हैं। Test Node.js 18+ में pass करता है और हमारे SOC 2 false-positive adjudication ledger में evidence item CRYPTO-001 के रूप में referenced है।
क्या नहीं बदला
- Key derivation algorithm अब भी HKDF-SHA-256 है
- Output key अब भी 256-bit AES-GCM है
- Output key Worker context से अब भी non-extractable है
- IKM source अब भी stable
credentialIdहै (ECDSA signature नहीं) - HKDF info string अब भी
paycal-passkey-kekहै - Per-user salts unchanged हैं
- कोई user action, re-enrollment, या credential rotation जरूरी नहीं
- पुराने KEK के तहत wrapped data encryption key (DEK) नए KEK के साथ fully compatible है: bytes वही key हैं
हम इसे क्यों publish कर रहे हैं
यह बदलाव security को प्रभावित नहीं करता। लेकिन यह specific browser engine पर हमारे code के behavior को प्रभावित करता है और client-side cryptography stack के सबसे security-sensitive हिस्से को छूता है।
हम इसे publish कर रहे हैं क्योंकि हमारी transparency policy मांग करती है कि key derivation logic में कोई भी बदलाव, भले behaviorally neutral हो, publicly documented रहे, technical reasoning और verification evidence users और auditors के review के लिए उपलब्ध हो।
Cryptographic changes कभी invisible नहीं होने चाहिए, भले वे intentionally inert हों।
संदर्भ
-
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.