First commit
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/.idea
|
||||
115
README.md
Normal file
115
README.md
Normal file
@@ -0,0 +1,115 @@
|
||||
# Démonstration : sécurisation des mots de passe (MD5 vs Argon2)
|
||||
|
||||
## Objectif
|
||||
|
||||
Comprendre pourquoi les algorithmes de hachage rapides comme MD5 ne doivent **jamais** être utilisés pour stocker des mots de passe. Implémenter la méthode moderne et sécurisée, **Argon2id**, de manière asynchrone pour ne pas bloquer le serveur.
|
||||
|
||||
Nous allons utiliser le routeur `routes/users.js` de notre application Express.
|
||||
|
||||
Voici un aperçu du flux général de l'application utilisateur → serveur :
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
client["client http"]
|
||||
router["routes /users"]
|
||||
md5["route /users/register-bad hash md5"]
|
||||
argon["route /users/register-good hash argon2id"]
|
||||
|
||||
client --> router
|
||||
router --> md5
|
||||
router --> argon
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Étape 1 : L'erreur (MD5) - ne faites pas ça ! (register)
|
||||
|
||||
L'objectif ici est de simuler ce qu'il ne faut **pas** faire.
|
||||
|
||||
Ouvrez le fichier `routes/users.js` et constatez que la route `POST /users/register-bad` est déjà créée pour vous.
|
||||
|
||||
Elle utilise l'algorithme de hachage MD5, qui est **inadapté** pour les mots de passe :
|
||||
|
||||
```javascript
|
||||
// Indice pour le hachage MD5
|
||||
const hash = crypto.createHash('md5').update(password).digest('hex');
|
||||
```
|
||||
|
||||
### Étape 2 : La preuve du danger (register)
|
||||
|
||||
1. Démarrez votre serveur Express :
|
||||
```bash
|
||||
nodemon ./bin/www
|
||||
```
|
||||
1. Avec un outil comme Postman, faites un appel `POST` sur `http://localhost:3000/users/register-bad` avec le JSON suivant :
|
||||
```json
|
||||
{
|
||||
"username": "testeur",
|
||||
"password": "123456"
|
||||
}
|
||||
```
|
||||
2. **Notez le hash renvoyé.** (Il devrait être `e10adc3949ba59abbe56e057f20f883e`. Vous pouvez le confirmer également via ce site : https://www.md5hashgenerator.com/).
|
||||
3. Allez sur le site [crackstation.net](https://crackstation.net/).
|
||||
4. Collez ce hash dans leur outil. Que se passe-t-il ?
|
||||
|
||||
|
||||
---
|
||||
|
||||
### Étape 3 : la correction (Argon2id Asynchrone)
|
||||
|
||||
Maintenant, implémentons la bonne pratique. Argon2 est conçu pour être lent et gourmand en mémoire, le rendant résistant aux attaques. Nous devons **absolument** l'utiliser en asynchrone pour ne pas bloquer le *thread* principal de Node.js.
|
||||
|
||||
1. Installez la bibliothèque recommandée :
|
||||
```bash
|
||||
npm install argon2
|
||||
```
|
||||
2. Dans `routes/users.js`, importez `argon2` :
|
||||
```javascript
|
||||
const argon2 = require('argon2');
|
||||
```
|
||||
3. Complétez la route `POST /users/register-good` :
|
||||
* Cette route **doit** être `async`.
|
||||
* Récupérez `username` et `password` depuis `req.body`.
|
||||
* Utilisez `await argon2.hash(...)` pour hacher le mot de passe.
|
||||
* Utilisez un bloc `try...catch` pour gérer les erreurs de hachage.
|
||||
* Renvoie le nouveau hachage sécurisé.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant c as client
|
||||
participant u as route /register-good
|
||||
participant a as argon2id
|
||||
|
||||
c->>u: envoie username + password
|
||||
u->>a: "hash du mot de passe"
|
||||
a-->>u: "hash argon2id"
|
||||
u-->>c: "hash retourné"
|
||||
```
|
||||
|
||||
|
||||
### Étape 4 : la vérification (login)
|
||||
|
||||
Pour un cycle complet, testez les routes de connexion suivantes :
|
||||
|
||||
1. `POST /users/login-bad` :
|
||||
* Récupère le mot de passe, le hache en MD5.
|
||||
* Compare le hash obtenu avec un hash "stocké" (ex: `e10adc3949ba59abbe56e057f20f883e`).
|
||||
* *Problème :* C'est une simple comparaison de chaînes de caractères.
|
||||
|
||||
2. `POST /users/login-good` : implémentation à faire !
|
||||
* Route `async`.
|
||||
* Récupère le hash "stocké" (celui de l'étape 3) et le mot de passe de la tentative.
|
||||
* Utilise `await argon2.verify(hashStocke, motDePasseTentative)`.
|
||||
* Cette fonction renvoie `true` ou `false`.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant c as client
|
||||
participant u as route /login-good
|
||||
participant a as argon2id
|
||||
|
||||
c->>u: envoie password
|
||||
u->>a: "verification du mot de passe"
|
||||
a-->>u: "true ou false"
|
||||
u-->>c: resultat authentification
|
||||
```
|
||||
37
app.js
Normal file
37
app.js
Normal file
@@ -0,0 +1,37 @@
|
||||
var createError = require('http-errors');
|
||||
var express = require('express');
|
||||
var logger = require('morgan');
|
||||
var usersRouter = require('./routes/users');
|
||||
|
||||
var app = express();
|
||||
|
||||
app.use(logger('dev'));
|
||||
app.use(express.json()); // Indispensable pour lire req.body en JSON
|
||||
app.use(express.urlencoded({ extended: false }));
|
||||
|
||||
// --- Routes ---
|
||||
app.use('/users', usersRouter); // Notre route API principale
|
||||
|
||||
// --- Gestion des erreurs ---
|
||||
|
||||
// catch 404 (route non trouvée) et transfert à l'errorHandler
|
||||
app.use(function(req, res, next) {
|
||||
next(createError(404, 'Route non trouvée'));
|
||||
});
|
||||
|
||||
// error handler
|
||||
app.use(function(err, req, res, next) {
|
||||
const errorDetails = req.app.get('env') === 'development' ? err : {};
|
||||
|
||||
// Renvoyer une erreur JSON au lieu d'une page HTML (res.render)
|
||||
res.status(err.status || 500);
|
||||
res.json({
|
||||
error: {
|
||||
message: err.message,
|
||||
status: err.status,
|
||||
details: errorDetails.stack // Affiche le stack trace en dev
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = app;
|
||||
90
bin/www
Normal file
90
bin/www
Normal file
@@ -0,0 +1,90 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
var app = require('../app');
|
||||
var debug = require('debug')('hachage:server');
|
||||
var http = require('http');
|
||||
|
||||
/**
|
||||
* Get port from environment and store in Express.
|
||||
*/
|
||||
|
||||
var port = normalizePort(process.env.PORT || '3000');
|
||||
app.set('port', port);
|
||||
|
||||
/**
|
||||
* Create HTTP server.
|
||||
*/
|
||||
|
||||
var server = http.createServer(app);
|
||||
|
||||
/**
|
||||
* Listen on provided port, on all network interfaces.
|
||||
*/
|
||||
|
||||
server.listen(port);
|
||||
server.on('error', onError);
|
||||
server.on('listening', onListening);
|
||||
|
||||
/**
|
||||
* Normalize a port into a number, string, or false.
|
||||
*/
|
||||
|
||||
function normalizePort(val) {
|
||||
var port = parseInt(val, 10);
|
||||
|
||||
if (isNaN(port)) {
|
||||
// named pipe
|
||||
return val;
|
||||
}
|
||||
|
||||
if (port >= 0) {
|
||||
// port number
|
||||
return port;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event listener for HTTP server "error" event.
|
||||
*/
|
||||
|
||||
function onError(error) {
|
||||
if (error.syscall !== 'listen') {
|
||||
throw error;
|
||||
}
|
||||
|
||||
var bind = typeof port === 'string'
|
||||
? 'Pipe ' + port
|
||||
: 'Port ' + port;
|
||||
|
||||
// handle specific listen errors with friendly messages
|
||||
switch (error.code) {
|
||||
case 'EACCES':
|
||||
console.error(bind + ' requires elevated privileges');
|
||||
process.exit(1);
|
||||
break;
|
||||
case 'EADDRINUSE':
|
||||
console.error(bind + ' is already in use');
|
||||
process.exit(1);
|
||||
break;
|
||||
default:
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Event listener for HTTP server "listening" event.
|
||||
*/
|
||||
|
||||
function onListening() {
|
||||
var addr = server.address();
|
||||
var bind = typeof addr === 'string'
|
||||
? 'pipe ' + addr
|
||||
: 'port ' + addr.port;
|
||||
debug('Listening on ' + bind);
|
||||
}
|
||||
1893
package-lock.json
generated
Normal file
1893
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
18
package.json
Normal file
18
package.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "hachage",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "node ./bin/www"
|
||||
},
|
||||
"dependencies": {
|
||||
"argon2": "^0.44.0",
|
||||
"cookie-parser": "~1.4.4",
|
||||
"debug": "~2.6.9",
|
||||
"express": "~4.16.1",
|
||||
"http-errors": "~1.6.3",
|
||||
"morgan": "~1.9.1",
|
||||
"nodemon": "^3.1.10",
|
||||
"pug": "2.0.0-beta11"
|
||||
}
|
||||
}
|
||||
153
routes/users.js
Normal file
153
routes/users.js
Normal file
@@ -0,0 +1,153 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const crypto = require('crypto'); // Natif à Node.js
|
||||
const argon2 = require('argon2'); // Bibliothèque installée
|
||||
|
||||
/* Simulation d'une base de données utilisateurs.
|
||||
Dans une vraie app, on utiliserait MongoDB, MySQL, PostgreSQL, etc.
|
||||
On la place ici pour que la démo soit auto-contenue.
|
||||
*/
|
||||
const fakeDatabase = {};
|
||||
|
||||
/*
|
||||
==================================================================
|
||||
DÉMONSTRATION 1 : LA MAUVAISE MÉTHODE (MD5)
|
||||
==================================================================
|
||||
*/
|
||||
|
||||
/**
|
||||
* @route POST /users/register-bad
|
||||
* @desc Enregistre un utilisateur avec MD5 (DANGEREUX !)
|
||||
*/
|
||||
router.post('/register-bad', (req, res) => {
|
||||
try {
|
||||
const { username, password } = req.body;
|
||||
|
||||
if (!username || !password) {
|
||||
return res.status(400).json({ msg: 'Veuillez fournir un username et un password' });
|
||||
}
|
||||
|
||||
// Hachage MD5 : rapide, synchrone, non salé.
|
||||
// Une simple recherche Google ou Crackstation inverse ce hash.
|
||||
const hash = crypto.createHash('md5').update(password).digest('hex');
|
||||
|
||||
// Stockage du hash "cassable"
|
||||
fakeDatabase[username] = { hash, algorithm: 'md5' };
|
||||
|
||||
console.log('Utilisateur (MD5) créé :', fakeDatabase[username]);
|
||||
|
||||
res.status(201).json({
|
||||
msg: 'Utilisateur créé avec MD5 (Non sécurisé !)',
|
||||
username: username,
|
||||
hash: hash,
|
||||
// On montre que le mot de passe "123456" donne bien le hash connu
|
||||
note: (password === '123456' ? 'Hash pour 123456: e10adc3949ba59abbe56e057f20f883e' : '')
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @route POST /users/login-bad
|
||||
* @desc Connecte un utilisateur avec MD5
|
||||
*/
|
||||
router.post('/login-bad', (req, res) => {
|
||||
const { username, password } = req.body;
|
||||
const user = fakeDatabase[username];
|
||||
|
||||
if (!user || user.algorithm !== 'md5') {
|
||||
return res.status(404).json({ msg: 'Utilisateur non trouvé ou mauvais algo' });
|
||||
}
|
||||
|
||||
// On re-hache la tentative de mdp
|
||||
const attemptHash = crypto.createHash('md5').update(password).digest('hex');
|
||||
|
||||
// Simple comparaison de chaînes.
|
||||
if (user.hash === attemptHash) {
|
||||
res.json({ msg: 'Connexion MD5 réussie ! (Mais vous n\'êtes pas en sécurité)' });
|
||||
} else {
|
||||
res.status(401).json({ msg: 'Mot de passe incorrect' });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
/*
|
||||
==================================================================
|
||||
DÉMONSTRATION 2 : LA BONNE MÉTHODE (Argon2id)
|
||||
==================================================================
|
||||
*/
|
||||
|
||||
/**
|
||||
* @route POST /users/register-good
|
||||
* @desc Enregistre un utilisateur avec Argon2id (SÉCURISÉ)
|
||||
*/
|
||||
router.post('/register-good', async (req, res) => {
|
||||
// La route est déclarée "async"
|
||||
const { username, password } = req.body;
|
||||
|
||||
if (!username || !password) {
|
||||
return res.status(400).json({ msg: 'Veuillez fournir un username et un password' });
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. Le 'await' est crucial.
|
||||
// Pendant que le CPU calcule le hash (ce qui est LENT),
|
||||
// le thread principal de Node.js est libéré et peut traiter
|
||||
// d'autres requêtes.
|
||||
|
||||
const hash = ''; // TODO
|
||||
|
||||
// 2. Le hash contient le sel (salt) et les paramètres de coût.
|
||||
// Il est auto-suffisant.
|
||||
// ex: $argon2id$v=19$m=65536,t=3,p=1$Gb...
|
||||
fakeDatabase[username] = { hash, algorithm: 'argon2id' };
|
||||
|
||||
console.log('Utilisateur (Argon2id) créé :', fakeDatabase[username]);
|
||||
|
||||
res.status(201).json({
|
||||
msg: 'Utilisateur créé avec Argon2id (Sécurisé !)',
|
||||
username: username,
|
||||
hash: hash
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ msg: 'Erreur lors du hachage du mot de passe' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @route POST /users/login-good
|
||||
* @desc Connecte un utilisateur avec Argon2id
|
||||
*/
|
||||
router.post('/login-good', async (req, res) => {
|
||||
const { username, password } = req.body;
|
||||
const user = fakeDatabase[username];
|
||||
|
||||
if (!user || user.algorithm !== 'argon2id') {
|
||||
return res.status(404).json({ msg: 'Utilisateur non trouvé ou mauvais algo' });
|
||||
}
|
||||
|
||||
try {
|
||||
// 3. On utilise 'argon2.verify'
|
||||
// Cette fonction lit le hash, extrait le sel et les paramètres,
|
||||
// re-hache la tentative (password) et compare en temps constant.
|
||||
const isMatch = false; // TODO
|
||||
|
||||
if (isMatch) {
|
||||
res.json({ msg: 'Connexion Argon2id réussie ! (Vous êtes en sécurité)' });
|
||||
} else {
|
||||
res.status(401).json({ msg: 'Mot de passe incorrect' });
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
// Si le format du hash est mauvais, ou autre erreur
|
||||
console.error(err);
|
||||
res.status(500).json({ msg: 'Erreur lors de la vérification' });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
module.exports = router;
|
||||
Reference in New Issue
Block a user