Ga naar inhoud
HexaTransfer
Terug naar blog
Encryptie & beveiliging

AES-GCM implementatiegids: geauthenticeerde encryptie correct

Implementeer AES-GCM correct. Nonce-beheer, sleutelafhandeling en veelvoorkomende valkuilen bij geauthenticeerde encryptie.

AES-GCM (Galois/Counter Mode) combineert AES-CTR versleuteling met GHASH-authenticatie om geauthenticeerde versleuteling met bijbehorende data (AEAD) te produceren. Een correcte implementatie gebruikt een 256-bits sleutel, een 96-bits (12 byte) nonce die uniek is per sleutel (nooit hergebruikt), een 128-bits authenticatietag en optioneel geauthenticeerde bijbehorende data (AAD) die wordt geverifieerd maar niet versleuteld. NIST SP 800-38D specificeert de exacte constructie. Maak een van deze dingen fout — met name nonce-hergebruik — en de beveiliging van GCM implodeert: één enkel herhaald (sleutel, nonce)-paar laat aanvallers de authenticatiesleutel herstellen en willekeurige ciphertexts vervalsen. Deze gids behandelt de correcte manier om AES-GCM te gebruiken in browser-, Node- en servercontexten.

Wat GCM daadwerkelijk garandeert

Twee eigenschappen:

Vertrouwelijkheid: de plaintext kan niet worden hersteld zonder de sleutel. De CTR-modus versleutelingslaag van AES-GCM biedt dit.

Integriteit en authenticiteit: elke wijziging aan ciphertext, nonce of bijbehorende data zorgt ervoor dat ontsleuteling mislukt. GHASH produceert een 128-bits tag die constant-time wordt geverifieerd bij ontsleuteling.

Wat GCM niet garandeert: onweerlegbaarheid (het is symmetrisch, dus iedereen met de sleutel kan geldige ciphertexts produceren), replaybescherming (dat is een zorg op hogere laag) of ordening (voor stromen moet je op een of andere manier ketenen).

Het sleutelinzicht: GCM blijft alleen veilig wanneer nonces uniek zijn per sleutel. Niet grotendeels uniek, niet meestal uniek, daadwerkelijk uniek. Het veiligheidsbewijs valt uiteen bij hergebruik.

Nonce-beheer: de cruciale factor

Een 96-bits nonce kan op twee manieren worden gegenereerd:

Willekeurig: crypto.getRandomValues(new Uint8Array(12)). Met willekeurige 96-bits nonces onder één sleutel verschijnen verjaardag-botsingen rond 2^48 versleutelingen. NIST suggereert een veiligheidsmarge, dus begrens gebruik tot 2^32 per sleutel.

Teller: verhoog een 96-bits geheel getal. Garandeert uniciteit tot 2^96 berichten. Vereist betrouwbare monotone staat, moeilijk in gedistribueerde systemen.

Voor bestandsoverdracht met een nieuwe sleutel per bestand zijn willekeurige nonces volledig veilig — je bereikt nooit 2^32 versleutelingen met één sleutel. Voor gesegmenteerde versleuteling onder één bestandssleutel gebruik je een teller waarbij de nonce de chunk-index codeert:

const nonce = new Uint8Array(12);
new DataView(nonce.buffer).setUint32(0, messageId);
new DataView(nonce.buffer).setBigUint64(4, BigInt(chunkIndex));

Het rampscenario: meerdere processen die versleutelen onder dezelfde gedeelde sleutel met willekeurige nonces, opgeschaald tot miljoenen versleutelingen per seconde. Verjaardag-botsingen worden waarschijnlijk. Als je sleutels over processen moet delen, gebruik een gecoördineerde teller met een proces-ID-prefix.

Gebruik geen 64-bits nonces

AES-GCM ondersteunt variabele nonce-lengtes, maar alleen 96-bits nonces gebruiken de geoptimaliseerde constructie die is gespecificeerd in NIST 800-38D. Andere lengtes (typisch 64 of 128 bits) activeren een GHASH-voorverwerkingsstap die de prestaties vermindert en de complexiteit vergroot. De Web Crypto API accepteert niet-96-bits IV's maar de specificatie beveelt 96 aan. Gebruik gewoon 96.

