Ir para o conteúdo
HexaTransfer
Voltar ao blog
Criptografia e seguranca

Guia Web Crypto API: encriptação nativa do browser para programadores

Domine a Web Crypto API para aplicações de transferência encriptada. Guia completo de AES-GCM, RSA-OAEP e gestão de chaves no browser.

A Web Crypto API (especificada na recomendação W3C Web Cryptography API, exposta via window.crypto.subtle) é a forma nativa do browser de executar criptografia sem enviar uma biblioteca criptográfica pela rede. Suporta AES-GCM, AES-CBC, AES-CTR, AES-KW, HMAC, RSA-OAEP, RSA-PSS, RSASSA-PKCS1-v1_5, ECDH, ECDSA, HKDF e PBKDF2 em todos os browsers modernos (Chrome 37+, Firefox 34+, Safari 10.1+, Edge 79+). Para aplicações de transferência de ficheiros, isto importa porque cada byte de texto cifrado pode ser gerado do lado do cliente antes do carregamento, com o browser a fornecer uma implementação auditada e em tempo constante. Este guia percorre os primitivos que interessam para a transferência encriptada de ficheiros e os erros que afetam todas as primeiras implementações.

SubtleCrypto é baseado em Promise e assíncrono

Cada método em crypto.subtle retorna uma Promise. Isso é deliberado: as operações criptográficas podem ser descarregadas para hardware ou threads em segundo plano, pelo que forçar o comportamento assíncrono evita que a API seja mal utilizada de formas que bloqueiem a thread principal. Forma do código:

const key = await crypto.subtle.generateKey(
  { name: "AES-GCM", length: 256 },
  true, // extraível
  ["encrypt", "decrypt"]
);

O segundo argumento (true) marca a chave como extraível, o que significa que pode ser exportada posteriormente via crypto.subtle.exportKey(). Para chaves de longa duração, defina-o como false para manter os bytes brutos inacessíveis a partir do JavaScript. Para chaves que precisem de ser serializadas num fragmento de URL (o padrão HexaTransfer), defina-o como true.

O terceiro argumento é um array de utilizações da chave. Uma chave gerada com ["encrypt"] não pode ser utilizada para decifrar, mesmo que o AES-GCM seja simétrico. Esta separação evita que um fluxo de encriptação comprometido seja abusado para decifrar dados históricos.

AES-GCM para encriptação simétrica de ficheiros

O AES-GCM é o motor de trabalho para o conteúdo de ficheiros. Fornece encriptação autenticada com dados associados (AEAD): texto cifrado mais etiqueta de autenticação mais dados associados opcionais que são autenticados mas não encriptados. Para transferência de ficheiros, use uma chave de 256 bits e um nonce de 96 bits conforme as recomendações NIST SP 800-38D.

const iv = crypto.getRandomValues(new Uint8Array(12)); // nonce de 96 bits
const ciphertext = await crypto.subtle.encrypt(
  { name: "AES-GCM", iv },
  key,
  plaintext
);

O output inclui uma etiqueta de autenticação GCM de 128 bits anexada ao texto cifrado. A decifração verifica automaticamente a etiqueta e lança um erro se não coincidir. Nunca reutilize um nonce com a mesma chave; as propriedades de segurança do GCM colapsam catastroficamente com a reutilização de nonce (os atacantes conseguem recuperar a chave de autenticação). Para transferência de ficheiros onde cada ficheiro recebe uma chave nova, os nonces aleatórios são seguros; para chaves de longa duração, use um contador.

PBKDF2 para chaves derivadas de palavras-passe

Quando os utilizadores escrevem uma palavra-passe para proteger um ficheiro, não pode usar a palavra-passe diretamente como chave AES. Execute-a primeiro através do PBKDF2:

const passwordKey = await crypto.subtle.importKey(
  "raw",
  new TextEncoder().encode(password),
  "PBKDF2",
  false,
  ["deriveKey"]
);

const aesKey = await crypto.subtle.deriveKey(
  {
    name: "PBKDF2",
    salt: crypto.getRandomValues(new Uint8Array(16)),
    iterations: 600000,
    hash: "SHA-256",
  },
  passwordKey,
  { name: "AES-GCM", length: 256 },
  false,
  ["encrypt", "decrypt"]
);

As orientações de hashing de palavras-passe OWASP de 2023 recomendam 600 000 iterações para PBKDF2-SHA-256. Qualquer valor abaixo de 310 000 está abaixo das boas práticas atuais. O salt tem de ser aleatório e armazenado junto ao texto cifrado (não é segredo, apenas tem de ser único).

Para código novo em 2026, considere o Argon2id em vez do PBKDF2. O Argon2 ainda não está na Web Crypto API, mas bibliotecas como argon2-browser ou @noble/hashes fornecem implementações JavaScript/WASM. O Argon2id resiste muito melhor a ataques GPU do que o PBKDF2.

RSA-OAEP para encapsulamento de chaves

Para cenários em que pretende encriptar a chave AES de um ficheiro com a chave pública de um destinatário, use RSA-OAEP. Gere chaves:

const keyPair = await crypto.subtle.generateKey(
  {
    name: "RSA-OAEP",
    modulusLength: 4096,
    publicExponent: new Uint8Array([1, 0, 1]), // 65537
    hash: "SHA-256",
  },
  true,
  ["encrypt", "decrypt"]
);

Use modulusLength 4096 para chaves novas; 2048 é aceitável mas começará a ser descontinuado à medida que os prazos quânticos se concretizem. O RSA-OAEP encripta apenas payloads pequenos (no máximo modulusLength/8 - 2*hashLength - 2 bytes), pelo que encapsule uma chave AES de 256 bits em vez de encriptar o conteúdo do ficheiro diretamente.

