WebCrypto API er W3C-standarden for kryptografi i JavaScript, og siden Node.js 19.0.0 er den tilgængelig globalt via globalThis.crypto uden import. Det giver dig AES-GCM-kryptering, ECDSA-signaturer, PBKDF2-nøgleafledning og meget mere med et enkelt, promise-baseret interface, der virker identisk i Node.js, browsere, Cloudflare Workers og Deno. Denne guide viser dig præcis, hvordan du bruger det, trin for trin.

Hvad er WebCrypto API, og hvorfor bruge det?

WebCrypto API eksponerer kryptografiske primitiver via SubtleCrypto-grænsefladen. Den hedder “subtle” som en advarsel: kryptografi er kompliceret, og fejl har konsekvenser. Grænsefladen dækker seks kerneoperationer: kryptering, dekryptering, signering, verifikation, nøglegenerering og nøgleafledning.

Den klassiske node:crypto-modul er Node.js-specifik og synkron. WebCrypto API er asynkron, returnerer Promises og er identisk på tværs af platforme. Skriver du kode, der skal virke både på serveren og i browseren, er WebCrypto det rigtige valg. Fejltypen DOMException med name: 'InvalidAccessError' erstatter Node.js-specifikke fejlkoder.

Node.js 24.x tilføjede i 2025 støtte for ChaCha20-Poly1305, ML-KEM, ML-DSA, SHA-3, SHAKE og Argon2 via WebCrypto-grænsefladen, men denne guide fokuserer på den stabile kerne, der virker fra Node.js 20 og frem: AES-GCM, ECDSA, ECDH og PBKDF2.

Forudsætninger og systemkrav

Du behøver følgende for at følge guiden:

KomponentMinimumversionAnbefalet versionBemærkning
Node.js20.0.0 (LTS)22.x LTSglobalThis.crypto globalt tilgængeligt
npm9.x10.xMedfølger Node.js
OperativsystemLinux / macOS / WindowsUbuntu 22.04+Alle platforme understøttet
EditorkendskabGrundlæggende JavaScriptES2022+async/await er et krav
Sikkerheds-videnIngen forudsætningerKryptografibegreberne forklaresBegyndervenlig

Verifikér din Node.js-version med:

node --version
# Forventet output: v22.x.x eller nyere

Ingen tredjepartspakker er nødvendige. WebCrypto API er 100% indbygget i Node.js 20+.

Projektopsætning og mappestruktur

Opret et nyt projektmappe og initialiser det. Strukturen afspejler de 12 trin i guiden, og hvert script kan køres uafhængigt:

mkdir webcrypto-tutorial && cd webcrypto-tutorial
npm init -y

Opret følgende filer i projektet:

webcrypto-tutorial/
├── 01-random.js        # Trin 1-2: Tilfældig data og hashing
├── 02-aes-gcm.js       # Trin 3-5: AES-GCM kryptering
├── 03-ecdsa.js         # Trin 6-8: Digital signatur
├── 04-ecdh.js          # Trin 9: Nøgleaftale
├── 05-pbkdf2.js        # Trin 10-11: Nøgleafledning
├── 06-export.js        # Trin 12: Eksport og import af nøgler
└── index.js            # Komplet projekt

Projektet bruger udelukkende Node.js’ indbyggede moduler. Kør hvert script med node filnavn.js.

Trin 1-2: Adgang til WebCrypto og kryptografisk tilfældighed

Siden Node.js 19.0.0 er globalThis.crypto tilgængeligt uden import. I Node.js 18 skal du bruge require('node:crypto').webcrypto. Skriv kompatibel kode, der håndterer begge tilfælde:

// 01-random.js
// Node.js 20+: globalThis.crypto er tilgængeligt globalt
// Node.js 18: brug require('node:crypto').webcrypto

const { subtle, getRandomValues } = globalThis.crypto;

// Trin 1: Generer kryptografisk sikre tilfældige bytes
function genererTilfeldigeBytes(laengde) {
  const bytes = new Uint8Array(laengde);
  getRandomValues(bytes);
  return bytes;
}

// Hjælpefunktion: konverter ArrayBuffer til hex-streng
function tilHex(buffer) {
  return Buffer.from(buffer).toString('hex');
}

// Generer 32 bytes (256 bit) tilfældig data
const tilfaeldigNoegle = genererTilfeldigeBytes(32);
console.log('Tilfældig nøgle (hex):', tilHex(tilfaeldigNoegle));

// Trin 2: Generer UUID v4
const uuid = globalThis.crypto.randomUUID();
console.log('UUID:', uuid);

Kør scriptet og se output:

node 01-random.js
# Tilfældig nøgle (hex): a3f2c891b4e7d05621f9...
# UUID: 550e8400-e29b-41d4-a716-446655440000

Kritisk sikkerhedsregel: getRandomValues() bruger operativsystemets kryptografisk sikre tilfældighedsgenerator (CSPRNG), typisk /dev/urandom på Linux. Brug aldrig Math.random() til kryptografiske formål. Den er forudsigelig, og en angriber kan beregne tidligere og fremtidige værdier, hvis han kender et enkelt output.

Trin 3: SHA-256 og SHA-512 hashing med SubtleCrypto

WebCrypto API understøtter SHA-256, SHA-384 og SHA-512. Alle operationer er asynkrone og returnerer en Promise<ArrayBuffer>. Brug TextEncoder til at konvertere JavaScript-strenge til Uint8Array, som SubtleCrypto kræver:

// Tilføj til 01-random.js

async function beregnHash(besked, algoritme = 'SHA-256') {
  const encoder = new TextEncoder();
  const data = encoder.encode(besked);

  // subtle.digest() returnerer Promise
  const hashBuffer = await subtle.digest(algoritme, data);

  return tilHex(hashBuffer);
}

