const crypto = require('crypto'); const db = require('./db'); const TOKEN_TTL_DAYS = 30; function parseJsonMaybe(value) { if (!value) { return null; } if (typeof value === 'object') { return value; } try { return JSON.parse(value); } catch (error) { return null; } } function sha256(value) { return crypto.createHash('sha256').update(String(value || '')).digest('hex'); } function randomToken(size) { return crypto.randomBytes(size).toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, ''); } function normalizeEmail(email) { return String(email || '').trim().toLowerCase(); } function hashPassword(password) { return new Promise(function(resolve, reject) { var salt = crypto.randomBytes(16).toString('base64'); crypto.scrypt(String(password), salt, 64, function(error, derivedKey) { if (error) { reject(error); return; } resolve('scrypt$' + salt + '$' + derivedKey.toString('base64')); }); }); } function verifyPassword(password, storedHash) { return new Promise(function(resolve, reject) { var parts = String(storedHash || '').split('$'); if (parts.length !== 3 || parts[0] !== 'scrypt') { resolve(false); return; } var salt = parts[1]; var expected = Buffer.from(parts[2], 'base64'); crypto.scrypt(String(password), salt, expected.length, function(error, derivedKey) { if (error) { reject(error); return; } resolve(crypto.timingSafeEqual(expected, derivedKey)); }); }); } async function createAuthToken(userId, deviceFingerprint) { var rawToken = randomToken(32); var expiresAt = new Date(Date.now() + TOKEN_TTL_DAYS * 24 * 60 * 60 * 1000); await db.execute( 'INSERT INTO auth_tokens (user_id, token_hash, device_fingerprint, expires_at, last_seen_at) VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)', [userId, sha256(rawToken), deviceFingerprint || null, expiresAt] ); return rawToken; } async function touchAuthToken(tokenHash) { await db.execute('UPDATE auth_tokens SET last_seen_at = CURRENT_TIMESTAMP WHERE token_hash = ?', [tokenHash]); } async function revokeAuthToken(tokenHash) { await db.execute('UPDATE auth_tokens SET revoked_at = CURRENT_TIMESTAMP WHERE token_hash = ? AND revoked_at IS NULL', [tokenHash]); } async function getAuthContextFromToken(rawToken) { if (!rawToken) { return null; } var tokenHash = sha256(rawToken); var rows = await db.query( 'SELECT t.id AS token_id, t.user_id, t.token_hash, t.device_fingerprint, t.expires_at, t.revoked_at, u.email, u.status, k.wrapped_dek, k.kdf_salt, k.kdf_params, k.key_version FROM auth_tokens t INNER JOIN users u ON u.id = t.user_id LEFT JOIN user_keyrings k ON k.user_id = u.id WHERE t.token_hash = ? LIMIT 1', [tokenHash] ); if (!rows.length) { return null; } var row = rows[0]; if (row.revoked_at) { return null; } if (new Date(row.expires_at).getTime() <= Date.now()) { return null; } if (Number(row.status || 0) !== 1) { return null; } await touchAuthToken(tokenHash); return { tokenHash: tokenHash, tokenId: row.token_id, user: { id: row.user_id, email: row.email }, keyring: row.wrapped_dek ? { wrappedDek: row.wrapped_dek, kdfSalt: row.kdf_salt, kdfParams: parseJsonMaybe(row.kdf_params), keyVersion: Number(row.key_version || 1) } : null }; } async function requireAuth(request, reply) { var header = request.headers.authorization || ''; var match = String(header).match(/^Bearer\s+(.+)$/i); if (!match) { reply.code(401); throw new Error('未登录'); } var authContext = await getAuthContextFromToken(match[1]); if (!authContext) { reply.code(401); throw new Error('登录已失效'); } request.authContext = authContext; } module.exports = { createAuthToken, getAuthContextFromToken, hashPassword, normalizeEmail, parseJsonMaybe, requireAuth, revokeAuthToken, sha256, verifyPassword };