Web Crypto API: Tutorial Completo de Encriptação
Domine a Web Crypto API para encriptação de ficheiros no browser. AES-GCM, RSA-OAEP e gestão de chaves em aplicações JavaScript.
A Web Crypto API permite encriptar ficheiros diretamente no browser usando métodos nativos window.crypto.subtle, sem necessidade de bibliotecas externas. Para encriptação de ficheiros, tipicamente deriva-se uma chave de uma senha via PBKDF2 (210.000 iterações, SHA-256), depois encriptam-se os bytes do ficheiro com AES-GCM usando um IV de 96 bits e uma auth tag de 128 bits. Os fluxos de trabalho com chave pública usam RSA-OAEP com chaves de 4096 bits para envolver a chave simétrica. A API está disponível sobre HTTPS em todos os browsers modernos e corre dentro do backend de cripto nativo em vez de JavaScript.
Por que o SubtleCrypto Supera as Bibliotecas Pure-JS
O window.crypto.subtle chama o backend de cripto nativo auditado do browser — geralmente BoringSSL no Chromium ou CommonCrypto no Safari. Em comparação com opções pure-JS como CryptoJS ou sjcl, o SubtleCrypto corre 30 a 80 vezes mais rápido para AES-GCM, evita timing side-channels nos interpretadores JavaScript, e não envia zero bytes para os utilizadores. O trade-off é uma API baseada em Promise que só opera em objetos ArrayBuffer e CryptoKey, pelo que se passa muito tempo a converter entre Uint8Array, Blob e ReadableStream. Para tamanhos de ficheiro acima de 100 MB, essa infraestrutura importa mais do que a velocidade bruta de cripto.
Derivação de Chave a partir de uma Senha com PBKDF2
Nunca use uma senha diretamente como chave AES. Em vez disso, importe a senha como material bruto, depois derive uma chave de 256 bits:
async function deriveKey(password, salt) {
const enc = new TextEncoder();
const material = await crypto.subtle.importKey(
'raw', enc.encode(password), 'PBKDF2', false, ['deriveKey']
);
return crypto.subtle.deriveKey(
{ name: 'PBKDF2', salt, iterations: 210000, hash: 'SHA-256' },
material,
{ name: 'AES-GCM', length: 256 },
false,
['encrypt', 'decrypt']
);
}
A orientação da OWASP para 2026 recomenda pelo menos 600.000 iterações com SHA-256, embora 210.000 permaneça aceitável para contextos de baixo risco. Gere um salt fresco de 16 bytes por ficheiro com crypto.getRandomValues e armazene-o junto ao ciphertext. O Argon2id seria mais forte mas ainda não está exposto pelo SubtleCrypto.
Encriptação de um Ficheiro com AES-GCM
O AES-GCM fornece confidencialidade e autenticidade numa única passagem. A regra crítica é nunca reutilizar um par (chave, IV):
async function encryptFile(file, key) {
const iv = crypto.getRandomValues(new Uint8Array(12));
const plaintext = await file.arrayBuffer();
const ciphertext = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv, tagLength: 128 },
key,
plaintext
);
return { iv, ciphertext };
}
Para um ficheiro de 2 GB, file.arrayBuffer() vai alocar o buffer completo, o que frequentemente faz o Safari móvel falhar. Divida o ficheiro em chunks de 4 MB, encripte cada um com um IV único derivado de um contador concatenado com um prefixo aleatório, e acrescente um byte de versão e salt para que o desencriptador saiba com o que está a lidar.
Streaming de Ficheiros Grandes através de TransformStream
Para evitar o aumento de memória, envolva a encriptação num TransformStream e passe o ficheiro por ele:
const chunkSize = 4 * 1024 * 1024;
const encryptor = new TransformStream({
async transform(chunk, controller) {
const iv = nextIV(counter++);
const ct = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, chunk);
controller.enqueue(new Uint8Array([...iv, ...new Uint8Array(ct)]));
}
});
await file.stream()
.pipeThrough(sliceByChunks(chunkSize))
.pipeThrough(encryptor)
.pipeTo(uploadSink);
file.stream() devolve um ReadableStream<Uint8Array> que lê do disco de forma lazy. O slicer produz chunks de tamanho fixo para que as tags GCM se alinhem previsivelmente. A memória de pico fica abaixo de 20 MB mesmo para um upload de 10 GB.
Envolvimento da Chave Simétrica com RSA-OAEP
Quando precisa de partilhar um ficheiro com um destinatário específico, gere o par de chaves RSA uma vez e publique a chave pública:
const keypair = await crypto.subtle.generateKey(
{ name: 'RSA-OAEP', modulusLength: 4096,
publicExponent: new Uint8Array([1,0,1]), hash: 'SHA-256' },
true, ['wrapKey', 'unwrapKey']
);
Gere uma chave AES-GCM para o ficheiro, depois envolva-a:
const wrapped = await crypto.subtle.wrapKey(
'raw', fileKey, keypair.publicKey,
{ name: 'RSA-OAEP' }
);
As chaves RSA de 4096 bits fornecem aproximadamente 150 bits de segurança até 2030 segundo o NIST SP 800-57. Se precisar de forward secrecy ou resistência pós-quântica, combine RSA-OAEP com ECDH sobre P-384 ou migre para ML-KEM (Kyber) quando o grupo de trabalho WebCrypto o implementar.
Armazenamento Seguro de Chaves no IndexedDB
Os objetos CryptoKey são não extraíveis por padrão, o que significa que pode persistê-los no IndexedDB sem nunca expor os bytes brutos ao JavaScript:
const db = await openDB('keystore', 1);
await db.put('keys', keypair.privateKey, 'user-signing-key');
Os browsers serializam a chave usando o algoritmo de clone estruturado e mantêm os bytes reais no backend de cripto. Um script comprometido pode chamar encrypt ou decrypt com a chave armazenada, mas não consegue ler o seu material. Este é um passo de reforço significativo em comparação com guardar chaves base64 no localStorage.
Tratamento de Erros que a API Lança
O SubtleCrypto lança OperationError para falhas de desencriptação autenticada, o que geralmente significa que o ciphertext foi adulterado, o IV está errado, ou o utilizador escreveu a senha errada. Lança DataError quando o buffer de entrada tem o comprimento errado, NotSupportedError quando o algoritmo não está implementado, e InvalidAccessError quando a chave não foi importada com os sinalizadores de uso corretos. Envolva sempre a desencriptação em try/catch, apresente uma mensagem neutra "o ficheiro não pôde ser desencriptado", e evite revelar se a tag ou a estrutura falhou.
Problemas Reais a Ter em Conta
O Firefox no Android limita as iterações de deriveKey a cerca de 1 milhão antes de a thread de UI bloquear durante vários segundos, pelo que execute a derivação de chaves dentro de um Worker dedicado. O Safari abaixo da versão 16.4 não suporta crypto.subtle.verify com padding PSS. O Chrome limita as chamadas de getRandomValues acima de 64 KB por invocação, pelo que faça um ciclo se precisar de mais entropia. E as transferências ArrayBuffer através de postMessage são zero-copy mas desanexam o original, o que apanha as pessoas desprevenidas.
O HexaTransfer usa exatamente este pipeline de AES-GCM mais PBKDF2 para cada upload, com chaves derivadas num Worker e ciphertext transmitido para o armazenamento sem que o servidor alguma vez veja o plaintext. Experimente em https://hexatransfer.com — gratuito, sem conta, máximo de 10 GB.
Juntando Tudo
Um fluxo mínimo de upload encriptado: gere salt e IV com getRandomValues, derive uma chave AES-GCM a partir da senha do utilizador via PBKDF2, transmita o ficheiro através de um TransformStream que encripta cada chunk de 4 MB, acrescente um cabeçalho pequeno contendo versão, salt e contagem de chunks, e faça POST do resultado para o seu servidor. No download, inverta o processo chunk por chunk, apanhando OperationError como sinal de senha errada ou corrupção. A Web Crypto API fornece tudo o que precisa, e a implementação nativa do browser superará qualquer alternativa JavaScript por uma ordem de magnitude.
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