49 lines
1.7 KiB
JavaScript
49 lines
1.7 KiB
JavaScript
import { createHash, randomBytes, scrypt, timingSafeEqual } from 'node:crypto';
|
|
import { promisify } from 'node:util';
|
|
|
|
const scryptAsync = promisify(scrypt);
|
|
const N = 16_384;
|
|
const R = 8;
|
|
const P = 1;
|
|
|
|
export const SESSION_COOKIE = 'court_session';
|
|
export const SESSION_SECONDS = 60 * 60;
|
|
|
|
export function hashToken(token) {
|
|
return createHash('sha256').update(token).digest('hex');
|
|
}
|
|
|
|
export async function hashPassword(password) {
|
|
const salt = randomBytes(16);
|
|
const derived = await scryptAsync(password, salt, 64, { N, r: R, p: P, maxmem: 32 * 1024 * 1024 });
|
|
return `scrypt$${N},${R},${P}$${salt.toString('base64url')}$${Buffer.from(derived).toString('base64url')}`;
|
|
}
|
|
|
|
export async function verifyPassword(password, encoded) {
|
|
try {
|
|
const [algorithm, params, saltText, hashText] = String(encoded).split('$');
|
|
if (algorithm !== 'scrypt' || !params || !saltText || !hashText) return false;
|
|
const [nText, rText, pText] = params.split(',');
|
|
const n = Number(nText); const r = Number(rText); const p = Number(pText);
|
|
if (n !== N || r !== R || p !== P) return false;
|
|
const expected = Buffer.from(hashText, 'base64url');
|
|
const derived = Buffer.from(await scryptAsync(password, Buffer.from(saltText, 'base64url'), expected.length, { N: n, r, p, maxmem: 32 * 1024 * 1024 }));
|
|
if (derived.length !== expected.length) return false;
|
|
return timingSafeEqual(derived, expected);
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
export function newSessionToken() {
|
|
return randomBytes(32).toString('base64url');
|
|
}
|
|
|
|
export function sessionExpiry(now = Date.now()) {
|
|
return new Date(now + SESSION_SECONDS * 1000).toISOString();
|
|
}
|
|
|
|
export function validEmail(email) {
|
|
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
|
|
}
|