Ga naar inhoud
HexaTransfer
Terug naar blog
Technische verdiepingen

Bouw an Versleuteld Bestanden delen App from Scratch

Step-by-step tutorial to build an encrypted bestand sharing application. Frontend encryption, secure backend, en deployment walkthrough.

De Autoriteit Persoonsgegevens (AP) stelt uitdrukkelijk dat persoonsgegevens bij overdracht moeten worden beveiligd. Een versleutelde bestandsdelingsapplicatie waarbij de server uitsluitend ciphertekst verwerkt, voldoet aan die eis per ontwerp — niet via beleid maar via architectuur. De minimale stack: een Vite-aangedreven React-frontend die versleutelt met AES-256-GCM via de Web Crypto API, een Fastify-backend die elke upload als een ondoorzichtige blob behandelt, S3-compatibele opslag (Cloudflare R2 of Backblaze B2), en een korte deel-URL waarbij de ontsleutelingssleutel achter het #-teken staat zodat hij nooit de server bereikt. U kunt een werkende versie bouwen in circa 400 regels code en hosten voor minder dan vijf euro per maand.

Architectuurbeslissingen die ertoe doen

De belangrijkste beslissing is waar de versleutelingssleutel verblijft. Raakt hij ooit uw server, dan heeft u geen end-to-end encryptie maar server-side versleuteling met extra stappen. Het juiste patroon: genereer een willekeurige 256-bit sleutel in de browser, versleutel het bestand ermee, upload de ciphertekst, en plaats de sleutel in het URL-fragment (https://uwapp.nl/f/abc123#k=base64sleutel). Browsers sturen fragmenten nooit mee in HTTP-verzoeken, dus de sleutel blijft aan de clientkant.

Een tweede keuze: chunked uploads. Voor bestanden boven 100 MB hebt u hervattbare multipart-uploads nodig, anders maakt instabiel wifi elke overdracht onbetrouwbaar. De S3-multipart-API ondersteunt chunks van minimaal 5 MB en maximaal 10.000 onderdelen, wat een bovengrens van 50 GB geeft. Plan hier vanaf dag één op.

Het projectskelet opzetten

Begin met twee pakketten: een Vite + React-frontend en een Fastify-backend. De frontend verwerkt alle cryptografie, de backend beheert opslag en metadata. Gebruik TypeScript voor type-veiligheid op ArrayBuffer en CryptoKey.

pnpm create vite@latest frontend -- --template react-ts
pnpm create fastify backend

Voeg aan de backend toe: @fastify/multipart, @aws-sdk/client-s3, @aws-sdk/s3-request-presigner en better-sqlite3 voor deelmetadata. Houd het SQLite-schema minimaal: shares(id, object_key, size_bytes, expires_at, download_count, max_downloads). Geen bestandsnamen, geen gebruikersdata, geen IP-adressen.

De frontend-versleutelingspipeline schrijven

Genereer een sleutel, versleutel met AES-256-GCM en produceer een ciphertext-blob plus een base64-sleutel voor het URL-fragment:

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) };
}

Voor bestanden boven 50 MB vervangt u dit door een streamingversie die stukken van 4 MB versleutelt en ze toevoegt aan een ReadableStream. De geheugenheap van mobile Safari loopt vast bij meer dan circa 400 MB in één ArrayBuffer.

Het upload-eindpunt ontwerpen

De backend hoeft niets nuttigs te weten. Accepteer een POST met de ciphertekst, genereer een willekeurige 16-karakter URL-veilige ID, bewaar die in SQLite met vervaldatum, en stream de body rechtstreeks naar 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 };
});

Standaard vervaldatum zeven dagen, maximaal tien downloads. Gebruik een nachtelijke cron om verlopen blobs te verwijderen.

Deel-URLs genereren met fragmentsleutels

Zodra de upload gereed is, bouwt u de deel-URL aan de clientkant:

const { id } = await uploadResponse.json();
const shareUrl = `${location.origin}/f/${id}#k=${keyBase64}`;

Dat fragment verlaat nooit de browser van de gebruiker. Wanneer een ontvanger op de link klikt, leest uw React-app window.location.hash, parseert de sleutel, haalt de ciphertekst op en ontsleutelt lokaal. De server heeft geen pad naar de plaintekst — tenzij hij kwaadaardige JavaScript aan de gebruiker pusht. Precies daarom moet u een strikte Content Security Policy instellen en subresource-hashes pinnen.

De downloadflow implementeren

Op de downloadpagina haalt u de ciphertekst als stream op en ontsleutelt u stukgewijs:

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]));

Gebruik voor grote bestanden StreamSaver.js of de File System Access API zodat de ontsleutelde bytes rechtstreeks naar schijf gaan in plaats van naar RAM.

De hele applicatie deployen

Frontend: zet online via Cloudflare Pages of Vercel — beide gratis lagen volstaan. Backend: één $5-DigitalOcean-droplet met Node 22 achter Caddy (automatisch TLS 1.3). Opslag: Cloudflare R2 brengt nul egresskosten in rekening, wat de economie van bestandsdelen fundamenteel verandert. Stel strikte headers in via Caddy: Strict-Transport-Security, Content-Security-Policy: default-src 'self'; script-src 'self', X-Content-Type-Options: nosniff en Referrer-Policy: no-referrer. Deze sluiten de meest gangbare lekkanalen voor het URL-fragment af.

De applicatie AVG-vriendelijk maken

Omdat de server uitsluitend ciphertekst ziet, is er nauwelijks sprake van persoonsgegevens in de zin van AVG artikel 4. Documenteer toch uw verwerkersrelaties (R2 en DigitalOcean), stel een maximale bewaartermijn van 30 dagen in voor blobs, en publiceer een duidelijke kennisgeving dat deel-links sleutels bevatten die de verzender via betrouwbare kanalen moet versturen. Beperk uploads via snelheidsbeperking tot 50 per IP per uur om misbruik te ontmoedigen zonder gebruikersidentiteiten te loggen.

HexaTransfer is gebouwd op precies deze architectuur — Web Crypto in de browser, ondoorzichtige blob-opslag, sleutels in URL-fragmenten, nul plaintekst op servers. Probeer het op https://hexatransfer.com — gratis, geen account vereist, maximaal 10 GB.

Wat toe te voegen zodra de MVP werkt

De meest waardevolle uitbreidingen na de eerste release: wachtwoordbeveiliging bovenop de fragmentsleutel (PBKDF2 met 600.000 iteraties), per-download-e-mailnotificaties via een tijdelijk adres, en misbruikrapportage waarmee de hash van de ciphertekst gemarkeerd kan worden zonder de inhoud te onthullen. Voeg geen accounts toe tenzij u daar een specifieke reden voor heeft — ze veranderen uw applicatie van een privacytool in een dataverplichting over één nacht. Houd het oppervlak klein, wees eigenzinnig in uw keuzes en laat de cryptografie het zware werk doen.

Verstuur grote bestanden veilig met end-to-end-versleuteling

Draag bestanden tot 10 GB gratis over met end-to-end-versleuteling. Geen account nodig. Uw bestanden worden in uw browser versleuteld voordat ze worden geüpload — niemand anders kan ze lezen.

Een bestand verzenden