// Password hashing and the route guard. // // Hashes are scrypt, from node's own crypto — no native build step, and the // stored form carries its own parameters so they can be raised later without // invalidating existing rows: // // scrypt$$$

$$ const crypto = require('crypto'); const SCRYPT = { N: 16384, r: 8, p: 1, keylen: 64 }; function hashPassword(password, params = SCRYPT) { const { N, r, p, keylen } = params; const salt = crypto.randomBytes(16); const dk = crypto.scryptSync(password, salt, keylen, { N, r, p, maxmem: 256 * 1024 * 1024 }); return `scrypt$${N}$${r}$${p}$${salt.toString('base64')}$${dk.toString('base64')}`; } // Constant-time compare. Returns false rather than throwing on a malformed or // legacy hash, so one bad row can't 500 the login route. function verifyPassword(password, stored) { if (typeof stored !== 'string') return false; const parts = stored.split('$'); if (parts.length !== 6 || parts[0] !== 'scrypt') return false; const [, N, r, p, saltB64, dkB64] = parts; try { const salt = Buffer.from(saltB64, 'base64'); const expected = Buffer.from(dkB64, 'base64'); const actual = crypto.scryptSync(password, salt, expected.length, { N: Number(N), r: Number(r), p: Number(p), maxmem: 256 * 1024 * 1024, }); return crypto.timingSafeEqual(actual, expected); } catch { return false; } } // Every /api route except the auth ones sits behind this. function requireAuth(req, res, next) { if (req.session?.user?.username) return next(); res.status(401).json({ error: 'Not authenticated' }); } // The identity used for pf.log.pf_user and the created_by/closed_by columns. // Read from the session only — never from the request body, which the browser // controls and which used to carry a hardcoded 'admin'. function sessionUser(req) { return req.session?.user?.username || null; } module.exports = { hashPassword, verifyPassword, requireAuth, sessionUser, SCRYPT }; // CLI: `node lib/auth.js hash` reads a password on stdin and prints its hash, // so ./pf.sh can create users without the plaintext touching argv or psql. if (require.main === module) { if (process.argv[2] !== 'hash') { console.error('usage: node lib/auth.js hash (password on stdin)'); process.exit(2); } let input = ''; process.stdin.setEncoding('utf8'); process.stdin.on('data', chunk => { input += chunk; }); process.stdin.on('end', () => { const password = input.replace(/\r?\n$/, ''); if (!password) { console.error('empty password'); process.exit(2); } process.stdout.write(hashPassword(password) + '\n'); }); }