This commit is contained in:
张成
2025-12-19 22:24:23 +08:00
parent abe2ae3c3a
commit 10aff2f266
12 changed files with 1101 additions and 147 deletions

181
api/utils/crypto_utils.js Normal file
View File

@@ -0,0 +1,181 @@
/**
* 加密工具函数
* 提供密码加密、验证等安全相关功能
*/
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
};