diff --git a/app/backend/src/lib/password-strength.ts b/app/backend/src/lib/password-strength.ts new file mode 100644 index 0000000..abbfcd5 --- /dev/null +++ b/app/backend/src/lib/password-strength.ts @@ -0,0 +1,68 @@ +const COMMON_PASSWORDS = new Set([ + 'password', '12345678', 'qwerty12', 'abc12345', 'password1', + '123456789', '1234567890', 'iloveyou', 'sunshine1', 'princess', + 'football', 'charlie1', 'access12', 'master12', 'michael1', + 'shadow12', 'monkey12', ' letmein1', 'dragon12', 'trustno1', +]); + +export interface PasswordStrength { + score: number; + issues: string[]; + entropy: number; +} + +export function validatePasswordStrength(password: string): PasswordStrength { + const issues: string[] = []; + let score = 0; + + if (password.length >= 8) score += 1; + if (password.length >= 12) score += 1; + if (password.length >= 16) score += 1; + + const hasLower = /[a-z]/.test(password); + const hasUpper = /[A-Z]/.test(password); + const hasDigit = /[0-9]/.test(password); + const hasSpecial = /[^a-zA-Z0-9]/.test(password); + const hasPersian = /[\u0600-\u06FF]/.test(password); + + const charsetSize = + (hasLower ? 26 : 0) + + (hasUpper ? 26 : 0) + + (hasDigit ? 10 : 0) + + (hasSpecial ? 33 : 0) + + (hasPersian ? 33 : 0); + + const entropy = Math.log2(Math.max(charsetSize, 1)) * password.length; + + if (hasLower) score += 1; + if (hasUpper) score += 1; + if (hasDigit) score += 1; + if (hasSpecial) score += 1; + + const repeats = /(.)\1{2,}/.test(password); + const sequential = /(?:abc|bcd|cde|def|efg|fgh|ghi|hij|ijk|jkl|klm|lmn|mno|nop|opq|pqr|qrs|rst|stu|tuv|uvw|vwx|wxy|xyz|012|123|234|345|456|567|678|789)/i.test(password); + const keyboardRows = /(?:qwerty|asdf|zxcv|qaz|wsx|edc)/i.test(password); + + if (repeats) { score -= 1; issues.push('repeating_characters'); } + if (sequential) { score -= 1; issues.push('sequential_characters'); } + if (keyboardRows) { score -= 1; issues.push('keyboard_pattern'); } + + if (charsetSize < 2) issues.push('needs_variety'); + if (entropy < 30) issues.push('low_entropy'); + if (COMMON_PASSWORDS.has(password.toLowerCase())) issues.push('common_password'); + + if (password.length < 8) issues.push('too_short'); + if (password.length > 256) issues.push('too_long'); + + return { + score: Math.max(0, Math.min(10, score)), + issues, + entropy: Math.round(entropy * 10) / 10, + }; +} + +export function isPasswordAcceptable(password: string): { ok: boolean; issues: string[] } { + const result = validatePasswordStrength(password); + const ok = result.issues.length === 0 && result.score >= 4; + return { ok, issues: result.issues }; +}