async function main() {
  const besked = 'Hej Danmark!';

  const sha256 = await beregnHash(besked, 'SHA-256');
  const sha512 = await beregnHash(besked, 'SHA-512');

  console.log('SHA-256 (32 bytes):', sha256);
  console.log('SHA-256 længde:', sha256.length / 2, 'bytes');

  console.log('SHA-512 (64 bytes):', sha512);
  console.log('SHA-512 længde:', sha512.length / 2, 'bytes');
}

main().catch(console.error);

SHA-256 producerer en 32-byte digest (256 bit), og SHA-512 producerer 64 bytes (512 bit). Begge er egnet til dataintegritetskontrol og digitale signaturer. Til adgangskodehashing skal du bruge PBKDF2 i Trin 11, da SHA alene er for hurtigt og sårbart overfor GPU-baserede brutforce-angreb.

Trin 4-6: Symmetrisk kryptering med AES-GCM

AES-GCM (Galois/Counter Mode) er den anbefalede symmetriske krypteringsalgoritme i 2026. Den giver både fortrolighed og autenticitet i én operation, hvad der gør den til “authenticated encryption with associated data” (AEAD). NIST SP 800-38D specificerer protokollen. En 12-byte (96-bit) initialiseringsvektor (IV) er standarden for AES-GCM og giver den bedste ydeevne.

Generering af AES-256-GCM nøgle (Trin 4)

Nøglelængden 256 bit er anbefalet til al ny implementering. Den tilbyder 128 bit effektiv sikkerhed, hvad der er modstandsdygtigt mod fremtidige kvantecomputerangreb via Grovers algoritme.

// 02-aes-gcm.js

const { subtle, getRandomValues } = globalThis.crypto;
const encoder = new TextEncoder();
const decoder = new TextDecoder();

// Trin 4: Generer en AES-GCM 256-bit nøgle
async function genererAESNoegle() {
  const noegle = await subtle.generateKey(
    {
      name: 'AES-GCM',
      length: 256         // 256-bit nøgle (32 bytes)
    },
    true,                 // extractable: kan eksporteres til JWK
    ['encrypt', 'decrypt']  // tilladte operationer
  );
  return noegle;
}

// Trin 5: Kryptér data med AES-GCM
async function krypter(noegle, klartekst) {
  // IV: 12 bytes (96 bit) er standarden for AES-GCM ifølge NIST SP 800-38D
  const iv = new Uint8Array(12);
  getRandomValues(iv);

  const data = encoder.encode(klartekst);

  const ciphertext = await subtle.encrypt(
    {
      name: 'AES-GCM',
      iv: iv,
      tagLength: 128      // 128-bit GCM-autentificeringstag (standard og anbefalet)
    },
    noegle,
    data
  );

  // Sammensæt IV og ciphertext til én buffer til transmission
  const resultat = new Uint8Array(iv.length + ciphertext.byteLength);
  resultat.set(iv, 0);
  resultat.set(new Uint8Array(ciphertext), iv.length);

  return resultat;
}

// Trin 6: Dekryptér data
async function dekrypter(noegle, krypteretData) {
  // Udpak IV (første 12 bytes) og ciphertext (resten)
  const iv = krypteretData.slice(0, 12);
  const ciphertext = krypteretData.slice(12);

  const klartext = await subtle.decrypt(
    {
      name: 'AES-GCM',
      iv: iv,
      tagLength: 128
    },
    noegle,
    ciphertext
  );

  return decoder.decode(klartext);
}

async function main() {
  const noegle = await genererAESNoegle();
  console.log('Nøgle genereret:', noegle.type, noegle.algorithm.name, noegle.algorithm.length + 'bit');

  const originalBesked = 'Fortrolig besked til dansk sikkerhedstest 2026';
  console.log('Original:', originalBesked);

  const krypteretData = await krypter(noegle, originalBesked);
  console.log('Krypteret (hex):', Buffer.from(krypteretData).toString('hex'));
  console.log('Krypteret længde:', krypteretData.length, 'bytes');

  const dekrypteretBesked = await dekrypter(noegle, krypteretData);
  console.log('Dekrypteret:', dekrypteretBesked);
  console.log('Match:', originalBesked === dekrypteretBesked ? 'JA' : 'NEJ');
}

main().catch(console.error);

Forventet output:

Nøgle genereret: secret AES-GCM 256bit
Original: Fortrolig besked til dansk sikkerhedstest 2026
Krypteret (hex): 8f3a1b2c... (variabel hex-streng)
Krypteret længde: 78 bytes
Dekrypteret: Fortrolig besked til dansk sikkerhedstest 2026
Match: JA

AES-GCM ciphertext er altid præcis 16 bytes længere end klarteksten, fordi GCM-autentificeringstaget er tilføjet. Ændrer du én byte i ciphertext og forsøger at dekryptere, kaster subtle.decrypt() en DOMException: The operation failed, hvad der bekræfter at integriteten er sikret. Det er en designfunktion, ikke en fejl.

Trin 7-9: Digital signatur med ECDSA P-256

ECDSA (Elliptic Curve Digital Signature Algorithm) med P-256-kurven giver stærke digitale signaturer med korte nøgler. En P-256 privat nøgle er 32 bytes og giver det samme sikkerhedsniveau som en 3072-bit RSA-nøgle. Signering bekræfter, at en bestemt besked er skabt af indehaveren af den private nøgle, og at den ikke er ændret undervejs. Se vores guide til Ed25519 signaturer i Node.js for et endnu mere moderne alternativ.

ECDSA nøglepar, signering og verifikation (Trin 7-9)

// 03-ecdsa.js

