A Web Crypto API oferece primitivas criptográficas poderosas. O problema é que ela não traz uma interface para streams: encrypt() e decrypt() operam sobre um ArrayBuffer único, não sobre um ReadableStream.
Se você quiser criptografar arquivos grandes ou uploads em chunks sem colocar tudo na memória, teria de escrever sua própria lógica de framing e divisão em registros. Este artigo mostra como pular essa parte usando o subcaminho encryption de @hsblabs/web-stream-extras.
npm install @hsblabs/web-stream-extrasFunciona em Node.js ≥22 ou em navegadores modernos com suporte a Web Streams + Web Crypto.
Criptografar e descriptografar um stream
encryptStream e decryptStream recebem e devolvem ReadableStream<Uint8Array>, então entram em um pipeline existente sem adaptadores.
import { readableFromChunks, readAllBytes, stringToBinary, binaryToString,} from "@hsblabs/web-stream-extras";import { encryptStream, decryptStream,} from "@hsblabs/web-stream-extras/encryption";
const key = crypto.getRandomValues(new Uint8Array(32));
const plaintext = readableFromChunks([stringToBinary("secret payload")]);const encrypted = encryptStream(key, plaintext);const decrypted = decryptStream(key, encrypted);
console.log(binaryToString(await readAllBytes(decrypted))); // "secret payload"O resultado criptografado continua sendo um ReadableStream<Uint8Array> padrão. Você pode ligá-lo ao body de um fetch, gravá-lo com a File System Access API ou passá-lo a qualquer consumidor de stream.
Criptografar um arquivo escolhido pelo usuário
File.stream() já retorna um ReadableStream<Uint8Array>, então não há conversão necessária:
async function encryptFile(file: File, key: Uint8Array): Promise<Uint8Array> { const plaintext = file.stream() as ReadableStream<Uint8Array>; const encrypted = encryptStream(key, plaintext); return readAllBytes(encrypted);}O resultado é apenas uma sequência de bytes — pronta para ser salva, enviada ou repassada para outro stream.
Gerenciar chaves por stream com webCryptoStream
Em aplicações reais, normalmente surgem necessidades como:
- usar uma chave de criptografia diferente por arquivo
- armazenar essa chave junto com o registro do arquivo
- proteger cada chave de arquivo com uma chave mestra para que o material bruto nunca fique em texto puro
webCryptoStream encapsula esse padrão. Ele recebe uma CryptoKey AES-GCM não extraível e devolve um helper que cria e gerencia chaves por stream como strings base64url.
import { webCryptoStream } from "@hsblabs/web-stream-extras/encryption";
const masterKey = await crypto.subtle.generateKey( { name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"],);
const crypto$ = webCryptoStream(masterKey);Criptografar um arquivo:
// streamKey é uma string base64url — salve junto com o registro do arquivoconst streamKey = await crypto$.createStreamKey();
const plaintext = file.stream() as ReadableStream<Uint8Array>;const encrypted = await crypto$.encrypt(streamKey, plaintext);const encryptedBytes = await readAllBytes(encrypted);
// Armazenar: { encryptedBytes, streamKey }Descriptografar depois:
const encrypted = readableFromChunks([encryptedBytes]);const decrypted = await crypto$.decrypt(streamKey, encrypted);const result = await readAllBytes(decrypted);A string streamKey é ela mesma criptografada com a chave mestra via AES-GCM. Isso permite armazená-la em banco de dados sem expor material bruto de chave em texto puro.
O que a biblioteca faz e o que ela não faz
O que ela cuida internamente:
- dividir o stream em claro em registros de tamanho fixo
- derivar uma chave e um nonce únicos por registro com HKDF
- autenticar cada registro com AES-GCM e detectar adulteração na descriptografia
O que ela não cobre — e continua sendo responsabilidade da aplicação:
- armazenamento e persistência de chaves
- derivação de chave baseada em senha (PBKDF2 / Argon2 precisam entrar por conta própria)
- autenticação de usuários e controle de acesso
A biblioteca permanece na camada de transformação de streams.
Compondo com outros transforms
Como entrada e saída são ReadableStream<Uint8Array>, a composição é direta:
// Comprimir antes de criptografarconst compressed = plaintext.pipeThrough(new CompressionStream("gzip"));const encrypted = encryptStream(key, compressed);// Enviar o stream criptografado diretamenteawait fetch("/upload", { method: "POST", body: encrypted, duplex: "half",});
hsb.horse