Scrypt er den mest hukommelseskrævende standard for password-hashing, og Node.js har understøttet den via det indbyggede node:crypto-modul siden version 10.5.0. Algoritmen blev designet til at gøre GPU- og ASIC-angreb 10-100 gange dyrere end bcrypt, fordi den kræver op til 128 MB RAM per hash. Denne guide viser dig præcis, hvordan du implementerer scrypt korrekt i Node.js, trin for trin, fra installation til produktionsklar kode med Express, PostgreSQL og en komplet testpakke.
Hvad er scrypt, og hvorfor bruge det i 2026?
Scrypt er en password-baseret nøgleafledningsfunktion (PBKDF), designet af Colin Percival og offentliggjort i 2009. Den er standardiseret i RFC 7914 og er i dag brugt i Litecoin (proof-of-work), WPA3-netværk (Wi-Fi-nøgleafledning) og adskillige open source password-managere. Tricket er, at scrypt ikke bare er CPU-tung, den er hukommelsestung: for at beregne én hash skal algoritmen allokere en stor mængde RAM og tilgå den i et pseudo-tilfældigt mønster, der er svært at parallelisere på GPU’er og specialiserede ASIC-chips.
Klassiske hashfunktioner som MD5 eller SHA-256 kan beregnes af tusindvis af GPU-kerner parallelt, fordi de kræver minimal RAM. bcrypt tilføjede CPU-belastning via en cost-faktor, men er stadig rimeligt effektiv på GPU: en moderne RTX 4090 kan cracke op til 10.000 bcrypt-hashes per sekund med cost=10. Scrypt tilføjer den tredje dimension, hukommelsesbåndbredde: den samme GPU kan kun beregne 50-200 scrypt-hashes per sekund med N=32.768, fordi hvert hash kræver 32 MB RAM der skal læses og skrives i et ikke-paralleliserbart mønster. Det gør offline dictionary-angreb 50-200 gange dyrere end med bcrypt.
OWASP anbefaler i sin Password Storage Cheat Sheet scrypt som et legitimt alternativ til Argon2id, med minimum-parametrene N=32.768, r=8, p=1. Argon2id er foretrukket, når platformen understøtter det, men scrypt er det rigtige valg, når du arbejder direkte med Node.js’ built-in kryptografi uden tredjeparts afhængigheder. Det er særlig relevant i 2026, hvor containere og serverless-funktioner sætter strenge begrænsninger på native native C++-kompilering.
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ældige data og derefter læser dem i afhængig rækkefølge, og endelig en ny PBKDF2-SHA256-fase der komprimerer resultatet til outputnøglen. ROMix-fasen er kernen i den hukommelseshardhed: dens RAM-adgangsmønster er designet til at maksimere cache-fejlhit, så en angriber ikke kan reducere RAM-behovet uden dramatisk at øge beregningsomkostningen. Det er i modsætning til bcrypt, der kun kræver 4 KB arbejdshukommelse og er effektiv på GPU’er med mange små kerner.
Forudsætninger og systemkrav
Scrypt er tilgængeligt i Node.js’ standard node:crypto-modul uden nogen npm-installation. Du behøver ingen ekstra pakker til basis-implementationen. De eneste afhængigheder i dette projekt er Express (webserver) og express-rate-limit (sikkerhed). Tjek følgende tabel, inden du begynder:
| Komponent | Minimumversion | Anbefalet version | Bemærkning |
|---|---|---|---|
| Node.js | 10.5.0 | 22.x LTS | crypto.scrypt() og crypto.scryptSync() tilgængeligt |
| npm | 6.x | 10.x | Kun til Express-eksemplet |
| RAM på server | 256 MB | 2 GB+ | N=32.768 kræver ~32 MB per hash; 10 samtidige login = 320 MB |
| CPU-kerner | 1 | 4+ | Scrypt er enkelt-trådet; Worker Threads anbefales til høj trafik |
| Operativsystem | Linux/macOS/Windows | Ubuntu 22.04+ | Alle platforme understøttet via OpenSSL |
| JavaScript-kendskab | ES2017 (async/await) | ES2022+ | Scrypt-API’et er callback-baseret men wrappes nemt |
Verifikér din Node.js-version og at scrypt er tilgængeligt:
node --version
# Forventet: v22.x.x eller nyere
node -e "const { scrypt, scryptSync } = require('node:crypto'); console.log('scrypt:', typeof scrypt, '| scryptSync:', typeof scryptSync);"
# Forventet: scrypt: function | scryptSync: function
Hvis du kører Node.js 10.5.0-11.x, er scrypt tilgængeligt men crypto.scryptSync() var ikke stabil før Node.js 12.0.0. Til alle produktionssystemer anbefales Node.js 20 LTS eller 22 LTS.
Trin 1: Projektopsætning og mappestruktur
Opret projektmappen og initialiser npm. Strukturen er enkel: én fil til hashing-logikken, én til brugerdatabasen og én til Express-serveren. Det gør koden let at teste isoleret.
mkdir scrypt-demo && cd scrypt-demo
npm init -y
npm install express express-rate-limit dotenv
mkdir -p src
Mappestrukturen:
scrypt-demo/
├── src/
│ ├── hash.js # Scrypt-hjælpefunktioner
│ ├── server.js # Express API-server
│ ├── users.js # In-memory brugerstore (erstat med DB)
│ └── benchmark.js # Benchmarking-script til parametervalg
├── package.json
└── .env
Opret .env-filen. Disse parametre er centerale for sikkerheden og bør aldrig hardcodes direkte i koden:
# .env
PORT=3000
# Scrypt-parametre - tilpas N til din servers RAM og CPU
# N=32768: ~32 MB RAM, ~200-300 ms per hash (OWASP-minimum)
# N=65536: ~64 MB RAM, ~400-600 ms per hash (anbefalet til nye systemer)
SCRYPT_N=32768
SCRYPT_R=8
SCRYPT_P=1
SCRYPT_KEYLEN=64
SCRYPT_SALT_BYTES=32
Tilføj en startscript-sektion til package.json:
{
"name": "scrypt-demo",
"version": "1.0.0",
"scripts": {
"start": "node -r dotenv/config src/server.js",
"dev": "node --watch -r dotenv/config src/server.js",
"benchmark": "node -r dotenv/config src/benchmark.js",
"test": "node --test src/hash.test.js"
},
"dependencies": {
"dotenv": "^16.4.0",
"express": "^4.21.0",
"express-rate-limit": "^7.4.0"
},
"engines": {
"node": ">=20.0.0"
}
}
Trin 2: Forstå N, r og p-parametrene
Scrypt har tre arbejdsfaktorer, og det er kritisk at forstå dem, inden du vælger værdier til produktion. En forkert konfiguration kan enten efterlade systemet sårbart over for angreb eller crashe serveren med out-of-memory-fejl.
| Parameter | Navn | Effekt | OWASP minimum 2025 | Anbefalet 2026 |
|---|---|---|---|---|
| N | CPU/hukommelsesomkostning | Potens af 2. Fordobling af N fordobler CPU-tid og RAM-forbrug lineært | 32.768 (215) | 65.536 (216) for nye systemer |
| r | Blokstørrelse | Blokmixer-størrelse. RAM = 128 × N × r bytes. Øgning øger RAM proportionalt | 8 | 8 (ændres sjældent) |
| p | Paralleliseringsfaktor | Multiplicerer CPU-belastningen sekventielt. Øger ikke hukommelse | 1 | 1 |
| keylen | Outputlængde | Længden på den afledte nøgle i bytes. Gemmes som hex | 32 bytes (256 bit) | 64 bytes (512 bit) |
| salt | Tilfældig salt | Unik per hash, forhindrer rainbow-table-angreb og ligner hashes | 16 bytes tilfældig | 32 bytes tilfældig |
RAM-formlen er afgørende for dimensionering: RAM-forbrug = 128 × N × r bytes. Med N=32.768 og r=8 bruger én scrypt-operation 128 × 32.768 × 8 = 33.554.432 bytes = 32 MB RAM. Med N=65.536 bruger den 64 MB. Med N=131.072 (2^17) bruger den 128 MB. Planér for antallet af samtidige login-forsøg din server skal håndtere: 20 samtidige logins med N=32.768 kræver 640 MB RAM kun til hashing, og det er eksklusiv øvrig serverhukommelse.
Parameteren N skal altid være en potens af 2. Værdier som N=30.000 eller N=50.000 er ugyldige og udløser en RangeError. Node.js validerer ikke altid dette ved opstart, men fejlen viser sig ved den første hash-operation. Valider N-værdien programmatisk ved serveropstart med bitvis AND-tjek: (N & (N - 1)) === 0.
Parameteren maxmem er Node.js-specifik og sætter et loft for, hvor meget RAM scrypt-operationen må bruge. Standard er 32 MB. Det er for lavt til N=65.536 og derover, og du skal sætte den eksplicit. Beregn den som 128 * N * r * 2 for et sikkert buffer.
Trin 3: Hash.js med asynkron scrypt
Node.js’ crypto.scrypt() bruger callback-modellen fra ældre Node.js-API’er. Til moderne async/await-kode wrapper du den med promisify fra node:util. Det er den anbefalede metode frem for at skrive en manuel Promise-wrapper, fordi promisify korrekt håndterer fejltilfælde og bevarer den originale kontekst.
'use strict';
const { scrypt, randomBytes, timingSafeEqual } = require('node:crypto');
const { promisify } = require('node:util');
const scryptAsync = promisify(scrypt);
// Hent parametre fra miljøvariabler - valider dem ved opstart
const N = parseInt(process.env.SCRYPT_N || '32768', 10);
const r = parseInt(process.env.SCRYPT_R || '8', 10);
const p = parseInt(process.env.SCRYPT_P || '1', 10);
const KEYLEN = parseInt(process.env.SCRYPT_KEYLEN || '64', 10);
const SALT_BYTES = parseInt(process.env.SCRYPT_SALT_BYTES || '32', 10);
// Valider N ved opstart
if (!Number.isInteger(N) || N < 16384 || (N & (N - 1)) !== 0) {
throw new Error(`Ugyldig SCRYPT_N=${N}. Kræver potens af 2 og min. 16384.`);
}
const SCRYPT_PARAMS = {
N,
r,
p,
// maxmem skal sættes eksplicit ved høje N-værdier
maxmem: 128 * N * r * 2,
};
/**
* Hash et password med scrypt.
* Returnerer en streng i formatet: salt_hex:hash_hex
*/
async function hashPassword(password) {
if (typeof password !== 'string' || password.length === 0) {
throw new TypeError('Password skal være en ikke-tom streng');
}
const salt = randomBytes(SALT_BYTES);
const derivedKey = await scryptAsync(password, salt, KEYLEN, SCRYPT_PARAMS);
return `${salt.toString('hex')}:${derivedKey.toString('hex')}`;
}
/**
* Verificer et password mod et gemt hash.
* Bruger timingSafeEqual til at undgå timing-angreb.
*/
async function verifyPassword(password, storedHash) {
if (typeof password !== 'string' || typeof storedHash !== 'string') {
throw new TypeError('Ugyldige argumenttyper til verifikation');
}
const colonIndex = storedHash.indexOf(':');
if (colonIndex === -1) {
throw new Error('Ugyldig hash-format: mangler kolon-separator');
}
const saltHex = storedHash.slice(0, colonIndex);
const hashHex = storedHash.slice(colonIndex + 1);
const salt = Buffer.from(saltHex, 'hex');
const storedKeyBuffer = Buffer.from(hashHex, 'hex');
const derivedKey = await scryptAsync(password, salt, KEYLEN, SCRYPT_PARAMS);
// timingSafeEqual er obligatorisk - aldrig === eller Buffer.equals()
if (derivedKey.length !== storedKeyBuffer.length) return false;
return timingSafeEqual(derivedKey, storedKeyBuffer);
}
module.exports = { hashPassword, verifyPassword, SCRYPT_PARAMS };
Tre sikkerhedsbeslutninger i koden fortjener forklaring. For det første bruger vi randomBytes(SALT_BYTES) og ikke Math.random() eller UUID: Math.random() er ikke kryptografisk sikker og kan forudsiges. For det andet bruger vi colonIndex = storedHash.indexOf(':') og ikke storedHash.split(':'): hvis hashen indeholder et kolon i hex-kodningen (usandsynligt men muligt i fremtidige formater), splitter .split(':') forkert. For det tredje kontrollerer vi derivedKey.length !== storedKeyBuffer.length inden timingSafeEqual, fordi timingSafeEqual kaster en fejl hvis bufferne har forskellig størrelse.
Trin 4: Synkron scrypt og hvornår du bruger den
Node.js tilbyder crypto.scryptSync(), 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ælder hele processen, ikke kun den aktuelle request.
Acceptabel brug af scryptSync() er begrænset til: CLI-scripts der kører én hash ad gangen, database-migreringsscripts der kører udenfor åbningstid, og test-setup der skaber brugerkonti inden testene kører. Brug aldrig scryptSync() i request-handlers, middleware, eller nogen kode der kører i en server med samtidige brugere.
'use strict';
// Kun til CLI-scripts - ALDRIG i produktions-request-handlers
const { scryptSync, randomBytes, timingSafeEqual } = require('node:crypto');
function hashPasswordSync(password) {
const salt = randomBytes(32);
const key = scryptSync(password, salt, 64, {
N: 32768,
r: 8,
p: 1,
maxmem: 128 * 1024 * 1024, // 128 MB buffer
});
return `${salt.toString('hex')}:${key.toString('hex')}`;
}
function verifyPasswordSync(password, storedHash) {
const colonIndex = storedHash.indexOf(':');
const salt = Buffer.from(storedHash.slice(0, colonIndex), 'hex');
const stored = Buffer.from(storedHash.slice(colonIndex + 1), 'hex');
const derived = scryptSync(password, salt, 64, {
N: 32768, r: 8, p: 1, maxmem: 128 * 1024 * 1024,
});
return timingSafeEqual(derived, stored);
}
// Eksempel: admin-CLI der opretter en initialkonto
const adminPassword = process.argv[2];
if (!adminPassword) {
console.error('Brug: node create-admin.js ');
process.exit(1);
}
const hash = hashPasswordSync(adminPassword);
console.log('Admin password hash (gem i .env eller database):');
console.log(hash);
// Output (kører i ~200 ms):
// Admin password hash:
// a3f8b2c1d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1:
// b2c3d4e5f6a7b8c9d0e1f2a3b4c5...d8e9f0a1
Trin 5: In-memory brugerstore
I et produkt vil du bruge PostgreSQL, MySQL eller MongoDB, men til demonstration opretter vi en simpel in-memory Map. Strukturen inkluderer login-tæller og lockout-tidsstempel til brute-force-beskyttelse. Erstat dette med en rigtig databaseklasse i trin 10.
'use strict';
// In-memory brugerstore - erstat med rigtig database i produktion
const users = new Map();
function createUser(username, hashedPassword) {
if (users.has(username)) {
throw new Error('Bruger eksisterer allerede');
}
users.set(username, {
username,
password: hashedPassword,
createdAt: new Date().toISOString(),
loginAttempts: 0,
lockedUntil: null,
lastLogin: null,
});
return { username };
}
function getUser(username) {
return users.get(username) || null;
}
function incrementLoginAttempts(username) {
const user = users.get(username);
if (!user) return;
user.loginAttempts += 1;
// Lås kontoen i 15 minutter efter 5 fejlede forsøg
if (user.loginAttempts >= 5) {
user.lockedUntil = new Date(Date.now() + 15 * 60 * 1000).toISOString();
console.warn(`Konto låst: ${username} (${user.loginAttempts} fejlede forsøg)`);
}
users.set(username, user);
}
function resetLoginAttempts(username) {
const user = users.get(username);
if (!user) return;
user.loginAttempts = 0;
user.lockedUntil = null;
user.lastLogin = new Date().toISOString();
users.set(username, user);
}
module.exports = { createUser, getUser, incrementLoginAttempts, resetLoginAttempts };
Trin 6: Express API-server med sikkerhedslag
Serveren implementerer tre sikkerhedslag: IP-baseret rate limiting via express-rate-limit, kontobaseret lockout i brugerstoren og konstante fejlbeskeder der ikke afslører om en bruger eksisterer. Det er nøglen til at forhindre to typer angreb: brute force og bruger-enumeration.
'use strict';
require('dotenv').config();
const express = require('express');
const { rateLimit } = require('express-rate-limit');
const { hashPassword, verifyPassword } = require('./hash');
const { createUser, getUser, incrementLoginAttempts, resetLoginAttempts } = require('./users');
const app = express();
app.use(express.json({ limit: '10kb' })); // Begræns request-størrelse
// Striks rate limiting for login-endpoint
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutter
limit: 10,
standardHeaders: 'draft-7',
legacyHeaders: false,
handler: (req, res) => {
res.status(429).json({
error: 'For mange login-forsøg. Prøv igen om 15 minutter.',
});
},
});
// POST /register - Opret ny bruger
app.post('/register', async (req, res) => {
const { username, password } = req.body || {};
if (!username || typeof username !== 'string' || username.length < 3) {
return res.status(400).json({ error: 'Brugernavn skal være mindst 3 tegn' });
}
if (!password || typeof password !== 'string' || password.length < 12) {
return res.status(400).json({ error: 'Password skal være mindst 12 tegn' });
}
// Saniter brugernavn - kun alfanumeriske tegn og understregning
if (!/^[a-zA-Z0-9_æøåÆØÅ]{3,50}$/.test(username)) {
return res.status(400).json({ error: 'Brugernavn indeholder ugyldige tegn' });
}
if (getUser(username)) {
return res.status(409).json({ error: 'Bruger eksisterer allerede' });
}
try {
const hashedPassword = await hashPassword(password);
createUser(username, hashedPassword);
return res.status(201).json({ message: 'Bruger oprettet', username });
} catch (err) {
console.error('Registreringsfejl:', err.message);
return res.status(500).json({ error: 'Intern serverfejl' });
}
});
// POST /login - Valider brugerlogin
app.post('/login', loginLimiter, async (req, res) => {
const { username, password } = req.body || {};
if (!username || !password) {
return res.status(400).json({ error: 'Brugernavn og password er påkrævet' });
}
const user = getUser(username);
if (!user) {
// Kør dummy-hashing for at undgå timing-baseret bruger-enumeration
await hashPassword('dummy-for-constant-time-' + username);
return res.status(401).json({ error: 'Ugyldige legitimationsoplysninger' });
}
// Tjek om kontoen er låst
if (user.lockedUntil && new Date(user.lockedUntil) > new Date()) {
const unlockTime = new Date(user.lockedUntil).toLocaleTimeString('da-DK');
return res.status(429).json({
error: `Kontoen er låst til ${unlockTime}. Brug "Glemt password" for at nulstille.`,
});
}
try {
const isValid = await verifyPassword(password, user.password);
if (!isValid) {
incrementLoginAttempts(username);
return res.status(401).json({ error: 'Ugyldige legitimationsoplysninger' });
}
resetLoginAttempts(username);
return res.status(200).json({ message: 'Login lykkedes', username });
} catch (err) {
console.error('Login-fejl:', err.message);
return res.status(500).json({ error: 'Intern serverfejl' });
}
});
// Health-check endpoint
app.get('/health', (req, res) => {
const memUsage = process.memoryUsage();
res.json({
status: 'ok',
scrypt: { N: parseInt(process.env.SCRYPT_N), r: parseInt(process.env.SCRYPT_R) },
memory: { heapUsedMB: Math.round(memUsage.heapUsed / 1024 / 1024) },
});
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server kører på port ${PORT}`);
console.log(`Scrypt: N=${process.env.SCRYPT_N || 32768}, r=${process.env.SCRYPT_R || 8}, p=${process.env.SCRYPT_P || 1}`);
console.log(`RAM per hash: ${Math.round(128 * parseInt(process.env.SCRYPT_N || 32768) * 8 / 1024 / 1024)} MB`);
});
module.exports = app;
Det vigtigste sikkerhedsmønster 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.
Trin 7: Start og test serveren
Start serveren og test alle endpoints med curl:
npm start
# Output:
# Server kører på port 3000
# Scrypt: N=32768, r=8, p=1
# RAM per hash: 32 MB
# Registrer ny bruger (tag tid - det tager ~300 ms)
time curl -X POST http://localhost:3000/register \
-H "Content-Type: application/json" \
-d '{"username":"alice","password":"SuperHemmeligtKodeord2026!"}'
# Output: {"message":"Bruger oprettet","username":"alice"}
# Tid: real 0m0.312s
# Log ind med korrekt password
curl -X POST http://localhost:3000/login \
-H "Content-Type: application/json" \
-d '{"username":"alice","password":"SuperHemmeligtKodeord2026!"}'
# Output: {"message":"Login lykkedes","username":"alice"}
# Forkert password
curl -X POST http://localhost:3000/login \
-H "Content-Type: application/json" \
-d '{"username":"alice","password":"ForkertKodeord"}'
# Output: {"error":"Ugyldige legitimationsoplysninger"}
# Ikke-eksisterende bruger (samme svartid som forkert password)
curl -X POST http://localhost:3000/login \
-H "Content-Type: application/json" \
-d '{"username":"findesikke","password":"test"}'
# Output: {"error":"Ugyldige legitimationsoplysninger"}
# Health-check
curl http://localhost:3000/health
# Output: {"status":"ok","scrypt":{"N":32768,"r":8},"memory":{"heapUsedMB":42}}
Svartiden på 200-400 ms er normal og tilsigtet. Det er scrypt der arbejder. Forsøg aldrig at reducere svartiden ved at sænke N, det svækker sikkerheden. Design i stedet frontend til at vise en loading-indikator og undgå HTTP-timeouts under 5 sekunder for auth-endpoints.
Trin 8: Benchmarking til parametervalg
Kør dette benchmark-script direkte på din produktionsserver for at finde den rigtige N-værdi. Kør det under repræsentativ serverbelastning for at simulere det reelle miljø. Resultater varierer meget mellem en cloud-VM, en dedikeret server og serverless:
'use strict';
const { scrypt, randomBytes } = require('node:crypto');
const { promisify } = require('node:util');
const scryptAsync = promisify(scrypt);
async function benchmark(N, iterations = 5) {
const password = 'BenchmarkPassword123!Sikker';
const salt = randomBytes(32);
const maxmem = 128 * N * 8 * 2;
const times = [];
for (let i = 0; i < iterations; i++) {
const start = process.hrtime.bigint();
await scryptAsync(password, salt, 64, { N, r: 8, p: 1, maxmem });
const end = process.hrtime.bigint();
times.push(Number(end - start) / 1_000_000);
}
const avg = times.reduce((a, b) => a + b, 0) / times.length;
const min = Math.min(...times);
const max = Math.max(...times);
const memMB = (128 * N * 8) / (1024 * 1024);
console.log(
`N=${String(N).padStart(7)} | RAM: ${String(memMB).padStart(4)} MB | ` +
`Gns: ${avg.toFixed(0).padStart(4)} ms | Min: ${min.toFixed(0).padStart(4)} ms | Max: ${max.toFixed(0).padStart(4)} ms`
);
return avg;
}
(async () => {
const os = require('node:os');
console.log(`Platform: ${os.type()} ${os.arch()} | Kerner: ${os.cpus().length} | RAM: ${Math.round(os.totalmem()/1024/1024)} MB`);
console.log(`Node.js: ${process.version} | Benchmark-parametre: r=8, p=1, keylen=64, 5 iterationer\n`);
console.log('N | RAM | Gns. tid | Min. tid | Maks. tid');
console.log('----------|---------|----------|----------|----------');
for (const N of [8192, 16384, 32768, 65536, 131072]) {
await benchmark(N);
}
console.log('\nAnbefalede valg:');
console.log(' Serverless / lav RAM (<512 MB): N=16384');
console.log(' Standardserver (1-4 GB RAM): N=32768 (OWASP-minimum)');
console.log(' Sikker server (8+ GB RAM): N=65536 (anbefalet 2026)');
console.log(' Høj sikkerhed / lav trafik: N=131072');
})();
// Typisk output på en moderne server:
// N= 8,192 | RAM: 8 MB | Gns: 51 ms | Min: 49 ms | Maks: 54 ms
// N= 16,384 | RAM: 16 MB | Gns: 102 ms | Min: 99 ms | Maks: 107 ms
// N= 32,768 | RAM: 32 MB | Gns: 205 ms | Min: 201 ms | Maks: 211 ms
// N= 65,536 | RAM: 64 MB | Gns: 413 ms | Min: 409 ms | Maks: 419 ms
// N= 131,072 | RAM: 128 MB | Gns: 847 ms | Min: 841 ms | Maks: 858 ms
Scrypt vs bcrypt vs Argon2: direkte sammenligning
Valget af password-hashing-algoritme er én af de vigtigste sikkerhedsbeslutninger i en applikation. Her er en objektiv sammenligning af de tre OWASP-anbefalede algoritmer:
| Egenskab | scrypt (N=32768) | bcrypt (cost=12) | Argon2id (t=2, m=65536) |
|---|---|---|---|
| RAM-forbrug | ~32 MB | <1 MB | ~64 MB |
| CPU-tid (moderne server) | 200-400 ms | 250-350 ms | 200-500 ms |
| GPU-modstand (RTX 4090) | ~100-200 hash/sek | ~10.000 hash/sek | ~50-100 hash/sek |
| ASIC-modstand | Høj | Lav (ASIC-chips eksisterer) | Meget høj |
| Node.js built-in | Ja (node:crypto) | Nej (npm: bcrypt) | Nej (npm: argon2, native C++) |
| OWASP-prioritet 2025 | 2. prioritet | 3. prioritet | 1. prioritet (foretrukket) |
| Password-maksimallængde | Ingen grænse | 72 bytes (kritisk fejl!) | Ingen grænse |
| RFC-standard | RFC 7914 (2016) | Ingen RFC | RFC 9106 (2021) |
| Alpine Linux Docker | Virker (built-in) | Kræver build-essential | Kræver build-essential + Python |
| Password Hashing Competition | Finalist | Ikke deltaget | Vinder (2015) |
bcrypts 72-byte-grænse 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å 100 tegn tror de er beskyttede, men en angriber der ved om denne grænse kan reducere angrebet markant. Scrypt og Argon2 har ingen sådan begrænsning.
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æver ingen native kompilering og er det bedste valg for teams der prioriterer zero-dependency og reproducerbare builds. Se Ed25519 i Node.js for et lignende eksempel på effektiv built-in kryptografi.
Trin 9: Lazy migration fra bcrypt til scrypt
Når du migrerer fra bcrypt til scrypt, skal du aldrig prøve at konvertere eksisterende bcrypt-hashes direkte. Du har ikke adgang til de originale passwords, og du bør heller ikke have det. Den korrekte strategi er lazy migration: opdater hashen ved næste succesfulde login, og gem et versionspræfiks i hash-feltet så du kan håndtere begge formater parallelt.
'use strict';
const { scrypt, randomBytes, timingSafeEqual } = require('node:crypto');
const { promisify } = require('node:util');
const scryptAsync = promisify(scrypt);
// Hash-versioner og deres parametre
const VERSIONS = {
'scrypt-v1': { N: 16384, r: 8, p: 1, keylen: 64, maxmem: 33554432 },
'scrypt-v2': { N: 32768, r: 8, p: 1, keylen: 64, maxmem: 67108864 },
};
const CURRENT_VERSION = 'scrypt-v2';
function detectVersion(storedHash) {
for (const version of Object.keys(VERSIONS)) {
if (storedHash.startsWith(version + ':')) return version;
}
return null; // Ukendt format (f.eks. bcrypt med $2b$)
}
async function verifyScryptHash(password, storedHash) {
const version = detectVersion(storedHash);
if (!version) return { valid: false, needsMigration: false, error: 'Ukendt hash-format' };
const params = VERSIONS[version];
const hashBody = storedHash.slice(version.length + 1); // Fjern "scrypt-vX:"
const colonIdx = hashBody.indexOf(':');
const salt = Buffer.from(hashBody.slice(0, colonIdx), 'hex');
const stored = Buffer.from(hashBody.slice(colonIdx + 1), 'hex');
const derived = await scryptAsync(password, salt, params.keylen, params);
if (derived.length !== stored.length) return { valid: false, needsMigration: false };
const isValid = timingSafeEqual(derived, stored);
if (!isValid) return { valid: false, needsMigration: false, newHash: null };
const needsMigration = version !== CURRENT_VERSION;
let newHash = null;
if (needsMigration) {
const np = VERSIONS[CURRENT_VERSION];
const newSalt = randomBytes(32);
const newKey = await scryptAsync(password, newSalt, np.keylen, np);
newHash = `${CURRENT_VERSION}:${newSalt.toString('hex')}:${newKey.toString('hex')}`;
}
return { valid: true, needsMigration, newHash };
}
async function createScryptHash(password) {
const np = VERSIONS[CURRENT_VERSION];
const salt = randomBytes(32);
const key = await scryptAsync(password, salt, np.keylen, np);
return `${CURRENT_VERSION}:${salt.toString('hex')}:${key.toString('hex')}`;
}
module.exports = { verifyScryptHash, createScryptHash };
// Brug i login-flowet:
// const { valid, needsMigration, newHash } = await verifyScryptHash(password, user.password_hash);
// if (valid && needsMigration && newHash) {
// await db.query('UPDATE users SET password_hash=$1 WHERE id=$2', [newHash, user.id]);
// }
Lazy migration har en vigtig egenskab: inaktive brugere migrerer aldrig automatisk. Det er acceptabelt, men du bør sætte en deadline (f.eks. 6 måneder) og tvinge brugere der ikke har logget ind til at nulstille password via e-mail. Kombiner versionspræfikset med en migreringsdato i databasen for at spore fremskridtet.
Trin 10: PostgreSQL-integration
I produktionssystemer gemmer du hashes i en relationsdatabase. Her er det komplette databaselag til PostgreSQL med beskyttelse mod SQL-injection via parametriserede forespørgsler. Brug vores guide til SQL Injection i Node.js for at forstå, hvorfor stringinterpolation i SQL-forespørgsler er farligt.
'use strict';
const { Pool } = require('pg');
const { hashPassword, verifyPassword } = require('./hash');
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
max: 10, // Maks. 10 databaseforbindelser i poolen
ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: true } : false,
});
// Kør ved serveropstart
const CREATE_TABLE_SQL = `
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(),
last_login TIMESTAMPTZ,
login_attempts SMALLINT DEFAULT 0,
locked_until TIMESTAMPTZ
);
CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
`;
async function initDatabase() {
const client = await pool.connect();
try {
await client.query(CREATE_TABLE_SQL);
console.log('Databaseskema klar');
} finally {
client.release();
}
}
async function registerUser(username, password) {
const hashedPassword = await hashPassword(password);
const result = await pool.query(
`INSERT INTO users (username, password_hash)
VALUES ($1, $2)
RETURNING id, username, created_at`,
[username, hashedPassword]
);
return result.rows[0];
}
async function loginUser(username, password) {
const result = await pool.query(
`SELECT id, username, password_hash, login_attempts, locked_until
FROM users WHERE username = $1`,
[username]
);
if (result.rows.length === 0) {
// Kald scrypt alligevel for konstant svartid
await hashPassword('timing-protection-dummy');
return null;
}
const user = result.rows[0];
if (user.locked_until && new Date(user.locked_until) > new Date()) {
const err = new Error('Kontoen er låst');
err.lockedUntil = user.locked_until;
throw err;
}
const isValid = await verifyPassword(password, user.password_hash);
if (!isValid) {
await pool.query(
`UPDATE users
SET login_attempts = login_attempts + 1,
locked_until = CASE
WHEN login_attempts >= 4
THEN NOW() + INTERVAL '15 minutes'
ELSE locked_until
END
WHERE username = $1`,
[username]
);
return null;
}
await pool.query(
`UPDATE users
SET login_attempts = 0, locked_until = NULL, last_login = NOW()
WHERE username = $1`,
[username]
);
return { id: user.id, username: user.username };
}
module.exports = { initDatabase, registerUser, loginUser };
Trin 11: Test med Node.js' indbyggede testmodul
Node.js 20+ inkluderer et built-in testmodul der eliminerer behovet for Jest eller Mocha til simple enhedstests. Brug N=1024 i test-miljøet for at holde testen under 1 sekund, og sæt N=32.768 til integrationstests der verificerer den fulde produktionskonfiguration.
'use strict';
const { test, describe } = require('node:test');
const assert = require('node:assert/strict');
// Lavere N til unit-tests - undgå timeouts i CI/CD
process.env.SCRYPT_N = '1024';
process.env.SCRYPT_R = '8';
process.env.SCRYPT_P = '1';
process.env.SCRYPT_KEYLEN = '64';
process.env.SCRYPT_SALT_BYTES = '32';
const { hashPassword, verifyPassword } = require('./hash');
describe('hashPassword', () => {
test('returnerer salt:hash format', async () => {
const hash = await hashPassword('TestPassword123!');
assert.equal(typeof hash, 'string');
const colonIdx = hash.indexOf(':');
assert.ok(colonIdx > 0, 'Hash skal indeholde ":"');
assert.equal(hash.slice(0, colonIdx).length, 64); // 32 bytes = 64 hex
assert.equal(hash.slice(colonIdx + 1).length, 128); // 64 bytes = 128 hex
});
test('producerer forskellig hash for samme password pga. tilfældig salt', async () => {
const h1 = await hashPassword('SammePassword!');
const h2 = await hashPassword('SammePassword!');
assert.notEqual(h1, h2);
// Men salt-prefixet alene er også unikt:
assert.notEqual(h1.split(':')[0], h2.split(':')[0]);
});
test('kaster TypeError for tomt password', async () => {
await assert.rejects(() => hashPassword(''), TypeError);
await assert.rejects(() => hashPassword(null), TypeError);
await assert.rejects(() => hashPassword(12345), TypeError);
await assert.rejects(() => hashPassword(undefined), TypeError);
});
test('hashPassword håndterer ikke-ASCII-tegn korrekt', async () => {
const hash = await hashPassword('KodeordMedÆØÅ!123');
assert.ok(hash.includes(':'));
});
});
describe('verifyPassword', () => {
test('verificerer korrekt password', async () => {
const pw = 'KorrektPassword2026!';
const hash = await hashPassword(pw);
assert.equal(await verifyPassword(pw, hash), true);
});
test('afviser forkert password', async () => {
const hash = await hashPassword('RigtigtPassword!');
assert.equal(await verifyPassword('ForkertPassword!', hash), false);
});
test('afviser tomt password mod gyldig hash', async () => {
const hash = await hashPassword('Rigtigt!');
assert.equal(await verifyPassword('', hash), false);
});
test('kaster fejl ved manglende kolon-separator', async () => {
await assert.rejects(() => verifyPassword('test', 'ingenkolonher'), Error);
});
test('kaster TypeError ved ugyldig input-type', async () => {
const hash = await hashPassword('test!12345678');
await assert.rejects(() => verifyPassword(null, hash), TypeError);
await assert.rejects(() => verifyPassword('test', null), TypeError);
});
});
// Kør med: node --test src/hash.test.js
// Forventet output:
# ▶ hashPassword
# ✔ returnerer salt:hash format (12.3ms)
# ✔ producerer forskellig hash for samme password (24.8ms)
# ✔ kaster TypeError for tomt password (0.2ms)
# ✔ hashPassword håndterer ikke-ASCII-tegn korrekt (12.1ms)
# ▶ verifyPassword
# ✔ verificerer korrekt password (12.4ms)
# ✔ afviser forkert password (12.2ms)
# ✔ afviser tomt password mod gyldig hash (0.1ms)
# ✔ kaster fejl ved manglende kolon-separator (0.1ms)
# ✔ kaster TypeError ved ugyldig input-type (0.1ms)
# ℹ tests 9, pass 9
Trin 12: Produktionscheckliste
Inden deployment til produktion, gennemgå denne checkliste punkt for punkt:
| Sikkerhedspunkt | Krav | Verifikation |
|---|---|---|
| N-parameter | Min. 32.768 i produktion | Kør benchmark-scriptet på produktionsserver |
| maxmem | Min. 128 × N × r × 1,5 bytes | Tjek ingen maxmem exceeded-fejl i logs |
| Salt | randomBytes(32), unik per hash | Verificer at ingen to hashes har samme salt-præfix |
| timingSafeEqual | Altid ved hash-verifikation | Code review: søg efter Buffer.equals og === |
| Rate limiting | Max 10 login-forsøg/IP/15 min | Test med Apache Bench: ab -n 15 -c 1 ... |
| Kontobaseret lockout | 5 forsøg, 15 min lockout | Test med 6 forkerte passwords for samme bruger |
| Dummy-hashing | Ved ukendt brugernavn | Mål svartid for eksisterende vs. ikke-eksisterende bruger |
| HTTPS | TLS 1.3 på alle endpoints | curl --verbose https://... (tjek TLS-version) |
| Password-minimumslængde | Min. 12 tegn (NIST SP 800-63B) | Test med 11-tegns password - skal afvises |
| Ingen hash i API-response | password_hash aldrig eksponeret | Tjek alle res.json()-kald i koden |
5 typiske fejl med scrypt i Node.js
Disse fejl optræder hyppigt i kodereviews og kan nullificere scrypts sikkerhedsfordele selv med ellers korrekt implementering.
Fejl 1: Manglende maxmem-konfiguration. Node.js' standard maxmem er 32 MB. Med N=65.536 og r=8 kræver scrypt 64 MB, og du får fejlen Error: Invalid scrypt params: maxmem = 33554432 exceeded. Denne fejl opstår typisk i produktion med første deployment af nye N-parametre, fordi udviklermaskinen havde en anden Node.js-version eller en nyere standard. Sæt altid maxmem eksplicit til mindst 128 * N * r * 2 for at have buffer til OS-overhead.
Fejl 2: Statisk eller forudsigeligt salt. Et fast salt som 'mysalt', brugernavnet som salt, eller et UUID fra en ekstern kilde gør alle hashes beregnelige på forhånd via rainbow tables. Med et forudsigeligt salt kan en angriber der stjæler databasen beregne alle hash-værdier for en stor ordbog af passwords én gang og derefter slå dem op i realtid. Brug altid randomBytes(32) og gem saltet i det samme felt som hashen.
Fejl 3: Sammenligning med === eller Buffer.equals(). Sammenligning med === eller Buffer.equals() returnerer false så snart det første forskellige byte er fundet. En angriber kan måle responstiden med høj præcision (mikrosekunder) og gradvist gætte den korrekte hash ved at sende mange requests og observere tidsvariationer. timingSafeEqual() bruger altid præcis den samme tid uanset, hvor hashes adskiller sig.
// FORKERT - lækker timing-information
if (derivedKey.toString('hex') === storedHash) { ... }
if (derivedKey.equals(storedKeyBuffer)) { ... }
// KORREKT - konstant tid
const { timingSafeEqual } = require('node:crypto');
if (derivedKey.length !== storedKeyBuffer.length) return false;
if (timingSafeEqual(derivedKey, storedKeyBuffer)) { ... }
Fejl 4: Test-N-værdier i produktion. 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øvariabel der ikke sættes korrekt i produktionsmiljøet, ender du med N=1024 eller endda N=undefined, der typisk defaulter til en lav fejlsikker værdi. Valider altid N ved opstart med en hard-coded minimumsgrænse.
Fejl 5: scryptSync i Express-request-handlers. crypto.scryptSync() blokerer Node.js' event loop i 200-400 ms. I den tid kan serveren ikke håndtere nogen andre requests, ikke engang health-checks, statiske filer eller database-keepalive-forespørgsler. 10 samtidige brugere der logger ind giver 2-4 sekunders total blokering per request i køen. Brug altid scryptAsync (promisify-wrapped version) i alle web-request-handlers.
8 fejlfindingsscenarier
Her er en guide til de hyppigste fejlbeskeder og problemer ved brug af scrypt i Node.js-produktion:
| Fejlbesked eller symptom | Årsag | Løsning |
|---|---|---|
Error: Invalid scrypt params: maxmem exceeded | N × r × 128 bytes overstiger maxmem-grænsen | Sæt maxmem: 128 * N * r * 2 i SCRYPT_PARAMS |
Error: scrypt failed (generisk OpenSSL-fejl) | Utilstrækkelig RAM på serveren, typisk ved N=131072 | Reducer N eller forøg serverens RAM-allokering |
TypeError: Invalid data, key must be a string or an instance of Buffer | Password-argumentet er null, undefined eller et tal | Valider input-typer inden scrypt-kaldet med typeof |
| Hash-verifikation returnerer altid false | Encoding-mismatch (utf8 vs. hex) eller forkert split-logik | Brug konsekvent hex-encoding; brug indexOf(':') ikke split(':') |
RangeError: scrypt work factors are out of range | N er ikke en potens af 2, eller er 0, negativt eller NaN | Valider: N > 0 && (N & (N-1)) === 0 |
| Server svarer ikke under høj login-trafik | Mange samtidige scrypt-operationer blokerer eller sætter serveren under pres | Reducer N, implementér rate limiting og Worker Thread-pool |
| Out-of-memory-crash ved spidsbelastning | N × antal samtidige login × 128 × r bytes overstiger serverens RAM | Sæt N=16384 i serverless; brug job-queue til batching af auth |
ERR_INVALID_ARG_TYPE: keylen must be an integer | SCRYPT_KEYLEN er en streng pga. manglende parseInt() | Brug altid parseInt(process.env.SCRYPT_KEYLEN, 10) |
Generel diagnostik: kør Node.js med node --trace-warnings src/server.js for alle runtime-advarsler. Mål hukommelsesforbrug under test med Apache Bench: ab -n 50 -c 10 -p login-body.json -T application/json http://localhost:3000/login og overvåg process.memoryUsage().heapUsed via health-endpoint.
Ved mystiske timing-fejl under migration: kontrollér at alle Node.js-versioner i dit produktionsmiljø er identiske. Node.js 10.5.0-11.x har en ældre OpenSSL-version med anderledes standarder for maxmem. Containerisér altid med en pinned Node.js-version (FROM node:22.14-alpine3.20) for reproducerbare builds.
Avancerede tips til produktionssystemer
Worker Thread-isolering: For applikationer med høj login-trafik bør 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åndtering. Implementér en simpel round-robin-scheduler der fordeler hash-requests til workers via parentPort.postMessage(). Med denne arkitektur kan main thread fortsat betjene nye requests, health-checks og database-queries under intens login-belastning.
Pepper som ekstra beskyttelseslag: En "pepper" er et hemmeligt, server-side-suffix tilføjet til passwordet inden hashing. I modsætning til salt gemmes pepper ikke i databasen, men kun i en miljøvariabel eller et Key Management System. Selv hvis en angriber stjæler hele databasen, kan de ikke cracke passwords uden at kende pepper. Brug pepper med: const peppered = password + process.env.PASSWORD_PEPPER og hash derefter peppered. Gem pepper aldrig i kode-repositoriet og roter den periodisk med lazy migration.
Scrypt til nøgleafledning (ikke kun password-hashing): Scrypt er standardiseret i RFC 7914 primært som en nøgleafledningsfunktion (KDF). WPA3-netværk bruger scrypt til at afkryptere den pre-shared nøgle fra et password. Du kan replikere dette i Node.js til envelope encryption: brug scrypt til at generere en 32-byte AES-nøgle fra et brugerpassword og krypter dine data med AES-256-GCM. Scrypt-nøglen bør genereres med et unikt salt per krypteringsoperation og gemmes ved siden af de krypterede data. Kombiner med WebCrypto API i Node.js til selve krypteringen.
Adaptive parameter-revision: Hardware bliver kraftigere hvert år. Angribere kan i 2026 bryde N=8.192 hashes 16 gange hurtigere end i 2018. Planlæg en halvårlig revision af N-parameteren og dokumentér versionen i databaseskemaet. Som tommelfingerregel: fordobl N hvert 2. år for at modsvare Moores lov. Med versionsbaseret lazy migration (se trin 9) er opgradering usynlig for brugerne.
Integreret audit-log: Log alle login-hændelser til en separat tabel med timestamp, username, IP-adresse, User-Agent og resultat (success/failure). Inkludér aldrig password eller password_hash i logfiler. Brug HMAC-SHA256 som beskrevet i vores Node.js-guide til at signere audit-log-poster kryptografisk, så en kompromitteret database ikke kan give en angriber mulighed for at slette beviser.
Containerisering med korrekt RAM-allokering: I Docker og Kubernetes skal du eksplicit allokere nok RAM til scrypt. Med N=32.768 og 20 samtidige login kræver du 640 MB kun til hashing. Sæt resources.limits.memory i Kubernetes-manifestet til mindst 1 GB, og sæt Node.js heap-limit med --max-old-space-size=768 for at undgå OOM-kills. Overvåg container_memory_usage_bytes i Prometheus med en alert ved over 80% af grænsen.
Relateret indhold
Relateret dækning
- Ed25519 i Node.js: kryptografiske signaturer i 12 trin
- WebCrypto API i Node.js: AES-GCM og ECDSA i 12 trin
- HMAC i Node.js: webhook-signaturer i 12 trin
- WebAuthn i Node.js: passwordless login i 12 trin
- SQL Injection i Node.js: 12 trin til sikker database
- Kodeordssikkerhed: hvad der egentlig beskytter konti
- Kryptografi: hashfunktioner, SHA og digital tillid
FAQ
Hvornår vælger jeg scrypt frem for Argon2id? Vælg scrypt, når du vil undgå native C++-afhængigheder. Argon2 kræver npm-pakken argon2 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æver nul afhængigheder. Argon2id er teknisk overlegen, men scrypt er fuldt OWASP-godkendt til produktion.
Hvad er den rigtige N-værdi til produktion i 2026? OWASP anbefaler N=32.768 (2^15) som minimum. For nye systemer med god RAM er N=65.536 (2^16) anbefalet. Kør benchmark-scriptet fra trin 8 på din faktiske produktionsserver og vælg den højeste N der holder svartiden under 500 ms under spidsbelastning. En server med 4 GB RAM og 4 kerner håndterer typisk N=65.536 med op til 15 samtidige logins.
Er scrypt sårbar over for kvantecomputerangreb? For password-hashing er kvantecomputing ikke et praktisk problem i den nærmeste fremtid. Scrypts hukommelseshardhed er ikke relevant mod Grover-algoritmen, men brug af 64-byte output (keylen=64) og passwords med mindst 16 tilfældige tegn modvirker den effektive sikkerhedsreduktion som kvantecomputing giver.
Kan jeg bruge scrypt til API-nøgle-hashing? Nej. API-nøgler verificeres ved hvert request, og scrypts 200-400 ms hash-tid er helt uacceptabel ved høj API-trafik. Brug HMAC-SHA256 med et hemmeligt server-key til API-nøgle-verifikation: det beregnes på under 0,1 ms. Scrypt er kun egnet til passwords der autentificeres ved login.
Hvad er forskellen på scrypt og PBKDF2? PBKDF2 er kun CPU-tung og paralleliserer effektivt på 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.
Hvad sker der, hvis serveren løber tør for RAM under scrypt? Node.js kaster Error: scrypt failed eller maxmem exceeded. Serveren crasher ikke, men den aktuelle request fejler. Implementér circuit breaker: overvåg process.memoryUsage().heapUsed og returner HTTP 503 midlertidigt, hvis hukommelsesforbruget overstiger 80% af det tilgængelige. Log disse hændelser til alerting.
Virker scrypt i Cloudflare Workers? Nej. Cloudflare Workers' WebCrypto API understøtter ikke scrypt. PBKDF2 med 310.000 iterationer er Workers-alternativet. Alternativt kan du bruge Cloudflare Zero Trust og Cloudflare Access til at håndtere authentication eksternt og kun kalde din Node.js-server til den egentlige scrypt-verifikation. Se MDN SubtleCrypto.deriveKey() for Workers-kompatible KDF-algoritmer.
Kan jeg gemme scrypt-hashes i MongoDB? Ja. Hashes er hex-kodede strenge i formatet salt:hash (64 + 1 + 128 = 193 tegn med standard SALT_BYTES=32 og KEYLEN=64). Gem dem i et String-felt med maxlength: 300 for fremtidig fleksibilitet. Sæt aldrig et unikt index på password-hash-feltet og inkludér aldrig feltet i API-responses.




