Ga naar inhoud
HexaTransfer
Terug naar blog
Technische verdiepingen

Bestandsversleuteling in JavaScript: stap-voor-stap tutorial

Versleutel bestanden in de browser met JavaScript. Praktische tutorial over AES-versleuteling, sleutelafleiding en veilige verwerking van bestanden.

JavaScript kan een bestand volledig in de browser versleutelen via de Web Crypto API, zonder enige serverinvolving. De Autoriteit Persoonsgegevens (AP) beoordeelt versleuteling als een "passende technische maatregel" conform AVG artikel 32 — en browsergebaseerde versleuteling gaat verder dan vereist, want de server ziet nooit plaintekst. De standaard pipeline: lees het bestand als een ArrayBuffer, leid een 256-bit AES-GCM-sleutel af uit een wachtwoord via PBKDF2-SHA256 (600.000 iteraties), versleutel met een willekeurige IV van 12 bytes, en pak de salt, IV en ciphertekst in een downloadbare Blob. Elke moderne browser ondersteunt dit native via window.crypto.subtle, en voor bestanden tot 2-3 GB verloopt dit in minder dan tien seconden op een moderne laptop zonder externe bibliotheken.

De algoritme-keuzes die ertoe doen

Kies AES-256-GCM, niet AES-CBC. GCM biedt geauthenticeerde versleuteling in één stap en detecteert manipulatie via een 128-bit tag, terwijl CBC een afzonderlijke HMAC-stap vereist die de meeste tutorials verkeerd implementeren. Gebruik een 256-bit sleutel — het prestatieverschil ten opzichte van 128-bit is verwaarloosbaar op hardware met AES-NI. Kies PBKDF2-SHA256 voor wachtwoordgebaseerde sleutelafleiding, tenzij u Argon2id via WebAssembly kunt leveren — dat is sterker maar voegt 50 KB downloadgewicht toe.

Vermijd: ECB-modus (fundamenteel gebroken), handgemaakte padding (een decennium van CBC-padding-oracle-aanvallen), MD5 of SHA-1 (botsingsbreuk), en het crypto-js-pakket zonder te begrijpen dat het standaard CBC met PKCS7 gebruikt.

Een bestand in het geheugen laden

De File-API biedt drie manieren om bytes op te halen:

const buf = await file.arrayBuffer();          // heel bestand
const stream = file.stream();                  // streaming
const text = await file.text();                // UTF-8 gedecodeerd

Voor bestanden boven 500 MB mislukt arrayBuffer() vaak op mobile Safari. Gebruik liever streaming:

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

Elk stuk wordt lui van schijf gelezen, zodat het piekgeheugengebruik begrensd blijft.

Een sleutel afleiden uit een gebruikerswachtwoord

Geef een ruw wachtwoord nooit door aan encrypt. Leid eerst een sleutel af:

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

Genereer een verse 16-byte salt per bestand met crypto.getRandomValues(new Uint8Array(16)). Sla de salt naast de ciphertekst op — dezelfde salt hergebruiken voor meerdere bestanden maakt PBKDF2 zinloos. OWASP adviseert voor december 2026 600.000 iteraties voor PBKDF2-SHA256, wat neerkomt op circa 500 ms sleutelafleiding op een gemiddelde smartphone.

Het bestand versleutelen

Met een sleutel bij de hand is versleuteling één subtle.encrypt-aanroep per 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 };
}

GCM mislukt catastrofaal als u een (iv, sleutel)-paar hergebruikt — de keystream-botsing lekt beide plainteksten. Een willekeurige IV van 96 bits geeft circa 2^48 veilige versleutelingen met één sleutel, wat prima is voor bestandsversleuteling. Bij het versleutelen van veel stukken met dezelfde sleutel leidt u de IV af van een teller plus een willekeurig 32-bit prefix.

De uitvoer verpakken

De decryptor heeft de salt, IV en ciphertekst nodig. Pak ze in één blob met een kleine header:

function pack(salt, iv, ciphertext) {
  const magic = new TextEncoder().encode('ENC1');
  return new Blob([magic, salt, iv, new Uint8Array(ciphertext)]);
}

Versie de header (ENC1, ENC2…) zodat u later algoritmen kunt migreren zonder oude bestanden te breken. Bied een download aan via:

const url = URL.createObjectURL(packed);
const a = document.createElement('a');
a.href = url; a.download = `${file.name}.enc`; a.click();
URL.revokeObjectURL(url);

Bestanden ontsleutelen

Ontsleuteling keert het proces om en gooit OperationError als het wachtwoord onjuist is of het bestand is gemanipuleerd:

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('Onbekend formaat');
  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]);
}

Geef één foutmelding — "kon niet ontsleutelen, verkeerd wachtwoord of beschadigd bestand" — in plaats van onderscheid te maken tussen tagfout en structuurfout. Dat verwijdert het informatielek richting aanvallers.

Grote bestanden versleutelen zonder geheugen te verspillen

Voor alles boven 500 MB roept u arrayBuffer() niet aan op het hele bestand. Versleutel stukken afzonderlijk met unieke IV's afgeleid van een teller:

function ivForChunk(baseIV, index) {
  const iv = new Uint8Array(baseIV);
  const view = new DataView(iv.buffer);
  view.setUint32(8, index, false);
  return iv;
}

Leid het bestand door een TransformStream, versleutel elk stuk van 4 MB en schrijf de resultaten naar een WritableStream die via de File System Access API naar schijf wijst. Het piekgeheugengebruik blijft nabij 10 MB, zelfs voor een bestand van 20 GB. Het gestuktel formaat moet de stukgrootte en het aantal stukken in de header registreren zodat de decryptor correct kan herassembleren.

Veelgemaakte fouten die in productie belanden

Drie patronen duiken op in echte code-reviews van JavaScript-crypto:

Ten eerste: de ruwe sleutel opslaan in localStorage voor het gemak. localStorage is synchroon, origin-gebonden en leesbaar door elke XSS-aanval. Gebruik in plaats daarvan een niet-extraheerbare CryptoKey in IndexedDB.

Ten tweede: Math.random() gebruiken voor IV's of salts. Math.random() is voorspelbaar — gebruik altijd crypto.getRandomValues.

Ten derde: ervan uitgaan dat subtle.encrypt constant-time is. Dat is het in native browserimplementaties, maar elke JavaScript-wrapper eromheen is dat bijna zeker niet. Houd uw eigen code buiten het kritieke pad.

HexaTransfer past precies deze pipeline toe — PBKDF2 bij 600.000 iteraties, AES-256-GCM, gestreamde stukken — zodat de server uitsluitend ondoorzichtige ciphertekst opslaat. Probeer het op https://hexatransfer.com — gratis, geen account vereist, maximaal 10 GB.

De implementatie testen

Schrijf een testharnas dat een willekeurige blob van 10 MB round-tript, één byte muteert en assereert dat ontsleuteling een fout gooit. Voeg fuzzing toe voor misvormde headers en afgeknipte cipherteksten — de meest voorkomende bug zijn ontbrekende grenscontroles op slice() die crashen in plaats van afwijzen. Benchmark op een iPhone SE, een gemiddelde Android-telefoon en een Chromebook. Alles wat meer dan twee seconden sleutelafleiding kost, is te traag voor mobiele gebruikers. Laat de implementatie door een tweede paar ogen controleren voordat u live gaat — cryptocode ziet er eenvoudig uit en breekt op subtiele manieren die tests missen.

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