Construa an Criptografado Compartilhamento de arquivos App from Scratch
Step-by-step tutorial to build an encrypted arquivo sharing application. Frontend encryption, secure backend, e deployment walkthrough.
Construir uma aplicação de partilha de ficheiros encriptada significa colocar a cripto no browser, não no servidor. Uma stack mínima tem este aspeto: um frontend React alimentado por Vite que encripta com AES-256-GCM via Web Crypto API, um backend Fastify que trata cada upload como um blob opaco, armazenamento compatível com S3 (Cloudflare R2 ou Backblaze B2), e um URL de partilha curto onde a chave de desencriptação vive após o # para nunca atingir o servidor. Pode lançar uma versão funcional em cerca de 400 linhas de código e hospedá-la por menos de 5 euros por mês.
Decisões de Arquitetura que Realmente Importam
A decisão mais importante é onde vive a chave de encriptação. Se alguma vez tocar no servidor, não tem encriptação ponta-a-ponta — tem encriptação do lado do servidor com passos extra. O padrão correto: gere uma chave aleatória de 256 bits no browser, encripte o ficheiro com ela, faça upload do ciphertext, e coloque a chave no fragmento do URL (https://suaapp.com/f/abc123#k=base64key). Os browsers nunca enviam fragmentos em pedidos HTTP, pelo que a chave fica do lado do cliente.
Uma segunda decisão: uploads em chunks. Para ficheiros acima de 100 MB precisa de uploads multipart retomáveis, ou qualquer Wi-Fi instável mata a transferência. A API multipart do S3 suporta chunks mínimos de 5 MB e até 10.000 partes, dando-lhe um teto de 50 GB. Planeie para isso desde o início.
Configuração do Esqueleto do Projeto
Comece com dois pacotes: um frontend Vite + React e um backend Fastify. O frontend trata de toda a cripto, o backend trata do armazenamento e metadados. Use TypeScript para ter type safety em ArrayBuffer e CryptoKey.
pnpm create vite@latest frontend -- --template react-ts
pnpm create fastify backend
Adicione estas dependências ao backend: @fastify/multipart, @aws-sdk/client-s3, @aws-sdk/s3-request-presigner, e better-sqlite3 para metadados de partilha. Mantenha o esquema SQLite pequeno: shares(id, object_key, size_bytes, expires_at, download_count, max_downloads). Sem nomes de ficheiros, sem dados de utilizadores, sem IPs.
Escrita do Pipeline de Encriptação do Frontend
Gere uma chave, encripte com AES-256-GCM, e produza um blob ciphertext mais uma chave base64 para o fragmento do URL:
async function encryptFile(file: File) {
const key = await crypto.subtle.generateKey(
{ name: 'AES-GCM', length: 256 }, true, ['encrypt', 'decrypt']
);
const iv = crypto.getRandomValues(new Uint8Array(12));
const plaintext = await file.arrayBuffer();
const ciphertext = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv }, key, plaintext
);
const rawKey = await crypto.subtle.exportKey('raw', key);
const blob = new Blob([iv, new Uint8Array(ciphertext)]);
return { blob, keyBase64: toBase64(rawKey) };
}
Para ficheiros acima de 50 MB, substitua isto por uma versão de streaming que encripta chunks de 4 MB e os anexa a um ReadableStream. A heap do Safari móvel bloqueia em qualquer coisa maior do que cerca de 400 MB num único ArrayBuffer.
Design do Endpoint de Upload
O backend não deve saber nada útil. Aceite um POST com o ciphertext, gere um ID aleatório de 16 caracteres seguro para URL, armazene-o no SQLite com expiração, e transmita o corpo diretamente para o S3:
fastify.post('/upload', async (req, reply) => {
const id = nanoid(16);
const key = `blobs/${id}`;
const upload = new Upload({
client: s3,
params: { Bucket: 'hexa-transfers', Key: key, Body: req.raw }
});
await upload.done();
db.prepare('INSERT INTO shares VALUES (?, ?, ?, ?, 0, ?)').run(
id, key, req.headers['content-length'], Date.now() + 7*86400*1000, 10
);
return { id };
});
Expiração padrão em 7 dias, máximo de 10 downloads. Quer defaults agressivos porque a alternativa é crescimento ilimitado de armazenamento. Use um cron para eliminar blobs expirados todas as noites.
Geração de Links de Partilha com Chaves no Fragmento
Assim que o upload termina, construa o URL de partilha do lado do cliente:
const { id } = await uploadResponse.json();
const shareUrl = `${location.origin}/f/${id}#k=${keyBase64}`;
Esse fragmento nunca sai do browser do utilizador. Quando um destinatário clica no link, a aplicação React lê window.location.hash, analisa a chave, busca o ciphertext, e desencripta localmente. O servidor não tem caminho para o plaintext além de enviar JavaScript malicioso ao utilizador — razão exata pela qual deve enviar uma Content Security Policy rigorosa e fixar hashes de subrecursos.
Implementação do Fluxo de Download
Na página de download, puxe o ciphertext como stream e desencripte alinhado por chunks:
const keyRaw = base64ToBytes(location.hash.slice(3));
const key = await crypto.subtle.importKey(
'raw', keyRaw, { name: 'AES-GCM' }, false, ['decrypt']
);
const resp = await fetch(`/blob/${id}`);
const iv = new Uint8Array(await resp.body.getReader().read().then(r => r.value.slice(0, 12)));
const ct = await resp.arrayBuffer();
const pt = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, key, ct.slice(12));
const url = URL.createObjectURL(new Blob([pt]));
Para ficheiros grandes, use StreamSaver.js ou a File System Access API para que os bytes desencriptados atinjam o disco diretamente em vez da RAM.
Deployment de Tudo
Frontend: envie para Cloudflare Pages ou Vercel, ambos os tiers gratuitos tratam disto. Backend: um único droplet DigitalOcean de $5 a correr Node 22 por detrás do Caddy (TLS 1.3 automático). Armazenamento: o Cloudflare R2 dá-lhe zero custos de egresso, que é a funcionalidade diferenciadora para partilha de ficheiros à escala. WeTransfer, Smash e SwissTransfer gastam uma fortuna em egresso AWS; o R2 muda completamente a economia.
Defina cabeçalhos rigorosos via Caddy: Strict-Transport-Security, Content-Security-Policy: default-src 'self'; script-src 'self', X-Content-Type-Options: nosniff, e Referrer-Policy: no-referrer. Estes fecham os vetores comuns para fuga do fragmento do URL via referrers ou scripts injetados.
Tornar a Aplicação Compatível com o RGPD por Padrão
Porque o servidor apenas vê ciphertext, tem quase nada que se qualifique como dados pessoais nos termos do Artigo 4 do RGPD. Ainda assim, documente as suas relações com processadores (R2 e DigitalOcean), defina uma retenção máxima de blob de 30 dias, e publique um aviso claro de que os links de partilha contêm chaves e devem ser transmitidos por canais em que o remetente confia. Adicione limitação de taxa de 50 uploads por IP por hora para desencorajar abusos sem registar identidades de utilizadores. A CNPD (Comissão Nacional de Proteção de Dados) pode verificar a conformidade com o RGPD em qualquer serviço que processe dados de residentes portugueses.
O que Adicionar Quando o MVP Funciona
Uma vez que o fluxo básico é lançado, as adições de maior valor são: proteção por senha por cima da chave do fragmento (PBKDF2 com 600.000 iterações), notificações de email por download via uma caixa de correio temporária, e denúncia de abuso que permite que o hash do ciphertext seja sinalizado sem revelar o conteúdo. Evite adicionar contas a não ser que tenha uma razão específica — transformam a sua aplicação de uma ferramenta de privacidade numa responsabilidade de dados de um dia para o outro. O HexaTransfer é construído essencialmente nesta arquitetura — Web Crypto no browser, armazenamento de blob opaco, chaves em fragmentos de URL, zero plaintext nos servidores.
Experimente em https://hexatransfer.com — gratuito, sem conta, máximo de 10 GB.
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