108 lines
2.8 KiB
JavaScript
108 lines
2.8 KiB
JavaScript
|
|
/**
|
|||
|
|
* 密码加密和验证工具函数
|
|||
|
|
* @module services/utils/password
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
const crypto = require('crypto')
|
|||
|
|
|
|||
|
|
const N = 16384
|
|||
|
|
const r = 8
|
|||
|
|
const p = 1
|
|||
|
|
const KEYLEN = 64
|
|||
|
|
const SALT_LEN = 16
|
|||
|
|
const MD5_LEN = 32
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 计算 MD5 哈希
|
|||
|
|
* @param {string} str - 输入字符串
|
|||
|
|
* @returns {string} MD5 哈希值(十六进制)
|
|||
|
|
*/
|
|||
|
|
function md5(str) {
|
|||
|
|
return crypto.createHash('md5').update(str).digest('hex')
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 使用 scrypt 计算哈希
|
|||
|
|
* @param {string} password - 密码
|
|||
|
|
* @param {Buffer} [saltBuf] - 可选的 salt
|
|||
|
|
* @returns {Object} { salt, hash }
|
|||
|
|
*/
|
|||
|
|
function scryptHash(password, saltBuf) {
|
|||
|
|
const salt = saltBuf || crypto.randomBytes(SALT_LEN)
|
|||
|
|
const derived = crypto.scryptSync(String(password), salt, KEYLEN, { N, r, p, maxmem: 64 * 1024 * 1024 })
|
|||
|
|
return { salt, hash: derived }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 哈希密码(使用 scrypt)
|
|||
|
|
* @param {string} password - 密码
|
|||
|
|
* @returns {string} 编码后的密码哈希
|
|||
|
|
*/
|
|||
|
|
function hashPassword(password) {
|
|||
|
|
const { salt, hash } = scryptHash(password)
|
|||
|
|
const saltB64 = salt.toString('base64').replace(/=+$/, '')
|
|||
|
|
const hashB64 = hash.toString('base64').replace(/=+$/, '')
|
|||
|
|
return `scrypt$${N}$${r}$${p}$${saltB64}$${hashB64}`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 验证 scrypt 密码
|
|||
|
|
* @param {string} password - 密码
|
|||
|
|
* @param {string} encoded - 编码后的密码哈希
|
|||
|
|
* @returns {boolean} 是否验证通过
|
|||
|
|
*/
|
|||
|
|
function verifyScrypt(password, encoded) {
|
|||
|
|
try {
|
|||
|
|
const parts = encoded.split('$')
|
|||
|
|
if (parts.length !== 6 || parts[0] !== 'scrypt') return false
|
|||
|
|
const saltB64 = parts[4]
|
|||
|
|
const expectedB64 = parts[5]
|
|||
|
|
const salt = Buffer.from(saltB64, 'base64')
|
|||
|
|
const expected = Buffer.from(expectedB64, 'base64')
|
|||
|
|
const { hash } = scryptHash(password, salt)
|
|||
|
|
if (hash.length !== expected.length) return false
|
|||
|
|
return crypto.timingSafeEqual(hash, expected)
|
|||
|
|
} catch {
|
|||
|
|
return false
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 验证密码(支持 scrypt 和旧版 MD5)
|
|||
|
|
* @param {string} password - 密码
|
|||
|
|
* @param {string} stored - 存储的密码哈希
|
|||
|
|
* @returns {boolean} 是否验证通过
|
|||
|
|
*/
|
|||
|
|
function verifyPassword(password, stored) {
|
|||
|
|
if (!stored) return false
|
|||
|
|
if (stored.startsWith('scrypt$')) return verifyScrypt(password, stored)
|
|||
|
|
if (/^[a-f0-9]{32}$/i.test(stored)) return md5(password).toLowerCase() === stored.toLowerCase()
|
|||
|
|
return false
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 检查是否是旧版 MD5 哈希
|
|||
|
|
* @param {string} stored - 存储的密码哈希
|
|||
|
|
* @returns {boolean} 是否是旧版哈希
|
|||
|
|
*/
|
|||
|
|
function isLegacyHash(stored) {
|
|||
|
|
return stored && /^[a-f0-9]{32}$/i.test(stored)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 检查是否需要重新哈希密码
|
|||
|
|
* @param {string} stored - 存储的密码哈希
|
|||
|
|
* @returns {boolean} 是否需要重新哈希
|
|||
|
|
*/
|
|||
|
|
function needsRehash(stored) {
|
|||
|
|
return !stored || !stored.startsWith('scrypt$')
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
module.exports = {
|
|||
|
|
hashPassword,
|
|||
|
|
verifyPassword,
|
|||
|
|
isLegacyHash,
|
|||
|
|
needsRehash,
|
|||
|
|
md5
|
|||
|
|
}
|