- Add @esengine/database-drivers for MongoDB/Redis connection management - Add @esengine/database for Repository pattern with CRUD, pagination, soft delete - Refactor @esengine/transaction MongoStorage to use shared connection - Add comprehensive documentation in Chinese and English
190 lines
4.5 KiB
TypeScript
190 lines
4.5 KiB
TypeScript
/**
|
||
* @zh 密码加密工具
|
||
* @en Password hashing utilities
|
||
*
|
||
* @zh 使用 Node.js 内置的 crypto 模块实现安全的密码哈希
|
||
* @en Uses Node.js built-in crypto module for secure password hashing
|
||
*/
|
||
|
||
import { randomBytes, scrypt, timingSafeEqual } from 'crypto'
|
||
import { promisify } from 'util'
|
||
|
||
const scryptAsync = promisify(scrypt)
|
||
|
||
/**
|
||
* @zh 密码哈希配置
|
||
* @en Password hash configuration
|
||
*/
|
||
export interface PasswordHashConfig {
|
||
/**
|
||
* @zh 盐的字节长度(默认 16)
|
||
* @en Salt length in bytes (default 16)
|
||
*/
|
||
saltLength?: number
|
||
|
||
/**
|
||
* @zh scrypt 密钥长度(默认 64)
|
||
* @en scrypt key length (default 64)
|
||
*/
|
||
keyLength?: number
|
||
}
|
||
|
||
const DEFAULT_CONFIG: Required<PasswordHashConfig> = {
|
||
saltLength: 16,
|
||
keyLength: 64
|
||
}
|
||
|
||
/**
|
||
* @zh 对密码进行哈希处理
|
||
* @en Hash a password
|
||
*
|
||
* @param password - @zh 明文密码 @en Plain text password
|
||
* @param config - @zh 哈希配置 @en Hash configuration
|
||
* @returns @zh 格式为 "salt:hash" 的哈希字符串 @en Hash string in "salt:hash" format
|
||
*
|
||
* @example
|
||
* ```typescript
|
||
* const hashedPassword = await hashPassword('myPassword123')
|
||
* // 存储 hashedPassword 到数据库
|
||
* ```
|
||
*/
|
||
export async function hashPassword(
|
||
password: string,
|
||
config?: PasswordHashConfig
|
||
): Promise<string> {
|
||
const { saltLength, keyLength } = { ...DEFAULT_CONFIG, ...config }
|
||
|
||
const salt = randomBytes(saltLength).toString('hex')
|
||
const derivedKey = (await scryptAsync(password, salt, keyLength)) as Buffer
|
||
|
||
return `${salt}:${derivedKey.toString('hex')}`
|
||
}
|
||
|
||
/**
|
||
* @zh 验证密码是否正确
|
||
* @en Verify if a password is correct
|
||
*
|
||
* @param password - @zh 明文密码 @en Plain text password
|
||
* @param hashedPassword - @zh 存储的哈希密码 @en Stored hashed password
|
||
* @param config - @zh 哈希配置 @en Hash configuration
|
||
* @returns @zh 密码是否匹配 @en Whether the password matches
|
||
*
|
||
* @example
|
||
* ```typescript
|
||
* const isValid = await verifyPassword('myPassword123', storedHash)
|
||
* if (isValid) {
|
||
* // 登录成功
|
||
* }
|
||
* ```
|
||
*/
|
||
export async function verifyPassword(
|
||
password: string,
|
||
hashedPassword: string,
|
||
config?: PasswordHashConfig
|
||
): Promise<boolean> {
|
||
const { keyLength } = { ...DEFAULT_CONFIG, ...config }
|
||
|
||
const [salt, storedHash] = hashedPassword.split(':')
|
||
if (!salt || !storedHash) {
|
||
return false
|
||
}
|
||
|
||
try {
|
||
const derivedKey = (await scryptAsync(password, salt, keyLength)) as Buffer
|
||
const storedBuffer = Buffer.from(storedHash, 'hex')
|
||
|
||
return timingSafeEqual(derivedKey, storedBuffer)
|
||
} catch {
|
||
return false
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @zh 密码强度等级
|
||
* @en Password strength level
|
||
*/
|
||
export type PasswordStrength = 'weak' | 'fair' | 'good' | 'strong'
|
||
|
||
/**
|
||
* @zh 密码强度检查结果
|
||
* @en Password strength check result
|
||
*/
|
||
export interface PasswordStrengthResult {
|
||
/**
|
||
* @zh 强度分数 (0-6)
|
||
* @en Strength score (0-6)
|
||
*/
|
||
score: number
|
||
|
||
/**
|
||
* @zh 强度等级
|
||
* @en Strength level
|
||
*/
|
||
level: PasswordStrength
|
||
|
||
/**
|
||
* @zh 改进建议
|
||
* @en Improvement suggestions
|
||
*/
|
||
feedback: string[]
|
||
}
|
||
|
||
/**
|
||
* @zh 检查密码强度
|
||
* @en Check password strength
|
||
*
|
||
* @param password - @zh 明文密码 @en Plain text password
|
||
* @returns @zh 密码强度信息 @en Password strength information
|
||
*/
|
||
export function checkPasswordStrength(password: string): PasswordStrengthResult {
|
||
const feedback: string[] = []
|
||
let score = 0
|
||
|
||
if (password.length >= 8) {
|
||
score += 1
|
||
} else {
|
||
feedback.push('Password should be at least 8 characters')
|
||
}
|
||
|
||
if (password.length >= 12) {
|
||
score += 1
|
||
}
|
||
|
||
if (/[a-z]/.test(password)) {
|
||
score += 1
|
||
} else {
|
||
feedback.push('Password should contain lowercase letters')
|
||
}
|
||
|
||
if (/[A-Z]/.test(password)) {
|
||
score += 1
|
||
} else {
|
||
feedback.push('Password should contain uppercase letters')
|
||
}
|
||
|
||
if (/[0-9]/.test(password)) {
|
||
score += 1
|
||
} else {
|
||
feedback.push('Password should contain numbers')
|
||
}
|
||
|
||
if (/[^a-zA-Z0-9]/.test(password)) {
|
||
score += 1
|
||
} else {
|
||
feedback.push('Password should contain special characters')
|
||
}
|
||
|
||
let level: PasswordStrength
|
||
if (score <= 2) {
|
||
level = 'weak'
|
||
} else if (score <= 3) {
|
||
level = 'fair'
|
||
} else if (score <= 4) {
|
||
level = 'good'
|
||
} else {
|
||
level = 'strong'
|
||
}
|
||
|
||
return { score, level, feedback }
|
||
}
|