Aller au contenu
Sylvain Joffraud l4w
Retour

Hash de mot de passe en Node : scrypt sans dépendance native

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 :

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

Questions fréquentes

Comment hasher un mot de passe en Node.js sans dépendance native comme bcrypt ou argon2 ?
Utilisez crypto.scrypt, présent dans la stdlib Node depuis Node 10, via promisify(crypto.scrypt). Vous générez un sel aléatoire avec crypto.randomBytes(16), dérivez une clé, et stockez le tout. Zéro dépendance, zéro compilation native, zéro vuln à npm audit pour cette fonctionnalité.
Pourquoi éviter bcrypt et argon2 pour un projet Node dans Docker Alpine ?
Argon2 et bcrypt compilent du C natif, ce qui impose node-gyp, des headers Python et une toolchain C dans l’image Docker, qui galère souvent sur Alpine. Cela alourdit l’image, ralentit les builds et élargit la surface d’attaque supply chain. De plus, bcrypt tronque silencieusement l’input au-delà de 72 octets, ce qui est un piège pour les passphrases longues.
Quels paramètres scrypt utiliser pour respecter les recommandations OWASP ?
OWASP recommande pour scrypt N=16384, r=8, p=1 et keylen=64. Cela représente un coût mémoire d’environ 16 Mo et ~50 ms de CPU sur un Raspberry Pi 4, soit une bonne balance entre ralentir le brute-force et rester tolérable à chaque login interactif.
Comment comparer deux hashs de mot de passe sans risque d’attaque par chronométrage ?
N’utilisez jamais === pour comparer deux hashs : la durée de comparaison fuit de l’information exploitable par timing. Utilisez crypto.timingSafeEqual, en vérifiant d’abord que les deux buffers ont la même longueur avant de les passer à la fonction.
Comment migrer plus tard de scrypt vers argon2id sans casser les comptes existants ?
Stockez le hash au format algo$N$r$p$saltBase64$hashBase64 : les paramètres voyagent avec le hash, donc il est auto-portable. À la vérification, testez le préfixe algo pour appliquer le bon algorithme, puis re-hashez au prochain login réussi. La migration vers argon2id devient triviale et les anciens mots de passe restent vérifiables.

Partager cet article :

Article précédent
Coolify « Pre-deployment Command » : un exec, pas un shell
Article suivant
Sécuriser un webhook avec un secret dans l’URL (Fastify)