// ---------------------------------------------------------------------------
//  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'));
