Files
esengine/packages/framework/database/src/password.ts

190 lines
4.5 KiB
TypeScript
Raw Normal View History

/**
* @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 }
}