First commit

This commit is contained in:
Johan
2025-12-18 15:28:26 +01:00
commit 954e3640ff
8 changed files with 4431 additions and 0 deletions

208
server.js Normal file
View File

@@ -0,0 +1,208 @@
const express = require('express');
const sqlite3 = require('sqlite3').verbose();
const argon2 = require('argon2');
const jwt = require('jsonwebtoken');
const crypto = require('crypto');
const { v4: uuidv4 } = require('uuid');
const cors = require('cors');
const cookieParser = require('cookie-parser');
const app = express();
app.use(cors({
// Note : si vous ouvrez le fichier HTML en double cliquant (file://), les cookies ne marcheront pas.
// Il faut utiliser "Live Server" WebStorm (clic droit sur le fichier "index.html" > "Run 'index.html'")
origin: 'http://localhost:63342', // L'adresse EXACTE de votre client (pas *)
credentials: true // Autorise l'envoi de cookies
}));
app.use(express.json());
app.use(cookieParser());
// --- CONFIGURATION ---
const JWT_SECRET = 'jwt_secret_pour_signature';
const SERVER_MASTER_KEY = crypto.randomBytes(32); // Clé interne serveur pour protéger le JWT
// Note : Dans la vraie vie, SERVER_MASTER_KEY doit être fixe et dans un .env ou var. env. (sinon au restart serveur, les JWT ne marchent plus)
const DB_SOURCE = "db_final.sqlite";
// --- OUTILS CRYPTO ---
// 1. Chiffrement Générique AES-GCM
const encryptAES = (text, key) => {
const iv = crypto.randomBytes(12);
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
let encrypted = cipher.update(text, 'utf8', 'hex');
encrypted += cipher.final('hex');
return { content: encrypted, iv: iv.toString('hex'), tag: cipher.getAuthTag().toString('hex') };
};
// 2. Déchiffrement Générique AES-GCM
const decryptAES = (encryptedHex, key, ivHex, tagHex) => {
const decipher = crypto.createDecipheriv('aes-256-gcm', key, Buffer.from(ivHex, 'hex'));
decipher.setAuthTag(Buffer.from(tagHex, 'hex'));
let decrypted = decipher.update(encryptedHex, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
};
// 3. Dérivation Argon2 (Password -> Key)
const deriveKeyFromPass = async (pass, salt) => {
return await argon2.hash(pass, {
type: argon2.argon2id, raw: true, salt: Buffer.from(salt, 'hex'), hashLength: 32
});
};
// --- BDD ---
const db = new sqlite3.Database(DB_SOURCE, (err) => {
if (err) throw err;
// Table Users : On stocke la CLÉ DU COFFRE (Vault Key) protégée par le mot de passe
db.run(`CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT UNIQUE,
auth_hash TEXT, -- Hash pour vérifier le login (rapide)
vault_key_salt TEXT, -- Sel pour dériver la clé
encrypted_vault_key TEXT, -- La clé AES de l'user, chiffrée par son pass
key_iv TEXT,
key_tag TEXT
)`);
// Table Entries : Le contenu est chiffré par la Vault Key
db.run(`CREATE TABLE IF NOT EXISTS entries (
id TEXT PRIMARY KEY,
owner_id INTEGER,
title TEXT,
username_field TEXT,
url TEXT,
encrypted_blob TEXT,
iv TEXT,
auth_tag TEXT,
FOREIGN KEY(owner_id) REFERENCES users(id)
)`);
});
// --- MIDDLEWARE ---
// C'est lui qui récupère la clé de chiffrement des secrets dans le JWT
const authenticateAndUnpackKey = (req, res, next) => {
const token = req.cookies.auth_token;
if (!token) return res.sendStatus(401);
jwt.verify(token, JWT_SECRET, (err, decoded) => {
if (err) return res.sendStatus(403);
try {
// On récupère la clé de chiffrement de l'utilisateur stockée dans le token
// Elle est chiffrée avec la clé globale du serveur (ServerMasterKey), donc on la déchiffre.
const userVaultKeyHex = decryptAES(
decoded.encryptedKey,
SERVER_MASTER_KEY,
decoded.iv,
decoded.tag
);
// On attache la clé (Buffer) à la requête pour les routes suivantes
req.user = { ...decoded, vaultKey: Buffer.from(userVaultKeyHex, 'hex') };
next();
} catch (e) {
return res.status(403).json({error: "Token invalide ou corruption serveur"});
}
});
};
// --- ROUTES ---
// 1. Inscription
app.post('/register', async (req, res) => {
const { email, password } = req.body;
try {
// A. Login Auth Hash
const authHash = await argon2.hash(password);
// B. Création du "Trousseau" (Vault Key)
const userVaultKey = crypto.randomBytes(32); // La clé réelle qui chiffrera les données
const vaultSalt = crypto.randomBytes(16).toString('hex');
// C. On chiffre ce trousseau avec le mot de passe de l'user
const keyDerivedFromPass = await deriveKeyFromPass(password, vaultSalt);
const encryptedVault = encryptAES(userVaultKey.toString('hex'), keyDerivedFromPass);
db.run(`INSERT INTO users (email, auth_hash, vault_key_salt, encrypted_vault_key, key_iv, key_tag) VALUES (?,?,?,?,?,?)`,
[email, authHash, vaultSalt, encryptedVault.content, encryptedVault.iv, encryptedVault.tag],
function(err) {
if (err) return res.status(400).json({ error: "Erreur création" });
res.json({ message: "Compte créé avec succès" });
}
);
} catch (e) { res.status(500).json({ error: e.message }); }
});
// 2. Login (C'est ici qu'on prépare le JWT "chargé")
app.post('/login', (req, res) => {
const { email, password } = req.body;
db.get("SELECT id, email, encrypted_vault_key, vault_key_salt, key_iv, key_tag, auth_hash FROM users WHERE email = ?", [email], async (err, user) => {
if (!user || !await argon2.verify(user.auth_hash, password)) {
return res.status(401).json({ error: "Creds invalides" });
}
try {
// A. On recrée la clé qui protège le trousseau
const keyDerivedFromPass = await deriveKeyFromPass(password, user.vault_key_salt);
// B. On déchiffre le trousseau (Vault Key)
// Si ça marche, c'est que le mot de passe est bon ET qu'on a accès aux données
const userVaultKeyHex = decryptAES(user.encrypted_vault_key, keyDerivedFromPass, user.key_iv, user.key_tag);
// C. On met ce trousseau DANS le JWT, mais chiffré par le SERVEUR
// (Pour que le client ne puisse pas le voir, et pour ne pas le stocker en session serveur)
const tokenPayloadEncrypted = encryptAES(userVaultKeyHex, SERVER_MASTER_KEY);
const token = jwt.sign({
id: user.id,
email: user.email,
// On injecte le payload chiffré
encryptedKey: tokenPayloadEncrypted.content,
iv: tokenPayloadEncrypted.iv,
tag: tokenPayloadEncrypted.tag
}, JWT_SECRET, { expiresIn: '1h' });
// res.json({ token }); // <-- Avant, on envoyait le token dans le corps de la réponse, maintenant on l'envoie en cookie sécurisé (protection XSS/CSRF), cf ci-dessous
res.cookie('auth_token', token, {
httpOnly: true, // JS ne peut pas le lire (Protection XSS, CSRF)
secure: false, // Mettre à 'true' si vous êtes en HTTPS
sameSite: 'lax', // Protection CSRF
maxAge: 3600000 // 1 heure (en ms)
});
res.json({ message: "Connexion réussie" }); // On n'envoie plus le token dans le corps !
} catch (e) {
res.status(500).json({ error: "Erreur crypto login" });
}
});
});
// 3. Créer une entrée
app.post('/entries', authenticateAndUnpackKey, (req, res) => {
const { title, username_field, url, password } = req.body;
// TODO
});
// 4. Déchiffrer une entrée
app.get('/entries/:id/password', authenticateAndUnpackKey, (req, res) => {
// TODO
});
// 5. Lister les entrées (Métadonnées uniquement)
app.get('/entries', authenticateAndUnpackKey, (req, res) => {
// TODO
});
// 6. Logout (supprime le cookie)
app.post('/logout', (req, res) => {
res.clearCookie('auth_token');
res.json({ message: "Déconnecté" });
});
app.listen(3000, () => console.log("Serveur Secure Token démarré"));