Taglengte: verkort niet

GCM's tag is tot 128 bits. Sommige specificaties staan afkorting toe tot 96, 64 of zelfs 32 bits. Doe dit niet. Verkorte tags maken vervalsingsaanvallen eenvoudiger, en de besparing (4-12 bytes per bericht) is irrelevant voor bestandsoverdracht. Web Crypto's AES-GCM gebruikt standaard 128-bits tags via de tagLength-parameter (standaard 128). Laat dit met rust.

Bijbehorende data (AAD)

AAD is data die is geauthenticeerd maar niet versleuteld. Gebruik het voor metadata die je gebonden wilt hebben aan de ciphertext: bestandsnaam, inhoudstype, vervaldatumtijdstempel, uploader-ID.

await crypto.subtle.encrypt(
  {
    name: "AES-GCM",
    iv: nonce,
    additionalData: new TextEncoder().encode(JSON.stringify({
      filename: "rapport.pdf",
      contentType: "application/pdf",
      expires: 1712345678,
    })),
  },
  key,
  plaintext
);

Als een aanvaller de AAD wijzigt, mislukt ontsleuteling. Dit voorkomt swap-aanvallen waarbij iemand de bestandsnaam op een opgeslagen ciphertext vervangt zonder detectie. De ontvanger moet de exacte AAD kennen om te ontsleutelen, dus sla het op naast de ciphertext.

Sleutelgeneratie en -afleiding

Voor per-bestand sleutels:

const key = await crypto.subtle.generateKey(
  { name: "AES-GCM", length: 256 },
  true,
  ["encrypt", "decrypt"]
);

