scrypt er en minnehard nøkkelderiveringsfunksjon som er innebygd i Node.js sin crypto-modul, og den er et av de tre algoritmevalgene OWASP fortsatt anbefaler for passordhashing i 2026. Denne veiledningen viser deg hvordan du bygger sikker scrypt passordhashing i Node.js fra bunnen av: salt, kostnadsparametre, lagringsformat, konstanttidssammenligning og en komplett, gjenbrukbar modul du kan koble rett inn i en Express-app. Du trenger ingen tredjepartspakker, kun det som følger med Node.js.

Regn med rundt 30 minutter på å jobbe deg gjennom de 11 stegene. Når du er ferdig, har du et komplett prosjekt som hasher passord med OWASP-godkjente parametre, verifiserer dem trygt mot tidsangrep, og oppgraderer gamle hasher automatisk når du øker sikkerhetsmarginen.

scrypt passordhashing i Node.js: rask oversikt

Et passord skal aldri lagres i klartekst. Det skal heller ikke krypteres, for kryptering kan reverseres med en nøkkel. Riktig fremgangsmåte er å lagre et envegs-avtrykk laget av en passordhashing-funksjon. scrypt ble designet av Colin Percival i 2009 nettopp for dette. Algoritmen er definert i RFC 7914, og den er minnehard, som betyr at den med vilje krever store mengder RAM. Det gjør den dyr å knekke med spesialisert maskinvare som GPU-er og ASIC-er, der minne er den begrensende ressursen.

Node.js har hatt en innebygd implementasjon av scrypt i crypto-modulen helt siden versjon 10. Det betyr at du i 2026 kan hashe passord trygt uten å installere én eneste npm-pakke. Du får tilgang til to funksjoner: crypto.scrypt() (asynkron, anbefalt for servere) og crypto.scryptSync() (synkron, nyttig for skript og tester). Begge bruker den samme algoritmen, men den asynkrone varianten blokkerer ikke event-loopen mens den maler gjennom hundretusenvis av runder.

scrypt hører hjemme i samme familie som kryptografiske hashfunksjoner, men det er en viktig forskjell. En rå hashfunksjon som SHA-256 er bygget for å være rask. Passordhashing skal være tregt og minnekrevende med vilje, slik at en angriper som stjeler databasen din ikke kan gjette milliarder av passord i sekundet. Denne distinksjonen er hele poenget med god passordsikkerhet, og den er grunnen til at du aldri skal hashe passord med en vanlig SHA-funksjon alene.

I denne veiledningen bygger vi en produksjonsklar modul som dekker alt OWASP krever: tilfeldig salt per passord, kalibrerte kostnadsparametre, et selvbeskrivende lagringsformat, konstanttidssammenligning og automatisk rehashing. Vi avslutter med feilsøking, benchmarking og en ærlig sammenligning mot bcrypt og Argon2id.

scrypt vs bcrypt vs Argon2id: hvilken bør du velge?

Før du skriver kode bør du forstå hvor scrypt står blant alternativene. OWASP anbefaler i 2026 tre algoritmer for passordlagring: Argon2id som førstevalg, scrypt der Argon2 ikke er tilgjengelig, og bcrypt for eldre systemer. Tabellen under oppsummerer de praktiske forskjellene for en Node.js-utvikler.

EgenskapscryptbcryptArgon2id
Innebygd i Node.jsJa (crypto-modulen)Nei (npm-pakke)Nei (npm-pakke)
MinnehardJaNeiJa
GPU-/ASIC-motstandHøyMiddelsSvært høy
Justerbart minneJa (N, r)NeiJa
Maks passordlengdeIngen reell grense72 byteIngen reell grense
OWASP 2026-rangeringAndrevalgEldre systemerFørstevalg
Avhengigheter å vedlikeholdeNullÉn (med binær)Én (med binær)

scrypt sin sterkeste fordel i Node.js er at den er innebygd. Du slipper å vedlikeholde en npm-avhengighet som kompilerer en native binær, noe som ofte skaper trøbbel i Docker-bilder, serverless-miljøer og ved Node-oppgraderinger. bcrypt har en beryktet begrensning: den ignorerer alt etter de første 72 bytene av passordet, noe scrypt ikke gjør. Argon2id vinner på ren motstandskraft, men krever pakken argon2 og en byggekjede.

Velg scrypt når du vil ha minnehard sikkerhet uten eksterne avhengigheter, for eksempel i et lite team, et bibliotek du distribuerer videre, eller et miljø der native moduler er vanskelige. Velg Argon2id når du har full kontroll over byggemiljøet og vil ha det aller sterkeste forsvaret. Uansett valg er prinsippene i denne veiledningen, salt, kalibrering, konstanttidssammenligning og rehashing, de samme.

Forutsetninger: versjoner og verktøy

