Initial import: live state on api.qbirr.com (server v0.6.3)
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user