First commit
This commit is contained in:
208
server.js
Normal file
208
server.js
Normal 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é"));
|
||||
Reference in New Issue
Block a user