Du trenger lite for å følge denne veiledningen, men versjonene betyr noe. scrypt-API-et er stabilt, men nyere Node.js-versjoner har bedre ytelse og sikkerhetsoppdateringer.

  • Node.js 24 LTS (anbefalt i 2026). Node.js 22 LTS fungerer også. Alt fra Node.js 10 og oppover har crypto.scrypt, men bruk en aktiv LTS-linje for sikkerhetsoppdateringer. Se Node.js sin utgivelsesplan.
  • npm 10 eller nyere (følger med Node.js 24). Vi bruker npm kun til prosjektoppsett, ikke til avhengigheter for hashingen.
  • En kodeeditor som VS Code, og en terminal.
  • Grunnleggende JavaScript og kjennskap til async/await og Promises.
  • Valgfritt: Express 5 for innloggings-eksempelet i steg 8.

Sjekk Node-versjonen din før du begynner. Kjør kommandoen under, og bekreft at du har minst versjon 22.

$ node --version
v24.4.0

$ npm --version
10.9.2

Hvis du ser en versjon eldre enn 22, installer en nyere fra nodejs.org eller via en versjonsmanager som nvm. Hele resten av veiledningen bruker kun den innebygde node:crypto-modulen, så det er ingen npm install for selve hashingen.

Slik fungerer scrypt: kostnadsparametrene N, r og p

scrypt styres av tre kostnadsparametre. Å forstå dem er forskjellen mellom en trygg implementasjon og en falsk trygghet. I Node.js sitt options-objekt heter de cost, blockSize og parallelization, men du kan også bruke aliasene N, r og p.

ParameterNode-navnAliasHva den styrerEffekt
CPU/minne-kostnadcostNAntall iterasjoner, må være en toer-potensDobling dobler både tid og minne
BlokkstørrelseblockSizerStørrelsen på minneblokkeneØker minnebruk lineært
ParallelliseringparallelizationpAntall uavhengige beregningerØker CPU-bruk lineært
MinnegrensemaxmemTak for minnebruk i byteStandard 32 MiB, må heves
NøkkellengdekeylenLengden på avledet nøkkel i byte64 byte er et godt valg

Minnebruken til scrypt følger formelen 128 * N * r byte. Med OWASP sin 2026-anbefaling på N = 131072 (2^17) og r = 8 blir det 128 * 131072 * 8 = 134 217 728 byte, altså omtrent 128 MiB per hashing. Dette tallet er kritisk, for Node.js setter en standard maxmem-grense på 32 MiB. Hvis du ber om mer minne enn det uten å heve maxmem, kaster Node en feil i stedet for å fortsette med svakere parametre. Det er en sikkerhetsfunksjon, ikke en bug.

Den viktigste innsikten er at N er den dominerende sikkerhetsspaken. En dobling av N dobler både regnetiden og minnebruken, noe som dobler kostnaden for en angriper. r og p justerer du sjelden bort fra 8 og 1. Vi setter konkrete tall i steg 4, men forstå nå at høyere N gir bedre sikkerhet helt til hashing tar så lang tid at det går ut over brukeropplevelsen din.

Steg 1: Sett opp prosjektet

Opprett en ny mappe og initialiser et Node-prosjekt. Vi bruker ES-moduler (type: "module") slik at vi kan bruke moderne import-syntaks.

$ mkdir scrypt-passord && cd scrypt-passord
$ npm init -y
$ npm pkg set type=module
$ node --version
v24.4.0

Det er hele oppsettet. Vi installerer ingen avhengigheter for selve hashingen, fordi alt ligger i node:crypto. Opprett en fil som heter password.js. Den blir den gjenbrukbare modulen vi bygger gjennom hele veiledningen, og vi tester den underveis fra et lite skript.

Steg 2: Generer en kryptografisk salt

En salt er en tilfeldig verdi som legges til passordet før hashing. Den sørger for at to brukere med samme passord får helt ulike hasher, og den gjør forhåndsberegnede tabeller (rainbow tables) ubrukelige. Hver bruker skal ha sin egen unike salt, og den må komme fra en kryptografisk sikker kilde.

Bruk crypto.randomBytes(), aldri Math.random(). Math.random() er ikke kryptografisk sikker og kan forutsies. Vi bruker 16 byte (128 bit) salt, som er standarden OWASP anbefaler.

import { randomBytes } from "node:crypto";

// Generer 16 byte (128 bit) kryptografisk tilfeldig salt
const salt = randomBytes(16);
console.log(salt.toString("base64"));
// Eksempel: "9Qk2rN1pY7sV3xB0aZ4cFg=="

Vi lagrer saltet sammen med hashen senere, i klartekst. Det er trygt og helt nødvendig: du trenger nøyaktig samme salt for å verifisere passordet ved innlogging. Salt er ingen hemmelighet, den skal bare være unik og uforutsigbar.

Steg 3: Hash passordet med crypto.scrypt

Nå kobler vi salt og scrypt sammen. crypto.scrypt() er asynkron og bruker en callback, så vi pakker den i et Promise for å kunne bruke async/await. Den fulle signaturen er crypto.scrypt(password, salt, keylen, options, callback).

import { scrypt, randomBytes } from "node:crypto";
import { promisify } from "node:util";

const scryptAsync = promisify(scrypt);

