Aller au contenu principal
Sécurité

Hashing & Salting

6 min de lecture
  • sécurité
  • authentification
  • cryptographie
Hashing & Salting : pourquoi le sel change tout

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 :

AlgorithmeSel intégréVitesse intentionnelleRecommandé
MD5NonTrès rapideNon ❌
SHA-256NonRapideNon ❌
bcryptOuiConfigurableOui ✅
argon2OuiConfigurableOui ✅
scryptOuiConfigurableOui ✅

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.