{"id":151,"date":"2026-06-20T16:38:15","date_gmt":"2026-06-20T16:38:15","guid":{"rendered":"https:\/\/shattered.io\/dk\/2026\/06\/20\/webcrypto-api-nodejs\/"},"modified":"2026-06-20T16:39:44","modified_gmt":"2026-06-20T16:39:44","slug":"webcrypto-api-nodejs","status":"publish","type":"post","link":"https:\/\/shattered.io\/dk\/webcrypto-api-nodejs\/","title":{"rendered":"WebCrypto API i Node.js: AES-GCM og ECDSA i 12 trin [2026]"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">WebCrypto API er W3C-standarden for kryptografi i JavaScript, og siden Node.js 19.0.0 er den tilg\u00e6ngelig globalt via <code>globalThis.crypto<\/code> uden import. Det giver dig AES-GCM-kryptering, ECDSA-signaturer, PBKDF2-n\u00f8gleafledning 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\u00e6cis, hvordan du bruger det, trin for trin.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"hvad-er-webcrypto-api-og-hvorfor-bruge-det\">Hvad er WebCrypto API, og hvorfor bruge det?<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">WebCrypto API eksponerer kryptografiske primitiver via <code>SubtleCrypto<\/code>-gr\u00e6nsefladen. Den hedder &#8220;subtle&#8221; som en advarsel: kryptografi er kompliceret, og fejl har konsekvenser. Gr\u00e6nsefladen d\u00e6kker seks kerneoperationer: kryptering, dekryptering, signering, verifikation, n\u00f8glegenerering og n\u00f8gleafledning.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Den klassiske <code>node:crypto<\/code>-modul er Node.js-specifik og synkron. WebCrypto API er asynkron, returnerer Promises og er identisk p\u00e5 tv\u00e6rs af platforme. Skriver du kode, der skal virke b\u00e5de p\u00e5 serveren og i browseren, er WebCrypto det rigtige valg. Fejltypen <code>DOMException<\/code> med <code>name: 'InvalidAccessError'<\/code> erstatter Node.js-specifikke fejlkoder.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Node.js 24.x tilf\u00f8jede i 2025 st\u00f8tte for ChaCha20-Poly1305, ML-KEM, ML-DSA, SHA-3, SHAKE og Argon2 via WebCrypto-gr\u00e6nsefladen, men denne guide fokuserer p\u00e5 den stabile kerne, der virker fra Node.js 20 og frem: AES-GCM, ECDSA, ECDH og PBKDF2.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"forudsaetninger-og-systemkrav\">Foruds\u00e6tninger og systemkrav<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Du beh\u00f8ver f\u00f8lgende for at f\u00f8lge guiden:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Komponent<\/th><th>Minimumversion<\/th><th>Anbefalet version<\/th><th>Bem\u00e6rkning<\/th><\/tr><\/thead><tbody><tr><td>Node.js<\/td><td>20.0.0 (LTS)<\/td><td>22.x LTS<\/td><td><code>globalThis.crypto<\/code> globalt tilg\u00e6ngeligt<\/td><\/tr><tr><td>npm<\/td><td>9.x<\/td><td>10.x<\/td><td>Medf\u00f8lger Node.js<\/td><\/tr><tr><td>Operativsystem<\/td><td>Linux \/ macOS \/ Windows<\/td><td>Ubuntu 22.04+<\/td><td>Alle platforme underst\u00f8ttet<\/td><\/tr><tr><td>Editorkendskab<\/td><td>Grundl\u00e6ggende JavaScript<\/td><td>ES2022+<\/td><td>async\/await er et krav<\/td><\/tr><tr><td>Sikkerheds-viden<\/td><td>Ingen foruds\u00e6tninger<\/td><td>Kryptografibegreberne forklares<\/td><td>Begyndervenlig<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Verifik\u00e9r din Node.js-version med:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>node --version\n# Forventet output: v22.x.x eller nyere<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Ingen tredjepartspakker er n\u00f8dvendige. WebCrypto API er 100% indbygget i Node.js 20+.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"projektopsaetning-og-mappestruktur\">Projektops\u00e6tning og mappestruktur<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Opret et nyt projektmappe og initialiser det. Strukturen afspejler de 12 trin i guiden, og hvert script kan k\u00f8res uafh\u00e6ngigt:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>mkdir webcrypto-tutorial && cd webcrypto-tutorial\nnpm init -y<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Opret f\u00f8lgende filer i projektet:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>webcrypto-tutorial\/\n\u251c\u2500\u2500 01-random.js        # Trin 1-2: Tilf\u00e6ldig data og hashing\n\u251c\u2500\u2500 02-aes-gcm.js       # Trin 3-5: AES-GCM kryptering\n\u251c\u2500\u2500 03-ecdsa.js         # Trin 6-8: Digital signatur\n\u251c\u2500\u2500 04-ecdh.js          # Trin 9: N\u00f8gleaftale\n\u251c\u2500\u2500 05-pbkdf2.js        # Trin 10-11: N\u00f8gleafledning\n\u251c\u2500\u2500 06-export.js        # Trin 12: Eksport og import af n\u00f8gler\n\u2514\u2500\u2500 index.js            # Komplet projekt<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Projektet bruger udelukkende Node.js&#8217; indbyggede moduler. K\u00f8r hvert script med <code>node filnavn.js<\/code>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"trin-1-2-adgang-til-webcrypto-og-kryptografisk-tilfaeldighed\">Trin 1-2: Adgang til WebCrypto og kryptografisk tilf\u00e6ldighed<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Siden Node.js 19.0.0 er <code>globalThis.crypto<\/code> tilg\u00e6ngeligt uden import. I Node.js 18 skal du bruge <code>require('node:crypto').webcrypto<\/code>. Skriv kompatibel kode, der h\u00e5ndterer begge tilf\u00e6lde:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ 01-random.js\n\/\/ Node.js 20+: globalThis.crypto er tilg\u00e6ngeligt globalt\n\/\/ Node.js 18: brug require('node:crypto').webcrypto\n\nconst { subtle, getRandomValues } = globalThis.crypto;\n\n\/\/ Trin 1: Generer kryptografisk sikre tilf\u00e6ldige bytes\nfunction genererTilfeldigeBytes(laengde) {\n  const bytes = new Uint8Array(laengde);\n  getRandomValues(bytes);\n  return bytes;\n}\n\n\/\/ Hj\u00e6lpefunktion: konverter ArrayBuffer til hex-streng\nfunction tilHex(buffer) {\n  return Buffer.from(buffer).toString('hex');\n}\n\n\/\/ Generer 32 bytes (256 bit) tilf\u00e6ldig data\nconst tilfaeldigNoegle = genererTilfeldigeBytes(32);\nconsole.log('Tilf\u00e6ldig n\u00f8gle (hex):', tilHex(tilfaeldigNoegle));\n\n\/\/ Trin 2: Generer UUID v4\nconst uuid = globalThis.crypto.randomUUID();\nconsole.log('UUID:', uuid);<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">K\u00f8r scriptet og se output:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>node 01-random.js\n# Tilf\u00e6ldig n\u00f8gle (hex): a3f2c891b4e7d05621f9...\n# UUID: 550e8400-e29b-41d4-a716-446655440000<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Kritisk sikkerhedsregel:<\/strong> <code>getRandomValues()<\/code> bruger operativsystemets kryptografisk sikre tilf\u00e6ldighedsgenerator (CSPRNG), typisk <code>\/dev\/urandom<\/code> p\u00e5 Linux. Brug aldrig <code>Math.random()<\/code> til kryptografiske form\u00e5l. Den er forudsigelig, og en angriber kan beregne tidligere og fremtidige v\u00e6rdier, hvis han kender et enkelt output.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"trin-3-sha-256-og-sha-512-hashing-med-subtlecrypto\">Trin 3: SHA-256 og SHA-512 hashing med SubtleCrypto<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">WebCrypto API underst\u00f8tter SHA-256, SHA-384 og SHA-512. Alle operationer er asynkrone og returnerer en <code>Promise&lt;ArrayBuffer&gt;<\/code>. Brug <code>TextEncoder<\/code> til at konvertere JavaScript-strenge til <code>Uint8Array<\/code>, som SubtleCrypto kr\u00e6ver:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ Tilf\u00f8j til 01-random.js\n\nasync function beregnHash(besked, algoritme = 'SHA-256') {\n  const encoder = new TextEncoder();\n  const data = encoder.encode(besked);\n\n  \/\/ subtle.digest() returnerer Promise<ArrayBuffer>\n  const hashBuffer = await subtle.digest(algoritme, data);\n\n  return tilHex(hashBuffer);\n}\n\nasync function main() {\n  const besked = 'Hej Danmark!';\n\n  const sha256 = await beregnHash(besked, 'SHA-256');\n  const sha512 = await beregnHash(besked, 'SHA-512');\n\n  console.log('SHA-256 (32 bytes):', sha256);\n  console.log('SHA-256 l\u00e6ngde:', sha256.length \/ 2, 'bytes');\n\n  console.log('SHA-512 (64 bytes):', sha512);\n  console.log('SHA-512 l\u00e6ngde:', sha512.length \/ 2, 'bytes');\n}\n\nmain().catch(console.error);<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">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\u00e5rbart overfor GPU-baserede brutforce-angreb.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"trin-4-6-symmetrisk-kryptering-med-aes-gcm\">Trin 4-6: Symmetrisk kryptering med AES-GCM<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">AES-GCM (Galois\/Counter Mode) er den anbefalede symmetriske krypteringsalgoritme i 2026. Den giver b\u00e5de fortrolighed og autenticitet i \u00e9n operation, hvad der g\u00f8r den til &#8220;authenticated encryption with associated data&#8221; (AEAD). NIST SP 800-38D specificerer protokollen. En 12-byte (96-bit) initialiseringsvektor (IV) er standarden for AES-GCM og giver den bedste ydeevne.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"generering-af-aes-256-gcm-noegle-trin-4\">Generering af AES-256-GCM n\u00f8gle (Trin 4)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">N\u00f8glel\u00e6ngden 256 bit er anbefalet til al ny implementering. Den tilbyder 128 bit effektiv sikkerhed, hvad der er modstandsdygtigt mod fremtidige kvantecomputerangreb via Grovers algoritme.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ 02-aes-gcm.js\n\nconst { subtle, getRandomValues } = globalThis.crypto;\nconst encoder = new TextEncoder();\nconst decoder = new TextDecoder();\n\n\/\/ Trin 4: Generer en AES-GCM 256-bit n\u00f8gle\nasync function genererAESNoegle() {\n  const noegle = await subtle.generateKey(\n    {\n      name: 'AES-GCM',\n      length: 256         \/\/ 256-bit n\u00f8gle (32 bytes)\n    },\n    true,                 \/\/ extractable: kan eksporteres til JWK\n    ['encrypt', 'decrypt']  \/\/ tilladte operationer\n  );\n  return noegle;\n}\n\n\/\/ Trin 5: Krypt\u00e9r data med AES-GCM\nasync function krypter(noegle, klartekst) {\n  \/\/ IV: 12 bytes (96 bit) er standarden for AES-GCM if\u00f8lge NIST SP 800-38D\n  const iv = new Uint8Array(12);\n  getRandomValues(iv);\n\n  const data = encoder.encode(klartekst);\n\n  const ciphertext = await subtle.encrypt(\n    {\n      name: 'AES-GCM',\n      iv: iv,\n      tagLength: 128      \/\/ 128-bit GCM-autentificeringstag (standard og anbefalet)\n    },\n    noegle,\n    data\n  );\n\n  \/\/ Sammens\u00e6t IV og ciphertext til \u00e9n buffer til transmission\n  const resultat = new Uint8Array(iv.length + ciphertext.byteLength);\n  resultat.set(iv, 0);\n  resultat.set(new Uint8Array(ciphertext), iv.length);\n\n  return resultat;\n}\n\n\/\/ Trin 6: Dekrypt\u00e9r data\nasync function dekrypter(noegle, krypteretData) {\n  \/\/ Udpak IV (f\u00f8rste 12 bytes) og ciphertext (resten)\n  const iv = krypteretData.slice(0, 12);\n  const ciphertext = krypteretData.slice(12);\n\n  const klartext = await subtle.decrypt(\n    {\n      name: 'AES-GCM',\n      iv: iv,\n      tagLength: 128\n    },\n    noegle,\n    ciphertext\n  );\n\n  return decoder.decode(klartext);\n}\n\nasync function main() {\n  const noegle = await genererAESNoegle();\n  console.log('N\u00f8gle genereret:', noegle.type, noegle.algorithm.name, noegle.algorithm.length + 'bit');\n\n  const originalBesked = 'Fortrolig besked til dansk sikkerhedstest 2026';\n  console.log('Original:', originalBesked);\n\n  const krypteretData = await krypter(noegle, originalBesked);\n  console.log('Krypteret (hex):', Buffer.from(krypteretData).toString('hex'));\n  console.log('Krypteret l\u00e6ngde:', krypteretData.length, 'bytes');\n\n  const dekrypteretBesked = await dekrypter(noegle, krypteretData);\n  console.log('Dekrypteret:', dekrypteretBesked);\n  console.log('Match:', originalBesked === dekrypteretBesked ? 'JA' : 'NEJ');\n}\n\nmain().catch(console.error);<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Forventet output:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>N\u00f8gle genereret: secret AES-GCM 256bit\nOriginal: Fortrolig besked til dansk sikkerhedstest 2026\nKrypteret (hex): 8f3a1b2c... (variabel hex-streng)\nKrypteret l\u00e6ngde: 78 bytes\nDekrypteret: Fortrolig besked til dansk sikkerhedstest 2026\nMatch: JA<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">AES-GCM ciphertext er altid pr\u00e6cis 16 bytes l\u00e6ngere end klarteksten, fordi GCM-autentificeringstaget er tilf\u00f8jet. \u00c6ndrer du \u00e9n byte i ciphertext og fors\u00f8ger at dekryptere, kaster <code>subtle.decrypt()<\/code> en <code>DOMException: The operation failed<\/code>, hvad der bekr\u00e6fter at integriteten er sikret. Det er en designfunktion, ikke en fejl.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"trin-7-9-digital-signatur-med-ecdsa-p-256\">Trin 7-9: Digital signatur med ECDSA P-256<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">ECDSA (Elliptic Curve Digital Signature Algorithm) med P-256-kurven giver st\u00e6rke digitale signaturer med korte n\u00f8gler. En P-256 privat n\u00f8gle er 32 bytes og giver det samme sikkerhedsniveau som en 3072-bit RSA-n\u00f8gle. Signering bekr\u00e6fter, at en bestemt besked er skabt af indehaveren af den private n\u00f8gle, og at den ikke er \u00e6ndret undervejs. Se vores guide til <a href=\"\/da\/ed25519-signaturer-nodejs\/\">Ed25519 signaturer i Node.js<\/a> for et endnu mere moderne alternativ.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"ecdsa-noeglepar-signering-og-verifikation-trin-7-9\">ECDSA n\u00f8glepar, signering og verifikation (Trin 7-9)<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ 03-ecdsa.js\n\nconst { subtle } = globalThis.crypto;\nconst encoder = new TextEncoder();\n\n\/\/ Trin 7: Generer ECDSA n\u00f8glepar med P-256 kurven\nasync function genererECDSANoeglepar() {\n  const noeglepar = await subtle.generateKey(\n    {\n      name: 'ECDSA',\n      namedCurve: 'P-256'   \/\/ Underst\u00f8ttet: P-256, P-384, P-521\n    },\n    true,                    \/\/ extractable (kan eksporteres)\n    ['sign', 'verify']\n  );\n  return noeglepar;\n}\n\n\/\/ Trin 8: Sign\u00e9r data med den private n\u00f8gle\nasync function signerBesked(privatNoegle, besked) {\n  const data = encoder.encode(besked);\n\n  const signatur = await subtle.sign(\n    {\n      name: 'ECDSA',\n      hash: 'SHA-256'        \/\/ Hash-algoritme kombineret med ECDSA\n    },\n    privatNoegle,\n    data\n  );\n\n  return new Uint8Array(signatur);\n}\n\n\/\/ Trin 9: Verific\u00e9r signatur med den offentlige n\u00f8gle\nasync function verificerSignatur(offentligNoegle, besked, signatur) {\n  const data = encoder.encode(besked);\n\n  const gyldig = await subtle.verify(\n    {\n      name: 'ECDSA',\n      hash: 'SHA-256'\n    },\n    offentligNoegle,\n    signatur,\n    data\n  );\n\n  return gyldig;  \/\/ boolean: true eller false\n}\n\nasync function main() {\n  const { privateKey, publicKey } = await genererECDSANoeglepar();\n  console.log('Privat n\u00f8gle type:', privateKey.type);\n  console.log('Offentlig n\u00f8gle type:', publicKey.type);\n\n  const besked = 'Kontrakt godkendt af Lars Andersen, 20. juni 2026';\n  const signatur = await signerBesked(privateKey, besked);\n  console.log('Signatur (hex):', Buffer.from(signatur).toString('hex'));\n  console.log('Signatur st\u00f8rrelse:', signatur.length, 'bytes');\n\n  \/\/ Verifikation med uber\u00f8rt besked\n  const gyldig = await verificerSignatur(publicKey, besked, signatur);\n  console.log('Signatur gyldig:', gyldig);\n\n  \/\/ Verifikation med \u00e6ndret besked (skal returnere false)\n  const aendretBesked = 'Kontrakt godkendt af Lars Andersen, 21. juni 2026';\n  const ugyldig = await verificerSignatur(publicKey, aendretBesked, signatur);\n  console.log('\u00c6ndret besked gyldig:', ugyldig);  \/\/ false\n}\n\nmain().catch(console.error);<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Forventet output:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Privat n\u00f8gle type: private\nOffentlig n\u00f8gle type: public\nSignatur (hex): 3045022100a3f2b1c9... (DER-kodet, ca. 70-72 bytes)\nSignatur st\u00f8rrelse: 71 bytes\nSignatur gyldig: true\n\u00c6ndret besked gyldig: false<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">ECDSA P-256-signaturer er DER-kodede og typisk 70-72 bytes lange. Den offentlige n\u00f8gle kan deles frit, mens den private n\u00f8gle aldrig m\u00e5 forlade det sikre servermilj\u00f8. For webhook-signering med symmetriske n\u00f8gler, se vores guide til <a href=\"\/da\/hmac-webhook-signaturer-nodejs\/\">HMAC webhook-signaturer i Node.js<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"trin-10-ecdh-noegleaftale-og-delt-hemmelighed\">Trin 10: ECDH n\u00f8gleaftale og delt hemmelighed<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">ECDH (Elliptic Curve Diffie-Hellman) l\u00f8ser problemet med n\u00f8gledistribution: to parter kan beregne en identisk delt hemmelighed, selv hvis al kommunikation er aflyttet. Angribere, der opsnapper den offentlige n\u00f8gleudbyte, kan ikke beregne den delte hemmelighed, fordi det kr\u00e6ver kendskab til mindst \u00e9n privat n\u00f8gle. ECDH er grundlaget for TLS 1.3&#8217;s forward secrecy.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ 04-ecdh.js\n\nconst { subtle, getRandomValues } = globalThis.crypto;\n\nasync function ecdhNoegleaftale() {\n  \/\/ Trin 10: Part A og B genererer hvert sit P-256 n\u00f8glepar\n  const partA = await subtle.generateKey(\n    { name: 'ECDH', namedCurve: 'P-256' },\n    true,\n    ['deriveKey', 'deriveBits']\n  );\n\n  const partB = await subtle.generateKey(\n    { name: 'ECDH', namedCurve: 'P-256' },\n    true,\n    ['deriveKey', 'deriveBits']\n  );\n\n  \/\/ Part A beregner delt hemmelighed med Part B's offentlige n\u00f8gle\n  \/\/ (I praksis sendes kun de offentlige n\u00f8gler over netv\u00e6rket)\n  const deltNoegleA = await subtle.deriveKey(\n    {\n      name: 'ECDH',\n      public: partB.publicKey    \/\/ Modpartens offentlige n\u00f8gle\n    },\n    partA.privateKey,            \/\/ Egen private n\u00f8gle\n    { name: 'AES-GCM', length: 256 },  \/\/ Afled AES-256-GCM n\u00f8gle\n    false,                       \/\/ Ikke-eksporterbar for bedre sikkerhed\n    ['encrypt', 'decrypt']\n  );\n\n  \/\/ Part B g\u00f8r det omvendte og f\u00e5r identisk n\u00f8gle\n  const deltNoegleB = await subtle.deriveKey(\n    {\n      name: 'ECDH',\n      public: partA.publicKey\n    },\n    partB.privateKey,\n    { name: 'AES-GCM', length: 256 },\n    false,\n    ['encrypt', 'decrypt']\n  );\n\n  return { deltNoegleA, deltNoegleB };\n}\n\nasync function main() {\n  console.log('Udf\u00f8rer ECDH P-256 n\u00f8gleaftale...');\n  const { deltNoegleA, deltNoegleB } = await ecdhNoegleaftale();\n\n  \/\/ Verific\u00e9r: krypt\u00e9r med A's n\u00f8gle, dekrypt\u00e9r med B's n\u00f8gle\n  const encoder = new TextEncoder();\n  const decoder = new TextDecoder();\n  const iv = new Uint8Array(12);\n  getRandomValues(iv);\n\n  const testBesked = 'Bekr\u00e6ftet: ECDH n\u00f8gleaftale vellykket';\n  const krypteretAfA = await subtle.encrypt(\n    { name: 'AES-GCM', iv },\n    deltNoegleA,\n    encoder.encode(testBesked)\n  );\n\n  const dekrypteretAfB = await subtle.decrypt(\n    { name: 'AES-GCM', iv },\n    deltNoegleB,\n    krypteretAfA\n  );\n\n  const resultat = decoder.decode(dekrypteretAfB);\n  console.log('Part A krypterede med sin ECDH-n\u00f8gle');\n  console.log('Part B dekrypterede med sin ECDH-n\u00f8gle');\n  console.log('Resultat:', resultat);\n  console.log('N\u00f8gler er identiske:', resultat === testBesked);\n}\n\nmain().catch(console.error);<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">I en rigtig applikation udveksler Part A og Part B kun de offentlige n\u00f8gler via netv\u00e6rket. Begge beregner den delte AES-n\u00f8gle lokalt. N\u00f8glen forlader aldrig nogen af de to enheder i ren form, hvad der er det centrale sikkerhedsprincip bag ECDH.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"trin-11-12-pbkdf2-noegleafledning-og-jwk-eksport\">Trin 11-12: PBKDF2 n\u00f8gleafledning og JWK-eksport<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Adgangskoder er menneskevalgte og har lav entropi. PBKDF2 (Password-Based Key Derivation Function 2, NIST SP 800-132) l\u00f8ser dette ved at anvende en hash-funktion mange tusinde gange, hvad der g\u00f8r brutforce-angreb tidskr\u00e6vende. OWASP anbefaler minimum 310.000 iterationer med SHA-256 for PBKDF2 i 2026. Salt p\u00e5 mindst 16 bytes forhindrer rainbow table-angreb.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ 05-pbkdf2.js\n\nconst { subtle, getRandomValues } = globalThis.crypto;\nconst encoder = new TextEncoder();\n\n\/\/ Trin 11: PBKDF2 n\u00f8gleafledning fra adgangskode\nasync function afledNoegleFraAdgangskode(adgangskode, salt = null) {\n  if (!salt) {\n    salt = new Uint8Array(16);   \/\/ 16 bytes = 128-bit salt\n    getRandomValues(salt);\n  }\n\n  \/\/ Importer adgangskoden som \"raw\" n\u00f8gle til PBKDF2\n  const basisNoegle = await subtle.importKey(\n    'raw',\n    encoder.encode(adgangskode),\n    'PBKDF2',\n    false,                       \/\/ Kan aldrig eksporteres\n    ['deriveKey', 'deriveBits']\n  );\n\n  \/\/ Afled AES-256-GCM n\u00f8gle via PBKDF2\n  const afledtNoegle = await subtle.deriveKey(\n    {\n      name: 'PBKDF2',\n      salt: salt,\n      iterations: 310000,   \/\/ OWASP 2026-anbefaling for SHA-256\n      hash: 'SHA-256'\n    },\n    basisNoegle,\n    { name: 'AES-GCM', length: 256 },\n    false,                   \/\/ Ikke-eksporterbar AES-n\u00f8gle\n    ['encrypt', 'decrypt']\n  );\n\n  return { afledtNoegle, salt };\n}\n\n\/\/ Trin 12: Eksport\u00e9r n\u00f8gle i JWK-format (JSON Web Key, RFC 7517)\nasync function eksporterNoegle(noegle) {\n  const jwk = await subtle.exportKey('jwk', noegle);\n  return jwk;\n}\n\n\/\/ Import\u00e9r n\u00f8gle fra JWK\nasync function importerNoegle(jwk) {\n  return subtle.importKey(\n    'jwk',\n    jwk,\n    { name: 'AES-GCM', length: 256 },\n    true,\n    ['encrypt', 'decrypt']\n  );\n}\n\nasync function main() {\n  const adgangskode = 'DanskeDevOps2026-Sikker!';\n\n  console.log('Afled krypteringsn\u00f8gle med PBKDF2 (310.000 iterationer)...');\n  const start = Date.now();\n  const { afledtNoegle, salt } = await afledNoegleFraAdgangskode(adgangskode);\n  const elapsed = Date.now() - start;\n\n  console.log('Salt (hex):', Buffer.from(salt).toString('hex'));\n  console.log('PBKDF2 tid:', elapsed, 'ms (bevidst langsom for sikkerhed)');\n  console.log('N\u00f8gle algoritme:', afledtNoegle.algorithm.name, afledtNoegle.algorithm.length + 'bit');\n\n  \/\/ Test kryptering med PBKDF2-afledt n\u00f8gle\n  const iv = new Uint8Array(12);\n  getRandomValues(iv);\n\n  const krypteretData = await subtle.encrypt(\n    { name: 'AES-GCM', iv },\n    afledtNoegle,\n    encoder.encode('Fortrolig data krypteret med adgangskode')\n  );\n  console.log('Krypteret OK. St\u00f8rrelse:', krypteretData.byteLength, 'bytes');\n\n  \/\/ Trin 12: Eksport\u00e9r en eksporterbar n\u00f8gle som JWK\n  const eksporterbarNoegle = await subtle.generateKey(\n    { name: 'AES-GCM', length: 256 },\n    true,  \/\/ Denne er eksporterbar\n    ['encrypt', 'decrypt']\n  );\n\n  const jwk = await eksporterNoegle(eksporterbarNoegle);\n  console.log('\\nJWK n\u00f8gle eksporteret:');\n  console.log('  kty (n\u00f8gletype):', jwk.kty);  \/\/ \"oct\" for symmetrisk n\u00f8gle\n  console.log('  alg (algoritme):', jwk.alg);  \/\/ \"A256GCM\"\n  console.log('  k (n\u00f8gle, base64url):', jwk.k.slice(0, 20) + '...');\n\n  \/\/ Reimport\u00e9r fra JWK\n  const genimporteret = await importerNoegle(jwk);\n  console.log('  Reimporteret:', genimporteret.algorithm.name, genimporteret.algorithm.length + 'bit');\n}\n\nmain().catch(console.error);<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Forventet output viser, at PBKDF2-beregningen tager 500-2000 ms, afh\u00e6ngigt af server-CPU&#8217;en. Det er bevidst designet til at bremse angribere, der fors\u00f8ger at afpr\u00f8ve millioner af adgangskoder.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Afled krypteringsn\u00f8gle med PBKDF2 (310.000 iterationer)...\nSalt (hex): 3a7f2d1e8c4b9a0f5e6d2c3b1a7f8e9d\nPBKDF2 tid: 847 ms (bevidst langsom for sikkerhed)\nN\u00f8gle algoritme: AES-GCM 256bit\nKrypteret OK. St\u00f8rrelse: 60 bytes\n\nJWK n\u00f8gle eksporteret:\n  kty (n\u00f8gletype): oct\n  alg (algoritme): A256GCM\n  k (n\u00f8gle, base64url): 3hG7kL2mNqP9rS4t...\n  Reimporteret: AES-GCM 256bit<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"komplet-arbejdende-projekt\">Komplet arbejdende projekt<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Det komplette projekt kombinerer alle 12 trin i en realistisk use case: krypt\u00e9r f\u00f8lsom JSON-data med en PBKDF2-afledt n\u00f8gle og sikr integriteten med en ECDSA-signatur. Dette m\u00f8nster bruges i sikre API-systemer, dokumentunderskrivelse og krypteret dataudveksling.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ index.js - Komplet WebCrypto API demonstration (alle 12 trin)\n\nconst { subtle, getRandomValues } = globalThis.crypto;\nconst encoder = new TextEncoder();\nconst decoder = new TextDecoder();\nconst tilHex = (buf) => Buffer.from(buf).toString('hex');\n\nasync function kompletDemo() {\n  console.log('=== WebCrypto API: Komplet Demo (Node.js 20+) ===\\n');\n\n  \/\/ Trin 7: ECDSA n\u00f8glepar til dokumentsignering\n  const { privateKey, publicKey } = await subtle.generateKey(\n    { name: 'ECDSA', namedCurve: 'P-256' },\n    true,\n    ['sign', 'verify']\n  );\n  console.log('[1\/4] ECDSA P-256 n\u00f8glepar genereret');\n\n  \/\/ Trin 11: PBKDF2 krypteringsn\u00f8gle fra adgangskode\n  const adgangskode = 'NordisKSecOps2026!';\n  const salt = new Uint8Array(16);\n  getRandomValues(salt);\n\n  const pbkdf2Basis = await subtle.importKey(\n    'raw', encoder.encode(adgangskode), 'PBKDF2', false, ['deriveKey']\n  );\n  const krypteringsNoegle = await subtle.deriveKey(\n    { name: 'PBKDF2', salt, iterations: 310000, hash: 'SHA-256' },\n    pbkdf2Basis,\n    { name: 'AES-GCM', length: 256 },\n    false,\n    ['encrypt', 'decrypt']\n  );\n  console.log('[2\/4] AES-256-GCM n\u00f8gle afledt via PBKDF2 (310.000 iterationer)');\n\n  \/\/ Trin 5: Krypt\u00e9r f\u00f8lsom JSON-data\n  const fortroligData = JSON.stringify({\n    bruger: 'Mette Nielsen',\n    rolle: 'Systemadministrator',\n    adgang: ['prod-server', 'database', 'backup'],\n    tidsstempel: '2026-06-20T12:00:00.000Z'\n  });\n\n  const iv = new Uint8Array(12);\n  getRandomValues(iv);\n\n  const krypteretPayload = await subtle.encrypt(\n    { name: 'AES-GCM', iv },\n    krypteringsNoegle,\n    encoder.encode(fortroligData)\n  );\n  console.log('[3\/4] Data krypteret med AES-256-GCM');\n  console.log('      Ciphertext st\u00f8rrelse:', krypteretPayload.byteLength, 'bytes');\n  console.log('      IV (hex):', tilHex(iv));\n\n  \/\/ Trin 8: Sign\u00e9r den krypterede payload med ECDSA\n  const signatur = await subtle.sign(\n    { name: 'ECDSA', hash: 'SHA-256' },\n    privateKey,\n    krypteretPayload\n  );\n  console.log('      Signatur (hex):', tilHex(signatur).slice(0, 40) + '...');\n\n  \/\/ --- Modtagersiden ---\n  console.log('\\n--- Modtager verificerer og dekrypterer ---');\n\n  \/\/ Trin 9: Verific\u00e9r ECDSA-signatur\n  const signaturGyldig = await subtle.verify(\n    { name: 'ECDSA', hash: 'SHA-256' },\n    publicKey,\n    signatur,\n    krypteretPayload\n  );\n\n  if (!signaturGyldig) {\n    throw new Error('ADVARSEL: Signatur ugyldig. Data kan v\u00e6re manipuleret!');\n  }\n  console.log('Signatur verificeret: OK');\n\n  \/\/ Trin 6: Dekrypt\u00e9r data\n  const dekrypteretBuffer = await subtle.decrypt(\n    { name: 'AES-GCM', iv },\n    krypteringsNoegle,\n    krypteretPayload\n  );\n\n  const originalData = JSON.parse(decoder.decode(dekrypteretBuffer));\n  console.log('[4\/4] Data dekrypteret og verificeret');\n  console.log('      Bruger:', originalData.bruger);\n  console.log('      Rolle:', originalData.rolle);\n  console.log('      Adgang:', originalData.adgang.join(', '));\n\n  console.log('\\n=== Demo afsluttet. Alle 12 trin gennemf\u00f8rt. ===');\n}\n\nkompletDemo().catch(console.error);<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">K\u00f8r det med <code>node index.js<\/code>. Det tager 1-2 sekunder pga. PBKDF2-beregningen, hvad der er normalt.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"algoritmer-understoettet-i-node-js-webcrypto-2026\">Algoritmer underst\u00f8ttet i Node.js WebCrypto [2026]<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Algoritme<\/th><th>Type<\/th><th>Stabil siden<\/th><th>Prim\u00e6r brug<\/th><th>N\u00f8glel\u00e6ngde<\/th><\/tr><\/thead><tbody><tr><td>AES-GCM<\/td><td>Symmetrisk kryptering<\/td><td>Node 19.0.0<\/td><td>Kryptering + autenticitet<\/td><td>128\/192\/256 bit<\/td><\/tr><tr><td>AES-CBC<\/td><td>Symmetrisk kryptering<\/td><td>Node 19.0.0<\/td><td>Kryptering (uden tag)<\/td><td>128\/192\/256 bit<\/td><\/tr><tr><td>AES-CTR<\/td><td>Symmetrisk kryptering<\/td><td>Node 19.0.0<\/td><td>Stream-kryptering<\/td><td>128\/192\/256 bit<\/td><\/tr><tr><td>RSA-OAEP<\/td><td>Asymmetrisk kryptering<\/td><td>Node 19.0.0<\/td><td>N\u00f8gleindpakning<\/td><td>2048\/4096 bit<\/td><\/tr><tr><td>RSA-PSS<\/td><td>Asymmetrisk signatur<\/td><td>Node 19.0.0<\/td><td>Signering<\/td><td>2048\/4096 bit<\/td><\/tr><tr><td>ECDSA<\/td><td>Asymmetrisk signatur<\/td><td>Node 19.0.0<\/td><td>Signering (korte n\u00f8gler)<\/td><td>P-256\/P-384\/P-521<\/td><\/tr><tr><td>ECDH<\/td><td>N\u00f8gleaftale<\/td><td>Node 19.0.0<\/td><td>Delt hemmelighed<\/td><td>P-256\/P-384\/P-521<\/td><\/tr><tr><td>Ed25519<\/td><td>Asymmetrisk signatur<\/td><td>Node 20.19.3<\/td><td>Moderne signering<\/td><td>256 bit<\/td><\/tr><tr><td>X25519<\/td><td>N\u00f8gleaftale<\/td><td>Node 20.19.3<\/td><td>Moderne DH<\/td><td>256 bit<\/td><\/tr><tr><td>PBKDF2<\/td><td>N\u00f8gleafledning<\/td><td>Node 19.0.0<\/td><td>Adgangskode til n\u00f8gle<\/td><td>Afh\u00e6nger af hash<\/td><\/tr><tr><td>HKDF<\/td><td>N\u00f8gleafledning<\/td><td>Node 19.0.0<\/td><td>N\u00f8gleudvidelse<\/td><td>Variabel<\/td><\/tr><tr><td>SHA-256\/384\/512<\/td><td>Hash<\/td><td>Node 19.0.0<\/td><td>Digest<\/td><td>256\/384\/512 bit<\/td><\/tr><tr><td>ChaCha20-Poly1305<\/td><td>Symmetrisk kryptering<\/td><td>Node 24.7.0<\/td><td>Mobiloptimeret<\/td><td>256 bit<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Ed25519 og X25519 er backporteret til Node 20.19.3 og 22.13.0. Brug <code>require('node:crypto').webcrypto<\/code> i Node.js 18.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"6-hyppige-fejl-og-faldgruber-med-webcrypto-api\">6 hyppige fejl og faldgruber med WebCrypto API<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Faldgrube 1: IV-genbrug med AES-GCM.<\/strong> Genbruger du den samme IV med den samme n\u00f8gle i AES-GCM, \u00f8del\u00e6gger du al sikkerhed fuldst\u00e6ndigt. En angriber kan XOR to ciphertexts, der er krypteret med samme n\u00f8gle og IV, og n\u00e6rme sig klarteksten. Generer <em>altid<\/em> en ny, tilf\u00e6ldig 12-byte IV for hvert krypteringsopkald med <code>getRandomValues()<\/code>. Gem IV&#8217;en sammen med ciphertext, da du skal bruge den til dekryptering.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Faldgrube 2: Direkte SHA-hashing af adgangskoder.<\/strong> <code>subtle.digest('SHA-256', password)<\/code> er <em>ikke<\/em> adgangskodehashing. SHA-256 er designet til at v\u00e6re hurtig og kan beregnes med milliarder af operationer per sekund p\u00e5 moderne GPU&#8217;er. Brug PBKDF2 med 310.000 iterationer eller <a href=\"\/da\/argon2-password-hashing-nodejs\/\">Argon2 i Node.js<\/a> til adgangskodehashing.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Faldgrube 3: Eksport af private n\u00f8gler un\u00f8digt.<\/strong> S\u00e6tter du <code>extractable: true<\/code> p\u00e5 private n\u00f8gler og eksporterer dem, giver du angribere mulighed for at stj\u00e6le n\u00f8glematerialet, hvis de f\u00e5r adgang til lagringen. S\u00e6t private n\u00f8gler til <code>extractable: false<\/code> og brug n\u00f8gleomslagning (<code>subtle.wrapKey()<\/code> med RSA-OAEP) til sikker opbevaring.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Faldgrube 4: Ignorere CryptoKey &#8220;usages&#8221;-feltet.<\/strong> Genererer du en n\u00f8gle med kun <code>['encrypt']<\/code> og fors\u00f8ger at bruge den til <code>decrypt<\/code>, kaster SubtleCrypto en <code>InvalidAccessError<\/code>. Planl\u00e6g tilladte operationer fra starten. En n\u00f8gle til kryptering og dekryptering kr\u00e6ver <code>['encrypt', 'decrypt']<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Faldgrube 5: Manglende try\/catch med forkert fejltype.<\/strong> SubtleCrypto-metoder kaster <code>DOMException<\/code>, ikke <code>Error<\/code>. Kontroll\u00e9r altid <code>error.name<\/code> i catch-blokken. Specifikke navne inkluderer: <code>DataError<\/code> (forkert n\u00f8gleformat), <code>InvalidAccessError<\/code> (forkert n\u00f8gletype), <code>NotSupportedError<\/code> (algoritme ikke underst\u00f8ttet) og <code>OperationError<\/code> (dekryptering mislykkedes, typisk fordi data er manipuleret).<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Faldgrube 6: AES-CBC til nyudvikling.<\/strong> AES-CBC krypterer ikke autentificeret og er s\u00e5rbar overfor padding oracle-angreb (fx POODLE). AES-GCM er det rette valg til al nyudvikling. Brug kun AES-CBC til kompatibilitet med \u00e6ldre systemer.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"fejlfinding-8-loesninger-til-hyppige-problemer\">Fejlfinding: 8 l\u00f8sninger til hyppige problemer<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Problem 1: <code>TypeError: Cannot read properties of undefined (reading 'subtle')<\/code><\/strong><br>Dit er Node.js under version 19. L\u00f8sning: opgrader til Node.js 20+ eller tilf\u00f8j dette \u00f8verst i filen:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ Node.js 18 kompatibilitets-shim\nconst { webcrypto } = require('node:crypto');\nconst { subtle, getRandomValues } = webcrypto;<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Problem 2: <code>DOMException [OperationError]: The operation failed<\/code> ved dekryptering.<\/strong><br>Skyldes: (a) forkert IV (du bruger en ny IV i stedet for den gemte), (b) ciphertext er \u00e6ndret, eller (c) forkert n\u00f8gle. AES-GCM opdager manipulation og returnerer fejl frem for korrupt data. Det er en sikkerhedsfunktion. Gem altid IV&#8217;en ved siden af ciphertext.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Problem 3: <code>DOMException [DataError]: Failed to execute 'importKey'<\/code>.<\/strong><br>N\u00f8gleformatet matcher ikke det faktiske format. Hvis n\u00f8glen er base64-kodet, skal du dekode til <code>ArrayBuffer<\/code> med <code>Buffer.from(key, 'base64').buffer<\/code> inden import via <code>subtle.importKey('raw', ...)<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Problem 4: <code>DOMException [InvalidAccessError]: key.usages does not permit this operation<\/code>.<\/strong><br>N\u00f8glens tilladte operationer matcher ikke den du pr\u00f8ver. Regener\u00e9r n\u00f8glen med de korrekte usages. En AES-GCM n\u00f8gle til kryptering og dekryptering kr\u00e6ver <code>['encrypt', 'decrypt']<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Problem 5: ArrayBuffer vs Buffer forvirring.<\/strong><br>WebCrypto returnerer <code>ArrayBuffer<\/code>, men Node.js-funktioner forventer <code>Buffer<\/code>. Konverter med <code>Buffer.from(arrayBuffer)<\/code> eller <code>new Uint8Array(arrayBuffer)<\/code>. For den modsatte konvertering: <code>Buffer.from(hexString, 'hex').buffer<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Problem 6: PBKDF2 er langsom i test.<\/strong><br>310.000 iterationer tager 500-2000 ms p\u00e5 en normal server-CPU. I testmilj\u00f8er, s\u00e6t iterationsantallet ned via en milj\u00f8variabel:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>const ITERATIONER = process.env.NODE_ENV === 'test' ? 1000 : 310000;<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Problem 7: <code>DOMException [NotSupportedError]<\/code> for Ed25519 p\u00e5 \u00e6ldre Node.js.<\/strong><br>Ed25519 er stabil fra Node.js 20.19.3, 22.13.0 og 23.5.0. K\u00f8r <code>node --version<\/code> og opgrad\u00e9r. Se vores <a href=\"\/da\/ed25519-signaturer-nodejs\/\">Ed25519 guide<\/a> for detaljer om versionskrav.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Problem 8: WebCrypto virker ikke i Jest.<\/strong><br>\u00c6ldre Jest-versioner med jsdom underst\u00f8tter ikke WebCrypto. S\u00e6t testmilj\u00f8et til node i <code>jest.config.js<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ jest.config.js\nmodule.exports = {\n  testEnvironment: 'node',  \/\/ Brug Node.js-milj\u00f8, ikke jsdom\n};<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"webcrypto-api-vs-nodecrypto-hvornaar-bruger-du-hvad\">WebCrypto API vs node:crypto: hvorn\u00e5r bruger du hvad?<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Funktion<\/th><th>WebCrypto API (SubtleCrypto)<\/th><th>node:crypto<\/th><\/tr><\/thead><tbody><tr><td>Portabilitet<\/td><td>Browser + Node.js + Workers + Deno<\/td><td>Kun Node.js<\/td><\/tr><tr><td>API-stil<\/td><td>Asynkron (Promise-baseret)<\/td><td>Synkron og callback-baseret<\/td><\/tr><tr><td>N\u00f8gletype<\/td><td>CryptoKey (ikke-udtr\u00e6kbar mulighed)<\/td><td>Buffer\/KeyObject<\/td><\/tr><tr><td>AES-GCM<\/td><td>Ja (SubtleCrypto)<\/td><td>Ja (createCipheriv)<\/td><\/tr><tr><td>ChaCha20<\/td><td>Ja (Node 24.7.0+)<\/td><td>Ja (alle nyere versioner)<\/td><\/tr><tr><td>PBKDF2<\/td><td>Ja (asynkront)<\/td><td>Ja (asynkront)<\/td><\/tr><tr><td>scrypt<\/td><td>Nej<\/td><td>Ja<\/td><\/tr><tr><td>Event loop blokering<\/td><td>Blokerer aldrig<\/td><td>Synkrone metoder blokerer<\/td><\/tr><tr><td>Streams<\/td><td>Ikke underst\u00f8ttet<\/td><td>Fuldt underst\u00f8ttet<\/td><\/tr><tr><td>Certifikater og X.509<\/td><td>Ikke underst\u00f8ttet<\/td><td>Underst\u00f8ttet<\/td><\/tr><tr><td>Anbefalet til<\/td><td>Portabel kode, standard-compliance<\/td><td>Streams, certifikater, legacy<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Brug WebCrypto API, n\u00e5r du skriver kode der skal k\u00f8re i browsere <em>og<\/em> p\u00e5 serveren (fx Next.js edge functions, Cloudflare Workers, Deno Deploy). Brug <code>node:crypto<\/code>, n\u00e5r du har brug for streams, X.509-certifikatmanipulation eller algoritmer som scrypt, der ikke er i W3C-specifikationen. De to API&#8217;er fungerer side om side i samme projekt.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"avancerede-teknikker-og-node-js-24-nyheder\">Avancerede teknikker og Node.js 24-nyheder<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>HKDF til n\u00f8gleudvidelse fra ECDH.<\/strong> Har du en delt hemmelighed fra ECDH og vil afkede flere n\u00f8gler fra den, er HKDF (HMAC-based Key Derivation Function, RFC 5869) det rette valg. Det tager eksisterende n\u00f8glemateriale med h\u00f8j entropi og str\u00e6kker det til den n\u00f8jagtige st\u00f8rrelse du beh\u00f8ver. Brug <code>info<\/code>-feltet til at binde n\u00f8glen til en kontekst (fx &#8216;kryptering-v1&#8217; og &#8216;autenticitet-v1&#8217;), s\u00e5 de samme ECDH-n\u00f8gler kan afkede separate kryptering- og autenticitetsn\u00f8gler uden konflikt:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ HKDF: afled 2 separate n\u00f8gler fra \u00e9n delt hemmelighed\nconst sharedBits = await subtle.deriveBits(\n  { name: 'ECDH', public: modpartOffentligNoegle },\n  minPrivatNoegle,\n  256\n);\n\nconst hkdfNoegle = await subtle.importKey(\n  'raw', sharedBits, 'HKDF', false, ['deriveKey']\n);\n\nconst krypteringsNoegle = await subtle.deriveKey(\n  {\n    name: 'HKDF',\n    hash: 'SHA-256',\n    salt: new Uint8Array(32),\n    info: new TextEncoder().encode('kryptering-v1')\n  },\n  hkdfNoegle,\n  { name: 'AES-GCM', length: 256 },\n  false,\n  ['encrypt', 'decrypt']\n);<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>ChaCha20-Poly1305 i Node.js 24.7.0.<\/strong> ChaCha20-Poly1305 er Googles foretrukne algoritme til mobilenheder, fordi den er 2-3 gange hurtigere end AES p\u00e5 processorer uden AES-NI-hardwareinstruktioner (fx \u00e6ldre ARM-chips). Node.js 24.7.0 tilf\u00f8jede underst\u00f8ttelse via WebCrypto:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ Kr\u00e6ver Node.js 24.7.0+\nconst noegle = await subtle.generateKey(\n  { name: 'ChaCha20-Poly1305', length: 256 },\n  false,\n  ['encrypt', 'decrypt']\n);\n\nconst iv = new Uint8Array(12);\nglobalThis.crypto.getRandomValues(iv);\n\nconst krypteret = await subtle.encrypt(\n  { name: 'ChaCha20-Poly1305', iv },\n  noegle,\n  encoder.encode('Mobil-optimeret kryptering')\n);<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Subresource Integrity (SRI) hash med WebCrypto.<\/strong> SHA-384-hashes til SRI-attributter p\u00e5 script- og link-tags beskytter mod CDN-kompromittering. WebCrypto genererer disse hashes nativt:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>async function genererSRIHash(filIndhold) {\n  const data = new TextEncoder().encode(filIndhold);\n  const hashBuffer = await subtle.digest('SHA-384', data);\n  const base64 = Buffer.from(hashBuffer).toString('base64');\n  return `sha384-${base64}`;\n}\n\nconst sriHash = await genererSRIHash(scriptIndhold);\n\/\/ Output: sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K\/...\n\/\/ Brug i HTML: <script src=\"...\" integrity=\"sha384-...\"><\/script><\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">For autentificering med asymmetriske n\u00f8gler i en moderne webapplikation, se vores guide til <a href=\"\/da\/oauth2-openid-connect-nodejs\/\">OAuth 2.0 og OpenID Connect i Node.js<\/a>, der bruger lignende kryptografiske m\u00f8nstre til token-signering og -verifikation.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"noeglestyring-og-sikkerhedsarkitektur-i-produktion\">N\u00f8glestyring og sikkerhedsarkitektur i produktion<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">WebCrypto API l\u00f8ser kryptografien, men n\u00f8gleh\u00e5ndtering er et separat problem. En st\u00e6rk algoritme er ubrugelig, hvis n\u00f8glen opbevares usikkert. Her er de vigtigste principper for produktionssystemer.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>N\u00f8glehierarkier og Key Encryption Keys (KEK).<\/strong> I stedet for at kryptere data direkte med en adgangskode-afledt n\u00f8gle, brug et to-niveaus hierarki: en &#8220;Data Encryption Key&#8221; (DEK) krypterer dataene, og en &#8220;Key Encryption Key&#8221; (KEK) krypterer DEK&#8217;en. Det giver dig mulighed for at rotere DEK&#8217;er uden at rekryptere alle data og at bruge hardware-sikkerhedsmoduler (HSM) til at beskytte KEK&#8217;en. Med WebCrypto API implementeres det via <code>subtle.wrapKey()<\/code> og <code>subtle.unwrapKey()<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ N\u00f8gleindpakning: krypt\u00e9r DEK med KEK via RSA-OAEP\nasync function indpakNoegle(dek, kek) {\n  \/\/ subtle.wrapKey() krypterer en CryptoKey med en anden n\u00f8gle\n  const indpakketDEK = await subtle.wrapKey(\n    'raw',         \/\/ Eksportformat for DEK\n    dek,           \/\/ Data Encryption Key der skal beskyttes\n    kek,           \/\/ Key Encryption Key (fx RSA-OAEP offentlig n\u00f8gle)\n    { name: 'RSA-OAEP' }\n  );\n  return indpakketDEK;\n}\n\nasync function udpakNoegle(indpakketDEK, kek) {\n  \/\/ subtle.unwrapKey() dekrypterer og importerer DEK i \u00e9t trin\n  const dek = await subtle.unwrapKey(\n    'raw',\n    indpakketDEK,\n    kek,           \/\/ RSA-OAEP privat n\u00f8gle\n    { name: 'RSA-OAEP' },\n    { name: 'AES-GCM', length: 256 },\n    false,         \/\/ Den udpakkede DEK er ikke-eksporterbar\n    ['encrypt', 'decrypt']\n  );\n  return dek;\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>N\u00f8glerotation.<\/strong> Krypteringsn\u00f8gler b\u00f8r roteres regelm\u00e6ssigt. Jo kortere n\u00f8glens levetid, jo mindre data kan en kompromitteret n\u00f8gle afsl\u00f8re. En typisk produktionsarkitektur bruger nye DEK&#8217;er pr. sessioner eller pr. bruger og roterer KEK&#8217;er hvert 90 dage. WebCrypto&#8217;s ikke-eksporterbare n\u00f8gler hj\u00e6lper med at h\u00e5ndh\u00e6ve politikken, da du ikke ved et uheld kan gemme dem i plaintext.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Tilf\u00e6ldighedskilder og entropiproblem.<\/strong> <code>getRandomValues()<\/code> er kryptografisk sikker p\u00e5 alle underst\u00f8ttede platforme. Cloudflare Workers og Node.js bruger begge OS-niveau CSPRNG. Dog er der \u00e9n f\u00e6lde: ved serveropstart umiddelbart efter boot kan entropipuljen p\u00e5 Linux-systemer v\u00e6re lav. Moderne Linux-kernels (5.18+) og Node.js 20+ h\u00e5ndterer dette korrekt, men p\u00e5 containeriserede milj\u00f8er i virtuelle maskiner b\u00f8r du konfigurere <code>\/dev\/urandom<\/code>-entropi korrekt via <code>virtio-rng<\/code> eller <code>haveged<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Tidsbaserede angreb og konstant-tidsammenligninger.<\/strong> Sammenligner du kryptografiske v\u00e6rdier med <code>===<\/code> i JavaScript, afsl\u00f8rer du information via timing. En angriber kan m\u00e5le, hvor hurtigt sammenligningen fejler, og slutte sig til de korrekte bytes \u00e9n ad gangen. Node.js&#8217;s <code>crypto.timingSafeEqual()<\/code> fra <code>node:crypto<\/code>-modulet l\u00f8ser dette for buffersammenligninger. WebCrypto API h\u00e5ndterer det internt for <code>subtle.verify()<\/code>, men husk dette, hvis du sammenligner andre kryptografiske v\u00e6rdier manuelt.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Content-Security-Policy og WebCrypto i browseren.<\/strong> WebCrypto API er tilg\u00e6ngeligt i alle moderne browsere, men er begr\u00e6nset til HTTPS-kontekster. P\u00e5 HTTP-sider er <code>window.crypto.subtle<\/code> ikke tilg\u00e6ngeligt og returnerer <code>undefined<\/code>. I dit produktionsmilj\u00f8 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\u00e6lder HTTPS-kravet ikke, da der ikke er en browser involveret.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Milj\u00f8specifikke overvejelser.<\/strong> P\u00e5 Cloudflare Workers er WebCrypto synkront cached p\u00e5 tv\u00e6rs af requests, men n\u00f8gler kan ikke deles p\u00e5 tv\u00e6rs af Worker-instanser. P\u00e5 Next.js edge runtime g\u00e6lder samme begr\u00e6nsninger. Generer altid request-specifikke IV&#8217;er og DEK&#8217;er, og undg\u00e5 at holde krypteringsn\u00f8gler i global mutable state, da det kan give race conditions i concurrent request-h\u00e5ndtering.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For en dybdeg\u00e5ende gennemgang af autentificeringsprotokoller der bruger kryptografiske n\u00f8gler i produktionssystemer, se vores guide til <a href=\"\/da\/webauthn-nodejs-passkeys\/\">WebAuthn passwordless login i Node.js<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"hyppigt-stillede-spoergsmaal\">Hyppigt stillede sp\u00f8rgsm\u00e5l<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Hvad er forskellen p\u00e5 WebCrypto API og node:crypto?<\/strong><br>WebCrypto (<code>globalThis.crypto.subtle<\/code>) er en W3C-standardiseret, asynkron API der virker i browsere, Node.js, Deno og Cloudflare Workers. <code>node:crypto<\/code> er Node.js-specifik, delvis synkron og bygger direkte p\u00e5 OpenSSL. Brug WebCrypto til portabel kode og <code>node:crypto<\/code> til Node.js-specifikke behov som streams og X.509-certifikater.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Er WebCrypto API hurtigere end node:crypto?<\/strong><br>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 <code>node:crypto<\/code>-metoder som <code>createHash().update().digest()<\/code> er synkrone og kan blokere ved store datam\u00e6ngder.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Kan jeg bruge WebCrypto API i Node.js 18?<\/strong><br>Ja. P\u00e5 Node.js 18 er WebCrypto tilg\u00e6ngeligt via <code>require('node:crypto').webcrypto<\/code>. Du kan aktivere det globalt med flaget <code>--experimental-global-webcrypto<\/code>. Fra Node.js 19.0.0 er det globalt tilg\u00e6ngeligt pr. standard uden flaget.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Hvorfor bruger AES-GCM en 12-byte IV og ikke 16 byte?<\/strong><br>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\u00f8gle og kr\u00e6ver ingen ekstra beregningsskridt. En 16-byte IV kr\u00e6ver en ekstra GHASH-operation. Brug altid 12 bytes for bedste sikkerhed og ydeevne.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Hvorn\u00e5r bruger jeg PBKDF2 vs HKDF?<\/strong><br>PBKDF2 er til adgangskoder: det tager brugerinput med lav entropi og g\u00f8r det beregningsintensivt at brutforce. HKDF er til n\u00f8glemateriale med h\u00f8j entropi (fx output fra ECDH): det str\u00e6kker og separerer eksisterende n\u00f8glemateriale til multiple n\u00f8gler. Brug aldrig HKDF til adgangskoder og aldrig PBKDF2 til h\u00f8j-entropi input.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Er Ed25519 bedre end ECDSA P-256?<\/strong><br>Ed25519 har lavere latency (deterministisk signering kr\u00e6ver ingen tilf\u00e6ldighedskilde under signeringsprocessen), er resistent mod visse side-channel angreb og producerer konsistente 64-byte signaturer. ECDSA P-256 er bredere underst\u00f8ttet i eksisterende systemer. Til nyt udviklingsarbejde i Node.js 22+ anbefales Ed25519. Se vores <a href=\"\/da\/ed25519-signaturer-nodejs\/\">Ed25519 guide<\/a>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Virker WebCrypto i Cloudflare Workers og Next.js Edge Functions?<\/strong><br>Ja. <code>globalThis.crypto<\/code> er tilg\u00e6ngeligt i Cloudflare Workers, Vercel Edge Functions, Next.js edge runtime og Deno Deploy. Det er kerneform\u00e5let med WebCrypto-standarden: skriv \u00e9t s\u00e6t kryptografisk kode og k\u00f8r det p\u00e5 tv\u00e6rs af Node.js, browsere og edge-platforme. De implementerer alle den samme W3C SubtleCrypto-specifikation.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Kan WebCrypto bruges til at generere X.509-certifikater?<\/strong><br>Ikke direkte. WebCrypto kan generere n\u00f8glepars og signere DER-kodet certifikatindhold, men selve X.509-strukturen og ASN.1-kodningen kr\u00e6ver <code>node:crypto<\/code> med <code>X509Certificate<\/code>-klassen, eller et bibliotek som <code>@peculiar\/x509<\/code>, der bygger oven p\u00e5 WebCrypto.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"relateret-indhold\">Relateret indhold<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><a href=\"\/da\/ed25519-signaturer-nodejs\/\">Ed25519 i Node.js: signaturer i 12 trin [2026]<\/a><\/li>\n<li><a href=\"\/da\/hmac-webhook-signaturer-nodejs\/\">HMAC i Node.js: webhook-signaturer i 12 trin [2026]<\/a><\/li>\n<li><a href=\"\/da\/oauth2-openid-connect-nodejs\/\">OAuth 2.0 og OpenID Connect i Node.js: 12 trin p\u00e5 30 min [2026]<\/a><\/li>\n<li><a href=\"\/da\/webauthn-nodejs-passkeys\/\">WebAuthn i Node.js: Passwordless login i 12 trin [2026]<\/a><\/li>\n<li><a href=\"\/da\/sql-injection-nodejs\/\">SQL Injection i Node.js: 12 trin til sikker database [2026]<\/a><\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Ekstern dokumentation:<\/strong> <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/API\/Web_Crypto_API\" rel=\"noopener noreferrer\" target=\"_blank\">MDN Web Crypto API-dokumentation<\/a>, <a href=\"https:\/\/nodejs.org\/api\/webcrypto.html\" rel=\"noopener noreferrer\" target=\"_blank\">Node.js WebCrypto API officiel dokumentation<\/a>, <a href=\"https:\/\/www.w3.org\/TR\/WebCryptoAPI\/\" rel=\"noopener noreferrer\" target=\"_blank\">W3C Web Cryptography API-specifikation<\/a>, <a href=\"https:\/\/cheatsheetseries.owasp.org\/cheatsheets\/Password_Storage_Cheat_Sheet.html\" rel=\"noopener noreferrer\" target=\"_blank\">OWASP Password Storage Cheat Sheet<\/a>, <a href=\"https:\/\/csrc.nist.gov\/publications\/detail\/sp\/800-38d\/final\" rel=\"noopener noreferrer\" target=\"_blank\">NIST SP 800-38D (AES-GCM specifikation)<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>WebCrypto API er W3C-standarden for kryptografi i JavaScript, og siden Node.js 19.0.0 er den tilg\u00e6ngelig globalt via globalThis.crypto uden import. Det giver dig AES-GCM-kryptering, ECDSA-signaturer, PBKDF2-n\u00f8gleafledning og meget mere med\u2026<\/p>\n","protected":false},"author":8,"featured_media":152,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[2],"tags":[],"class_list":["post-151","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-cryptography"],"_links":{"self":[{"href":"https:\/\/shattered.io\/dk\/wp-json\/wp\/v2\/posts\/151","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/shattered.io\/dk\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/shattered.io\/dk\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/shattered.io\/dk\/wp-json\/wp\/v2\/users\/8"}],"replies":[{"embeddable":true,"href":"https:\/\/shattered.io\/dk\/wp-json\/wp\/v2\/comments?post=151"}],"version-history":[{"count":1,"href":"https:\/\/shattered.io\/dk\/wp-json\/wp\/v2\/posts\/151\/revisions"}],"predecessor-version":[{"id":153,"href":"https:\/\/shattered.io\/dk\/wp-json\/wp\/v2\/posts\/151\/revisions\/153"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/shattered.io\/dk\/wp-json\/wp\/v2\/media\/152"}],"wp:attachment":[{"href":"https:\/\/shattered.io\/dk\/wp-json\/wp\/v2\/media?parent=151"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/shattered.io\/dk\/wp-json\/wp\/v2\/categories?post=151"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/shattered.io\/dk\/wp-json\/wp\/v2\/tags?post=151"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}