Résumé
| Date du changement | 18 mai 2026 |
| Fichier modifié | html/js/calendar/crypto-worker.js |
| Fonction concernée | derivePasskeyKEK() |
| Algorithme | HKDF-SHA-256 → AES-GCM-256 (inchangé) |
| Raison | Éviter le chemin de blocage connu de deriveKey() dans les Workers Safari / WebKit |
| Impact cryptographique | Aucun. Les deux chemins produisent le même matériau de clé. |
| Vérification | Le test de parité tests/crypto/hkdf-equivalence.mjs confirme un comportement de déchiffrement identique |
Contexte : architecture KEK des passkeys
PayCal protège les données par utilisateur avec une data encryption key (DEK) stockée chiffrée, jamais en clair. Pour la déchiffrer à la connexion, PayCal dérive une key encryption key (KEK) depuis la credential passkey de l’utilisateur.
La dérivation s’exécute dans un Web Worker, un thread d’arrière-plan isolé qui traite les opérations cryptographiques séparément du thread principal du navigateur. Le Worker appelle la WebCrypto API du navigateur (crypto.subtle) pour effectuer l’étape de dérivation HKDF.
L’entrée de la dérivation est le credentialId stable de la credential passkey WebAuthn, pas la signature. Les signatures ECDSA ne sont pas déterministes ; une KEK dérivée d’une signature changerait à chaque connexion et ne pourrait pas déballer la DEK. L’utilisation du credentialId garantit que la même KEK est produite à chaque authentification avec le même appareil.
Les paramètres HKDF sont :
- Algorithme : HKDF-SHA-256
- IKM : encodage UTF-8 de
credentialId - Salt : valeur aléatoire de 32 octets par utilisateur, stockée côté serveur
- Info : chaîne de domaine fixe
paycal-passkey-kek - Sortie : clé AES-GCM 256 bits (non extractible)
La WebCrypto API : deux chemins vers la même clé
La WebCrypto API expose deux façons de produire une clé dérivée depuis une entrée HKDF :
crypto.subtle.deriveKey(algorithm, baseKey, derivedKeyType, extractable, usages)- Effectue la dérivation HKDF et retourne directement un objet
CryptoKey. C’est l’appel le plus compact : l’appelant indique l’algorithme de clé de sortie, par exemple AES-GCM-256, en une seule étape. crypto.subtle.deriveBits(algorithm, baseKey, length)+crypto.subtle.importKey(format, keyData, algorithm, extractable, usages)- Effectue la dérivation HKDF et retourne d’abord des octets bruts. L’appelant importe ensuite ces octets en tant que
CryptoKeyavec un second appelimportKey. C’est un équivalent en deux étapes : la clé résultante est identique en type, longueur, extractibilité et usages autorisés.
Les deux chemins invoquent la même fonction HKDF-SHA-256, consomment les mêmes paramètres IKM, salt et info, et produisent les mêmes 256 bits de matériau de clé. La seule différence est la façon dont le runtime du Web Worker traite l’appel en interne.
Le problème : blocage observé de deriveKey() dans les Web Workers Safari / WebKit
Un chemin de blocage deriveKey() a été observé opérationnellement dans Safari / WebKit lorsque l’appel est effectué depuis un Web Worker. Des utilisateurs Safari pouvaient rencontrer un flux de connexion qui s’arrêtait simplement, sans erreur, sans timeout et sans retour visible, en attendant la fin de la dérivation par le Worker.
L’appel deriveBits(), utilisé pour produire la même sortie, ne présente pas ce blocage. Le problème est propre au chemin de code deriveKey() dans le contexte d’exécution du Worker ; il n’affecte pas l’algorithme HKDF sous-jacent.
deriveKey() et deriveBits() sont deux APIs WebCrypto de premier ordre, documentées par Apple et spécifiées par le W3C. Leurs sémantiques sont identiques pour ce cas d’usage. Le blocage est une régression runtime, pas une différence de conception entre les deux appels.
Aucun ticket Apple public canonique n’est indexé pour cette combinaison exacte Worker + HKDF + deriveKey(). Cela correspond à une classe de régressions crypto edge-case Safari/WebKit découvertes opérationnellement, contournées en production et non suivies dans un ticket public visible. Notre décision d’ingénierie est d’éviter entièrement le chemin problématique, quelle que soit la version WebKit, plutôt que de détecter l’état du navigateur au runtime.
Ce que nous avons changé
La fonction derivePasskeyKEK() dans html/js/calendar/crypto-worker.js a été mise à jour pour utiliser deriveBits() + importKey() comme seul chemin de dérivation. L’appel deriveKey() a été entièrement supprimé.
La nouvelle séquence de dérivation est :
- Importer les octets IKM comme clé de base HKDF via
importKey('raw', ikmMaterial, 'HKDF', false, ['deriveBits']) - Dériver 256 bits bruts via
deriveBits(hkdfParams, ikm, 256) - Importer ces bits comme clé AES-GCM-256 via
importKey('raw', keyBits, { name: 'AES-GCM', length: 256 }, false, ['encrypt', 'decrypt'])
Les paramètres HKDF (hash SHA-256, salt par utilisateur, chaîne info fixe paycal-passkey-kek) sont inchangés. Le type de clé de sortie, sa longueur et son indicateur non extractible sont inchangés. Les DEK déjà wrapped pour les utilisateurs ayant utilisé l’ancien chemin restent entièrement compatibles : aucun re-wrapping ni réenrôlement n’est requis.
Preuve d’équivalence cryptographique
Nous n’affirmons pas l’équivalence par raisonnement seul. Le fichier tests/crypto/hkdf-equivalence.mjs contient un test de parité qui :
- Dérive une clé via le chemin A (
deriveKey()) avec les mêmes paramètres HKDF - Dérive une clé via le chemin B (
deriveBits()+importKey()) avec les mêmes paramètres HKDF - Chiffre une charge de test avec la clé du chemin A
- Déchiffre ce ciphertext avec la clé du chemin B
- Vérifie que les octets déchiffrés correspondent exactement au plaintext d’origine
- Répète en sens inverse : chiffrement avec le chemin B, déchiffrement avec le chemin A
Si les deux déchiffrements réussissent et produisent les mêmes octets, les clés sont opérationnellement identiques. Le test passe sous Node.js 18+ et est référencé dans notre SOC 2 false-positive adjudication ledger comme élément de preuve CRYPTO-001.
Ce qui n’a pas changé
- L’algorithme de dérivation de clé reste HKDF-SHA-256
- La clé de sortie reste AES-GCM 256 bits
- La clé de sortie reste non extractible depuis le contexte du Worker
- La source IKM reste le
credentialIdstable (pas la signature ECDSA) - La chaîne info HKDF reste
paycal-passkey-kek - Les salts par utilisateur sont inchangés
- Aucune action utilisateur, aucun réenrôlement et aucune rotation de credential n’est nécessaire
- La data encryption key (DEK) wrapped sous l’ancienne KEK est entièrement compatible avec la nouvelle KEK : les octets sont la même clé
Pourquoi nous publions cela
Ce changement n’affecte pas la sécurité. Il affecte en revanche le comportement de notre code sur un moteur de navigateur spécifique et touche la partie la plus sensible de notre pile cryptographique côté client.
Nous le publions parce que notre politique de transparence exige que tout changement de logique de dérivation de clés, même neutre en comportement, soit documenté publiquement, avec le raisonnement technique et les preuves de vérification disponibles pour les utilisateurs et les auditeurs.
Les changements cryptographiques ne devraient jamais être invisibles, même lorsqu’ils sont intentionnellement inertes.
Références
-
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.