Ir para o conteúdo
HexaTransfer
Voltar ao blog
Analises tecnicas

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