256 bits is de standaard in 2026. 128-bits AES is nog steeds veilig maar heeft minder post-kwantummarge (Grover's algoritme halveert de effectieve sterkte).

Voor wachtwoordafgeleide sleutels:

const aesKey = await crypto.subtle.deriveKey(
  {
    name: "PBKDF2",
    salt: crypto.getRandomValues(new Uint8Array(16)),
    iterations: 600000,
    hash: "SHA-256",
  },
  passwordKey,
  { name: "AES-GCM", length: 256 },
  false,
  ["encrypt", "decrypt"]
);

Sla de salt op naast de ciphertext. Niet geheim; het moet gewoon uniek zijn per wachtwoord.

Het kritieke codepad

Een minimale versleutelingsfunctie:

async function encrypt(key, plaintext, aad = new Uint8Array()) {
  const nonce = crypto.getRandomValues(new Uint8Array(12));
  const ciphertext = new Uint8Array(
    await crypto.subtle.encrypt(
      { name: "AES-GCM", iv: nonce, additionalData: aad },
      key,
      plaintext
    )
  );
  return { nonce, ciphertext, aad };
}

Ontsleuteling met correcte foutafhandeling:

async function decrypt(key, { nonce, ciphertext, aad }) {
  try {
    return await crypto.subtle.decrypt(
      { name: "AES-GCM", iv: nonce, additionalData: aad },
      key,
      ciphertext
    );
  } catch (e) {
    // Authenticatiefout
    throw new Error("Ontsleuteling mislukt: ciphertext gemanipuleerd of verkeerde sleutel");
  }
}

De decrypt-aanroep gooit OperationError bij tag-mismatch, te korte ciphertext of verkeerde sleutel. Behandel elke uitzondering als een integriteitsdefect; probeer niet te onderscheiden.

Gesegmenteerde grote bestanden

Voor bestanden van meer dan een paar honderd megabytes segmenteer je ze om geheugendruk te vermijden:

async function encryptChunks(key, file, chunkSize = 1024 * 1024) {
  const chunks = [];
  let chunkIndex = 0;
  for (let offset = 0; offset < file.size; offset += chunkSize) {
    const chunk = await file.slice(offset, offset + chunkSize).arrayBuffer();
    const nonce = new Uint8Array(12);
    new DataView(nonce.buffer).setBigUint64(4, BigInt(chunkIndex++));
    const ct = await crypto.subtle.encrypt(
      { name: "AES-GCM", iv: nonce }, key, chunk
    );
    chunks.push(new Uint8Array(ct));
  }
  return chunks;
}

Voorbehoud: gesegmenteerde AES-GCM detecteert geen afkapping. Een aanvaller kan afsluitende chunks weggooien en elk overgebleven chunk ontsleutelt prima. Ter verdediging voeg je het totale aantal chunks toe in de AAD van elk chunk, of gebruik je libsodium's crypto_secretstream dat dit afhandelt.

Server-side ontsleuteling (Node.js)

Node's crypto-module kan data ontsleutelen die in de browser is versleuteld:

const { createDecipheriv } = require('crypto');

function decrypt(key, nonce, ciphertextWithTag) {
  const tag = ciphertextWithTag.slice(-16);
  const ct = ciphertextWithTag.slice(0, -16);
  const decipher = createDecipheriv('aes-256-gcm', key, nonce);
  decipher.setAuthTag(tag);
  return Buffer.concat([decipher.update(ct), decipher.final()]);
}

Web Crypto voegt de 128-bits tag toe aan de ciphertext; Node's API verwacht tag en ciphertext apart. Splits dienovereenkomstig.

Prestatiecijfers

Op typische hardware uit 2024-2026 met AES-NI:

  • Native (OpenSSL, AES-NI): 3-5 GB/s per kern
  • Web Crypto (browser met hardwareversnelling): 1-2 GB/s
  • libsodium.js WASM AES-GCM: 400-800 MB/s
  • Pure JS (@noble/ciphers): 50-150 MB/s

Voor een bestand van 1 GB duurt Web Crypto versleuteling 0,5-1 seconde. Pure JS duurt 7-20 seconden. Kies implementaties op basis van deze realiteit; voor grote bestandsoverdracht-UX is Web Crypto de praktische keuze.

Samenvatting veelgemaakte fouten

  • Nonce-hergebruik: catastrofaal. Grootste enkelvoudige faalwijze.
  • Math.random() gebruiken in plaats van crypto.getRandomValues().
  • Vergeten bijbehorende metadata te authenticeren met AAD.
  • CBC-modus gebruiken "omdat we er aan gewend zijn." CBC vereist een aparte MAC om de integriteit van GCM te evenaren; een HMAC-CBC-constructie is correct maar complex, GCM vermijdt de valkuil.
  • Stilvangen van ontsleutelingsfouten en garbage teruggeven. Faal altijd luid.
  • Je eigen GCM implementeren. Gebruik Web Crypto, libsodium of node:crypto. De GHASH-implementatie heeft side-channel valkuilen die experts jaren kostten om goed te krijgen.

HexaTransfer gebruikt Web Crypto's AES-256-GCM met 96-bits willekeurige nonces, 128-bits tags en geen AAD omdat de sleutel per bestand is en de bestandsnaam apart wordt opgeslagen in AEAD-beveiligde metadata. Eenvoudig, correct, snel.

Wanneer je iets anders kiest

AES-GCM is optimaal voor bestandsoverdracht, maar overweeg alternatieven in specifieke gevallen:

  • XChaCha20-Poly1305: 192-bits nonces maken willekeurige-nonce veiligheid triviaal op elke schaal. Iets langzamer op hardware met AES-NI, sneller op oudere ARM zonder AES-NI. libsodium biedt het.
  • AES-GCM-SIV: misbruikbestendig; nonce-hergebruik lekt de sleutel niet, onthult alleen of plaintexts gelijk waren. Nuttig wanneer je nonce-uniciteit niet kunt garanderen.

Voor de meeste bestandsoverdrachtsworkloads in een standaard webstack is AES-256-GCM met een verse sleutel per bestand en willekeurige 96-bits nonces de juiste keuze en de eenvoudigste om correct te implementeren.

Probeer het op hexatransfer.com — gratis, zonder account, tot 10 GB.

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