// OWASP 2026-parametre for scrypt
const PARAMS = {
  N: 2 ** 17,        // 131072, CPU/minne-kostnad
  r: 8,              // blokkstørrelse
  p: 1,              // parallellisering
  keylen: 64,        // lengde på avledet nøkkel i byte
  maxmem: 256 * 1024 * 1024, // 256 MiB, rom over 128 MiB-behovet
};

async function hashRaw(password, salt) {
  const derivedKey = await scryptAsync(password, salt, PARAMS.keylen, {
    cost: PARAMS.N,
    blockSize: PARAMS.r,
    parallelization: PARAMS.p,
    maxmem: PARAMS.maxmem,
  });
  return derivedKey; // Buffer på 64 byte
}

const salt = randomBytes(16);
const key = await hashRaw("riktig-hest-batteri-stift", salt);
console.log(key.toString("base64"));

Legg merke til maxmem: 256 * 1024 * 1024. Uten den ville Node kastet feilen Error: Invalid scrypt params, fordi N = 131072 og r = 8 krever rundt 128 MiB, langt over standardgrensen på 32 MiB. Vi setter taket til 256 MiB for å gi rom til litt intern overhead. Output er en 64-byte Buffer, den avledede nøkkelen. Den alene er ikke nok å lagre. Vi trenger også saltet og parametrene, noe vi løser i neste steg.

Steg 4: Velg riktige OWASP-parametre for 2026

OWASP Password Storage Cheat Sheet gir konkrete minimumsverdier for scrypt. Per 2026 er anbefalingen N = 2^17 (131072), r = 8 og p = 1. OWASP lister også likeverdige kombinasjoner med lavere minne og høyere parallellisering, nyttige hvis maskinen din er minnebegrenset.

ProfilNrpMinne (ca.)Bruk
OWASP-anbefaling 2026131072 (2^17)81128 MiBStandard backend-servere
Minnesparende65536 (2^16)8264 MiBBegrenset RAM, beholdt styrke
Lett (likeverdig)32768 (2^15)8332 MiBContainere med stramt minne
For svak (unngå)16384 (2^14)8116 MiBAldri i produksjon

Den praktiske regelen er enkel: velg de høyeste verdiene maskinvaren din tåler uten at innlogging føles treg. Et godt mål er at én hashing tar mellom 200 og 500 millisekunder på produksjonsserveren din. Det er raskt nok for en bruker som logger inn, men smertefullt dyrt for en angriper som prøver milliarder av gjetninger. Vi måler den faktiske tiden i steg 11.

Ikke kopier parametre blindt. En kraftig server tåler N = 2^18 uten problemer, mens en liten serverless-funksjon kanskje må ned på 2^15. Det viktige er at du aldri går under 2^15 i produksjon, og at du lagrer parametrene sammen med hashen slik at du kan oppgradere dem senere.

Steg 5: Lagre hash, salt og parametre i PHC-format

Den avledede nøkkelen er verdiløs uten saltet og parametrene som lagde den. Den ryddigste måten å lagre alt sammen på er PHC-strengformatet, den samme konvensjonen bcrypt og Argon2 bruker. Det pakker algoritme, parametre, salt og hash i én selvbeskrivende streng. Da kan du senere lese hvilke parametre en gammel hash brukte og oppgradere den.

// Bygg en PHC-lignende streng: $scrypt$n=...,r=...,p=...$salt$hash
function encode(derivedKey, salt) {
  const params = `n=${PARAMS.N},r=${PARAMS.r},p=${PARAMS.p}`;
  const saltB64 = salt.toString("base64");
  const hashB64 = derivedKey.toString("base64");
  return `$scrypt$${params}$${saltB64}$${hashB64}`;
}

// Les en PHC-streng tilbake til komponentene
function decode(phc) {
  const parts = phc.split("$"); // ["", "scrypt", "n=...,r=...,p=...", salt, hash]
  if (parts.length !== 5 || parts[1] !== "scrypt") {
    throw new Error("Ugyldig scrypt-hashformat");
  }
  const params = Object.fromEntries(
    parts[2].split(",").map((kv) => {
      const [k, v] = kv.split("=");
      return [k, Number(v)];
    })
  );
  return {
    N: params.n,
    r: params.r,
    p: params.p,
    salt: Buffer.from(parts[3], "base64"),
    hash: Buffer.from(parts[4], "base64"),
  };
}

En ferdig hash ser slik ut i databasen din:

$scrypt$n=131072,r=8,p=1$9Qk2rN1pY7sV3xB0aZ4cFg==$Tm8hX2...64byteIbase64...==

Lagre denne ene strengen i en VARCHAR(255)-kolonne. Du trenger ingen ekstra kolonner for salt eller parametre, alt ligger i strengen. Dette gjør rehashing trivielt, for du kan lese de gamle parametrene rett fra hashen og sammenligne dem med dine gjeldende mål.

Steg 6: Verifiser passord med timingSafeEqual

Verifisering er der mange gjør en farlig feil. Du skal aldri sammenligne hasher med === eller Buffer.equals(). Disse avslutter sammenligningen så snart de finner det første byteparet som ikke stemmer. Forskjellen i tid er målbar, og en tålmodig angriper kan bruke den til å gjette hashen byte for byte. Dette kalles et timing-angrep.

