bcrypt vs Argon2 vs scrypt: wachtwoorden hashen in 2026
Kort antwoord: kies voor elk nieuw project in 2026 Argon2id met m=19456, t=2, p=1. Dat is de basislijn uit de OWASP Password Storage Cheat Sheet, en van de algoritmen die je vandaag kunt uitrollen biedt dit de sterkste weerstand tegen GPU- en zijkanaalaanvallen.
Is Argon2 niet beschikbaar in je stack (zeldzaam, maar je tegenkomt het soms op embedded of oudere runtimes), pak dan scrypt met N=2^17, r=8, p=1. Bcrypt met cost=12 alleen als je vastzit aan een legacy systeem dat al bcrypt spreekt en je geen nieuwe afhankelijkheid kunt toevoegen. Bij verplichte FIPS-140-compliance gebruik je PBKDF2-HMAC-SHA-256 met 600.000 iteraties.
| Algoritme | OWASP 2026-parameters | Wanneer kiezen |
|---|---|---|
| Argon2id | m=19456 KiB, t=2, p=1 | Standaard voor nieuwe projecten |
| scrypt | N=2^17, r=8, p=1 | Argon2 niet beschikbaar |
| bcrypt | cost=12 (min. 10) | Alleen legacy systemen |
| PBKDF2 | HMAC-SHA-256, 600k iteraties | FIPS-140 vereist |
De rest van dit artikel legt uit waarom deze getallen, hoe je ze afstemt op je hardware en hoe je migreert zonder een wachtwoordreset af te dwingen. Voor sterke testwachtwoorden voor benchmarking gebruik je de willekeurig wachtwoord generator. Voor het bredere plaatje, zie de gids essentiële webbeveiliging.
Waarom wachtwoorden hashen iets anders is dan gewoon hashen
Hashfuncties zien er van buiten allemaal hetzelfde uit: data gaat erin, een digest van vaste lengte komt eruit, en je kunt het niet omkeren. Maar de ontwerpdoelen voor “hash deze 4 GB ISO” en “hash dit wachtwoord van 12 tekens” zijn lijnrecht tegengesteld. De ene moet zo snel zijn als het silicium toelaat. De andere moet zo traag zijn als je login-latency-budget verdraagt.
Wie die twee door elkaar haalt, verandert datalekken in account-overnames.
Waarom MD5 en SHA-256 niet volstaan voor wachtwoorden
Algemene hashes zoals MD5, SHA-1 en SHA-256 zijn ontworpen voor doorvoer. Ze verwerken gigabytes per seconde op gewone CPU’s en tientallen gigabytes per seconde op GPU’s. Dat maakt ze uitstekend voor bestandschecksums en content addressing, en rampzalig voor wachtwoorden.
Hashcat-benchmarks op één RTX 4090 lieten in 2024 ruwweg 164 GH/s zien voor MD5 en 22 GH/s voor SHA-256. Een wachtwoord van acht tekens met kleine letters en cijfers (36^8 ≈ 2,8 × 10^12 kandidaten) valt op één GPU binnen een minuut tegen MD5 en binnen een paar minuten tegen SHA-256. Een gelekte database die sha256(password) opslaat, is in feite platte tekst.
Salt redt je daar niet uit. Salt zorgt dat voorberekende rainbow tables niet werken, maar vertraagt een aanval per account niet: de aanvaller hasht gewoon elke kandidaat samen met de gelekte salt.
Voor checksums buiten de beveiliging blijven MD5 en SHA-256 nuttig, daar zijn tools als de MD5 & SHA-256 hashgenerator voor gemaakt. Voor een uitgebreidere vergelijking van wanneer welk algoritme past, lees MD5 vs SHA-256. Voor wachtwoorden heb je een hash nodig die met opzet traag is.
De drie eigenschappen van een moderne wachtwoord-hash
Een wachtwoord-hash die in 2026 het uitrollen waard is, heeft drie eigenschappen:
- Traag van ontwerp, met een instelbare werkfactor. Inloggen mag 100 tot 500 ms duren: snel genoeg dat gebruikers het niet merken, traag genoeg dat een offline aanvaller dagen verbrandt per miljoen pogingen. Die werkfactor moet een parameter zijn zodat je hem kunt opvoeren naarmate hardware verbetert.
- Salt per record. Een unieke willekeurige salt per wachtwoord verslaat rainbow tables en dwingt de aanvaller elk account apart aan te vallen. Moderne algoritmen genereren de salt en bedden hem voor je in in de uitvoerstring.
- Geheugenintensief. GPU’s en ASIC’s zijn snel in rekenen maar duur in geheugen met hoge bandbreedte. Een algoritme dat tientallen MiB per hash vereist, dwingt een aanvaller RAM in te zetten in verhouding tot zijn parallelisme, wat de kostenefficiëntie van GPU-farms onderuit haalt.
Bcrypt scoort op (1) en (2), maar niet op (3). Scrypt was het eerste algoritme dat alle drie raakte. Argon2 verfijnde het ontwerp en won de Password Hashing Competition. De volgende sectie pakt elk algoritme apart uit.
De drie algoritmen: architectuur en afwegingen
bcrypt: op Blowfish gebaseerd, time-hard
Bcrypt is in 1999 ontworpen door Niels Provos en David Mazières voor OpenBSD. Het is gebouwd op het Blowfish-cipher, met een dure key-setup-fase (“EksBlowfish”) die 2^cost keer wordt herhaald. De enige instelbare parameter is de cost factor (ook wel de “log rounds”): elke verhoging verdubbelt het werk. Een hash met cost=10 doet 1024 key schedules; cost=14 doet er 16.384.
Een bcrypt-hash ziet er zo uit:
$2b$12$R9h/cIPz0gi.URNNX3kh2OPST9/PgBkqquzi.Ss7KIUgO2t0jWMUW
│ │ │ │
│ │ │ └─ 31-teken base64 hash
│ │ └─ 22-teken base64 salt
│ └─ cost factor (12)
└─ algoritme-identifier ($2b$ = bcrypt v2)
Het formaat beschrijft zichzelf: verify() leest de cost en salt uit de opgeslagen string, aparte kolommen zijn overbodig.
De nadelen zijn reëel. De geheugenvoetafdruk van bcrypt is ongeveer 4 KiB, klein genoeg dat een high-end GPU duizenden bcrypt-cores parallel draait. Daarnaast kapt bcrypt invoer stilzwijgend af op 72 bytes. Een passphrase van 100 tekens heeft dezelfde beveiliging als de eerste 72 bytes ervan. De maximale cost is 31, maar alles boven ~16 begint pijn te doen aan de login-latency op gewone hardware.
scrypt: de pionier van memory-hardness
Scrypt is in 2009 gepubliceerd door Colin Percival voor de Tarsnap-backupdienst en in 2016 gestandaardiseerd als RFC 7914. Het introduceerde memory-hardness: het algoritme vult een grote buffer met pseudo-willekeurige data en leest vervolgens uit willekeurige posities, zodat elke implementatie het geheugen ook echt moet alloceren.
Scrypt neemt drie parameters:
- N: CPU/geheugenkosten (moet een macht van 2 zijn)
- r: blokgrootte in bytes (vermenigvuldiger op geheugen en mengrondes)
- p: parallellisme (onafhankelijke berekeningen, meestal gebruikt om CPU-tijd te schalen zonder geheugen te schalen)
Geheugengebruik is ongeveer 128 × N × r bytes. Met OWASP’s aanbevolen N=2^17, r=8 is dat 128 × 131072 × 8 = 134.217.728 bytes, oftewel 128 MiB per hash.
Scrypt is ook een sleutel-afleidingsfunctie, niet alleen een wachtwoord-hash. Het zit in cryptocurrency-wallets, full-disk encryptie en het oorspronkelijke Litecoin proof-of-work. Die dubbele rol komt goed uit wanneer je zowel wachtwoordopslag als sleutel-afleiding wilt regelen vanuit één bibliotheek.
Argon2 (id/i/d): winnaar van de Password Hashing Competition
De Password Hashing Competition liep van 2013 tot 2015 en evalueerde 24 kandidaat-algoritmen op memory-hardness, zijkanaalweerstand en implementatiegemak. Argon2 won. In 2021 werd het gestandaardiseerd als RFC 9106.
Argon2 heeft drie varianten. Het verschil zit in hoe het geheugen wordt geadresseerd tijdens het mengen:
- Argon2d gebruikt data-afhankelijke geheugenadressen. Dat maximaliseert de weerstand tegen GPU- en ASIC-aanvallen, maar lekt informatie via cache-timing zijkanalen. Geschikt voor cryptocurrency proof-of-work, niet voor authenticatie.
- Argon2i gebruikt data-onafhankelijke adressen. Veilig tegen zijkanalen, iets zwakker tegen GPU-tradeoff-aanvallen.
- Argon2id is een hybride: de eerste helft van de eerste pass gebruikt Argon2i-indexering (zijkanaalveilig), de rest gebruikt Argon2d-indexering (GPU-bestendig). RFC 9106 beveelt Argon2id expliciet aan voor wachtwoorden hashen, en OWASP doet dat ook.
Argon2 neemt drie parameters:
- m: geheugen in KiB
- t: tijdkosten (aantal passes over de geheugenbuffer)
- p: parallellisme (aantal lanes dat gelijktijdig wordt verwerkt)
Een Argon2id-hash gebruikt het PHC-stringformaat en ziet er zo uit:
$argon2id$v=19$m=19456,t=2,p=1$c29tZXNhbHQ$RdescudvJCsgt3ub+b+dWRWJTmaaJObG
Net als bij bcrypt zijn alle parameters in de string ingebed, dus verify() heeft geen parametertabel nodig.
OWASP 2026 aanbevolen parameters
De OWASP Password Storage Cheat Sheet is de canonieke referentie. De onderstaande getallen komen overeen met de huidige richtlijn. Ze zijn aan de behoudende kant, gedimensioneerd voor een typische webserver met een login-latency-budget van 100 tot 500 ms, en je benchmarkt nog steeds op je eigen hardware voordat je uitrolt.
Argon2id-parameters: eerste keuze
OWASP’s basisaanbeveling is m=19456 (19 MiB), t=2, p=1.
Heeft je server meer RAM-ruimte, dan kun je het werk verschuiven tussen geheugen en tijd. RFC 9106 publiceert equivalente profielen; OWASP keurt elk van deze goed:
| memoryCost (m) | timeCost (t) | parallelism (p) | RAM per hash |
|---|---|---|---|
| 47104 | 1 | 1 | 46 MiB |
| 19456 | 2 | 1 | 19 MiB (basislijn) |
| 12288 | 3 | 1 | 12 MiB |
| 9216 | 4 | 1 | 9 MiB |
| 7168 | 5 | 1 | 7 MiB |
Vuistregel voor afstemming: kies m eerst op basis van je RAM-budget bij piek-gelijktijdige logins. Verwacht je 100 gelijktijdige logins en heb je 4 GiB over, dan is dat 40 MiB per hash. Verhoog daarna t totdat één verify 100 tot 500 ms kost op je productie-CPU. Laat p=1 staan tenzij je een specifieke multi-core reden hebt om dat te veranderen (de meeste web-frameworks geven elk verzoek al een eigen thread).
scrypt-parameters: wanneer Argon2 niet beschikbaar is
OWASP’s aanbeveling is N=2^17 (131072), r=8, p=1, wat 128 MiB per hash gebruikt.
Is 128 MiB per gelijktijdige login te veel voor je server, dan staat OWASP zwakkere profielen toe:
| N | r | p | RAM per hash |
|---|---|---|---|
| 2^17 | 8 | 1 | 128 MiB (voorkeur) |
| 2^16 | 8 | 1 | 64 MiB |
| 2^15 | 8 | 1 | 32 MiB |
N moet een macht van twee zijn. r verhogen verhoogt zowel geheugen als CPU-werk evenredig; p verhogen verhoogt CPU-werk zonder het geheugen per instantie te raken. Laat voor wachtwoorden hashen r en p op de standaardwaarden en stem alleen N af.
bcrypt: cost factor 10 of hoger, alleen voor legacy
OWASP beveelt bcrypt niet meer aan voor nieuwe projecten, maar het zit nog overal: Devise, Spring Security, ASP.NET Identity en talloze zelfgebouwde auth-systemen pakken het standaard.
Zit je vast aan bcrypt, dan zijn de regels:
- Minimale bcrypt cost factor: 10. Onder 10 is snel genoeg dat één GPU een gelekte database in dagen afmaakt.
- Aanbevolen: 12 tot 14, afhankelijk van je hardware. Op een moderne x86-server kost
cost=12ongeveer 250 ms per hash;cost=13kost 500 ms. - Mik op 100 tot 300 ms per verify op je productiehardware. Benchmark, gok niet.
- Onthoud de invoerlimiet van 72 bytes. Mogen gebruikers passphrases kiezen, pre-hash dan met SHA-256 (zie de FAQ).
De GPU-weerstand van bcrypt wordt begrensd door de geheugenvoetafdruk van 4 KiB. Geen enkele bcrypt cost factor evenaart ooit de memory-hardness van Argon2id; kies Argon2id zodra je kunt.
Een praktische referentie: op een EPYC-server uit 2024 draait bcrypt(cost=12) in ruwweg 250 ms; op een high-end laptop dichter bij 350 ms. Liggen jouw cijfers een orde van grootte buiten 100 tot 500 ms, controleer dan of je bibliotheek echt native bcrypt uitvoert of terugvalt op een trage JavaScript-polyfill (sommige bundlers strippen native dependencies in serverless builds).
PBKDF2: route voor FIPS-140-compliance
PBKDF2 (RFC 8018) is het algoritme van laatste redmiddel in de beveiligingsrichtlijnen. Het is ouder dan bcrypt, niet memory-hard, en het sneuvelt sneller voor GPU-aanvallen dan elk van de drie hierboven. Maar het is de enige primitief voor wachtwoorden hashen die FIPS-140-gevalideerd is, wat telt voor de federale overheid, HIPAA in de zorg en bepaalde financiële uitrol.
Heb je PBKDF2 nodig, gebruik dan:
- HMAC-SHA-256 als de PRF (gebruik geen SHA-1; gebruik geen kale SHA-256 zonder HMAC)
- minimaal 600.000 iteraties (OWASP 2026-basislijn)
- minstens 16 bytes willekeurige salt per wachtwoord
Geldt FIPS niet voor jou, kies Argon2id. Het ontwerp van PBKDF2 met vaste output en vast geheugen betekent dat elke dollar GPU-silicium die een aanvaller koopt rechtstreeks vertaalt in meer wachtwoordpogingen per seconde.
NIST’s SP 800-63B noemt PBKDF2-HMAC “approved” voor wachtwoorden hashen, maar gaat niet zo ver om het boven memory-hard alternatieven aan te bevelen. Lees dat als: NIST staat PBKDF2 toe omdat het schrappen ervan elke legacy overheidsuitrol ongeldig zou maken, niet omdat het de beste keuze is voor een greenfield-project.
Beslisschema: welk algoritme moet je kiezen?
Vergelijkingstabel
| Dimensie | bcrypt | scrypt | Argon2id | PBKDF2 |
|---|---|---|---|---|
| Memory-hard | Nee | Ja | Ja | Nee |
| GPU-weerstand | Gemiddeld | Hoog | Zeer hoog | Laag |
| Zijkanaalweerstand | Gemiddeld | Gemiddeld | Hoog (id) | Gemiddeld |
| Parametercomplexiteit | 1 (cost) | 3 (N, r, p) | 3 (m, t, p) | 1 (iteraties) |
| Volwassenheid bibliotheken | Uitstekend | Goed | Goed | Uitstekend |
| Maximale invoerlengte | 72 bytes | Geen | Geen | Geen |
| Standaardisatie | de facto | RFC 7914 | RFC 9106 | RFC 8018 |
| OWASP 2026-status | Alleen legacy | Alternatief | Eerste keuze | Alleen FIPS |
Pak standaard Argon2id
Voor een nieuw project (typische webapp, moderne Node/Python/Go/Rust/JVM-stack, geen FIPS-beperking) gebruik je Argon2id met m=19456, t=2, p=1. Je krijgt de sterkste GPU- en zijkanaalweerstand die beschikbaar is, een formaat met ingebedde parameters dat library-upgrades overleeft, en geen verrassingen rond invoerlengte. Het bibliotheekecosysteem is volwassen: argon2 op npm, argon2-cffi op PyPI, golang.org/x/crypto/argon2, de argon2-crate op crates.io, allemaal onderhouden en gebenchmarkt.
Wanneer kies je in plaats daarvan voor scrypt of bcrypt
Pak scrypt wanneer Argon2 niet beschikbaar is in je runtime (in 2026 echt zeldzaam, zelfs Cloudflare Workers en Deno hebben het nu), of wanneer je al een scrypt-gebaseerd systeem in productie hebt en de migratiekosten zwaarder wegen dan het beveiligingsverschil. Scrypt is nog steeds een degelijk algoritme; het mist alleen de zijkanaalpolijst van Argon2id.
Pak bcrypt wanneer je een legacy systeem onderhoudt, je afhankelijkheden tot het minimum moet beperken (geen native code, geen extra packages), en de invoerlimiet van 72 bytes acceptabel is voor je gebruikersbestand. Bcrypt draait al twee decennia op internetschaal; zijn faalpatronen zijn goed begrepen.
Pak PBKDF2 wanneer de toezichthouder dat zegt. Dat is de enige reden. Accepteert je auditor Argon2id (steeds meer doen dat nu voor niet-FIPS-werklasten), gebruik dan Argon2id.
Veelvoorkomende fouten om te vermijden
De meeste wachtwoordopslag-incidenten van het afgelopen decennium zijn terug te voeren op een kleine set terugkerende engineering-fouten. Geen daarvan is exotisch; ze worden allemaal opgevangen door je auth-code te reviewen met onderstaande lijst ernaast.
- Wachtwoorden hashen met kale SHA-256 of MD5. Dit is de meestvoorkomende manier waarop wachtwoordopslag faalt. Zie MD5 vs SHA-256 voor waarom deze fout zijn voor wachtwoorden.
- Eén globale salt voor alle gebruikers hergebruiken. Een salt moet uniek zijn per record. Argon2 en bcrypt genereren er een voor je; overschrijf dat niet.
- Hash-tijd onder 50 ms zetten. Je hebt beveiliging ingeruild voor een snelheidswinst die geen gebruiker opmerkt. Mik op 100 tot 500 ms.
- Hash-tijd boven 1 seconde zetten. Je hebt een denial-of-service-vector tegen je eigen login-endpoint gemaakt. Houd het op ~500 ms.
- Wachtwoorden client-side hashen en de digest naar de server sturen. De hash is nu het wachtwoord. Wie de database steelt kan authenticeren zonder hem ooit te inverteren. Hash altijd op de server.
- De algoritmeparameters in een aparte kolom opslaan. Het PHC-stringformaat bedt ze in de hash in. Gebruik dat.
- Wachtwoorden of hashes loggen tijdens foutafhandeling. Beide horen toe aan de gebruiker, niet aan je log-aggregator. Strip ze in de request-parsing-laag voordat ze enige logger bereiken.
- Excepties van
verify()als authenticatiefouten behandelen. Een bibliotheek die op een misvormde opgeslagen hash een exceptie gooit, moet die fout aan de oppervlakte brengen en niet stilletjes doorvallen naar “verkeerd wachtwoord”. Maak onderscheid tussen “verkeerd wachtwoord” (geef 401 terug) en “opgeslagen hash is corrupt” (geef 500 terug en piep on-call op).
Implementatie in de praktijk
Argon2id in Node.js
Het argon2-pakket (native bindings naar de referentie-implementatie) is de canonieke keuze op Node.
import argon2 from 'argon2';
// Hashen bij signup of wachtwoordwijziging
const hash = await argon2.hash(password, {
type: argon2.argon2id,
memoryCost: 19456, // 19 MiB
timeCost: 2,
parallelism: 1,
});
// → '$argon2id$v=19$m=19456,t=2,p=1$<salt>$<hash>'
// Verifiëren bij login
const ok = await argon2.verify(hash, candidate);
if (!ok) throw new Error('Invalid credentials');
// Verouderde parameters detecteren en re-hashen na succesvolle login
if (argon2.needsRehash(hash, { type: argon2.argon2id, memoryCost: 19456, timeCost: 2, parallelism: 1 })) {
const upgraded = await argon2.hash(candidate, {
type: argon2.argon2id, memoryCost: 19456, timeCost: 2, parallelism: 1,
});
await db.users.update({ id: user.id }, { password_hash: upgraded });
}
De needsRehash-stap is wat migratie op lange termijn werkbaar maakt: elke succesvolle login wordt een kans om de opgeslagen hash bij te werken naar de huidige parameters, zonder de gebruiker lastig te vallen.
Hetzelfde patroon in Python met argon2-cffi:
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError
ph = PasswordHasher(memory_cost=19456, time_cost=2, parallelism=1)
# Hash
stored = ph.hash(password)
# Verifiëren
try:
ph.verify(stored, candidate)
except VerifyMismatchError:
raise ValueError('Invalid credentials')
# Re-hashen bij parameter-upgrade
if ph.check_needs_rehash(stored):
stored = ph.hash(candidate)
In Go met golang.org/x/crypto/argon2:
import (
"crypto/rand"
"golang.org/x/crypto/argon2"
)
func hashPassword(password string) ([]byte, []byte) {
salt := make([]byte, 16)
rand.Read(salt)
hash := argon2.IDKey([]byte(password), salt, 2, 19456, 1, 32)
return hash, salt
}
De Go-standaardbibliotheek levert geen PHC-format-encoder; gebruik je de primitief argon2.IDKey rechtstreeks, dan codeer je zelf de parameters en salt naast de hash. De meeste Go-projecten pakken een wrapper als github.com/alexedwards/argon2id daarvoor.
Rust met de argon2-crate is even idiomatisch:
use argon2::{Argon2, PasswordHasher, PasswordVerifier, password_hash::{SaltString, rand_core::OsRng}};
let salt = SaltString::generate(&mut OsRng);
let argon2 = Argon2::default(); // Argon2id, m=19456, t=2, p=1 standaard
let hash = argon2.hash_password(password.as_bytes(), &salt)?.to_string();
// Bij verify
let parsed = argon2::password_hash::PasswordHash::new(&hash)?;
argon2.verify_password(candidate.as_bytes(), &parsed)?;
In alle drie de runtimes is de geproduceerde string uitwisselbaar: een hash gemaakt in Node verifieert moeiteloos in Python of Rust. Die compatibiliteit tussen runtimes maakt Argon2 een veiligere gok voor polyglotte architecturen dan algoritme-specifieke wrappers.
Migratiepatroon van bcrypt naar Argon2id
Je krijgt bijna nooit de kans om de gebruikerstabel te wissen en opnieuw te beginnen. Het juiste migratiepatroon is hetzelfde als in de MD5-naar-bcrypt-sectie van onze hashgenerator-FAQ: een zachte, login-gestuurde upgrade.
Voeg een kolom toe die het algoritme bijhoudt:
ALTER TABLE users ADD COLUMN password_algo VARCHAR(16) NOT NULL DEFAULT 'bcrypt';
Bij login dispatch je naar de juiste verifier:
async function verifyAndMaybeRehash(user, candidate) {
let ok;
if (user.password_algo === 'argon2id') {
ok = await argon2.verify(user.password_hash, candidate);
} else if (user.password_algo === 'bcrypt') {
ok = await bcrypt.compare(candidate, user.password_hash);
if (ok) {
// Succesvolle legacy verify → re-hash met Argon2id
const newHash = await argon2.hash(candidate, {
type: argon2.argon2id, memoryCost: 19456, timeCost: 2, parallelism: 1,
});
await db.users.update({ id: user.id }, {
password_hash: newHash,
password_algo: 'argon2id',
});
}
}
return ok;
}
Zet een sunset-venster van 6 tot 12 maanden. Stuur op het 9-maanden-punt een mail met “je wachtwoord wordt opgeslagen met een verouderde methode, log in om te upgraden”. Na 12 maanden vereisen accounts die nog op bcrypt staan een geforceerde wachtwoordreset bij de volgende login. Actieve gebruikers migreren transparant; inactieve accounts krijgen één eenmalig frictiemoment.
Hetzelfde patroon werkt voor migratie weg van scrypt of PBKDF2. De enige state die je nodig hebt is de kolom password_algo.
Pepper, lengtelimieten en encoding-valkuilen
Een paar scherpe randen die je in de praktijk tegenkomt.
Pepper. Een pepper is een geheim op applicatieniveau dat je aan elk wachtwoord toevoegt vóór het hashen, los opgeslagen van de database (in een KMS, env-variabele of Hashicorp Vault). Lekt je database maar je app-secret niet, dan zijn de gelekte hashes onaanvalbaar zonder de pepper. Pas hem toe als HMAC, niet als concatenatie:
import { createHmac } from 'crypto';
const peppered = createHmac('sha256', process.env.PEPPER).update(password).digest();
const hash = await argon2.hash(peppered, { type: argon2.argon2id, /* ... */ });
Roteer de pepper zelden (het vereist re-hashen), maar ondersteun rotatie wel door hem te versioneren: PEPPER_V2, met een fallback naar PEPPER_V1 bij verify.
Bcrypt 72-byte-limiet. Moet je bcrypt gebruiken en wil je wachtwoorden van willekeurige lengte ondersteunen, pre-hash dan met SHA-256 en codeer base64 (zodat ingebedde NUL-bytes worden vermeden, die bcrypt ook inconsistent afhandelt):
import { createHash } from 'crypto';
const prepped = createHash('sha256').update(password, 'utf8').digest('base64');
const hash = await bcrypt.hash(prepped, 12);
Dezelfde transformatie naar prepped moet bij verify draaien. Documenteer dit in je auth-code met een grote, onmiskenbare comment, zodat je het over een jaar zelf nog terugvindt.
UTF-8-normalisatie. De string "café" kan worden gecodeerd als c-a-f-é (4 codepoints, NFC) of c-a-f-e + combining acute (5 codepoints, NFD). Ze zien er identiek uit maar produceren verschillende hashes. Normaliseer altijd naar NFC vóór het hashen:
const normalized = password.normalize('NFC');
Dit raakt mobiele toetsenborden en kopieer-plakken uit pdf’s vaker dan je zou verwachten.
Nooit pre-hashen op de client. Een door de client berekende hash die naar de server wordt gestuurd, is het nieuwe wachtwoord. Wie je database leest kan dan authenticeren. Hash op de server, punt uit. JWT’s veranderen daar niets aan; zie JWT-token decoderen voor wat JWT’s wel en niet authenticeren.
Benchmark op productiehardware, niet op je laptop. Een Intel-laptop van de 13e generatie die Argon2id draait met m=19456, t=2, p=1 is klaar in ruwweg 35 ms. Dezelfde parameters op een t3.small EC2-instance kosten dichter bij 180 ms; op een Raspberry Pi 4 ruim 600 ms. Kies de hardware waarop productie ook draait, time 1000 verifies en stem af op de mediaan. Variantie in login-latency door cold-start serverless containers is ook de moeite van het meten waard. Lambda-coldstarts kunnen 200 tot 800 ms toevoegen die los staan van het hashen.
FAQ
Wat is het verschil tussen wachtwoorden hashen en encryptie?
Hashen is eenrichtingsverkeer: je berekent een vingerafdruk van vaste lengte die je niet kunt omkeren om de invoer te herstellen. Encryptie is tweerichtingsverkeer: met de juiste sleutel decodeer je terug naar het origineel. Wachtwoorden hash je, je versleutelt ze niet. Een server hoort het wachtwoord van geen enkele gebruiker te kunnen herstellen, zo blijft een datalek geen credential-lek.
Waarom kan ik niet gewoon SHA-256 gebruiken voor wachtwoorden?
SHA-256 is ontworpen voor snelheid. Een moderne GPU berekent 22 miljard SHA-256-hashes per seconde, dus een wachtwoord van 8 tekens met kleine letters uit een gelekte database valt in minuten. Wachtwoord-hashes hebben drie eigenschappen nodig die SHA-256 mist: doelbewust trage uitvoering, salt per record en memory-hardness. De afweging staat ook in de richtlijn “Don’t Use MD5 for Security” van onze hashgenerator, en hoe aanvallers zwakke hashes omzetten in platte tekst lees je in wachtwoord-entropie.
Is bcrypt nog veilig in 2026?
Bcrypt zelf is niet gebroken. De op Blowfish gebaseerde key schedule blijft cryptografisch deugdelijk. Wat veranderd is, is het dreigingsmodel: GPU’s en ASIC’s maken het gebrek aan memory-hardness van bcrypt een betekenisvolle zwakte vergeleken met Argon2id. Het standpunt van OWASP voor 2026 is dat bcrypt acceptabel blijft voor legacy systemen met cost ≥ 10, maar dat nieuwe projecten Argon2id kiezen.
Argon2i vs Argon2d vs Argon2id, welke moet ik gebruiken?
Gebruik Argon2id. RFC 9106 specificeert het als de aanbevolen variant voor wachtwoorden hashen. Argon2i is data-onafhankelijk (zijkanaalveilig maar zwakker tegen GPU-tradeoff-aanvallen). Argon2d is data-afhankelijk (sterk tegen GPU’s, kwetsbaar voor cache-timing zijkanalen). Argon2id is een hybride die beide eigenschappen combineert voor de prijs van één.
Hoe kies ik Argon2id-parameters voor mijn app?
Begin bij de OWASP-basislijn: m=19456, t=2, p=1. Benchmark daarna op je productie-CPU en pas aan:
- Bepaal je RAM-budget per login (bijvoorbeeld 50 MiB bij piek-gelijktijdigheid).
- Stel
min op die waarde of lager. - Draai
argon2.hash()in een lus en meet de wandkloktijd. - Verhoog
ttotdat de mediaan tussen 100 en 500 ms ligt.
Laat p=1 staan tenzij je geprofileerd hebt en weet dat multi-lane-parallellisme jouw runtime helpt. Voor auth-servers met veel verkeer geeft een neiging naar hogere t en lagere m vaak betere RAM-ruimte.
Wat is de 72-byte-limiet van bcrypt en hoe ga ik om met lange passphrases?
Bcrypt voert zijn invoer in de Blowfish key schedule, die op 72 bytes afkapt. Een passphrase van 150 tekens heeft dezelfde beveiliging als de eerste 72 bytes; de rest wordt genegeerd. De oplossing is pre-hashen met SHA-256 (32 bytes) of SHA-512 (64 bytes), de digest base64-coderen om NUL-bytes te vermijden, en dat aan bcrypt voeren. Argon2id en scrypt kennen die limiet niet; ze accepteren rechtstreeks willekeurig lange invoer.
Kan ik bcrypt naar Argon2 migreren zonder wachtwoordresets af te dwingen?
Ja. Het patroon: sla beide algoritmen op achter een kolom password_algo, dispatch verificatie naar de juiste bibliotheek, en re-hash bij elke succesvolle bcrypt-verify direct met Argon2id en update het record. Actieve gebruikers migreren stilletjes binnen hun normale login-cadans. Zet voor inactieve accounts een sunset-venster van 6 tot 12 maanden, en forceer daarna een wachtwoordreset voor elk record dat nog op bcrypt staat. Hetzelfde patroon werkt voor elke algoritme-naar-algoritme-migratie.
Is PBKDF2 nog steeds een goede keuze in 2026?
Alleen wanneer FIPS-140-compliance je dwingt, typisch in de federale overheid, gereguleerde gezondheidszorg (HIPAA) en bepaalde financiële systemen. Gebruik HMAC-SHA-256 als de PRF met minstens 600.000 iteraties. PBKDF2 is niet memory-hard, dus bij gelijke latency-budgetten sneuvelt het sneller voor GPU-aanvallen dan Argon2id. Geldt FIPS niet, kies dan Argon2id en sla de compliance-gymnastiek over.
Het antwoord voor 2026 is kort: pak standaard Argon2id met OWASP’s basisparameters, val terug op scrypt als Argon2 niet beschikbaar is, houd bcrypt alleen waar legacy het eist, en reserveer PBKDF2 voor FIPS-gebonden systemen. Combineer de hash met een salt per record (elke moderne bibliotheek regelt dat automatisch), een pepper op applicatieniveau die buiten de database wordt bewaard, en een login-gestuurde re-hash-loop waarmee je de werkfactor opvoert naarmate hardware verbetert.
Genereer een representatieve set wachtwoorden met de willekeurig wachtwoord generator, benchmark je verify-pad tegen je productie-CPU, en zet de parameters in een constants-bestand zodat de volgende engineer precies weet wat hij in 2028 moet ophogen. De volledige beveiligingscontext (TLS, sessiebeheer, rate limiting, MFA) staat in onze gids essentiële webbeveiliging. Kies vandaag de juiste hash voor je app.