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

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