From 4d6a8f0476cba604ad4cc69cfd80e2c8470b053e Mon Sep 17 00:00:00 2001
From: Joel Biddle
Date: Fri, 25 Aug 2023 01:44:02 +1000
Subject: [PATCH] Fixed form (error messages too long). Consolidated tests &
errors. Moved regexes to another file. Added regex to check for PII & reject
pwd if true. Confirmed hashing & encryption/decryption works with top 50
languages, emojis etc (screen videos & unit tests to come).
---
frontend/public/locales/en/translations.json | 18 +-
frontend/public/locales/es/translations.json | 18 +-
frontend/public/locales/fr/translations.json | 20 +--
frontend/public/locales/ko/translations.json | 20 +--
.../public/locales/pt-BR/translations.json | 18 +-
frontend/public/locales/tr/translations.json | 20 +--
.../src/components/signup/UserInfoStep.tsx | 12 +-
.../utilities/checks/PasswordCheck.ts | 168 +++++++-----------
.../utilities/checks/checkPassword.ts | 141 +++++++--------
.../utilities/checks/passwordRegexes.ts | 35 ++++
frontend/src/pages/password-reset.tsx | 161 +++++++----------
frontend/src/pages/signupinvite.tsx | 12 +-
.../ChangePasswordSection.tsx | 12 +-
.../UserInfoSSOStep/UserInfoSSOStep.tsx | 12 +-
14 files changed, 293 insertions(+), 374 deletions(-)
create mode 100644 frontend/src/components/utilities/checks/passwordRegexes.ts
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;
};
/**