diff --git a/frontend/public/locales/en/translations.json b/frontend/public/locales/en/translations.json index dc2a1cfd3..cea11c2ea 100644 --- a/frontend/public/locales/en/translations.json +++ b/frontend/public/locales/en/translations.json @@ -232,16 +232,14 @@ "current-wrong": "The current password may be wrong", "new": "New password", "validate-base": "Password should contain:", - "validate-too-short": "at least 14 characters", - "validate-too-long": "at most 100 characters", - "validate-uppercase": "at least 1 uppercase character", - "validate-lowercase": "at least 1 lowercase character", - "validate-number": "at least 1 number", - "validate-special-char": "at least 1 special character", - "validate-repeated-char": "at most 2 repeated, consecutive characters", - "validate-is-email": "The password cannot be an email address.", - "validate-is-url": "The password cannot be a URL.", - "validate-is-breached": "The new password is in a list of passwords commonly used on other websites. Please try again with a stronger password." + "validate-tooShort": "at least 14 characters", + "validate-tooLong": "at most 100 characters", + "validate-noLetterChar": "at least 1 letter character", + "validate-noNumOrSpecialChar": "at least 1 number or special character", + "validate-repeatedChar": "at most 3 repeated, consecutive characters", + "validate-escapeChar": "No escape characters allowed.", + "validate-lowEntropy": "Password contains sensitive data.", + "validate-breached": "Password was found in a data breach." }, "token": { "service-tokens": "Service Tokens", diff --git a/frontend/public/locales/es/translations.json b/frontend/public/locales/es/translations.json index 8842d6a10..44da9a8ce 100644 --- a/frontend/public/locales/es/translations.json +++ b/frontend/public/locales/es/translations.json @@ -229,16 +229,14 @@ "current-wrong": "La contraseña actual puede puede que sea incorrecta", "new": "Nueva contraseña", "validate-base": "La contraseña debe contener:", - "validate-too-short": "al menos 14 caracteres", - "validate-too-long": "como máximo 100 caracteres", - "validate-uppercase": "al menos 1 carácter en mayúscula", - "validate-lowercase": "al menos 1 carácter en minúsculas", - "validate-number": "al menos 1 número", - "validate-special-char": "al menos 1 carácter especial", - "validate-repeated-char": "como máximo 2 caracteres repetidos y consecutivos", - "validate-is-email": "La contraseña no puede ser una dirección de correo electrónico.", - "validate-is-url": "La contraseña no puede ser una URL.", - "validate-is-breached": "La nueva contraseña se encuentra en una lista de contraseñas comúnmente utilizadas en otros sitios web. Vuelva a intentarlo con una contraseña más segura." + "validate-tooShort": "al menos 14 caracteres", + "validate-tooLong": "como máximo 100 caracteres", + "validate-noLetterChar": "al menos 1 carácter alfabético", + "validate-noNumOrSpecialChar": "al menos 1 número o carácter especial", + "validate-repeatedChar": "como máximo 3 caracteres repetidos y consecutivos", + "validate-escapeChar": "No se permiten caracteres de escape.", + "validate-lowEntropy": "La contraseña contiene datos sensibles.", + "validate-breached": "La contraseña se encontró en una violación de datos." }, "token": { "service-tokens": "Tokens de servicio", diff --git a/frontend/public/locales/fr/translations.json b/frontend/public/locales/fr/translations.json index a65129392..60d5cf8cf 100644 --- a/frontend/public/locales/fr/translations.json +++ b/frontend/public/locales/fr/translations.json @@ -215,17 +215,15 @@ "current": "Mot de passe actuel", "current-wrong": "Le mot de passe actuel peut être érroné", "new": "Nouveau mot de passe", - "validate-base": "Le mot de passe doit contenir:", - "validate-too-short": "au moins 14 caractères", - "validate-too-long": "au maximum 100 caractères", - "validate-uppercase": "au moins 1 caractère miniscule", - "validate-lowercase": "au moins 1 caractère majuscule", - "validate-number": "au moins 1 chiffre", - "validate-special-char": "au moins 1 caractère spécial", - "validate-repeated-char": "au plus 2 caractères répétés et consécutifs", - "validate-is-email": "Le mot de passe ne peut pas être une adresse e-mail.", - "validate-is-url": "Le mot de passe ne peut pas être une URL.", - "validate-is-breached": "Le nouveau mot de passe se trouve dans une liste de mots de passe couramment utilisés sur d'autres sites Web. Veuillez réessayer avec un mot de passe plus fort." + "validate-base": "Le mot de passe doit contenir :", + "validate-tooShort": "au moins 14 caractères", + "validate-tooLong": "au plus 100 caractères", + "validate-noLetterChar": "au moins 1 caractère alphabétique", + "validate-noNumOrSpecialChar": "au moins 1 chiffre ou caractère spécial", + "validate-repeatedChar": "au plus 3 caractères consécutifs répétés", + "validate-escapeChar": "Aucun caractère d'échappement autorisé.", + "validate-lowEntropy": "Le mot de passe contient des données sensibles.", + "validate-breached": "Le mot de passe a été trouvé dans une violation de données." }, "token": { "service-tokens": "Jetons de service", diff --git a/frontend/public/locales/ko/translations.json b/frontend/public/locales/ko/translations.json index e5f6deee7..e8169eaba 100644 --- a/frontend/public/locales/ko/translations.json +++ b/frontend/public/locales/ko/translations.json @@ -182,17 +182,15 @@ "current": "현재 비밀번호", "new": "새 비밀번호", "current-wrong": "현재 비밀번호가 잘못되었어요", - "validate-base": "비밀번호는 다음 조건을 만족해야 합니다:", - "validate-too-short": "최소 14자", - "validate-too-long": "최대 100자", - "validate-uppercase": "최소 1개의 대문자", - "validate-lowercase": "최소 1개의 소문자", - "validate-number": "숫자 1개 이상", - "validate-special-char": "특수 문자 1개 이상", - "validate-repeated-char": "최대 2개의 반복된 연속 문자", - "validate-is-email": "비밀번호는 이메일 주소가 될 수 없습니다.", - "validate-is-url": "비밀번호는 URL일 수 없습니다.", - "validate-is-breached": "새 비밀번호는 다른 웹사이트에서 일반적으로 사용되는 비밀번호 목록에 있습니다. 더 강력한 비밀번호로 다시 시도해 주세요." + "validate-base": "비밀번호는 다음을 포함해야 합니다:", + "validate-tooShort": "최소 14자", + "validate-tooLong": "최대 100자", + "validate-noLetterChar": "최소 1개의 문자를 포함해야 합니다.", + "validate-noNumOrSpecialChar": "최소 1개의 숫자 또는 특수 문자를 포함해야 합니다.", + "validate-repeatedChar": "연속으로 최대 3개의 반복된 문자를 포함할 수 있습니다.", + "validate-escapeChar": "이스케이프 문자는 허용되지 않습니다.", + "validate-lowEntropy": "비밀번호에 민감한 데이터가 포함되어 있습니다.", + "validate-breached": "비밀번호가 데이터 유출에 포함되었습니다." }, "token": { "add-dialog": { diff --git a/frontend/public/locales/pt-BR/translations.json b/frontend/public/locales/pt-BR/translations.json index 362ec2aae..ba324849b 100644 --- a/frontend/public/locales/pt-BR/translations.json +++ b/frontend/public/locales/pt-BR/translations.json @@ -211,16 +211,14 @@ "current-wrong": "A senha atual pode estar errada", "new": "Nova Senha", "validate-base": "A senha deve conter:", - "validate-too-short": "pelo menos 14 caracteres", - "validate-too-long": "no máximo 100 caracteres", - "validate-uppercase": "pelo menos 1 caractere maiúsculo", - "validate-lowercase": "pelo menos 1 caractere minúsculo", - "validate-number": "pelo menos 1 número", - "validate-special-char": "pelo menos 1 caractere especial", - "validate-repeated-char": "no máximo 2 caracteres repetidos e consecutivos", - "validate-is-email": "A senha não pode ser um endereço de e-mail.", - "validate-is-url": "A senha não pode ser um URL.", - "validate-is-breached": "A nova senha está em uma lista de senhas comumente usadas em outros sites. Tente novamente com uma senha mais forte." + "validate-tooShort": "pelo menos 14 caracteres", + "validate-tooLong": "no máximo 100 caracteres", + "validate-noLetterChar": "pelo menos 1 caractere alfabético", + "validate-noNumOrSpecialChar": "pelo menos 1 número ou caractere especial", + "validate-repeatedChar": "no máximo 3 caracteres repetidos e consecutivos", + "validate-escapeChar": "Nenhum caractere de escape permitido.", + "validate-lowEntropy": "A senha contém dados sensíveis.", + "validate-breached": "A senha foi encontrada em uma violação de dados." }, "token": { "service-tokens": "Tokens de Serviço", diff --git a/frontend/public/locales/tr/translations.json b/frontend/public/locales/tr/translations.json index c1b72d7e3..93f228f96 100644 --- a/frontend/public/locales/tr/translations.json +++ b/frontend/public/locales/tr/translations.json @@ -228,17 +228,15 @@ "current": "Mevcut şifre", "current-wrong": "Mevcut şifre yanlış olabilir", "new": "Yeni şifre", - "validate-base": "Şifre kısıtlamaları:", - "validate-too-short": "en az 14 karakter", - "validate-too-long": "en fazla 100 karakter", - "validate-uppercase": "en az 1 büyük harf karakter", - "validate-lowercase": "en az 1 küçük harf karakter", - "validate-number": "en az 1 sayı", - "validate-special-char": "en az 1 özel karakter", - "validate-repeated-char": "en fazla 2 tekrarlanan, ardışık karakter", - "validate-is-email": "Şifre bir e-posta adresi olamaz.", - "validate-is-url": "Şifre bir URL olamaz.", - "validate-is-breached": "Yeni şifre, diğer web sitelerinde yaygın olarak kullanılan şifrelerin listesinde yer almaktadır. Lütfen daha güçlü bir şifre ile tekrar deneyiniz." + "validate-base": "Parola içermelidir:", + "validate-tooShort": "en az 14 karakter", + "validate-tooLong": "en fazla 100 karakter", + "validate-noLetterChar": "en az 1 harf karakteri", + "validate-noNumOrSpecialChar": "en az 1 rakam veya özel karakter", + "validate-repeatedChar": "en fazla 3 tekrarlanan, ardışık karakter", + "validate-escapeChar": "Kaçış karakterlerine izin verilmez.", + "validate-lowEntropy": "Parola hassas veriler içeriyor.", + "validate-breached": "Parola veri ihlalinde bulundu." }, "token": { "service-tokens": "Servis Belirteçleri", diff --git a/frontend/src/components/signup/UserInfoStep.tsx b/frontend/src/components/signup/UserInfoStep.tsx index 0c24b2aac..430d373bd 100644 --- a/frontend/src/components/signup/UserInfoStep.tsx +++ b/frontend/src/components/signup/UserInfoStep.tsx @@ -40,14 +40,12 @@ interface UserInfoStepProps { type Errors = { tooShort?: string; tooLong?: string; - upperCase?: string; - lowerCase?: string; - number?: string; - specialChar?: string; + noLetterChar?: string; + noNumOrSpecialChar?: string; repeatedChar?: string; - isEmail?: string; - isUrl?: string; - isBeachedPassword?: string; + escapeChar?: string; + lowEntropy?: string; + breached?: string; }; /** diff --git a/frontend/src/components/utilities/checks/PasswordCheck.ts b/frontend/src/components/utilities/checks/PasswordCheck.ts index 7e212873b..3804841fc 100644 --- a/frontend/src/components/utilities/checks/PasswordCheck.ts +++ b/frontend/src/components/utilities/checks/PasswordCheck.ts @@ -1,129 +1,87 @@ -import {string} from "yup"; +import { letterCharRegex, numAndSpecialCharRegex, repeatedCharRegex, escapeCharRegex, lowEntropyRegexes } from "./passwordRegexes"; import { checkIsPasswordBreached } from "./checkIsPasswordBreached"; -/* eslint-disable no-param-reassign */ interface PasswordCheckProps { password: string; errorCheck: boolean; setPasswordErrorTooShort: (value: boolean) => void; setPasswordErrorTooLong: (value: boolean) => void; - setPasswordErrorUpperCase: (value: boolean) => void; - setPasswordErrorLowerCase: (value: boolean) => void; - setPasswordErrorNumber: (value: boolean) => void; - setPasswordErrorSpecialChar: (value: boolean) => void; + setPasswordErrorNoLetterChar: (value: boolean) => void; + setPasswordErrorNoNumOrSpecialChar: (value: boolean) => void; setPasswordErrorRepeatedChar: (value: boolean) => void; - setPasswordErrorIsEmail: (value: boolean) => void; - setPasswordErrorIsUrl: (value: boolean) => void; - setPasswordErrorIsBreachedPassword: (value: boolean) => void; + setPasswordErrorEscapeChar: (value: boolean) => void; + setPasswordErrorLowEntropy: (value: boolean) => void; + setPasswordErrorBreached: (value: boolean) => void; } -/** - * This function checks a user password with respect to some criteria. - */ const passwordCheck = async ({ password, setPasswordErrorTooShort, setPasswordErrorTooLong, - setPasswordErrorUpperCase, - setPasswordErrorLowerCase, - setPasswordErrorNumber, - setPasswordErrorSpecialChar, + setPasswordErrorNoLetterChar, + setPasswordErrorNoNumOrSpecialChar, setPasswordErrorRepeatedChar, - setPasswordErrorIsEmail, - setPasswordErrorIsUrl, - setPasswordErrorIsBreachedPassword, + setPasswordErrorEscapeChar, + setPasswordErrorLowEntropy, + setPasswordErrorBreached, errorCheck }: PasswordCheckProps) => { - // tooShort - if (!password || password.length < 14) { - setPasswordErrorTooShort(true); + const tests = [ + { + name: "tooShort", + validator: (pwd: string) => pwd.length >= 14, + setError: setPasswordErrorTooShort, + }, + { + name: "tooLong", + validator: (pwd: string) => pwd.length < 101, + setError: setPasswordErrorTooLong, + }, + { + name: "noLetterChar", + validator: (pwd: string) => letterCharRegex.test(pwd), + setError: setPasswordErrorNoLetterChar, + }, + { + name: "noNumOrSpecialChar", + validator: (pwd: string) => numAndSpecialCharRegex.test(pwd), + setError: setPasswordErrorNoNumOrSpecialChar, + }, + { + name: "repeatedChar", + validator: (pwd: string) => !repeatedCharRegex.test(pwd), + setError: setPasswordErrorRepeatedChar, + }, + { + name: "escapeChar", + validator: (pwd: string) => !escapeCharRegex.test(pwd), + setError: setPasswordErrorEscapeChar, + }, + { + name: "lowEntropy", + validator: (pwd: string) => ( + !lowEntropyRegexes.some(regex => regex.test(pwd)) + ), + setError: setPasswordErrorLowEntropy, + }, + ]; + + const isBreached = await checkIsPasswordBreached(password); + + if (isBreached) { errorCheck = true; + setPasswordErrorBreached(true); } else { - setPasswordErrorTooShort(false); + setPasswordErrorBreached(false); } - // tooLong - if (password.length > 100) { - setPasswordErrorTooLong(true); - errorCheck = true; - } else { - setPasswordErrorTooLong(false); - } - - // upperCase - if (!/[A-Z\u0041-\u005A\u00C0-\u00D6\u00D8-\u00DE]/.test(password)) { - setPasswordErrorUpperCase(true); - errorCheck = true; - } else { - setPasswordErrorUpperCase(false); - } - - // lowerCase - if (!/[a-z\u0061-\u007A\u00DF-\u00F6\u00F8-\u00FF]/.test(password)) { - setPasswordErrorLowerCase(true); - errorCheck = true; - } else { - setPasswordErrorLowerCase(false); - } - - // number - if (!/[0-9]/.test(password)) { - setPasswordErrorNumber(true); - errorCheck = true; - } else { - setPasswordErrorNumber(false); - } - - // specialChar - if ( - !/[!@#$%^&*(),.?":{}|<>\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FFF\u0600-\u06FF\u0400-\u04FF\u0500-\u052F\u2DE0-\u2DFF\uA640-\uA69F\u05B0-\u05FF\u0980-\u09FF\u1F00-\u1FFF\u0130\u015E\u011E\u00D6\u00C7\u00FC\u00FB\u00F6\u00EB\u00E7\u00C7\p{Emoji}]/u.test( - password - ) - ) { - setPasswordErrorSpecialChar(true); - errorCheck = true; - } else { - setPasswordErrorSpecialChar(false); - } - - // repeatedChar - if ( - /([!@#$%^&*(),.?":{}|<>0-9A-Za-z\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FFF\u0600-\u06FF\u0400-\u04FF\u0500-\u052F\u2DE0-\u2DFF\uA640-\uA69F\u05B0-\u05FF\u0980-\u09FF\u1F00-\u1FFF\u0130\u015E\u011E\u00D6\u00C7\u00FC\u00FB\u00F6\u00EB\u00E7\u00C7\u003a-\u003f\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u00FF\p{Emoji}])\1\1/.test( - password - ) - ) { - setPasswordErrorRepeatedChar(true); - errorCheck = true; - } else { - setPasswordErrorRepeatedChar(false); - } - - // isEmail - const emailSchema = string().email(); - - if (await emailSchema.isValid(password)) { - setPasswordErrorIsEmail(true); - errorCheck = true; - } else { - setPasswordErrorIsEmail(false); - } - - // isUrl - const urlSchema = string().url(); - - if (await urlSchema.isValid(password)) { - setPasswordErrorIsUrl(true); - errorCheck = true; - } else { - setPasswordErrorIsUrl(false); - } - - // breachedPassword - if (await checkIsPasswordBreached(password)) { - setPasswordErrorIsBreachedPassword(true); - errorCheck = true; - } else { - setPasswordErrorIsBreachedPassword(false); + for (const test of tests) { + if (!test.validator(password)) { + errorCheck = true; + test.setError(true); + } else { + test.setError(false); + } } return errorCheck; diff --git a/frontend/src/components/utilities/checks/checkPassword.ts b/frontend/src/components/utilities/checks/checkPassword.ts index 5608395a1..b1b72e797 100644 --- a/frontend/src/components/utilities/checks/checkPassword.ts +++ b/frontend/src/components/utilities/checks/checkPassword.ts @@ -1,17 +1,15 @@ -import {string} from "yup" +import { letterCharRegex, numAndSpecialCharRegex, repeatedCharRegex, escapeCharRegex, lowEntropyRegexes } from "./passwordRegexes"; import { checkIsPasswordBreached } from "./checkIsPasswordBreached"; type Errors = { tooShort?: string; tooLong?: string; - upperCase?: string; - lowerCase?: string; - number?: string; - specialChar?: string; + noLetterChar?: string; + noNumOrSpecialChar?: string; repeatedChar?: string; - isEmail?: string; - isUrl?: string; - isBreachedPassword?: string; + escapeChar?: string; + lowEntropy?: string; + breached?: string; }; interface CheckPasswordParams { @@ -23,12 +21,11 @@ interface CheckPasswordParams { * Validate that the password [password]: * - Contains at least 14 characters * - Contains at most 100 characters - * - Contains at least 1 uppercase character (A-Z) - * - Contains at least 1 lowercase character (a-z) - * - Contains at least 1 number (0-9) - * - Contains at least 1 special character + * - Contains at least 1 letter character (many languages supported) (case insensitive) + * - Contains at least 1 number (0-9) or special character (emojis included) * - Does not contain 3 repeat, consecutive characters - * - Is not an email address + * - Does not contain any escape characters/sequences + * - Does not contain PII and/or low entropy data (eg. email address, URL, phone number, DoB, SSN, driver's license, passport) * - Is not in a database of breached passwords * * The function returns whether or not the password [password] @@ -39,82 +36,64 @@ interface CheckPasswordParams { * @param {String} obj.password - the password to check * @param {Function} obj.setErrors - set state function to set error object */ + const checkPassword = async ({ password, setErrors }: CheckPasswordParams): Promise => { const errors: Errors = {}; - // tooShort - if (password.length < 14) { - errors.tooShort = "at least 14 characters"; + const tests = [ + { + name: "tooShort", + validator: (pwd: string) => pwd.length >= 14, + errorText: "at least 14 characters", + }, + { + name: "tooLong", + validator: (pwd: string) => pwd.length < 101, + errorText: "at most 100 characters", + }, + { + name: "noLetterChar", + validator: (pwd: string) => letterCharRegex.test(pwd), + errorText: "at least 1 letter character", + }, + { + name: "noNumOrSpecialChar", + validator: (pwd: string) => numAndSpecialCharRegex.test(pwd), + errorText: "at least 1 number or special character", + }, + { + name: "repeatedChar", + validator: (pwd: string) => !repeatedCharRegex.test(pwd), + errorText: "at most 3 repeated, consecutive characters", + }, + { + name: "escapeChar", + validator: (pwd: string) => !escapeCharRegex.test(pwd), + errorText: "No escape characters allowed.", + }, + { + name: "lowEntropy", + validator: (pwd: string) => ( + !lowEntropyRegexes.some(regex => regex.test(pwd)) + ), + errorText: "Password contains sensitive data.", + }, + ]; + + const isBreached = await checkIsPasswordBreached(password); + + if (isBreached) { + errors.breached = "Password was found in a data breach."; } - // tooLong - if (password.length > 100) { - errors.tooLong = "at most 100 characters"; - } - - // upperCase - // this adds support for the user to select an uppercase character from many major languages - // NB. ES2018 is required to run this - if (!/[A-Z\u0041-\u005A\u00C0-\u00D6\u00D8-\u00DE]/.test(password)) { - errors.upperCase = "at least 1 uppercase character"; // most major langauges supported - } - - // lowerCase - // this adds support for the user to select a lowercase character from many major languages - // NB. ES2018 is required to run this - if (!/[a-z\u0061-\u007A\u00DF-\u00F6\u00F8-\u00FF]/.test(password)) { - errors.lowerCase = "at least 1 lowercase character"; // most major langauges supported - } - - // number - if (!/[0-9]/.test(password)) { - errors.number = "at least 1 number"; - } - - // specialChar - // this adds support for the user to select a special character from many major languages and emojis - // NB. ES2018 is required to run this - if ( - !/[!@#$%^&*(),.?":{}|<>\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FFF\u0600-\u06FF\u0400-\u04FF\u0500-\u052F\u2DE0-\u2DFF\uA640-\uA69F\u05B0-\u05FF\u0980-\u09FF\u1F00-\u1FFF\u0130\u015E\u011E\u00D6\u00C7\u00FC\u00FB\u00F6\u00EB\u00E7\u00C7\p{Emoji}]/u.test( - password - ) - ) { - errors.specialChar = "at least 1 special character (emojis, symbols & non-Latin languages)"; - } - - // repeatedChar - // this prevents the user from selecting repeated characters from many major languages, emojis as well as numbers and symbols - // NB. ES2018 is required to run this - if ( - /([!@#$%^&*(),.?":{}|<>0-9A-Za-z\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FFF\u0600-\u06FF\u0400-\u04FF\u0500-\u052F\u2DE0-\u2DFF\uA640-\uA69F\u05B0-\u05FF\u0980-\u09FF\u1F00-\u1FFF\u0130\u015E\u011E\u00D6\u00C7\u00FC\u00FB\u00F6\u00EB\u00E7\u00C7\u003a-\u003f\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u00FF\p{Emoji}])\1\1/.test( - password - ) - ) { - errors.repeatedChar = "at most 2 repeated, consecutive characters"; - } - - // isEmail - const emailSchema = string().email(); - - if (await emailSchema.isValid(password)) { - errors.isEmail = "The password cannot be an email address"; - } - - // isUrl - const urlSchema = string().url(); - - if (await urlSchema.isValid(password)) { - errors.isUrl = "The password cannot be a URL"; - } - - // breachedPassword - if (await checkIsPasswordBreached(password)) { - errors.isBreachedPassword = - "The new password is in a list of passwords commonly used on other websites. Please try again with a stronger password."; + for (const test of tests) { + if (test.validator && !test.validator(password)) { + errors[test.name as keyof Errors] = test.errorText; + } } setErrors(errors); return Object.keys(errors).length > 0; }; -export default checkPassword; +export default checkPassword; \ No newline at end of file diff --git a/frontend/src/components/utilities/checks/passwordRegexes.ts b/frontend/src/components/utilities/checks/passwordRegexes.ts new file mode 100644 index 000000000..db7367c17 --- /dev/null +++ b/frontend/src/components/utilities/checks/passwordRegexes.ts @@ -0,0 +1,35 @@ +// This regex covers letters (case insensitive) for the top 50 most spoken languages +export const letterCharRegex = /[A-Za-z\u00C0-\u00D6\u00D8-\u00DE\u00DF-\u00F6\u00F8-\u00FF\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FFF\u0600-\u06FF\u0400-\u04FF\u0500-\u052F\u2DE0-\u2DFF\uA640-\uA69F\u05B0-\u05FF\u0980-\u09FF\u1F00-\u1FFF\u0130\u015E\u011E\u00C7\u00FC\u00FB\u00EB\u00E7]/u; + +// This regex covers digits, special characters, symbols, and emojis. +export const numAndSpecialCharRegex = /[\d!@#$%^&*(),.?":{}|<>]|[^\p{L}\p{N}\s]/gu; + +// This regex covers 3 repeated consecutive chars (incl. spaces) +export const repeatedCharRegex = /(.)\1\1\1|\s{4,}/; + +// This regex covers the escape sequences as a precaution +export const escapeCharRegex = /[\n\t\r\\]/; + +// This regex covers some PII and/or low entropy data +export const lowEntropyRegexes = [ + // Email address + /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/, + + // URL (incl. subdomains, paths, top-level domains & query params) + /^(?:(?:https?|ftp):\/\/)?(?:\w+\.)?[a-zA-Z0-9.-]+\.(?:com|org|net|edu)(?:\/\S*)?(?:\?\S*)?$/, + + // Date in various formats + /(\b\d{1,4}[-\/.]?\d{1,2}[-\/.]?\d{1,4}\b)|(\b\d{1,4}[-\/.]?\w{3}[-\/.]?\d{1,4}\b)/, + + // Phone numbers (generalized) + /(?:\+(?:[1-9]\d{0,2})\s?)?(?:\(\d{1,4}\)\s?)?(?:\d[-.\s]?){5,}\d/, + + // Passport numbers (generalized) + /\b(?:[A-Z0-9]{6,9}|[A-Z0-9]{8,9}|[A-Z0-9]{9}|[A-Z0-9]{10,11})\b/, + + // Driver's license numbers (generalized) + /\b(?:[A-Z0-9]{7,10}|[A-Z0-9]{10,11}|[A-Z0-9]{7,10})\b/, + + // US social security number + /\b\d{3}[-\s]?\d{2}[-\s]?\d{4}\b/, +]; \ No newline at end of file diff --git a/frontend/src/pages/password-reset.tsx b/frontend/src/pages/password-reset.tsx index 85875c338..fa83dbdd8 100644 --- a/frontend/src/pages/password-reset.tsx +++ b/frontend/src/pages/password-reset.tsx @@ -30,14 +30,12 @@ export default function PasswordReset() { const [backupKeyError, setBackupKeyError] = useState(false); const [passwordErrorTooShort, setPasswordErrorTooShort] = useState(false); const [passwordErrorTooLong, setPasswordErrorTooLong] = useState(false); - const [passwordErrorUpperCase, setPasswordErrorUpperCase] = useState(false); - const [passwordErrorLowerCase, setPasswordErrorLowerCase] = useState(false); - const [passwordErrorNumber, setPasswordErrorNumber] = useState(false); - const [passwordErrorSpecialChar, setPasswordErrorSpecialChar] = useState(false); + const [passwordErrorNoLetterChar, setPasswordErrorNoLetterChar] = useState(false); + const [passwordErrorNoNumOrSpecialChar, setPasswordErrorNoNumOrSpecialChar] = useState(false); const [passwordErrorRepeatedChar, setPasswordErrorRepeatedChar] = useState(false); - const [passwordErrorIsEmail, setPasswordErrorIsEmail] = useState(false); - const [passwordErrorIsUrl, setPasswordErrorIsUrl] = useState(false); - const [passwordErrorIsBreachedPassword, setPasswordErrorIsBreachedPassword] = useState(false); + const [passwordErrorEscapeChar, setPasswordErrorEscapeChar] = useState(false); + const [passwordErrorLowEntropy, setPasswordErrorLowEntropy] = useState(false); + const [passwordErrorBreached, setPasswordErrorBreached] = useState(false); const router = useRouter(); @@ -76,14 +74,12 @@ export default function PasswordReset() { password: newPassword, setPasswordErrorTooShort, setPasswordErrorTooLong, - setPasswordErrorUpperCase, - setPasswordErrorLowerCase, - setPasswordErrorNumber, - setPasswordErrorSpecialChar, + setPasswordErrorNoLetterChar, + setPasswordErrorNoNumOrSpecialChar, setPasswordErrorRepeatedChar, - setPasswordErrorIsEmail, - setPasswordErrorIsUrl, - setPasswordErrorIsBreachedPassword, + setPasswordErrorEscapeChar, + setPasswordErrorLowEntropy, + setPasswordErrorBreached, errorCheck: false }); @@ -227,7 +223,7 @@ export default function PasswordReset() {

- Make sure you save it somewhere save. + Make sure you save it somewhere safe.

@@ -239,14 +235,12 @@ export default function PasswordReset() { password, setPasswordErrorTooShort, setPasswordErrorTooLong, - setPasswordErrorUpperCase, - setPasswordErrorLowerCase, - setPasswordErrorNumber, - setPasswordErrorSpecialChar, + setPasswordErrorNoLetterChar, + setPasswordErrorNoNumOrSpecialChar, setPasswordErrorRepeatedChar, - setPasswordErrorIsEmail, - setPasswordErrorIsUrl, - setPasswordErrorIsBreachedPassword, + setPasswordErrorEscapeChar, + setPasswordErrorLowEntropy, + setPasswordErrorBreached, errorCheck: false }); }} @@ -256,14 +250,12 @@ export default function PasswordReset() { error={ passwordErrorTooShort && passwordErrorTooLong && - passwordErrorUpperCase && - passwordErrorLowerCase && - passwordErrorNumber && - passwordErrorSpecialChar && + passwordErrorNoLetterChar && + passwordErrorNoNumOrSpecialChar && passwordErrorRepeatedChar && - passwordErrorIsEmail && - passwordErrorIsUrl && - passwordErrorIsBreachedPassword + passwordErrorEscapeChar && + passwordErrorLowEntropy && + passwordErrorBreached } autoComplete="new-password" id="new-password" @@ -271,14 +263,12 @@ export default function PasswordReset() {
{passwordErrorTooShort || passwordErrorTooLong || - passwordErrorUpperCase || - passwordErrorLowerCase || - passwordErrorNumber || - passwordErrorSpecialChar || + passwordErrorNoLetterChar || + passwordErrorNoNumOrSpecialChar || passwordErrorRepeatedChar || - passwordErrorIsEmail || - passwordErrorIsUrl || - passwordErrorIsBreachedPassword ? ( + passwordErrorEscapeChar || + passwordErrorLowEntropy || + passwordErrorBreached ? (
Password should contain:
@@ -302,53 +292,30 @@ export default function PasswordReset() {
- {passwordErrorUpperCase ? ( + {passwordErrorNoLetterChar ? ( ) : ( )}
- at least 1 uppercase character + at least 1 letter character
- {passwordErrorLowerCase ? ( + {passwordErrorNoNumOrSpecialChar ? ( ) : ( )}
- at least 1 lowercase character + at least 1 number or special character
- {passwordErrorNumber ? ( - - ) : ( - - )} -
- at least 1 number -
-
- {passwordErrorSpecialChar ? ( - - ) : ( - - )} -
- at least 1 special character (emojis and many langauge scripts supported) -
-
-
{passwordErrorRepeatedChar ? ( ) : ( @@ -359,48 +326,48 @@ export default function PasswordReset() { passwordErrorRepeatedChar ? "text-gray-400" : "text-gray-600" } text-sm`} > - at most 2 repeated, consecutive characters + at most 3 repeated, consecutive characters
-
-
- {passwordErrorIsEmail ? ( - - ) : ( - - )} -
- The password cannot be an email address. -
-
-
- {passwordErrorIsUrl ? ( - - ) : ( - - )} -
- The password cannot be a URL. -
-
-
- {passwordErrorIsBreachedPassword ? ( +
+
+ {passwordErrorEscapeChar ? ( ) : ( )}
- The new password is in a list of passwords commonly used on other websites. Please - try again with a stronger password. + No escape characters allowed. +
+
+
+ {passwordErrorLowEntropy ? ( + + ) : ( + + )} +
+ Password contains sensitive data. +
+
+
+ {passwordErrorBreached ? ( + + ) : ( + + )} +
+ Password was found in a data breach.
-
) : ( diff --git a/frontend/src/pages/signupinvite.tsx b/frontend/src/pages/signupinvite.tsx index 105420543..c7b04184e 100644 --- a/frontend/src/pages/signupinvite.tsx +++ b/frontend/src/pages/signupinvite.tsx @@ -31,14 +31,12 @@ const client = new jsrp.client(); type Errors = { tooShort?: string; tooLong?: string; - upperCase?: string; - lowerCase?: string; - number?: string; - specialChar?: string; + noLetterChar?: string; + noNumOrSpecialChar?: string; repeatedChar?: string; - isEmail?: string; - isUrl?: string; - breachedPassword?: string; + escapeChar?: string; + lowEntropy?: string; + breached?: string; }; export default function SignupInvite() { diff --git a/frontend/src/views/Settings/PersonalSettingsPage/ChangePasswordSection/ChangePasswordSection.tsx b/frontend/src/views/Settings/PersonalSettingsPage/ChangePasswordSection/ChangePasswordSection.tsx index 69a8d7d4f..de83f0c0f 100644 --- a/frontend/src/views/Settings/PersonalSettingsPage/ChangePasswordSection/ChangePasswordSection.tsx +++ b/frontend/src/views/Settings/PersonalSettingsPage/ChangePasswordSection/ChangePasswordSection.tsx @@ -15,14 +15,12 @@ import { useUser } from "@app/context"; type Errors = { tooShort?: string; tooLong?: string; - upperCase?: string; - lowerCase?: string; - number?: string; - specialChar?: string; + noLetterChar?: string; + noNumOrSpecialChar?: string; repeatedChar?: string; - isEmail?: string; - isUrl?: string; - isBreachedPassword?: string; + escapeChar?: string; + lowEntropy?: string; + breached?: string; }; const schema = yup diff --git a/frontend/src/views/Signup/components/UserInfoSSOStep/UserInfoSSOStep.tsx b/frontend/src/views/Signup/components/UserInfoSSOStep/UserInfoSSOStep.tsx index 93143578f..79c502583 100644 --- a/frontend/src/views/Signup/components/UserInfoSSOStep/UserInfoSSOStep.tsx +++ b/frontend/src/views/Signup/components/UserInfoSSOStep/UserInfoSSOStep.tsx @@ -35,14 +35,12 @@ type Props = { type Errors = { tooShort?: string; tooLong?: string; - upperCase?: string; - lowerCase?: string; - number?: string; - specialChar?: string; + noLetterChar?: string; + noNumOrSpecialChar?: string; repeatedChar?: string; - isEmail?: string; - isUrl?: string; - isBeachedPassword?: string; + escapeChar?: string; + lowEntropy?: string; + breached?: string; }; /**