Løsningen er crypto.timingSafeEqual(), som alltid bruker like lang tid uansett hvor i bufferne forskjellen ligger. Funksjonen krever at begge buffere har samme lengde, så vi rehasher det innsendte passordet med de lagrede parametrene før vi sammenligner.

import { timingSafeEqual } from "node:crypto";

async function verify(password, phc) {
  const { N, r, p, salt, hash } = decode(phc);

  const derivedKey = await scryptAsync(password, salt, hash.length, {
    cost: N,
    blockSize: r,
    parallelization: p,
    maxmem: 256 * 1024 * 1024,
  });

  // Konstanttidssammenligning, beskytter mot timing-angrep
  return timingSafeEqual(derivedKey, hash);
}

const phc = "$scrypt$n=131072,r=8,p=1$...salt...$...hash...";
console.log(await verify("riktig-hest-batteri-stift", phc)); // true
console.log(await verify("feil-passord", phc));              // false

Legg merke til at vi bruker hash.length som keylen og leser N, r og p fra den lagrede hashen, ikke fra de gjeldende globale parametrene. Det er avgjørende: en bruker som registrerte seg for et år siden kan ha en hash laget med svakere parametre, og verifisering må bruke akkurat de gamle verdiene for å regne ut samme resultat.

Steg 7: Bygg en komplett, gjenbrukbar passordmodul

Nå samler vi alt i én ferdig password.js du kan importere hvor som helst. Modulen eksporterer tre funksjoner: hash(), verify() og needsRehash(). Dette er det komplette, fungerende prosjektet.

// password.js
import { scrypt, randomBytes, timingSafeEqual } from "node:crypto";
import { promisify } from "node:util";

const scryptAsync = promisify(scrypt);

// Gjeldende OWASP-mål for nye hasher
export const CURRENT = {
  N: 2 ** 17,
  r: 8,
  p: 1,
  keylen: 64,
  saltBytes: 16,
  maxmem: 256 * 1024 * 1024,
};

export async function hash(password) {
  if (typeof password !== "string" || password.length === 0) {
    throw new Error("Passordet må være en ikke-tom streng");
  }
  const salt = randomBytes(CURRENT.saltBytes);
  const key = await scryptAsync(password, salt, CURRENT.keylen, {
    cost: CURRENT.N,
    blockSize: CURRENT.r,
    parallelization: CURRENT.p,
    maxmem: CURRENT.maxmem,
  });
  return `$scrypt$n=${CURRENT.N},r=${CURRENT.r},p=${CURRENT.p}$` +
    `${salt.toString("base64")}$${key.toString("base64")}`;
}

export async function verify(password, phc) {
  if (typeof phc !== "string" || !phc.startsWith("$scrypt$")) {
    return false;
  }
  const parts = phc.split("$");
  if (parts.length !== 5) return false;

  const params = Object.fromEntries(
    parts[2].split(",").map((kv) => kv.split("="))
  );
  const N = Number(params.n);
  const r = Number(params.r);
  const p = Number(params.p);
  const salt = Buffer.from(parts[3], "base64");
  const hash = Buffer.from(parts[4], "base64");

  const key = await scryptAsync(password, salt, hash.length, {
    cost: N, blockSize: r, parallelization: p,
    maxmem: CURRENT.maxmem,
  });
  return key.length === hash.length && timingSafeEqual(key, hash);
}

export function needsRehash(phc) {
  if (typeof phc !== "string" || !phc.startsWith("$scrypt$")) return true;
  const params = Object.fromEntries(
    phc.split("$")[2].split(",").map((kv) => kv.split("="))
  );
  return Number(params.n) < CURRENT.N ||
         Number(params.r) < CURRENT.r ||
         Number(params.p) < CURRENT.p;
}

Test modulen med et lite skript. Opprett test.js og kjør node test.js.

// test.js
import { hash, verify, needsRehash } from "./password.js";

const phc = await hash("S0lsikke-Sommer-2026!");
console.log("Lagret hash:\n", phc);
console.log("Riktig passord:", await verify("S0lsikke-Sommer-2026!", phc));
console.log("Feil passord: ", await verify("feil", phc));
console.log("Trenger rehash:", needsRehash(phc));
$ node test.js
Lagret hash:
 $scrypt$n=131072,r=8,p=1$k3Jd...==$Tm8h...==
Riktig passord: true
Feil passord:  false
Trenger rehash: false

Modulen er nå komplett og produksjonsklar. Den genererer unik salt, bruker OWASP-parametre, lagrer alt i ett felt, verifiserer i konstant tid og kan oppdage gamle hasher som bør oppgraderes.

Steg 8: Integrer med Express-registrering og innlogging

La oss koble modulen inn i en ekte innloggingsflyt med Express 5. Installer Express med npm install express. Eksempelet bruker et enkelt minneobjekt som database, men i produksjon bytter du dette mot PostgreSQL, MySQL eller en annen vedvarende lagring.

// server.js
import express from "express";
import { hash, verify, needsRehash } from "./password.js";

