// Auth utilities — password hashing (scrypt), tokens, and TOTP MFA. // Uses only Node's built-in crypto, no external auth deps. const crypto = require('crypto'); // ---- Passwords (scrypt) ---- function hashPassword(password, salt = crypto.randomBytes(16).toString('hex')) { const hash = crypto.scryptSync(password, salt, 64).toString('hex'); return { hash, salt }; } function verifyPassword(password, salt, expectedHash) { const hash = crypto.scryptSync(password, salt, 64).toString('hex'); return crypto.timingSafeEqual(Buffer.from(hash), Buffer.from(expectedHash)); } // ---- Random tokens ---- const token = (bytes = 24) => crypto.randomBytes(bytes).toString('hex'); const id = () => crypto.randomBytes(8).toString('hex'); const numericCode = (digits = 6) => String(crypto.randomInt(0, 10 ** digits)).padStart(digits, '0'); // ---- TOTP (RFC 6238), SHA-1, 30s, 6 digits ---- const B32 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; function base32Encode(buf) { let bits = 0, value = 0, out = ''; for (const byte of buf) { value = (value << 8) | byte; bits += 8; while (bits >= 5) { out += B32[(value >>> (bits - 5)) & 31]; bits -= 5; } } if (bits > 0) out += B32[(value << (5 - bits)) & 31]; return out; } function base32Decode(str) { const clean = str.replace(/=+$/, '').toUpperCase(); let bits = 0, value = 0; const out = []; for (const ch of clean) { const idx = B32.indexOf(ch); if (idx === -1) continue; value = (value << 5) | idx; bits += 5; if (bits >= 8) { out.push((value >>> (bits - 8)) & 0xff); bits -= 8; } } return Buffer.from(out); } function newMfaSecret() { return base32Encode(crypto.randomBytes(20)); } function totp(secret, timeStep = Math.floor(Date.now() / 30000)) { const key = base32Decode(secret); const buf = Buffer.alloc(8); buf.writeBigUInt64BE(BigInt(timeStep)); const hmac = crypto.createHmac('sha1', key).update(buf).digest(); const offset = hmac[hmac.length - 1] & 0xf; const code = ((hmac[offset] & 0x7f) << 24) | ((hmac[offset + 1] & 0xff) << 16) | ((hmac[offset + 2] & 0xff) << 8) | (hmac[offset + 3] & 0xff); return String(code % 1_000_000).padStart(6, '0'); } function verifyTotp(secret, code, window = 1) { if (!/^\d{6}$/.test(String(code || ''))) return false; const step = Math.floor(Date.now() / 30000); for (let i = -window; i <= window; i++) { if (totp(secret, step + i) === String(code)) return true; } return false; } function otpauthUrl(secret, email, issuer = 'RemoteAccess') { return `otpauth://totp/${encodeURIComponent(issuer)}:${encodeURIComponent(email)}` + `?secret=${secret}&issuer=${encodeURIComponent(issuer)}&algorithm=SHA1&digits=6&period=30`; } module.exports = { hashPassword, verifyPassword, token, id, numericCode, newMfaSecret, totp, verifyTotp, otpauthUrl, };