158 lines
4.9 KiB
JavaScript
158 lines
4.9 KiB
JavaScript
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
|
|
};
|