const app = express();
app.use(express.json());

const users = new Map(); // brukernavn -> { passwordHash }

app.post("/register", async (req, res) => {
  const { username, password } = req.body;
  if (!username || !password || password.length < 12) {
    return res.status(400).json({ feil: "Passord må være minst 12 tegn" });
  }
  if (users.has(username)) {
    return res.status(409).json({ feil: "Brukernavn er opptatt" });
  }
  const passwordHash = await hash(password);
  users.set(username, { passwordHash });
  res.status(201).json({ ok: true });
});

app.post("/login", async (req, res) => {
  const { username, password } = req.body;
  const user = users.get(username);

  // Samme svar uansett om bruker finnes, mot brukeropptelling
  const ok = user ? await verify(password, user.passwordHash) : false;
  if (!ok) {
    return res.status(401).json({ feil: "Feil brukernavn eller passord" });
  }

  // Oppgrader hashen i bakgrunnen hvis parametrene er utdaterte
  if (needsRehash(user.passwordHash)) {
    user.passwordHash = await hash(password);
  }
  res.json({ ok: true, melding: "Innlogget" });
});

app.listen(3000, () => console.log("Kjører på http://localhost:3000"));

To detaljer her er viktige for sikkerheten. For det første svarer innlogging med nøyaktig samme feilmelding enten brukeren ikke finnes eller passordet er feil. Det hindrer en angriper i å finne ut hvilke brukernavn som eksisterer. For det andre oppgraderer vi hashen ved vellykket innlogging hvis needsRehash() sier at parametrene er utdaterte. Det er det eneste tidspunktet vi har passordet i klartekst, så det er da rehashing må skje.

Test det med curl. Registrer en bruker, og logg deretter inn.

$ curl -X POST localhost:3000/register \
  -H "Content-Type: application/json" \
  -d '{"username":"kari","password":"Fjord-Vinter-2026!"}'
{"ok":true}

$ curl -X POST localhost:3000/login \
  -H "Content-Type: application/json" \
  -d '{"username":"kari","password":"Fjord-Vinter-2026!"}'
{"ok":true,"melding":"Innlogget"}

Steg 9: Håndter maxmem og ytelse riktig

maxmem er den parameteren som oftest velter en scrypt-implementasjon. Node.js setter standardgrensen til 32 MiB (33 554 432 byte). OWASP-parametrene våre krever rundt 128 MiB, så uten å heve maxmem får du en feil. Men du kan heller ikke sette maxmem vilkårlig høyt, for da risikerer du at mange samtidige innlogginger spiser opp serverens RAM.

Regn ut det faktiske behovet og legg på en margin. Behovet er 128 * N * r byte. For N = 131072 og r = 8 er det 128 MiB. Sett maxmem til rundt det dobbelte, 256 MiB, slik vi gjorde, for å dekke intern overhead. Husk så at hver samtidige hashing bruker dette minnet. Hvis 10 brukere logger inn samtidig med 128 MiB hver, er det 1,28 GiB. Dette er en reell begrensning på trafikktunge tjenester.

// Beregn minnebehov og sett maxmem dynamisk
function memoryFor(N, r) {
  const needed = 128 * N * r;          // faktisk behov i byte
  return Math.ceil(needed * 2);         // dobbel margin
}

console.log(memoryFor(2 ** 17, 8) / (1024 * 1024)); // 256 (MiB)

Fordi crypto.scrypt() er asynkron, kjører den tunge beregningen i Node sin trådpool (libuv) og blokkerer ikke event-loopen. Det betyr at serveren fortsatt kan svare på andre forespørsler mens en hashing pågår. Bruk derfor alltid den asynkrone varianten i en webserver. scryptSync() blokkerer hele prosessen og hører kun hjemme i skript, migrasjonsverktøy og tester.

På tjenester med svært høy innloggingsrate bør du vurdere å begrense antall samtidige hashinger med en kø, slik at du ikke utløser en utilsiktet minneeksplosjon. En enkel semafor som tillater for eksempel fire samtidige hashinger gir forutsigbar minnebruk.

Steg 10: Rehashing og parameteroppgradering

Maskinvare blir raskere hvert år, og det som var trygt i 2024 kan være for svakt i 2027. Derfor må du kunne øke kostnadsparametrene over tid uten å be alle brukere bytte passord. Det er nettopp dette PHC-formatet og needsRehash() løser.

Strategien er enkel. Når du bestemmer deg for å heve N fra 2^17 til 2^18, oppdaterer du bare CURRENT.N i modulen. Eksisterende hasher fortsetter å verifisere riktig fordi verify() leser de gamle parametrene fra selve hashen. Ved neste vellykkede innlogging ser needsRehash() at den lagrede N er lavere enn den nye, og du lager en fersk hash med de sterkere parametrene.

// Inne i login-handleren, etter vellykket verify:
if (needsRehash(user.passwordHash)) {
  user.passwordHash = await hash(password); // bruker nye CURRENT-parametre
  await db.updatePasswordHash(user.id, user.passwordHash);
  console.log(`Oppgraderte hash for bruker ${user.id}`);
}

