import { createHash, createHmac, randomBytes, randomInt, timingSafeEqual, createCipheriv, createDecipheriv } from "node:crypto"; function secret(name: "OTP_HMAC_SECRET" | "SESSION_SECRET"): string { const v = process.env[name]; if (!v || v.length < 16) throw new Error(`${name} manquante ou trop courte`); return v; } /** Adresse normalisée : minuscules, espaces retirés ; points et « + » conservés (pas de suppression arbitraire). */ export function normalizeEmail(raw: string): string | null { const e = raw.trim().toLowerCase().replace(/\s+/g, ""); if (!/^[^@\s]{1,64}@[^@\s]+\.[a-z]{2,}$/i.test(e) || e.length > 254) return null; return e; } export function sha256(s: string): string { return createHash("sha256").update(s).digest("hex"); } export function ipHash(ip: string | null | undefined): string { return sha256(`ip:${ip ?? "?"}:${secret("SESSION_SECRET").slice(0, 8)}`).slice(0, 32); } export function emailHash(email: string): string { return sha256(`email:${email}`).slice(0, 32); } /** Code OTP à 6 chiffres par CSPRNG (randomInt est uniforme). */ export function generateOtp(): string { return String(randomInt(0, 1_000_000)).padStart(6, "0"); } /** Empreinte HMAC-SHA256 du code, liée à l'identifiant du défi (évite les tables arc-en-ciel sur 10^6 codes). */ export function otpDigest(code: string, challengeId: string): string { return createHmac("sha256", secret("OTP_HMAC_SECRET")).update(`${challengeId}:${code}`).digest("hex"); } export function safeEqualHex(a: string, b: string): boolean { const ba = Buffer.from(a, "hex"), bb = Buffer.from(b, "hex"); return ba.length === bb.length && timingSafeEqual(ba, bb); } export function newSessionToken(): string { return randomBytes(32).toString("base64url"); } export function sessionDigest(token: string): string { return createHmac("sha256", secret("SESSION_SECRET")).update(token).digest("hex"); } /** Chiffrement symétrique (AES-256-GCM) pour champs sensibles (téléphone, secret TOTP) — clé dérivée de SESSION_SECRET. */ function encKey(): Buffer { return createHash("sha256").update(`enc:${secret("SESSION_SECRET")}`).digest(); } export function encrypt(plain: string): string { const iv = randomBytes(12); const c = createCipheriv("aes-256-gcm", encKey(), iv); const ct = Buffer.concat([c.update(plain, "utf8"), c.final()]); return `${iv.toString("base64url")}.${ct.toString("base64url")}.${c.getAuthTag().toString("base64url")}`; } export function decrypt(blob: string): string { const [iv, ct, tag] = blob.split(".").map((x) => Buffer.from(x, "base64url")); const d = createDecipheriv("aes-256-gcm", encKey(), iv); d.setAuthTag(tag); return Buffer.concat([d.update(ct), d.final()]).toString("utf8"); }