const { subtle } = globalThis.crypto;
const encoder = new TextEncoder();

// Trin 7: Generer ECDSA nøglepar med P-256 kurven
async function genererECDSANoeglepar() {
  const noeglepar = await subtle.generateKey(
    {
      name: 'ECDSA',
      namedCurve: 'P-256'   // Understøttet: P-256, P-384, P-521
    },
    true,                    // extractable (kan eksporteres)
    ['sign', 'verify']
  );
  return noeglepar;
}

// Trin 8: Signér data med den private nøgle
async function signerBesked(privatNoegle, besked) {
  const data = encoder.encode(besked);

  const signatur = await subtle.sign(
    {
      name: 'ECDSA',
      hash: 'SHA-256'        // Hash-algoritme kombineret med ECDSA
    },
    privatNoegle,
    data
  );

  return new Uint8Array(signatur);
}

// Trin 9: Verificér signatur med den offentlige nøgle
async function verificerSignatur(offentligNoegle, besked, signatur) {
  const data = encoder.encode(besked);

  const gyldig = await subtle.verify(
    {
      name: 'ECDSA',
      hash: 'SHA-256'
    },
    offentligNoegle,
    signatur,
    data
  );

  return gyldig;  // boolean: true eller false
}

async function main() {
  const { privateKey, publicKey } = await genererECDSANoeglepar();
  console.log('Privat nøgle type:', privateKey.type);
  console.log('Offentlig nøgle type:', publicKey.type);

  const besked = 'Kontrakt godkendt af Lars Andersen, 20. juni 2026';
  const signatur = await signerBesked(privateKey, besked);
  console.log('Signatur (hex):', Buffer.from(signatur).toString('hex'));
  console.log('Signatur størrelse:', signatur.length, 'bytes');

  // Verifikation med uberørt besked
  const gyldig = await verificerSignatur(publicKey, besked, signatur);
  console.log('Signatur gyldig:', gyldig);

  // Verifikation med ændret besked (skal returnere false)
  const aendretBesked = 'Kontrakt godkendt af Lars Andersen, 21. juni 2026';
  const ugyldig = await verificerSignatur(publicKey, aendretBesked, signatur);
  console.log('Ændret besked gyldig:', ugyldig);  // false
}

main().catch(console.error);

Forventet output:

Privat nøgle type: private
Offentlig nøgle type: public
Signatur (hex): 3045022100a3f2b1c9... (DER-kodet, ca. 70-72 bytes)
Signatur størrelse: 71 bytes
Signatur gyldig: true
Ændret besked gyldig: false

ECDSA P-256-signaturer er DER-kodede og typisk 70-72 bytes lange. Den offentlige nøgle kan deles frit, mens den private nøgle aldrig må forlade det sikre servermiljø. For webhook-signering med symmetriske nøgler, se vores guide til HMAC webhook-signaturer i Node.js.

Trin 10: ECDH nøgleaftale og delt hemmelighed

ECDH (Elliptic Curve Diffie-Hellman) løser problemet med nøgledistribution: to parter kan beregne en identisk delt hemmelighed, selv hvis al kommunikation er aflyttet. Angribere, der opsnapper den offentlige nøgleudbyte, kan ikke beregne den delte hemmelighed, fordi det kræver kendskab til mindst én privat nøgle. ECDH er grundlaget for TLS 1.3’s forward secrecy.

// 04-ecdh.js

const { subtle, getRandomValues } = globalThis.crypto;

async function ecdhNoegleaftale() {
  // Trin 10: Part A og B genererer hvert sit P-256 nøglepar
  const partA = await subtle.generateKey(
    { name: 'ECDH', namedCurve: 'P-256' },
    true,
    ['deriveKey', 'deriveBits']
  );

  const partB = await subtle.generateKey(
    { name: 'ECDH', namedCurve: 'P-256' },
    true,
    ['deriveKey', 'deriveBits']
  );

  // Part A beregner delt hemmelighed med Part B's offentlige nøgle
  // (I praksis sendes kun de offentlige nøgler over netværket)
  const deltNoegleA = await subtle.deriveKey(
    {
      name: 'ECDH',
      public: partB.publicKey    // Modpartens offentlige nøgle
    },
    partA.privateKey,            // Egen private nøgle
    { name: 'AES-GCM', length: 256 },  // Afled AES-256-GCM nøgle
    false,                       // Ikke-eksporterbar for bedre sikkerhed
    ['encrypt', 'decrypt']
  );

  // Part B gør det omvendte og får identisk nøgle
  const deltNoegleB = await subtle.deriveKey(
    {
      name: 'ECDH',
      public: partA.publicKey
    },
    partB.privateKey,
    { name: 'AES-GCM', length: 256 },
    false,
    ['encrypt', 'decrypt']
  );

  return { deltNoegleA, deltNoegleB };
}

async function main() {
  console.log('Udfører ECDH P-256 nøgleaftale...');
  const { deltNoegleA, deltNoegleB } = await ecdhNoegleaftale();

  // Verificér: kryptér med A's nøgle, dekryptér med B's nøgle
  const encoder = new TextEncoder();
  const decoder = new TextDecoder();
  const iv = new Uint8Array(12);
  getRandomValues(iv);

  const testBesked = 'Bekræftet: ECDH nøgleaftale vellykket';
  const krypteretAfA = await subtle.encrypt(
    { name: 'AES-GCM', iv },
    deltNoegleA,
    encoder.encode(testBesked)
  );

  const dekrypteretAfB = await subtle.decrypt(
    { name: 'AES-GCM', iv },
    deltNoegleB,
    krypteretAfA
  );

  const resultat = decoder.decode(dekrypteretAfB);
  console.log('Part A krypterede med sin ECDH-nøgle');
  console.log('Part B dekrypterede med sin ECDH-nøgle');
  console.log('Resultat:', resultat);
  console.log('Nøgler er identiske:', resultat === testBesked);
}