Denne tilnærmingen er gradvis og usynlig for brukeren. Aktive brukere får automatisk sterkere hasher, og du slipper en stor migrasjon. For brukere som ikke har logget inn på lenge, beholder du de gamle hashene helt trygt, fordi selv de gamle parametrene fortsatt er over OWASP-minimum hvis du har fulgt rådene her.

Den samme mekanismen lar deg migrere fra en annen algoritme. Hvis du har gamle bcrypt-hasher, kan du la verify() gjenkjenne $2b$-prefikset, verifisere med bcrypt, og deretter rehashe til scrypt ved innlogging. Slik bytter du algoritme over tid uten nedetid.

Steg 11: Benchmark og kalibrer på din maskinvare

Parametre som er trygge på papiret kan være for trege eller for raske på din konkrete server. Mål alltid den faktiske tiden på maskinvaren du skal kjøre i produksjon. Målet er 200 til 500 millisekunder per hashing.

// benchmark.js
import { scryptSync, randomBytes } from "node:crypto";

const salt = randomBytes(16);
const password = "benchmark-passord";

for (const exp of [15, 16, 17, 18]) {
  const N = 2 ** exp;
  const start = process.hrtime.bigint();
  scryptSync(password, salt, 64, {
    cost: N, blockSize: 8, parallelization: 1,
    maxmem: 512 * 1024 * 1024,
  });
  const ms = Number(process.hrtime.bigint() - start) / 1e6;
  console.log(`N=2^${exp} (${N}): ${ms.toFixed(1)} ms`);
}
$ node benchmark.js
N=2^15 (32768): 58.3 ms
N=2^16 (65536): 116.9 ms
N=2^17 (131072): 232.4 ms
N=2^18 (262144): 467.1 ms

Tallene over er typiske for en moderne server, men dine vil variere. Velg den N-verdien som lander mellom 200 og 500 ms. På maskinen i eksempelet treffer N = 2^17 perfekt på rundt 232 ms, mens N = 2^18 fortsatt er akseptabelt på 467 ms hvis du vil ha ekstra margin. Kjør benchmarken på nytt etter hver maskinvareoppgradering, og juster CURRENT.N deretter.

Vanlige fallgruver ved scrypt i Node.js

Disse feilene dukker opp gang på gang i ekte kodebaser. Unngå dem, så har du en solid implementasjon.

  • Glemmer å heve maxmem. Med OWASP-parametrene kaster Node Error: Invalid scrypt params fordi standardgrensen på 32 MiB er for lav. Sett alltid maxmem til minst det dobbelte av 128 * N * r.
  • Bruker === eller Buffer.equals(). Disse er sårbare for timing-angrep. Bruk alltid crypto.timingSafeEqual() til å sammenligne hasher.
  • Gjenbruker salt mellom brukere. Hver bruker skal ha sin egen tilfeldige salt fra crypto.randomBytes(). Et fast eller delt salt ødelegger hele poenget.
  • Lagrer kun hashen uten parametre. Uten N, r, p og salt kan du verken verifisere eller rehashe. Bruk PHC-formatet og lagre alt i ett felt.
  • Bruker Math.random() til salt. Den er ikke kryptografisk sikker og kan forutsies. Kun crypto.randomBytes() duger.
  • Bruker scryptSync() i en webserver. Den blokkerer event-loopen og fryser hele serveren under hashing. Bruk den asynkrone crypto.scrypt().
  • Verifiserer med gjeldende parametre i stedet for de lagrede. En gammel hash må verifiseres med akkurat de N, r og p den ble laget med, lest fra selve hashen.

Feilsøking: 8 vanlige feil og løsninger

SymptomÅrsakLøsning
Error: Invalid scrypt paramsmaxmem er for lav for valgt N og rSett maxmem til minst 128 * N * r, gjerne det dobbelte
Input buffers must have the same byte lengthtimingSafeEqual fikk buffere av ulik lengdeBruk hash.length som keylen, og sjekk lengde før sammenligning
verify() returnerer alltid falseLeser nye parametre i stedet for lagredeHent N, r, p og salt fra PHC-strengen, ikke fra CURRENT
Innlogging tar flere sekunderN er satt for høyt for maskinvarenKjør benchmark og senk N til 200-500 ms
Serveren fryser under lastscryptSync blokkerer event-loopenBytt til asynkron crypto.scrypt med promisify
Minnet renner over ved mye trafikkFor mange samtidige hashinger med høy maxmemInnfør en semafor som begrenser samtidige hashinger
cost must be a power of 2N er ikke en toer-potensBruk verdier som 2 ** 17, ikke vilkårlige tall
Gamle hasher feiler etter parameterendringEndret format uten bakoverkompatibilitetBehold PHC-parsing slik at gamle parametre fortsatt leses

De fleste problemer koker ned til to ting: minnegrensen maxmem og forvekslingen mellom lagrede og gjeldende parametre. Hvis verify alltid feiler, legg inn en logglinje som skriver ut N, r og p fra både den lagrede hashen og den ferske beregningen. Da ser du raskt om de stemmer overens.