Para aplicações sensíveis ao desempenho, o ECDH com P-256 ou P-384 é uma alternativa melhor ao RSA. A geração de chaves é uma ordem de grandeza mais rápida e os tamanhos das chaves são muito menores.

Streaming para ficheiros grandes

Um ficheiro de 2 GB não cabe confortavelmente num ArrayBuffer do browser. O Chrome, Firefox e Safari permitem todos ler Ficheiros com File.stream() retornando um ReadableStream, processando em fragmentos. A própria Web Crypto API ainda não tem métodos de encriptação/decifração em streaming (isso é uma lacuna na especificação), pelo que existem duas alternativas:

  1. Dividir em fragmentos (64 KB ou 1 MB) e encriptar cada um com um nonce único. O destinatário concatena por ordem. Isto perde a verdadeira AEAD sobre o ficheiro completo mas funciona para a maioria dos casos.
  2. Usar uma biblioteca criptográfica WASM (libsodium.js, @noble/ciphers com backend WASM) que suporta modos AEAD em streaming como XChaCha20-Poly1305 ou AES-GCM-SIV.

Para transferências abaixo de algumas centenas de megabytes, o AES-GCM em buffer funciona bem e é muito mais simples. Acima disso, o streaming torna-se necessário para evitar pressão de memória.

Exportação, importação de chaves e fragmentos de URL

Para fluxos ao estilo HexaTransfer onde a chave viaja no fragmento do URL:

const rawKey = await crypto.subtle.exportKey("raw", aesKey);
const keyBase64 = btoa(String.fromCharCode(...new Uint8Array(rawKey)));
// URL de partilha como https://example.com/f/abc123#key=keyBase64

Os fragmentos de URL nunca são enviados aos servidores nos pedidos HTTP (o browser remove-os). Isto mantém a chave do lado do cliente mesmo que o utilizador partilhe uma ligação. No lado do destinatário:

const keyBase64 = window.location.hash.slice(5); // remover "#key="
const rawKey = Uint8Array.from(atob(keyBase64), c => c.charCodeAt(0));
const key = await crypto.subtle.importKey(
  "raw", rawKey, "AES-GCM", false, ["decrypt"]
);

Use codificação base64url (substituir + por -, / por _, remover o preenchimento) para evitar problemas de codificação de URL.

Erros comuns

Usar Math.random() para salts ou IVs. Math.random() não é criptograficamente seguro. Use sempre crypto.getRandomValues().

Reutilizar IVs com a mesma chave. As propriedades de segurança do GCM colapsam completamente com a reutilização de nonce. Os nonces aleatórios de 96 bits colidem após cerca de 2^48 encriptações sob a mesma chave (limite de aniversário). Para transferência de ficheiros onde cada ficheiro tem a sua própria chave, é seguro; para chaves de longa duração, use um contador.

Esquecer HTTPS. crypto.subtle só está disponível em contextos seguros (HTTPS ou localhost). Numa origem insegura, crypto.subtle é undefined.

Armazenar chaves extraíveis no IndexedDB sem proteção. Se precisar de persistir chaves, encapsule-as (por exemplo, com uma chave derivada de uma frase-passe) antes de armazenar. Nunca armazene chaves AES brutas em localStorage, que é acessível a qualquer script na origem.

Confiar em palavras-passe fornecidas pelo utilizador sem PBKDF2. Uma palavra-passe bruta convertida em bytes UTF-8 não é uma chave de 256 bits. Derive sempre.

Não verificar as etiquetas de autenticação. crypto.subtle.decrypt() faz isto automaticamente para AES-GCM, mas se implementar protocolos personalizados por cima, não ignore a verificação.

Nuances de suporte nos browsers

Todos os browsers principais suportam Web Crypto em HTTPS. Algumas particularidades:

  • O PBKDF2 do Safari era mais lento do que o Chrome/Firefox durante anos; a diferença foi eliminada no Safari 15.
  • O Firefox impõe validação de input mais rigorosa; código que corre no Chrome pode lançar OperationError no Firefox. Teste em ambos.
  • A Web Crypto em service workers funciona mas exige que o âmbito de registo seja HTTPS.
  • O Node.js fornece require("crypto").webcrypto com uma API compatível desde o Node 15, útil para código criptográfico isomórfico.

Quando usar uma biblioteca em vez da API nativa

A Web Crypto cobre bem os fundamentos mas carece de primitivos modernos como ChaCha20-Poly1305, Argon2, X25519 e Ed25519 (embora o Ed25519 esteja a chegar). Para isso, o libsodium.js (via WASM) ou @noble/ciphers / @noble/curves (JavaScript puro, auditado) são as opções de referência. O HexaTransfer usa os primitivos da Web Crypto diretamente para AES-GCM e PBKDF2 visto que estes cobrem o caminho de transferência de ficheiros sem dependências.

Para um fluxo de transferência encriptada completo, a Web Crypto consegue-o em menos de 100 linhas de código: gerar chave AES, derivar ou aleatória, encriptar ficheiro, carregar texto cifrado, partilhar ligação com chave no fragmento, o destinatário importa a chave e decifra. É tudo.

Experimente em hexatransfer.com — gratuito, sem conta, até 10 GB.

Envie arquivos grandes com segurança e criptografia de ponta a ponta

Transfira arquivos de até 10 GB gratuitamente com criptografia de ponta a ponta. Sem necessidade de conta. Seus arquivos são criptografados no navegador antes do envio — ninguém mais pode lê-los.

Enviar um arquivo