Files
skeet_loader/server/auth.js
T
2026-06-08 15:51:52 +08:00

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
};