Jour 29 Day 29 · vendredi 11 septembre 2026 Friday 11 September 2026 Sécurité Fondamental
Mots de passe : hashing & stockage Passwords: hashing & storage
Salt, bcrypt, argon2id, timing attacks : savoir stocker un mot de passe correctement est LA question sécurité éliminatoire en entretien de stage — et la réponse tient en trois réflexes. Salt, bcrypt, argon2id, timing attacks: knowing how to store a password correctly is THE make-or-break security question in internship interviews — and the answer boils down to three reflexes.
L’essentiel
Un mot de passe ne se stocke jamais en clair — évident. Mais il ne se stocke pas non plus chiffré : le chiffrement est réversible par définition. Si la clé fuit (et elle finit toujours par fuiter : dump de la base, backup volé, admin malveillant), tous les mots de passe sont récupérables d’un coup.
La bonne réponse : un hash cryptographique, une fonction à sens unique. On stocke hash(mot_de_passe), jamais le mot de passe. À la connexion, on recalcule le hash de ce que l’utilisateur tape et on compare. Personne — pas même vous — ne peut retrouver le mot de passe d’origine à partir du hash.
⚠️ Chiffré ≠ hashé — le chiffrement (AES, RSA…) est fait pour être inversé avec la bonne clé ; le hash est fait pour ne jamais l’être. Dire « je chiffre les mots de passe » en entretien est un signal d’alarme immédiat. Le mot juste : « je les hashe avec un algorithme dédié et un salt ».
Mais attention : tous les hashs ne se valent pas. MD5 et SHA-256 sont des hashs cryptographiques… conçus pour être rapides. Un GPU moderne calcule des milliards de SHA-256 par seconde : un attaquant qui vole votre base teste tout un dictionnaire en minutes. Il faut un algorithme volontairement lent et coûteux : bcrypt, scrypt ou argon2id.
Comment ça marche
Le flow complet, inscription puis connexion :
SIGNUP
user ──"hunter2"──▶ serveur
│ salt = aléatoire unique
│ hash = bcrypt(salt + "hunter2")
▼
DB: { email, hash } (le salt est
dans le hash)
LOGIN
user ──"hunter2"──▶ serveur
│ lit le hash en DB
│ recalcule avec le même salt
│ compare en temps constant
▼
égal ? ── oui ──▶ session/JWT
└─ non ─▶ 401 (message vague)
Deux ingrédients rendent ce flow solide :
- Le salt : une valeur aléatoire unique par utilisateur, concaténée au mot de passe avant hashing. Sans salt, deux utilisateurs avec le même mot de passe ont le même hash, et surtout un attaquant peut précalculer des rainbow tables (tables géantes hash → mot de passe) une fois pour toutes. Avec un salt unique, chaque hash doit être attaqué individuellement. Le salt n’est pas secret : bcrypt le stocke en clair dans la chaîne de sortie.
- Le coût paramétrable : bcrypt a un cost factor (2^cost itérations), argon2id des paramètres mémoire/temps/parallélisme. On règle pour que le calcul prenne ~100 ms côté serveur : imperceptible pour un login, catastrophique pour un attaquant qui doit tester des milliards de candidats. Et quand le matériel progresse, on augmente le paramètre.
| Algorithme | Vitesse | Salt intégré | Usage correct |
|---|---|---|---|
| MD5 | Très rapide, cassé | Non | Plus rien (checksum non sécurisé au mieux) |
| SHA-256 | Très rapide | Non | Intégrité de fichiers, signatures — pas les mots de passe |
| bcrypt | Lente, coût réglable | Oui | Mots de passe (standard éprouvé, limite 72 octets) |
| scrypt | Lente, coûteuse en RAM | Oui | Mots de passe |
| argon2id | Lente, RAM + CPU réglables | Oui | Mots de passe (recommandation OWASP actuelle) |
Concepts clés à maîtriser
- Fonction à sens unique : facile à calculer dans un sens, infaisable à inverser. La « cassure » d’un hash de mot de passe n’est jamais une inversion mathématique : c’est du brute force ou du dictionnaire — d’où l’importance de la lenteur.
- Rainbow tables vs salt : la table précalculée ne sert que si tout le monde hashe pareil. Un salt de 16 octets aléatoires par utilisateur rend le précalcul inutile.
- Timing attack : comparer deux chaînes avec
===s’arrête au premier octet différent — le temps de réponse fuit de l’information. Pour tout secret (tokens d’API, signatures HMAC), utiliser une comparaison en temps constant (crypto.timingSafeEqualen Node). Bonne nouvelle :bcrypt.comparele fait déjà pour vous. - Reset de mot de passe bien fait : générer un token aléatoire, à usage unique, expirant (15-60 min), en stocker le hash en DB (le lien email est un secret comme un autre), l’invalider après usage, et répondre « si ce compte existe, un email a été envoyé » pour ne pas révéler quels emails sont inscrits (énumération de comptes).
- Pepper (bonus) : un secret global côté serveur (hors DB) ajouté avant hashing — un dump de la base seule ne suffit plus. Optionnel, à mentionner comme approfondissement.
- MFA & passkeys (survol) : le hash protège le stockage, pas le phishing. Le second facteur (TOTP, WebAuthn) protège même si le mot de passe fuit ; les passkeys (paires de clés WebAuthn) suppriment carrément le mot de passe — rien de secret à stocker côté serveur, juste des clés publiques.
En Node, la version correcte tient en quelques lignes :
import bcrypt from "bcrypt";
const COST = 12; // 2^12 itérations ≈ 100-250 ms ; à augmenter avec le matériel
// Inscription : génère le salt ET hashe en un appel
async function register(email, password) {
const hash = await bcrypt.hash(password, COST);
// hash = "$2b$12$N9qo8uLO...": algo, coût et salt inclus dans la chaîne
await db.users.insert({ email, hash }); // on ne stocke QUE le hash
}
// Connexion : bcrypt relit le salt dans le hash stocké,
// recalcule, et compare en temps constant
async function login(email, password) {
const user = await db.users.findByEmail(email);
// comparer même si l'utilisateur n'existe pas → temps de réponse uniforme
const ok = user && (await bcrypt.compare(password, user.hash));
if (!ok) throw new AuthError("Identifiants invalides"); // message volontairement vague
return createSession(user);
}
💡 Le format bcrypt — la chaîne
$2b$12$...contient tout : la version de l’algo (2b), le coût (12), puis salt + hash encodés. C’est pour ça qu’il n’y a pas de colonnesaltdans la table : montrer qu’on le sait fait très bon effet.
En entretien
« Comment stockes-tu les mots de passe de tes utilisateurs ? » — Jamais en clair, jamais chiffrés (réversible). Hash avec un algorithme dédié — bcrypt ou argon2id — avec un salt unique par utilisateur et un facteur de coût réglé autour de 100 ms. À la connexion, on recalcule et on compare en temps constant.
« Pourquoi SHA-256 ne suffit pas alors que c’est un hash cryptographique ? » — Parce qu’il est conçu pour être rapide : des milliards de hashs/seconde sur GPU, donc dictionnaires et brute force redeviennent praticables sur un dump. Les algorithmes de mots de passe sont volontairement lents et coûteux en mémoire, avec un paramètre qu’on augmente au fil des années.
« À quoi sert le salt, et doit-il rester secret ? » — Il rend chaque hash unique : il neutralise les rainbow tables et empêche de repérer deux utilisateurs avec le même mot de passe. Il n’est pas secret — bcrypt le stocke en clair dans sa sortie ; c’est la lenteur de l’algo qui protège, pas le secret du salt.
« Qu’est-ce qu’une timing attack ? » — Une comparaison naïve (===) s’arrête au premier caractère différent : en mesurant le temps de réponse, un attaquant devine un secret octet par octet. Contre-mesure : comparaison en temps constant (crypto.timingSafeEqual, bcrypt.compare). Valable pour tout secret : tokens, signatures de webhooks.
« Comment concevoir un “mot de passe oublié” sûr ? » — Token aléatoire à usage unique, expirant (15-60 min), dont on stocke le hash en DB ; invalidé après usage ; réponse identique que l’email existe ou non pour éviter l’énumération de comptes ; et on ne révèle jamais l’ancien mot de passe — on ne le connaît pas, c’est justement le but.
Pièges & idées reçues
⚠️ « On m’a renvoyé mon mot de passe par email » — signal rouge absolu : si un site peut vous le renvoyer, il le stocke en clair ou chiffré. Un reset bien fait envoie un lien, jamais le mot de passe.
- « Je double le hash : md5(sha1(x)), c’est plus sûr » — non : empiler des hashs rapides reste rapide. La sécurité vient du coût paramétrable, pas de l’exotisme de la recette maison. Règle d’or : ne jamais inventer sa crypto.
- « Le salt doit être caché dans une autre table » — inutile : le modèle de menace suppose que l’attaquant a tout. Le salt combat le précalcul, pas la lecture.
- Imposer des règles absurdes (majuscule + symbole + rotation tous les 90 jours) produit
Password1!puisPassword2!. Les recommandations NIST actuelles : longueur d’abord, vérifier contre les listes de mots de passe compromis, pas de rotation forcée. - Limiter les tentatives reste indispensable : le meilleur hash du monde ne protège pas contre un brute force en ligne sur le formulaire. Rate limiting, backoff, verrouillage progressif.
- bcrypt tronque à 72 octets : au-delà, le reste du mot de passe est ignoré. C’est documenté, rarement bloquant, mais bon à savoir (argon2id n’a pas cette limite).
Pour aller plus loin
- OWASP Password Storage Cheat Sheet — la référence à citer en entretien
- NIST SP 800-63B — les recommandations officielles sur les règles de mots de passe
- Have I Been Pwned et son API Pwned Passwords (k-anonymity) pour rejeter les mots de passe déjà compromis
- webauthn.guide pour comprendre passkeys et WebAuthn — la suite logique de cette fiche
The essentials
A password is never stored in plaintext — obvious. But it’s not stored encrypted either: encryption is reversible by definition. If the key leaks (and it always ends up leaking: database dump, stolen backup, malicious admin), every password is recoverable at once.
The right answer: a cryptographic hash, a one-way function. You store hash(password), never the password. At login, you recompute the hash of what the user typed and compare. Nobody — not even you — can recover the original password from the hash.
⚠️ Encrypted ≠ hashed — encryption (AES, RSA…) is designed to be reversed with the right key; a hash is designed to never be. Saying “I encrypt passwords” in an interview is an instant red flag. The right phrasing: “I hash them with a dedicated algorithm and a salt”.
But beware: not all hashes are equal. MD5 and SHA-256 are cryptographic hashes… designed to be fast. A modern GPU computes billions of SHA-256 per second: an attacker who steals your database runs through an entire dictionary in minutes. You need a deliberately slow and expensive algorithm: bcrypt, scrypt or argon2id.
How it works
The full flow, signup then login:
SIGNUP
user ──"hunter2"──▶ server
│ salt = unique random value
│ hash = bcrypt(salt + "hunter2")
▼
DB: { email, hash } (the salt lives
inside the hash)
LOGIN
user ──"hunter2"──▶ server
│ reads the hash from the DB
│ recomputes with the same salt
│ constant-time compare
▼
equal? ── yes ──▶ session/JWT
└─ no ──▶ 401 (vague message)
Two ingredients make this flow solid:
- The salt: a unique random value per user, concatenated with the password before hashing. Without a salt, two users with the same password get the same hash, and worse, an attacker can precompute rainbow tables (giant hash → password tables) once and for all. With a unique salt, every hash must be attacked individually. The salt is not secret: bcrypt stores it in plain sight inside its output string.
- The tunable cost: bcrypt has a cost factor (2^cost iterations), argon2id has memory/time/parallelism parameters. You tune it so the computation takes ~100 ms server-side: imperceptible for one login, catastrophic for an attacker who must test billions of candidates. And when hardware improves, you raise the parameter.
| Algorithm | Speed | Built-in salt | Correct use |
|---|---|---|---|
| MD5 | Very fast, broken | No | Nothing anymore (non-security checksum at best) |
| SHA-256 | Very fast | No | File integrity, signatures — not passwords |
| bcrypt | Slow, tunable cost | Yes | Passwords (battle-tested standard, 72-byte limit) |
| scrypt | Slow, RAM-hungry | Yes | Passwords |
| argon2id | Slow, tunable RAM + CPU | Yes | Passwords (current OWASP recommendation) |
Key concepts to master
- One-way function: easy to compute one way, infeasible to invert. “Cracking” a password hash is never a mathematical inversion: it’s brute force or a dictionary — which is exactly why slowness matters.
- Rainbow tables vs salt: a precomputed table only helps if everyone hashes the same way. A random 16-byte salt per user makes precomputation useless.
- Timing attack: comparing two strings with
===stops at the first differing byte — the response time leaks information. For any secret (API tokens, HMAC signatures), use a constant-time comparison (crypto.timingSafeEqualin Node). Good news:bcrypt.comparealready does it for you. - Password reset done right: generate a random, single-use, expiring token (15-60 min), store its hash in the DB (the email link is a secret like any other), invalidate it after use, and respond “if this account exists, an email has been sent” so you don’t reveal which emails are registered (account enumeration).
- Pepper (bonus): a global server-side secret (kept out of the DB) added before hashing — a database dump alone is no longer enough. Optional, worth mentioning as a deep cut.
- MFA & passkeys (overview): hashing protects storage, not phishing. A second factor (TOTP, WebAuthn) protects even if the password leaks; passkeys (WebAuthn key pairs) remove the password entirely — nothing secret to store server-side, just public keys.
In Node, the correct version fits in a few lines:
import bcrypt from "bcrypt";
const COST = 12; // 2^12 iterations ≈ 100-250 ms; raise it as hardware improves
// Signup: generates the salt AND hashes in one call
async function register(email, password) {
const hash = await bcrypt.hash(password, COST);
// hash = "$2b$12$N9qo8uLO...": algo, cost and salt embedded in the string
await db.users.insert({ email, hash }); // store ONLY the hash
}
// Login: bcrypt reads the salt back from the stored hash,
// recomputes, and compares in constant time
async function login(email, password) {
const user = await db.users.findByEmail(email);
// compare even if the user doesn't exist → uniform response time
const ok = user && (await bcrypt.compare(password, user.hash));
if (!ok) throw new AuthError("Invalid credentials"); // deliberately vague message
return createSession(user);
}
💡 The bcrypt format — the
$2b$12$...string contains everything: the algorithm version (2b), the cost (12), then salt + hash encoded. That’s why there is nosaltcolumn in the table: showing you know this lands very well.
In an interview
“How do you store your users’ passwords?” — Never plaintext, never encrypted (reversible). Hashed with a dedicated algorithm — bcrypt or argon2id — with a unique salt per user and a cost factor tuned to around 100 ms. At login, recompute and compare in constant time.
“Why isn’t SHA-256 enough, since it’s a cryptographic hash?” — Because it’s designed to be fast: billions of hashes/second on a GPU, so dictionaries and brute force become practical again on a dump. Password algorithms are deliberately slow and memory-hard, with a parameter you raise over the years.
“What is the salt for, and must it stay secret?” — It makes every hash unique: it defeats rainbow tables and prevents spotting two users with the same password. It’s not secret — bcrypt stores it in plain sight in its output; the algorithm’s slowness protects you, not the salt’s secrecy.
“What is a timing attack?” — A naive comparison (===) stops at the first differing character: by measuring response times, an attacker guesses a secret byte by byte. Countermeasure: constant-time comparison (crypto.timingSafeEqual, bcrypt.compare). It applies to any secret: tokens, webhook signatures.
“How do you design a safe ‘forgot password’ flow?” — Random single-use token, expiring (15-60 min), whose hash is stored in the DB; invalidated after use; identical response whether the email exists or not to prevent account enumeration; and you never reveal the old password — you don’t know it, which is exactly the point.
Pitfalls & misconceptions
⚠️ “They emailed me my password back” — absolute red flag: if a site can send it back, it stores it plaintext or encrypted. A proper reset sends a link, never the password.
- “I double the hash: md5(sha1(x)), it’s safer” — no: stacking fast hashes stays fast. Security comes from the tunable cost, not from the exoticism of a homemade recipe. Golden rule: never roll your own crypto.
- “The salt must be hidden in another table” — pointless: the threat model assumes the attacker has everything. The salt defeats precomputation, not reading.
- Absurd complexity rules (uppercase + symbol + rotation every 90 days) produce
Password1!thenPassword2!. Current NIST guidance: length first, check against breached-password lists, no forced rotation. - Rate limiting is still essential: the best hash in the world doesn’t stop an online brute force against the login form. Rate limiting, backoff, progressive lockout.
- bcrypt truncates at 72 bytes: beyond that, the rest of the password is ignored. Documented, rarely a blocker, but good to know (argon2id has no such limit).
Going further
- OWASP Password Storage Cheat Sheet — the reference to cite in an interview
- NIST SP 800-63B — the official guidance on password rules
- Have I Been Pwned and its Pwned Passwords API (k-anonymity) to reject already-breached passwords
- webauthn.guide to understand passkeys and WebAuthn — the natural sequel to this card