182 lines
4.4 KiB
JavaScript
182 lines
4.4 KiB
JavaScript
/**
|
|
* 加密工具函数
|
|
* 提供密码加密、验证等安全相关功能
|
|
*/
|
|
|
|
const crypto = require('crypto');
|
|
|
|
// 配置参数
|
|
const SALT_LENGTH = 16; // salt 长度
|
|
const KEY_LENGTH = 64; // 密钥长度
|
|
const ITERATIONS = 100000; // 迭代次数
|
|
const DIGEST = 'sha256'; // 摘要算法
|
|
|
|
/**
|
|
* 生成密码哈希
|
|
* @param {string} password - 明文密码
|
|
* @returns {Promise<string>} 加密后的密码字符串 (格式: salt$hash)
|
|
*/
|
|
async function hashPassword(password) {
|
|
if (!password || typeof password !== 'string') {
|
|
throw new Error('密码不能为空');
|
|
}
|
|
|
|
return new Promise((resolve, reject) => {
|
|
// 生成随机 salt
|
|
const salt = crypto.randomBytes(SALT_LENGTH).toString('hex');
|
|
|
|
// 使用 pbkdf2 生成密钥
|
|
crypto.pbkdf2(password, salt, ITERATIONS, KEY_LENGTH, DIGEST, (err, derivedKey) => {
|
|
if (err) {
|
|
reject(err);
|
|
} else {
|
|
const hash = derivedKey.toString('hex');
|
|
// 返回格式: salt$hash
|
|
resolve(`${salt}$${hash}`);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 验证密码
|
|
* @param {string} password - 明文密码
|
|
* @param {string} hashedPassword - 加密后的密码 (格式: salt$hash)
|
|
* @returns {Promise<boolean>} 是否匹配
|
|
*/
|
|
async function verifyPassword(password, hashedPassword) {
|
|
if (!password || !hashedPassword) {
|
|
return false;
|
|
}
|
|
|
|
return new Promise((resolve, reject) => {
|
|
try {
|
|
// 解析 salt 和 hash
|
|
const parts = hashedPassword.split('$');
|
|
if (parts.length !== 2) {
|
|
resolve(false);
|
|
return;
|
|
}
|
|
|
|
const [salt, originalHash] = parts;
|
|
|
|
// 使用相同的 salt 生成密钥
|
|
crypto.pbkdf2(password, salt, ITERATIONS, KEY_LENGTH, DIGEST, (err, derivedKey) => {
|
|
if (err) {
|
|
reject(err);
|
|
} else {
|
|
const hash = derivedKey.toString('hex');
|
|
// 使用恒定时间比较,防止时序攻击
|
|
resolve(crypto.timingSafeEqual(Buffer.from(hash), Buffer.from(originalHash)));
|
|
}
|
|
});
|
|
} catch (error) {
|
|
reject(error);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 生成安全的随机 token
|
|
* @param {number} length - token 长度(字节数),默认 32
|
|
* @returns {string} 十六进制字符串
|
|
*/
|
|
function generateToken(length = 32) {
|
|
return crypto.randomBytes(length).toString('hex');
|
|
}
|
|
|
|
/**
|
|
* 生成设备 ID
|
|
* @param {string} prefix - 前缀,默认 'device'
|
|
* @returns {string} 设备 ID
|
|
*/
|
|
function generateDeviceId(prefix = 'device') {
|
|
const timestamp = Date.now();
|
|
const random = crypto.randomBytes(8).toString('hex');
|
|
return `${prefix}_${timestamp}_${random}`;
|
|
}
|
|
|
|
/**
|
|
* 验证设备 ID 格式
|
|
* @param {string} deviceId - 设备 ID
|
|
* @returns {boolean} 是否有效
|
|
*/
|
|
function validateDeviceId(deviceId) {
|
|
if (!deviceId || typeof deviceId !== 'string') {
|
|
return false;
|
|
}
|
|
|
|
// 检查格式: prefix_timestamp_randomhex
|
|
const pattern = /^[a-z]+_\d{13}_[a-f0-9]{16}$/i;
|
|
return pattern.test(deviceId);
|
|
}
|
|
|
|
/**
|
|
* 脱敏处理 - 手机号
|
|
* @param {string} phone - 手机号
|
|
* @returns {string} 脱敏后的手机号
|
|
*/
|
|
function maskPhone(phone) {
|
|
if (!phone || typeof phone !== 'string') {
|
|
return '';
|
|
}
|
|
if (phone.length < 11) {
|
|
return phone;
|
|
}
|
|
return phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2');
|
|
}
|
|
|
|
/**
|
|
* 脱敏处理 - 邮箱
|
|
* @param {string} email - 邮箱
|
|
* @returns {string} 脱敏后的邮箱
|
|
*/
|
|
function maskEmail(email) {
|
|
if (!email || typeof email !== 'string') {
|
|
return '';
|
|
}
|
|
const parts = email.split('@');
|
|
if (parts.length !== 2) {
|
|
return email;
|
|
}
|
|
const [username, domain] = parts;
|
|
if (username.length <= 2) {
|
|
return `*@${domain}`;
|
|
}
|
|
const masked = username[0] + '***' + username[username.length - 1];
|
|
return `${masked}@${domain}`;
|
|
}
|
|
|
|
/**
|
|
* 脱敏处理 - 通用对象(用于日志)
|
|
* @param {Object} obj - 要脱敏的对象
|
|
* @param {Array<string>} sensitiveFields - 敏感字段列表
|
|
* @returns {Object} 脱敏后的对象
|
|
*/
|
|
function maskSensitiveData(obj, sensitiveFields = ['password', 'pwd', 'token', 'secret', 'key']) {
|
|
if (!obj || typeof obj !== 'object') {
|
|
return obj;
|
|
}
|
|
|
|
const masked = { ...obj };
|
|
|
|
for (const field of sensitiveFields) {
|
|
if (masked[field]) {
|
|
masked[field] = '***MASKED***';
|
|
}
|
|
}
|
|
|
|
return masked;
|
|
}
|
|
|
|
module.exports = {
|
|
hashPassword,
|
|
verifyPassword,
|
|
generateToken,
|
|
generateDeviceId,
|
|
validateDeviceId,
|
|
maskPhone,
|
|
maskEmail,
|
|
maskSensitiveData
|
|
};
|