main().catch(console.error);

I en rigtig applikation udveksler Part A og Part B kun de offentlige nøgler via netværket. Begge beregner den delte AES-nøgle lokalt. Nøglen forlader aldrig nogen af de to enheder i ren form, hvad der er det centrale sikkerhedsprincip bag ECDH.

Trin 11-12: PBKDF2 nøgleafledning og JWK-eksport

Adgangskoder er menneskevalgte og har lav entropi. PBKDF2 (Password-Based Key Derivation Function 2, NIST SP 800-132) løser dette ved at anvende en hash-funktion mange tusinde gange, hvad der gør brutforce-angreb tidskrævende. OWASP anbefaler minimum 310.000 iterationer med SHA-256 for PBKDF2 i 2026. Salt på mindst 16 bytes forhindrer rainbow table-angreb.

// 05-pbkdf2.js

const { subtle, getRandomValues } = globalThis.crypto;
const encoder = new TextEncoder();

// Trin 11: PBKDF2 nøgleafledning fra adgangskode
async function afledNoegleFraAdgangskode(adgangskode, salt = null) {
  if (!salt) {
    salt = new Uint8Array(16);   // 16 bytes = 128-bit salt
    getRandomValues(salt);
  }

  // Importer adgangskoden som "raw" nøgle til PBKDF2
  const basisNoegle = await subtle.importKey(
    'raw',
    encoder.encode(adgangskode),
    'PBKDF2',
    false,                       // Kan aldrig eksporteres
    ['deriveKey', 'deriveBits']
  );

  // Afled AES-256-GCM nøgle via PBKDF2
  const afledtNoegle = await subtle.deriveKey(
    {
      name: 'PBKDF2',
      salt: salt,
      iterations: 310000,   // OWASP 2026-anbefaling for SHA-256
      hash: 'SHA-256'
    },
    basisNoegle,
    { name: 'AES-GCM', length: 256 },
    false,                   // Ikke-eksporterbar AES-nøgle
    ['encrypt', 'decrypt']
  );

  return { afledtNoegle, salt };
}

// Trin 12: Eksportér nøgle i JWK-format (JSON Web Key, RFC 7517)
async function eksporterNoegle(noegle) {
  const jwk = await subtle.exportKey('jwk', noegle);
  return jwk;
}

// Importér nøgle fra JWK
async function importerNoegle(jwk) {
  return subtle.importKey(
    'jwk',
    jwk,
    { name: 'AES-GCM', length: 256 },
    true,
    ['encrypt', 'decrypt']
  );
}

async function main() {
  const adgangskode = 'DanskeDevOps2026-Sikker!';

  console.log('Afled krypteringsnøgle med PBKDF2 (310.000 iterationer)...');
  const start = Date.now();
  const { afledtNoegle, salt } = await afledNoegleFraAdgangskode(adgangskode);
  const elapsed = Date.now() - start;

  console.log('Salt (hex):', Buffer.from(salt).toString('hex'));
  console.log('PBKDF2 tid:', elapsed, 'ms (bevidst langsom for sikkerhed)');
  console.log('Nøgle algoritme:', afledtNoegle.algorithm.name, afledtNoegle.algorithm.length + 'bit');

  // Test kryptering med PBKDF2-afledt nøgle
  const iv = new Uint8Array(12);
  getRandomValues(iv);

  const krypteretData = await subtle.encrypt(
    { name: 'AES-GCM', iv },
    afledtNoegle,
    encoder.encode('Fortrolig data krypteret med adgangskode')
  );
  console.log('Krypteret OK. Størrelse:', krypteretData.byteLength, 'bytes');

  // Trin 12: Eksportér en eksporterbar nøgle som JWK
  const eksporterbarNoegle = await subtle.generateKey(
    { name: 'AES-GCM', length: 256 },
    true,  // Denne er eksporterbar
    ['encrypt', 'decrypt']
  );

  const jwk = await eksporterNoegle(eksporterbarNoegle);
  console.log('\nJWK nøgle eksporteret:');
  console.log('  kty (nøgletype):', jwk.kty);  // "oct" for symmetrisk nøgle
  console.log('  alg (algoritme):', jwk.alg);  // "A256GCM"
  console.log('  k (nøgle, base64url):', jwk.k.slice(0, 20) + '...');

  // Reimportér fra JWK
  const genimporteret = await importerNoegle(jwk);
  console.log('  Reimporteret:', genimporteret.algorithm.name, genimporteret.algorithm.length + 'bit');
}

main().catch(console.error);

Forventet output viser, at PBKDF2-beregningen tager 500-2000 ms, afhængigt af server-CPU’en. Det er bevidst designet til at bremse angribere, der forsøger at afprøve millioner af adgangskoder.

Afled krypteringsnøgle med PBKDF2 (310.000 iterationer)...
Salt (hex): 3a7f2d1e8c4b9a0f5e6d2c3b1a7f8e9d
PBKDF2 tid: 847 ms (bevidst langsom for sikkerhed)
Nøgle algoritme: AES-GCM 256bit
Krypteret OK. Størrelse: 60 bytes

JWK nøgle eksporteret:
  kty (nøgletype): oct
  alg (algoritme): A256GCM
  k (nøgle, base64url): 3hG7kL2mNqP9rS4t...
  Reimporteret: AES-GCM 256bit

Komplet arbejdende projekt

Det komplette projekt kombinerer alle 12 trin i en realistisk use case: kryptér følsom JSON-data med en PBKDF2-afledt nøgle og sikr integriteten med en ECDSA-signatur. Dette mønster bruges i sikre API-systemer, dokumentunderskrivelse og krypteret dataudveksling.

