The repository carried two different servers side by side - the early
`justrp` prototype with its Python auth API, and a snapshot of the real
one under `server-code/`. Only the second one is a server anyone should
start from, so the prototype is gone and the real one moved to the root.
Taken from the live box, so the UI is the finished version (the tokens,
the county-records sheets and the variable fonts landed after the last
snapshot was pushed):
resources/[rp]/ rp_db, rp_core, rp_session, rp_loading, rp_ui,
rp_selftest, rp_dbtest
bin/ supervise.sh (keep-alive + console FIFO), rcon, init script
etc/schema.sql accounts, characters, transactions, inventory, vehicles
assets/fonts/ the bundled subsets
docs/SERVER.md how it is put together, resource by resource
install.sh turns a clean Ubuntu/Debian box into this server in one
command: recommended FXServer build, MariaDB with the schema, resources,
server.cfg + a generated database password, boot entry (systemd or
init.d), then it waits for the Cfx registration. --name/--port/--db-name
let a second server live on the same machine. Tested end to end on a
spare install root: build 25770 fetched, schema applied, boot entry
written, FXServer started and stopped exactly where a wrong licence key
should stop it.
No secrets travel with it: the licence key and the database password live
in data/secrets.cfg on the machine, and .gitignore now names it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
63 lines
2.4 KiB
JavaScript
63 lines
2.4 KiB
JavaScript
// ---------------------------------------------------------------------------
|
|
// Password hashing for rp_auth.
|
|
//
|
|
// scrypt with per-password random salt. The cost parameters are stored inside
|
|
// the hash string, so they can be raised later without invalidating existing
|
|
// accounts - verify() always uses the parameters the hash was made with.
|
|
//
|
|
// Format: scrypt$N$r$p$<salt b64>$<derived key b64>
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const crypto = require('crypto');
|
|
|
|
const N = 16384; // CPU/memory cost
|
|
const R = 8; // block size
|
|
const P = 1; // parallelisation
|
|
const KEYLEN = 64;
|
|
const MAXMEM = 96 * 1024 * 1024; // scrypt needs ~128*N*r bytes = 16MB here
|
|
|
|
function derive(password, salt, n, r, p, cb) {
|
|
crypto.scrypt(password, salt, KEYLEN, { N: n, r, p, maxmem: MAXMEM }, cb);
|
|
}
|
|
|
|
global.exports('hashPassword', (password, cb) => {
|
|
if (typeof password !== 'string' || password.length === 0 || password.length > 200) {
|
|
return cb(null, 'invalid password');
|
|
}
|
|
const salt = crypto.randomBytes(16);
|
|
derive(password, salt, N, R, P, (err, dk) => {
|
|
if (err) return cb(null, err.message);
|
|
cb(`scrypt$${N}$${R}$${P}$${salt.toString('base64')}$${dk.toString('base64')}`, null);
|
|
});
|
|
});
|
|
|
|
global.exports('verifyPassword', (password, stored, cb) => {
|
|
if (typeof password !== 'string' || typeof stored !== 'string') return cb(false, null);
|
|
|
|
const parts = stored.split('$');
|
|
if (parts.length !== 6 || parts[0] !== 'scrypt') return cb(false, 'unrecognised hash format');
|
|
|
|
const n = parseInt(parts[1], 10);
|
|
const r = parseInt(parts[2], 10);
|
|
const p = parseInt(parts[3], 10);
|
|
if (!n || !r || !p) return cb(false, 'corrupt hash parameters');
|
|
|
|
let salt, expected;
|
|
try {
|
|
salt = Buffer.from(parts[4], 'base64');
|
|
expected = Buffer.from(parts[5], 'base64');
|
|
} catch (e) {
|
|
return cb(false, 'corrupt hash encoding');
|
|
}
|
|
|
|
derive(password, salt, n, r, p, (err, dk) => {
|
|
if (err) return cb(false, err.message);
|
|
// constant time: a wrong password must not be distinguishable by timing
|
|
const ok = dk.length === expected.length && crypto.timingSafeEqual(dk, expected);
|
|
cb(ok, null);
|
|
});
|
|
});
|
|
|
|
// Opaque tokens (character session handles, drop ids, ...)
|
|
global.exports('randomToken', (bytes) => crypto.randomBytes(Math.min(Math.max(bytes || 16, 8), 64)).toString('hex'));
|