Stocker les mots de passe correctement : bcrypt et argon2

1. Aperçu

Stocker les mots de passe de manière inadéquate est l’une des vulnérabilités les plus courantes qui exposent les données des utilisateurs lors d’une attaque contre la base de données. Cet article vous aide à comprendre pourquoi vous ne devriez pas utiliser MD5/SHA256 purs pour hacher les mots de passe, et comment utiliser BCryptPasswordEncoder ainsi que Argon2PasswordEncoder dans Spring Security. À la fin, vous serez capable de configurer un PasswordEncoder sécurisé, d’écrire des tests pour vérifier le comportement, et de choisir l’algorithme approprié pour votre projet.

2. Pourquoi vous ne devriez pas hacher les mots de passe avec SHA-256 pur

Les fonctions de hachage cryptographique comme SHA-256, SHA-512 sont conçues pour s’exécuter aussi rapidement que possible. C’est un avantage pour vérifier l’intégrité des fichiers, mais c’est un désastre pour les mots de passe.

import java.security.MessageDigest;
import java.nio.charset.StandardCharsets;

public class NaiveHasher {

    public static String sha256(String password) throws Exception {
        MessageDigest digest = MessageDigest.getInstance("SHA-256");
        byte[] hash = digest.digest(password.getBytes(StandardCharsets.UTF_8));
        return java.util.HexFormat.of().formatHex(hash);
    }
}

La fonction ci-dessus peut hacher des milliards de mots de passe par seconde sur un GPU milieu de gamme. Un attaquant n’a besoin que d’une table arc-en-ciel ou d’une attaque par force brute pour retrouver le mot de passe d’origine, surtout avec des mots de passe courants comme 123456, password1.

Deux problèmes fondamentaux :

  • Pas de salt : deux utilisateurs avec le même mot de passe produisent le même hash, vulnérable aux attaques par table arc-en-ciel.
  • Trop rapide : il n’y a aucun moyen de « ralentir » l’attaquant.

Remarque : N’ajoutez pas manuellement du salt et ne le concaténez pas avec SHA-256. C’est toujours une solution faible car il manque un « work factor » — la difficulté de calcul n’augmente pas avec l’amélioration du matériel.

3. Qu’est-ce qu’une fonction de hachage de mot de passe

Les algorithmes comme bcrypt, scrypt, argon2 sont conçus spécifiquement pour hacher les mots de passe, avec trois caractéristiques :

  1. Salt automatique : chaque fois que vous hachez, un salt aléatoire est généré et intégré directement dans la sortie du hash.
  2. Intentionnellement lent (work factor) : il y a des paramètres pour augmenter la difficulté et lutter contre les attaques par force brute à mesure que le matériel s’améliore.
  3. Memory-hard (avec argon2/scrypt) : consomme beaucoup de RAM, rendant l’exécution parallèle sur GPU/ASIC moins efficace.

Le résultat d’une chaîne de hachage bcrypt ressemble à ceci :

$2a$10$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW

Structure : $<version>$<cost>$<salt+hash base64>. Ici, 10 est le cost factor (2^10 itérations).

4. Utiliser bcrypt avec Spring Security

4.1. Dépendance

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

BCryptPasswordEncoder est déjà disponible dans spring-security-crypto, aucune dépendance supplémentaire n’est nécessaire.

4.2. Déclarer le bean

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
public class SecurityConfig {

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(12); // cost factor = 12
    }
}

Le paramètre 12 détermine le nombre d’itérations à 2^12. Cost factor plus élevé → plus lent → plus sûr, mais consomme aussi plus de CPU lors de la connexion. Une valeur de 10–12 est raisonnable pour la plupart des applications en 2026.

4.3. Utilisation lors de l’inscription et de la connexion

import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

@Service
public class UserRegistrationService {

    private final PasswordEncoder passwordEncoder;
    private final UserRepository userRepository;

    public UserRegistrationService(PasswordEncoder passwordEncoder, UserRepository userRepository) {
        this.passwordEncoder = passwordEncoder;
        this.userRepository = userRepository;
    }

