pseudonymize() used an unkeyed sha256 over 'salt:id' truncated to 64 bits, and the DSR deletion certificate hashed the raw subjectId with no salt at all (audit finding A13). Both now use HMAC-SHA256 keyed by AUDIT_PSEUDONYM_SALT, truncated to 128 bits. Rotation semantics are documented on pseudonymize(): a key rotation changes future pseudonyms only — stored rows keep old tokens and erasure still matches by real actorId — and the certificate change likewise affects new certificates only. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
35 lines
1.7 KiB
TypeScript
35 lines
1.7 KiB
TypeScript
import { createHmac } from "node:crypto";
|
|
|
|
/**
|
|
* Produces a stable, irreversible token for a GDPR-erased actorId.
|
|
*
|
|
* Format: `erased-<first-32-hex-chars-of-HMAC-SHA256(key, actorId)>` —
|
|
* 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)}`;
|
|
}
|