244 lines
9.2 KiB
JavaScript
244 lines
9.2 KiB
JavaScript
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;
|
|
|
|
// On utilise req.user.vaultKey qui a été extrait du JWT
|
|
const encrypted = encryptAES(password, req.user.vaultKey);
|
|
|
|
// Stockage en BDD
|
|
const id = uuidv4();
|
|
db.run(`INSERT INTO entries (id, owner_id, title, username_field, url, encrypted_blob, iv, auth_tag)
|
|
VALUES (?,?,?,?,?,?,?,?)`,
|
|
[id, req.user.id, title, username_field, url, encrypted.content, encrypted.iv, encrypted.tag],
|
|
(err) => {
|
|
if (err) return res.status(500).json({ error: err.message });
|
|
res.json({ id, message: "Sauvegardé" });
|
|
}
|
|
);
|
|
});
|
|
|
|
// 4. Déchiffrer une entrée
|
|
app.get('/entries/:id/password', authenticateAndUnpackKey, (req, res) => {
|
|
db.get("SELECT encrypted_blob, iv, auth_tag FROM entries WHERE id = ? AND owner_id = ?",
|
|
[req.params.id, req.user.id],
|
|
(err, entry) => {
|
|
if (!entry) return res.status(404).send();
|
|
|
|
try {
|
|
// Déchiffrement immédiat avec la clé du token
|
|
const clear = decryptAES(entry.encrypted_blob, req.user.vaultKey, entry.iv, entry.auth_tag);
|
|
res.json({ password: clear });
|
|
} catch (e) {
|
|
res.status(500).json({ error: "Erreur déchiffrement" });
|
|
}
|
|
}
|
|
);
|
|
});
|
|
|
|
// 5. Lister les entrées (Métadonnées uniquement)
|
|
app.get('/entries', authenticateAndUnpackKey, (req, res) => {
|
|
// Notez qu'on ne sélectionne PAS 'encrypted_blob'.
|
|
// Cette requête est très rapide et ne demande aucune opération crypto.
|
|
const sql = "SELECT id, title, url, username_field FROM entries WHERE owner_id = ?";
|
|
|
|
db.all(sql, [req.user.id], (err, rows) => {
|
|
if (err) {
|
|
return res.status(500).json({ error: "Erreur lors de la récupération" });
|
|
}
|
|
// SQLite renvoie un tableau vide [] s'il n'y a pas de résultats, ce qui est parfait pour le JSON.
|
|
res.json(rows);
|
|
});
|
|
});
|
|
|
|
// 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é")); |