Sur un projet récent Node 20 + Fastify, besoin d’un login/mot de passe basique sur l’UI. Single user pour démarrer, multi-user plus tard. La tentation immédiate : npm i bcrypt ou npm i argon2. Les deux compilent du C natif — donc il faut python3 + toolchain build dans l’image Docker, et ça galère souvent sur Alpine.
Sauf que Node a déjà ce qu’il faut dans la stdlib depuis Node 10 : crypto.scrypt.
La fonction de hash, en 30 lignes
import crypto from 'node:crypto';
import { promisify } from 'node:util';
const scrypt = promisify(crypto.scrypt);
const N = 16384, r = 8, p = 1, KEYLEN = 64;
export async function hashPassword(plain) {
const salt = crypto.randomBytes(16);
const key = await scrypt(plain, salt, KEYLEN, { N, r, p });
return `scrypt$${N}$${r}$${p}$` +
`${salt.toString('base64')}$${key.toString('base64')}`;
}
export async function verifyPassword(plain, encoded) {
const [algo, N, r, p, saltB64, hashB64] = encoded.split('$');
if (algo !== 'scrypt') return false;
const salt = Buffer.from(saltB64, 'base64');
const expected = Buffer.from(hashB64, 'base64');
const got = await scrypt(plain, salt, expected.length,
{ N: +N, r: +r, p: +p });
return expected.length === got.length &&
crypto.timingSafeEqual(expected, got);
}
Format de stockage : scrypt$N$r$p$saltBase64$hashBase64. Auto-portable : les paramètres voyagent avec le hash. Si demain je décide d’augmenter N, les anciens mots de passe restent vérifiables avec leurs paramètres d’origine, et je peux re-hasher au prochain login réussi.
Paramètres choisis
N = 16384, r = 8, p = 1, keylen = 64 — recommandation OWASP pour scrypt. Coût mémoire ~16 Mo, ~50 ms de CPU sur un Pi 4. Plus que suffisant pour ralentir une attaque brute-force tout en restant tolérable à chaque login interactif.
Pourquoi pas bcrypt / argon2 ?
Les deux sont meilleurs par certains aspects (argon2id est l’état de l’art actuel). Mais :
- Argon2 nécessite une compilation native —
node-gyp, headers Python, toolchain C. Image Docker plus grosse, builds plus lents, surface d’attaque supply chain élargie - Bcrypt a une limite de 72 octets sur la longueur du mot de passe en input (silencieusement tronqué) — piège connu et déconseillé pour les passphrases longues
- Scrypt est dans la stdlib Node depuis Node 10. Zéro dep, zéro maintenance, zéro vuln à
npm auditpour cette feature
Pour un projet perso ou un MVP, le compromis est facile : scrypt suffit largement. Le jour où tu fais du multi-tenant à grande échelle, tu migreras vers argon2id — et le format algo$...$saltB64$hashB64 rend la migration triviale (re-hash au prochain login réussi, avec un test sur le préfixe algo).
Lessons learned
node:crypto.scryptest utilisable depuis Node 10 : tout projet moderne peut s’en servir sans dépendance native- Format
algo$params$salt$hash:algoen préfixe permet de migrer vers un nouveau hash sans casser les comptes existants crypto.timingSafeEqualest obligatoire pour comparer deux hashs :===est exploitable par chronométrage- Param OWASP pour scrypt :
N=16384, r=8, p=1, keylen=64— bonne balance sécurité/UX sur du commodity hardware - Argon2id reste l’état de l’art ; choisir scrypt c’est un trade-off pragmatique sur la complexité de build, pas un déni de la cryptographie moderne