Avanserte tips for produksjon

Legg på en server-pepper

En pepper er en hemmelig nøkkel som ikke ligger i databasen, men i miljøvariabler eller en secrets-tjeneste. Du kombinerer passordet med pepperet før hashing, ofte med en HMAC. Hvis databasen lekker uten at applikasjonsserveren gjør det, er alle hasher fortsatt ubrukelige for angriperen. Pepper supplerer salt, det erstatter det ikke.

import { createHmac } from "node:crypto";

const PEPPER = process.env.PASSWORD_PEPPER; // last fra secrets, aldri hardkod

function withPepper(password) {
  return createHmac("sha256", PEPPER).update(password).digest();
}
// Send withPepper(password) inn i scrypt i stedet for rått passord

Sett en maks passordlengde mot tjenestenekt

scrypt har ingen 72-byte-grense slik bcrypt har, men det betyr at en angriper kan sende inn et passord på flere megabyte for å tvinge serveren til tung beregning. Sett en fornuftig øvre grense, for eksempel 128 eller 256 tegn, før du sender passordet til scrypt. Det stenger en enkel tjenestenektsvektor.

Kombiner med tofaktor og overvåking

Selv perfekt passordhashing beskytter ikke mot et lekket eller gjenbrukt passord. Legg på tofaktorautentisering, overvåk for mange mislykkede innlogginger fra samme IP, og sjekk passord mot kjente lekkasjelister ved registrering. Hashing er fundamentet, men en helhetlig passordsikkerhetsstrategi trenger flere lag. For å forstå hvordan selve avtrykket fungerer under panseret, se vår gjennomgang av digitale signaturer og kryptografisk tillit.

Lagring i database og valg av kolonnetype

Når modulen din produserer en PHC-streng, trenger du ett tekstfelt i databasen til å lagre den. Strengen inneholder algoritmenavn, parametrene n, r og p, et base64-kodet salt på 16 byte og en base64-kodet hash på 64 byte. Med OWASP-parametrene blir den typisk rundt 130 til 140 tegn lang, men la deg ikke friste til å bruke et felt som er nøyaktig så stort. Du vil øke parametrene over tid, og du vil kanskje støtte flere algoritmer, så gi deg selv slingringsmonn.

DatabaseAnbefalt kolonnetypeMerknad
PostgreSQLTEXT eller VARCHAR(255)TEXT har ingen ytelsesstraff i Postgres
MySQL / MariaDBVARCHAR(255)Bruk utf8mb4 og en binærsikker collation
SQLiteTEXTIngen lengdegrense å tenke på
MongoDBStringLagre som vanlig strengfelt
Redis (cache)StringAldri som eneste varig lagring

En vanlig feil er å splitte hashen i flere kolonner, én for salt, én for parametrene og én for selve hashen. Det gjør koden mer komplisert og gjør migrering vanskeligere. Hold deg til én streng i ett felt. Hele poenget med PHC-formatet er at alt du trenger for å verifisere og oppgradere ligger samlet på ett sted, akkurat slik bcrypt og Argon2 også gjør det.

Sørg også for at kolonnen aldri logges eller eksporteres ved et uhell. Passordhasher hører ikke hjemme i applikasjonslogger, feilrapporter eller analyseeksporter. Marker feltet som sensitivt i ORM-en din, og dobbeltsjekk at det ikke havner i serialiserte API-svar. En lekket hash er ikke like ille som et lekket klartekstpassord, men med svake parametre kan den fortsatt knekkes.

Enhetstesting av passordmodulen

En passordmodul fortjener tester, for feil her er stille og farlige. Node.js har en innebygd testløper siden versjon 18, så du trenger ingen ekstra pakke. Opprett password.test.js og kjør den med node --test.

// password.test.js
import { test } from "node:test";
import assert from "node:assert/strict";
import { hash, verify, needsRehash } from "./password.js";

test("riktig passord verifiserer til true", async () => {
  const phc = await hash("hemmelig-passord-123");
  assert.equal(await verify("hemmelig-passord-123", phc), true);
});

test("feil passord verifiserer til false", async () => {
  const phc = await hash("hemmelig-passord-123");
  assert.equal(await verify("feil-passord", phc), false);
});

test("samme passord gir ulike hasher (unik salt)", async () => {
  const a = await hash("samme-passord");
  const b = await hash("samme-passord");
  assert.notEqual(a, b);
});

test("ugyldig hashformat gir false, ikke unntak", async () => {
  assert.equal(await verify("passord", "ikke-en-gyldig-hash"), false);
});

test("needsRehash er true for svakere parametre", () => {
  const gammel = "$scrypt$n=16384,r=8,p=1$c2FsdA==$aGFzaA==";
  assert.equal(needsRehash(gammel), true);
});
$ node --test
✔ riktig passord verifiserer til true (241.7ms)
✔ feil passord verifiserer til false (238.4ms)
✔ samme passord gir ulike hasher (unik salt) (480.1ms)
✔ ugyldig hashformat gir false, ikke unntak (0.6ms)
✔ needsRehash er true for svakere parametre (0.4ms)
ℹ tests 5
ℹ pass 5
ℹ fail 0

