import { createCipheriv, createDecipheriv, createHash, randomBytes } from 'node:crypto';
import { getKey } from './secret-store';

// AES-256-GCM at-rest encryption for every secret column in Postgres. The property this exists to hold is
// that a database dump must not hand over the credentials in it, so these columns are never plaintext.
//
// Format = base64(iv[12] | authTag[16] | ciphertext).
//
// ── One key per purpose ──
//
// This took a single VAULT_STORE_KEY from the environment until 2026-08-13. That key encrypted seven
// unrelated things — the Headscale admin credential, the wallet seed, Vaultwarden's token set, Jellyfin,
// Immich, InvoiceShelf, and every app-store upstream secret — so one leak opened all of them, and its
// name pointed at whichever plugin happened to need it first.
//
// `purpose` is now the first argument everywhere, and the caller passes the one that owns the data. A
// plugin's key is created on first use and cannot decrypt another plugin's column, because the AES key
// derives from a different stored secret. See ./secret-store.ts and docs/secret-store.md.
//
// SHA-256 over the stored key rather than using its bytes directly, so the store is free to change how it
// represents a key without every ciphertext in the database becoming unreadable.

const cache = new Map<string, Buffer>();

function key(purpose: string): Buffer {
  const hit = cache.get(purpose);
  if (hit) return hit;
  const derived = createHash('sha256').update(getKey(purpose)).digest();
  cache.set(purpose, derived);
  return derived;
}

/** Encrypt a UTF-8 secret for at-rest storage → base64(iv|tag|ciphertext). */
export function encryptSecret(purpose: string, plaintext: string): string {
  const iv = randomBytes(12);
  const cipher = createCipheriv('aes-256-gcm', key(purpose), iv);
  const ct = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
  const tag = cipher.getAuthTag();
  return Buffer.concat([iv, tag, ct]).toString('base64');
}

/** Decrypt a value produced by encryptSecret under the SAME purpose. Throws if it does not verify. */
export function decryptSecret(purpose: string, blob: string): string {
  const buf = Buffer.from(blob, 'base64');
  const iv = buf.subarray(0, 12);
  const tag = buf.subarray(12, 28);
  const ct = buf.subarray(28);
  const decipher = createDecipheriv('aes-256-gcm', key(purpose), iv);
  decipher.setAuthTag(tag);
  return Buffer.concat([decipher.update(ct), decipher.final()]).toString('utf8');
}
