diff --git a/assets/locales/ur/auth.json b/assets/locales/ur/auth.json index 1dac24744..022bb663c 100644 --- a/assets/locales/ur/auth.json +++ b/assets/locales/ur/auth.json @@ -10,7 +10,7 @@ "EMAIL_INVALID": "Invalid Email", "EMAIL_ALREADY_REGISTERED": "Email is already registered", "DATE_OF_BIRTH_UNDERAGE": "You need to be {{years}} years or older", - "PASSWORD_REQUIREMENTS_MIN_LENGTH": "Must be at least {{min}} characters long.", + "PASSWORD_BAD_PASSWORD": "Password is too weak or common to use.", "CONSENT_REQUIRED": "You must agree to the Terms of Service and Privacy Policy.", "USERNAME_TOO_MANY_USERS": "Too many users have this username, please try another" } diff --git a/src/api/routes/auth/register.ts b/src/api/routes/auth/register.ts index de1cbd3d8..8c0a528f9 100644 --- a/src/api/routes/auth/register.ts +++ b/src/api/routes/auth/register.ts @@ -18,6 +18,7 @@ import { IPAnalysis, + checkPassword, getIpAdress, isProxy, route, @@ -155,7 +156,6 @@ router.post( } // TODO: gift_code_sku_id? - // TODO: check password strength const email = body.email; if (email) { @@ -222,16 +222,11 @@ router.post( } if (body.password) { - const min = register.password.minLength ?? 8; - - if (body.password.length < min) { + if (checkPassword(body.password) < register.password.strength) { throw FieldErrors({ password: { - code: "PASSWORD_REQUIREMENTS_MIN_LENGTH", - message: req.t( - "auth:register.PASSWORD_REQUIREMENTS_MIN_LENGTH", - { min: min }, - ), + code: "PASSWORD_BAD_PASSWORD", + message: req.t("auth:register.PASSWORD_BAD_PASSWORD"), }, }); } diff --git a/src/api/util/utility/passwordStrength.ts b/src/api/util/utility/passwordStrength.ts index fd627fbf7..ac6eeca10 100644 --- a/src/api/util/utility/passwordStrength.ts +++ b/src/api/util/utility/passwordStrength.ts @@ -20,8 +20,8 @@ import { Config } from "@spacebar/util"; import "missing-native-js-functions"; const reNUMBER = /[0-9]/g; -const reUPPERCASELETTER = /[A-Z]/g; -const reSYMBOLS = /[A-Z,a-z,0-9]/g; +const reUPPER = /[A-Z]/g; +const reSYMBOLS = /[^a-zA-Z0-9\s]/g; // const blocklist: string[] = []; // TODO: update ones passwordblocklist is stored in db /* @@ -35,50 +35,71 @@ const reSYMBOLS = /[A-Z,a-z,0-9]/g; * * Returns: 0 > pw > 1 */ + +function calculateEntropy(str: string) { + // Initialize entropy to 0 + let entropy = 0; + + // Initialize a frequency array with 256 elements all set to 0 + // This array will hold the frequency of each character in the string + const frequency = new Array(256).fill(0); + + // Get the length of the string + const length = str.length; + + // Iterate over each character in the string + for (let i = 0; i < length; i++) { + // Increment the frequency of the current character + frequency[str.charCodeAt(i)]++; + } + + // Iterate over each possible character + for (let i = 0; i < 256; i++) { + // Calculate the probability of the current character + const p = frequency[i] / length; + + // If the character appears in the string (probability > 0) + // add its contribution to the entropy + if (p > 0) entropy -= p * Math.log2(p); + } + + // Normalize the entropy to the range [0, 1] + const MAX_ENTROPY_PER_CHAR = Math.log2(95); // Maximum entropy per character for all printable ASCII characters + const MAX_ENTROPY = MAX_ENTROPY_PER_CHAR * length; // Maximum possible entropy for the password + entropy = entropy / MAX_ENTROPY; + + // Return the calculated entropy + return entropy; +} + export function checkPassword(password: string): number { - const { minLength, minNumbers, minUpperCase, minSymbols } = - Config.get().register.password; let strength = 0; // checks for total password len - if (password.length >= minLength - 1) { - strength += 0.05; - } + if (password.length >= 7) strength += 0.2; - // checks for amount of Numbers - if (password.count(reNUMBER) >= minNumbers - 1) { - strength += 0.05; - } + // checks for numbers + const numbers = password.match(reNUMBER); + if (numbers) strength += 0.2; - // checks for amount of Uppercase Letters - if (password.count(reUPPERCASELETTER) >= minUpperCase - 1) { - strength += 0.05; - } + // checks for uppercase Letters + const uppercase = password.match(reUPPER); + if (uppercase) strength += 0.3; - // checks for amount of symbols - if (password.replace(reSYMBOLS, "").length >= minSymbols - 1) { - strength += 0.05; - } + // checks for symbols + const symbols = password.match(reSYMBOLS); + if (symbols) strength += 0.3; // checks if password only consists of numbers or only consists of chars - if ( - password.length == password.count(reNUMBER) || - password.length === password.count(reUPPERCASELETTER) - ) { - strength = 0; - } - - const entropyMap: { [key: string]: number } = {}; - for (let i = 0; i < password.length; i++) { - if (entropyMap[password[i]]) entropyMap[password[i]]++; - else entropyMap[password[i]] = 1; + if (numbers && uppercase) { + if ( + password.length == numbers.length || + password.length === uppercase.length + ) { + strength = 0; + } } - const entropies = Object.values(entropyMap); - - entropies.map((x) => x / entropyMap.length); - strength += - entropies.reduceRight((a: number, x: number) => a - x * Math.log2(x)) / - Math.log2(password.length); + strength += calculateEntropy(password); return strength; } diff --git a/src/util/config/types/subconfigurations/register/Password.ts b/src/util/config/types/subconfigurations/register/Password.ts index 36c3e3dc4..4f0522bb2 100644 --- a/src/util/config/types/subconfigurations/register/Password.ts +++ b/src/util/config/types/subconfigurations/register/Password.ts @@ -18,8 +18,5 @@ export class PasswordConfiguration { required: boolean = false; - minLength: number = 8; - minNumbers: number = 2; - minUpperCase: number = 2; - minSymbols: number = 0; + strength: number = 0.5; }