    public void register(String email, String rawPassword) {
        String hashed = passwordEncoder.encode(rawPassword);
        userRepository.save(new Customer(email, hashed));
    }

    public boolean authenticate(String email, String rawPassword) {
        Customer customer = userRepository.findByEmail(email)
                .orElseThrow(() -> new IllegalArgumentException("Email n'existe pas"));
        return passwordEncoder.matches(rawPassword, customer.getPasswordHash());
    }
}
  • encode() génère un salt aléatoire à chaque appel, donc le même rawPassword produira un hash différent à chaque fois — c’est le bon comportement, pas un bug.
  • matches() extrait automatiquement le salt du hash stocké en DB, hache à nouveau le mot de passe saisi, puis le compare, vous n’avez pas besoin de gérer vous-même le salt.

4.4. Tester le comportement

import org.junit.jupiter.api.Test;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

import static org.assertj.core.api.Assertions.assertThat;

class BcryptPasswordEncoderTest {

    private final BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(10);

    @Test
    void shouldGenerateDifferentHashesForSamePassword() {
        String hash1 = encoder.encode("S3cretPass!");
        String hash2 = encoder.encode("S3cretPass!");

        assertThat(hash1).isNotEqualTo(hash2);
        assertThat(encoder.matches("S3cretPass!", hash1)).isTrue();
        assertThat(encoder.matches("S3cretPass!", hash2)).isTrue();
    }

    @Test
    void shouldRejectWrongPassword() {
        String hash = encoder.encode("S3cretPass!");

        assertThat(encoder.matches("WrongPass!", hash)).isFalse();
    }
}

Ce test confirme deux choses importantes : le salt aléatoire crée des hashes différents, et matches() reconnaît toujours correctement le mot de passe d’origine même si le hash est différent.

Remarque : bcrypt a une limite de longueur d’entrée de 72 octets. Si le mot de passe est plus long, la partie qui dépasse est silencieusement tronquée. Avec des mots de passe normaux (moins de 72 caractères), ce n’est pas un problème, mais si vous autorisez des passphrase très longues, considérez argon2.

5. Utiliser argon2 — un choix plus moderne

Argon2 a remporté le Password Hashing Competition en 2015 et est actuellement recommandé par l’OWASP comme premier choix grâce à sa meilleure résistance aux attaques GPU (memory-hard).

5.1. Dépendance

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-crypto</artifactId>
</dependency>
<dependency>
    <groupId>org.bouncycastle</groupId>
    <artifactId>bcprov-jdk18on</artifactId>
    <version>1.78.1</version>
</dependency>

Spring Security utilise Bouncy Castle comme fournisseur pour Argon2PasswordEncoder, vous devez donc ajouter cette dépendance.

5.2. Déclarer le bean

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.argon2.Argon2PasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
public class SecurityConfig {

    @Bean
    public PasswordEncoder passwordEncoder() {
        // saltLength=16, hashLength=32, parallelism=1, memory=1<<16 (64MB), iterations=3
        return Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8();
    }
}

La méthode factory defaultsForSpringSecurity_v5_8() retourne une configuration de paramètres vérifiée par l’équipe Spring Security comme sûre pour la plupart des applications, vous n’avez pas besoin de calculer vous-même la mémoire/les itérations.

5.3. Comparer la sortie

$argon2id$v=19$m=65536,t=3,p=1$c29tZXNhbHQ$5t1h3u7K3iC2rT0...

La structure comprend la variante (argon2id), la version, les paramètres mémoire (m), les itérations (t), le parallélisme (p), puis le salt et le hash — plus transparent que bcrypt car expose clairement le cost.

5.4. Tester la comparaison entre deux encodeurs

import org.junit.jupiter.api.Test;
import org.springframework.security.crypto.argon2.Argon2PasswordEncoder;

import static org.assertj.core.api.Assertions.assertThat;

class Argon2PasswordEncoderTest {

    private final Argon2PasswordEncoder encoder =
            Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8();

