57 lines
1.8 KiB
TypeScript
57 lines
1.8 KiB
TypeScript
|
|
/**
|
|||
|
|
* 密码哈希与校验(仅用于 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
|
|||
|
|
}
|