Resumen
| Fecha del cambio | 18 de mayo de 2026 |
| Archivo modificado | html/js/calendar/crypto-worker.js |
| Función afectada | derivePasskeyKEK() |
| Algoritmo | HKDF-SHA-256 → AES-GCM-256 (sin cambios) |
| Motivo | Evitar la ruta de bloqueo conocida de deriveKey() en Workers de Safari / WebKit |
| Impacto criptográfico | Ninguno. Ambas rutas producen el mismo material de clave. |
| Verificación | La prueba de paridad tests/crypto/hkdf-equivalence.mjs confirma comportamiento de descifrado idéntico |
Contexto: arquitectura KEK de passkeys
PayCal protege los datos por usuario con una data encryption key (DEK) que se almacena cifrada, nunca en texto plano. Para descifrarla al iniciar sesión, PayCal deriva una key encryption key (KEK) desde la credencial passkey del usuario.
La derivación se ejecuta dentro de un Web Worker, un hilo de fondo aislado que maneja operaciones criptográficas separado del hilo principal del navegador. El Worker llama a la WebCrypto API del navegador (crypto.subtle) para ejecutar el paso de derivación HKDF.
La entrada de la derivación es el credentialId estable de la credencial passkey WebAuthn, no la firma. Las firmas ECDSA no son deterministas, por lo que cualquier KEK derivada de una firma cambiaría en cada inicio de sesión y no podría desenvolver la DEK. Usar el credentialId garantiza que se produzca la misma KEK cada vez que el usuario se autentica con el mismo dispositivo.
Los parámetros HKDF son:
- Algoritmo: HKDF-SHA-256
- IKM: codificación UTF-8 de
credentialId - Salt: valor aleatorio de 32 bytes por usuario almacenado en el servidor
- Info: cadena de dominio fija
paycal-passkey-kek - Salida: clave AES-GCM de 256 bits (no extraíble)
La WebCrypto API: dos rutas hacia la misma clave
La WebCrypto API expone dos formas de producir una clave derivada desde una entrada HKDF:
crypto.subtle.deriveKey(algorithm, baseKey, derivedKeyType, extractable, usages)- Ejecuta la derivación HKDF y devuelve directamente un objeto
CryptoKey. Es la llamada más compacta: quien llama especifica el algoritmo de clave de salida, como AES-GCM-256, en un solo paso. crypto.subtle.deriveBits(algorithm, baseKey, length)+crypto.subtle.importKey(format, keyData, algorithm, extractable, usages)- Ejecuta la derivación HKDF y devuelve primero bytes sin procesar. Luego esos bytes se importan como
CryptoKeymediante una segunda llamadaimportKey. Es un equivalente de dos pasos: el objeto de clave resultante es idéntico en tipo, longitud, capacidad de extracción y usos permitidos.
Ambas rutas invocan la misma función HKDF-SHA-256, consumen los mismos parámetros IKM, salt e info, y producen los mismos 256 bits de material de clave. La única diferencia es cómo el runtime del Web Worker procesa internamente la llamada.
El problema: bloqueo observado de deriveKey() en Web Workers de Safari / WebKit
Existe una ruta de bloqueo de deriveKey() observada operativamente en Safari / WebKit cuando la llamada se hace desde un Web Worker. Los usuarios de Safari podían encontrar un flujo de inicio de sesión que simplemente se detenía, sin error, sin timeout y sin feedback visible, mientras esperaba que el Worker completara la derivación.
La llamada deriveBits(), usada para producir la misma salida, no muestra este bloqueo. El problema es específico de la ruta de código deriveKey() dentro del contexto de ejecución del Worker; no afecta al algoritmo HKDF subyacente.
deriveKey() y deriveBits() son APIs WebCrypto de primera clase documentadas por Apple y especificadas por el W3C. Sus semánticas son idénticas para este caso de uso. El bloqueo es una regresión de runtime, no una diferencia de diseño entre las dos llamadas.
No hay un ticket público canónico de Apple indexado para esta combinación exacta de Worker + HKDF + deriveKey(). Esto coincide con una clase de regresiones criptográficas de borde en Safari/WebKit que se descubren operativamente, se rodean en producción y no quedan formalmente rastreadas en un ticket público. Nuestra decisión de ingeniería es evitar por completo la ruta problemática, sin depender de detectar el estado del navegador en runtime.
Qué cambiamos
La función derivePasskeyKEK() en html/js/calendar/crypto-worker.js se actualizó para usar deriveBits() + importKey() como única ruta de derivación. La llamada deriveKey() se eliminó por completo.
La nueva secuencia de derivación es:
- Importar bytes IKM como clave base HKDF mediante
importKey('raw', ikmMaterial, 'HKDF', false, ['deriveBits']) - Derivar 256 bits sin procesar mediante
deriveBits(hkdfParams, ikm, 256) - Importar esos bits como clave AES-GCM-256 mediante
importKey('raw', keyBits, { name: 'AES-GCM', length: 256 }, false, ['encrypt', 'decrypt'])
Los parámetros HKDF (hash SHA-256, salt por usuario y cadena info fija paycal-passkey-kek) no cambian. El tipo de clave de salida, la longitud y la marca no extraíble no cambian. Las DEK envueltas existentes de usuarios que usaban la ruta antigua son totalmente compatibles: no se requiere re-wrapping ni nuevo registro.
Prueba de equivalencia criptográfica
No afirmamos equivalencia solo por razonamiento. El archivo tests/crypto/hkdf-equivalence.mjs contiene una prueba de paridad que:
- Deriva una clave por la Ruta A (
deriveKey()) usando los mismos parámetros HKDF - Deriva una clave por la Ruta B (
deriveBits()+importKey()) usando los mismos parámetros HKDF - Cifra una carga de prueba con la clave de la Ruta A
- Descifra ese ciphertext con la clave de la Ruta B
- Afirma que los bytes descifrados coinciden exactamente con el texto plano original
- Repite en sentido inverso: cifra con la Ruta B y descifra con la Ruta A
Si ambos descifrados tienen éxito y producen los mismos bytes, las claves son operacionalmente idénticas. La prueba pasa en Node.js 18+ y se referencia en nuestro SOC 2 false-positive adjudication ledger como evidencia CRYPTO-001.
Qué no cambió
- El algoritmo de derivación de clave sigue siendo HKDF-SHA-256
- La clave de salida sigue siendo AES-GCM de 256 bits
- La clave de salida sigue siendo no extraíble desde el contexto del Worker
- La fuente IKM sigue siendo el
credentialIdestable (no la firma ECDSA) - La cadena info HKDF sigue siendo
paycal-passkey-kek - Los salts por usuario no cambian
- No se requiere acción del usuario, nuevo registro ni rotación de credenciales
- La data encryption key (DEK) envuelta bajo la KEK antigua es totalmente compatible con la nueva KEK: los bytes son la misma clave
Por qué publicamos esto
Este cambio no afecta la seguridad. Sí afecta cómo se comporta nuestro código en un motor de navegador específico y toca la parte más sensible de nuestra pila criptográfica del lado del cliente.
Lo publicamos porque nuestra política de transparencia exige que cualquier cambio en la lógica de derivación de claves, incluso uno neutral en comportamiento, se documente públicamente con la razón técnica y la evidencia de verificación disponibles para usuarios y auditores.
Los cambios criptográficos nunca deberían ser invisibles, incluso cuando son intencionalmente inertes.
Referencias
-
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.