/** * 密码哈希与校验(仅用于 Web 用户注册/登录,与后台管理员密码无关) * 使用 Node crypto.scrypt,存储格式 saltHex:hashHex,兼容旧明文密码 */ import { scryptSync, timingSafeEqual, randomFillSync } from 'crypto' const SALT_LEN = 16 const KEYLEN = 32 function bufferToHex(buf: Buffer): string { return buf.toString('hex') } function hexToBuffer(hex: string): Buffer { return Buffer.from(hex, 'hex') } /** * 对明文密码做哈希,存入数据库 * 格式: saltHex:hashHex(约 97 字符,适配 VARCHAR(100)) * 与 verifyPassword 一致:内部先 trim,保证注册/登录/重置用同一套规则 */ export function hashPassword(plain: string): string { const trimmed = String(plain).trim() const salt = Buffer.allocUnsafe(SALT_LEN) randomFillSync(salt) const hash = scryptSync(trimmed, salt, KEYLEN, { N: 16384, r: 8, p: 1 }) return bufferToHex(salt) + ':' + bufferToHex(hash) } /** * 校验密码:支持新格式(salt:hash)与旧明文(兼容历史数据) * 与 hashPassword 一致:对输入先 trim 再参与校验 */ export function verifyPassword(plain: string, stored: string | null | undefined): boolean { const trimmed = String(plain).trim() if (stored == null || stored === '') { return trimmed === '' } if (stored.includes(':')) { const [saltHex, hashHex] = stored.split(':') if (!saltHex || !hashHex || saltHex.length !== SALT_LEN * 2 || hashHex.length !== KEYLEN * 2) { return false } try { const salt = hexToBuffer(saltHex) const expected = hexToBuffer(hashHex) const derived = scryptSync(trimmed, salt, KEYLEN, { N: 16384, r: 8, p: 1 }) return derived.length === expected.length && timingSafeEqual(derived, expected) } catch { return false } } return trimmed === stored }