// index.js - Komplet WebCrypto API demonstration (alle 12 trin)

const { subtle, getRandomValues } = globalThis.crypto;
const encoder = new TextEncoder();
const decoder = new TextDecoder();
const tilHex = (buf) => Buffer.from(buf).toString('hex');

async function kompletDemo() {
  console.log('=== WebCrypto API: Komplet Demo (Node.js 20+) ===\n');

  // Trin 7: ECDSA nøglepar til dokumentsignering
  const { privateKey, publicKey } = await subtle.generateKey(
    { name: 'ECDSA', namedCurve: 'P-256' },
    true,
    ['sign', 'verify']
  );
  console.log('[1/4] ECDSA P-256 nøglepar genereret');

  // Trin 11: PBKDF2 krypteringsnøgle fra adgangskode
  const adgangskode = 'NordisKSecOps2026!';
  const salt = new Uint8Array(16);
  getRandomValues(salt);

  const pbkdf2Basis = await subtle.importKey(
    'raw', encoder.encode(adgangskode), 'PBKDF2', false, ['deriveKey']
  );
  const krypteringsNoegle = await subtle.deriveKey(
    { name: 'PBKDF2', salt, iterations: 310000, hash: 'SHA-256' },
    pbkdf2Basis,
    { name: 'AES-GCM', length: 256 },
    false,
    ['encrypt', 'decrypt']
  );
  console.log('[2/4] AES-256-GCM nøgle afledt via PBKDF2 (310.000 iterationer)');

  // Trin 5: Kryptér følsom JSON-data
  const fortroligData = JSON.stringify({
    bruger: 'Mette Nielsen',
    rolle: 'Systemadministrator',
    adgang: ['prod-server', 'database', 'backup'],
    tidsstempel: '2026-06-20T12:00:00.000Z'
  });

  const iv = new Uint8Array(12);
  getRandomValues(iv);

  const krypteretPayload = await subtle.encrypt(
    { name: 'AES-GCM', iv },
    krypteringsNoegle,
    encoder.encode(fortroligData)
  );
  console.log('[3/4] Data krypteret med AES-256-GCM');
  console.log('      Ciphertext størrelse:', krypteretPayload.byteLength, 'bytes');
  console.log('      IV (hex):', tilHex(iv));

  // Trin 8: Signér den krypterede payload med ECDSA
  const signatur = await subtle.sign(
    { name: 'ECDSA', hash: 'SHA-256' },
    privateKey,
    krypteretPayload
  );
  console.log('      Signatur (hex):', tilHex(signatur).slice(0, 40) + '...');

  // --- Modtagersiden ---
  console.log('\n--- Modtager verificerer og dekrypterer ---');

  // Trin 9: Verificér ECDSA-signatur
  const signaturGyldig = await subtle.verify(
    { name: 'ECDSA', hash: 'SHA-256' },
    publicKey,
    signatur,
    krypteretPayload
  );

  if (!signaturGyldig) {
    throw new Error('ADVARSEL: Signatur ugyldig. Data kan være manipuleret!');
  }
  console.log('Signatur verificeret: OK');

  // Trin 6: Dekryptér data
  const dekrypteretBuffer = await subtle.decrypt(
    { name: 'AES-GCM', iv },
    krypteringsNoegle,
    krypteretPayload
  );

  const originalData = JSON.parse(decoder.decode(dekrypteretBuffer));
  console.log('[4/4] Data dekrypteret og verificeret');
  console.log('      Bruger:', originalData.bruger);
  console.log('      Rolle:', originalData.rolle);
  console.log('      Adgang:', originalData.adgang.join(', '));

  console.log('\n=== Demo afsluttet. Alle 12 trin gennemført. ===');
}

kompletDemo().catch(console.error);

Kør det med node index.js. Det tager 1-2 sekunder pga. PBKDF2-beregningen, hvad der er normalt.

Algoritmer understøttet i Node.js WebCrypto [2026]

AlgoritmeTypeStabil sidenPrimær brugNøglelængde
AES-GCMSymmetrisk krypteringNode 19.0.0Kryptering + autenticitet128/192/256 bit
AES-CBCSymmetrisk krypteringNode 19.0.0Kryptering (uden tag)128/192/256 bit
AES-CTRSymmetrisk krypteringNode 19.0.0Stream-kryptering128/192/256 bit
RSA-OAEPAsymmetrisk krypteringNode 19.0.0Nøgleindpakning2048/4096 bit
RSA-PSSAsymmetrisk signaturNode 19.0.0Signering2048/4096 bit
ECDSAAsymmetrisk signaturNode 19.0.0Signering (korte nøgler)P-256/P-384/P-521
ECDHNøgleaftaleNode 19.0.0Delt hemmelighedP-256/P-384/P-521
Ed25519Asymmetrisk signaturNode 20.19.3Moderne signering256 bit
X25519NøgleaftaleNode 20.19.3Moderne DH256 bit
PBKDF2NøgleafledningNode 19.0.0Adgangskode til nøgleAfhænger af hash
HKDFNøgleafledningNode 19.0.0NøgleudvidelseVariabel
SHA-256/384/512HashNode 19.0.0Digest256/384/512 bit
ChaCha20-Poly1305Symmetrisk krypteringNode 24.7.0Mobiloptimeret256 bit

Ed25519 og X25519 er backporteret til Node 20.19.3 og 22.13.0. Brug require('node:crypto').webcrypto i Node.js 18.

6 hyppige fejl og faldgruber med WebCrypto API

