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