Hashing & Salting
- sécurité
- authentification
- cryptographie
En 2012, LinkedIn se fait pirater. 6,5 millions de mots de passe sont publiés en ligne. Quelques heures plus tard, la majorité sont déchiffrés. Le problème ? LinkedIn utilisait SHA-1 sans salting. Des mots de passe banals comme "password" ou "123456" sont craqués en quelques secondes grâce à des tables précalculées.
Comprendre pourquoi ça arrive, et comment l'éviter, c'est l'objet du Hashing & Salting.
Le hachage (Hashing)
Un hash est une fonction à sens unique : elle transforme n'importe quelle entrée en une chaîne de longueur fixe. On ne peut pas revenir en arrière.
SHA-256("password") → 5e884898da28047151d0e56f8dc62927...
SHA-256("password1") → 0b14d501a594442a01c6859541bcb3e8...
L'idée : stocker le hash du mot de passe, jamais le mot de passe lui-même. Lors de la connexion, on hache ce que l'utilisateur tape et on compare.
// À l'inscription
const hash = sha256(motDePasse);
db.save({ userId, hash });
// À la connexion
const hash = sha256(motDePasseSaisi);
const valide = hash === db.get(userId).hash;C'est mieux que stocker en clair — mais pas suffisant.
Le problème : les Rainbow Tables
Les attaquants n'ont pas besoin de "déchiffrer" un hash. Ils précalculent des millions de hashs courants et les stockent dans des rainbow tables :
"password" → 5e884898da2804...
"123456" → 8d969eef6ecad3...
"azerty" → ab87d24bdc7452...
"iloveyou" → e10adc3949ba59...
Si votre hash correspond à une entrée de leur table : craqué en millisecondes.
Et pire encore : si deux utilisateurs ont le même mot de passe, ils ont le même hash. Cracker l'un craque l'autre automatiquement.
La solution : le salting
Le sel (salt) est une chaîne aléatoire unique, générée pour chaque utilisateur, ajoutée au mot de passe avant de le hacher.
hash = SHA-256(motDePasse + sel)
// À l'inscription
const sel = generateRandomBytes(32); // chaîne aléatoire unique
const hash = sha256(motDePasse + sel);
db.save({ userId, hash, sel }); // on stocke le sel en clair, c'est normal
// À la connexion
const { hash, sel } = db.get(userId);
const hashSaisi = sha256(motDePasseSaisi + sel);
const valide = hashSaisi === hash;Le sel n'est pas secret — il est stocké en clair à côté du hash. Son rôle n'est pas d'être secret mais de rendre chaque hash unique.
Utilisateur A : SHA-256("password" + "a3f8c2...") → 9d2a71b...
Utilisateur B : SHA-256("password" + "b7e1d4...") → 4f8c23a...
Même mot de passe, sels différents → hashs complètement différents. Les rainbow tables deviennent inutilisables : il faudrait en recalculer une pour chaque sel unique.
En pratique : bcrypt, argon2, scrypt
SHA-256 est rapide — trop rapide. Un attaquant peut tenter des milliards de combinaisons par seconde avec du matériel adapté (GPU). Les algorithmes modernes de hachage de mots de passe sont intentionnellement lents :
| Algorithme | Sel intégré | Vitesse intentionnelle | Recommandé |
|---|---|---|---|
| MD5 | Non | Très rapide | Non ❌ |
| SHA-256 | Non | Rapide | Non ❌ |
| bcrypt | Oui | Configurable | Oui ✅ |
| argon2 | Oui | Configurable | Oui ✅ |
| scrypt | Oui | Configurable | Oui ✅ |
bcrypt est le standard le plus utilisé aujourd'hui. Il intègre automatiquement le sel et un facteur de coût (work factor) qui détermine la lenteur :
import bcrypt from "bcrypt";
// À l'inscription — le sel est généré et intégré automatiquement
const rounds = 12; // facteur de coût (2^12 itérations)
const hash = await bcrypt.hash(motDePasse, rounds);
// → "$2b$12$K9L8mN2pQ7rS4tU6vW8xY.O3P5R7T9V1X3Z5B7D9F1H3J5L7N9P1"
// ↑ algorithme + rounds + sel + hash, tout dans une seule chaîne
// À la connexion — bcrypt extrait le sel du hash stocké
const valide = await bcrypt.compare(motDePasseSaisi, hash);Le facteur de coût est crucial : augmentez-le chaque année pour rester en phase avec la puissance des machines. À rounds=12, un hash prend ~300ms — tolérable pour un utilisateur, dissuasif pour un attaquant qui en teste des millions.
Ce qu'il ne faut jamais faire
// ❌ Stocker en clair
db.save({ userId, motDePasse });
// ❌ Chiffrement (réversible — si la clé fuite, tout fuite)
db.save({ userId, motDePasse: encrypt(motDePasse, secretKey) });
// ❌ MD5 ou SHA-1 (trop rapides, vulnérables)
db.save({ userId, hash: md5(motDePasse) });
// ❌ SHA-256 sans sel
db.save({ userId, hash: sha256(motDePasse) });
// ✅ bcrypt, argon2 ou scrypt
db.save({ userId, hash: await bcrypt.hash(motDePasse, 12) });Pepper : la couche supplémentaire (optionnelle)
Certains systèmes ajoutent un poivre (pepper) : une valeur secrète stockée dans la configuration de l'application (pas en base), ajoutée en plus du sel.
hash = bcrypt(motDePasse + sel + poivre)
Si la base de données est compromise mais pas le code/config : le poivre protège même les hashs récupérés. C'est une défense en profondeur — utile, mais la priorité reste un bcrypt correctement configuré.
Le Hashing & Salting, c'est la base non-négociable de la gestion des mots de passe. Utilisez bcrypt ou argon2, laissez-les gérer le sel automatiquement, et choisissez un facteur de coût adapté. Tout le reste — chiffrement réversible, MD5, SHA-256 pur — est une faute de sécurité qui attend son heure.