import { createHash } from 'node:crypto'; import { Injectable } from '@nestjs/common'; import { assertLogUserIdSalt } from '../config/check-log-user-id-salt'; /** * Pseudonymises a user id (Entra `oid`) into the 16-hex-char hash * that both ADR-0013 (`audit_events.actor_id_hash`) and ADR-0012 * (Pino `user_id_hash`) store. The salt is read once at injection * time so the per-request hot path runs purely on `node:crypto`. * * Same salt + same input ⇒ same output, on both writers, across * application restarts within a given environment. That stability * is the *whole point*: an investigator joins audit rows with app * log lines on this hash, then re-hydrates the join key to the * cleartext user id only inside the live operational DB. * * Rotating the salt invalidates the join key for everything that * was written before the rotation. Treat as long-lived per * environment. * * Hash construction: * sha256(`${salt}:${userId}`).hex.slice(0, 16) * * SHA-256 is overkill for collision resistance at 16 hex chars * (64 bits of output), but truncation is intentional — log lines * and audit rows want a compact identifier, and 64 bits of * randomness is well above the birthday bound for any realistic * user count. The salt prevents rainbow attacks even on the * truncated form. */ @Injectable() export class HashUserIdService { private readonly salt: string; constructor() { this.salt = assertLogUserIdSalt(); } hash(userId: string): string { return createHash('sha256').update(`${this.salt}:${userId}`).digest('hex').slice(0, 16); } }