Riepilogo
| Data modifica | 18 maggio 2026 |
| File modificato | html/js/calendar/crypto-worker.js |
| Funzione interessata | derivePasskeyKEK() |
| Algoritmo | HKDF-SHA-256 → AES-GCM-256 (invariato) |
| Motivo | Evitare il percorso di blocco noto di deriveKey() nei Worker Safari / WebKit |
| Impatto crittografico | Nessuno. Entrambi i percorsi producono lo stesso materiale di chiave. |
| Verifica | Il test di parità tests/crypto/hkdf-equivalence.mjs conferma un comportamento di decrittazione identico |
Contesto: architettura KEK passkey
PayCal protegge i dati per utente con una data encryption key (DEK) conservata cifrata, mai in chiaro. Per decifrarla al login, PayCal deriva una key encryption key (KEK) dalla credenziale passkey dell’utente.
La derivazione avviene dentro un Web Worker, un thread di background isolato che gestisce operazioni crittografiche separato dal thread principale del browser. Il Worker chiama la WebCrypto API del browser (crypto.subtle) per eseguire lo step di derivazione HKDF.
L’input della derivazione è il credentialId stabile della credenziale passkey WebAuthn, non la firma. Le firme ECDSA sono non deterministiche, quindi una KEK derivata da una firma cambierebbe a ogni login e non riuscirebbe a unwrap della DEK. Usare il credentialId garantisce che venga prodotta la stessa KEK ogni volta che l’utente si autentica con lo stesso dispositivo.
I parametri HKDF sono:
- Algoritmo: HKDF-SHA-256
- IKM: codifica UTF-8 di
credentialId - Salt: valore casuale di 32 byte per utente conservato server-side
- Info: stringa di dominio fissa
paycal-passkey-kek - Output: chiave AES-GCM a 256 bit (non estraibile)
La WebCrypto API: due percorsi verso la stessa chiave
La WebCrypto API espone due modi per produrre una chiave derivata da un input HKDF:
crypto.subtle.deriveKey(algorithm, baseKey, derivedKeyType, extractable, usages)- Esegue la derivazione HKDF e restituisce direttamente un oggetto
CryptoKey. È la chiamata più compatta: il chiamante specifica in un solo step l’algoritmo della chiave di output, come AES-GCM-256. crypto.subtle.deriveBits(algorithm, baseKey, length)+crypto.subtle.importKey(format, keyData, algorithm, extractable, usages)- Esegue la derivazione HKDF e restituisce prima byte grezzi. Il chiamante poi importa quei byte come
CryptoKeycon una seconda chiamataimportKey. È un equivalente in due step: la chiave risultante è identica per tipo, lunghezza, estraibilità e usi consentiti.
Entrambi i percorsi invocano la stessa funzione HKDF-SHA-256, consumano gli stessi parametri IKM, salt e info, e producono gli stessi 256 bit di materiale di chiave. L’unica differenza è come il runtime del Web Worker elabora internamente la chiamata.
Il problema: blocco osservato di deriveKey() nei Web Worker Safari / WebKit
In Safari / WebKit è stato osservato operativamente un percorso di blocco di deriveKey() quando la chiamata viene effettuata da un Web Worker. Gli utenti Safari potevano incontrare un flusso di login che si fermava semplicemente, senza errore, timeout o feedback visibile, mentre attendeva la derivazione del Worker.
La chiamata deriveBits(), usata per produrre lo stesso output, non mostra questo blocco. Il problema è specifico del percorso di codice deriveKey() nel contesto di esecuzione del Worker; non riguarda l’algoritmo HKDF sottostante.
deriveKey() e deriveBits() sono entrambe API WebCrypto di prima classe, documentate da Apple e specificate dal W3C. Le loro semantiche sono identiche per questo caso d’uso. Il blocco è una regressione runtime, non una differenza di design fra le due chiamate.
Non esiste un ticket Apple pubblico canonico indicizzato per questa esatta combinazione Worker + HKDF + deriveKey(). È coerente con una classe di regressioni crypto edge-case Safari/WebKit scoperte operativamente, aggirate in produzione e non tracciate formalmente in un ticket pubblico. La nostra decisione tecnica è evitare completamente il percorso problematico, indipendentemente dalla versione WebKit, invece di rilevare lo stato del browser a runtime.
Cosa abbiamo cambiato
La funzione derivePasskeyKEK() in html/js/calendar/crypto-worker.js è stata aggiornata per usare deriveBits() + importKey() come unico percorso di derivazione. La chiamata deriveKey() è stata rimossa del tutto.
La nuova sequenza di derivazione è:
- Importare i byte IKM come chiave base HKDF tramite
importKey('raw', ikmMaterial, 'HKDF', false, ['deriveBits']) - Derivare 256 bit grezzi tramite
deriveBits(hkdfParams, ikm, 256) - Importare quei bit come chiave AES-GCM-256 tramite
importKey('raw', keyBits, { name: 'AES-GCM', length: 256 }, false, ['encrypt', 'decrypt'])
I parametri HKDF (hash SHA-256, salt per utente, stringa info fissa paycal-passkey-kek) sono invariati. Tipo di chiave di output, lunghezza e flag non estraibile sono invariati. Le DEK wrapped esistenti per utenti che avevano usato il vecchio percorso sono pienamente compatibili: non servono re-wrapping o nuova registrazione.
Prova di equivalenza crittografica
Non affermiamo equivalenza solo per ragionamento. Il file tests/crypto/hkdf-equivalence.mjs contiene un test di parità che:
- Deriva una chiave tramite Percorso A (
deriveKey()) usando gli stessi parametri HKDF - Deriva una chiave tramite Percorso B (
deriveBits()+importKey()) usando gli stessi parametri HKDF - Cifra un payload di test con la chiave del Percorso A
- Decifra quel ciphertext con la chiave del Percorso B
- Verifica che i byte decifrati corrispondano esattamente al plaintext originale
- Ripete al contrario: cifra con Percorso B, decifra con Percorso A
Se entrambe le decrittazioni riescono e producono gli stessi byte, le chiavi sono operativamente identiche. Il test passa in Node.js 18+ ed è referenziato nel nostro SOC 2 false-positive adjudication ledger come elemento di evidenza CRYPTO-001.
Cosa non è cambiato
- L’algoritmo di derivazione della chiave resta HKDF-SHA-256
- La chiave di output resta AES-GCM a 256 bit
- La chiave di output resta non estraibile dal contesto del Worker
- La sorgente IKM resta il
credentialIdstabile (non la firma ECDSA) - La stringa info HKDF resta
paycal-passkey-kek - I salt per utente sono invariati
- Non sono richieste azioni utente, nuova registrazione o rotazione credenziali
- La data encryption key (DEK) wrapped sotto la vecchia KEK è pienamente compatibile con la nuova KEK: i byte sono la stessa chiave
Perché lo pubblichiamo
Questa modifica non influisce sulla sicurezza. Influisce però sul comportamento del nostro codice su uno specifico motore browser e tocca la parte più sensibile del nostro stack crittografico client-side.
La pubblichiamo perché la nostra policy di trasparenza richiede che ogni modifica alla logica di derivazione delle chiavi, anche se neutra nel comportamento, sia documentata pubblicamente con ragionamento tecnico ed evidenza di verifica disponibili per utenti e auditor.
Le modifiche crittografiche non dovrebbero mai essere invisibili, anche quando sono intenzionalmente inerti.
Riferimenti
-
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.