82 lines
2.6 KiB
TypeScript
82 lines
2.6 KiB
TypeScript
/**
|
|
* 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<string, unknown>, 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<T = Record<string, unknown>>(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;
|
|
}
|
|
}
|