feat: chiffrement AES-256-GCM des secrets + lecture env
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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;
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user