Faldgrube 1: IV-genbrug med AES-GCM. Genbruger du den samme IV med den samme nøgle i AES-GCM, ødelægger du al sikkerhed fuldstændigt. En angriber kan XOR to ciphertexts, der er krypteret med samme nøgle og IV, og nærme sig klarteksten. Generer altid en ny, tilfældig 12-byte IV for hvert krypteringsopkald med getRandomValues(). Gem IV’en sammen med ciphertext, da du skal bruge den til dekryptering.

Faldgrube 2: Direkte SHA-hashing af adgangskoder. subtle.digest('SHA-256', password) er ikke adgangskodehashing. SHA-256 er designet til at være hurtig og kan beregnes med milliarder af operationer per sekund på moderne GPU’er. Brug PBKDF2 med 310.000 iterationer eller Argon2 i Node.js til adgangskodehashing.

Faldgrube 3: Eksport af private nøgler unødigt. Sætter du extractable: true på private nøgler og eksporterer dem, giver du angribere mulighed for at stjæle nøglematerialet, hvis de får adgang til lagringen. Sæt private nøgler til extractable: false og brug nøgleomslagning (subtle.wrapKey() med RSA-OAEP) til sikker opbevaring.

Faldgrube 4: Ignorere CryptoKey “usages”-feltet. Genererer du en nøgle med kun ['encrypt'] og forsøger at bruge den til decrypt, kaster SubtleCrypto en InvalidAccessError. Planlæg tilladte operationer fra starten. En nøgle til kryptering og dekryptering kræver ['encrypt', 'decrypt'].

Faldgrube 5: Manglende try/catch med forkert fejltype. SubtleCrypto-metoder kaster DOMException, ikke Error. Kontrollér altid error.name i catch-blokken. Specifikke navne inkluderer: DataError (forkert nøgleformat), InvalidAccessError (forkert nøgletype), NotSupportedError (algoritme ikke understøttet) og OperationError (dekryptering mislykkedes, typisk fordi data er manipuleret).

Faldgrube 6: AES-CBC til nyudvikling. AES-CBC krypterer ikke autentificeret og er sårbar overfor padding oracle-angreb (fx POODLE). AES-GCM er det rette valg til al nyudvikling. Brug kun AES-CBC til kompatibilitet med ældre systemer.

Fejlfinding: 8 løsninger til hyppige problemer

Problem 1: TypeError: Cannot read properties of undefined (reading 'subtle')
Dit er Node.js under version 19. Løsning: opgrader til Node.js 20+ eller tilføj dette øverst i filen:

// Node.js 18 kompatibilitets-shim
const { webcrypto } = require('node:crypto');
const { subtle, getRandomValues } = webcrypto;

Problem 2: DOMException [OperationError]: The operation failed ved dekryptering.
Skyldes: (a) forkert IV (du bruger en ny IV i stedet for den gemte), (b) ciphertext er ændret, eller (c) forkert nøgle. AES-GCM opdager manipulation og returnerer fejl frem for korrupt data. Det er en sikkerhedsfunktion. Gem altid IV’en ved siden af ciphertext.

Problem 3: DOMException [DataError]: Failed to execute 'importKey'.
Nøgleformatet matcher ikke det faktiske format. Hvis nøglen er base64-kodet, skal du dekode til ArrayBuffer med Buffer.from(key, 'base64').buffer inden import via subtle.importKey('raw', ...).

Problem 4: DOMException [InvalidAccessError]: key.usages does not permit this operation.
Nøglens tilladte operationer matcher ikke den du prøver. Regenerér nøglen med de korrekte usages. En AES-GCM nøgle til kryptering og dekryptering kræver ['encrypt', 'decrypt'].

Problem 5: ArrayBuffer vs Buffer forvirring.
WebCrypto returnerer ArrayBuffer, men Node.js-funktioner forventer Buffer. Konverter med Buffer.from(arrayBuffer) eller new Uint8Array(arrayBuffer). For den modsatte konvertering: Buffer.from(hexString, 'hex').buffer.

Problem 6: PBKDF2 er langsom i test.
310.000 iterationer tager 500-2000 ms på en normal server-CPU. I testmiljøer, sæt iterationsantallet ned via en miljøvariabel:

const ITERATIONER = process.env.NODE_ENV === 'test' ? 1000 : 310000;

Problem 7: DOMException [NotSupportedError] for Ed25519 på ældre Node.js.
Ed25519 er stabil fra Node.js 20.19.3, 22.13.0 og 23.5.0. Kør node --version og opgradér. Se vores Ed25519 guide for detaljer om versionskrav.

Problem 8: WebCrypto virker ikke i Jest.
Ældre Jest-versioner med jsdom understøtter ikke WebCrypto. Sæt testmiljøet til node i jest.config.js:

// jest.config.js
module.exports = {
  testEnvironment: 'node',  // Brug Node.js-miljø, ikke jsdom
};

WebCrypto API vs node:crypto: hvornår bruger du hvad?

FunktionWebCrypto API (SubtleCrypto)node:crypto
PortabilitetBrowser + Node.js + Workers + DenoKun Node.js
API-stilAsynkron (Promise-baseret)Synkron og callback-baseret
NøgletypeCryptoKey (ikke-udtrækbar mulighed)Buffer/KeyObject
AES-GCMJa (SubtleCrypto)Ja (createCipheriv)
ChaCha20Ja (Node 24.7.0+)Ja (alle nyere versioner)
PBKDF2Ja (asynkront)Ja (asynkront)
scryptNejJa
Event loop blokeringBlokerer aldrigSynkrone metoder blokerer
StreamsIkke understøttetFuldt understøttet
Certifikater og X.509Ikke understøttetUnderstøttet
Anbefalet tilPortabel kode, standard-complianceStreams, certifikater, legacy

