{"id":154,"date":"2026-06-20T20:46:09","date_gmt":"2026-06-20T20:46:09","guid":{"rendered":"https:\/\/shattered.io\/dk\/2026\/06\/20\/scrypt-password-hashing-nodejs\/"},"modified":"2026-06-20T20:47:55","modified_gmt":"2026-06-20T20:47:55","slug":"scrypt-password-hashing-nodejs","status":"publish","type":"post","link":"https:\/\/shattered.io\/dk\/scrypt-password-hashing-nodejs\/","title":{"rendered":"Scrypt i Node.js: sikker password-hashing i 12 trin [2026]"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Scrypt er den mest hukommelseskr\u00e6vende standard for password-hashing, og Node.js har underst\u00f8ttet den via det indbyggede <code>node:crypto<\/code>-modul siden version 10.5.0. Algoritmen blev designet til at g\u00f8re GPU- og ASIC-angreb 10-100 gange dyrere end bcrypt, fordi den kr\u00e6ver op til 128 MB RAM per hash. Denne guide viser dig pr\u00e6cis, hvordan du implementerer scrypt korrekt i Node.js, trin for trin, fra installation til produktionsklar kode med Express, PostgreSQL og en komplet testpakke.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"hvad-er-scrypt-og-hvorfor-bruge-det-i-2026\">Hvad er scrypt, og hvorfor bruge det i 2026?<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Scrypt er en password-baseret n\u00f8gleafledningsfunktion (PBKDF), designet af Colin Percival og offentliggjort i 2009. Den er standardiseret i <a href=\"https:\/\/www.rfc-editor.org\/rfc\/rfc7914\" target=\"_blank\" rel=\"noopener\">RFC 7914<\/a> og er i dag brugt i Litecoin (proof-of-work), WPA3-netv\u00e6rk (Wi-Fi-n\u00f8gleafledning) og adskillige open source password-managere. Tricket er, at scrypt ikke bare er CPU-tung, den er <strong>hukommelsestung<\/strong>: for at beregne \u00e9n hash skal algoritmen allokere en stor m\u00e6ngde RAM og tilg\u00e5 den i et pseudo-tilf\u00e6ldigt m\u00f8nster, der er sv\u00e6rt at parallelisere p\u00e5 GPU&#8217;er og specialiserede ASIC-chips.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Klassiske hashfunktioner som MD5 eller SHA-256 kan beregnes af tusindvis af GPU-kerner parallelt, fordi de kr\u00e6ver minimal RAM. bcrypt tilf\u00f8jede CPU-belastning via en cost-faktor, men er stadig rimeligt effektiv p\u00e5 GPU: en moderne RTX 4090 kan cracke op til 10.000 bcrypt-hashes per sekund med cost=10. Scrypt tilf\u00f8jer den tredje dimension, hukommelsesb\u00e5ndbredde: den samme GPU kan kun beregne 50-200 scrypt-hashes per sekund med N=32.768, fordi hvert hash kr\u00e6ver 32 MB RAM der skal l\u00e6ses og skrives i et ikke-paralleliserbart m\u00f8nster. Det g\u00f8r offline dictionary-angreb 50-200 gange dyrere end med bcrypt.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">OWASP anbefaler i sin <a href=\"https:\/\/cheatsheetseries.owasp.org\/cheatsheets\/Password_Storage_Cheat_Sheet.html\" target=\"_blank\" rel=\"noopener\">Password Storage Cheat Sheet<\/a> scrypt som et legitimt alternativ til Argon2id, med minimum-parametrene N=32.768, r=8, p=1. Argon2id er foretrukket, n\u00e5r platformen underst\u00f8tter det, men scrypt er det rigtige valg, n\u00e5r du arbejder direkte med Node.js&#8217; built-in kryptografi uden tredjeparts afh\u00e6ngigheder. Det er s\u00e6rlig relevant i 2026, hvor containere og serverless-funktioner s\u00e6tter strenge begr\u00e6nsninger p\u00e5 native native C++-kompilering.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Scrypt bruger tre internt sammenkoblede faser: en PBKDF2-SHA256-fase der afkomprimerer passwordet til en blok, en ROMix-fase der udfylder RAM med pseudo-tilf\u00e6ldige data og derefter l\u00e6ser dem i afh\u00e6ngig r\u00e6kkef\u00f8lge, og endelig en ny PBKDF2-SHA256-fase der komprimerer resultatet til outputn\u00f8glen. ROMix-fasen er kernen i den hukommelseshardhed: dens RAM-adgangsm\u00f8nster er designet til at maksimere cache-fejlhit, s\u00e5 en angriber ikke kan reducere RAM-behovet uden dramatisk at \u00f8ge beregningsomkostningen. Det er i mods\u00e6tning til bcrypt, der kun kr\u00e6ver 4 KB arbejdshukommelse og er effektiv p\u00e5 GPU&#8217;er med mange sm\u00e5 kerner.<\/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\">Scrypt er tilg\u00e6ngeligt i Node.js&#8217; standard <code>node:crypto<\/code>-modul uden nogen npm-installation. Du beh\u00f8ver ingen ekstra pakker til basis-implementationen. De eneste afh\u00e6ngigheder i dette projekt er Express (webserver) og express-rate-limit (sikkerhed). Tjek f\u00f8lgende tabel, inden du begynder:<\/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>10.5.0<\/td><td>22.x LTS<\/td><td><code>crypto.scrypt()<\/code> og <code>crypto.scryptSync()<\/code> tilg\u00e6ngeligt<\/td><\/tr><tr><td>npm<\/td><td>6.x<\/td><td>10.x<\/td><td>Kun til Express-eksemplet<\/td><\/tr><tr><td>RAM p\u00e5 server<\/td><td>256 MB<\/td><td>2 GB+<\/td><td>N=32.768 kr\u00e6ver ~32 MB per hash; 10 samtidige login = 320 MB<\/td><\/tr><tr><td>CPU-kerner<\/td><td>1<\/td><td>4+<\/td><td>Scrypt er enkelt-tr\u00e5det; Worker Threads anbefales til h\u00f8j trafik<\/td><\/tr><tr><td>Operativsystem<\/td><td>Linux\/macOS\/Windows<\/td><td>Ubuntu 22.04+<\/td><td>Alle platforme underst\u00f8ttet via OpenSSL<\/td><\/tr><tr><td>JavaScript-kendskab<\/td><td>ES2017 (async\/await)<\/td><td>ES2022+<\/td><td>Scrypt-API&#8217;et er callback-baseret men wrappes nemt<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Verifik\u00e9r din Node.js-version og at scrypt er tilg\u00e6ngeligt:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>node --version\n# Forventet: v22.x.x eller nyere\n\nnode -e \"const { scrypt, scryptSync } = require('node:crypto'); console.log('scrypt:', typeof scrypt, '| scryptSync:', typeof scryptSync);\"\n# Forventet: scrypt: function | scryptSync: function<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Hvis du k\u00f8rer Node.js 10.5.0-11.x, er scrypt tilg\u00e6ngeligt men <code>crypto.scryptSync()<\/code> var ikke stabil f\u00f8r Node.js 12.0.0. Til alle produktionssystemer anbefales Node.js 20 LTS eller 22 LTS.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"trin-1-projektopsaetning-og-mappestruktur\">Trin 1: Projektops\u00e6tning og mappestruktur<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Opret projektmappen og initialiser npm. Strukturen er enkel: \u00e9n fil til hashing-logikken, \u00e9n til brugerdatabasen og \u00e9n til Express-serveren. Det g\u00f8r koden let at teste isoleret.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>mkdir scrypt-demo && cd scrypt-demo\nnpm init -y\nnpm install express express-rate-limit dotenv\nmkdir -p src<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Mappestrukturen:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>scrypt-demo\/\n\u251c\u2500\u2500 src\/\n\u2502   \u251c\u2500\u2500 hash.js          # Scrypt-hj\u00e6lpefunktioner\n\u2502   \u251c\u2500\u2500 server.js        # Express API-server\n\u2502   \u251c\u2500\u2500 users.js         # In-memory brugerstore (erstat med DB)\n\u2502   \u2514\u2500\u2500 benchmark.js     # Benchmarking-script til parametervalg\n\u251c\u2500\u2500 package.json\n\u2514\u2500\u2500 .env<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Opret <code>.env<\/code>-filen. Disse parametre er centerale for sikkerheden og b\u00f8r aldrig hardcodes direkte i koden:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># .env\nPORT=3000\n\n# Scrypt-parametre - tilpas N til din servers RAM og CPU\n# N=32768: ~32 MB RAM, ~200-300 ms per hash (OWASP-minimum)\n# N=65536: ~64 MB RAM, ~400-600 ms per hash (anbefalet til nye systemer)\nSCRYPT_N=32768\nSCRYPT_R=8\nSCRYPT_P=1\nSCRYPT_KEYLEN=64\nSCRYPT_SALT_BYTES=32<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Tilf\u00f8j en startscript-sektion til <code>package.json<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n  \"name\": \"scrypt-demo\",\n  \"version\": \"1.0.0\",\n  \"scripts\": {\n    \"start\": \"node -r dotenv\/config src\/server.js\",\n    \"dev\": \"node --watch -r dotenv\/config src\/server.js\",\n    \"benchmark\": \"node -r dotenv\/config src\/benchmark.js\",\n    \"test\": \"node --test src\/hash.test.js\"\n  },\n  \"dependencies\": {\n    \"dotenv\": \"^16.4.0\",\n    \"express\": \"^4.21.0\",\n    \"express-rate-limit\": \"^7.4.0\"\n  },\n  \"engines\": {\n    \"node\": \">=20.0.0\"\n  }\n}<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"trin-2-forstaa-n-r-og-p-parametrene\">Trin 2: Forst\u00e5 N, r og p-parametrene<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Scrypt har tre arbejdsfaktorer, og det er kritisk at forst\u00e5 dem, inden du v\u00e6lger v\u00e6rdier til produktion. En forkert konfiguration kan enten efterlade systemet s\u00e5rbart over for angreb eller crashe serveren med out-of-memory-fejl.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Parameter<\/th><th>Navn<\/th><th>Effekt<\/th><th>OWASP minimum 2025<\/th><th>Anbefalet 2026<\/th><\/tr><\/thead><tbody><tr><td><strong>N<\/strong><\/td><td>CPU\/hukommelsesomkostning<\/td><td>Potens af 2. Fordobling af N fordobler CPU-tid og RAM-forbrug line\u00e6rt<\/td><td>32.768 (2<sup>15<\/sup>)<\/td><td>65.536 (2<sup>16<\/sup>) for nye systemer<\/td><\/tr><tr><td><strong>r<\/strong><\/td><td>Blokst\u00f8rrelse<\/td><td>Blokmixer-st\u00f8rrelse. RAM = 128 \u00d7 N \u00d7 r bytes. \u00d8gning \u00f8ger RAM proportionalt<\/td><td>8<\/td><td>8 (\u00e6ndres sj\u00e6ldent)<\/td><\/tr><tr><td><strong>p<\/strong><\/td><td>Paralleliseringsfaktor<\/td><td>Multiplicerer CPU-belastningen sekventielt. \u00d8ger ikke hukommelse<\/td><td>1<\/td><td>1<\/td><\/tr><tr><td><strong>keylen<\/strong><\/td><td>Outputl\u00e6ngde<\/td><td>L\u00e6ngden p\u00e5 den afledte n\u00f8gle i bytes. Gemmes som hex<\/td><td>32 bytes (256 bit)<\/td><td>64 bytes (512 bit)<\/td><\/tr><tr><td><strong>salt<\/strong><\/td><td>Tilf\u00e6ldig salt<\/td><td>Unik per hash, forhindrer rainbow-table-angreb og ligner hashes<\/td><td>16 bytes tilf\u00e6ldig<\/td><td>32 bytes tilf\u00e6ldig<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>RAM-formlen er afg\u00f8rende for dimensionering:<\/strong> RAM-forbrug = 128 \u00d7 N \u00d7 r bytes. Med N=32.768 og r=8 bruger \u00e9n scrypt-operation 128 \u00d7 32.768 \u00d7 8 = <strong>33.554.432 bytes = 32 MB RAM<\/strong>. Med N=65.536 bruger den 64 MB. Med N=131.072 (2^17) bruger den 128 MB. Plan\u00e9r for antallet af samtidige login-fors\u00f8g din server skal h\u00e5ndtere: 20 samtidige logins med N=32.768 kr\u00e6ver 640 MB RAM kun til hashing, og det er eksklusiv \u00f8vrig serverhukommelse.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Parameteren <strong>N skal altid v\u00e6re en potens af 2<\/strong>. V\u00e6rdier som N=30.000 eller N=50.000 er ugyldige og udl\u00f8ser en <code>RangeError<\/code>. Node.js validerer ikke altid dette ved opstart, men fejlen viser sig ved den f\u00f8rste hash-operation. Valider N-v\u00e6rdien programmatisk ved serveropstart med bitvis AND-tjek: <code>(N & (N - 1)) === 0<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Parameteren <strong>maxmem<\/strong> er Node.js-specifik og s\u00e6tter et loft for, hvor meget RAM scrypt-operationen m\u00e5 bruge. Standard er 32 MB. Det er for lavt til N=65.536 og derover, og du skal s\u00e6tte den eksplicit. Beregn den som <code>128 * N * r * 2<\/code> for et sikkert buffer.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"trin-3-hash-js-med-asynkron-scrypt\">Trin 3: Hash.js med asynkron scrypt<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Node.js&#8217; <code>crypto.scrypt()<\/code> bruger callback-modellen fra \u00e6ldre Node.js-API&#8217;er. Til moderne async\/await-kode wrapper du den med <code>promisify<\/code> fra <code>node:util<\/code>. Det er den anbefalede metode frem for at skrive en manuel Promise-wrapper, fordi <code>promisify<\/code> korrekt h\u00e5ndterer fejltilf\u00e6lde og bevarer den originale kontekst.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>'use strict';\n\nconst { scrypt, randomBytes, timingSafeEqual } = require('node:crypto');\nconst { promisify } = require('node:util');\n\nconst scryptAsync = promisify(scrypt);\n\n\/\/ Hent parametre fra milj\u00f8variabler - valider dem ved opstart\nconst N = parseInt(process.env.SCRYPT_N || '32768', 10);\nconst r = parseInt(process.env.SCRYPT_R || '8', 10);\nconst p = parseInt(process.env.SCRYPT_P || '1', 10);\nconst KEYLEN = parseInt(process.env.SCRYPT_KEYLEN || '64', 10);\nconst SALT_BYTES = parseInt(process.env.SCRYPT_SALT_BYTES || '32', 10);\n\n\/\/ Valider N ved opstart\nif (!Number.isInteger(N) || N < 16384 || (N &#038; (N - 1)) !== 0) {\n  throw new Error(`Ugyldig SCRYPT_N=${N}. Kr\u00e6ver potens af 2 og min. 16384.`);\n}\n\nconst SCRYPT_PARAMS = {\n  N,\n  r,\n  p,\n  \/\/ maxmem skal s\u00e6ttes eksplicit ved h\u00f8je N-v\u00e6rdier\n  maxmem: 128 * N * r * 2,\n};\n\n\/**\n * Hash et password med scrypt.\n * Returnerer en streng i formatet: salt_hex:hash_hex\n *\/\nasync function hashPassword(password) {\n  if (typeof password !== 'string' || password.length === 0) {\n    throw new TypeError('Password skal v\u00e6re en ikke-tom streng');\n  }\n\n  const salt = randomBytes(SALT_BYTES);\n  const derivedKey = await scryptAsync(password, salt, KEYLEN, SCRYPT_PARAMS);\n\n  return `${salt.toString('hex')}:${derivedKey.toString('hex')}`;\n}\n\n\/**\n * Verificer et password mod et gemt hash.\n * Bruger timingSafeEqual til at undg\u00e5 timing-angreb.\n *\/\nasync function verifyPassword(password, storedHash) {\n  if (typeof password !== 'string' || typeof storedHash !== 'string') {\n    throw new TypeError('Ugyldige argumenttyper til verifikation');\n  }\n\n  const colonIndex = storedHash.indexOf(':');\n  if (colonIndex === -1) {\n    throw new Error('Ugyldig hash-format: mangler kolon-separator');\n  }\n\n  const saltHex = storedHash.slice(0, colonIndex);\n  const hashHex = storedHash.slice(colonIndex + 1);\n  const salt = Buffer.from(saltHex, 'hex');\n  const storedKeyBuffer = Buffer.from(hashHex, 'hex');\n\n  const derivedKey = await scryptAsync(password, salt, KEYLEN, SCRYPT_PARAMS);\n\n  \/\/ timingSafeEqual er obligatorisk - aldrig === eller Buffer.equals()\n  if (derivedKey.length !== storedKeyBuffer.length) return false;\n  return timingSafeEqual(derivedKey, storedKeyBuffer);\n}\n\nmodule.exports = { hashPassword, verifyPassword, SCRYPT_PARAMS };<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Tre sikkerhedsbeslutninger i koden fortjener forklaring. For det f\u00f8rste bruger vi <code>randomBytes(SALT_BYTES)<\/code> og ikke <code>Math.random()<\/code> eller UUID: <code>Math.random()<\/code> er ikke kryptografisk sikker og kan forudsiges. For det andet bruger vi <code>colonIndex = storedHash.indexOf(':')<\/code> og ikke <code>storedHash.split(':')<\/code>: hvis hashen indeholder et kolon i hex-kodningen (usandsynligt men muligt i fremtidige formater), splitter <code>.split(':')<\/code> forkert. For det tredje kontrollerer vi <code>derivedKey.length !== storedKeyBuffer.length<\/code> inden <code>timingSafeEqual<\/code>, fordi <code>timingSafeEqual<\/code> kaster en fejl hvis bufferne har forskellig st\u00f8rrelse.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"trin-4-synkron-scrypt-og-hvornaar-du-bruger-den\">Trin 4: Synkron scrypt og hvorn\u00e5r du bruger den<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Node.js tilbyder <code>crypto.scryptSync()<\/code>, men den blokerer event loop under beregningen. Med N=32.768 varer det 200-400 ms, i hvilken tid Node.js ikke kan betjene nogen andre requests, timers eller I\/O-operationer. Det g\u00e6lder hele processen, ikke kun den aktuelle request.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Acceptabel brug af <code>scryptSync()<\/code> er begr\u00e6nset til: CLI-scripts der k\u00f8rer \u00e9n hash ad gangen, database-migreringsscripts der k\u00f8rer udenfor \u00e5bningstid, og test-setup der skaber brugerkonti inden testene k\u00f8rer. Brug aldrig <code>scryptSync()<\/code> i request-handlers, middleware, eller nogen kode der k\u00f8rer i en server med samtidige brugere.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>'use strict';\n\n\/\/ Kun til CLI-scripts - ALDRIG i produktions-request-handlers\nconst { scryptSync, randomBytes, timingSafeEqual } = require('node:crypto');\n\nfunction hashPasswordSync(password) {\n  const salt = randomBytes(32);\n  const key = scryptSync(password, salt, 64, {\n    N: 32768,\n    r: 8,\n    p: 1,\n    maxmem: 128 * 1024 * 1024, \/\/ 128 MB buffer\n  });\n  return `${salt.toString('hex')}:${key.toString('hex')}`;\n}\n\nfunction verifyPasswordSync(password, storedHash) {\n  const colonIndex = storedHash.indexOf(':');\n  const salt = Buffer.from(storedHash.slice(0, colonIndex), 'hex');\n  const stored = Buffer.from(storedHash.slice(colonIndex + 1), 'hex');\n  const derived = scryptSync(password, salt, 64, {\n    N: 32768, r: 8, p: 1, maxmem: 128 * 1024 * 1024,\n  });\n  return timingSafeEqual(derived, stored);\n}\n\n\/\/ Eksempel: admin-CLI der opretter en initialkonto\nconst adminPassword = process.argv[2];\nif (!adminPassword) {\n  console.error('Brug: node create-admin.js <password>');\n  process.exit(1);\n}\nconst hash = hashPasswordSync(adminPassword);\nconsole.log('Admin password hash (gem i .env eller database):');\nconsole.log(hash);\n\/\/ Output (k\u00f8rer i ~200 ms):\n\/\/ Admin password hash:\n\/\/ a3f8b2c1d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1:\n\/\/ b2c3d4e5f6a7b8c9d0e1f2a3b4c5...d8e9f0a1<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"trin-5-in-memory-brugerstore\">Trin 5: In-memory brugerstore<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">I et produkt vil du bruge PostgreSQL, MySQL eller MongoDB, men til demonstration opretter vi en simpel in-memory Map. Strukturen inkluderer login-t\u00e6ller og lockout-tidsstempel til brute-force-beskyttelse. Erstat dette med en rigtig databaseklasse i trin 10.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>'use strict';\n\n\/\/ In-memory brugerstore - erstat med rigtig database i produktion\nconst users = new Map();\n\nfunction createUser(username, hashedPassword) {\n  if (users.has(username)) {\n    throw new Error('Bruger eksisterer allerede');\n  }\n  users.set(username, {\n    username,\n    password: hashedPassword,\n    createdAt: new Date().toISOString(),\n    loginAttempts: 0,\n    lockedUntil: null,\n    lastLogin: null,\n  });\n  return { username };\n}\n\nfunction getUser(username) {\n  return users.get(username) || null;\n}\n\nfunction incrementLoginAttempts(username) {\n  const user = users.get(username);\n  if (!user) return;\n  user.loginAttempts += 1;\n  \/\/ L\u00e5s kontoen i 15 minutter efter 5 fejlede fors\u00f8g\n  if (user.loginAttempts >= 5) {\n    user.lockedUntil = new Date(Date.now() + 15 * 60 * 1000).toISOString();\n    console.warn(`Konto l\u00e5st: ${username} (${user.loginAttempts} fejlede fors\u00f8g)`);\n  }\n  users.set(username, user);\n}\n\nfunction resetLoginAttempts(username) {\n  const user = users.get(username);\n  if (!user) return;\n  user.loginAttempts = 0;\n  user.lockedUntil = null;\n  user.lastLogin = new Date().toISOString();\n  users.set(username, user);\n}\n\nmodule.exports = { createUser, getUser, incrementLoginAttempts, resetLoginAttempts };<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"trin-6-express-api-server-med-sikkerhedslag\">Trin 6: Express API-server med sikkerhedslag<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Serveren implementerer tre sikkerhedslag: IP-baseret rate limiting via <code>express-rate-limit<\/code>, kontobaseret lockout i brugerstoren og konstante fejlbeskeder der ikke afsl\u00f8rer om en bruger eksisterer. Det er n\u00f8glen til at forhindre to typer angreb: brute force og bruger-enumeration.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>'use strict';\n\nrequire('dotenv').config();\nconst express = require('express');\nconst { rateLimit } = require('express-rate-limit');\nconst { hashPassword, verifyPassword } = require('.\/hash');\nconst { createUser, getUser, incrementLoginAttempts, resetLoginAttempts } = require('.\/users');\n\nconst app = express();\napp.use(express.json({ limit: '10kb' })); \/\/ Begr\u00e6ns request-st\u00f8rrelse\n\n\/\/ Striks rate limiting for login-endpoint\nconst loginLimiter = rateLimit({\n  windowMs: 15 * 60 * 1000, \/\/ 15 minutter\n  limit: 10,\n  standardHeaders: 'draft-7',\n  legacyHeaders: false,\n  handler: (req, res) => {\n    res.status(429).json({\n      error: 'For mange login-fors\u00f8g. Pr\u00f8v igen om 15 minutter.',\n    });\n  },\n});\n\n\/\/ POST \/register - Opret ny bruger\napp.post('\/register', async (req, res) => {\n  const { username, password } = req.body || {};\n\n  if (!username || typeof username !== 'string' || username.length < 3) {\n    return res.status(400).json({ error: 'Brugernavn skal v\u00e6re mindst 3 tegn' });\n  }\n\n  if (!password || typeof password !== 'string' || password.length < 12) {\n    return res.status(400).json({ error: 'Password skal v\u00e6re mindst 12 tegn' });\n  }\n\n  \/\/ Saniter brugernavn - kun alfanumeriske tegn og understregning\n  if (!\/^[a-zA-Z0-9_\u00e6\u00f8\u00e5\u00c6\u00d8\u00c5]{3,50}$\/.test(username)) {\n    return res.status(400).json({ error: 'Brugernavn indeholder ugyldige tegn' });\n  }\n\n  if (getUser(username)) {\n    return res.status(409).json({ error: 'Bruger eksisterer allerede' });\n  }\n\n  try {\n    const hashedPassword = await hashPassword(password);\n    createUser(username, hashedPassword);\n    return res.status(201).json({ message: 'Bruger oprettet', username });\n  } catch (err) {\n    console.error('Registreringsfejl:', err.message);\n    return res.status(500).json({ error: 'Intern serverfejl' });\n  }\n});\n\n\/\/ POST \/login - Valider brugerlogin\napp.post('\/login', loginLimiter, async (req, res) => {\n  const { username, password } = req.body || {};\n\n  if (!username || !password) {\n    return res.status(400).json({ error: 'Brugernavn og password er p\u00e5kr\u00e6vet' });\n  }\n\n  const user = getUser(username);\n\n  if (!user) {\n    \/\/ K\u00f8r dummy-hashing for at undg\u00e5 timing-baseret bruger-enumeration\n    await hashPassword('dummy-for-constant-time-' + username);\n    return res.status(401).json({ error: 'Ugyldige legitimationsoplysninger' });\n  }\n\n  \/\/ Tjek om kontoen er l\u00e5st\n  if (user.lockedUntil && new Date(user.lockedUntil) > new Date()) {\n    const unlockTime = new Date(user.lockedUntil).toLocaleTimeString('da-DK');\n    return res.status(429).json({\n      error: `Kontoen er l\u00e5st til ${unlockTime}. Brug \"Glemt password\" for at nulstille.`,\n    });\n  }\n\n  try {\n    const isValid = await verifyPassword(password, user.password);\n\n    if (!isValid) {\n      incrementLoginAttempts(username);\n      return res.status(401).json({ error: 'Ugyldige legitimationsoplysninger' });\n    }\n\n    resetLoginAttempts(username);\n    return res.status(200).json({ message: 'Login lykkedes', username });\n  } catch (err) {\n    console.error('Login-fejl:', err.message);\n    return res.status(500).json({ error: 'Intern serverfejl' });\n  }\n});\n\n\/\/ Health-check endpoint\napp.get('\/health', (req, res) => {\n  const memUsage = process.memoryUsage();\n  res.json({\n    status: 'ok',\n    scrypt: { N: parseInt(process.env.SCRYPT_N), r: parseInt(process.env.SCRYPT_R) },\n    memory: { heapUsedMB: Math.round(memUsage.heapUsed \/ 1024 \/ 1024) },\n  });\n});\n\nconst PORT = process.env.PORT || 3000;\napp.listen(PORT, () => {\n  console.log(`Server k\u00f8rer p\u00e5 port ${PORT}`);\n  console.log(`Scrypt: N=${process.env.SCRYPT_N || 32768}, r=${process.env.SCRYPT_R || 8}, p=${process.env.SCRYPT_P || 1}`);\n  console.log(`RAM per hash: ${Math.round(128 * parseInt(process.env.SCRYPT_N || 32768) * 8 \/ 1024 \/ 1024)} MB`);\n});\n\nmodule.exports = app;<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Det vigtigste sikkerhedsm\u00f8nster i login-endpoint er dummy-hashing ved ukendt bruger. Uden det vil en angriber se, at serveren svarer hurtigere for ukendte brugere (0 ms for bruger-opslag + ingen hashing) end for kendte brugere (200-400 ms for scrypt-verifikation). Den tidsforskel kan bruges til at enumerere alle gyldige brugernavne. Med dummy-hashing er responstiden ens uanset om brugeren eksisterer.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"trin-7-start-og-test-serveren\">Trin 7: Start og test serveren<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Start serveren og test alle endpoints med curl:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>npm start\n# Output:\n# Server k\u00f8rer p\u00e5 port 3000\n# Scrypt: N=32768, r=8, p=1\n# RAM per hash: 32 MB\n\n# Registrer ny bruger (tag tid - det tager ~300 ms)\ntime curl -X POST http:\/\/localhost:3000\/register \\\n  -H \"Content-Type: application\/json\" \\\n  -d '{\"username\":\"alice\",\"password\":\"SuperHemmeligtKodeord2026!\"}'\n# Output: {\"message\":\"Bruger oprettet\",\"username\":\"alice\"}\n# Tid: real 0m0.312s\n\n# Log ind med korrekt password\ncurl -X POST http:\/\/localhost:3000\/login \\\n  -H \"Content-Type: application\/json\" \\\n  -d '{\"username\":\"alice\",\"password\":\"SuperHemmeligtKodeord2026!\"}'\n# Output: {\"message\":\"Login lykkedes\",\"username\":\"alice\"}\n\n# Forkert password\ncurl -X POST http:\/\/localhost:3000\/login \\\n  -H \"Content-Type: application\/json\" \\\n  -d '{\"username\":\"alice\",\"password\":\"ForkertKodeord\"}'\n# Output: {\"error\":\"Ugyldige legitimationsoplysninger\"}\n\n# Ikke-eksisterende bruger (samme svartid som forkert password)\ncurl -X POST http:\/\/localhost:3000\/login \\\n  -H \"Content-Type: application\/json\" \\\n  -d '{\"username\":\"findesikke\",\"password\":\"test\"}'\n# Output: {\"error\":\"Ugyldige legitimationsoplysninger\"}\n\n# Health-check\ncurl http:\/\/localhost:3000\/health\n# Output: {\"status\":\"ok\",\"scrypt\":{\"N\":32768,\"r\":8},\"memory\":{\"heapUsedMB\":42}}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Svartiden p\u00e5 200-400 ms er normal og tilsigtet. Det er scrypt der arbejder. Fors\u00f8g aldrig at reducere svartiden ved at s\u00e6nke N, det sv\u00e6kker sikkerheden. Design i stedet frontend til at vise en loading-indikator og undg\u00e5 HTTP-timeouts under 5 sekunder for auth-endpoints.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"trin-8-benchmarking-til-parametervalg\">Trin 8: Benchmarking til parametervalg<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">K\u00f8r dette benchmark-script direkte p\u00e5 din produktionsserver for at finde den rigtige N-v\u00e6rdi. K\u00f8r det under repr\u00e6sentativ serverbelastning for at simulere det reelle milj\u00f8. Resultater varierer meget mellem en cloud-VM, en dedikeret server og serverless:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>'use strict';\n\nconst { scrypt, randomBytes } = require('node:crypto');\nconst { promisify } = require('node:util');\nconst scryptAsync = promisify(scrypt);\n\nasync function benchmark(N, iterations = 5) {\n  const password = 'BenchmarkPassword123!Sikker';\n  const salt = randomBytes(32);\n  const maxmem = 128 * N * 8 * 2;\n\n  const times = [];\n  for (let i = 0; i < iterations; i++) {\n    const start = process.hrtime.bigint();\n    await scryptAsync(password, salt, 64, { N, r: 8, p: 1, maxmem });\n    const end = process.hrtime.bigint();\n    times.push(Number(end - start) \/ 1_000_000);\n  }\n\n  const avg = times.reduce((a, b) => a + b, 0) \/ times.length;\n  const min = Math.min(...times);\n  const max = Math.max(...times);\n  const memMB = (128 * N * 8) \/ (1024 * 1024);\n  console.log(\n    `N=${String(N).padStart(7)} | RAM: ${String(memMB).padStart(4)} MB | ` +\n    `Gns: ${avg.toFixed(0).padStart(4)} ms | Min: ${min.toFixed(0).padStart(4)} ms | Max: ${max.toFixed(0).padStart(4)} ms`\n  );\n  return avg;\n}\n\n(async () => {\n  const os = require('node:os');\n  console.log(`Platform: ${os.type()} ${os.arch()} | Kerner: ${os.cpus().length} | RAM: ${Math.round(os.totalmem()\/1024\/1024)} MB`);\n  console.log(`Node.js: ${process.version} | Benchmark-parametre: r=8, p=1, keylen=64, 5 iterationer\\n`);\n  console.log('N         | RAM     | Gns. tid | Min. tid | Maks. tid');\n  console.log('----------|---------|----------|----------|----------');\n\n  for (const N of [8192, 16384, 32768, 65536, 131072]) {\n    await benchmark(N);\n  }\n\n  console.log('\\nAnbefalede valg:');\n  console.log('  Serverless \/ lav RAM (<512 MB): N=16384');\n  console.log('  Standardserver (1-4 GB RAM):    N=32768 (OWASP-minimum)');\n  console.log('  Sikker server (8+ GB RAM):       N=65536 (anbefalet 2026)');\n  console.log('  H\u00f8j sikkerhed \/ lav trafik:     N=131072');\n})();\n\n\/\/ Typisk output p\u00e5 en moderne server:\n\/\/ N=   8,192 | RAM:    8 MB | Gns:   51 ms | Min:   49 ms | Maks:   54 ms\n\/\/ N=  16,384 | RAM:   16 MB | Gns:  102 ms | Min:   99 ms | Maks:  107 ms\n\/\/ N=  32,768 | RAM:   32 MB | Gns:  205 ms | Min:  201 ms | Maks:  211 ms\n\/\/ N=  65,536 | RAM:   64 MB | Gns:  413 ms | Min:  409 ms | Maks:  419 ms\n\/\/ N= 131,072 | RAM:  128 MB | Gns:  847 ms | Min:  841 ms | Maks:  858 ms<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"scrypt-vs-bcrypt-vs-argon2-direkte-sammenligning\">Scrypt vs bcrypt vs Argon2: direkte sammenligning<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Valget af password-hashing-algoritme er \u00e9n af de vigtigste sikkerhedsbeslutninger i en applikation. Her er en objektiv sammenligning af de tre OWASP-anbefalede algoritmer:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Egenskab<\/th><th>scrypt (N=32768)<\/th><th>bcrypt (cost=12)<\/th><th>Argon2id (t=2, m=65536)<\/th><\/tr><\/thead><tbody><tr><td><strong>RAM-forbrug<\/strong><\/td><td>~32 MB<\/td><td>&lt;1 MB<\/td><td>~64 MB<\/td><\/tr><tr><td><strong>CPU-tid (moderne server)<\/strong><\/td><td>200-400 ms<\/td><td>250-350 ms<\/td><td>200-500 ms<\/td><\/tr><tr><td><strong>GPU-modstand (RTX 4090)<\/strong><\/td><td>~100-200 hash\/sek<\/td><td>~10.000 hash\/sek<\/td><td>~50-100 hash\/sek<\/td><\/tr><tr><td><strong>ASIC-modstand<\/strong><\/td><td>H\u00f8j<\/td><td>Lav (ASIC-chips eksisterer)<\/td><td>Meget h\u00f8j<\/td><\/tr><tr><td><strong>Node.js built-in<\/strong><\/td><td>Ja (node:crypto)<\/td><td>Nej (npm: bcrypt)<\/td><td>Nej (npm: argon2, native C++)<\/td><\/tr><tr><td><strong>OWASP-prioritet 2025<\/strong><\/td><td>2. prioritet<\/td><td>3. prioritet<\/td><td>1. prioritet (foretrukket)<\/td><\/tr><tr><td><strong>Password-maksimall\u00e6ngde<\/strong><\/td><td>Ingen gr\u00e6nse<\/td><td>72 bytes (kritisk fejl!)<\/td><td>Ingen gr\u00e6nse<\/td><\/tr><tr><td><strong>RFC-standard<\/strong><\/td><td>RFC 7914 (2016)<\/td><td>Ingen RFC<\/td><td>RFC 9106 (2021)<\/td><\/tr><tr><td><strong>Alpine Linux Docker<\/strong><\/td><td>Virker (built-in)<\/td><td>Kr\u00e6ver build-essential<\/td><td>Kr\u00e6ver build-essential + Python<\/td><\/tr><tr><td><strong>Password Hashing Competition<\/strong><\/td><td>Finalist<\/td><td>Ikke deltaget<\/td><td>Vinder (2015)<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">bcrypts 72-byte-gr\u00e6nse er et alvorligt og underkendt problem. Passwords over 72 bytes trunkeres stille uden fejlbesked, hvilket betyder at to passwords der kun adskiller sig efter det 72. tegn producerer identiske hashes. En bruger der har et langt og \"sikkert\" password p\u00e5 100 tegn tror de er beskyttede, men en angriber der ved om denne gr\u00e6nse kan reducere angrebet markant. Scrypt og Argon2 har ingen s\u00e5dan begr\u00e6nsning.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Argon2id vinder den tekniske sammenligning som vinder af Password Hashing Competition 2015. Men i praksis giver native C++-kompilering problemer i Alpine Linux Docker-containere (standardbase for mange produktionssystemer), ARM64-baserede servere og AWS Lambda. En byggeproces der virker lokalt fejler i CI\/CD. Scrypt kr\u00e6ver ingen native kompilering og er det bedste valg for teams der prioriterer zero-dependency og reproducerbare builds. Se <a href=\"\/ed25519-signaturer-nodejs\/\">Ed25519 i Node.js<\/a> for et lignende eksempel p\u00e5 effektiv built-in kryptografi.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"trin-9-lazy-migration-fra-bcrypt-til-scrypt\">Trin 9: Lazy migration fra bcrypt til scrypt<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">N\u00e5r du migrerer fra bcrypt til scrypt, skal du aldrig pr\u00f8ve at konvertere eksisterende bcrypt-hashes direkte. Du har ikke adgang til de originale passwords, og du b\u00f8r heller ikke have det. Den korrekte strategi er lazy migration: opdater hashen ved n\u00e6ste succesfulde login, og gem et versionspr\u00e6fiks i hash-feltet s\u00e5 du kan h\u00e5ndtere begge formater parallelt.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>'use strict';\n\nconst { scrypt, randomBytes, timingSafeEqual } = require('node:crypto');\nconst { promisify } = require('node:util');\nconst scryptAsync = promisify(scrypt);\n\n\/\/ Hash-versioner og deres parametre\nconst VERSIONS = {\n  'scrypt-v1': { N: 16384, r: 8, p: 1, keylen: 64, maxmem: 33554432 },\n  'scrypt-v2': { N: 32768, r: 8, p: 1, keylen: 64, maxmem: 67108864 },\n};\nconst CURRENT_VERSION = 'scrypt-v2';\n\nfunction detectVersion(storedHash) {\n  for (const version of Object.keys(VERSIONS)) {\n    if (storedHash.startsWith(version + ':')) return version;\n  }\n  return null; \/\/ Ukendt format (f.eks. bcrypt med $2b$)\n}\n\nasync function verifyScryptHash(password, storedHash) {\n  const version = detectVersion(storedHash);\n  if (!version) return { valid: false, needsMigration: false, error: 'Ukendt hash-format' };\n\n  const params = VERSIONS[version];\n  const hashBody = storedHash.slice(version.length + 1); \/\/ Fjern \"scrypt-vX:\"\n  const colonIdx = hashBody.indexOf(':');\n  const salt = Buffer.from(hashBody.slice(0, colonIdx), 'hex');\n  const stored = Buffer.from(hashBody.slice(colonIdx + 1), 'hex');\n\n  const derived = await scryptAsync(password, salt, params.keylen, params);\n  if (derived.length !== stored.length) return { valid: false, needsMigration: false };\n  const isValid = timingSafeEqual(derived, stored);\n\n  if (!isValid) return { valid: false, needsMigration: false, newHash: null };\n\n  const needsMigration = version !== CURRENT_VERSION;\n  let newHash = null;\n  if (needsMigration) {\n    const np = VERSIONS[CURRENT_VERSION];\n    const newSalt = randomBytes(32);\n    const newKey = await scryptAsync(password, newSalt, np.keylen, np);\n    newHash = `${CURRENT_VERSION}:${newSalt.toString('hex')}:${newKey.toString('hex')}`;\n  }\n\n  return { valid: true, needsMigration, newHash };\n}\n\nasync function createScryptHash(password) {\n  const np = VERSIONS[CURRENT_VERSION];\n  const salt = randomBytes(32);\n  const key = await scryptAsync(password, salt, np.keylen, np);\n  return `${CURRENT_VERSION}:${salt.toString('hex')}:${key.toString('hex')}`;\n}\n\nmodule.exports = { verifyScryptHash, createScryptHash };\n\n\/\/ Brug i login-flowet:\n\/\/ const { valid, needsMigration, newHash } = await verifyScryptHash(password, user.password_hash);\n\/\/ if (valid && needsMigration && newHash) {\n\/\/   await db.query('UPDATE users SET password_hash=$1 WHERE id=$2', [newHash, user.id]);\n\/\/ }<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Lazy migration har en vigtig egenskab: inaktive brugere migrerer aldrig automatisk. Det er acceptabelt, men du b\u00f8r s\u00e6tte en deadline (f.eks. 6 m\u00e5neder) og tvinge brugere der ikke har logget ind til at nulstille password via e-mail. Kombiner versionspr\u00e6fikset med en migreringsdato i databasen for at spore fremskridtet.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"trin-10-postgresql-integration\">Trin 10: PostgreSQL-integration<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">I produktionssystemer gemmer du hashes i en relationsdatabase. Her er det komplette databaselag til PostgreSQL med beskyttelse mod SQL-injection via parametriserede foresp\u00f8rgsler. Brug <a href=\"\/sql-injection-nodejs\/\">vores guide til SQL Injection i Node.js<\/a> for at forst\u00e5, hvorfor stringinterpolation i SQL-foresp\u00f8rgsler er farligt.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>'use strict';\n\nconst { Pool } = require('pg');\nconst { hashPassword, verifyPassword } = require('.\/hash');\n\nconst pool = new Pool({\n  connectionString: process.env.DATABASE_URL,\n  max: 10, \/\/ Maks. 10 databaseforbindelser i poolen\n  ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: true } : false,\n});\n\n\/\/ K\u00f8r ved serveropstart\nconst CREATE_TABLE_SQL = `\n  CREATE TABLE IF NOT EXISTS users (\n    id SERIAL PRIMARY KEY,\n    username VARCHAR(50) UNIQUE NOT NULL,\n    password_hash TEXT NOT NULL,\n    created_at TIMESTAMPTZ DEFAULT NOW(),\n    last_login TIMESTAMPTZ,\n    login_attempts SMALLINT DEFAULT 0,\n    locked_until TIMESTAMPTZ\n  );\n  CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);\n`;\n\nasync function initDatabase() {\n  const client = await pool.connect();\n  try {\n    await client.query(CREATE_TABLE_SQL);\n    console.log('Databaseskema klar');\n  } finally {\n    client.release();\n  }\n}\n\nasync function registerUser(username, password) {\n  const hashedPassword = await hashPassword(password);\n  const result = await pool.query(\n    `INSERT INTO users (username, password_hash)\n     VALUES ($1, $2)\n     RETURNING id, username, created_at`,\n    [username, hashedPassword]\n  );\n  return result.rows[0];\n}\n\nasync function loginUser(username, password) {\n  const result = await pool.query(\n    `SELECT id, username, password_hash, login_attempts, locked_until\n     FROM users WHERE username = $1`,\n    [username]\n  );\n\n  if (result.rows.length === 0) {\n    \/\/ Kald scrypt alligevel for konstant svartid\n    await hashPassword('timing-protection-dummy');\n    return null;\n  }\n\n  const user = result.rows[0];\n  if (user.locked_until && new Date(user.locked_until) > new Date()) {\n    const err = new Error('Kontoen er l\u00e5st');\n    err.lockedUntil = user.locked_until;\n    throw err;\n  }\n\n  const isValid = await verifyPassword(password, user.password_hash);\n\n  if (!isValid) {\n    await pool.query(\n      `UPDATE users\n       SET login_attempts = login_attempts + 1,\n           locked_until = CASE\n             WHEN login_attempts >= 4\n             THEN NOW() + INTERVAL '15 minutes'\n             ELSE locked_until\n           END\n       WHERE username = $1`,\n      [username]\n    );\n    return null;\n  }\n\n  await pool.query(\n    `UPDATE users\n     SET login_attempts = 0, locked_until = NULL, last_login = NOW()\n     WHERE username = $1`,\n    [username]\n  );\n  return { id: user.id, username: user.username };\n}\n\nmodule.exports = { initDatabase, registerUser, loginUser };<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"trin-11-test-med-node-js-indbyggede-testmodul\">Trin 11: Test med Node.js' indbyggede testmodul<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Node.js 20+ inkluderer et built-in testmodul der eliminerer behovet for Jest eller Mocha til simple enhedstests. Brug N=1024 i test-milj\u00f8et for at holde testen under 1 sekund, og s\u00e6t N=32.768 til integrationstests der verificerer den fulde produktionskonfiguration.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>'use strict';\n\nconst { test, describe } = require('node:test');\nconst assert = require('node:assert\/strict');\n\n\/\/ Lavere N til unit-tests - undg\u00e5 timeouts i CI\/CD\nprocess.env.SCRYPT_N = '1024';\nprocess.env.SCRYPT_R = '8';\nprocess.env.SCRYPT_P = '1';\nprocess.env.SCRYPT_KEYLEN = '64';\nprocess.env.SCRYPT_SALT_BYTES = '32';\n\nconst { hashPassword, verifyPassword } = require('.\/hash');\n\ndescribe('hashPassword', () => {\n  test('returnerer salt:hash format', async () => {\n    const hash = await hashPassword('TestPassword123!');\n    assert.equal(typeof hash, 'string');\n    const colonIdx = hash.indexOf(':');\n    assert.ok(colonIdx > 0, 'Hash skal indeholde \":\"');\n    assert.equal(hash.slice(0, colonIdx).length, 64);  \/\/ 32 bytes = 64 hex\n    assert.equal(hash.slice(colonIdx + 1).length, 128); \/\/ 64 bytes = 128 hex\n  });\n\n  test('producerer forskellig hash for samme password pga. tilf\u00e6ldig salt', async () => {\n    const h1 = await hashPassword('SammePassword!');\n    const h2 = await hashPassword('SammePassword!');\n    assert.notEqual(h1, h2);\n    \/\/ Men salt-prefixet alene er ogs\u00e5 unikt:\n    assert.notEqual(h1.split(':')[0], h2.split(':')[0]);\n  });\n\n  test('kaster TypeError for tomt password', async () => {\n    await assert.rejects(() => hashPassword(''), TypeError);\n    await assert.rejects(() => hashPassword(null), TypeError);\n    await assert.rejects(() => hashPassword(12345), TypeError);\n    await assert.rejects(() => hashPassword(undefined), TypeError);\n  });\n\n  test('hashPassword h\u00e5ndterer ikke-ASCII-tegn korrekt', async () => {\n    const hash = await hashPassword('KodeordMed\u00c6\u00d8\u00c5!123');\n    assert.ok(hash.includes(':'));\n  });\n});\n\ndescribe('verifyPassword', () => {\n  test('verificerer korrekt password', async () => {\n    const pw = 'KorrektPassword2026!';\n    const hash = await hashPassword(pw);\n    assert.equal(await verifyPassword(pw, hash), true);\n  });\n\n  test('afviser forkert password', async () => {\n    const hash = await hashPassword('RigtigtPassword!');\n    assert.equal(await verifyPassword('ForkertPassword!', hash), false);\n  });\n\n  test('afviser tomt password mod gyldig hash', async () => {\n    const hash = await hashPassword('Rigtigt!');\n    assert.equal(await verifyPassword('', hash), false);\n  });\n\n  test('kaster fejl ved manglende kolon-separator', async () => {\n    await assert.rejects(() => verifyPassword('test', 'ingenkolonher'), Error);\n  });\n\n  test('kaster TypeError ved ugyldig input-type', async () => {\n    const hash = await hashPassword('test!12345678');\n    await assert.rejects(() => verifyPassword(null, hash), TypeError);\n    await assert.rejects(() => verifyPassword('test', null), TypeError);\n  });\n});\n\n\/\/ K\u00f8r med: node --test src\/hash.test.js\n\/\/ Forventet output:\n# \u25b6 hashPassword\n#   \u2714 returnerer salt:hash format (12.3ms)\n#   \u2714 producerer forskellig hash for samme password (24.8ms)\n#   \u2714 kaster TypeError for tomt password (0.2ms)\n#   \u2714 hashPassword h\u00e5ndterer ikke-ASCII-tegn korrekt (12.1ms)\n# \u25b6 verifyPassword\n#   \u2714 verificerer korrekt password (12.4ms)\n#   \u2714 afviser forkert password (12.2ms)\n#   \u2714 afviser tomt password mod gyldig hash (0.1ms)\n#   \u2714 kaster fejl ved manglende kolon-separator (0.1ms)\n#   \u2714 kaster TypeError ved ugyldig input-type (0.1ms)\n# \u2139 tests 9, pass 9<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"trin-12-produktionscheckliste\">Trin 12: Produktionscheckliste<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Inden deployment til produktion, gennemg\u00e5 denne checkliste punkt for punkt:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Sikkerhedspunkt<\/th><th>Krav<\/th><th>Verifikation<\/th><\/tr><\/thead><tbody><tr><td>N-parameter<\/td><td>Min. 32.768 i produktion<\/td><td>K\u00f8r benchmark-scriptet p\u00e5 produktionsserver<\/td><\/tr><tr><td>maxmem<\/td><td>Min. 128 \u00d7 N \u00d7 r \u00d7 1,5 bytes<\/td><td>Tjek ingen <code>maxmem exceeded<\/code>-fejl i logs<\/td><\/tr><tr><td>Salt<\/td><td>randomBytes(32), unik per hash<\/td><td>Verificer at ingen to hashes har samme salt-pr\u00e6fix<\/td><\/tr><tr><td>timingSafeEqual<\/td><td>Altid ved hash-verifikation<\/td><td>Code review: s\u00f8g efter <code>Buffer.equals<\/code> og <code>===<\/code><\/td><\/tr><tr><td>Rate limiting<\/td><td>Max 10 login-fors\u00f8g\/IP\/15 min<\/td><td>Test med Apache Bench: <code>ab -n 15 -c 1 ...<\/code><\/td><\/tr><tr><td>Kontobaseret lockout<\/td><td>5 fors\u00f8g, 15 min lockout<\/td><td>Test med 6 forkerte passwords for samme bruger<\/td><\/tr><tr><td>Dummy-hashing<\/td><td>Ved ukendt brugernavn<\/td><td>M\u00e5l svartid for eksisterende vs. ikke-eksisterende bruger<\/td><\/tr><tr><td>HTTPS<\/td><td>TLS 1.3 p\u00e5 alle endpoints<\/td><td>curl --verbose https:\/\/... (tjek TLS-version)<\/td><\/tr><tr><td>Password-minimumsl\u00e6ngde<\/td><td>Min. 12 tegn (NIST SP 800-63B)<\/td><td>Test med 11-tegns password - skal afvises<\/td><\/tr><tr><td>Ingen hash i API-response<\/td><td>password_hash aldrig eksponeret<\/td><td>Tjek alle res.json()-kald i koden<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"5-typiske-fejl-med-scrypt-i-node-js\">5 typiske fejl med scrypt i Node.js<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Disse fejl optr\u00e6der hyppigt i kodereviews og kan nullificere scrypts sikkerhedsfordele selv med ellers korrekt implementering.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Fejl 1: Manglende maxmem-konfiguration.<\/strong> Node.js' standard maxmem er 32 MB. Med N=65.536 og r=8 kr\u00e6ver scrypt 64 MB, og du f\u00e5r fejlen <code>Error: Invalid scrypt params: maxmem = 33554432 exceeded<\/code>. Denne fejl opst\u00e5r typisk i produktion med f\u00f8rste deployment af nye N-parametre, fordi udviklermaskinen havde en anden Node.js-version eller en nyere standard. S\u00e6t altid maxmem eksplicit til mindst <code>128 * N * r * 2<\/code> for at have buffer til OS-overhead.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Fejl 2: Statisk eller forudsigeligt salt.<\/strong> Et fast salt som <code>'mysalt'<\/code>, brugernavnet som salt, eller et UUID fra en ekstern kilde g\u00f8r alle hashes beregnelige p\u00e5 forh\u00e5nd via rainbow tables. Med et forudsigeligt salt kan en angriber der stj\u00e6ler databasen beregne alle hash-v\u00e6rdier for en stor ordbog af passwords \u00e9n gang og derefter sl\u00e5 dem op i realtid. Brug altid <code>randomBytes(32)<\/code> og gem saltet i det samme felt som hashen.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Fejl 3: Sammenligning med === eller Buffer.equals().<\/strong> Sammenligning med <code>===<\/code> eller <code>Buffer.equals()<\/code> returnerer <code>false<\/code> s\u00e5 snart det f\u00f8rste forskellige byte er fundet. En angriber kan m\u00e5le responstiden med h\u00f8j pr\u00e6cision (mikrosekunder) og gradvist g\u00e6tte den korrekte hash ved at sende mange requests og observere tidsvariationer. <code>timingSafeEqual()<\/code> bruger altid pr\u00e6cis den samme tid uanset, hvor hashes adskiller sig.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ FORKERT - l\u00e6kker timing-information\nif (derivedKey.toString('hex') === storedHash) { ... }\nif (derivedKey.equals(storedKeyBuffer)) { ... }\n\n\/\/ KORREKT - konstant tid\nconst { timingSafeEqual } = require('node:crypto');\nif (derivedKey.length !== storedKeyBuffer.length) return false;\nif (timingSafeEqual(derivedKey, storedKeyBuffer)) { ... }<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Fejl 4: Test-N-v\u00e6rdier i produktion.<\/strong> Med N=1024 (som bruges i test-suite) beregner en moderne GPU op til 100.000 hashes per sekund. Med N=32.768 falder den til under 200. Producerer du N fra en milj\u00f8variabel der ikke s\u00e6ttes korrekt i produktionsmilj\u00f8et, ender du med N=1024 eller endda N=undefined, der typisk defaulter til en lav fejlsikker v\u00e6rdi. Valider altid N ved opstart med en hard-coded minimumsgr\u00e6nse.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Fejl 5: scryptSync i Express-request-handlers.<\/strong> <code>crypto.scryptSync()<\/code> blokerer Node.js' event loop i 200-400 ms. I den tid kan serveren ikke h\u00e5ndtere nogen andre requests, ikke engang health-checks, statiske filer eller database-keepalive-foresp\u00f8rgsler. 10 samtidige brugere der logger ind giver 2-4 sekunders total blokering per request i k\u00f8en. Brug altid <code>scryptAsync<\/code> (promisify-wrapped version) i alle web-request-handlers.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"8-fejlfindingsscenarier\">8 fejlfindingsscenarier<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Her er en guide til de hyppigste fejlbeskeder og problemer ved brug af scrypt i Node.js-produktion:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Fejlbesked eller symptom<\/th><th>\u00c5rsag<\/th><th>L\u00f8sning<\/th><\/tr><\/thead><tbody><tr><td><code>Error: Invalid scrypt params: maxmem exceeded<\/code><\/td><td>N \u00d7 r \u00d7 128 bytes overstiger maxmem-gr\u00e6nsen<\/td><td>S\u00e6t <code>maxmem: 128 * N * r * 2<\/code> i SCRYPT_PARAMS<\/td><\/tr><tr><td><code>Error: scrypt failed<\/code> (generisk OpenSSL-fejl)<\/td><td>Utilstr\u00e6kkelig RAM p\u00e5 serveren, typisk ved N=131072<\/td><td>Reducer N eller for\u00f8g serverens RAM-allokering<\/td><\/tr><tr><td><code>TypeError: Invalid data, key must be a string or an instance of Buffer<\/code><\/td><td>Password-argumentet er null, undefined eller et tal<\/td><td>Valider input-typer inden scrypt-kaldet med <code>typeof<\/code><\/td><\/tr><tr><td>Hash-verifikation returnerer altid false<\/td><td>Encoding-mismatch (utf8 vs. hex) eller forkert split-logik<\/td><td>Brug konsekvent hex-encoding; brug <code>indexOf(':')<\/code> ikke <code>split(':')<\/code><\/td><\/tr><tr><td><code>RangeError: scrypt work factors are out of range<\/code><\/td><td>N er ikke en potens af 2, eller er 0, negativt eller NaN<\/td><td>Valider: <code>N > 0 && (N & (N-1)) === 0<\/code><\/td><\/tr><tr><td>Server svarer ikke under h\u00f8j login-trafik<\/td><td>Mange samtidige scrypt-operationer blokerer eller s\u00e6tter serveren under pres<\/td><td>Reducer N, implement\u00e9r rate limiting og Worker Thread-pool<\/td><\/tr><tr><td>Out-of-memory-crash ved spidsbelastning<\/td><td>N \u00d7 antal samtidige login \u00d7 128 \u00d7 r bytes overstiger serverens RAM<\/td><td>S\u00e6t N=16384 i serverless; brug job-queue til batching af auth<\/td><\/tr><tr><td><code>ERR_INVALID_ARG_TYPE: keylen must be an integer<\/code><\/td><td>SCRYPT_KEYLEN er en streng pga. manglende parseInt()<\/td><td>Brug altid <code>parseInt(process.env.SCRYPT_KEYLEN, 10)<\/code><\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Generel diagnostik: k\u00f8r Node.js med <code>node --trace-warnings src\/server.js<\/code> for alle runtime-advarsler. M\u00e5l hukommelsesforbrug under test med Apache Bench: <code>ab -n 50 -c 10 -p login-body.json -T application\/json http:\/\/localhost:3000\/login<\/code> og overv\u00e5g <code>process.memoryUsage().heapUsed<\/code> via health-endpoint.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Ved mystiske timing-fejl under migration: kontroll\u00e9r at alle Node.js-versioner i dit produktionsmilj\u00f8 er identiske. Node.js 10.5.0-11.x har en \u00e6ldre OpenSSL-version med anderledes standarder for maxmem. Containeris\u00e9r altid med en pinned Node.js-version (<code>FROM node:22.14-alpine3.20<\/code>) for reproducerbare builds.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"avancerede-tips-til-produktionssystemer\">Avancerede tips til produktionssystemer<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Worker Thread-isolering:<\/strong> For applikationer med h\u00f8j login-trafik b\u00f8r du flytte scrypt-beregningerne til Worker Threads. En server med 4 CPU-kerner kan oprette 3 scrypt-workers og 1 main thread til HTTP-h\u00e5ndtering. Implement\u00e9r en simpel round-robin-scheduler der fordeler hash-requests til workers via <code>parentPort.postMessage()<\/code>. Med denne arkitektur kan main thread fortsat betjene nye requests, health-checks og database-queries under intens login-belastning.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Pepper som ekstra beskyttelseslag:<\/strong> En \"pepper\" er et hemmeligt, server-side-suffix tilf\u00f8jet til passwordet inden hashing. I mods\u00e6tning til salt gemmes pepper ikke i databasen, men kun i en milj\u00f8variabel eller et Key Management System. Selv hvis en angriber stj\u00e6ler hele databasen, kan de ikke cracke passwords uden at kende pepper. Brug pepper med: <code>const peppered = password + process.env.PASSWORD_PEPPER<\/code> og hash derefter <code>peppered<\/code>. Gem pepper aldrig i kode-repositoriet og roter den periodisk med lazy migration.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Scrypt til n\u00f8gleafledning (ikke kun password-hashing):<\/strong> Scrypt er standardiseret i RFC 7914 prim\u00e6rt som en n\u00f8gleafledningsfunktion (KDF). WPA3-netv\u00e6rk bruger scrypt til at afkryptere den pre-shared n\u00f8gle fra et password. Du kan replikere dette i Node.js til envelope encryption: brug scrypt til at generere en 32-byte AES-n\u00f8gle fra et brugerpassword og krypter dine data med AES-256-GCM. Scrypt-n\u00f8glen b\u00f8r genereres med et unikt salt per krypteringsoperation og gemmes ved siden af de krypterede data. Kombiner med <a href=\"\/webcrypto-api-nodejs\/\">WebCrypto API i Node.js<\/a> til selve krypteringen.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Adaptive parameter-revision:<\/strong> Hardware bliver kraftigere hvert \u00e5r. Angribere kan i 2026 bryde N=8.192 hashes 16 gange hurtigere end i 2018. Planl\u00e6g en halv\u00e5rlig revision af N-parameteren og dokument\u00e9r versionen i databaseskemaet. Som tommelfingerregel: fordobl N hvert 2. \u00e5r for at modsvare Moores lov. Med versionsbaseret lazy migration (se trin 9) er opgradering usynlig for brugerne.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Integreret audit-log:<\/strong> Log alle login-h\u00e6ndelser til en separat tabel med timestamp, username, IP-adresse, User-Agent og resultat (success\/failure). Inklud\u00e9r aldrig password eller password_hash i logfiler. Brug <a href=\"\/hmac-webhook-signaturer-nodejs\/\">HMAC-SHA256 som beskrevet i vores Node.js-guide<\/a> til at signere audit-log-poster kryptografisk, s\u00e5 en kompromitteret database ikke kan give en angriber mulighed for at slette beviser.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Containerisering med korrekt RAM-allokering:<\/strong> I Docker og Kubernetes skal du eksplicit allokere nok RAM til scrypt. Med N=32.768 og 20 samtidige login kr\u00e6ver du 640 MB kun til hashing. S\u00e6t <code>resources.limits.memory<\/code> i Kubernetes-manifestet til mindst 1 GB, og s\u00e6t Node.js heap-limit med <code>--max-old-space-size=768<\/code> for at undg\u00e5 OOM-kills. Overv\u00e5g <code>container_memory_usage_bytes<\/code> i Prometheus med en alert ved over 80% af gr\u00e6nsen.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"relateret-indhold\">Relateret indhold<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"relateret-daekning\">Relateret d\u00e6kning<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><a href=\"\/ed25519-signaturer-nodejs\/\">Ed25519 i Node.js: kryptografiske signaturer i 12 trin<\/a><\/li>\n<li><a href=\"\/webcrypto-api-nodejs\/\">WebCrypto API i Node.js: AES-GCM og ECDSA i 12 trin<\/a><\/li>\n<li><a href=\"\/hmac-webhook-signaturer-nodejs\/\">HMAC i Node.js: webhook-signaturer i 12 trin<\/a><\/li>\n<li><a href=\"\/webauthn-nodejs-passkeys\/\">WebAuthn i Node.js: passwordless login i 12 trin<\/a><\/li>\n<li><a href=\"\/sql-injection-nodejs\/\">SQL Injection i Node.js: 12 trin til sikker database<\/a><\/li>\n<li><a href=\"\/kodeordssikkerhed\/\">Kodeordssikkerhed: hvad der egentlig beskytter konti<\/a><\/li>\n<li><a href=\"\/cryptography-hub\/\">Kryptografi: hashfunktioner, SHA og digital tillid<\/a><\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"faq\">FAQ<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Hvorn\u00e5r v\u00e6lger jeg scrypt frem for Argon2id?<\/strong> V\u00e6lg scrypt, n\u00e5r du vil undg\u00e5 native C++-afh\u00e6ngigheder. Argon2 kr\u00e6ver npm-pakken <code>argon2<\/code> med native addons der kompileres ved installation og giver problemer i Alpine Linux Docker-containere og serverless-platforme. Scrypt er 100% built-in i Node.js og kr\u00e6ver nul afh\u00e6ngigheder. Argon2id er teknisk overlegen, men scrypt er fuldt OWASP-godkendt til produktion.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Hvad er den rigtige N-v\u00e6rdi til produktion i 2026?<\/strong> OWASP anbefaler N=32.768 (2^15) som minimum. For nye systemer med god RAM er N=65.536 (2^16) anbefalet. K\u00f8r benchmark-scriptet fra trin 8 p\u00e5 din faktiske produktionsserver og v\u00e6lg den h\u00f8jeste N der holder svartiden under 500 ms under spidsbelastning. En server med 4 GB RAM og 4 kerner h\u00e5ndterer typisk N=65.536 med op til 15 samtidige logins.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Er scrypt s\u00e5rbar over for kvantecomputerangreb?<\/strong> For password-hashing er kvantecomputing ikke et praktisk problem i den n\u00e6rmeste fremtid. Scrypts hukommelseshardhed er ikke relevant mod Grover-algoritmen, men brug af 64-byte output (keylen=64) og passwords med mindst 16 tilf\u00e6ldige tegn modvirker den effektive sikkerhedsreduktion som kvantecomputing giver.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Kan jeg bruge scrypt til API-n\u00f8gle-hashing?<\/strong> Nej. API-n\u00f8gler verificeres ved hvert request, og scrypts 200-400 ms hash-tid er helt uacceptabel ved h\u00f8j API-trafik. Brug HMAC-SHA256 med et hemmeligt server-key til API-n\u00f8gle-verifikation: det beregnes p\u00e5 under 0,1 ms. Scrypt er kun egnet til passwords der autentificeres ved login.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Hvad er forskellen p\u00e5 scrypt og PBKDF2?<\/strong> PBKDF2 er kun CPU-tung og paralleliserer effektivt p\u00e5 GPU: en RTX 4090 kan beregne millioner af PBKDF2-hashes per sekund. Scrypt er CPU- og hukommelsestung og kan ikke paralleliseres effektivt: den samme GPU beregner kun 100-200 scrypt-hashes per sekund. OWASP rangerer PBKDF2 lavest af de tre anbefalede algoritmer og anbefaler det kun for FIPS-compliance-krav.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Hvad sker der, hvis serveren l\u00f8ber t\u00f8r for RAM under scrypt?<\/strong> Node.js kaster <code>Error: scrypt failed<\/code> eller <code>maxmem exceeded<\/code>. Serveren crasher ikke, men den aktuelle request fejler. Implement\u00e9r circuit breaker: overv\u00e5g <code>process.memoryUsage().heapUsed<\/code> og returner HTTP 503 midlertidigt, hvis hukommelsesforbruget overstiger 80% af det tilg\u00e6ngelige. Log disse h\u00e6ndelser til alerting.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Virker scrypt i Cloudflare Workers?<\/strong> Nej. Cloudflare Workers' WebCrypto API underst\u00f8tter ikke scrypt. PBKDF2 med 310.000 iterationer er Workers-alternativet. Alternativt kan du bruge Cloudflare Zero Trust og Cloudflare Access til at h\u00e5ndtere authentication eksternt og kun kalde din Node.js-server til den egentlige scrypt-verifikation. Se <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/API\/SubtleCrypto\/deriveKey\" target=\"_blank\" rel=\"noopener\">MDN SubtleCrypto.deriveKey()<\/a> for Workers-kompatible KDF-algoritmer.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Kan jeg gemme scrypt-hashes i MongoDB?<\/strong> Ja. Hashes er hex-kodede strenge i formatet <code>salt:hash<\/code> (64 + 1 + 128 = 193 tegn med standard SALT_BYTES=32 og KEYLEN=64). Gem dem i et <code>String<\/code>-felt med <code>maxlength: 300<\/code> for fremtidig fleksibilitet. S\u00e6t aldrig et unikt index p\u00e5 password-hash-feltet og inklud\u00e9r aldrig feltet i API-responses.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Scrypt er den mest hukommelseskr\u00e6vende standard for password-hashing, og Node.js har underst\u00f8ttet den via det indbyggede node:crypto-modul siden version 10.5.0. Algoritmen blev designet til at g\u00f8re GPU- og ASIC-angreb 10-100\u2026<\/p>\n","protected":false},"author":6,"featured_media":155,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[2],"tags":[],"class_list":["post-154","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\/154","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\/6"}],"replies":[{"embeddable":true,"href":"https:\/\/shattered.io\/dk\/wp-json\/wp\/v2\/comments?post=154"}],"version-history":[{"count":1,"href":"https:\/\/shattered.io\/dk\/wp-json\/wp\/v2\/posts\/154\/revisions"}],"predecessor-version":[{"id":156,"href":"https:\/\/shattered.io\/dk\/wp-json\/wp\/v2\/posts\/154\/revisions\/156"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/shattered.io\/dk\/wp-json\/wp\/v2\/media\/155"}],"wp:attachment":[{"href":"https:\/\/shattered.io\/dk\/wp-json\/wp\/v2\/media?parent=154"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/shattered.io\/dk\/wp-json\/wp\/v2\/categories?post=154"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/shattered.io\/dk\/wp-json\/wp\/v2\/tags?post=154"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}