// This is rewrite of `cms/libs/jquery-password-strength/jquery.password-strength.js` in typescript export const PASSWORD_STRENGTH = { short: 'short', bad: 'bad', good: 'good', strong: 'strong', } as const; export type PasswordStrength = (typeof PASSWORD_STRENGTH)[keyof typeof PASSWORD_STRENGTH]; export function getPasswordStrength(password: string, username?: string): PasswordStrength { //password < 4 if (password.length < 4) { return PASSWORD_STRENGTH.short; } //password == user name if (password.toLowerCase() == username?.toLowerCase()) { return PASSWORD_STRENGTH.bad; } const score = passwordScore(password, username); if (score < 34) { return PASSWORD_STRENGTH.bad; } if (score < 68) { return PASSWORD_STRENGTH.good; } return PASSWORD_STRENGTH.strong; } function passwordScore(password: string, username?: string) { let score = 0; //password length score += password.length * 4; score += (checkRepetition(1, password).length - password.length) * 1; score += (checkRepetition(2, password).length - password.length) * 1; score += (checkRepetition(3, password).length - password.length) * 1; score += (checkRepetition(4, password).length - password.length) * 1; //password has 3 numbers if (password.match(/\d\D+\d+\D+\d+/)) { score += 10; } //password has 2 symbols if (password.match(/[^\dA-Za-z].*[^\dA-Za-z]/)) { score += 10; } //password has Upper and Lower chars if (password.match(/([a-z].*[A-Z])|([A-Z].*[a-z])/)) { score += 5; } //password has number and chars if (password.match(/([A-Za-z])/) && password.match(/(\d)/)) { score += 15; } // //password has number and symbol if (password.match(/([^\dA-Za-z])/) && password.match(/(\d)/)) { score += 15; } //password has char and symbol if (password.match(/([^\dA-Za-z])/) && password.match(/([A-Za-z])/)) { score += 15; } //password is just a numbers or chars if (password.match(/^\w+$/) || password.match(/^\d+$/)) { score -= 10; } // password is just a number if (password.match(/^\d+$/)) { score *= 0.3; } // Common patterns or if (password.match(/(^[A-Za-z]+\d+$)|(^\d+[A-Za-z]+$)/)) { score -= 10; } //verifying 0 < score < 100 if (score < 0) { score = 0; } if (score > 100) { score = 100; } // It has to be strong password to allow long part similar to username if (username && pwLongestCommonSubstring(password, username).length >= Math.round(0.45 * password.length)) { score -= 35; } return score; } function checkRepetition(pLen: number, str: string) { let res = ''; for (let i = 0; i < str.length; i++) { let repeated = true; let j = 0; for (; j < pLen && j + i + pLen < str.length; j++) { repeated = repeated && str.charAt(j + i) == str.charAt(j + i + pLen); } if (j < pLen) { repeated = false; } if (repeated) { i += pLen - 1; repeated = false; } else { res += str.charAt(i); } } return res; } /** * Returns the longest common sequence from two strings */ function pwLongestCommonSubstring(s1: string, s2: string) { const state: number[][] = [...s1].map(() => Array.from({ length: s2.length + 1 }).fill(0)); let longest = 0; let longestX = 0; for (let i = 1; i <= s1.length; i++) { for (let j = 1; j <= s2.length; j++) { if (pwSameChar(s1[i - 1]!, s2[j - 1]!)) { state[i]![j] = state[i - 1]![j - 1]! + 1; if (longest < state[i]![j]!) { longest = state[i]![j]!; longestX = i; } } else { state[i]![j] = 0; } } } // oxlint-disable-next-line unicorn/prefer-string-slice return s1.substring(longestX - longest, longestX); } /** * Check whether characters should be considered same in case of password * strength calculation */ function pwSameChar(c1: string, c2: string) { const leetFrom = ['4', '@', '3', '1', '!', '0', '6', '5', '7'] as const; const leetTo = ['a', 'a', 'e', 'i', 'i', 'o', 'g', 's', 't'] as const; c1 = c1.toLowerCase(); c2 = c2.toLowerCase(); if (c1 == c2) { return true; } let c1l = c1; for (const [i, element] of leetFrom.entries()) { c1l = c1l.replace(element, leetTo[i]!); } if (c1l == c2) { return true; } let c2l = c2; for (const [i, element] of leetFrom.entries()) { c2l = c2l.replace(element, leetTo[i]!); } if (c1 == c2l) { return true; } return false; }