Add native host management and role-based operator authentication
Introduce an outbound Python host agent, Debian installer, durable remote operations, immutable game-file libraries, host-aware deployments and ports, and occupancy-safe campaigns and draining. Native hosts do not require Komodo. Add administrator, operator and viewer accounts, protected management routes, secure sessions, password recovery, audit history and separate telemetry tokens. Document first-admin setup, reporter migration and live-host acceptance checks. Validation: 1,343 TypeScript tests and 39 agent tests passed, along with type checking, the production build, compiled SSR/API smoke tests and administrator recovery. Real Debian/Docker T5/T6 acceptance remains unrun.
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
import { randomBytes, scrypt, timingSafeEqual } from 'node:crypto';
|
||||
|
||||
// OWASP's scrypt baseline: 128 MiB, r=8, p=1. Bound concurrent work so
|
||||
// unauthenticated password attempts cannot allocate unlimited memory.
|
||||
const parameters = { N: 131072, r: 8, p: 1, maxmem: 256 * 1024 * 1024 };
|
||||
let active = 0;
|
||||
|
||||
/** @param {unknown} password */
|
||||
export function validatePassword(password) {
|
||||
if (typeof password !== 'string' || [...password].length < 15 || [...password].length > 128) {
|
||||
throw new Error('Use a password or passphrase between 15 and 128 characters.');
|
||||
}
|
||||
return password;
|
||||
}
|
||||
|
||||
/** @param {string} password @param {string} salt @returns {Promise<Buffer>} */
|
||||
function derive(password, salt) {
|
||||
if (active >= 2) return Promise.reject(new Error('Password service is busy. Try again shortly.'));
|
||||
active++;
|
||||
return new Promise((resolve, reject) => {
|
||||
scrypt(password, salt, 64, parameters, (error, result) => {
|
||||
active--;
|
||||
if (error) reject(error); else resolve(result);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** @param {unknown} password */
|
||||
export async function hashPassword(password) {
|
||||
const validated = validatePassword(password);
|
||||
const salt = randomBytes(32).toString('hex');
|
||||
const key = await derive(validated, salt);
|
||||
return `scrypt$131072$8$1$${salt}$${key.toString('hex')}`;
|
||||
}
|
||||
|
||||
/** @param {unknown} password @param {string | null} encoded */
|
||||
export async function verifyPassword(password, encoded) {
|
||||
if (typeof password !== 'string' || [...password].length > 128) return false;
|
||||
const parts = encoded?.split('$');
|
||||
const valid = parts?.length === 6 && parts[0] === 'scrypt' && parts[1] === '131072' && parts[2] === '8' && parts[3] === '1'
|
||||
&& /^[a-f0-9]{64}$/.test(parts[4]) && /^[a-f0-9]{128}$/.test(parts[5]);
|
||||
// Missing accounts perform the same expensive operation as real accounts.
|
||||
const result = await derive(password, valid ? parts[4] : '0'.repeat(64));
|
||||
return timingSafeEqual(result, Buffer.from(valid ? parts[5] : '0'.repeat(128), 'hex')) && !!valid;
|
||||
}
|
||||
Reference in New Issue
Block a user