Encriptação de Ficheiros em JavaScript: Tutorial Passo-a-Passo
Encripte ficheiros no browser com JavaScript. Tutorial prático sobre encriptação AES, derivação de chaves e manipulação segura de ficheiros.
O JavaScript pode encriptar um ficheiro inteiramente no browser usando a Web Crypto API, sem qualquer envolvimento do servidor. O pipeline padrão: ler o ficheiro como ArrayBuffer, derivar uma chave AES-GCM de 256 bits a partir de uma palavra-passe via PBKDF2-SHA256 (600.000 iterações), encriptar com um IV aleatório de 12 bytes, e empacotar o salt, o IV e o texto cifrado num Blob para transferência. Todos os principais browsers suportam isto nativamente através de window.crypto.subtle, e para ficheiros até 2-3 GB o processo corre em menos de dez segundos num portátil moderno sem tocar em qualquer biblioteca de terceiros.
As Escolhas de Algoritmo que Importam
Escolha AES-GCM, não AES-CBC. O GCM proporciona encriptação autenticada numa só passagem, detetando adulterações com uma etiqueta de 128 bits, enquanto o CBC precisa de um passo HMAC separado que a maioria dos tutoriais implementa incorretamente. Use uma chave de 256 bits — a diferença de desempenho em relação aos 128 bits é negligenciável em hardware com AES-NI. Escolha PBKDF2-SHA256 para derivação de chaves baseada em palavras-passe, a menos que consiga incluir Argon2id via WebAssembly, que é mais forte mas acrescenta 50 KB de peso de transferência.
Evite: o modo ECB (fundamentalmente quebrado), padding implementado manualmente (uma década de ataques de padding oracle em CBC), MD5 ou SHA-1 (com colisões conhecidas), e qualquer coisa do pacote crypto-js sem perceber que usa CBC com PKCS7 por defeito.
Ler um Ficheiro para a Memória
A File API disponibiliza três formas de obter os bytes:
const buf = await file.arrayBuffer(); // ficheiro inteiro
const stream = file.stream(); // em streaming
const text = await file.text(); // decodificado como UTF-8
Para ficheiros acima de 500 MB, arrayBuffer() frequentemente falha no Safari para iOS. Use streaming em alternativa:
async function* chunks(file, size = 4 * 1024 * 1024) {
for (let off = 0; off < file.size; off += size) {
yield new Uint8Array(await file.slice(off, off + size).arrayBuffer());
}
}
Cada fatia é lida de forma preguiçosa a partir do disco, pelo que o pico de memória se mantém limitado.
Derivar uma Chave a Partir de uma Palavra-passe
Nunca passe uma palavra-passe em bruto para encrypt. Derive primeiro uma chave:
async function deriveKey(password, salt) {
const enc = new TextEncoder();
const material = await crypto.subtle.importKey(
'raw', enc.encode(password), { name: 'PBKDF2' }, false, ['deriveKey']
);
return crypto.subtle.deriveKey(
{ name: 'PBKDF2', salt, iterations: 600000, hash: 'SHA-256' },
material,
{ name: 'AES-GCM', length: 256 },
false,
['encrypt', 'decrypt']
);
}
Gere um salt fresco de 16 bytes por ficheiro com crypto.getRandomValues(new Uint8Array(16)). Armazene o salt juntamente com o texto cifrado — reutilizar um salt entre ficheiros anula o propósito do PBKDF2. A folha de referência de armazenamento de palavras-passe da OWASP de dezembro de 2026 recomenda atualmente 600.000 iterações para PBKDF2-SHA256, o que se traduz em cerca de 500 ms de derivação de chave num telemóvel de gama média.
Encriptar o Ficheiro
Com uma chave disponível, a encriptação é uma chamada subtle.encrypt por buffer:
async function encryptBuffer(key, plaintext) {
const iv = crypto.getRandomValues(new Uint8Array(12));
const ciphertext = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv, tagLength: 128 },
key,
plaintext
);
return { iv, ciphertext };
}
O GCM falha catastroficamente se reutilizar um par (iv, chave) — a colisão de keystream expõe ambos os textos simples. Um IV aleatório de 96 bits proporciona cerca de 2^48 encriptações seguras com uma chave, o que é suficiente para a encriptação de ficheiros. Se estiver a encriptar muitos fragmentos com a mesma chave, derive o IV a partir de um contador mais um prefixo aleatório de 32 bits.
Empacotar a Saída
O desencriptador precisa do salt, do IV e do texto cifrado. Empacote-os num único blob com um cabeçalho pequeno:
function pack(salt, iv, ciphertext) {
const magic = new TextEncoder().encode('ENC1');
return new Blob([magic, salt, iv, new Uint8Array(ciphertext)]);
}
Versione o cabeçalho (ENC1, ENC2…) para poder migrar algoritmos mais tarde sem quebrar ficheiros antigos. Ofereça uma transferência via:
const url = URL.createObjectURL(packed);
const a = document.createElement('a');
a.href = url; a.download = `${file.name}.enc`; a.click();
URL.revokeObjectURL(url);
Desencriptar Ficheiros
A desencriptação inverte o processo e lança OperationError se a palavra-passe estiver errada ou o ficheiro tiver sido adulterado:
async function decryptFile(blob, password) {
const buf = await blob.arrayBuffer();
const view = new Uint8Array(buf);
const magic = new TextDecoder().decode(view.slice(0, 4));
if (magic !== 'ENC1') throw new Error('Formato desconhecido');
const salt = view.slice(4, 20);
const iv = view.slice(20, 32);
const ct = view.slice(32);
const key = await deriveKey(password, salt);
const pt = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, key, ct);
return new Blob([pt]);
}
Apresente uma única mensagem de erro — "não foi possível desencriptar, palavra-passe errada ou ficheiro corrompido" — em vez de distinguir entre falha de etiqueta e falha estrutural. Isso elimina a fuga de informação para atacantes semelhante ao padding oracle.
Encriptar Ficheiros Grandes Sem Esgotar a RAM
Para qualquer coisa acima de 500 MB, não chame arrayBuffer() em todo o ficheiro. Encripte fragmentos de forma independente com IVs únicos derivados de um contador:
function ivForChunk(baseIV, index) {
const iv = new Uint8Array(baseIV);
const view = new DataView(iv.buffer);
view.setUint32(8, index, false);
return iv;
}
Passe o ficheiro por um TransformStream, encripte cada fragmento de 4 MB e escreva os resultados para um WritableStream apontado para o disco via File System Access API. O pico de memória mantém-se perto de 10 MB mesmo para um ficheiro de 20 GB. O formato fragmentado precisa de registar o tamanho e a contagem de fragmentos no cabeçalho para que o desencriptador possa remontar corretamente.
Erros Comuns que Chegam a Produção
Três padrões surgem em revisões de código real de criptografia em JavaScript:
Primeiro, armazenar a chave em bruto no localStorage por conveniência. O localStorage é síncrono, com âmbito de origem e legível por qualquer XSS. Use uma CryptoKey não extraível no IndexedDB em alternativa.
Segundo, usar Math.random() para IVs ou salts. Math.random() é previsível; use sempre crypto.getRandomValues.
Terceiro, assumir que subtle.encrypt é de tempo constante. É nas implementações nativas dos browsers, mas qualquer wrapper JavaScript à volta dele quase certamente não é. Mantenha o seu próprio código fora do caminho crítico.
O HexaTransfer aplica exatamente este pipeline — PBKDF2 com 600k iterações, AES-256-GCM, fragmentos em streaming — para que o servidor armazene apenas texto cifrado opaco. Experimente em hexatransfer.com — gratuito, sem conta necessária, máximo de 10 GB.
Testar a Implementação
Escreva um conjunto de testes que faça uma viagem de ida e volta com um blob aleatório de 10 MB, mute um byte e confirme que a desencriptação lança uma exceção. Acrescente fuzzing contra cabeçalhos mal formados e textos cifrados truncados — o bug comum são verificações de limites em falta no slice() que causam crash em vez de rejeição. Faça benchmarks num iPhone SE, num Android de gama média e num Chromebook; qualquer derivação de chave que demore mais de 2 segundos é demasiado lenta para utilizadores de dispositivos móveis. Faça auditar por um segundo par de olhos antes de ir para produção — o código de criptografia parece simples e quebra de formas subtis que os testes não detetam.
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