Files
basket_utils/server/security.js
T

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_DAYS = 7;
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_DAYS * 24 * 60 * 60 * 1000).toISOString();
}
export function validEmail(email) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}