Resumo
| Data da mudança | 18 de maio de 2026 |
| Arquivo alterado | html/js/calendar/crypto-worker.js |
| Função afetada | derivePasskeyKEK() |
| Algoritmo | HKDF-SHA-256 → AES-GCM-256 (inalterado) |
| Motivo | Evitar o caminho conhecido de travamento de deriveKey() em Workers Safari / WebKit |
| Impacto criptográfico | Nenhum. Os dois caminhos produzem o mesmo material de chave. |
| Verificação | O teste de paridade tests/crypto/hkdf-equivalence.mjs confirma comportamento de descriptografia idêntico |
Contexto: arquitetura KEK de passkey
A PayCal protege dados por usuário com uma data encryption key (DEK) armazenada criptografada, nunca em texto claro. Para descriptografá-la no login, a PayCal deriva uma key encryption key (KEK) da credencial passkey do usuário.
A derivação roda dentro de um Web Worker, uma thread de background isolada que trata operações criptográficas separada da thread principal do navegador. O Worker chama a WebCrypto API do navegador (crypto.subtle) para executar a etapa de derivação HKDF.
A entrada da derivação é o credentialId estável da credencial passkey WebAuthn, não a assinatura. Assinaturas ECDSA não são determinísticas; uma KEK derivada de assinatura mudaria a cada login e falharia ao desembrulhar a DEK. Usar o credentialId garante que a mesma KEK seja produzida toda vez que o usuário autentica com o mesmo dispositivo.
Os parâmetros HKDF são:
- Algoritmo: HKDF-SHA-256
- IKM: codificação UTF-8 de
credentialId - Salt: valor aleatório de 32 bytes por usuário armazenado no servidor
- Info: string de domínio fixa
paycal-passkey-kek - Saída: chave AES-GCM de 256 bits (não extraível)
A WebCrypto API: dois caminhos para a mesma chave
A WebCrypto API expõe duas formas de produzir uma chave derivada a partir de uma entrada HKDF:
crypto.subtle.deriveKey(algorithm, baseKey, derivedKeyType, extractable, usages)- Executa a derivação HKDF e retorna diretamente um objeto
CryptoKey. É a chamada mais compacta: quem chama especifica o algoritmo da chave de saída, como AES-GCM-256, em uma única etapa. crypto.subtle.deriveBits(algorithm, baseKey, length)+crypto.subtle.importKey(format, keyData, algorithm, extractable, usages)- Executa a derivação HKDF e retorna primeiro bytes brutos. Depois esses bytes são importados como
CryptoKeycom uma segunda chamadaimportKey. É um equivalente em duas etapas: o objeto de chave resultante é idêntico em tipo, comprimento, extraibilidade e usos permitidos.
Os dois caminhos invocam a mesma função HKDF-SHA-256, consomem os mesmos parâmetros IKM, salt e info, e produzem os mesmos 256 bits de material de chave. A única diferença é como o runtime do Web Worker processa internamente a chamada.
O problema: travamento observado de deriveKey() em Web Workers Safari / WebKit
Há um caminho de travamento de deriveKey() observado operacionalmente no Safari / WebKit quando a chamada é feita dentro de um Web Worker. Usuários do Safari podiam encontrar um fluxo de login que simplesmente parava, sem erro, sem timeout e sem feedback visível, enquanto aguardava o Worker concluir a derivação.
A chamada deriveBits(), usada para produzir a mesma saída, não apresenta esse travamento. O problema é específico do caminho de código deriveKey() no contexto de execução do Worker; ele não afeta o algoritmo HKDF subjacente.
deriveKey() e deriveBits() são APIs WebCrypto de primeira classe, documentadas pela Apple e especificadas pelo W3C. Suas semânticas são idênticas para este caso de uso. O travamento é uma regressão de runtime, não uma diferença de desenho entre as duas chamadas.
Não há um ticket público canônico da Apple indexado para esta combinação exata Worker + HKDF + deriveKey(). Isso é consistente com uma classe de regressões criptográficas edge-case do Safari/WebKit descobertas operacionalmente, contornadas em produção e não formalmente rastreadas em um ticket público. Nossa decisão de engenharia é evitar totalmente o caminho problemático, independentemente da versão WebKit, em vez de tentar detectar o estado do navegador em runtime.
O que mudamos
A função derivePasskeyKEK() em html/js/calendar/crypto-worker.js foi atualizada para usar deriveBits() + importKey() como o único caminho de derivação. A chamada deriveKey() foi removida por completo.
A nova sequência de derivação é:
- Importar bytes IKM como chave base HKDF via
importKey('raw', ikmMaterial, 'HKDF', false, ['deriveBits']) - Derivar 256 bits brutos via
deriveBits(hkdfParams, ikm, 256) - Importar esses bits como chave AES-GCM-256 via
importKey('raw', keyBits, { name: 'AES-GCM', length: 256 }, false, ['encrypt', 'decrypt'])
Os parâmetros HKDF (hash SHA-256, salt por usuário, string info fixa paycal-passkey-kek) não mudaram. O tipo de chave de saída, o comprimento e o flag não extraível não mudaram. DEKs wrapped existentes para usuários que usavam o caminho antigo continuam totalmente compatíveis: não é necessário re-wrapping nem novo registro.
Prova de equivalência criptográfica
Não afirmamos equivalência apenas por raciocínio. O arquivo tests/crypto/hkdf-equivalence.mjs contém um teste de paridade que:
- Deriva uma chave pela Rota A (
deriveKey()) usando os mesmos parâmetros HKDF - Deriva uma chave pela Rota B (
deriveBits()+importKey()) usando os mesmos parâmetros HKDF - Criptografa uma carga de teste com a chave da Rota A
- Descriptografa esse ciphertext com a chave da Rota B
- Confirma que os bytes descriptografados correspondem exatamente ao plaintext original
- Repete ao contrário: criptografa com a Rota B e descriptografa com a Rota A
Se ambas as descriptografias têm sucesso e produzem os mesmos bytes, as chaves são operacionalmente idênticas. O teste passa em Node.js 18+ e é referenciado no nosso SOC 2 false-positive adjudication ledger como item de evidência CRYPTO-001.
O que não mudou
- O algoritmo de derivação de chave continua HKDF-SHA-256
- A chave de saída continua AES-GCM de 256 bits
- A chave de saída continua não extraível do contexto do Worker
- A fonte IKM continua sendo o
credentialIdestável (não a assinatura ECDSA) - A string info HKDF continua
paycal-passkey-kek - Salts por usuário permanecem inalterados
- Nenhuma ação do usuário, novo registro ou rotação de credencial é necessária
- A data encryption key (DEK) wrapped sob a KEK antiga é totalmente compatível com a nova KEK: os bytes são a mesma chave
Por que estamos publicando isto
Esta mudança não afeta a segurança. Ela afeta como nosso código se comporta em um mecanismo de navegador específico e toca a parte mais sensível da nossa pilha criptográfica client-side.
Estamos publicando porque nossa política de transparência exige que qualquer mudança na lógica de derivação de chaves, mesmo uma mudança neutra em comportamento, seja documentada publicamente com a razão técnica e a evidência de verificação disponíveis para usuários e auditores.
Mudanças criptográficas nunca deveriam ser invisíveis, mesmo quando são intencionalmente inertes.
Referências
-
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.