SPB Git forge

spb/toveli

Public
14commits 1branches 0releases
1.1 MBsize
maindefault branch
3 h agolast push
TypeScript 50.6% JavaScript 46.1% SQL 2.1% Shell 0.6% CSS 0.5%
2.6 KB · 29 lines typescript
Raw Blame History
1import { createHash, createHmac, randomBytes, randomInt, timingSafeEqual, createCipheriv, createDecipheriv } from "node:crypto";23function secret(name: "OTP_HMAC_SECRET" | "SESSION_SECRET"): string {4  const v = process.env[name]; if (!v || v.length < 16) throw new Error(`${name} manquante ou trop courte`); return v;5}6/** Adresse normalisée : minuscules, espaces retirés ; points et « + » conservés (pas de suppression arbitraire). */7export function normalizeEmail(raw: string): string | null {8  const e = raw.trim().toLowerCase().replace(/\s+/g, "");9  if (!/^[^@\s]{1,64}@[^@\s]+\.[a-z]{2,}$/i.test(e) || e.length > 254) return null;10  return e;11}12export function sha256(s: string): string { return createHash("sha256").update(s).digest("hex"); }13export function ipHash(ip: string | null | undefined): string { return sha256(`ip:${ip ?? "?"}:${secret("SESSION_SECRET").slice(0, 8)}`).slice(0, 32); }14export function emailHash(email: string): string { return sha256(`email:${email}`).slice(0, 32); }1516/** Code OTP à 6 chiffres par CSPRNG (randomInt est uniforme). */17export function generateOtp(): string { return String(randomInt(0, 1_000_000)).padStart(6, "0"); }18/** Empreinte HMAC-SHA256 du code, liée à l'identifiant du défi (évite les tables arc-en-ciel sur 10^6 codes). */19export function otpDigest(code: string, challengeId: string): string { return createHmac("sha256", secret("OTP_HMAC_SECRET")).update(`${challengeId}:${code}`).digest("hex"); }20export 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); }2122export function newSessionToken(): string { return randomBytes(32).toString("base64url"); }23export function sessionDigest(token: string): string { return createHmac("sha256", secret("SESSION_SECRET")).update(token).digest("hex"); }2425/** Chiffrement symétrique (AES-256-GCM) pour champs sensibles (téléphone, secret TOTP) — clé dérivée de SESSION_SECRET. */26function encKey(): Buffer { return createHash("sha256").update(`enc:${secret("SESSION_SECRET")}`).digest(); }27export 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")}`; }28export 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"); }29