const argon2 = require('argon2'); const jwt = require('jsonwebtoken'); const fs = require('fs'); const path = require('path'); const crypto = require('crypto'); // ─── Load RSA keys ──────────────────────────────────── function loadKeys() { const privatePath = process.env.JWT_PRIVATE_KEY_PATH || path.join(__dirname, 'private.pem'); const publicPath = process.env.JWT_PUBLIC_KEY_PATH || path.join(__dirname, 'public.pem'); return { privateKey: fs.readFileSync(privatePath, 'utf8'), publicKey: fs.readFileSync(publicPath, 'utf8') }; } // ─── Argon2id (with client-side SHA-256 for HTTP transport) ── /** * Hash a raw password for storage. * Step 1: SHA-256(raw) → 64-char hex * Step 2: Argon2id(sha256_hex) → stored in DB * * This way the client sends SHA-256(password) over HTTP, * and the server never sees or stores the raw password. */ async function hashPassword(password) { const sha256 = crypto.createHash('sha256').update(password).digest('hex'); return argon2.hash(sha256, { type: argon2.argon2id, memoryCost: 65536, // 64 MB timeCost: 3, // 3 iterations parallelism: 4 }); } /** * Verify a client-submitted password hash against stored Argon2id hash. * @param {string} storedHash — Argon2id(SHA-256(raw_password)) * @param {string} receivedSha256 — SHA-256(raw_password) from client (64-char hex) */ async function verifyPassword(storedHash, receivedSha256) { return argon2.verify(storedHash, receivedSha256); } // ─── JWT ────────────────────────────────────────────── function signAccessToken(user, deviceId) { const { privateKey } = loadKeys(); return jwt.sign( { sub: user.id, username: user.username, role: user.role, device_id: deviceId }, privateKey, { algorithm: 'RS256', expiresIn: '30m' } ); } function verifyAccessToken(token) { const { publicKey } = loadKeys(); return jwt.verify(token, publicKey, { algorithms: ['RS256'] }); } // ─── Refresh Token ──────────────────────────────────── function generateRefreshToken() { return crypto.randomBytes(32).toString('hex'); // 256-bit → 64 hex chars } function hashToken(token) { return crypto.createHash('sha256').update(token).digest('hex'); } // ─── HWID Matching ──────────────────────────────────── /** * Dynamic HWID matching. * Rule 1: CPU + Board both match → immediate pass (core hardware). * Rule 2: Weighted score ≥ 80%. * CPU: 45%, Board: 45%, Disk: 8%, MAC: 2% */ function hwidMatchRate(device, incoming) { const cpuMatch = device.cpu_hash === incoming.cpu; const boardMatch = device.board_hash === incoming.board; const diskMatch = device.disk_hash === incoming.disk; const macMatch = device.mac_hash === incoming.mac; // Rule 1: CPU + Board if (cpuMatch && boardMatch) return { match: true, rate: 0.80 }; // Rule 2: weighted let score = 0; if (cpuMatch) score += 0.45; if (boardMatch) score += 0.45; if (diskMatch) score += 0.08; if (macMatch) score += 0.02; return { match: score >= 0.80, rate: score }; } /** * Compute a combined HWID hash for quick lookup. */ function combinedHwidHash(components) { const combined = `${components.cpu}|${components.board}|${components.disk}|${components.mac}`; return crypto.createHash('sha256').update(combined).digest('hex'); } // ─── Admin RBAC ─────────────────────────────────────── function requireAdmin(req, res, next) { const authHeader = req.headers.authorization; if (!authHeader || !authHeader.startsWith('Bearer ')) { return res.status(401).json({ success: false, error: 'UNAUTHORIZED' }); } try { const token = authHeader.slice(7); const payload = verifyAccessToken(token); if (payload.role !== 'admin') { return res.status(403).json({ success: false, error: 'FORBIDDEN', message: 'Admin role required' }); } req.user = payload; next(); } catch (err) { return res.status(401).json({ success: false, error: 'TOKEN_INVALID' }); } } /** * Hash a pre-computed SHA-256 hex string (from client) with Argon2id. * Used for registration when the client sends SHA-256(password). */ async function hashPasswordFromHash(sha256hex) { return argon2.hash(sha256hex, { type: argon2.argon2id, memoryCost: 65536, timeCost: 3, parallelism: 4 }); } module.exports = { hashPassword, hashPasswordFromHash, verifyPassword, signAccessToken, verifyAccessToken, generateRefreshToken, hashToken, hwidMatchRate, combinedHwidHash, requireAdmin };