    @Test
    void shouldVerifyCorrectPassword() {
        String hash = encoder.encode("MyP@ssw0rd123");

        assertThat(encoder.matches("MyP@ssw0rd123", hash)).isTrue();
        assertThat(encoder.matches("wrong", hash)).isFalse();
    }
}

6. Supporter plusieurs encodeurs à la fois avec DelegatingPasswordEncoder

En pratique, votre système peut avoir besoin de changer d’algorithme au fil du temps (par exemple, migrer de bcrypt vers argon2) sans casser les anciens hash. Spring Security fournit une solution intégrée pour cela.

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
public class SecurityConfig {

    @Bean
    public PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }
}

createDelegatingPasswordEncoder() crée un encodeur qui sait lire les préfixes {bcrypt}, {argon2}, {noop}… dans la chaîne de hash pour sélectionner le bon algorithme de décodage. Par défaut, il utilise bcrypt pour encoder les nouveaux mots de passe, mais il matches() toujours correctement avec les anciens hash créés avec un algorithme différent.

{bcrypt}$2a$10$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW
{argon2}$argon2id$v=19$m=65536,t=3,p=1$...

Grâce à ce préfixe, vous pouvez migrer progressivement : les anciens utilisateurs peuvent toujours se connecter, les nouveaux utilisateurs ou ceux qui changent leur mot de passe seront hachés avec l’algorithme le plus récent.

7. Comparaison rapide bcrypt et argon2

Critère bcrypt argon2
Année de création 1999 2015 (lauréat du PHC)
Résistance GPU/ASIC Bon Meilleur (memory-hard)
Limite d’entrée 72 octets Pas de limite pratique
Popularité Très élevée, supporté partout De plus en plus populaire
Recommandation OWASP 2e choix 1er choix

Si c’est un nouveau projet et que vous n’êtes pas limité par un système hérité, choisissez argon2id (la variante argon2id est par défaut dans Spring Security, équilibrant la défense contre les attaques par side-channel et les attaques GPU). Si votre système utilise bcrypt depuis des années sans problème, vous n’avez pas besoin de migrer immédiatement — augmenter le cost factor bcrypt à 12–14 est toujours assez sûr pour la plupart des applications.

8. Erreurs courantes

  • Implémenter vous-même salt + SHA-256 : comme mentionné à la section 2, sans work factor, c’est toujours faible contre les attaques par force brute modernes.
  • Utiliser un cost factor trop bas : BCryptPasswordEncoder() sans paramètre utilise par défaut un cost de 10, mais si vous le réduisez à 4–5 pour « aller plus vite » pendant les tests, vous risquez d’oublier de le remonter en production.
  • Comparer les hash avec String.equals() : ne comparez jamais les chaînes de hash vous-même — utilisez toujours matches() du PasswordEncoder, car ces bibliothèques font la comparaison en temps constant, évitant les attaques par timing.
  • Stocker le salt dans une colonne séparée : inutile, bcrypt et argon2 intègrent déjà le salt dans la chaîne de hash de sortie.
  • Pas de limite de taux de connexion : peu importe la puissance du hash, sans rate limiting ou verrouillage de compte, les attaques par force brute en ligne restent possibles.

Remarque : N’utilisez jamais l’encodeur {noop} (plaintext) en production. Il ne devrait apparaître que dans les tests ou démos locales.

9. Conclusion

Hacher les mots de passe correctement signifie utiliser un algorithme conçu spécifiquement à cet effet — bcrypt ou argon2 — plutôt que des fonctions de hachage rapides comme SHA-256. Spring Security rend cela simple : déclarez simplement un bean PasswordEncoder, appelez encode() lors de la sauvegarde et matches() lors de l’authentification, sans avoir besoin de gérer le salt vous-même. Si vous avez besoin de migrer flexiblement entre les algorithmes, DelegatingPasswordEncoder est le choix sûr et rétro-compatible. Que vous choisissiez bcrypt ou argon2, rappelez-vous que le hachage puissant n’est qu’une couche de défense — combiné avec rate limiting et MFA, il crée un système d’authentification véritablement sécurisé.

web-securityspring-securitybcryptargon2authentication