diff --git a/server/crypto/secrets.test.ts b/server/crypto/secrets.test.ts new file mode 100644 index 0000000..c83b93c --- /dev/null +++ b/server/crypto/secrets.test.ts @@ -0,0 +1,23 @@ +// server/crypto/secrets.test.ts +import { describe, it, expect } from "vitest"; +import { encryptSecret, decryptSecret } from "./secrets.js"; + +const KEY = "a".repeat(64); // 32 octets en hex + +describe("secrets", () => { + it("round-trip encrypt/decrypt restitue le texte clair", () => { + const blob = encryptSecret("hunter2", KEY); + expect(blob).not.toContain("hunter2"); + expect(decryptSecret(blob, KEY)).toBe("hunter2"); + }); + + it("produit un blob différent à chaque chiffrement (IV aléatoire)", () => { + expect(encryptSecret("x", KEY)).not.toBe(encryptSecret("x", KEY)); + }); + + it("échoue si le blob a été altéré (tag GCM)", () => { + const blob = encryptSecret("secret", KEY); + const tampered = blob.slice(0, -2) + (blob.endsWith("a") ? "b" : "a"); + expect(() => decryptSecret(tampered, KEY)).toThrow(); + }); +}); diff --git a/server/crypto/secrets.ts b/server/crypto/secrets.ts new file mode 100644 index 0000000..f61098f --- /dev/null +++ b/server/crypto/secrets.ts @@ -0,0 +1,23 @@ +// server/crypto/secrets.ts +import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto"; + +const ALGO = "aes-256-gcm"; + +/** Chiffre une chaîne. Format de sortie: base64(iv).base64(tag).base64(ciphertext). */ +export function encryptSecret(plaintext: string, keyHex: string): string { + const key = Buffer.from(keyHex, "hex"); + const iv = randomBytes(12); + const cipher = createCipheriv(ALGO, key, iv); + const ct = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]); + const tag = cipher.getAuthTag(); + return [iv.toString("base64"), tag.toString("base64"), ct.toString("base64")].join("."); +} + +export function decryptSecret(blob: string, keyHex: string): string { + const key = Buffer.from(keyHex, "hex"); + const [ivB64, tagB64, ctB64] = blob.split("."); + if (!ivB64 || !tagB64 || !ctB64) throw new Error("Blob chiffré invalide"); + const decipher = createDecipheriv(ALGO, key, Buffer.from(ivB64, "base64")); + decipher.setAuthTag(Buffer.from(tagB64, "base64")); + return Buffer.concat([decipher.update(Buffer.from(ctB64, "base64")), decipher.final()]).toString("utf8"); +} diff --git a/server/env.ts b/server/env.ts new file mode 100644 index 0000000..945b6af --- /dev/null +++ b/server/env.ts @@ -0,0 +1,18 @@ +// server/env.ts +function required(name: string): string { + const v = process.env[name]; + if (!v) throw new Error(`Variable d'environnement manquante: ${name}`); + return v; +} + +export const env = { + masterKeyHex: process.env.SU_MASTER_KEY ?? "", + dbPath: process.env.SU_DB_PATH ?? "./data/system-update.db", + reportsDir: process.env.SU_REPORTS_DIR ?? "./reports", + port: Number(process.env.SU_PORT ?? 8787), + requireMasterKey(): string { + const k = required("SU_MASTER_KEY"); + if (k.length !== 64) throw new Error("SU_MASTER_KEY doit faire 64 caractères hex (32 octets)."); + return k; + }, +};