Brug WebCrypto API, når du skriver kode der skal køre i browsere og på serveren (fx Next.js edge functions, Cloudflare Workers, Deno Deploy). Brug node:crypto, når du har brug for streams, X.509-certifikatmanipulation eller algoritmer som scrypt, der ikke er i W3C-specifikationen. De to API’er fungerer side om side i samme projekt.

Avancerede teknikker og Node.js 24-nyheder

HKDF til nøgleudvidelse fra ECDH. Har du en delt hemmelighed fra ECDH og vil afkede flere nøgler fra den, er HKDF (HMAC-based Key Derivation Function, RFC 5869) det rette valg. Det tager eksisterende nøglemateriale med høj entropi og strækker det til den nøjagtige størrelse du behøver. Brug info-feltet til at binde nøglen til en kontekst (fx ‘kryptering-v1’ og ‘autenticitet-v1’), så de samme ECDH-nøgler kan afkede separate kryptering- og autenticitetsnøgler uden konflikt:

// HKDF: afled 2 separate nøgler fra én delt hemmelighed
const sharedBits = await subtle.deriveBits(
  { name: 'ECDH', public: modpartOffentligNoegle },
  minPrivatNoegle,
  256
);

const hkdfNoegle = await subtle.importKey(
  'raw', sharedBits, 'HKDF', false, ['deriveKey']
);

const krypteringsNoegle = await subtle.deriveKey(
  {
    name: 'HKDF',
    hash: 'SHA-256',
    salt: new Uint8Array(32),
    info: new TextEncoder().encode('kryptering-v1')
  },
  hkdfNoegle,
  { name: 'AES-GCM', length: 256 },
  false,
  ['encrypt', 'decrypt']
);

ChaCha20-Poly1305 i Node.js 24.7.0. ChaCha20-Poly1305 er Googles foretrukne algoritme til mobilenheder, fordi den er 2-3 gange hurtigere end AES på processorer uden AES-NI-hardwareinstruktioner (fx ældre ARM-chips). Node.js 24.7.0 tilføjede understøttelse via WebCrypto:

// Kræver Node.js 24.7.0+
const noegle = await subtle.generateKey(
  { name: 'ChaCha20-Poly1305', length: 256 },
  false,
  ['encrypt', 'decrypt']
);

const iv = new Uint8Array(12);
globalThis.crypto.getRandomValues(iv);

const krypteret = await subtle.encrypt(
  { name: 'ChaCha20-Poly1305', iv },
  noegle,
  encoder.encode('Mobil-optimeret kryptering')
);

Subresource Integrity (SRI) hash med WebCrypto. SHA-384-hashes til SRI-attributter på script- og link-tags beskytter mod CDN-kompromittering. WebCrypto genererer disse hashes nativt:

async function genererSRIHash(filIndhold) {
  const data = new TextEncoder().encode(filIndhold);
  const hashBuffer = await subtle.digest('SHA-384', data);
  const base64 = Buffer.from(hashBuffer).toString('base64');
  return `sha384-${base64}`;
}

const sriHash = await genererSRIHash(scriptIndhold);
// Output: sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/...
// Brug i HTML: 

For autentificering med asymmetriske nøgler i en moderne webapplikation, se vores guide til OAuth 2.0 og OpenID Connect i Node.js, der bruger lignende kryptografiske mønstre til token-signering og -verifikation.

Nøglestyring og sikkerhedsarkitektur i produktion

WebCrypto API løser kryptografien, men nøglehåndtering er et separat problem. En stærk algoritme er ubrugelig, hvis nøglen opbevares usikkert. Her er de vigtigste principper for produktionssystemer.

Nøglehierarkier og Key Encryption Keys (KEK). I stedet for at kryptere data direkte med en adgangskode-afledt nøgle, brug et to-niveaus hierarki: en “Data Encryption Key” (DEK) krypterer dataene, og en “Key Encryption Key” (KEK) krypterer DEK’en. Det giver dig mulighed for at rotere DEK’er uden at rekryptere alle data og at bruge hardware-sikkerhedsmoduler (HSM) til at beskytte KEK’en. Med WebCrypto API implementeres det via subtle.wrapKey() og subtle.unwrapKey():

// Nøgleindpakning: kryptér DEK med KEK via RSA-OAEP
async function indpakNoegle(dek, kek) {
  // subtle.wrapKey() krypterer en CryptoKey med en anden nøgle
  const indpakketDEK = await subtle.wrapKey(
    'raw',         // Eksportformat for DEK
    dek,           // Data Encryption Key der skal beskyttes
    kek,           // Key Encryption Key (fx RSA-OAEP offentlig nøgle)
    { name: 'RSA-OAEP' }
  );
  return indpakketDEK;
}

async function udpakNoegle(indpakketDEK, kek) {
  // subtle.unwrapKey() dekrypterer og importerer DEK i ét trin
  const dek = await subtle.unwrapKey(
    'raw',
    indpakketDEK,
    kek,           // RSA-OAEP privat nøgle
    { name: 'RSA-OAEP' },
    { name: 'AES-GCM', length: 256 },
    false,         // Den udpakkede DEK er ikke-eksporterbar
    ['encrypt', 'decrypt']
  );
  return dek;
}

Nøglerotation. Krypteringsnøgler bør roteres regelmæssigt. Jo kortere nøglens levetid, jo mindre data kan en kompromitteret nøgle afsløre. En typisk produktionsarkitektur bruger nye DEK’er pr. sessioner eller pr. bruger og roterer KEK’er hvert 90 dage. WebCrypto’s ikke-eksporterbare nøgler hjælper med at håndhæve politikken, da du ikke ved et uheld kan gemme dem i plaintext.

