/** * Self-contained crypto helpers — no native deps. * - Password hashing via scrypt (built-in). * - HS256 JWTs for dashboard sessions. * - Random license keys / ids. */ import { randomBytes, randomUUID, scryptSync, timingSafeEqual, createHmac, } from 'node:crypto'; import { config } from '../config.js'; // ---- password hashing (scrypt) ---- export function hashPassword(password: string): string { const salt = randomBytes(16); const hash = scryptSync(password, salt, 64); return `scrypt$${salt.toString('hex')}$${hash.toString('hex')}`; } export function verifyPassword(password: string, stored: string): boolean { const parts = stored.split('$'); if (parts.length !== 3 || parts[0] !== 'scrypt') return false; const salt = Buffer.from(parts[1]!, 'hex'); const expected = Buffer.from(parts[2]!, 'hex'); const actual = scryptSync(password, salt, expected.length); return actual.length === expected.length && timingSafeEqual(actual, expected); } // ---- ids / keys ---- export function newId(): string { return randomUUID(); } /** License key like "wpide_live_<32hex>". */ export function newLicenseKey(): string { return `wpide_live_${randomBytes(16).toString('hex')}`; } /** Opaque random token (oauth state, etc.). */ export function randomToken(bytes = 24): string { return randomBytes(bytes).toString('base64url'); } // ---- minimal HS256 JWT ---- function b64url(input: Buffer | string): string { return Buffer.from(input).toString('base64url'); } export function signJwt(payload: Record, ttlSeconds = 60 * 60 * 24 * 30): string { const header = { alg: 'HS256', typ: 'JWT' }; const now = Math.floor(Date.now() / 1000); const body = { ...payload, iat: now, exp: now + ttlSeconds }; const head = b64url(JSON.stringify(header)); const data = b64url(JSON.stringify(body)); const sig = createHmac('sha256', config.JWT_SECRET).update(`${head}.${data}`).digest('base64url'); return `${head}.${data}.${sig}`; } export function verifyJwt>(token: string): T | null { const parts = token.split('.'); if (parts.length !== 3) return null; const [head, data, sig] = parts as [string, string, string]; const expected = createHmac('sha256', config.JWT_SECRET).update(`${head}.${data}`).digest('base64url'); const a = Buffer.from(sig); const b = Buffer.from(expected); if (a.length !== b.length || !timingSafeEqual(a, b)) return null; try { const payload = JSON.parse(Buffer.from(data, 'base64url').toString('utf8')) as { exp?: number }; if (payload.exp && payload.exp < Math.floor(Date.now() / 1000)) return null; return payload as T; } catch { return null; } }