← Cybersécurité

Stockage des mots de passe

Sommaire

Défi n°2 du TP Cyber-Rescue : passer d'un stockage en clair à un stockage conforme aux recommandations ANSSI / OWASP.

1. Le constat

SELECT pseudo, mdp FROM joueurs LIMIT 5;

Si la colonne mdp affiche des mots de passe lisibles, ou des chaînes de 32 caractères hexadécimaux (signature d'un MD5 sans sel), le stockage est inacceptable.

L'ANSSI et l'OWASP recommandent une fonction de dérivation lente et salée : bcrypt, scrypt ou Argon2.

2. Hachage, pas chiffrement

ChiffrementHachage
RéversibleOui, avec la cléNon, par construction
UsageProtéger une donnée qu'il faudra relireVérifier une correspondance sans stocker l'original
Pour un mot de passeInadapté : la clé est sur le serveur, donc récupérable avec la baseAdapté : même compromise, la base ne rend pas les mots de passe

On ne « décrypte » jamais un mot de passe : on rehache la saisie et l'on compare les empreintes. C'est aussi pourquoi un site sérieux ne peut pas vous renvoyer votre mot de passe par mail — il ne le connaît pas.

3. Le sel

Un sel est une valeur aléatoire, unique par utilisateur, ajoutée au mot de passe avant hachage. Il est stocké en clair à côté du hash — son rôle n'est pas d'être secret.

Sans sel, deux utilisateurs ayant le même mot de passe produisent le même hash. Un attaquant peut alors :

Avec un sel unique, chaque mot de passe doit être attaqué séparément, ce qui multiplie le coût par le nombre de comptes. bcrypt génère et intègre automatiquement un sel : il est inclus dans la chaîne produite par password_hash(), aux côtés de l'algorithme et du coût. Rien à gérer manuellement.

$2y$10$N9qo8uLOickgx2ZMRZoMye....
 │   │  └── sel (22 caractères) puis empreinte
 │   └───── coût (2^10 itérations)
 └───────── identifiant d'algorithme (bcrypt)

4. Migrer une base existante

<?php
// migrate_passwords.php — à exécuter une seule fois, après sauvegarde de la base
require 'db.php';

$users = $pdo->query("SELECT id, mdp FROM joueurs")->fetchAll(PDO::FETCH_ASSOC);
$maj   = $pdo->prepare("UPDATE joueurs SET mdp = :hash WHERE id = :id");

foreach ($users as $u) {
    // Ne pas rehacher un mot de passe déjà haché
    if (password_get_info($u['mdp'])['algo']) {
        continue;
    }
    $hash = password_hash($u['mdp'], PASSWORD_BCRYPT);
    $maj->execute([':hash' => $hash, ':id' => $u['id']]);
}

5. bcrypt et Argon2

bcryptArgon2id
Année19992015, lauréat de la Password Hashing Competition
Paramètre de coûtTemps (nombre d'itérations)Temps, mémoire et parallélisme
Résistance GPU / ASICLimitée : faible empreinte mémoire, donc très parallélisableForte : la consommation mémoire imposée rend le calcul massivement parallèle coûteux
PHPPASSWORD_BCRYPTPASSWORD_ARGON2ID (PHP 7.3+)

C'est le paramètre mémoire qui explique le remplacement progressif de bcrypt : une carte graphique peut mener des milliers de calculs bcrypt en parallèle, mais si chaque calcul exige plusieurs dizaines de mégaoctets, le parallélisme s'effondre.

$hash = password_hash($mdp, PASSWORD_ARGON2ID, [
    'memory_cost' => 65536,  // 64 Mo
    'time_cost'   => 4,
    'threads'     => 2,
]);

Utiliser PASSWORD_DEFAULT plutôt qu'un algorithme figé permet à PHP de basculer vers un algorithme plus robuste au fil des versions. À coupler avec password_needs_rehash() lors de la connexion, pour remettre à niveau les anciens hachages de façon transparente.