import { createHmac } from "node:crypto"; /** * Produces a stable, irreversible token for a GDPR-erased actorId. * * Format: `erased-` — * 128 bits of a KEYED digest (audit finding A13). The previous scheme was an * unkeyed `sha256("salt:actorId")` truncated to 64 bits, which invited both * brute-force reversal of small id spaces and birthday collisions. * * The HMAC key is read from `AUDIT_PSEUDONYM_SALT` at call time so that * production binding can pre-validate the var at boot (see `bindAudit`) * while tests can override it per-test via `process.env`. * * Rotation expectations (documented, by design): * - Rotating the key changes pseudonyms produced FROM THEN ON only. Audit * rows already pseudonymized keep tokens derived from the previous key; * nothing re-keys stored rows, so a subject's pre- and post-rotation * tokens no longer correlate. That linkage break is acceptable — the * token's only job is severing PII linkage, not long-term correlation. * - Re-erasing a subject after rotation still works: erasure matches rows * by the REAL actorId, not by a previous pseudonym. * - Rotate by replacing the env value (e.g. `openssl rand -hex 32`); keep * retired keys only if you have an explicit need to re-correlate old rows. * * Fallback key is intentionally weak and labelled so that any token * produced with it is recognisable as a dev/test artefact. */ export function pseudonymize(actorId: string): string { const key = process.env["AUDIT_PSEUDONYM_SALT"] ?? "dev-fallback-salt-replace-in-prod"; const digest = createHmac("sha256", key).update(actorId).digest("hex"); return `erased-${digest.slice(0, 32)}`; }