Hachage des mots de passe avec bcrypt et argon2

Guide d'utilisation de bcrypt et argon2 pour le hachage sécurisé des mots de passe.

bcrypt en Node.js

const bcrypt = require('bcrypt');

class UserService {
  // Configuration recommandée
  static SALT_ROUNDS = 12;

  static async hashPassword(password) {
    try {
      const salt = await bcrypt.genSalt(this.SALT_ROUNDS);
      return await bcrypt.hash(password, salt);
    } catch (error) {
      throw new Error('Password hashing failed');
    }
  }

  static async verifyPassword(password, hash) {
    try {
      return await bcrypt.compare(password, hash);
    } catch (error) {
      throw new Error('Password verification failed');
    }
  }
}

// Utilisation dans un contrôleur
class AuthController {
  async register(req, res) {
    try {
      const { email, password } = req.body;

      const hashedPassword = await UserService.hashPassword(password);

      const user = await User.create({
        email,
        password: hashedPassword
      });

      res.status(201).json({
        message: 'User created successfully'
      });
    } catch (error) {
      res.status(500).json({
        message: 'Registration failed'
      });
    }
  }

  async login(req, res) {
    try {
      const { email, password } = req.body;

      const user = await User.findOne({ email });
      if (!user) {
        return res.status(401).json({
          message: 'Invalid credentials'
        });
      }

      const isValid = await UserService.verifyPassword(
        password,
        user.password
      );

      if (!isValid) {
        return res.status(401).json({
          message: 'Invalid credentials'
        });
      }

      // Génération du token JWT...
    } catch (error) {
      res.status(500).json({
        message: 'Login failed'
      });
    }
  }
}

argon2 en Python

from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError

class PasswordService:
    def __init__(self):
        # Configuration recommandée
        self.ph = PasswordHasher(
            time_cost=3,        # Nombre d'itérations
            memory_cost=65536,  # Utilisation mémoire (64 MB)
            parallelism=4,      # Degré de parallélisme
            hash_len=32,        # Longueur du hash
            salt_len=16         # Longueur du sel
        )

    def hash_password(self, password: str) -> str:
        try:
            return self.ph.hash(password)
        except Exception as e:
            raise ValueError("Password hashing failed") from e

    def verify_password(self, hash: str, password: str) -> bool:
        try:
            self.ph.verify(hash, password)
            return True
        except VerifyMismatchError:
            return False
        except Exception as e:
            raise ValueError("Password verification failed") from e

# Utilisation dans FastAPI
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel

app = FastAPI()
password_service = PasswordService()

class UserCreate(BaseModel):
    email: str
    password: str

@app.post("/register")
async def register(user: UserCreate):
    try:
        hashed_password = password_service.hash_password(
            user.password
        )

        # Création de l'utilisateur dans la base
        db_user = await User.create(
            email=user.email,
            password=hashed_password
        )

        return {"message": "User created successfully"}
    except Exception as e:
        raise HTTPException(
            status_code=500,
            detail="Registration failed"
        )

@app.post("/login")
async def login(user: UserCreate):
    try:
        db_user = await User.get(email=user.email)
        if not db_user:
            raise HTTPException(
                status_code=401,
                detail="Invalid credentials"
            )

        is_valid = password_service.verify_password(
            db_user.password,
            user.password
        )

        if not is_valid:
            raise HTTPException(
                status_code=401,
                detail="Invalid credentials"
            )

        # Génération du token JWT...
    except Exception as e:
        raise HTTPException(
            status_code=500,
            detail="Login failed"
        )

Meilleures pratiques de sécurité

Configuration

  • bcrypt : utiliser un coût minimum de 12
  • argon2 : préférer argon2id
  • Mémoire : au moins 64MB pour argon2
  • Temps : ajuster selon la charge serveur

Stockage

  • Ne jamais stocker en clair
  • Ne pas tronquer les hashes
  • Utiliser VARCHAR(255) minimum
  • Backup chiffré des données

Sécurité

  • Validation des mots de passe
  • Limiter les tentatives de connexion
  • Éviter les messages d'erreur précis
  • HTTPS obligatoire
// Validation du mot de passe
function validatePassword(password) {
  const MIN_LENGTH = 12;
  const REGEX = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{12,}$/;

  return password.length >= MIN_LENGTH && REGEX.test(password);
}

// Limitation des tentatives
const rateLimit = require('express-rate-limit');

const loginLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 5 // 5 tentatives
});

Migration des hashes

class PasswordMigrationService {
  static async migrateHash(user, plainPassword) {
    // Vérifier l'ancien hash (ex: MD5)
    if (user.password === md5(plainPassword)) {
      // Générer un nouveau hash sécurisé
      const newHash = await bcrypt.hash(
        plainPassword,
        12
      );

      // Mettre à jour l'utilisateur
      await User.update({
        id: user.id,
        password: newHash,
        password_version: 2
      });

      return true;
    }
    return false;
  }
}

// Utilisation pendant la connexion
async function login(email, password) {
  const user = await User.findOne({ email });

  if (user.password_version === 1) {
    // Ancienne version : tenter la migration
    const success = await PasswordMigrationService
      .migrateHash(user, password);
    if (!success) {
      throw new Error('Invalid credentials');
    }
  } else {
    // Nouvelle version : vérification normale
    const valid = await bcrypt.compare(
      password,
      user.password
    );
    if (!valid) {
      throw new Error('Invalid credentials');
    }
  }
}

Tests de sécurité

// Tests unitaires
describe('PasswordService', () => {
  const service = new PasswordService();

  test('should hash password correctly', async () => {
    const password = 'MySecurePass123!';
    const hash = await service.hashPassword(password);

    expect(hash).not.toBe(password);
    expect(hash).toMatch(/^\$2[aby]\$/);
  });

  test('should verify password correctly', async () => {
    const password = 'MySecurePass123!';
    const hash = await service.hashPassword(password);

    const isValid = await service.verifyPassword(
      hash,
      password
    );
    expect(isValid).toBe(true);

    const isInvalid = await service.verifyPassword(
      hash,
      'WrongPassword123!'
    );
    expect(isInvalid).toBe(false);
  });

  test('should use sufficient rounds', async () => {
    const start = Date.now();
    await service.hashPassword('test');
    const duration = Date.now() - start;

    // Devrait prendre au moins 250ms
    expect(duration).toBeGreaterThan(250);
  });
});