Tilfældighedskilder og entropiproblem. getRandomValues() er kryptografisk sikker på alle understøttede platforme. Cloudflare Workers og Node.js bruger begge OS-niveau CSPRNG. Dog er der én fælde: ved serveropstart umiddelbart efter boot kan entropipuljen på Linux-systemer være lav. Moderne Linux-kernels (5.18+) og Node.js 20+ håndterer dette korrekt, men på containeriserede miljøer i virtuelle maskiner bør du konfigurere /dev/urandom-entropi korrekt via virtio-rng eller haveged.

Tidsbaserede angreb og konstant-tidsammenligninger. Sammenligner du kryptografiske værdier med === i JavaScript, afslører du information via timing. En angriber kan måle, hvor hurtigt sammenligningen fejler, og slutte sig til de korrekte bytes én ad gangen. Node.js’s crypto.timingSafeEqual() fra node:crypto-modulet løser dette for buffersammenligninger. WebCrypto API håndterer det internt for subtle.verify(), men husk dette, hvis du sammenligner andre kryptografiske værdier manuelt.

Content-Security-Policy og WebCrypto i browseren. WebCrypto API er tilgængeligt i alle moderne browsere, men er begrænset til HTTPS-kontekster. På HTTP-sider er window.crypto.subtle ikke tilgængeligt og returnerer undefined. I dit produktionsmiljø skal du sikre at alle sider serveres over HTTPS, og at din Content-Security-Policy ikke blokerer inline scripts, der kalder WebCrypto. For Node.js-servere gælder HTTPS-kravet ikke, da der ikke er en browser involveret.

Miljøspecifikke overvejelser. På Cloudflare Workers er WebCrypto synkront cached på tværs af requests, men nøgler kan ikke deles på tværs af Worker-instanser. På Next.js edge runtime gælder samme begrænsninger. Generer altid request-specifikke IV’er og DEK’er, og undgå at holde krypteringsnøgler i global mutable state, da det kan give race conditions i concurrent request-håndtering.

For en dybdegående gennemgang af autentificeringsprotokoller der bruger kryptografiske nøgler i produktionssystemer, se vores guide til WebAuthn passwordless login i Node.js.

Hyppigt stillede spørgsmål

Hvad er forskellen på WebCrypto API og node:crypto?
WebCrypto (globalThis.crypto.subtle) er en W3C-standardiseret, asynkron API der virker i browsere, Node.js, Deno og Cloudflare Workers. node:crypto er Node.js-specifik, delvis synkron og bygger direkte på OpenSSL. Brug WebCrypto til portabel kode og node:crypto til Node.js-specifikke behov som streams og X.509-certifikater.

Er WebCrypto API hurtigere end node:crypto?
For de fleste operationer er hastighederne sammenlignelige, da begge bruger det samme underliggende OpenSSL-bibliotek. Den vigtigste forskel er API-stilen: WebCrypto blokerer aldrig event-loopet, mens visse node:crypto-metoder som createHash().update().digest() er synkrone og kan blokere ved store datamængder.

Kan jeg bruge WebCrypto API i Node.js 18?
Ja. På Node.js 18 er WebCrypto tilgængeligt via require('node:crypto').webcrypto. Du kan aktivere det globalt med flaget --experimental-global-webcrypto. Fra Node.js 19.0.0 er det globalt tilgængeligt pr. standard uden flaget.

Hvorfor bruger AES-GCM en 12-byte IV og ikke 16 byte?
AES-GCM er specificeret i NIST SP 800-38D og er optimeret til 96-bit (12-byte) IV. En 12-byte IV har den laveste sandsynlighed for kollision for en given nøgle og kræver ingen ekstra beregningsskridt. En 16-byte IV kræver en ekstra GHASH-operation. Brug altid 12 bytes for bedste sikkerhed og ydeevne.

Hvornår bruger jeg PBKDF2 vs HKDF?
PBKDF2 er til adgangskoder: det tager brugerinput med lav entropi og gør det beregningsintensivt at brutforce. HKDF er til nøglemateriale med høj entropi (fx output fra ECDH): det strækker og separerer eksisterende nøglemateriale til multiple nøgler. Brug aldrig HKDF til adgangskoder og aldrig PBKDF2 til høj-entropi input.

Er Ed25519 bedre end ECDSA P-256?
Ed25519 har lavere latency (deterministisk signering kræver ingen tilfældighedskilde under signeringsprocessen), er resistent mod visse side-channel angreb og producerer konsistente 64-byte signaturer. ECDSA P-256 er bredere understøttet i eksisterende systemer. Til nyt udviklingsarbejde i Node.js 22+ anbefales Ed25519. Se vores Ed25519 guide.

Virker WebCrypto i Cloudflare Workers og Next.js Edge Functions?
Ja. globalThis.crypto er tilgængeligt i Cloudflare Workers, Vercel Edge Functions, Next.js edge runtime og Deno Deploy. Det er kerneformålet med WebCrypto-standarden: skriv ét sæt kryptografisk kode og kør det på tværs af Node.js, browsere og edge-platforme. De implementerer alle den samme W3C SubtleCrypto-specifikation.

Kan WebCrypto bruges til at generere X.509-certifikater?
Ikke direkte. WebCrypto kan generere nøglepars og signere DER-kodet certifikatindhold, men selve X.509-strukturen og ASN.1-kodningen kræver node:crypto med X509Certificate-klassen, eller et bibliotek som @peculiar/x509, der bygger oven på WebCrypto.

Relateret indhold

Ekstern dokumentation: MDN Web Crypto API-dokumentation, Node.js WebCrypto API officiel dokumentation, W3C Web Cryptography API-specifikation, OWASP Password Storage Cheat Sheet, NIST SP 800-38D (AES-GCM specifikation).