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é"));