import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto"; import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { join } from "node:path"; export function loadOrCreateKey(dataDir: string): Buffer { mkdirSync(dataDir, { recursive: true }); const path = join(dataDir, "secret.key"); if (!existsSync(path)) writeFileSync(path, randomBytes(32), { mode: 0o600 }); const key = readFileSync(path); if (key.length !== 32) throw new Error(`secret.key must be 32 bytes, got ${key.length}`); return key; } export function encrypt(key: Buffer, plaintext: string): string { const iv = randomBytes(12); const cipher = createCipheriv("aes-256-gcm", key, iv); const ct = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]); return "enc:" + Buffer.concat([iv, cipher.getAuthTag(), ct]).toString("base64"); } export function decrypt(key: Buffer, sealed: string): string { if (!sealed.startsWith("enc:")) throw new Error("not an encrypted value"); const buf = Buffer.from(sealed.slice(4), "base64"); const decipher = createDecipheriv("aes-256-gcm", key, buf.subarray(0, 12)); decipher.setAuthTag(buf.subarray(12, 28)); return Buffer.concat([decipher.update(buf.subarray(28)), decipher.final()]).toString("utf8"); } export function mask(secret: string): string { return secret.length >= 5 ? "…" + secret.slice(-4) : "…"; }