De viktigste testene er at samme passord gir to ulike hasher, som beviser at saltet faktisk er unikt per kall, og at et ugyldig hashformat returnerer false i stedet for å kaste et unntak. Det siste er en sikkerhetsdetalj: en innloggingsfunksjon som krasjer på en korrupt hash kan lekke informasjon eller skape en tjenestenektmulighet. Testene bekrefter også at needsRehash() fanger opp gamle, svake parametre slik at oppgraderingsmekanismen faktisk slår inn.

Hvorfor scrypt fortsatt holder mål i 2026

Et naturlig spørsmål er om en algoritme fra 2009 fortsatt er trygg i 2026. Svaret er ja, forutsatt at du bruker riktige parametre. scrypt sin minnehardhet er fortsatt en effektiv barriere mot GPU- og ASIC-baserte angrep, og det er nettopp derfor OWASP fremdeles lister den som et godkjent valg. Den eneste grunnen til at Argon2id rangeres over scrypt er at Argon2 er nyere, vant Password Hashing Competition i 2015, og gir enda finere kontroll over minne kontra tid.

For en Node.js-utvikler er det avgjørende argumentet at scrypt er innebygd. Du eliminerer en hel klasse av problemer knyttet til native npm-pakker: byggefeil i CI, inkompatibilitet ved Node-oppgraderinger, og forsinkelser i sikkerhetspatcher fra tredjeparter. I et miljø der forsyningskjedeangrep mot npm har blitt et tilbakevendende tema, er det en reell fordel å holde antallet avhengigheter nede.

Det som derimot ikke holder mål, er å hashe passord med en rå hashfunksjon. Hvis du fortsatt ser crypto.createHash("sha256") brukt på passord i kodebasen din, er det en alvorlig sårbarhet. SHA-256 regner ut milliarder av hasher i sekundet på en GPU, så en lekket database med slike hasher kan knekkes nesten umiddelbart. Bytt til scrypt, bcrypt eller Argon2id med en gang. Bakgrunnen for hvorfor rene hashfunksjoner er upassende her, dekkes grundig i artikkelen om kryptografiske hashfunksjoner.

Ofte stilte spørsmål om scrypt i Node.js

Trenger jeg en npm-pakke for scrypt i Node.js?

Nei. scrypt er innebygd i node:crypto via crypto.scrypt() og crypto.scryptSync() og har vært det siden Node.js 10. Du trenger ingen avhengigheter for selve hashingen. Det er en av scrypt sine største fordeler over bcrypt og Argon2, som begge krever native npm-pakker.

Hvilke scrypt-parametre bør jeg bruke i 2026?

OWASP anbefaler N = 131072 (2^17), r = 8 og p = 1 som utgangspunkt. Disse krever rundt 128 MiB minne, så husk å heve maxmem. Kjør deretter en benchmark og juster N slik at én hashing tar 200 til 500 ms på din maskinvare. Gå aldri under N = 2^15 i produksjon.

Hvorfor får jeg “Invalid scrypt params”?

Feilen kommer nesten alltid av at maxmem er for lav. Node setter standardgrensen til 32 MiB, men OWASP-parametrene trenger rundt 128 MiB. Sett maxmem til minst 128 * N * r, gjerne det dobbelte for å dekke overhead. Sjekk også at N er en toer-potens.

Er scrypt sikrere enn bcrypt?

For de fleste formål, ja. scrypt er minnehard, noe bcrypt ikke er, og det gir bedre motstand mot GPU- og ASIC-angrep. bcrypt har dessuten en grense på 72 byte for passordlengde, som scrypt ikke har. Argon2id regnes likevel som det aller sterkeste valget i 2026.

Hvordan lagrer jeg saltet?

Saltet lagres i klartekst sammen med hashen, gjerne i PHC-strengformatet sammen med parametrene. Salt er ikke hemmelig, det skal bare være unikt per bruker og uforutsigbart. Du trenger nøyaktig samme salt for å verifisere passordet ved innlogging, så det må lagres.

Kan jeg bytte fra bcrypt til scrypt uten å nullstille passord?

Ja. La verify-funksjonen gjenkjenne det gamle bcrypt-prefikset ($2b$), verifisere med bcrypt, og deretter rehashe til scrypt ved vellykket innlogging. Over tid migreres alle aktive brukere automatisk uten at de merker noe og uten nedetid.

Bør jeg bruke scrypt eller Argon2id til et nytt prosjekt?

Hvis du vil ha det sterkeste forsvaret og har full kontroll over byggemiljøet, velg Argon2id. Hvis du verdsetter null avhengigheter og enkel drift, er scrypt et utmerket og OWASP-godkjent valg. Begge er trygge når de er riktig konfigurert. Prinsippene i denne veiledningen gjelder uansett hvilken du velger.

Relatert innhold

Kilder og videre lesning: Node.js crypto-dokumentasjon, OWASP Password Storage Cheat Sheet, RFC 7914 (scrypt), Node.js utgivelsesplan og scrypt på Wikipedia.