From 534d96ffb67c090b79934ac59b3820a7049fae73 Mon Sep 17 00:00:00 2001 From: Joel Biddle Date: Tue, 22 Aug 2023 14:05:00 +1000 Subject: [PATCH 01/33] Set max password length (100 chars) to help prevent DDOS attack --- frontend/public/locales/en/translations.json | 9 ++--- frontend/public/locales/fr/translations.json | 9 ++--- .../src/components/signup/UserInfoStep.tsx | 3 +- .../utilities/checks/PasswordCheck.ts | 17 ++++++--- .../utilities/checks/checkPassword.ts | 30 +++++++++------- frontend/src/pages/password-reset.tsx | 35 +++++++++++++------ .../ChangePasswordSection.tsx | 3 +- 7 files changed, 69 insertions(+), 37 deletions(-) diff --git a/frontend/public/locales/en/translations.json b/frontend/public/locales/en/translations.json index a70a7dd8b..506495b3f 100644 --- a/frontend/public/locales/en/translations.json +++ b/frontend/public/locales/en/translations.json @@ -231,10 +231,11 @@ "current": "Current password", "current-wrong": "The current password may be wrong", "new": "New password", - "validate-base": "Password should contain at least:", - "validate-length": "14 characters", - "validate-case": "1 lowercase character", - "validate-number": "1 number" + "validate-base": "Password should contain:", + "validate-too-short": "at least 14 characters", + "validate-too-long": "at most 100 characters", + "validate-case": "at least 1 lowercase character", + "validate-number": "at least 1 number" }, "token": { "service-tokens": "Service Tokens", diff --git a/frontend/public/locales/fr/translations.json b/frontend/public/locales/fr/translations.json index 6914e7ea1..49edd33fa 100644 --- a/frontend/public/locales/fr/translations.json +++ b/frontend/public/locales/fr/translations.json @@ -215,10 +215,11 @@ "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 au moins:", - "validate-length": "14 caractères", - "validate-case": "1 caractère miniscule", - "validate-number": "1 chiffre" + "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-case": "au moins 1 caractère miniscule", + "validate-number": "au moins 1 chiffre" }, "token": { "service-tokens": "Jetons de service", diff --git a/frontend/src/components/signup/UserInfoStep.tsx b/frontend/src/components/signup/UserInfoStep.tsx index db4d040ad..3123950c3 100644 --- a/frontend/src/components/signup/UserInfoStep.tsx +++ b/frontend/src/components/signup/UserInfoStep.tsx @@ -39,7 +39,8 @@ interface UserInfoStepProps { } type Errors = { - length?: string, + tooShort?: string, + tooLong?: string, upperCase?: string, lowerCase?: string, number?: string, diff --git a/frontend/src/components/utilities/checks/PasswordCheck.ts b/frontend/src/components/utilities/checks/PasswordCheck.ts index 5fb9dfe2c..cd75b53f9 100644 --- a/frontend/src/components/utilities/checks/PasswordCheck.ts +++ b/frontend/src/components/utilities/checks/PasswordCheck.ts @@ -2,7 +2,8 @@ interface PasswordCheckProps { password: string; errorCheck: boolean; - setPasswordErrorLength: (value: boolean) => void; + setPasswordErrorTooShort: (value: boolean) => void; + setPasswordErrorTooLong: (value: boolean) => void; setPasswordErrorNumber: (value: boolean) => void; setPasswordErrorLowerCase: (value: boolean) => void; } @@ -12,17 +13,25 @@ interface PasswordCheckProps { */ const passwordCheck = ({ password, - setPasswordErrorLength, + setPasswordErrorTooShort, setPasswordErrorNumber, setPasswordErrorLowerCase, + setPasswordErrorTooLong, errorCheck }: PasswordCheckProps) => { if (!password || password.length < 14) { - setPasswordErrorLength(true); + setPasswordErrorTooShort(true); errorCheck = true; } else { - setPasswordErrorLength(false); + setPasswordErrorTooShort(false); + } + + if (password.length > 100) { + setPasswordErrorTooLong(true); + errorCheck = true; + } else { + setPasswordErrorTooLong(false); } if (!/\d/.test(password)) { diff --git a/frontend/src/components/utilities/checks/checkPassword.ts b/frontend/src/components/utilities/checks/checkPassword.ts index 69dba2397..7f8b5d45f 100644 --- a/frontend/src/components/utilities/checks/checkPassword.ts +++ b/frontend/src/components/utilities/checks/checkPassword.ts @@ -1,5 +1,6 @@ type Errors = { - length?: string, + tooShort?: string, + tooLong?: string, upperCase?: string, lowerCase?: string, number?: string, @@ -15,11 +16,12 @@ interface CheckPasswordParams { } /** - * Validate that the password [password] is at least: - * - 8 characters long - * - Contains 1 uppercase character (A-Z) - * - Contains 1 lowercase character (a-z) - * - Contains 1 number (0-9) + * Validate that the password [password]: + * - Contains at least 14 characters long + * - Contains at most 100 characters long + * - Contains at least 1 uppercase character (A-Z) + * - Contains at least 1 lowercase character (a-z) + * - Contains at least 1 number (0-9) * - Does not contain 3 repeat, consecutive characters * * The function returns whether or not the password [password] @@ -37,24 +39,28 @@ const checkPassword = ({ }: CheckPasswordParams): boolean => { const errors: Errors = {}; - if (password.length < 8) { - errors.length = "8 characters"; + if (password.length < 14) { + errors.tooShort = "at least 14 characters"; + } + + if (password.length > 100) { + errors.tooLong = "at most 100 characters"; } if (!/[A-Z]/.test(password)) { - errors.upperCase = "1 uppercase character (A-Z)"; + errors.upperCase = "at least 1 uppercase character (A-Z)"; } if (!/[a-z]/.test(password)) { - errors.lowerCase = "1 lowercase character (a-z)"; + errors.lowerCase = "at least 1 lowercase character (a-z)"; } if (!/[0-9]/.test(password)) { - errors.number = "1 number (0-9)"; + errors.number = "at least 1 number (0-9)"; } if (!/[!@#$%^&*(),.?":{}|<>]/.test(password)) { - errors.specialChar = "1 special character (!@#$%^&*(),.?)"; + errors.specialChar = "at least 1 special character (!@#$%^&*(),.?)"; } if (/([A-Za-z0-9])\1\1\1/.test(password)) { diff --git a/frontend/src/pages/password-reset.tsx b/frontend/src/pages/password-reset.tsx index b38f67c01..90fd6be51 100644 --- a/frontend/src/pages/password-reset.tsx +++ b/frontend/src/pages/password-reset.tsx @@ -28,7 +28,8 @@ export default function PasswordReset() { const [privateKey, setPrivateKey] = useState(""); const [newPassword, setNewPassword] = useState(""); const [backupKeyError, setBackupKeyError] = useState(false); - const [passwordErrorLength, setPasswordErrorLength] = useState(false); + const [passwordErrorTooShort, setPasswordErrorTooShort] = useState(false); + const [passwordErrorTooLong, setPasswordErrorTooLong] = useState(false); const [passwordErrorNumber, setPasswordErrorNumber] = useState(false); const [passwordErrorLowerCase, setPasswordErrorLowerCase] = useState(false); @@ -67,7 +68,8 @@ export default function PasswordReset() { e.preventDefault(); const errorCheck = passwordCheck({ password: newPassword, - setPasswordErrorLength, + setPasswordErrorTooShort, + setPasswordErrorTooLong, setPasswordErrorNumber, setPasswordErrorLowerCase, errorCheck: false @@ -221,7 +223,8 @@ export default function PasswordReset() { setNewPassword(password); passwordCheck({ password, - setPasswordErrorLength, + setPasswordErrorTooShort, + setPasswordErrorTooLong, setPasswordErrorNumber, setPasswordErrorLowerCase, errorCheck: false @@ -230,22 +233,32 @@ export default function PasswordReset() { type="password" value={newPassword} isRequired - error={passwordErrorLength && passwordErrorLowerCase && passwordErrorNumber} + error={passwordErrorTooShort && passwordErrorTooLong && passwordErrorLowerCase && passwordErrorNumber} autoComplete="new-password" id="new-password" /> - {passwordErrorLength || passwordErrorLowerCase || passwordErrorNumber ? ( + {passwordErrorTooShort || passwordErrorTooLong || passwordErrorLowerCase || passwordErrorNumber ? (
-
Password should contain at least:
+
Password should contain:
- {passwordErrorLength ? ( + {passwordErrorTooShort ? ( ) : ( )} -
- 14 characters +
+ at least 14 characters +
+
+
+ {passwordErrorTooLong ? ( + + ) : ( + + )} +
+ at most 100 characters
@@ -257,7 +270,7 @@ export default function PasswordReset() {
- 1 lowercase character + at least 1 lowercase character
@@ -267,7 +280,7 @@ export default function PasswordReset() { )}
- 1 number + at least 1 number
diff --git a/frontend/src/views/Settings/PersonalSettingsPage/ChangePasswordSection/ChangePasswordSection.tsx b/frontend/src/views/Settings/PersonalSettingsPage/ChangePasswordSection/ChangePasswordSection.tsx index fefa7516e..c873395c4 100644 --- a/frontend/src/views/Settings/PersonalSettingsPage/ChangePasswordSection/ChangePasswordSection.tsx +++ b/frontend/src/views/Settings/PersonalSettingsPage/ChangePasswordSection/ChangePasswordSection.tsx @@ -18,7 +18,8 @@ import { useUser } from "@app/context"; import { useGetCommonPasswords } from "@app/hooks/api"; type Errors = { - length?: string, + tooShort?: string, + tooLong?: string, upperCase?: string, lowerCase?: string, number?: string, From 0d1aa713eac2ea663040f4057abf2d428285056d Mon Sep 17 00:00:00 2001 From: Joel Biddle Date: Tue, 22 Aug 2023 14:57:02 +1000 Subject: [PATCH 02/33] added translations for error messges (used Google translate) --- frontend/public/locales/es/translations.json | 9 +++++---- frontend/public/locales/ko/translations.json | 3 ++- frontend/public/locales/pt-BR/translations.json | 9 +++++---- frontend/public/locales/tr/translations.json | 9 +++++---- 4 files changed, 17 insertions(+), 13 deletions(-) diff --git a/frontend/public/locales/es/translations.json b/frontend/public/locales/es/translations.json index e734fb3f2..28e2793cb 100644 --- a/frontend/public/locales/es/translations.json +++ b/frontend/public/locales/es/translations.json @@ -228,10 +228,11 @@ "current": "Contraseña actual", "current-wrong": "La contraseña actual puede puede que sea incorrecta", "new": "Nueva contraseña", - "validate-base": "La contraseña debe contener como mínimo:", - "validate-length": "14 caracteres", - "validate-case": "1 letra en minúsculas", - "validate-number": "1 número" + "validate-base": "La contraseña debe contener:", + "validate-too-short": "como mínimo 14 caracteres", + "validate-too-long": "como máximo 100 caracteres", + "validate-case": "como mínimo 1 letra en minúsculas", + "validate-number": "como mínimo 1 número" }, "token": { "service-tokens": "Tokens de servicio", diff --git a/frontend/public/locales/ko/translations.json b/frontend/public/locales/ko/translations.json index eea81f37e..d29a5846c 100644 --- a/frontend/public/locales/ko/translations.json +++ b/frontend/public/locales/ko/translations.json @@ -183,7 +183,8 @@ "new": "새 비밀번호", "current-wrong": "현재 비밀번호가 잘못되었어요", "validate-base": "비밀번호는 다음 조건을 만족해야 합니다:", - "validate-length": "14 글자 이상", + "validate-too-short": "14 글자 이상", + "validate-too-long": "100 자 이하", "validate-case": "1개 이상의 소문자", "validate-number": "1개 이상의 숫자" }, diff --git a/frontend/public/locales/pt-BR/translations.json b/frontend/public/locales/pt-BR/translations.json index 5b53ce503..e04de50da 100644 --- a/frontend/public/locales/pt-BR/translations.json +++ b/frontend/public/locales/pt-BR/translations.json @@ -210,10 +210,11 @@ "current": "Senha atual", "current-wrong": "A senha atual pode estar errada", "new": "Nova Senha", - "validate-base": "A senha deve conter pelo menos:", - "validate-length": "14 caracteres", - "validate-case": "1 caractere minúsculo", - "validate-number": "1 número" + "validate-base": "A senha deve conter:", + "validate-too-short": "pelo menos 14 caracteres", + "validate-too-long": "no máximo 100 caracteres", + "validate-case": "pelo menos 1 caractere minúsculo", + "validate-number": "pelo menos 1 número" }, "token": { "service-tokens": "Tokens de Serviço", diff --git a/frontend/public/locales/tr/translations.json b/frontend/public/locales/tr/translations.json index 706998495..c98399f7f 100644 --- a/frontend/public/locales/tr/translations.json +++ b/frontend/public/locales/tr/translations.json @@ -228,10 +228,11 @@ "current": "Mevcut şifre", "current-wrong": "Mevcut şifre yanlış olabilir", "new": "Yeni şifre", - "validate-base": "Şifre en az şunları içermelidir:", - "validate-length": "14 karakter", - "validate-case": "1 küçük harf", - "validate-number": "1 rakam" + "validate-base": "Şifre kısıtlamaları:", + "validate-too-short": "en az 14 karakter", + "validate-too-long": "en fazla 100 karakter", + "validate-case": "en az 1 küçük harf", + "validate-number": "en az 1 rakam" }, "token": { "service-tokens": "Servis Belirteçleri", From fbeb2109657f4cee8740ff8420289e038da25dbc Mon Sep 17 00:00:00 2001 From: Joel Biddle Date: Tue, 22 Aug 2023 15:34:45 +1000 Subject: [PATCH 03/33] add to pwd length issue --- .../Signup/components/UserInfoSSOStep/UserInfoSSOStep.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/views/Signup/components/UserInfoSSOStep/UserInfoSSOStep.tsx b/frontend/src/views/Signup/components/UserInfoSSOStep/UserInfoSSOStep.tsx index 65be231b9..d5703c361 100644 --- a/frontend/src/views/Signup/components/UserInfoSSOStep/UserInfoSSOStep.tsx +++ b/frontend/src/views/Signup/components/UserInfoSSOStep/UserInfoSSOStep.tsx @@ -35,7 +35,8 @@ type Props = { } type Errors = { - length?: string, + tooShort?: string, + tooLong?: string, upperCase?: string, lowerCase?: string, number?: string, From 0eb21919fb83371cc34dce9caad792d8c78f93ca Mon Sep 17 00:00:00 2001 From: Joel Biddle Date: Tue, 22 Aug 2023 16:49:17 +1000 Subject: [PATCH 04/33] Password breach check --- .../src/components/signup/UserInfoStep.tsx | 3 +- .../checks/checkIsPasswordBreached.ts | 51 +++++++++++++++++++ .../utilities/checks/checkPassword.ts | 17 +++++-- frontend/src/pages/signupinvite.tsx | 3 +- .../ChangePasswordSection.tsx | 5 +- .../UserInfoSSOStep/UserInfoSSOStep.tsx | 3 +- 6 files changed, 73 insertions(+), 9 deletions(-) create mode 100644 frontend/src/components/utilities/checks/checkIsPasswordBreached.ts diff --git a/frontend/src/components/signup/UserInfoStep.tsx b/frontend/src/components/signup/UserInfoStep.tsx index 3123950c3..581298c42 100644 --- a/frontend/src/components/signup/UserInfoStep.tsx +++ b/frontend/src/components/signup/UserInfoStep.tsx @@ -46,6 +46,7 @@ type Errors = { number?: string, specialChar?: string, repeatedChar?: string, + breachedPassword?: string }; /** @@ -101,7 +102,7 @@ export default function UserInfoStep({ setOrganizationNameError(false); } - errorCheck = checkPassword({ + errorCheck = await checkPassword({ password, commonPasswords, setErrors diff --git a/frontend/src/components/utilities/checks/checkIsPasswordBreached.ts b/frontend/src/components/utilities/checks/checkIsPasswordBreached.ts new file mode 100644 index 000000000..8852ada2f --- /dev/null +++ b/frontend/src/components/utilities/checks/checkIsPasswordBreached.ts @@ -0,0 +1,51 @@ +import axios from "axios"; +import crypto from "crypto"; + +///// REMINDER: ensure all logs are deleted!!! ///// + +export const checkIsPasswordBreached = async (password: string) => { + const dataBreachCheckAPIBaseURL = "https://api.pwnedpasswords.com/range/"; + try { + const textEncoder = new TextEncoder(); + + const encodedPwd = textEncoder.encode(password); + console.log("encodedPwd:", encodedPwd); // delete later!!! + + const hashBuffer = await crypto.subtle.digest("SHA-1", encodedPwd); + console.log("hashBuffer:", hashBuffer); // delete later!!! + + const hashedPwd = Array.from(new Uint8Array(hashBuffer)) + .map((byte) => byte.toString(16).padStart(2, "0")) + .join("") + .toUpperCase(); + + console.log("hashedPwd:", hashedPwd); // delete later!!! + + const response = await axios.get( + `${dataBreachCheckAPIBaseURL}${hashedPwd.slice(0, 5)}` + ); + console.log("response:", response); // delete later!!! + + const responseData = response.data.toUpperCase(); + console.log("responseData:", responseData); // delete later!!! + + const isBreachedPassword = responseData.includes(hashedPwd.slice(5, 40)); + console.log("isBreachedPassword:", isBreachedPassword); // delete later!!! + + // Clear the hashed password from memory + crypto.subtle.digest("SHA-1", encodedPwd); + + return isBreachedPassword; + } catch (err: any) { + if ( + axios.isAxiosError(err) && + err.response && + err.response.status === 429 + ) { + console.error("Received a 429 response from the Pwnd Passwords API"); + // Handle the 429 error here + } else { + console.error(err); + } + } +}; diff --git a/frontend/src/components/utilities/checks/checkPassword.ts b/frontend/src/components/utilities/checks/checkPassword.ts index 7f8b5d45f..0ef1e949b 100644 --- a/frontend/src/components/utilities/checks/checkPassword.ts +++ b/frontend/src/components/utilities/checks/checkPassword.ts @@ -1,3 +1,5 @@ +import { checkIsPasswordBreached } from "./checkIsPasswordBreached"; + type Errors = { tooShort?: string, tooLong?: string, @@ -6,7 +8,8 @@ type Errors = { number?: string, specialChar?: string, repeatedChar?: string, - commonPassword?: string + commonPassword?: string, + breachedPassword?: string }; interface CheckPasswordParams { @@ -32,13 +35,15 @@ interface CheckPasswordParams { * @param {String} obj.password - the password to check * @param {Function} obj.setErrors - set state function to set error object */ -const checkPassword = ({ +const checkPassword = async ({ password, commonPasswords, setErrors -}: CheckPasswordParams): boolean => { +}: CheckPasswordParams): Promise => { const errors: Errors = {}; - + + const isBreachedPassword = await checkIsPasswordBreached(password) + if (password.length < 14) { errors.tooShort = "at least 14 characters"; } @@ -70,6 +75,10 @@ const checkPassword = ({ if (commonPasswords.includes(password)) { errors.commonPassword = "No common passwords"; } + + if (isBreachedPassword) { + errors.breachedPassword = "The password you provided is in a list of passwords commonly used on other websites. Please try again with a stronger password."; + } setErrors(errors); return Object.keys(errors).length > 0; diff --git a/frontend/src/pages/signupinvite.tsx b/frontend/src/pages/signupinvite.tsx index 9b1fe990f..894c36ea6 100644 --- a/frontend/src/pages/signupinvite.tsx +++ b/frontend/src/pages/signupinvite.tsx @@ -41,6 +41,7 @@ type Errors = { number?: string, specialChar?: string, repeatedChar?: string, + breachedPassword?: string }; export default function SignupInvite() { @@ -80,7 +81,7 @@ export default function SignupInvite() { setLastNameError(false); } - errorCheck = checkPassword({ + errorCheck = await checkPassword({ password, commonPasswords, setErrors diff --git a/frontend/src/views/Settings/PersonalSettingsPage/ChangePasswordSection/ChangePasswordSection.tsx b/frontend/src/views/Settings/PersonalSettingsPage/ChangePasswordSection/ChangePasswordSection.tsx index c873395c4..e17864485 100644 --- a/frontend/src/views/Settings/PersonalSettingsPage/ChangePasswordSection/ChangePasswordSection.tsx +++ b/frontend/src/views/Settings/PersonalSettingsPage/ChangePasswordSection/ChangePasswordSection.tsx @@ -25,6 +25,7 @@ type Errors = { number?: string, specialChar?: string, repeatedChar?: string, + breachedPassword?: string }; const schema = yup.object({ @@ -53,8 +54,8 @@ export const ChangePasswordSection = () => { try { if (!user?.email) return; if (!commonPasswords) return; - - const errorCheck = checkPassword({ + + const errorCheck = await checkPassword({ password: newPassword, commonPasswords, setErrors diff --git a/frontend/src/views/Signup/components/UserInfoSSOStep/UserInfoSSOStep.tsx b/frontend/src/views/Signup/components/UserInfoSSOStep/UserInfoSSOStep.tsx index d5703c361..c44fab790 100644 --- a/frontend/src/views/Signup/components/UserInfoSSOStep/UserInfoSSOStep.tsx +++ b/frontend/src/views/Signup/components/UserInfoSSOStep/UserInfoSSOStep.tsx @@ -42,6 +42,7 @@ type Errors = { number?: string, specialChar?: string, repeatedChar?: string, + breachedPassword?: string }; /** @@ -99,7 +100,7 @@ export const UserInfoSSOStep = ({ setOrganizationNameError(false); } - errorCheck = checkPassword({ + errorCheck = await checkPassword({ password, commonPasswords, setErrors From 20f34b4764ca1182269698569828722643ec402c Mon Sep 17 00:00:00 2001 From: Joel Biddle Date: Tue, 22 Aug 2023 17:14:18 +1000 Subject: [PATCH 05/33] removed async in crypto.subtle --- .../components/utilities/checks/checkIsPasswordBreached.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/utilities/checks/checkIsPasswordBreached.ts b/frontend/src/components/utilities/checks/checkIsPasswordBreached.ts index 8852ada2f..5ae7ef7a6 100644 --- a/frontend/src/components/utilities/checks/checkIsPasswordBreached.ts +++ b/frontend/src/components/utilities/checks/checkIsPasswordBreached.ts @@ -6,12 +6,15 @@ import crypto from "crypto"; export const checkIsPasswordBreached = async (password: string) => { const dataBreachCheckAPIBaseURL = "https://api.pwnedpasswords.com/range/"; try { + + console.log("password:", password); // delete later!!! + const textEncoder = new TextEncoder(); const encodedPwd = textEncoder.encode(password); console.log("encodedPwd:", encodedPwd); // delete later!!! - const hashBuffer = await crypto.subtle.digest("SHA-1", encodedPwd); + const hashBuffer = crypto.subtle.digest("SHA-1", encodedPwd); // removed async console.log("hashBuffer:", hashBuffer); // delete later!!! const hashedPwd = Array.from(new Uint8Array(hashBuffer)) From e855d4a0baa3b296c5cc5c479b94a424379bc662 Mon Sep 17 00:00:00 2001 From: Joel Biddle Date: Tue, 22 Aug 2023 17:26:00 +1000 Subject: [PATCH 06/33] added types for crypto --- frontend/package-lock.json | 2 +- frontend/package.json | 2 +- .../utilities/checks/checkIsPasswordBreached.ts | 15 ++++----------- 3 files changed, 6 insertions(+), 13 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 568410bb2..990b03377 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -102,7 +102,7 @@ "@storybook/testing-library": "^0.2.0", "@tailwindcss/typography": "^0.5.4", "@types/jsrp": "^0.2.4", - "@types/node": "18.11.9", + "@types/node": "^18.11.9", "@types/react": "^18.0.26", "@types/sanitize-html": "^2.9.0", "@typescript-eslint/eslint-plugin": "^5.48.1", diff --git a/frontend/package.json b/frontend/package.json index 9aa355d4b..fab8b7033 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -110,7 +110,7 @@ "@storybook/testing-library": "^0.2.0", "@tailwindcss/typography": "^0.5.4", "@types/jsrp": "^0.2.4", - "@types/node": "18.11.9", + "@types/node": "^18.11.9", "@types/react": "^18.0.26", "@types/sanitize-html": "^2.9.0", "@typescript-eslint/eslint-plugin": "^5.48.1", diff --git a/frontend/src/components/utilities/checks/checkIsPasswordBreached.ts b/frontend/src/components/utilities/checks/checkIsPasswordBreached.ts index 5ae7ef7a6..2d19d20d2 100644 --- a/frontend/src/components/utilities/checks/checkIsPasswordBreached.ts +++ b/frontend/src/components/utilities/checks/checkIsPasswordBreached.ts @@ -1,12 +1,11 @@ import axios from "axios"; -import crypto from "crypto"; +import crypto from "crypto"; // added types from @types/node ///// REMINDER: ensure all logs are deleted!!! ///// export const checkIsPasswordBreached = async (password: string) => { const dataBreachCheckAPIBaseURL = "https://api.pwnedpasswords.com/range/"; try { - console.log("password:", password); // delete later!!! const textEncoder = new TextEncoder(); @@ -14,7 +13,7 @@ export const checkIsPasswordBreached = async (password: string) => { const encodedPwd = textEncoder.encode(password); console.log("encodedPwd:", encodedPwd); // delete later!!! - const hashBuffer = crypto.subtle.digest("SHA-1", encodedPwd); // removed async + const hashBuffer = await crypto.subtle.digest("SHA-1", encodedPwd); // returns promise console.log("hashBuffer:", hashBuffer); // delete later!!! const hashedPwd = Array.from(new Uint8Array(hashBuffer)) @@ -24,9 +23,7 @@ export const checkIsPasswordBreached = async (password: string) => { console.log("hashedPwd:", hashedPwd); // delete later!!! - const response = await axios.get( - `${dataBreachCheckAPIBaseURL}${hashedPwd.slice(0, 5)}` - ); + const response = await axios.get(`${dataBreachCheckAPIBaseURL}${hashedPwd.slice(0, 5)}`); console.log("response:", response); // delete later!!! const responseData = response.data.toUpperCase(); @@ -40,11 +37,7 @@ export const checkIsPasswordBreached = async (password: string) => { return isBreachedPassword; } catch (err: any) { - if ( - axios.isAxiosError(err) && - err.response && - err.response.status === 429 - ) { + if (axios.isAxiosError(err) && err.response && err.response.status === 429) { console.error("Received a 429 response from the Pwnd Passwords API"); // Handle the 429 error here } else { From d6222d5ceed2f789cee43af2b0e24e4b1d0471c1 Mon Sep 17 00:00:00 2001 From: Joel Biddle Date: Tue, 22 Aug 2023 17:33:35 +1000 Subject: [PATCH 07/33] attempt to fix crypto.subtle issue --- .../utilities/checks/checkIsPasswordBreached.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/utilities/checks/checkIsPasswordBreached.ts b/frontend/src/components/utilities/checks/checkIsPasswordBreached.ts index 2d19d20d2..b9d93b55c 100644 --- a/frontend/src/components/utilities/checks/checkIsPasswordBreached.ts +++ b/frontend/src/components/utilities/checks/checkIsPasswordBreached.ts @@ -13,10 +13,10 @@ export const checkIsPasswordBreached = async (password: string) => { const encodedPwd = textEncoder.encode(password); console.log("encodedPwd:", encodedPwd); // delete later!!! - const hashBuffer = await crypto.subtle.digest("SHA-1", encodedPwd); // returns promise - console.log("hashBuffer:", hashBuffer); // delete later!!! + const hash = crypto.createHash("sha1").update(encodedPwd).digest(); + console.log("hash:", hash); // delete later!!! - const hashedPwd = Array.from(new Uint8Array(hashBuffer)) + const hashedPwd = Array.from(new Uint8Array(hash)) .map((byte) => byte.toString(16).padStart(2, "0")) .join("") .toUpperCase(); @@ -33,7 +33,8 @@ export const checkIsPasswordBreached = async (password: string) => { console.log("isBreachedPassword:", isBreachedPassword); // delete later!!! // Clear the hashed password from memory - crypto.subtle.digest("SHA-1", encodedPwd); + const zeroBuffer = new Uint8Array(encodedPwd.length); + encodedPwd.set(zeroBuffer); return isBreachedPassword; } catch (err: any) { From 196beb8355d310c24bd7ffb82033b63c203109b6 Mon Sep 17 00:00:00 2001 From: Joel Biddle Date: Tue, 22 Aug 2023 17:50:43 +1000 Subject: [PATCH 08/33] removed logs & added pwndpasswords.com api to CSP --- backend/src/index.ts | 48 ++++++++++++++----- .../checks/checkIsPasswordBreached.ts | 10 ---- 2 files changed, 35 insertions(+), 23 deletions(-) diff --git a/backend/src/index.ts b/backend/src/index.ts index 17c030fed..d48a5b97b 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -24,7 +24,7 @@ import { secretSnapshot as eeSecretSnapshotRouter, users as eeUsersRouter, workspace as eeWorkspaceRouter, - secretScanning as v1SecretScanningRouter, + secretScanning as v1SecretScanningRouter } from "./ee/routes/v1"; import { auth as v1AuthRouter, @@ -58,7 +58,7 @@ import { signup as v2SignupRouter, tags as v2TagsRouter, users as v2UsersRouter, - workspace as v2WorkspaceRouter, + workspace as v2WorkspaceRouter } from "./routes/v2"; import { auth as v3AuthRouter, @@ -70,14 +70,21 @@ import { healthCheck } from "./routes/status"; import { getLogger } from "./utils/logger"; import { RouteNotFoundError } from "./utils/errors"; import { requestErrorHandler } from "./middleware/requestErrorHandler"; -import { getNodeEnv, getPort, getSecretScanningGitAppId, getSecretScanningPrivateKey, getSecretScanningWebhookProxy, getSecretScanningWebhookSecret, getSiteURL } from "./config"; +import { + getNodeEnv, + getPort, + getSecretScanningGitAppId, + getSecretScanningPrivateKey, + getSecretScanningWebhookProxy, + getSecretScanningWebhookSecret, + getSiteURL +} from "./config"; import { setup } from "./utils/setup"; import { syncSecretsToThirdPartyServices } from "./queues/integrations/syncSecretsToThirdPartyServices"; import { githubPushEventSecretScan } from "./queues/secret-scanning/githubScanPushEvent"; -const SmeeClient = require('smee-client') // eslint-disable-line +const SmeeClient = require("smee-client"); // eslint-disable-line const main = async () => { - await setup(); await EELicenseService.initGlobalFeatureSet(); @@ -94,11 +101,15 @@ const main = async () => { }) ); - if (await getSecretScanningGitAppId() && await getSecretScanningWebhookSecret() && await getSecretScanningPrivateKey()) { + if ( + (await getSecretScanningGitAppId()) && + (await getSecretScanningWebhookSecret()) && + (await getSecretScanningPrivateKey()) + ) { const probot = new Probot({ appId: await getSecretScanningGitAppId(), privateKey: await getSecretScanningPrivateKey(), - secret: await getSecretScanningWebhookSecret(), + secret: await getSecretScanningWebhookSecret() }); if ((await getNodeEnv()) != "production") { @@ -106,12 +117,14 @@ const main = async () => { source: await getSecretScanningWebhookProxy(), target: "http://backend:4000/ss-webhook", logger: console - }) + }); - smee.start() + smee.start(); } - app.use(createNodeMiddleware(GithubSecretScanningService, { probot, webhooksPath: "/ss-webhook" })); // secret scanning webhook + app.use( + createNodeMiddleware(GithubSecretScanningService, { probot, webhooksPath: "/ss-webhook" }) + ); // secret scanning webhook } if ((await getNodeEnv()) === "production") { @@ -119,7 +132,16 @@ const main = async () => { // in production app.disable("x-powered-by"); app.use(apiLimiter); - app.use(helmet()); + app.use( + helmet.contentSecurityPolicy({ + useDefaults: true, + directives: { + defaultSrc: ["'self'"], + imgSrc: ["*", "data:"], + connectSrc: ["'self'", "https://api.pwnedpasswords.com/range/"] + } + }) + ); } app.use((req, res, next) => { @@ -207,8 +229,8 @@ const main = async () => { server.on("close", async () => { await DatabaseService.closeDatabase(); - syncSecretsToThirdPartyServices.close() - githubPushEventSecretScan.close() + syncSecretsToThirdPartyServices.close(); + githubPushEventSecretScan.close(); }); return server; diff --git a/frontend/src/components/utilities/checks/checkIsPasswordBreached.ts b/frontend/src/components/utilities/checks/checkIsPasswordBreached.ts index b9d93b55c..623b43413 100644 --- a/frontend/src/components/utilities/checks/checkIsPasswordBreached.ts +++ b/frontend/src/components/utilities/checks/checkIsPasswordBreached.ts @@ -1,28 +1,18 @@ import axios from "axios"; import crypto from "crypto"; // added types from @types/node -///// REMINDER: ensure all logs are deleted!!! ///// - export const checkIsPasswordBreached = async (password: string) => { const dataBreachCheckAPIBaseURL = "https://api.pwnedpasswords.com/range/"; try { - console.log("password:", password); // delete later!!! - const textEncoder = new TextEncoder(); - const encodedPwd = textEncoder.encode(password); - console.log("encodedPwd:", encodedPwd); // delete later!!! - const hash = crypto.createHash("sha1").update(encodedPwd).digest(); - console.log("hash:", hash); // delete later!!! const hashedPwd = Array.from(new Uint8Array(hash)) .map((byte) => byte.toString(16).padStart(2, "0")) .join("") .toUpperCase(); - console.log("hashedPwd:", hashedPwd); // delete later!!! - const response = await axios.get(`${dataBreachCheckAPIBaseURL}${hashedPwd.slice(0, 5)}`); console.log("response:", response); // delete later!!! From e288402ec4d548648a6bf54a7455332304e9d913 Mon Sep 17 00:00:00 2001 From: Joel Biddle Date: Tue, 22 Aug 2023 17:58:10 +1000 Subject: [PATCH 09/33] Properly added pwndpasswords API to CSP --- backend/src/index.ts | 11 +-------- frontend/next.config.js | 50 ++++++++++++++++++++--------------------- 2 files changed, 26 insertions(+), 35 deletions(-) diff --git a/backend/src/index.ts b/backend/src/index.ts index d48a5b97b..098da2aad 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -132,16 +132,7 @@ const main = async () => { // in production app.disable("x-powered-by"); app.use(apiLimiter); - app.use( - helmet.contentSecurityPolicy({ - useDefaults: true, - directives: { - defaultSrc: ["'self'"], - imgSrc: ["*", "data:"], - connectSrc: ["'self'", "https://api.pwnedpasswords.com/range/"] - } - }) - ); + app.use(helmet()); } app.use((req, res, next) => { diff --git a/frontend/next.config.js b/frontend/next.config.js index b133818bd..3e9336f15 100644 --- a/frontend/next.config.js +++ b/frontend/next.config.js @@ -3,7 +3,7 @@ /** * @type {import('next').NextConfig} **/ -const path = require('path'); +const path = require("path"); const ContentSecurityPolicy = ` default-src 'self'; @@ -11,7 +11,7 @@ const ContentSecurityPolicy = ` style-src 'self' https://rsms.me 'unsafe-inline'; child-src https://api.stripe.com; frame-src https://js.stripe.com/ https://api.stripe.com https://www.youtube.com/; - connect-src 'self' wss://nexus-websocket-a.intercom.io https://api-iam.intercom.io https://api.heroku.com/ https://id.heroku.com/oauth/authorize https://id.heroku.com/oauth/token https://checkout.stripe.com https://app.posthog.com https://api.stripe.com http://localhost:*; + connect-src 'self' wss://nexus-websocket-a.intercom.io https://api-iam.intercom.io https://api.heroku.com/ https://id.heroku.com/oauth/authorize https://id.heroku.com/oauth/token https://checkout.stripe.com https://app.posthog.com https://api.stripe.com https://api.pwnedpasswords.com http://localhost:*; img-src 'self' https://static.intercomassets.com https://js.intercomcdn.com https://downloads.intercomcdn.com https://*.stripe.com https://i.ytimg.com/ data:; media-src https://js.intercomcdn.com; font-src 'self' https://fonts.intercomcdn.com/ https://maxcdn.bootstrapcdn.com https://rsms.me https://fonts.gstatic.com; @@ -21,50 +21,50 @@ const ContentSecurityPolicy = ` // after learning more below. const securityHeaders = [ { - key: 'X-DNS-Prefetch-Control', - value: 'on' + key: "X-DNS-Prefetch-Control", + value: "on" }, { - key: 'Strict-Transport-Security', - value: 'max-age=63072000; includeSubDomains; preload' + key: "Strict-Transport-Security", + value: "max-age=63072000; includeSubDomains; preload" }, { - key: 'X-XSS-Protection', - value: '1; mode=block' + key: "X-XSS-Protection", + value: "1; mode=block" }, { - key: 'X-Frame-Options', - value: 'SAMEORIGIN' + key: "X-Frame-Options", + value: "SAMEORIGIN" }, { - key: 'Permissions-Policy', - value: 'camera=(), microphone=()' + key: "Permissions-Policy", + value: "camera=(), microphone=()" }, { - key: 'X-Content-Type-Options', - value: 'nosniff' + key: "X-Content-Type-Options", + value: "nosniff" }, { - key: 'Referrer-Policy', - value: 'strict-origin-when-cross-origin' + key: "Referrer-Policy", + value: "strict-origin-when-cross-origin" }, { - key: 'Content-Security-Policy', - value: ContentSecurityPolicy.replace(/\s{2,}/g, ' ').trim() + key: "Content-Security-Policy", + value: ContentSecurityPolicy.replace(/\s{2,}/g, " ").trim() } ]; module.exports = { - output: 'standalone', + output: "standalone", i18n: { - locales: ['en', 'ko', 'fr', 'pt-BR', 'pt-PT', 'es'], - defaultLocale: 'en' + locales: ["en", "ko", "fr", "pt-BR", "pt-PT", "es"], + defaultLocale: "en" }, async headers() { return [ { // Apply these headers to all routes in your application. - source: '/:path*', + source: "/:path*", headers: securityHeaders } ]; @@ -73,15 +73,15 @@ module.exports = { // config config.module.rules.push({ test: /\.wasm$/, - loader: 'base64-loader', - type: 'javascript/auto' + loader: "base64-loader", + type: "javascript/auto" }); config.module.noParse = /\.wasm$/; config.module.rules.forEach((rule) => { (rule.oneOf || []).forEach((oneOf) => { - if (oneOf.loader && oneOf.loader.indexOf('file-loader') >= 0) { + if (oneOf.loader && oneOf.loader.indexOf("file-loader") >= 0) { oneOf.exclude.push(/\.wasm$/); } }); From c5ae40278774d427d544830ae8859deae38f6d43 Mon Sep 17 00:00:00 2001 From: Joel Biddle Date: Tue, 22 Aug 2023 18:14:03 +1000 Subject: [PATCH 10/33] Added comments to explain breach passwords API --- .../checks/checkIsPasswordBreached.ts | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/frontend/src/components/utilities/checks/checkIsPasswordBreached.ts b/frontend/src/components/utilities/checks/checkIsPasswordBreached.ts index 623b43413..e032ebd91 100644 --- a/frontend/src/components/utilities/checks/checkIsPasswordBreached.ts +++ b/frontend/src/components/utilities/checks/checkIsPasswordBreached.ts @@ -2,7 +2,15 @@ import axios from "axios"; import crypto from "crypto"; // added types from @types/node export const checkIsPasswordBreached = async (password: string) => { - const dataBreachCheckAPIBaseURL = "https://api.pwnedpasswords.com/range/"; + // see API details here: https://haveibeenpwned.com/API/v3#SearchingPwnedPasswordsByRange + // in short, the pending password is hashed (SHA-1), fist 5 chars are sliced and compared against a ranged hash table + // if there is a match, that password has been involved in a password breach and should not be accepted + + // the database consists of ~700 mln breached passwords and is continuously being updated including with data from the FBI & the UK's NCA + // https://www.troyhunt.com/open-source-pwned-passwords-with-fbi-feed-and-225m-new-nca-passwords-is-now-live/ + + const dataBreachCheckAPIBaseURL = "https://api.pwnedpasswords.com/range/"; // added to CSP + try { const textEncoder = new TextEncoder(); const encodedPwd = textEncoder.encode(password); @@ -13,24 +21,21 @@ export const checkIsPasswordBreached = async (password: string) => { .join("") .toUpperCase(); + // Ensure that ONLY the first five SHA-1 hash chars are sent over HTTPS const response = await axios.get(`${dataBreachCheckAPIBaseURL}${hashedPwd.slice(0, 5)}`); - console.log("response:", response); // delete later!!! - const responseData = response.data.toUpperCase(); - console.log("responseData:", responseData); // delete later!!! - const isBreachedPassword = responseData.includes(hashedPwd.slice(5, 40)); - console.log("isBreachedPassword:", isBreachedPassword); // delete later!!! - // Clear the hashed password from memory + // Clear the hashed password from memory (good practice) const zeroBuffer = new Uint8Array(encodedPwd.length); encodedPwd.set(zeroBuffer); - return isBreachedPassword; + return isBreachedPassword; // boolean } catch (err: any) { if (axios.isAxiosError(err) && err.response && err.response.status === 429) { console.error("Received a 429 response from the Pwnd Passwords API"); - // Handle the 429 error here + // Handle the 429 error here (not 100% sure what the rate limits are for the password API) + // an error here should not cause a fail of setting/resetting/changing the password } else { console.error(err); } From 0b359cd797d6ea5c96e7cdf23dc5f28906590764 Mon Sep 17 00:00:00 2001 From: Joel Biddle Date: Tue, 22 Aug 2023 19:45:35 +1000 Subject: [PATCH 11/33] Made breached pwd API comments clearer --- .../utilities/checks/PasswordCheck.ts | 5 ++--- .../checks/checkIsPasswordBreached.ts | 21 +++++++++---------- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/frontend/src/components/utilities/checks/PasswordCheck.ts b/frontend/src/components/utilities/checks/PasswordCheck.ts index cd75b53f9..92d891d97 100644 --- a/frontend/src/components/utilities/checks/PasswordCheck.ts +++ b/frontend/src/components/utilities/checks/PasswordCheck.ts @@ -14,12 +14,11 @@ interface PasswordCheckProps { const passwordCheck = ({ password, setPasswordErrorTooShort, + setPasswordErrorTooLong, setPasswordErrorNumber, setPasswordErrorLowerCase, - setPasswordErrorTooLong, errorCheck }: PasswordCheckProps) => { - if (!password || password.length < 14) { setPasswordErrorTooShort(true); errorCheck = true; @@ -27,7 +26,7 @@ const passwordCheck = ({ setPasswordErrorTooShort(false); } - if (password.length > 100) { + if (password.length > 100) { setPasswordErrorTooLong(true); errorCheck = true; } else { diff --git a/frontend/src/components/utilities/checks/checkIsPasswordBreached.ts b/frontend/src/components/utilities/checks/checkIsPasswordBreached.ts index e032ebd91..40a9330db 100644 --- a/frontend/src/components/utilities/checks/checkIsPasswordBreached.ts +++ b/frontend/src/components/utilities/checks/checkIsPasswordBreached.ts @@ -3,10 +3,10 @@ import crypto from "crypto"; // added types from @types/node export const checkIsPasswordBreached = async (password: string) => { // see API details here: https://haveibeenpwned.com/API/v3#SearchingPwnedPasswordsByRange - // in short, the pending password is hashed (SHA-1), fist 5 chars are sliced and compared against a ranged hash table + // in short, the pending password is hashed (SHA-1), the first 5 chars are sliced and compared against a ranged hash table // if there is a match, that password has been involved in a password breach and should not be accepted - // the database consists of ~700 mln breached passwords and is continuously being updated including with data from the FBI & the UK's NCA + // the database consists of ~700 mln breached passwords and is continuously updated // https://www.troyhunt.com/open-source-pwned-passwords-with-fbi-feed-and-225m-new-nca-passwords-is-now-live/ const dataBreachCheckAPIBaseURL = "https://api.pwnedpasswords.com/range/"; // added to CSP @@ -15,27 +15,26 @@ export const checkIsPasswordBreached = async (password: string) => { const textEncoder = new TextEncoder(); const encodedPwd = textEncoder.encode(password); const hash = crypto.createHash("sha1").update(encodedPwd).digest(); - - const hashedPwd = Array.from(new Uint8Array(hash)) + let hashedPwd = Array.from(new Uint8Array(hash)) .map((byte) => byte.toString(16).padStart(2, "0")) .join("") .toUpperCase(); + hashedPwd = hashedPwd.slice(0, 5); // ONLY the first five SHA-1 hash chars are sent over HTTPS (the whole string can be sent but that's not very secure due to SHA-1 flaws) - // Ensure that ONLY the first five SHA-1 hash chars are sent over HTTPS - const response = await axios.get(`${dataBreachCheckAPIBaseURL}${hashedPwd.slice(0, 5)}`); + const response = await axios.get(`${dataBreachCheckAPIBaseURL}${hashedPwd}`); const responseData = response.data.toUpperCase(); - const isBreachedPassword = responseData.includes(hashedPwd.slice(5, 40)); + const isBreachedPassword = responseData.includes(hashedPwd.slice(5, 40)); // compare against the API's ranged db's hash table - // Clear the hashed password from memory (good practice) + // Clear the hashed password from memory const zeroBuffer = new Uint8Array(encodedPwd.length); encodedPwd.set(zeroBuffer); - return isBreachedPassword; // boolean + return isBreachedPassword; // boolean: true === "password has been involved in a data breach" } catch (err: any) { if (axios.isAxiosError(err) && err.response && err.response.status === 429) { console.error("Received a 429 response from the Pwnd Passwords API"); - // Handle the 429 error here (not 100% sure what the rate limits are for the password API) - // an error here should not cause a fail of setting/resetting/changing the password + // Handle the 429 error here (not 100% sure what the rate limits are for the password API but looks like <10 calls/min) + // an error here should not cause the setting/resetting/changing password to fail (unless desired) } else { console.error(err); } From f47a119474479cdb202c16a4a80891fedafccb4a Mon Sep 17 00:00:00 2001 From: Joel Biddle Date: Tue, 22 Aug 2023 20:20:13 +1000 Subject: [PATCH 12/33] fixed breached pwd error messages --- frontend/src/pages/password-reset.tsx | 86 ++++++++++++++++++--------- 1 file changed, 57 insertions(+), 29 deletions(-) diff --git a/frontend/src/pages/password-reset.tsx b/frontend/src/pages/password-reset.tsx index 90fd6be51..6aa3ea275 100644 --- a/frontend/src/pages/password-reset.tsx +++ b/frontend/src/pages/password-reset.tsx @@ -12,7 +12,7 @@ import Button from "@app/components/basic/buttons/Button"; import InputField from "@app/components/basic/InputField"; import passwordCheck from "@app/components/utilities/checks/PasswordCheck"; import Aes256Gcm from "@app/components/utilities/cryptography/aes-256-gcm"; -import { useResetPassword,useVerifyPasswordResetCode } from "@app/hooks/api"; +import { useResetPassword, useVerifyPasswordResetCode } from "@app/hooks/api"; import { getBackupEncryptedPrivateKey } from "@app/hooks/api/auth/queries"; import { deriveArgonKey } from "../components/utilities/cryptography/crypto"; @@ -32,12 +32,13 @@ export default function PasswordReset() { const [passwordErrorTooLong, setPasswordErrorTooLong] = useState(false); const [passwordErrorNumber, setPasswordErrorNumber] = useState(false); const [passwordErrorLowerCase, setPasswordErrorLowerCase] = useState(false); + const [passwordErrorIsBreached, setPasswordErrorIsBreached] = useState(false); const router = useRouter(); const { mutateAsync: verifyPasswordResetCodeMutateAsync } = useVerifyPasswordResetCode(); const { mutateAsync: resetPasswordMutateAsync } = useResetPassword(); - + const parsedUrl = queryString.parse(router.asPath.split("?")[1]); const token = parsedUrl.token as string; const email = (parsedUrl.to as string)?.replace(" ", "+").trim(); @@ -47,7 +48,7 @@ export default function PasswordReset() { e.preventDefault(); try { const result = await getBackupEncryptedPrivateKey({ verificationToken }); - + setPrivateKey( Aes256Gcm.decrypt({ ciphertext: result.encryptedPrivateKey, @@ -57,7 +58,7 @@ export default function PasswordReset() { }) ); setStep(3); - } catch(err) { + } catch (err) { console.error(err); setBackupKeyError(true); } @@ -66,12 +67,13 @@ export default function PasswordReset() { // If everything is correct, reset the password const resetPasswordHandler = async (e: FormEvent) => { e.preventDefault(); - const errorCheck = passwordCheck({ + const errorCheck = await passwordCheck({ password: newPassword, setPasswordErrorTooShort, setPasswordErrorTooLong, setPasswordErrorNumber, setPasswordErrorLowerCase, + setPasswordErrorIsBreached, errorCheck: false }); @@ -129,10 +131,10 @@ export default function PasswordReset() { verifier: result.verifier, verificationToken }); - + router.push("/login"); - setLoading(false) + setLoading(false); }); } ); @@ -171,13 +173,17 @@ export default function PasswordReset() { // Input backup key const stepInputBackupKey = ( -
+

Enter your backup key

-
-

- You can find it in your emergency kit. You had to download the emergency kit during signup. +

+

+ You can find it in your emergency kit. You had to download the emergency kit during + signup.

@@ -194,12 +200,7 @@ export default function PasswordReset() {
-
@@ -207,7 +208,10 @@ export default function PasswordReset() { // Enter new password const stepEnterNewPassword = ( -
+

Enter new password

@@ -227,18 +231,29 @@ export default function PasswordReset() { setPasswordErrorTooLong, setPasswordErrorNumber, setPasswordErrorLowerCase, + setPasswordErrorIsBreached, errorCheck: false }); }} type="password" value={newPassword} isRequired - error={passwordErrorTooShort && passwordErrorTooLong && passwordErrorLowerCase && passwordErrorNumber} + error={ + passwordErrorTooShort && + passwordErrorTooLong && + passwordErrorNumber && + passwordErrorLowerCase && + passwordErrorIsBreached + } autoComplete="new-password" id="new-password" />
- {passwordErrorTooShort || passwordErrorTooLong || passwordErrorLowerCase || passwordErrorNumber ? ( + {passwordErrorTooShort || + passwordErrorTooLong || + passwordErrorNumber || + passwordErrorLowerCase || + passwordErrorIsBreached ? (
Password should contain:
@@ -261,6 +276,16 @@ export default function PasswordReset() { at most 100 characters
+
+ {passwordErrorNumber ? ( + + ) : ( + + )} +
+ at least 1 number +
+
{passwordErrorLowerCase ? ( @@ -272,15 +297,18 @@ export default function PasswordReset() { > at least 1 lowercase character
-
-
- {passwordErrorNumber ? ( - - ) : ( - - )} -
- at least 1 number +
+ {passwordErrorIsBreached ? ( + + ) : ( + + )} +
+ The password you provided is in a list of passwords commonly used on other websites. + Please try again with a stronger password. +
From 1242d88acb8438f5a5f50aaf92025d42edf05cfe Mon Sep 17 00:00:00 2001 From: Joel Biddle Date: Tue, 22 Aug 2023 20:20:54 +1000 Subject: [PATCH 13/33] Fixed breached pwd error messages --- frontend/public/locales/en/translations.json | 3 +- frontend/public/locales/es/translations.json | 4 +- frontend/public/locales/fr/translations.json | 6 +- frontend/public/locales/ko/translations.json | 6 +- .../public/locales/pt-BR/translations.json | 6 +- frontend/public/locales/tr/translations.json | 4 +- .../utilities/checks/PasswordCheck.ts | 13 ++- .../utilities/checks/checkPassword.ts | 109 +++++++++--------- 8 files changed, 81 insertions(+), 70 deletions(-) diff --git a/frontend/public/locales/en/translations.json b/frontend/public/locales/en/translations.json index 506495b3f..84554d8fc 100644 --- a/frontend/public/locales/en/translations.json +++ b/frontend/public/locales/en/translations.json @@ -234,8 +234,9 @@ "validate-base": "Password should contain:", "validate-too-short": "at least 14 characters", "validate-too-long": "at most 100 characters", + "validate-number": "at least 1 number", "validate-case": "at least 1 lowercase character", - "validate-number": "at least 1 number" + "validate-breached": "The password you provided is in a list of passwords commonly used on other websites. Please try again with a stronger password." }, "token": { "service-tokens": "Service Tokens", diff --git a/frontend/public/locales/es/translations.json b/frontend/public/locales/es/translations.json index 28e2793cb..b9fe5f335 100644 --- a/frontend/public/locales/es/translations.json +++ b/frontend/public/locales/es/translations.json @@ -231,8 +231,8 @@ "validate-base": "La contraseña debe contener:", "validate-too-short": "como mínimo 14 caracteres", "validate-too-long": "como máximo 100 caracteres", - "validate-case": "como mínimo 1 letra en minúsculas", - "validate-number": "como mínimo 1 número" + "validate-number": "como mínimo 1 número", + "validate-case": "como mínimo 1 letra en minúsculas" }, "token": { "service-tokens": "Tokens de servicio", diff --git a/frontend/public/locales/fr/translations.json b/frontend/public/locales/fr/translations.json index 49edd33fa..00556b1d8 100644 --- a/frontend/public/locales/fr/translations.json +++ b/frontend/public/locales/fr/translations.json @@ -218,8 +218,8 @@ "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-case": "au moins 1 caractère miniscule", - "validate-number": "au moins 1 chiffre" + "validate-number": "au moins 1 chiffre", + "validate-case": "au moins 1 caractère miniscule" }, "token": { "service-tokens": "Jetons de service", @@ -297,4 +297,4 @@ "step5-subtitle": "Infisical a pour but d'être utilisé avec vos coéquipiers. Invitez-les à le tester.", "step5-skip": "Passer" } -} \ No newline at end of file +} diff --git a/frontend/public/locales/ko/translations.json b/frontend/public/locales/ko/translations.json index d29a5846c..b40109400 100644 --- a/frontend/public/locales/ko/translations.json +++ b/frontend/public/locales/ko/translations.json @@ -185,8 +185,8 @@ "validate-base": "비밀번호는 다음 조건을 만족해야 합니다:", "validate-too-short": "14 글자 이상", "validate-too-long": "100 자 이하", - "validate-case": "1개 이상의 소문자", - "validate-number": "1개 이상의 숫자" + "validate-number": "1개 이상의 숫자", + "validate-case": "1개 이상의 소문자" }, "token": { "add-dialog": { @@ -257,4 +257,4 @@ "step4-description3": "분실시 접근하거나 복구할 수 없는 시크릿 키가 포함되어 있어요.", "step4-download": "PDF 다운로드" } -} \ No newline at end of file +} diff --git a/frontend/public/locales/pt-BR/translations.json b/frontend/public/locales/pt-BR/translations.json index e04de50da..805abd126 100644 --- a/frontend/public/locales/pt-BR/translations.json +++ b/frontend/public/locales/pt-BR/translations.json @@ -213,8 +213,8 @@ "validate-base": "A senha deve conter:", "validate-too-short": "pelo menos 14 caracteres", "validate-too-long": "no máximo 100 caracteres", - "validate-case": "pelo menos 1 caractere minúsculo", - "validate-number": "pelo menos 1 número" + "validate-number": "pelo menos 1 número", + "validate-case": "pelo menos 1 caractere minúsculo" }, "token": { "service-tokens": "Tokens de Serviço", @@ -291,4 +291,4 @@ "step5-subtitle": "Infisical foi feito para ser usado com seus colegas. Convide-os para testar também.", "step5-skip": "Pular" } -} \ No newline at end of file +} diff --git a/frontend/public/locales/tr/translations.json b/frontend/public/locales/tr/translations.json index c98399f7f..a03b9eb2c 100644 --- a/frontend/public/locales/tr/translations.json +++ b/frontend/public/locales/tr/translations.json @@ -231,8 +231,8 @@ "validate-base": "Şifre kısıtlamaları:", "validate-too-short": "en az 14 karakter", "validate-too-long": "en fazla 100 karakter", - "validate-case": "en az 1 küçük harf", - "validate-number": "en az 1 rakam" + "validate-number": "en az 1 rakam", + "validate-case": "en az 1 küçük harf" }, "token": { "service-tokens": "Servis Belirteçleri", diff --git a/frontend/src/components/utilities/checks/PasswordCheck.ts b/frontend/src/components/utilities/checks/PasswordCheck.ts index 92d891d97..133eb5a35 100644 --- a/frontend/src/components/utilities/checks/PasswordCheck.ts +++ b/frontend/src/components/utilities/checks/PasswordCheck.ts @@ -1,3 +1,5 @@ +import { checkIsPasswordBreached } from "./checkIsPasswordBreached"; + /* eslint-disable no-param-reassign */ interface PasswordCheckProps { password: string; @@ -6,17 +8,19 @@ interface PasswordCheckProps { setPasswordErrorTooLong: (value: boolean) => void; setPasswordErrorNumber: (value: boolean) => void; setPasswordErrorLowerCase: (value: boolean) => void; + setPasswordErrorIsBreached: (value: boolean) => void; } /** * This function checks a user password with respect to some criteria. */ -const passwordCheck = ({ +const passwordCheck = async ({ password, setPasswordErrorTooShort, setPasswordErrorTooLong, setPasswordErrorNumber, setPasswordErrorLowerCase, + setPasswordErrorIsBreached, errorCheck }: PasswordCheckProps) => { if (!password || password.length < 14) { @@ -57,6 +61,13 @@ const passwordCheck = ({ setPasswordErrorLowerCase(false); } + if (await checkIsPasswordBreached(password)) { + setPasswordErrorIsBreached(true); + errorCheck = true; + } else { + setPasswordErrorIsBreached(false); + } + // if (!/[A-Z]/.test(password)) { // setPasswordErrorUpperCase(true); // errorCheck = true; diff --git a/frontend/src/components/utilities/checks/checkPassword.ts b/frontend/src/components/utilities/checks/checkPassword.ts index 0ef1e949b..4f0d0a523 100644 --- a/frontend/src/components/utilities/checks/checkPassword.ts +++ b/frontend/src/components/utilities/checks/checkPassword.ts @@ -1,21 +1,21 @@ import { checkIsPasswordBreached } from "./checkIsPasswordBreached"; type Errors = { - tooShort?: string, - tooLong?: string, - upperCase?: string, - lowerCase?: string, - number?: string, - specialChar?: string, - repeatedChar?: string, - commonPassword?: string, - breachedPassword?: string - }; + tooShort?: string; + tooLong?: string; + upperCase?: string; + lowerCase?: string; + number?: string; + specialChar?: string; + repeatedChar?: string; + commonPassword?: string; + breachedPassword?: string; +}; interface CheckPasswordParams { - password: string; - commonPasswords: string[]; - setErrors: (value: Errors) => void; + password: string; + commonPasswords: string[]; + setErrors: (value: Errors) => void; } /** @@ -26,62 +26,61 @@ interface CheckPasswordParams { * - Contains at least 1 lowercase character (a-z) * - Contains at least 1 number (0-9) * - Does not contain 3 repeat, consecutive characters - * + * * The function returns whether or not the password [password] - * passes the minimum requirements above. It sets errors on + * passes the minimum requirements above. It sets errors on * an erorr object via [setErrors]. - * + * * @param {Object} obj * @param {String} obj.password - the password to check * @param {Function} obj.setErrors - set state function to set error object */ const checkPassword = async ({ - password, - commonPasswords, - setErrors + password, + commonPasswords, + setErrors }: CheckPasswordParams): Promise => { - const errors: Errors = {}; + const errors: Errors = {}; - const isBreachedPassword = await checkIsPasswordBreached(password) - - if (password.length < 14) { - errors.tooShort = "at least 14 characters"; - } + if (password.length < 14) { + errors.tooShort = "at least 14 characters"; + } - if (password.length > 100) { - errors.tooLong = "at most 100 characters"; - } + if (password.length > 100) { + errors.tooLong = "at most 100 characters"; + } - if (!/[A-Z]/.test(password)) { - errors.upperCase = "at least 1 uppercase character (A-Z)"; - } + if (!/[A-Z]/.test(password)) { + errors.upperCase = "at least 1 uppercase character (A-Z)"; + } - if (!/[a-z]/.test(password)) { - errors.lowerCase = "at least 1 lowercase character (a-z)"; - } + if (!/[a-z]/.test(password)) { + errors.lowerCase = "at least 1 lowercase character (a-z)"; + } - if (!/[0-9]/.test(password)) { - errors.number = "at least 1 number (0-9)"; - } + if (!/[0-9]/.test(password)) { + errors.number = "at least 1 number (0-9)"; + } - if (!/[!@#$%^&*(),.?":{}|<>]/.test(password)) { - errors.specialChar = "at least 1 special character (!@#$%^&*(),.?)"; - } + if (!/[!@#$%^&*(),.?":{}|<>]/.test(password)) { + errors.specialChar = "at least 1 special character (!@#$%^&*(),.?)"; + } - if (/([A-Za-z0-9])\1\1\1/.test(password)) { - errors.repeatedChar = "No 3 repeat, consecutive characters"; - } - - if (commonPasswords.includes(password)) { - errors.commonPassword = "No common passwords"; - } + if (/([A-Za-z0-9])\1\1\1/.test(password)) { + errors.repeatedChar = "No 3 repeat, consecutive characters"; + } - if (isBreachedPassword) { - errors.breachedPassword = "The password you provided is in a list of passwords commonly used on other websites. Please try again with a stronger password."; - } - - setErrors(errors); - return Object.keys(errors).length > 0; -} + if (commonPasswords.includes(password)) { + errors.commonPassword = "No common passwords"; + } -export default checkPassword; \ No newline at end of file + if (await checkIsPasswordBreached(password)) { + errors.breachedPassword = + "The password you provided is in a list of passwords commonly used on other websites. Please try again with a stronger password."; + } + + setErrors(errors); + return Object.keys(errors).length > 0; +}; + +export default checkPassword; From 026ea29847ea4e154fd17c40a931f1dc1c7efd39 Mon Sep 17 00:00:00 2001 From: Joel Biddle Date: Tue, 22 Aug 2023 20:42:07 +1000 Subject: [PATCH 14/33] further fixes to password check logic --- .../checks/checkIsPasswordBreached.ts | 13 +- .../utilities/checks/checkPassword.ts | 8 +- .../ChangePasswordSection.tsx | 264 +++++++++--------- 3 files changed, 138 insertions(+), 147 deletions(-) diff --git a/frontend/src/components/utilities/checks/checkIsPasswordBreached.ts b/frontend/src/components/utilities/checks/checkIsPasswordBreached.ts index 40a9330db..fba537977 100644 --- a/frontend/src/components/utilities/checks/checkIsPasswordBreached.ts +++ b/frontend/src/components/utilities/checks/checkIsPasswordBreached.ts @@ -15,21 +15,26 @@ export const checkIsPasswordBreached = async (password: string) => { const textEncoder = new TextEncoder(); const encodedPwd = textEncoder.encode(password); const hash = crypto.createHash("sha1").update(encodedPwd).digest(); - let hashedPwd = Array.from(new Uint8Array(hash)) + + const hashedPwd = Array.from(new Uint8Array(hash)) .map((byte) => byte.toString(16).padStart(2, "0")) .join("") .toUpperCase(); - hashedPwd = hashedPwd.slice(0, 5); // ONLY the first five SHA-1 hash chars are sent over HTTPS (the whole string can be sent but that's not very secure due to SHA-1 flaws) - const response = await axios.get(`${dataBreachCheckAPIBaseURL}${hashedPwd}`); + const hashedPwdToSend = hashedPwd.slice(0, 5); // ONLY the first five SHA-1 hash chars are sent over HTTPS (the whole string can be sent but that's not very secure due to SHA-1 flaws) + + const response = await axios.get(`${dataBreachCheckAPIBaseURL}${hashedPwdToSend}`); const responseData = response.data.toUpperCase(); + const isBreachedPassword = responseData.includes(hashedPwd.slice(5, 40)); // compare against the API's ranged db's hash table + console.log("isBreachedPassword:", isBreachedPassword); // remove log later // Clear the hashed password from memory + const zeroBuffer = new Uint8Array(encodedPwd.length); encodedPwd.set(zeroBuffer); - return isBreachedPassword; // boolean: true === "password has been involved in a data breach" + return isBreachedPassword; // boolean: true indicates the password has been involved in a data breach } catch (err: any) { if (axios.isAxiosError(err) && err.response && err.response.status === 429) { console.error("Received a 429 response from the Pwnd Passwords API"); diff --git a/frontend/src/components/utilities/checks/checkPassword.ts b/frontend/src/components/utilities/checks/checkPassword.ts index 4f0d0a523..210d3f040 100644 --- a/frontend/src/components/utilities/checks/checkPassword.ts +++ b/frontend/src/components/utilities/checks/checkPassword.ts @@ -70,15 +70,15 @@ const checkPassword = async ({ errors.repeatedChar = "No 3 repeat, consecutive characters"; } - if (commonPasswords.includes(password)) { - errors.commonPassword = "No common passwords"; - } - if (await checkIsPasswordBreached(password)) { errors.breachedPassword = "The password you provided is in a list of passwords commonly used on other websites. Please try again with a stronger password."; } + if (commonPasswords.includes(password)) { + errors.commonPassword = "No common passwords"; + } + setErrors(errors); return Object.keys(errors).length > 0; }; diff --git a/frontend/src/views/Settings/PersonalSettingsPage/ChangePasswordSection/ChangePasswordSection.tsx b/frontend/src/views/Settings/PersonalSettingsPage/ChangePasswordSection/ChangePasswordSection.tsx index e17864485..86cb58a01 100644 --- a/frontend/src/views/Settings/PersonalSettingsPage/ChangePasswordSection/ChangePasswordSection.tsx +++ b/frontend/src/views/Settings/PersonalSettingsPage/ChangePasswordSection/ChangePasswordSection.tsx @@ -1,5 +1,5 @@ import { useState } from "react"; -import { Controller, useForm } from "react-hook-form"; +import { Controller, useForm } from "react-hook-form"; import { useTranslation } from "react-i18next"; import { faXmark } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; @@ -9,160 +9,146 @@ import * as yup from "yup"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; import attemptChangePassword from "@app/components/utilities/attemptChangePassword"; import checkPassword from "@app/components/utilities/checks/checkPassword"; -import { - Button, - FormControl, - Input -} from "@app/components/v2"; +import { Button, FormControl, Input } from "@app/components/v2"; import { useUser } from "@app/context"; import { useGetCommonPasswords } from "@app/hooks/api"; type Errors = { - tooShort?: string, - tooLong?: string, - upperCase?: string, - lowerCase?: string, - number?: string, - specialChar?: string, - repeatedChar?: string, - breachedPassword?: string + tooShort?: string; + tooLong?: string; + upperCase?: string; + lowerCase?: string; + number?: string; + specialChar?: string; + repeatedChar?: string; + commonPassword?: string; + breachedPassword?: string; }; -const schema = yup.object({ +const schema = yup + .object({ oldPassword: yup.string().required("Old password is required"), newPassword: yup.string().required("New password is required") -}).required(); + }) + .required(); export type FormData = yup.InferType; export const ChangePasswordSection = () => { - const { t } = useTranslation(); - const { createNotification } = useNotificationContext(); - const { user } = useUser(); - const { data: commonPasswords } = useGetCommonPasswords(); - const { reset, control, handleSubmit } = useForm({ - defaultValues: { - oldPassword: "", - newPassword: "" - }, - resolver: yupResolver(schema) - }); - const [errors, setErrors] = useState({}); - const [isLoading, setIsLoading] = useState(false); + const { t } = useTranslation(); + const { createNotification } = useNotificationContext(); + const { user } = useUser(); + const { data: commonPasswords } = useGetCommonPasswords(); + const { reset, control, handleSubmit } = useForm({ + defaultValues: { + oldPassword: "", + newPassword: "" + }, + resolver: yupResolver(schema) + }); + const [errors, setErrors] = useState({}); + const [isLoading, setIsLoading] = useState(false); - const onFormSubmit = async ({ oldPassword, newPassword }: FormData) => { - try { - if (!user?.email) return; - if (!commonPasswords) return; + const onFormSubmit = async ({ oldPassword, newPassword }: FormData) => { + try { + if (!user?.email) return; + if (!commonPasswords) return; - const errorCheck = await checkPassword({ - password: newPassword, - commonPasswords, - setErrors - }); + const errorCheck = await checkPassword({ + password: newPassword, + commonPasswords, + setErrors + }); - if (errorCheck) return; - - setIsLoading(true); - await attemptChangePassword({ - email: user.email, - currentPassword: oldPassword, - newPassword - }); - - setIsLoading(false); - createNotification({ - text: "Successfully changed password", - type: "success" - }); + if (errorCheck) return; - reset(); - window.location.href = "/login"; - - } catch (err) { - console.error(err); - setIsLoading(false); - createNotification({ - text: "Failed to change password", - type: "error" - }); - } + setIsLoading(true); + await attemptChangePassword({ + email: user.email, + currentPassword: oldPassword, + newPassword + }); + + setIsLoading(false); + createNotification({ + text: "Successfully changed password", + type: "success" + }); + + reset(); + window.location.href = "/login"; + } catch (err) { + console.error(err); + setIsLoading(false); + createNotification({ + text: "Failed to change password", + type: "error" + }); } - - return ( - -

- Change password -

-
- ( - - - - )} - control={control} - name="oldPassword" - /> -
-
- ( - - - - )} - control={control} - name="newPassword" - /> -
- {Object.keys(errors).length > 0 && ( -
-
{t("section.password.validate-base")}
- {Object.keys(errors).map((key) => { - if (errors[key as keyof Errors]) { - return ( -
-
- -
-

- {errors[key as keyof Errors]} -

-
- ); - } + }; - return null; - })} + return ( + +

Change password

+
+ ( + + + + )} + control={control} + name="oldPassword" + /> +
+
+ ( + + + + )} + control={control} + name="newPassword" + /> +
+ {Object.keys(errors).length > 0 && ( +
+
{t("section.password.validate-base")}
+ {Object.keys(errors).map((key) => { + if (errors[key as keyof Errors]) { + return ( +
+
+ +
+

{errors[key as keyof Errors]}

- )} - - - ); -} \ No newline at end of file + ); + } + + return null; + })} +
+ )} + + + ); +}; From 00089a6bba65b21f8d8ad17eb764f97a6c4870b3 Mon Sep 17 00:00:00 2001 From: Joel Biddle Date: Tue, 22 Aug 2023 20:57:12 +1000 Subject: [PATCH 15/33] Added breached pwd error translations --- frontend/public/locales/es/translations.json | 3 ++- frontend/public/locales/fr/translations.json | 3 ++- frontend/public/locales/ko/translations.json | 3 ++- frontend/public/locales/pt-BR/translations.json | 3 ++- frontend/public/locales/tr/translations.json | 3 ++- 5 files changed, 10 insertions(+), 5 deletions(-) diff --git a/frontend/public/locales/es/translations.json b/frontend/public/locales/es/translations.json index b9fe5f335..ffb0a5dd4 100644 --- a/frontend/public/locales/es/translations.json +++ b/frontend/public/locales/es/translations.json @@ -232,7 +232,8 @@ "validate-too-short": "como mínimo 14 caracteres", "validate-too-long": "como máximo 100 caracteres", "validate-number": "como mínimo 1 número", - "validate-case": "como mínimo 1 letra en minúsculas" + "validate-case": "como mínimo 1 letra en minúsculas", + "validate-breached": "La contraseña que proporcionó se encuentra en una lista de contraseñas comúnmente utilizadas en otros sitios web. Inténtelo de nuevo con una contraseña más segura." }, "token": { "service-tokens": "Tokens de servicio", diff --git a/frontend/public/locales/fr/translations.json b/frontend/public/locales/fr/translations.json index 00556b1d8..7ce136191 100644 --- a/frontend/public/locales/fr/translations.json +++ b/frontend/public/locales/fr/translations.json @@ -219,7 +219,8 @@ "validate-too-short": "au moins 14 caractères", "validate-too-long": "au maximum 100 caractères", "validate-number": "au moins 1 chiffre", - "validate-case": "au moins 1 caractère miniscule" + "validate-case": "au moins 1 caractère miniscule", + "validate-breached": "Le mot de passe que vous avez fourni figure 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." }, "token": { "service-tokens": "Jetons de service", diff --git a/frontend/public/locales/ko/translations.json b/frontend/public/locales/ko/translations.json index b40109400..bd86c9fdc 100644 --- a/frontend/public/locales/ko/translations.json +++ b/frontend/public/locales/ko/translations.json @@ -186,7 +186,8 @@ "validate-too-short": "14 글자 이상", "validate-too-long": "100 자 이하", "validate-number": "1개 이상의 숫자", - "validate-case": "1개 이상의 소문자" + "validate-case": "1개 이상의 소문자", + "validate-breached": "귀하가 제공한 비밀번호는 다른 웹사이트에서 일반적으로 사용되는 비밀번호 목록에 포함되어 있습니다. 더 강력한 비밀번호로 다시 시도해 주세요." }, "token": { "add-dialog": { diff --git a/frontend/public/locales/pt-BR/translations.json b/frontend/public/locales/pt-BR/translations.json index 805abd126..9412facdb 100644 --- a/frontend/public/locales/pt-BR/translations.json +++ b/frontend/public/locales/pt-BR/translations.json @@ -214,7 +214,8 @@ "validate-too-short": "pelo menos 14 caracteres", "validate-too-long": "no máximo 100 caracteres", "validate-number": "pelo menos 1 número", - "validate-case": "pelo menos 1 caractere minúsculo" + "validate-case": "pelo menos 1 caractere minúsculo", + "validate-breached": "A senha que você forneceu está em uma lista de senhas comumente usadas em outros sites. Tente novamente com uma senha mais forte." }, "token": { "service-tokens": "Tokens de Serviço", diff --git a/frontend/public/locales/tr/translations.json b/frontend/public/locales/tr/translations.json index a03b9eb2c..53d1bfddc 100644 --- a/frontend/public/locales/tr/translations.json +++ b/frontend/public/locales/tr/translations.json @@ -232,7 +232,8 @@ "validate-too-short": "en az 14 karakter", "validate-too-long": "en fazla 100 karakter", "validate-number": "en az 1 rakam", - "validate-case": "en az 1 küçük harf" + "validate-case": "en az 1 küçük harf", + "validate-breached": "Sağladığınız şifre, diğer web sitelerinde yaygın olarak kullanılan şifreler listesinde yer almaktadır. Lütfen daha güçlü bir şifre ile tekrar deneyiniz." }, "token": { "service-tokens": "Servis Belirteçleri", From 1f60a3d73e799309d5579251b09884d7fb19050c Mon Sep 17 00:00:00 2001 From: Joel Biddle Date: Tue, 22 Aug 2023 22:42:02 +1000 Subject: [PATCH 16/33] fixed more error handling for password checks & translations --- frontend/public/locales/en/translations.json | 7 +- frontend/public/locales/es/translations.json | 11 +- frontend/public/locales/fr/translations.json | 7 +- frontend/public/locales/ko/translations.json | 11 +- .../public/locales/pt-BR/translations.json | 5 +- frontend/public/locales/tr/translations.json | 7 +- .../src/components/signup/UserInfoStep.tsx | 93 ++++++++------ .../utilities/checks/PasswordCheck.ts | 79 +++++++----- .../utilities/checks/checkPassword.ts | 24 +++- frontend/src/pages/password-reset.tsx | 89 ++++++++++--- .../ChangePasswordSection.tsx | 4 +- .../UserInfoSSOStep/UserInfoSSOStep.tsx | 120 ++++++++++-------- 12 files changed, 294 insertions(+), 163 deletions(-) diff --git a/frontend/public/locales/en/translations.json b/frontend/public/locales/en/translations.json index 84554d8fc..8bac30094 100644 --- a/frontend/public/locales/en/translations.json +++ b/frontend/public/locales/en/translations.json @@ -234,9 +234,12 @@ "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-case": "at least 1 lowercase character", - "validate-breached": "The password you provided is in a list of passwords commonly used on other websites. Please try again with a stronger password." + "validate-special-char": "at least 1 special character", + "validate-repeated-char": "at most 2 repeated consecutive characters", + "validate-is-breached": "The password you provided is in a list of passwords commonly used on other websites. Please try again with a stronger password." }, "token": { "service-tokens": "Service Tokens", diff --git a/frontend/public/locales/es/translations.json b/frontend/public/locales/es/translations.json index ffb0a5dd4..7f3bbf84f 100644 --- a/frontend/public/locales/es/translations.json +++ b/frontend/public/locales/es/translations.json @@ -229,11 +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": "como mínimo 14 caracteres", + "validate-too-short": "al menos 14 caracteres", "validate-too-long": "como máximo 100 caracteres", - "validate-number": "como mínimo 1 número", - "validate-case": "como mínimo 1 letra en minúsculas", - "validate-breached": "La contraseña que proporcionó se encuentra en una lista de contraseñas comúnmente utilizadas en otros sitios web. Inténtelo de nuevo con una contraseña más segura." + "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": "au plus 2 caracteres consecutivos repetidos", + "validate-breached": "La contraseña que proporcionó 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." }, "token": { "service-tokens": "Tokens de servicio", diff --git a/frontend/public/locales/fr/translations.json b/frontend/public/locales/fr/translations.json index 7ce136191..25e2de3c0 100644 --- a/frontend/public/locales/fr/translations.json +++ b/frontend/public/locales/fr/translations.json @@ -218,9 +218,12 @@ "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-case": "au moins 1 caractère miniscule", - "validate-breached": "Le mot de passe que vous avez fourni figure 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-special-char": "au moins 1 caractère spécial", + "validate-repeated-char": "au plus 2 caractères consécutifs répétés", + "validate-is-breached": "Le mot de passe que vous avez fourni figure 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." }, "token": { "service-tokens": "Jetons de service", diff --git a/frontend/public/locales/ko/translations.json b/frontend/public/locales/ko/translations.json index bd86c9fdc..c6bec94f4 100644 --- a/frontend/public/locales/ko/translations.json +++ b/frontend/public/locales/ko/translations.json @@ -183,10 +183,13 @@ "new": "새 비밀번호", "current-wrong": "현재 비밀번호가 잘못되었어요", "validate-base": "비밀번호는 다음 조건을 만족해야 합니다:", - "validate-too-short": "14 글자 이상", - "validate-too-long": "100 자 이하", - "validate-number": "1개 이상의 숫자", - "validate-case": "1개 이상의 소문자", + "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-breached": "귀하가 제공한 비밀번호는 다른 웹사이트에서 일반적으로 사용되는 비밀번호 목록에 포함되어 있습니다. 더 강력한 비밀번호로 다시 시도해 주세요." }, "token": { diff --git a/frontend/public/locales/pt-BR/translations.json b/frontend/public/locales/pt-BR/translations.json index 9412facdb..d6958f5a8 100644 --- a/frontend/public/locales/pt-BR/translations.json +++ b/frontend/public/locales/pt-BR/translations.json @@ -213,8 +213,11 @@ "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-case": "pelo menos 1 caractere minúsculo", + "validate-special-char": "pelo menos 1 caractere especial", + "validate-repeated-char": "au plus 2 caractères consécutifs répétés", "validate-breached": "A senha que você forneceu está em uma lista de senhas comumente usadas em outros sites. Tente novamente com uma senha mais forte." }, "token": { diff --git a/frontend/public/locales/tr/translations.json b/frontend/public/locales/tr/translations.json index 53d1bfddc..cf441d0ae 100644 --- a/frontend/public/locales/tr/translations.json +++ b/frontend/public/locales/tr/translations.json @@ -231,8 +231,11 @@ "validate-base": "Şifre kısıtlamaları:", "validate-too-short": "en az 14 karakter", "validate-too-long": "en fazla 100 karakter", - "validate-number": "en az 1 rakam", - "validate-case": "en az 1 küçük harf", + "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": "veya artı 2 karakter ardışık tekrar", "validate-breached": "Sağladığınız şifre, diğer web sitelerinde yaygın olarak kullanılan şifreler listesinde yer almaktadır. Lütfen daha güçlü bir şifre ile tekrar deneyiniz." }, "token": { diff --git a/frontend/src/components/signup/UserInfoStep.tsx b/frontend/src/components/signup/UserInfoStep.tsx index 581298c42..45ae09d0d 100644 --- a/frontend/src/components/signup/UserInfoStep.tsx +++ b/frontend/src/components/signup/UserInfoStep.tsx @@ -39,14 +39,15 @@ interface UserInfoStepProps { } type Errors = { - tooShort?: string, - tooLong?: string, - upperCase?: string, - lowerCase?: string, - number?: string, - specialChar?: string, - repeatedChar?: string, - breachedPassword?: string + tooShort?: string; + tooLong?: string; + upperCase?: string; + lowerCase?: string; + number?: string; + specialChar?: string; + repeatedChar?: string; + isBeachedPassword?: string; + isCommonPassword?: string; }; /** @@ -73,7 +74,7 @@ export default function UserInfoStep({ setOrganizationName, attributionSource, setAttributionSource, - providerAuthToken, + providerAuthToken }: UserInfoStepProps): JSX.Element { const { data: commonPasswords } = useGetCommonPasswords(); const [nameError, setNameError] = useState(false); @@ -101,7 +102,7 @@ export default function UserInfoStep({ } else { setOrganizationNameError(false); } - + errorCheck = await checkPassword({ password, commonPasswords, @@ -176,7 +177,7 @@ export default function UserInfoStep({ salt: result.salt, verifier: result.verifier, organizationName, - attributionSource, + attributionSource }); // unset signup JWT token and set JWT token @@ -193,7 +194,7 @@ export default function UserInfoStep({ }); const userOrgs = await fetchOrganizations(); - + const orgId = userOrgs[0]?._id; const project = await ProjectService.initProject({ organizationId: orgId, @@ -217,13 +218,15 @@ export default function UserInfoStep({ }; return ( -
-

+

+

{t("signup.step3-message")}

-
-
-

Your Name

+
+
+

+ Your Name +

setName(e.target.value)} @@ -232,10 +235,16 @@ export default function UserInfoStep({ autoComplete="given-name" className="h-12" /> - {nameError &&

Please, specify your name

} + {nameError && ( +

+ Please, specify your name +

+ )}
-
-

Organization Name

+
+

+ Organization Name +

setOrganizationName(e.target.value)} @@ -243,10 +252,16 @@ export default function UserInfoStep({ isRequired className="h-12" /> - {organizationNameError &&

Please, specify your organization name

} + {organizationNameError && ( +

+ Please, specify your organization name +

+ )}
-
-

Where did you hear about us? (optional)

+
+

+ Where did you hear about us? (optional) +

setAttributionSource(e.target.value)} @@ -254,7 +269,7 @@ export default function UserInfoStep({ className="h-12" />
-
+
{ @@ -274,23 +289,20 @@ export default function UserInfoStep({ /> {Object.keys(errors).length > 0 && (
-
{t("section.password.validate-base")}
+
+ {t("section.password.validate-base")} +
{Object.keys(errors).map((key) => { if (errors[key as keyof Errors]) { return ( -
+
-
-

- {errors[key as keyof Errors]} -

+

{errors[key as keyof Errors]}

); } @@ -300,18 +312,21 @@ export default function UserInfoStep({
)}
-
-
+
+
+ > + {" "} + {String(t("signup.signup"))}{" "} +
diff --git a/frontend/src/components/utilities/checks/PasswordCheck.ts b/frontend/src/components/utilities/checks/PasswordCheck.ts index 133eb5a35..be0a275f4 100644 --- a/frontend/src/components/utilities/checks/PasswordCheck.ts +++ b/frontend/src/components/utilities/checks/PasswordCheck.ts @@ -6,9 +6,12 @@ interface PasswordCheckProps { errorCheck: boolean; setPasswordErrorTooShort: (value: boolean) => void; setPasswordErrorTooLong: (value: boolean) => void; - setPasswordErrorNumber: (value: boolean) => void; + setPasswordErrorUpperCase: (value: boolean) => void; setPasswordErrorLowerCase: (value: boolean) => void; - setPasswordErrorIsBreached: (value: boolean) => void; + setPasswordErrorNumber: (value: boolean) => void; + setPasswordErrorSpecialChar: (value: boolean) => void; + setPasswordErrorRepeatedChar: (value: boolean) => void; + setPasswordErrorIsBreachedPassword: (value: boolean) => void; } /** @@ -18,11 +21,15 @@ const passwordCheck = async ({ password, setPasswordErrorTooShort, setPasswordErrorTooLong, - setPasswordErrorNumber, + setPasswordErrorUpperCase, setPasswordErrorLowerCase, - setPasswordErrorIsBreached, + setPasswordErrorNumber, + setPasswordErrorSpecialChar, + setPasswordErrorRepeatedChar, + setPasswordErrorIsBreachedPassword, errorCheck }: PasswordCheckProps) => { + // tooShort if (!password || password.length < 14) { setPasswordErrorTooShort(true); errorCheck = true; @@ -30,6 +37,7 @@ const passwordCheck = async ({ setPasswordErrorTooShort(false); } + // tooLong if (password.length > 100) { setPasswordErrorTooLong(true); errorCheck = true; @@ -37,51 +45,54 @@ const passwordCheck = async ({ setPasswordErrorTooLong(false); } - if (!/\d/.test(password)) { + // upperCase + if (!/[A-Z]/.test(password)) { + setPasswordErrorUpperCase(true); + errorCheck = true; + } else { + setPasswordErrorUpperCase(false); + } + + // lowerCase + if (!/[a-z]/.test(password)) { + setPasswordErrorLowerCase(true); + errorCheck = true; + } else { + setPasswordErrorLowerCase(false); + } + + // number + if (!/[0-9]/.test(password)) { setPasswordErrorNumber(true); errorCheck = true; } else { setPasswordErrorNumber(false); } - if (!/[a-z]/.test(password)) { - setPasswordErrorLowerCase(true); + // specialChar + if (!/[!@#$%^&*(),.?":{}|<>]/.test(password)) { + setPasswordErrorSpecialChar(true); errorCheck = true; - // } else if (/(.)(?:(?!\1).){1,2}/.test(password)) { - // console.log(111) - // setPasswordError(true); - // setPasswordErrorMessage("Password should not contain repeating characters."); - // errorCheck = true; - // } else if (RegExp(`[${email}]`).test(password)) { - // console.log(222) - // setPasswordError(true); - // setPasswordErrorMessage("Password should not contain your email."); - // errorCheck = true; } else { - setPasswordErrorLowerCase(false); + setPasswordErrorSpecialChar(false); } + // repeatedChar + if (/([A-Za-z0-9])\1\1\1/.test(password)) { + setPasswordErrorRepeatedChar(true); + errorCheck = true; + } else { + setPasswordErrorRepeatedChar(false); + } + + // breachedPassword if (await checkIsPasswordBreached(password)) { - setPasswordErrorIsBreached(true); + setPasswordErrorIsBreachedPassword(true); errorCheck = true; } else { - setPasswordErrorIsBreached(false); + setPasswordErrorIsBreachedPassword(false); } - // if (!/[A-Z]/.test(password)) { - // setPasswordErrorUpperCase(true); - // errorCheck = true; - // } else { - // setPasswordErrorUpperCase(false); - // } - - // if (!/(?=.*[!@#$%^&*])/.test(password)) { - // setPasswordErrorSpecialChar(true); - // // "Please add at least 1 special character (*, !, #, %)." - // errorCheck = true; - // } else { - // setPasswordErrorSpecialChar(false); - // } return errorCheck; }; diff --git a/frontend/src/components/utilities/checks/checkPassword.ts b/frontend/src/components/utilities/checks/checkPassword.ts index 210d3f040..8ad4c6032 100644 --- a/frontend/src/components/utilities/checks/checkPassword.ts +++ b/frontend/src/components/utilities/checks/checkPassword.ts @@ -8,8 +8,8 @@ type Errors = { number?: string; specialChar?: string; repeatedChar?: string; - commonPassword?: string; - breachedPassword?: string; + isBreachedPassword?: string; + isCommonPassword?: string; }; interface CheckPasswordParams { @@ -20,12 +20,15 @@ interface CheckPasswordParams { /** * Validate that the password [password]: - * - Contains at least 14 characters long - * - Contains at most 100 characters long + * - 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 * - Does not contain 3 repeat, consecutive characters + * - Is not in a database of breached passwords + * - Is not in a list of common passwords * * The function returns whether or not the password [password] * passes the minimum requirements above. It sets errors on @@ -42,41 +45,50 @@ const checkPassword = async ({ }: CheckPasswordParams): Promise => { const errors: Errors = {}; + // tooShort if (password.length < 14) { errors.tooShort = "at least 14 characters"; } + // toolong if (password.length > 100) { errors.tooLong = "at most 100 characters"; } + // upperCase if (!/[A-Z]/.test(password)) { errors.upperCase = "at least 1 uppercase character (A-Z)"; } + // lowerCase if (!/[a-z]/.test(password)) { errors.lowerCase = "at least 1 lowercase character (a-z)"; } + // number if (!/[0-9]/.test(password)) { errors.number = "at least 1 number (0-9)"; } + // specialChar if (!/[!@#$%^&*(),.?":{}|<>]/.test(password)) { errors.specialChar = "at least 1 special character (!@#$%^&*(),.?)"; } + // repeatedChar if (/([A-Za-z0-9])\1\1\1/.test(password)) { errors.repeatedChar = "No 3 repeat, consecutive characters"; } + // breachedPassword if (await checkIsPasswordBreached(password)) { - errors.breachedPassword = + errors.isBreachedPassword = "The password you provided is in a list of passwords commonly used on other websites. Please try again with a stronger password."; } + // commonPassword if (commonPasswords.includes(password)) { - errors.commonPassword = "No common passwords"; + errors.isCommonPassword = "No common passwords"; } setErrors(errors); diff --git a/frontend/src/pages/password-reset.tsx b/frontend/src/pages/password-reset.tsx index 6aa3ea275..4a58f09fb 100644 --- a/frontend/src/pages/password-reset.tsx +++ b/frontend/src/pages/password-reset.tsx @@ -30,9 +30,12 @@ export default function PasswordReset() { const [backupKeyError, setBackupKeyError] = useState(false); const [passwordErrorTooShort, setPasswordErrorTooShort] = useState(false); const [passwordErrorTooLong, setPasswordErrorTooLong] = useState(false); - const [passwordErrorNumber, setPasswordErrorNumber] = useState(false); + const [passwordErrorUpperCase, setPasswordErrorUpperCase] = useState(false); const [passwordErrorLowerCase, setPasswordErrorLowerCase] = useState(false); - const [passwordErrorIsBreached, setPasswordErrorIsBreached] = useState(false); + const [passwordErrorNumber, setPasswordErrorNumber] = useState(false); + const [passwordErrorSpecialChar, setPasswordErrorSpecialChar] = useState(false); + const [passwordErrorRepeatedChar, setPasswordErrorRepeatedChar] = useState(false); + const [passwordErrorIsBreachedPassword, setPasswordErrorIsBreachedPassword] = useState(false); const router = useRouter(); @@ -43,7 +46,7 @@ export default function PasswordReset() { const token = parsedUrl.token as string; const email = (parsedUrl.to as string)?.replace(" ", "+").trim(); - // Unencrypt the private key with a backup key + // Decrypt the private key with a backup key const getEncryptedKeyHandler = async (e: FormEvent) => { e.preventDefault(); try { @@ -71,9 +74,12 @@ export default function PasswordReset() { password: newPassword, setPasswordErrorTooShort, setPasswordErrorTooLong, - setPasswordErrorNumber, + setPasswordErrorUpperCase, setPasswordErrorLowerCase, - setPasswordErrorIsBreached, + setPasswordErrorNumber, + setPasswordErrorSpecialChar, + setPasswordErrorRepeatedChar, + setPasswordErrorIsBreachedPassword, errorCheck: false }); @@ -229,9 +235,12 @@ export default function PasswordReset() { password, setPasswordErrorTooShort, setPasswordErrorTooLong, - setPasswordErrorNumber, + setPasswordErrorUpperCase, setPasswordErrorLowerCase, - setPasswordErrorIsBreached, + setPasswordErrorNumber, + setPasswordErrorSpecialChar, + setPasswordErrorRepeatedChar, + setPasswordErrorIsBreachedPassword, errorCheck: false }); }} @@ -241,9 +250,12 @@ export default function PasswordReset() { error={ passwordErrorTooShort && passwordErrorTooLong && - passwordErrorNumber && + passwordErrorUpperCase && passwordErrorLowerCase && - passwordErrorIsBreached + passwordErrorNumber && + passwordErrorSpecialChar && + passwordErrorRepeatedChar && + passwordErrorIsBreachedPassword } autoComplete="new-password" id="new-password" @@ -251,9 +263,12 @@ export default function PasswordReset() {
{passwordErrorTooShort || passwordErrorTooLong || - passwordErrorNumber || + passwordErrorUpperCase || passwordErrorLowerCase || - passwordErrorIsBreached ? ( + passwordErrorNumber || + passwordErrorSpecialChar || + passwordErrorRepeatedChar || + passwordErrorIsBreachedPassword ? (
Password should contain:
@@ -277,13 +292,15 @@ export default function PasswordReset() {
- {passwordErrorNumber ? ( + {passwordErrorUpperCase ? ( ) : ( )} -
- at least 1 number +
+ at least 1 uppercase character
@@ -297,14 +314,54 @@ export default function PasswordReset() { > at least 1 lowercase character
+
+
+ {passwordErrorNumber ? ( + + ) : ( + + )} +
+ at least 1 number +
- {passwordErrorIsBreached ? ( + {passwordErrorSpecialChar ? ( ) : ( )}
+ at least 1 special character +
+
+
+ {passwordErrorRepeatedChar ? ( + + ) : ( + + )} +
+ at most 2 repeated characters +
+
+
+ {passwordErrorIsBreachedPassword ? ( + + ) : ( + + )} +
The password you provided is in a list of passwords commonly used on other websites. Please try again with a stronger password. diff --git a/frontend/src/views/Settings/PersonalSettingsPage/ChangePasswordSection/ChangePasswordSection.tsx b/frontend/src/views/Settings/PersonalSettingsPage/ChangePasswordSection/ChangePasswordSection.tsx index 86cb58a01..725fb0e47 100644 --- a/frontend/src/views/Settings/PersonalSettingsPage/ChangePasswordSection/ChangePasswordSection.tsx +++ b/frontend/src/views/Settings/PersonalSettingsPage/ChangePasswordSection/ChangePasswordSection.tsx @@ -21,8 +21,8 @@ type Errors = { number?: string; specialChar?: string; repeatedChar?: string; - commonPassword?: string; - breachedPassword?: string; + isBreachedPassword?: string; + isCommonPassword?: 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 c44fab790..f142a2bef 100644 --- a/frontend/src/views/Signup/components/UserInfoSSOStep/UserInfoSSOStep.tsx +++ b/frontend/src/views/Signup/components/UserInfoSSOStep/UserInfoSSOStep.tsx @@ -1,4 +1,3 @@ - import crypto from "crypto"; import React, { useEffect, useState } from "react"; @@ -25,24 +24,25 @@ import ProjectService from "@app/services/ProjectService"; const client = new jsrp.client(); type Props = { - setStep: (step: number) => void; - email: string; - password: string; - setPassword: (value: string) => void; - name: string; - providerOrganizationName: string; - providerAuthToken?: string; -} + setStep: (step: number) => void; + email: string; + password: string; + setPassword: (value: string) => void; + name: string; + providerOrganizationName: string; + providerAuthToken?: string; +}; type Errors = { - tooShort?: string, - tooLong?: string, - upperCase?: string, - lowerCase?: string, - number?: string, - specialChar?: string, - repeatedChar?: string, - breachedPassword?: string + tooShort?: string; + tooLong?: string; + upperCase?: string; + lowerCase?: string; + number?: string; + specialChar?: string; + repeatedChar?: string; + isBeachedPassword?: string; + isCommonPassword?: string; }; /** @@ -65,7 +65,7 @@ export const UserInfoSSOStep = ({ password, setPassword, setStep, - providerAuthToken, + providerAuthToken }: Props) => { const { data: commonPasswords } = useGetCommonPasswords(); const [nameError, setNameError] = useState(false); @@ -99,7 +99,7 @@ export const UserInfoSSOStep = ({ } else { setOrganizationNameError(false); } - + errorCheck = await checkPassword({ password, commonPasswords, @@ -212,15 +212,17 @@ export const UserInfoSSOStep = ({ setIsLoading(false); } }; - + return ( -
-

+

+

{t("signup.step3-message")}

-
-
-

Your Name

+
+
+

+ Your Name +

- {nameError &&

Please, specify your name

} + {nameError && ( +

+ Please, specify your name +

+ )}
{providerOrganizationName === undefined && ( -
-

Organization Name

+
+

+ Organization Name +

- {organizationNameError &&

Please, specify your organization name

} + {organizationNameError && ( +

+ Please, specify your organization name +

+ )}
)} {providerOrganizationName === undefined && ( -
-

Where did you hear about us? (optional)

+
+

+ Where did you hear about us? (optional) +

setAttributionSource(e.target.value)} @@ -256,12 +270,12 @@ export const UserInfoSSOStep = ({ />
)} -
+
{ + onChangeHandler={async (pass: string) => { setPassword(pass); - checkPassword({ + await checkPassword({ password: pass, commonPasswords, setErrors @@ -274,26 +288,27 @@ export const UserInfoSSOStep = ({ autoComplete="new-password" id="new-password" /> -
Infisical Password is used as part of the encryption mechanism so that even the authentication provider is not able to access your secrets.
+
+ + Infisical Password is used as part of the encryption mechanism so that even the + authentication provider is not able to access your secrets. +
{Object.keys(errors).length > 0 && (
-
{t("section.password.validate-base")}
+
+ {t("section.password.validate-base")} +
{Object.keys(errors).map((key) => { if (errors[key as keyof Errors]) { return ( -
+
-
-

- {errors[key as keyof Errors]} -

+

{errors[key as keyof Errors]}

); } @@ -303,21 +318,24 @@ export const UserInfoSSOStep = ({
)}
-
-
+
+
+ > + {" "} + {String(t("signup.signup"))}{" "} +
); -} +}; From 3e36adcf5c47290fe7d6574d9a1dd53549f93651 Mon Sep 17 00:00:00 2001 From: Joel Biddle Date: Tue, 22 Aug 2023 23:30:24 +1000 Subject: [PATCH 17/33] Removed all references to commonPasswords & the data file. This api route can be deprecated in favor of the client-side secure call to the haveIBeenPwnd password API. Further the datafile contains no passwords that meet the minimum password criteria. --- backend/src/controllers/v1/authController.ts | 181 +- backend/src/data/common_passwords.txt | 1497 ----------------- backend/src/routes/v1/auth.ts | 25 +- .../src/components/signup/UserInfoStep.tsx | 9 +- .../utilities/checks/checkPassword.ts | 14 +- frontend/src/hooks/api/auth/index.tsx | 6 +- frontend/src/hooks/api/auth/queries.tsx | 140 +- frontend/src/pages/signupinvite.tsx | 119 +- .../ChangePasswordSection.tsx | 5 - .../UserInfoSSOStep/UserInfoSSOStep.tsx | 5 - 10 files changed, 208 insertions(+), 1793 deletions(-) delete mode 100644 backend/src/data/common_passwords.txt diff --git a/backend/src/controllers/v1/authController.ts b/backend/src/controllers/v1/authController.ts index 03a9a7717..14c717e38 100644 --- a/backend/src/controllers/v1/authController.ts +++ b/backend/src/controllers/v1/authController.ts @@ -1,32 +1,20 @@ import { Request, Response } from "express"; -import fs from "fs"; -import path from "path"; import jwt from "jsonwebtoken"; import * as bigintConversion from "bigint-conversion"; // eslint-disable-next-line @typescript-eslint/no-var-requires const jsrp = require("jsrp"); -import { - LoginSRPDetail, - TokenVersion, - User, -} from "../../models"; +import { LoginSRPDetail, TokenVersion, User } from "../../models"; import { clearTokens, createToken, issueAuthTokens } from "../../helpers/auth"; import { checkUserDevice } from "../../helpers/user"; -import { - ACTION_LOGIN, - ACTION_LOGOUT, -} from "../../variables"; -import { - BadRequestError, - UnauthorizedRequestError, -} from "../../utils/errors"; +import { ACTION_LOGIN, ACTION_LOGOUT } from "../../variables"; +import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors"; import { EELogService } from "../../ee/services"; import { getUserAgentType } from "../../utils/posthog"; import { getHttpsEnabled, getJwtAuthLifetime, getJwtAuthSecret, - getJwtRefreshSecret, + getJwtRefreshSecret } from "../../config"; import { ActorType } from "../../ee/models"; @@ -44,13 +32,10 @@ declare module "jsonwebtoken" { * @returns */ export const login1 = async (req: Request, res: Response) => { - const { - email, - clientPublicKey, - }: { email: string; clientPublicKey: string } = req.body; + const { email, clientPublicKey }: { email: string; clientPublicKey: string } = req.body; const user = await User.findOne({ - email, + email }).select("+salt +verifier"); if (!user) throw new Error("Failed to find user"); @@ -59,21 +44,25 @@ export const login1 = async (req: Request, res: Response) => { server.init( { salt: user.salt, - verifier: user.verifier, + verifier: user.verifier }, async () => { // generate server-side public key const serverPublicKey = server.getPublicKey(); - await LoginSRPDetail.findOneAndReplace({ email: email }, { - email: email, - clientPublicKey: clientPublicKey, - serverBInt: bigintConversion.bigintToBuf(server.bInt), - }, { upsert: true, returnNewDocument: false }) + await LoginSRPDetail.findOneAndReplace( + { email: email }, + { + email: email, + clientPublicKey: clientPublicKey, + serverBInt: bigintConversion.bigintToBuf(server.bInt) + }, + { upsert: true, returnNewDocument: false } + ); return res.status(200).send({ serverPublicKey, - salt: user.salt, + salt: user.salt }); } ); @@ -89,15 +78,19 @@ export const login1 = async (req: Request, res: Response) => { export const login2 = async (req: Request, res: Response) => { const { email, clientProof } = req.body; const user = await User.findOne({ - email, + email }).select("+salt +verifier +publicKey +encryptedPrivateKey +iv +tag"); if (!user) throw new Error("Failed to find user"); - const loginSRPDetailFromDB = await LoginSRPDetail.findOneAndDelete({ email: email }) + const loginSRPDetailFromDB = await LoginSRPDetail.findOneAndDelete({ email: email }); if (!loginSRPDetailFromDB) { - return BadRequestError(Error("It looks like some details from the first login are not found. Please try login one again")) + return BadRequestError( + Error( + "It looks like some details from the first login are not found. Please try login one again" + ) + ); } const server = new jsrp.server(); @@ -105,7 +98,7 @@ export const login2 = async (req: Request, res: Response) => { { salt: user.salt, verifier: user.verifier, - b: loginSRPDetailFromDB.serverBInt, + b: loginSRPDetailFromDB.serverBInt }, async () => { server.setClientPublicKey(loginSRPDetailFromDB.clientPublicKey); @@ -117,13 +110,13 @@ export const login2 = async (req: Request, res: Response) => { await checkUserDevice({ user, ip: req.realIP, - userAgent: req.headers["user-agent"] ?? "", + userAgent: req.headers["user-agent"] ?? "" }); - const tokens = await issueAuthTokens({ + const tokens = await issueAuthTokens({ userId: user._id, ip: req.realIP, - userAgent: req.headers["user-agent"] ?? "", + userAgent: req.headers["user-agent"] ?? "" }); // store (refresh) token in httpOnly cookie @@ -131,20 +124,21 @@ export const login2 = async (req: Request, res: Response) => { httpOnly: true, path: "/", sameSite: "strict", - secure: await getHttpsEnabled(), + secure: await getHttpsEnabled() }); const loginAction = await EELogService.createAction({ name: ACTION_LOGIN, - userId: user._id, + userId: user._id }); - loginAction && await EELogService.createLog({ - userId: user._id, - actions: [loginAction], - channel: getUserAgentType(req.headers["user-agent"]), - ipAddress: req.realIP, - }); + loginAction && + (await EELogService.createLog({ + userId: user._id, + actions: [loginAction], + channel: getUserAgentType(req.headers["user-agent"]), + ipAddress: req.realIP + })); // return (access) token in response return res.status(200).send({ @@ -152,12 +146,12 @@ export const login2 = async (req: Request, res: Response) => { publicKey: user.publicKey, encryptedPrivateKey: user.encryptedPrivateKey, iv: user.iv, - tag: user.tag, + tag: user.tag }); } return res.status(400).send({ - message: "Failed to authenticate. Try again?", + message: "Failed to authenticate. Try again?" }); } ); @@ -171,7 +165,7 @@ export const login2 = async (req: Request, res: Response) => { */ export const logout = async (req: Request, res: Response) => { if (req.authData.actor.type === ActorType.USER && req.authData.tokenVersionId) { - await clearTokens(req.authData.tokenVersionId) + await clearTokens(req.authData.tokenVersionId); } // clear httpOnly cookie @@ -179,49 +173,44 @@ export const logout = async (req: Request, res: Response) => { httpOnly: true, path: "/", sameSite: "strict", - secure: (await getHttpsEnabled()) as boolean, + secure: (await getHttpsEnabled()) as boolean }); const logoutAction = await EELogService.createAction({ name: ACTION_LOGOUT, - userId: req.user._id, + userId: req.user._id }); - logoutAction && await EELogService.createLog({ - userId: req.user._id, - actions: [logoutAction], - channel: getUserAgentType(req.headers["user-agent"]), - ipAddress: req.realIP, - }); + logoutAction && + (await EELogService.createLog({ + userId: req.user._id, + actions: [logoutAction], + channel: getUserAgentType(req.headers["user-agent"]), + ipAddress: req.realIP + })); return res.status(200).send({ - message: "Successfully logged out.", + message: "Successfully logged out." }); }; -export const getCommonPasswords = async (req: Request, res: Response) => { - const commonPasswords = fs.readFileSync( - path.resolve(__dirname, "../../data/" + "common_passwords.txt"), - "utf8" - ).split("\n"); - - return res.status(200).send(commonPasswords); -} - export const revokeAllSessions = async (req: Request, res: Response) => { - await TokenVersion.updateMany({ - user: req.user._id, - }, { - $inc: { - refreshVersion: 1, - accessVersion: 1, + await TokenVersion.updateMany( + { + user: req.user._id }, - }); + { + $inc: { + refreshVersion: 1, + accessVersion: 1 + } + } + ); return res.status(200).send({ - message: "Successfully revoked all sessions.", - }); -} + message: "Successfully revoked all sessions." + }); +}; /** * Return user is authenticated @@ -231,9 +220,9 @@ export const revokeAllSessions = async (req: Request, res: Response) => { */ export const checkAuth = async (req: Request, res: Response) => { return res.status(200).send({ - message: "Authenticated", + message: "Authenticated" }); -} +}; /** * Return new JWT access token by first validating the refresh token @@ -244,47 +233,47 @@ export const checkAuth = async (req: Request, res: Response) => { export const getNewToken = async (req: Request, res: Response) => { const refreshToken = req.cookies.jid; - if (!refreshToken) throw BadRequestError({ - message: "Failed to find refresh token in request cookies" - }); + if (!refreshToken) + throw BadRequestError({ + message: "Failed to find refresh token in request cookies" + }); - const decodedToken = ( - jwt.verify(refreshToken, await getJwtRefreshSecret()) - ); + const decodedToken = jwt.verify(refreshToken, await getJwtRefreshSecret()); const user = await User.findOne({ - _id: decodedToken.userId, + _id: decodedToken.userId }).select("+publicKey +refreshVersion +accessVersion"); if (!user) throw new Error("Failed to authenticate unfound user"); - if (!user?.publicKey) - throw new Error("Failed to authenticate not fully set up account"); - + if (!user?.publicKey) throw new Error("Failed to authenticate not fully set up account"); + const tokenVersion = await TokenVersion.findById(decodedToken.tokenVersionId); - if (!tokenVersion) throw UnauthorizedRequestError({ - message: "Failed to validate refresh token", - }); + if (!tokenVersion) + throw UnauthorizedRequestError({ + message: "Failed to validate refresh token" + }); - if (decodedToken.refreshVersion !== tokenVersion.refreshVersion) throw BadRequestError({ - message: "Failed to validate refresh token", - }); + if (decodedToken.refreshVersion !== tokenVersion.refreshVersion) + throw BadRequestError({ + message: "Failed to validate refresh token" + }); const token = createToken({ payload: { userId: decodedToken.userId, tokenVersionId: tokenVersion._id.toString(), - accessVersion: tokenVersion.refreshVersion, + accessVersion: tokenVersion.refreshVersion }, expiresIn: await getJwtAuthLifetime(), - secret: await getJwtAuthSecret(), + secret: await getJwtAuthSecret() }); return res.status(200).send({ - token, + token }); }; export const handleAuthProviderCallback = (req: Request, res: Response) => { res.redirect(`/login/provider/success?token=${encodeURIComponent(req.providerAuthToken)}`); -} +}; diff --git a/backend/src/data/common_passwords.txt b/backend/src/data/common_passwords.txt deleted file mode 100644 index 01a442b17..000000000 --- a/backend/src/data/common_passwords.txt +++ /dev/null @@ -1,1497 +0,0 @@ -123456 -123456789 -111111 -password -qwerty -abc123 -12345678 -password1 -1234567 -123123 -1234567890 -000000 -12345 -iloveyou -1q2w3e4r5t -1234 -123456a -qwertyuiop -monkey -123321 -dragon -654321 -666666 -123 -myspace1 -a123456 -121212 -1qaz2wsx -123qwe -123abc -tinkle -target123 -gwerty -1g2w3e4r -gwerty123 -zag12wsx -7777777 -qwerty1 -1q2w3e4r -987654321 -222222 -qwe123 -qwerty123 -zxcvbnm -555555 -112233 -fuckyou -asdfghjkl -12345a -123123123 -1q2w3e -qazwsx -computer -aaaaaa -159753 -iloveyou1 -fuckyou1 -princess -789456123 -11111111 -123654 -princess1 -888888 -linkedin -michael -sunshine -football -11111 -777777 -1234qwer -999999 -j38ifUbn -monkey1 -football1 -daniel -azerty -a12345 -123456789a -789456 -asdfgh -love123 -abcd1234 -jordan23 -88888888 -5201314 -12qwaszx -FQRG7CS493 -ashley -asdf -asd123 -superman -jessica -love -samsung -shadow -blink182 -333333 -michael1 -babygirl1 -jesus1 -qwert -k.: -baseball -charlie -0 -hello1 -soccer -killer -131313 -master -1111111 -gfhjkm -0123456789 -987654 -iloveyou2 -angel1 -jordan -147258369 -bitch1 -michelle -q1w2e3r4 -jessica1 -qwer1234 -159357 -soccer1 -liverpool -101010 -zxcvbn -thomas -asdasd -fuckyou2 -justin -nicole -1111111111 -1 -1111 -qazwsxedc -baseball1 -andrew -hello -apple -0987654321 -anthony1 -102030 -money1 -parola -abc -147258 -anthony -111222 -jennifer -number1 -naruto -123456q -696969 -00000000 -joshua -golfer -29rsavoy -myspace -andrea -basketball -qwerty12 -charlie1 -passw0rd -asshole1 -hunter -marina -welcome -010203 -superman1 -password12 -xbox360 -sunshine1 -ashley1 -lovely -babygirl -! -trustno1 -666 -asdf1234 -chocolate -buster -summer -tigger -purple -freedom -loveme -matthew -50cent -password2 -maggie -george -chelsea -12341234 -amanda -hannah -q1w2e3 -friends -shadow1 -william -abcdefg -samantha -12344321 -nicole1 -q1w2e3r4t5y6 -robert -mother -jordan1 -secret -letmein -qweasdzxc -212121 -pokemon -$HEX -internet -batman -love12 -a123456789 -VQsaBLPzLa -qweqwe -hello123 -232323 -butterfly -martin -flower -forever -mustang -1qazxsw2 -iloveu -cjmasterinf -orange -harley -user -brandon1 -london -1234567891 -pepper -chris1 -lol123 -abcdef -whatever -1342 -alexander -loveyou -290966 -wall.e -junior -12413 -qweasd -PE#5GZ29PTZMSE -tudelft -dpbk1234 -DIOSESFIEL -U38fa39 -147852 -cookie -family -jasmine -dragon1 -12345q -nikita -pakistan -123654789 -123789 -amanda1 -joseph -happy1 -ginger -: -matthew1 -snoopy -justin1 -lastfm -3rJs1la7qE -пїЅпїЅпїЅпїЅпїЅпїЅ -antonio -barcelona -matrix -computer1 -hottie1 -sophie -sandra -michelle1 -12345678910 -qqqqqq -arsenal -444444 -brandon -daniel1 -jonathan -killer1 -liverpool1 -mickey -ghbdtn -purple1 -mercedes -patrick -11223344 -diamond -456789 -victoria -asshole -taylor -qwertyu -andrew1 -red123 -lucky1 -eminem -12345qwert -111222tianya -yellow -william1 -bailey -angel -chicken1 -richard -0000 -banana -0000000000 -jasmine1 -benjamin -welcome1 -starwars -hunter1 -cheese -melissa -angela -christian -1234554321 -oliver -chocolate1 -butterfly1 -peanut -55555 -hockey -mylove -natasha -NULL -mommy1 -1234561 -q1w2e3r4t5 -america -252525 -monster -school -456123 -james1 -slipknot -hannah1 -zaq12wsx -chicken -147852369 -gabriel -elizabeth -cookie1 -Status -87654321 -robert1 -ferrari -nathan -1password -buddy1 -1314520 -america1 -metallica -chelsea1 -zzzzzz -prince -adidas -jackson -morgan -rainbow -silver -1234567a -angels -iw14Fi9j -loveme1 -juventus -jennifer1 -!~!1 -bubbles -samuel -fuckoff -lovers -cheese1 -0123456 -123asd -999999999 -madison -elizabeth1 -music -buster1 -lauren -david1 -tigger1 -123qweasd -taylor1 -carlos -tinkerbell -samantha1 -Sojdlg123aljg -joshua1 -poop -stella -myspace123 -asdasd5 -freedom1 -whatever1 -xxxxxx -00000 -valentina -a1b2c3 -741852963 -austin -monica -qaz123 -lovely1 -music1 -harley1 -family1 -spongebob1 -steven -nirvana -1234abcd -hellokitty -thomas1 -7654321 -madison1 -daddy1 -summer1 -cocacola -nicholas -zxc123 -123456m -qwertyui -spiderman -vanessa -diamond1 -142536 -danielle -badoo -7758521 -bandit -pokemon1 -mustang1 -1qaz2wsx3edc -alexis -loulou -justinbieb -yamaha -qwert1 -scooter -rachel -tennis -ronaldo -i -mexico1 -friends1 -victor -maggie1 -asdfasdf -qwerty12345 -lover1 -jesus -123hfjdk147 -nicolas -batman1 -weed420 -password123 -loser1 -123456j -iloveyou! -pepper1 -fuckoff1 -555666 -iloveu2 -sabrina -pussy1 -bubbles1 -098765 -master1 -smokey -a1b2c3d4 -123456789q -qwaszx -heather -jasper -booboo -heather1 -4815162342 -peanut1 -chester -123456s -123456b -google -edward -yankees1 -canada -Exigent -destiny -success -nigger1 -135790 -asdfghjkl1 -124578 -casper -lalala -mother1 -sexy123 -qazxsw -naruto1 -1q2w3e4r5t6y -david -money -yellow1 -patrick1 -flower1 -12121212 -alexander1 -raiders1 -Password1 -sebastian -134679 -zxcvbnm1 -dennis -852456 -hahaha -daniela -ginger1 -olivia -melissa1 -010101 -slipknot1 -spiderman1 -cowboys1 -0000000 -rebecca -741852 -jeremy -a1234567 -dakota -123456d -1a2b3c -apple1 -november -alexandra -159951 -iloveu1 -veronica -fuckme1 -baby123 -yankees -stupid1 -cristina -newyork1 -jackson1 -playboy -friend -iloveyou12 -sammy1 -pimpin1 -phoenix -PolniyPizdec0211 -rocky1 -password! -joseph1 -753951 -p -a838hfiD -richard1 -beautiful1 -mickey1 -carolina -j123456 -202020 -newyork -patricia -charles -stephanie -orange1 -m123456 -421uiopy258 -myspace2 -cameron -spider -barbie -woaini -vincent -mexico -scorpion -monster1 -aaaaa -elephant -asdf123 -963852741 -zk.: -guitar -fucker1 -destiny1 -hotmail -johnny -doudou -q123456 -bailey1 -asdfgh1 -fucker -louise -sparky -sweety -123456abc -shorty1 -booboo1 -december -9876543210 -manchester -midnight -246810 -jessie -dallas -austin1 -s123456 -pass -12345678a -claudia -пїЅпїЅпїЅпїЅпїЅпїЅпїЅ -kristina -lakers -lovelove -crazy1 -tiger1 -thunder -dolphin -a -gangsta1 -jackie -151515 -charlotte -scooter1 -caroline -fuck -merlin -junior1 -super123 -scooby -marseille -aaaa -metallica1 -kitty1 -chris -beautiful -black1 -danielle1 -blessed1 -skater1 -1029384756 -qazwsx123 -456456 -b123456 -genius -guitar1 -tyler1 -peaches -california -sakura -tigers -soleil -lauren1 -green1 -smokey1 -cooper -520520 -muffin -christian1 -love13 -fucku2 -arsenal1 -lucky7 -diablo -apples -george1 -babyboy1 -crystal -1122334455 -player1 -aa123456 -vfhbyf -forever1 -Password -winston -chivas1 -sexy -hockey1 -1a2b3c4d -pussy -playboy1 -stalker -cherry -tweety -toyota -creative -gemini -pretty1 -пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ -maverick -brittany1 -nathan1 -letmein1 -cameron1 -secret1 -google1 -heaven -martina -murphy -spongebob -uQA9Ebw445 -fernando -pretty -startfinding -softball -dolphin1 -fuckme -test123 -qwerty1234 -kobe24 -alejandro -adrian -september -aaaaaa1 -bubba1 -isabella -abc123456 -password3 -jason1 -abcdefg123 -loveyou1 -shannon -100200 -manuel -leonardo -molly1 -flowers -123456z -007007 -password. -321321 -miguel -samsung1 -sergey -sweet1 -abc1234 -windows -qwert123 -vfrcbv -poohbear -d123456 -school1 -badboy -951753 -123456c -111 -steven1 -snoopy1 -garfield -YAgjecc826 -compaq -candy1 -sarah1 -qwerty123456 -123456l -eminem1 -141414 -789789 -maria -steelers -iloveme1 -morgan1 -winner -boomer -lolita -nastya -alexis1 -carmen -angelo -nicholas1 -portugal -precious -jackass1 -jonathan1 -yfnfif -bitch -tiffany -rabbit -rainbow1 -angel123 -popcorn -barbara -brandy -fuckyou! -starwars1 -barney -natalia -hiphop -tiffany1 -shorty -poohbear1 -simone -albert -marlboro -hardcore -cowboys -sydney -alex -scorpio -1234512345 -q12345 -qq123456 -onelove -bond007 -abcdefg1 -eagles -crystal1 -azertyuiop -winter -sexy12 -angelina -james -svetlana -fatima -123456k -icecream -popcorn1 -121314 -john316 -qazwsx1 -victoria1 -twilight -iloveme -9379992 -pass123 -dancer -brittany -beauty -bonjour -maxwell -coffee -dexter -454545 -qazqaz -snickers -love11 -samson -aaaaaaaa -swordfish -fyfcnfcbz -abcd123 -aaa111 -natalie -hottie -passion -alyssa -rockstar1 -lovers1 -florida -alicia -happy -blue123 -123456t -ranger -yourmom1 -pumpkin -denise -edward1 -tweety1 -christine -august -54321 -bella1 -marie1 -seven7 -steelers1 -aaaaa1 -shannon1 -amber1 -cutie1 -peaches1 -florida1 -bonnie -stephanie1 -lollipop -cassie -k. -rachel1 -greenday1 -krishna -teresa -october -iverson3 -motorola -rockstar -hahaha1 -police -lakers24 -fylhtq -andrey -loveme2 -turtle -southside1 -baby -bismillah -pa55word -blessed -emmanuel -666999 -012345 -fluffy -5555555555 -stupid -karina -fishing -musica -password11 -love4ever -melanie -greenday -isabelle -nothing -abcd -chicago -cowboy -mnbvcxz -andrea1 -242424 -babygurl1 -santiago -ssssss -kevin1 -lakers1 -chester1 -321654 -kimberly -carlos1 -z123456 -daisy1 -jackass -m -5555555 -zoosk -boston -happy123 -55555555 -satan666 -111111a -pamela -090909 -francesco -horses -456852 -qwer -vanessa1 -redsox -pookie -a12345678 -110110 -tucker -marley -corvette -778899 -realmadrid -raiders -rangers -people -1123581321 -soccer12 -sayang -shelby -christ -12345t -fktrcfylh -kitten -player -c123456 -qwert12345 -baby12 -trinity -1v7Upjw3nT -p@ssw0rd -thunder1 -zxcvbnm123 -midnight1 -lebron23 -golden -strawberry -orlando -love1234 -lucky13 -asdfg1 -marine -soccer10123456 -password -12345678 -1234 -pussy -12345 -dragon -qwerty -696969 -mustang -letmein -baseball -master -michael -football -shadow -monkey -abc123 -pass -fuckme -6969 -jordan -harley -ranger -iwantu -jennifer -hunter -fuck -2000 -test -batman -trustno1 -thomas -tigger -robert -access -love -buster -1234567 -soccer -hockey -killer -george -sexy -andrew -charlie -superman -asshole -fuckyou -dallas -jessica -panties -pepper -1111 -austin -william -daniel -golfer -summer -heather -hammer -yankees -joshua -maggie -biteme -enter -ashley -thunder -cowboy -silver -richard -fucker -orange -merlin -michelle -corvette -bigdog -cheese -matthew -121212 -patrick -martin -freedom -ginger -blowjob -nicole -sparky -yellow -camaro -secret -dick -falcon -taylor -111111 -131313 -123123 -bitch -hello -scooter -please -porsche -guitar -chelsea -black -diamond -nascar -jackson -cameron -654321 -computer -amanda -wizard -xxxxxxxx -money -phoenix -mickey -bailey -knight -iceman -tigers -purple -andrea -horny -dakota -aaaaaa -player -sunshine -morgan -starwars -boomer -cowboys -edward -charles -girls -booboo -coffee -xxxxxx -bulldog -ncc1701 -rabbit -peanut -john -johnny -gandalf -spanky -winter -brandy -compaq -carlos -tennis -james -mike -brandon -fender -anthony -blowme -ferrari -cookie -chicken -maverick -chicago -joseph -diablo -sexsex -hardcore -666666 -willie -welcome -chris -panther -yamaha -justin -banana -driver -marine -angels -fishing -david -maddog -hooters -wilson -butthead -dennis -fucking -captain -bigdick -chester -smokey -xavier -steven -viking -snoopy -blue -eagles -winner -samantha -house -miller -flower -jack -firebird -butter -united -turtle -steelers -tiffany -zxcvbn -tomcat -golf -bond007 -bear -tiger -doctor -gateway -gators -angel -junior -thx1138 -porno -badboy -debbie -spider -melissa -booger -1212 -flyers -fish -porn -matrix -teens -scooby -jason -walter -cumshot -boston -braves -yankee -lover -barney -victor -tucker -princess -mercedes -5150 -doggie -zzzzzz -gunner -horney -bubba -2112 -fred -johnson -xxxxx -tits -member -boobs -donald -bigdaddy -bronco -penis -voyager -rangers -birdie -trouble -white -topgun -bigtits -bitches -green -super -qazwsx -magic -lakers -rachel -slayer -scott -2222 -asdf -video -london -7777 -marlboro -srinivas -internet -action -carter -jasper -monster -teresa -jeremy -11111111 -bill -crystal -peter -pussies -cock -beer -rocket -theman -oliver -prince -beach -amateur -7777777 -muffin -redsox -star -testing -shannon -murphy -frank -hannah -dave -eagle1 -11111 -mother -nathan -raiders -steve -forever -angela -viper -ou812 -jake -lovers -suckit -gregory -buddy -whatever -young -nicholas -lucky -helpme -jackie -monica -midnight -college -baby -cunt -brian -mark -startrek -sierra -leather -232323 -4444 -beavis -bigcock -happy -sophie -ladies -naughty -giants -booty -blonde -fucked -golden -0 -fire -sandra -pookie -packers -einstein -dolphins -chevy -winston -warrior -sammy -slut -8675309 -zxcvbnm -nipples -power -victoria -asdfgh -vagina -toyota -travis -hotdog -paris -rock -xxxx -extreme -redskins -erotic -dirty -ford -freddy -arsenal -access14 -wolf -nipple -iloveyou -alex -florida -eric -legend -movie -success -rosebud -jaguar -great -cool -cooper -1313 -scorpio -mountain -madison -987654 -brazil -lauren -japan -naked -squirt -stars -apple -alexis -aaaa -bonnie -peaches -jasmine -kevin -matt -qwertyui -danielle -beaver -4321 -4128 -runner -swimming -dolphin -gordon -casper -stupid -shit -saturn -gemini -apples -august -3333 -canada -blazer -cumming -hunting -kitty -rainbow -112233 -arthur -cream -calvin -shaved -surfer -samson -kelly -paul -mine -king -racing -5555 -eagle -hentai -newyork -little -redwings -smith -sticky -cocacola -animal -broncos -private -skippy -marvin -blondes -enjoy -girl -apollo -parker -qwert -time -sydney -women -voodoo -magnum -juice -abgrtyu -777777 -dreams -maxwell -music -rush2112 -russia -scorpion -rebecca -tester -mistress -phantom -billy -6666 -albert \ No newline at end of file diff --git a/backend/src/routes/v1/auth.ts b/backend/src/routes/v1/auth.ts index b633f82ea..ab141ac05 100644 --- a/backend/src/routes/v1/auth.ts +++ b/backend/src/routes/v1/auth.ts @@ -8,7 +8,8 @@ import { AuthMode } from "../../variables"; router.post("/token", validateRequest, authController.getNewToken); -router.post( // TODO endpoint: deprecate (moved to api/v3/auth/login1) +router.post( + // TODO endpoint: deprecate (moved to api/v3/auth/login1) "/login1", authLimiter, body("email").exists().trim().notEmpty(), @@ -17,7 +18,8 @@ router.post( // TODO endpoint: deprecate (moved to api/v3/auth/login1) authController.login1 ); -router.post( // TODO endpoint: deprecate (moved to api/v3/auth/login2) +router.post( + // TODO endpoint: deprecate (moved to api/v3/auth/login2) "/login2", authLimiter, body("email").exists().trim().notEmpty(), @@ -30,7 +32,7 @@ router.post( "/logout", authLimiter, requireAuth({ - acceptedAuthModes: [AuthMode.JWT], + acceptedAuthModes: [AuthMode.JWT] }), authController.logout ); @@ -38,24 +40,19 @@ router.post( router.post( "/checkAuth", requireAuth({ - acceptedAuthModes: [AuthMode.JWT], + acceptedAuthModes: [AuthMode.JWT] }), authController.checkAuth ); -router.get( - "/common-passwords", - authLimiter, - authController.getCommonPasswords -); - -router.delete( // TODO endpoint: deprecate (moved to DELETE v2/users/me/sessions) +router.delete( + // TODO endpoint: deprecate (moved to DELETE v2/users/me/sessions) "/sessions", authLimiter, requireAuth({ - acceptedAuthModes: [AuthMode.JWT], - }), + acceptedAuthModes: [AuthMode.JWT] + }), authController.revokeAllSessions ); -export default router; \ No newline at end of file +export default router; diff --git a/frontend/src/components/signup/UserInfoStep.tsx b/frontend/src/components/signup/UserInfoStep.tsx index 45ae09d0d..0cce660e0 100644 --- a/frontend/src/components/signup/UserInfoStep.tsx +++ b/frontend/src/components/signup/UserInfoStep.tsx @@ -8,7 +8,6 @@ import jsrp from "jsrp"; import nacl from "tweetnacl"; import { encodeBase64 } from "tweetnacl-util"; -import { useGetCommonPasswords } from "@app/hooks/api"; import { completeAccountSignup } from "@app/hooks/api/auth/queries"; import { fetchOrganizations } from "@app/hooks/api/organization/queries"; import ProjectService from "@app/services/ProjectService"; @@ -47,7 +46,6 @@ type Errors = { specialChar?: string; repeatedChar?: string; isBeachedPassword?: string; - isCommonPassword?: string; }; /** @@ -76,7 +74,6 @@ export default function UserInfoStep({ setAttributionSource, providerAuthToken }: UserInfoStepProps): JSX.Element { - const { data: commonPasswords } = useGetCommonPasswords(); const [nameError, setNameError] = useState(false); const [organizationNameError, setOrganizationNameError] = useState(false); @@ -105,7 +102,6 @@ export default function UserInfoStep({ errorCheck = await checkPassword({ password, - commonPasswords, setErrors }); @@ -272,11 +268,10 @@ export default function UserInfoStep({
{ + onChangeHandler={async (pass: string) => { setPassword(pass); - checkPassword({ + await checkPassword({ password: pass, - commonPasswords, setErrors }); }} diff --git a/frontend/src/components/utilities/checks/checkPassword.ts b/frontend/src/components/utilities/checks/checkPassword.ts index 8ad4c6032..aaa8bac7f 100644 --- a/frontend/src/components/utilities/checks/checkPassword.ts +++ b/frontend/src/components/utilities/checks/checkPassword.ts @@ -9,12 +9,10 @@ type Errors = { specialChar?: string; repeatedChar?: string; isBreachedPassword?: string; - isCommonPassword?: string; }; interface CheckPasswordParams { password: string; - commonPasswords: string[]; setErrors: (value: Errors) => void; } @@ -28,7 +26,6 @@ interface CheckPasswordParams { * - Contains at least 1 special character * - Does not contain 3 repeat, consecutive characters * - Is not in a database of breached passwords - * - Is not in a list of common passwords * * The function returns whether or not the password [password] * passes the minimum requirements above. It sets errors on @@ -38,11 +35,7 @@ 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, - commonPasswords, - setErrors -}: CheckPasswordParams): Promise => { +const checkPassword = async ({ password, setErrors }: CheckPasswordParams): Promise => { const errors: Errors = {}; // tooShort @@ -86,11 +79,6 @@ const checkPassword = async ({ "The password you provided is in a list of passwords commonly used on other websites. Please try again with a stronger password."; } - // commonPassword - if (commonPasswords.includes(password)) { - errors.isCommonPassword = "No common passwords"; - } - setErrors(errors); return Object.keys(errors).length > 0; }; diff --git a/frontend/src/hooks/api/auth/index.tsx b/frontend/src/hooks/api/auth/index.tsx index dbcc77a5a..66208a487 100644 --- a/frontend/src/hooks/api/auth/index.tsx +++ b/frontend/src/hooks/api/auth/index.tsx @@ -1,10 +1,10 @@ export { useGetAuthToken, - useGetCommonPasswords, useResetPassword, - useSendMfaToken, + useSendMfaToken, useSendPasswordResetEmail, useSendVerificationEmail, useVerifyEmailVerificationCode, useVerifyMfaToken, - useVerifyPasswordResetCode} from "./queries" + useVerifyPasswordResetCode +} from "./queries"; diff --git a/frontend/src/hooks/api/auth/queries.tsx b/frontend/src/hooks/api/auth/queries.tsx index 094fd4b61..a26297730 100644 --- a/frontend/src/hooks/api/auth/queries.tsx +++ b/frontend/src/hooks/api/auth/queries.tsx @@ -20,22 +20,22 @@ import { SRPR1Res, VerifyMfaTokenDTO, VerifyMfaTokenRes, - VerifySignupInviteDTO} from "./types"; + VerifySignupInviteDTO +} from "./types"; const authKeys = { - getAuthToken: ["token"] as const, - commonPasswords: ["common-passwords"] as const + getAuthToken: ["token"] as const }; export const login1 = async (loginDetails: Login1DTO) => { const { data } = await apiRequest.post("/api/v3/auth/login1", loginDetails); return data; -} +}; export const login2 = async (loginDetails: Login2DTO) => { const { data } = await apiRequest.post("/api/v3/auth/login2", loginDetails); return data; -} +}; export const useLogin1 = () => { return useMutation({ @@ -47,7 +47,7 @@ export const useLogin1 = () => { return login1(details); } }); -} +}; export const useLogin2 = () => { return useMutation({ @@ -59,22 +59,22 @@ export const useLogin2 = () => { return login2(details); } }); -} +}; export const srp1 = async (details: SRP1DTO) => { const { data } = await apiRequest.post("/api/v1/password/srp1", details); - return data; -} + return data; +}; export const completeAccountSignup = async (details: CompleteAccountSignupDTO) => { const { data } = await apiRequest.post("/api/v3/signup/complete-account/signup", details); - return data; -} + return data; +}; export const completeAccountSignupInvite = async (details: CompleteAccountDTO) => { const { data } = await apiRequest.post("/api/v2/signup/complete-account/invite", details); - return data; -} + return data; +}; export const useCompleteAccountSignup = () => { return useMutation({ @@ -82,7 +82,7 @@ export const useCompleteAccountSignup = () => { return completeAccountSignup(details); } }); -} +}; export const useSendMfaToken = () => { return useMutation<{}, {}, SendMfaTokenDTO>({ @@ -91,22 +91,16 @@ export const useSendMfaToken = () => { return data; } }); -} +}; -export const verifyMfaToken = async ({ - email, - mfaCode -}: { - email: string; - mfaCode: string; -}) => { +export const verifyMfaToken = async ({ email, mfaCode }: { email: string; mfaCode: string }) => { const { data } = await apiRequest.post("/api/v2/auth/mfa/verify", { email, mfaToken: mfaCode }); return data; -} +}; export const useVerifyMfaToken = () => { return useMutation({ @@ -117,87 +111,67 @@ export const useVerifyMfaToken = () => { }); } }); -} +}; export const verifySignupInvite = async (details: VerifySignupInviteDTO) => { const { data } = await apiRequest.post("/api/v1/invite-org/verify", details); return data; -} +}; export const useSendVerificationEmail = () => { return useMutation({ - mutationFn: async ({ - email - }: { - email: string; - }) => { + mutationFn: async ({ email }: { email: string }) => { const { data } = await apiRequest.post("/api/v1/signup/email/signup", { email }); - + return data; } }); -} +}; export const useVerifyEmailVerificationCode = () => { return useMutation({ - mutationFn: async ({ - email, - code - }: { - email: string; - code: string; - }) => { + mutationFn: async ({ email, code }: { email: string; code: string }) => { const { data } = await apiRequest.post("/api/v1/signup/email/verify", { email, code }); - + return data; } }); -} +}; export const useSendPasswordResetEmail = () => { return useMutation({ - mutationFn: async ({ - email - }: { - email: string; - }) => { + mutationFn: async ({ email }: { email: string }) => { const { data } = await apiRequest.post("/api/v1/password/email/password-reset", { email }); - + return data; } }); -} +}; export const useVerifyPasswordResetCode = () => { return useMutation({ - mutationFn: async ({ - email, - code - }: { - email: string; - code: string; - }) => { + mutationFn: async ({ email, code }: { email: string; code: string }) => { const { data } = await apiRequest.post("/api/v1/password/email/password-reset-verify", { email, code }); - + return data; } }); -} +}; export const issueBackupPrivateKey = async (details: IssueBackupPrivateKeyDTO) => { const { data } = await apiRequest.post("/api/v1/password/backup-private-key", details); return data; -} +}; export const getBackupEncryptedPrivateKey = async ({ verificationToken @@ -207,37 +181,41 @@ export const getBackupEncryptedPrivateKey = async ({ Authorization: `Bearer ${verificationToken}` } }); - + return data.backupPrivateKey; -} +}; export const useResetPassword = () => { return useMutation({ mutationFn: async (details: ResetPasswordDTO) => { - const { data } = await apiRequest.post("/api/v1/password/password-reset", { - protectedKey: details.protectedKey, - protectedKeyIV: details.protectedKeyIV, - protectedKeyTag: details.protectedKeyTag, - encryptedPrivateKey: details.encryptedPrivateKey, - encryptedPrivateKeyIV: details.encryptedPrivateKeyIV, - encryptedPrivateKeyTag: details.encryptedPrivateKeyTag, - salt: details.salt, - verifier: details.verifier - }, { - headers: { - Authorization: `Bearer ${details.verificationToken}` + const { data } = await apiRequest.post( + "/api/v1/password/password-reset", + { + protectedKey: details.protectedKey, + protectedKeyIV: details.protectedKeyIV, + protectedKeyTag: details.protectedKeyTag, + encryptedPrivateKey: details.encryptedPrivateKey, + encryptedPrivateKeyIV: details.encryptedPrivateKeyIV, + encryptedPrivateKeyTag: details.encryptedPrivateKeyTag, + salt: details.salt, + verifier: details.verifier + }, + { + headers: { + Authorization: `Bearer ${details.verificationToken}` + } } - }); - + ); + return data; } }); -} +}; export const changePassword = async (details: ChangePasswordDTO) => { const { data } = await apiRequest.post("/api/v1/password/change-password", details); return data; -} +}; export const useChangePassword = () => { // note: use after srp1 @@ -246,7 +224,7 @@ export const useChangePassword = () => { return changePassword(details); } }); -} +}; // Refresh token is set as cookie when logged in // Using that we fetch the auth bearer token needed for auth calls @@ -263,11 +241,3 @@ export const useGetAuthToken = () => onSuccess: (data) => setAuthToken(data.token), retry: 0 }); - -const fetchCommonPasswords = async () => { - const { data } = await apiRequest.get("/api/v1/auth/common-passwords"); - return data || []; -}; - -export const useGetCommonPasswords = () => - useQuery({ queryKey: authKeys.commonPasswords, queryFn: fetchCommonPasswords }); \ No newline at end of file diff --git a/frontend/src/pages/signupinvite.tsx b/frontend/src/pages/signupinvite.tsx index 894c36ea6..7eb478352 100644 --- a/frontend/src/pages/signupinvite.tsx +++ b/frontend/src/pages/signupinvite.tsx @@ -22,31 +22,23 @@ import { deriveArgonKey } from "@app/components/utilities/cryptography/crypto"; import issueBackupKey from "@app/components/utilities/cryptography/issueBackupKey"; import { saveTokenToLocalStorage } from "@app/components/utilities/saveTokenToLocalStorage"; import SecurityClient from "@app/components/utilities/SecurityClient"; -import { - useGetCommonPasswords -} from "@app/hooks/api"; -import { - completeAccountSignupInvite, - verifySignupInvite -} from "@app/hooks/api/auth/queries"; +import { completeAccountSignupInvite, verifySignupInvite } from "@app/hooks/api/auth/queries"; import { fetchOrganizations } from "@app/hooks/api/organization/queries"; // eslint-disable-next-line new-cap const client = new jsrp.client(); type Errors = { - length?: string, - upperCase?: string, - lowerCase?: string, - number?: string, - specialChar?: string, - repeatedChar?: string, - breachedPassword?: string + length?: string; + upperCase?: string; + lowerCase?: string; + number?: string; + specialChar?: string; + repeatedChar?: string; + breachedPassword?: string; }; export default function SignupInvite() { - const { data: commonPasswords } = useGetCommonPasswords(); - const [password, setPassword] = useState(""); const [firstName, setFirstName] = useState(""); const [lastName, setLastName] = useState(""); @@ -80,10 +72,9 @@ export default function SignupInvite() { } else { setLastNameError(false); } - + errorCheck = await checkPassword({ password, - commonPasswords, setErrors }); @@ -117,7 +108,7 @@ export default function SignupInvite() { if (!derivedKey) throw new Error("Failed to derive key from password"); const key = crypto.randomBytes(32); - + // create encrypted private key by encrypting the private // key with the symmetric key [key] const { @@ -128,7 +119,7 @@ export default function SignupInvite() { text: privateKey, secret: key }); - + // create the protected key by encrypting the symmetric key // [key] with the derived key const { @@ -139,10 +130,8 @@ export default function SignupInvite() { text: key.toString("hex"), secret: Buffer.from(derivedKey.hash) }); - - const { - token: jwtToken - } = await completeAccountSignupInvite({ + + const { token: jwtToken } = await completeAccountSignupInvite({ email, firstName, lastName, @@ -156,20 +145,20 @@ export default function SignupInvite() { salt: result.salt, verifier: result.verifier }); - + // unset temporary signup JWT token and set JWT token SecurityClient.setSignupToken(""); SecurityClient.setToken(jwtToken); saveTokenToLocalStorage({ - publicKey, - encryptedPrivateKey, - iv: encryptedPrivateKeyIV, - tag: encryptedPrivateKeyTag, - privateKey + publicKey, + encryptedPrivateKey, + iv: encryptedPrivateKeyIV, + tag: encryptedPrivateKeyTag, + privateKey }); - const userOrgs = await fetchOrganizations(); + const userOrgs = await fetchOrganizations(); const orgId = userOrgs[0]._id; localStorage.setItem("orgData.id", orgId); @@ -189,12 +178,12 @@ export default function SignupInvite() { // Step 4 of the sign up process (download the emergency kit pdf) const stepConfirmEmail = ( -
-

+

+

Confirm your email

verify email -
+
diff --git a/frontend/src/pages/signupinvite.tsx b/frontend/src/pages/signupinvite.tsx index 7eb478352..081dfeadc 100644 --- a/frontend/src/pages/signupinvite.tsx +++ b/frontend/src/pages/signupinvite.tsx @@ -35,6 +35,7 @@ type Errors = { number?: string; specialChar?: string; repeatedChar?: string; + isEmail?: string; breachedPassword?: string; }; diff --git a/frontend/src/views/Settings/PersonalSettingsPage/ChangePasswordSection/ChangePasswordSection.tsx b/frontend/src/views/Settings/PersonalSettingsPage/ChangePasswordSection/ChangePasswordSection.tsx index 680b0fdbc..a7611f8bf 100644 --- a/frontend/src/views/Settings/PersonalSettingsPage/ChangePasswordSection/ChangePasswordSection.tsx +++ b/frontend/src/views/Settings/PersonalSettingsPage/ChangePasswordSection/ChangePasswordSection.tsx @@ -20,6 +20,7 @@ type Errors = { number?: string; specialChar?: string; repeatedChar?: string; + isEmail?: string; isBreachedPassword?: string; }; diff --git a/frontend/src/views/Signup/components/UserInfoSSOStep/UserInfoSSOStep.tsx b/frontend/src/views/Signup/components/UserInfoSSOStep/UserInfoSSOStep.tsx index 493d2b0cc..c159d7784 100644 --- a/frontend/src/views/Signup/components/UserInfoSSOStep/UserInfoSSOStep.tsx +++ b/frontend/src/views/Signup/components/UserInfoSSOStep/UserInfoSSOStep.tsx @@ -40,6 +40,7 @@ type Errors = { number?: string; specialChar?: string; repeatedChar?: string; + isEmail?: string; isBeachedPassword?: string; }; From 25fc508d5e36fd5b6f0699f53526fd4d6cc4aca8 Mon Sep 17 00:00:00 2001 From: Joel Biddle Date: Wed, 23 Aug 2023 02:56:03 +1000 Subject: [PATCH 25/33] Fixed spelling --- frontend/src/components/utilities/checks/checkPassword.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/frontend/src/components/utilities/checks/checkPassword.ts b/frontend/src/components/utilities/checks/checkPassword.ts index 34c09ac9d..b44c47df4 100644 --- a/frontend/src/components/utilities/checks/checkPassword.ts +++ b/frontend/src/components/utilities/checks/checkPassword.ts @@ -78,8 +78,7 @@ const checkPassword = async ({ password, setErrors }: CheckPasswordParams): Prom password ) ) { - errors.specialChar = - "at least 1 special character (emojis and many langauge scripts supported)"; + errors.specialChar = "at least 1 special character (emojis, symbols & non-Latin languages)"; } // repeatedChar From 7ec00475c6a458fa96e18a108ea47750d7308a1b Mon Sep 17 00:00:00 2001 From: Joel Biddle Date: Wed, 23 Aug 2023 12:59:00 +1000 Subject: [PATCH 26/33] +maxRetryAttempts, padding & safer error handling. Improved readability & comments. --- .../checks/checkIsPasswordBreached.ts | 92 +++++++++++++------ 1 file changed, 64 insertions(+), 28 deletions(-) diff --git a/frontend/src/components/utilities/checks/checkIsPasswordBreached.ts b/frontend/src/components/utilities/checks/checkIsPasswordBreached.ts index 75caf9dcf..abfeac508 100644 --- a/frontend/src/components/utilities/checks/checkIsPasswordBreached.ts +++ b/frontend/src/components/utilities/checks/checkIsPasswordBreached.ts @@ -1,47 +1,83 @@ import axios from "axios"; import { createHash } from "crypto"; // added types from @types/node -export const checkIsPasswordBreached = async (password: string) => { // see API details here: https://haveibeenpwned.com/API/v3#SearchingPwnedPasswordsByRange // in short, the pending password is hashed (SHA-1), the first 5 chars are sliced and compared against a ranged hash table - // if there is a match, that password has been involved in a password breach and should NOT be accepted - + // this hash table is formed from the 5 char hash prefix (ie. 00000-FFFFF) so 16^5 results + // returns a hash table of 800-1000 results + // padding has been added to prevent MiTM attacker determining which hash table was called by the response size + // the last 35 chars of the password hash are compared client-side against the table + // if there is a match, that password has been involved in a password breach (ie. pwnd) and should NOT be accepted // the database consists of ~700 mln breached passwords and is continuously updated, including with law enforcement ingestion // https://www.troyhunt.com/open-source-pwned-passwords-with-fbi-feed-and-225m-new-nca-passwords-is-now-live/ + // The HIBP API follows NIST guidance (pg.14) https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-63b.pdf + // "When processing requests to establish and change memorized secrets, verifiers SHALL compare + // the prospective secrets against a list that contains values known to be commonly-used, expected, + // or compromised. For example, the list MAY include, but is not limited to: + // • Passwords obtained from previous breach corpuses. + // • Dictionary words. + // • Repetitive or sequential characters (e.g. ‘aaaaaa’, ‘1234abcd’). + // • Context-specific words, such as the name of the service, the username, and derivatives + // thereof." + +export const checkIsPasswordBreached = async (password: string) => { const dataBreachCheckAPIBaseURL = "https://api.pwnedpasswords.com/range/"; // added to CSP + const maxRetryAttempts = 3; try { const textEncoder = new TextEncoder(); const encodedPwd = textEncoder.encode(password); const hash = createHash("sha1").update(encodedPwd).digest(); + const hashedPwd = hash.toString("hex").toUpperCase(); + const hashedPwdToSend = hashedPwd.slice(0, 5); // ONLY the first five hash chars are sent + const rangedHashTableUri = `${dataBreachCheckAPIBaseURL}${hashedPwdToSend}`; + + let response; + let retryAttempt = 0; - const hashedPwd = Array.from(new Uint8Array(hash)) - .map((byte) => byte.toString(16).padStart(2, "0")) - .join("") - .toUpperCase(); + while (retryAttempt < maxRetryAttempts) { + try { + response = await axios.get(rangedHashTableUri, { + headers: { + "Add-Padding": "true", // see https://www.troyhunt.com/enhancing-pwned-passwords-privacy-with-padding/ + "Content-Type": "text/plain", + }, + }); - // ONLY the first five SHA-1 hash chars need to be sent (must be over HTTPS) - const hashedPwdToSend = hashedPwd.slice(0, 5); - - const response = await axios.get(`${dataBreachCheckAPIBaseURL}${hashedPwdToSend}`); - const responseData = response.data.toUpperCase(); - - // compare against the API's ranged db's hash table - const isBreachedPassword = responseData.includes(hashedPwd.slice(5, 40)); - - // Clear the hashed password from memory as a precaution - const zeroBuffer = new Uint8Array(encodedPwd.length); - encodedPwd.set(zeroBuffer); - - return isBreachedPassword; // boolean: true indicates the password has been involved in a data breach - } catch (err: any) { - if (axios.isAxiosError(err) && err.response && err.response.status === 429) { - console.error("Received a 429 response from the Pwnd Passwords API"); - // Handle the 429 error here (not 100% sure what the rate limits are for the password API but looks like <10 calls/min) - // an error here should probably not cause the setting/resetting/changing password to fail (unless desired) - } else { - console.error(err); + if (response.status === 200) { + break; + } else { + retryAttempt++; + } + } catch (err) { + if (!axios.isAxiosError(err)) { + throw err; + } + retryAttempt++; + } } + + if (response && response.status === 200) { + const responseData = response.data.toUpperCase(); + // compare last 35 hash chars to the returned ranged hash table + // returns a boolean: true indicates the password has been involved in a data breach (ie. pwnd) + const isBreachedPassword = responseData.includes(hashedPwd.slice(5, 40)); + + // Clear the hashed password from memory as a precaution + const zeroBuffer = new Uint8Array(encodedPwd.length); + encodedPwd.set(zeroBuffer); + + return isBreachedPassword; + } + + console.error( + `Received a non-200 response (${response ? response.status : "unknown"}) from the Pwnd Passwords API` + ); + return false; // better to return a safe response if no breach can be determined + } catch (err: any) { + console.error("An unexpected error has occurred:", err.message); + return false; // Return a safe response in case of unexpected errors + // the HIBP API could return 400 (empty string supplied), 429 or 503 if Cloudflare edge node is down) } }; From 368855a44e67097409e2aafa68e83ebbc2a136bb Mon Sep 17 00:00:00 2001 From: Joel Biddle Date: Thu, 24 Aug 2023 12:59:24 +1000 Subject: [PATCH 27/33] >>> yup for email & url validation, fixed minor err in error msgs --- frontend/public/locales/en/translations.json | 1 + frontend/public/locales/es/translations.json | 3 ++- frontend/public/locales/fr/translations.json | 1 + frontend/public/locales/ko/translations.json | 3 ++- .../public/locales/pt-BR/translations.json | 3 ++- frontend/public/locales/tr/translations.json | 3 ++- .../src/components/signup/UserInfoStep.tsx | 1 + .../utilities/checks/PasswordCheck.ts | 18 ++++++++++++++++-- .../utilities/checks/checkPassword.ts | 14 ++++++++++++-- frontend/src/pages/password-reset.tsx | 17 +++++++++++++++++ frontend/src/pages/signupinvite.tsx | 4 +++- .../ChangePasswordSection.tsx | 1 + .../UserInfoSSOStep/UserInfoSSOStep.tsx | 1 + 13 files changed, 61 insertions(+), 9 deletions(-) diff --git a/frontend/public/locales/en/translations.json b/frontend/public/locales/en/translations.json index 776810383..dc2a1cfd3 100644 --- a/frontend/public/locales/en/translations.json +++ b/frontend/public/locales/en/translations.json @@ -240,6 +240,7 @@ "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." }, "token": { diff --git a/frontend/public/locales/es/translations.json b/frontend/public/locales/es/translations.json index 8891efcbe..8842d6a10 100644 --- a/frontend/public/locales/es/translations.json +++ b/frontend/public/locales/es/translations.json @@ -237,7 +237,8 @@ "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-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-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." }, "token": { "service-tokens": "Tokens de servicio", diff --git a/frontend/public/locales/fr/translations.json b/frontend/public/locales/fr/translations.json index a8638dc3b..a65129392 100644 --- a/frontend/public/locales/fr/translations.json +++ b/frontend/public/locales/fr/translations.json @@ -224,6 +224,7 @@ "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." }, "token": { diff --git a/frontend/public/locales/ko/translations.json b/frontend/public/locales/ko/translations.json index 8f21f6349..e5f6deee7 100644 --- a/frontend/public/locales/ko/translations.json +++ b/frontend/public/locales/ko/translations.json @@ -191,7 +191,8 @@ "validate-special-char": "특수 문자 1개 이상", "validate-repeated-char": "최대 2개의 반복된 연속 문자", "validate-is-email": "비밀번호는 이메일 주소가 될 수 없습니다.", - "validate-breached": "새 비밀번호는 다른 웹사이트에서 일반적으로 사용되는 비밀번호 목록에 있습니다. 더 강력한 비밀번호로 다시 시도해 주세요." + "validate-is-url": "비밀번호는 URL일 수 없습니다.", + "validate-is-breached": "새 비밀번호는 다른 웹사이트에서 일반적으로 사용되는 비밀번호 목록에 있습니다. 더 강력한 비밀번호로 다시 시도해 주세요." }, "token": { "add-dialog": { diff --git a/frontend/public/locales/pt-BR/translations.json b/frontend/public/locales/pt-BR/translations.json index 238cee3fa..362ec2aae 100644 --- a/frontend/public/locales/pt-BR/translations.json +++ b/frontend/public/locales/pt-BR/translations.json @@ -219,7 +219,8 @@ "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-breached": "A nova senha está em uma lista de senhas comumente usadas em outros sites. Tente novamente com uma senha mais forte." + "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." }, "token": { "service-tokens": "Tokens de Serviço", diff --git a/frontend/public/locales/tr/translations.json b/frontend/public/locales/tr/translations.json index 32c3d4635..c1b72d7e3 100644 --- a/frontend/public/locales/tr/translations.json +++ b/frontend/public/locales/tr/translations.json @@ -237,7 +237,8 @@ "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-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-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." }, "token": { "service-tokens": "Servis Belirteçleri", diff --git a/frontend/src/components/signup/UserInfoStep.tsx b/frontend/src/components/signup/UserInfoStep.tsx index 907ce4c57..0c24b2aac 100644 --- a/frontend/src/components/signup/UserInfoStep.tsx +++ b/frontend/src/components/signup/UserInfoStep.tsx @@ -46,6 +46,7 @@ type Errors = { specialChar?: string; repeatedChar?: string; isEmail?: string; + isUrl?: string; isBeachedPassword?: string; }; diff --git a/frontend/src/components/utilities/checks/PasswordCheck.ts b/frontend/src/components/utilities/checks/PasswordCheck.ts index 7ab633c05..7e212873b 100644 --- a/frontend/src/components/utilities/checks/PasswordCheck.ts +++ b/frontend/src/components/utilities/checks/PasswordCheck.ts @@ -1,4 +1,4 @@ -import isEmail from "validator/lib/isEmail"; +import {string} from "yup"; import { checkIsPasswordBreached } from "./checkIsPasswordBreached"; /* eslint-disable no-param-reassign */ @@ -13,6 +13,7 @@ interface PasswordCheckProps { setPasswordErrorSpecialChar: (value: boolean) => void; setPasswordErrorRepeatedChar: (value: boolean) => void; setPasswordErrorIsEmail: (value: boolean) => void; + setPasswordErrorIsUrl: (value: boolean) => void; setPasswordErrorIsBreachedPassword: (value: boolean) => void; } @@ -29,6 +30,7 @@ const passwordCheck = async ({ setPasswordErrorSpecialChar, setPasswordErrorRepeatedChar, setPasswordErrorIsEmail, + setPasswordErrorIsUrl, setPasswordErrorIsBreachedPassword, errorCheck }: PasswordCheckProps) => { @@ -97,13 +99,25 @@ const passwordCheck = async ({ } // isEmail - if (isEmail(password)) { + 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); diff --git a/frontend/src/components/utilities/checks/checkPassword.ts b/frontend/src/components/utilities/checks/checkPassword.ts index b44c47df4..5608395a1 100644 --- a/frontend/src/components/utilities/checks/checkPassword.ts +++ b/frontend/src/components/utilities/checks/checkPassword.ts @@ -1,4 +1,4 @@ -import isEmail from "validator/lib/isEmail"; +import {string} from "yup" import { checkIsPasswordBreached } from "./checkIsPasswordBreached"; type Errors = { @@ -10,6 +10,7 @@ type Errors = { specialChar?: string; repeatedChar?: string; isEmail?: string; + isUrl?: string; isBreachedPassword?: string; }; @@ -93,10 +94,19 @@ const checkPassword = async ({ password, setErrors }: CheckPasswordParams): Prom } // isEmail - if (isEmail(password)) { + 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 = diff --git a/frontend/src/pages/password-reset.tsx b/frontend/src/pages/password-reset.tsx index c1ba52605..85875c338 100644 --- a/frontend/src/pages/password-reset.tsx +++ b/frontend/src/pages/password-reset.tsx @@ -36,6 +36,7 @@ export default function PasswordReset() { const [passwordErrorSpecialChar, setPasswordErrorSpecialChar] = useState(false); const [passwordErrorRepeatedChar, setPasswordErrorRepeatedChar] = useState(false); const [passwordErrorIsEmail, setPasswordErrorIsEmail] = useState(false); + const [passwordErrorIsUrl, setPasswordErrorIsUrl] = useState(false); const [passwordErrorIsBreachedPassword, setPasswordErrorIsBreachedPassword] = useState(false); const router = useRouter(); @@ -81,6 +82,7 @@ export default function PasswordReset() { setPasswordErrorSpecialChar, setPasswordErrorRepeatedChar, setPasswordErrorIsEmail, + setPasswordErrorIsUrl, setPasswordErrorIsBreachedPassword, errorCheck: false }); @@ -243,6 +245,7 @@ export default function PasswordReset() { setPasswordErrorSpecialChar, setPasswordErrorRepeatedChar, setPasswordErrorIsEmail, + setPasswordErrorIsUrl, setPasswordErrorIsBreachedPassword, errorCheck: false }); @@ -259,6 +262,7 @@ export default function PasswordReset() { passwordErrorSpecialChar && passwordErrorRepeatedChar && passwordErrorIsEmail && + passwordErrorIsUrl && passwordErrorIsBreachedPassword } autoComplete="new-password" @@ -273,6 +277,7 @@ export default function PasswordReset() { passwordErrorSpecialChar || passwordErrorRepeatedChar || passwordErrorIsEmail || + passwordErrorIsUrl || passwordErrorIsBreachedPassword ? (
Password should contain:
@@ -369,6 +374,18 @@ export default function PasswordReset() { The password cannot be an email address.
+
+ {passwordErrorIsUrl ? ( + + ) : ( + + )} +
+ The password cannot be a URL. +
+
{passwordErrorIsBreachedPassword ? ( diff --git a/frontend/src/pages/signupinvite.tsx b/frontend/src/pages/signupinvite.tsx index 081dfeadc..105420543 100644 --- a/frontend/src/pages/signupinvite.tsx +++ b/frontend/src/pages/signupinvite.tsx @@ -29,13 +29,15 @@ import { fetchOrganizations } from "@app/hooks/api/organization/queries"; const client = new jsrp.client(); type Errors = { - length?: string; + tooShort?: string; + tooLong?: string; upperCase?: string; lowerCase?: string; number?: string; specialChar?: string; repeatedChar?: string; isEmail?: string; + isUrl?: string; breachedPassword?: string; }; diff --git a/frontend/src/views/Settings/PersonalSettingsPage/ChangePasswordSection/ChangePasswordSection.tsx b/frontend/src/views/Settings/PersonalSettingsPage/ChangePasswordSection/ChangePasswordSection.tsx index a7611f8bf..69a8d7d4f 100644 --- a/frontend/src/views/Settings/PersonalSettingsPage/ChangePasswordSection/ChangePasswordSection.tsx +++ b/frontend/src/views/Settings/PersonalSettingsPage/ChangePasswordSection/ChangePasswordSection.tsx @@ -21,6 +21,7 @@ type Errors = { specialChar?: string; repeatedChar?: string; isEmail?: string; + isUrl?: string; isBreachedPassword?: string; }; diff --git a/frontend/src/views/Signup/components/UserInfoSSOStep/UserInfoSSOStep.tsx b/frontend/src/views/Signup/components/UserInfoSSOStep/UserInfoSSOStep.tsx index c159d7784..93143578f 100644 --- a/frontend/src/views/Signup/components/UserInfoSSOStep/UserInfoSSOStep.tsx +++ b/frontend/src/views/Signup/components/UserInfoSSOStep/UserInfoSSOStep.tsx @@ -41,6 +41,7 @@ type Errors = { specialChar?: string; repeatedChar?: string; isEmail?: string; + isUrl?: string; isBeachedPassword?: string; }; From 14fc78eaaf049850aa52bf34433a81efbbf26f3d Mon Sep 17 00:00:00 2001 From: Joel Biddle Date: Thu, 24 Aug 2023 14:01:26 +1000 Subject: [PATCH 28/33] Switched to crypto.subtle, cleaned up code, added types & properly cleared sensitive data from memory (even if error) --- .../checks/checkIsPasswordBreached.ts | 83 +++++++++++++------ 1 file changed, 56 insertions(+), 27 deletions(-) diff --git a/frontend/src/components/utilities/checks/checkIsPasswordBreached.ts b/frontend/src/components/utilities/checks/checkIsPasswordBreached.ts index abfeac508..1a646d387 100644 --- a/frontend/src/components/utilities/checks/checkIsPasswordBreached.ts +++ b/frontend/src/components/utilities/checks/checkIsPasswordBreached.ts @@ -1,11 +1,10 @@ import axios from "axios"; -import { createHash } from "crypto"; // added types from @types/node // see API details here: https://haveibeenpwned.com/API/v3#SearchingPwnedPasswordsByRange // in short, the pending password is hashed (SHA-1), the first 5 chars are sliced and compared against a ranged hash table // this hash table is formed from the 5 char hash prefix (ie. 00000-FFFFF) so 16^5 results // returns a hash table of 800-1000 results - // padding has been added to prevent MiTM attacker determining which hash table was called by the response size + // padding has been added to prevent MitM attacker determining which hash table was called by the response size // the last 35 chars of the password hash are compared client-side against the table // if there is a match, that password has been involved in a password breach (ie. pwnd) and should NOT be accepted // the database consists of ~700 mln breached passwords and is continuously updated, including with law enforcement ingestion @@ -21,18 +20,43 @@ import { createHash } from "crypto"; // added types from @types/node // • Context-specific words, such as the name of the service, the username, and derivatives // thereof." -export const checkIsPasswordBreached = async (password: string) => { - const dataBreachCheckAPIBaseURL = "https://api.pwnedpasswords.com/range/"; // added to CSP +export const checkIsPasswordBreached = async (password: string): Promise => { + const dataBreachCheckAPIBaseURL = "https://api.pwnedpasswords.com/range/"; const maxRetryAttempts = 3; + let encodedPwd: Uint8Array | undefined; + let hashedPwdBuffer: ArrayBuffer | undefined; + try { + // Convert the password to a Uint8Array (UTF-8 encoded bytes) const textEncoder = new TextEncoder(); - const encodedPwd = textEncoder.encode(password); - const hash = createHash("sha1").update(encodedPwd).digest(); - const hashedPwd = hash.toString("hex").toUpperCase(); - const hashedPwdToSend = hashedPwd.slice(0, 5); // ONLY the first five hash chars are sent - const rangedHashTableUri = `${dataBreachCheckAPIBaseURL}${hashedPwdToSend}`; - + encodedPwd = textEncoder.encode(password); + + // SHA-1 hash the password using the SubtleCrypto API + async function hashPassword(passwordBytes: ArrayBuffer): Promise { + const buffer = await crypto.subtle.digest("SHA-1", passwordBytes); + return buffer; + } + + // Convert the hashed password buffer to a hexadecimal string + function bufferToHex(buffer: ArrayBuffer): string { + const byteArray = new Uint8Array(buffer); + const hexParts: string[] = []; + byteArray.forEach((byte) => { + const hex = byte.toString(16).padStart(2, "0"); + hexParts.push(hex); + }); + return hexParts.join(""); + } + + // Hash the password and convert it to a useful format for the HIBP API + hashedPwdBuffer = await hashPassword(encodedPwd!.buffer); + const hashedPwd = bufferToHex(hashedPwdBuffer).toUpperCase(); + // ONLY send the first 5 hash chars (over HTTPS) + const hashedPwdToSend = hashedPwd.slice(0, 5); + const safeHashedPwdToSend = encodeURIComponent(hashedPwdToSend); // Ensure URL safety + const rangedHashTableUri = `${dataBreachCheckAPIBaseURL}${safeHashedPwdToSend}`; + let response; let retryAttempt = 0; @@ -46,7 +70,11 @@ export const checkIsPasswordBreached = async (password: string) => { }); if (response.status === 200) { - break; + // now we get back one of 16^5 hash prefix tables with random padding + const responseData = response.data.toUpperCase(); + // check the last 35 hash chars to see if there's a match + const isBreachedPassword: boolean = responseData.includes(hashedPwd.slice(5, 40)); + return isBreachedPassword; } else { retryAttempt++; } @@ -58,26 +86,27 @@ export const checkIsPasswordBreached = async (password: string) => { } } - if (response && response.status === 200) { - const responseData = response.data.toUpperCase(); - // compare last 35 hash chars to the returned ranged hash table - // returns a boolean: true indicates the password has been involved in a data breach (ie. pwnd) - const isBreachedPassword = responseData.includes(hashedPwd.slice(5, 40)); - - // Clear the hashed password from memory as a precaution - const zeroBuffer = new Uint8Array(encodedPwd.length); - encodedPwd.set(zeroBuffer); - - return isBreachedPassword; - } - console.error( `Received a non-200 response (${response ? response.status : "unknown"}) from the Pwnd Passwords API` ); - return false; // better to return a safe response if no breach can be determined + return false; } catch (err: any) { console.error("An unexpected error has occurred:", err.message); - return false; // Return a safe response in case of unexpected errors - // the HIBP API could return 400 (empty string supplied), 429 or 503 if Cloudflare edge node is down) + return false; + } finally { + + // Clear the UTF-8 encoded password from memory + + if (encodedPwd) { + const zeroEncodedPwdBuffer = new Uint8Array(encodedPwd.length); + encodedPwd.set(zeroEncodedPwdBuffer); + } + + // Clear the hashed password buffer from memory + + if (hashedPwdBuffer) { + const zeroHashedPwdBuffer = new Uint8Array(hashedPwdBuffer); + zeroHashedPwdBuffer.fill(0); + } } }; From 688cf91eb7fc90123cac5138c938915c875fb007 Mon Sep 17 00:00:00 2001 From: Joel Biddle Date: Thu, 24 Aug 2023 14:08:11 +1000 Subject: [PATCH 29/33] Removed unnecessary validator library & @types/validator in favor of yup --- frontend/package-lock.json | 27 --------------------------- frontend/package.json | 2 -- 2 files changed, 29 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 150b373a2..990b03377 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -87,7 +87,6 @@ "tweetnacl-util": "^0.15.1", "uuid": "^8.3.2", "uuidv4": "^6.2.13", - "validator": "^13.11.0", "yaml": "^2.2.2", "yup": "^0.32.11" }, @@ -106,7 +105,6 @@ "@types/node": "^18.11.9", "@types/react": "^18.0.26", "@types/sanitize-html": "^2.9.0", - "@types/validator": "^13.11.1", "@typescript-eslint/eslint-plugin": "^5.48.1", "@typescript-eslint/parser": "^5.45.0", "autoprefixer": "^10.4.7", @@ -8157,12 +8155,6 @@ "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz", "integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==" }, - "node_modules/@types/validator": { - "version": "13.11.1", - "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.11.1.tgz", - "integrity": "sha512-d/MUkJYdOeKycmm75Arql4M5+UuXmf4cHdHKsyw1GcvnNgL6s77UkgSgJ8TE/rI5PYsnwYq5jkcWBLuN/MpQ1A==", - "dev": true - }, "node_modules/@types/yargs": { "version": "17.0.24", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz", @@ -22666,14 +22658,6 @@ "spdx-expression-parse": "^3.0.0" } }, - "node_modules/validator": { - "version": "13.11.0", - "resolved": "https://registry.npmjs.org/validator/-/validator-13.11.0.tgz", - "integrity": "sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==", - "engines": { - "node": ">= 0.10" - } - }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -28932,12 +28916,6 @@ "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz", "integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==" }, - "@types/validator": { - "version": "13.11.1", - "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.11.1.tgz", - "integrity": "sha512-d/MUkJYdOeKycmm75Arql4M5+UuXmf4cHdHKsyw1GcvnNgL6s77UkgSgJ8TE/rI5PYsnwYq5jkcWBLuN/MpQ1A==", - "dev": true - }, "@types/yargs": { "version": "17.0.24", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz", @@ -39709,11 +39687,6 @@ "spdx-expression-parse": "^3.0.0" } }, - "validator": { - "version": "13.11.0", - "resolved": "https://registry.npmjs.org/validator/-/validator-13.11.0.tgz", - "integrity": "sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==" - }, "vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index 3104ff1a3..fab8b7033 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -95,7 +95,6 @@ "tweetnacl-util": "^0.15.1", "uuid": "^8.3.2", "uuidv4": "^6.2.13", - "validator": "^13.11.0", "yaml": "^2.2.2", "yup": "^0.32.11" }, @@ -114,7 +113,6 @@ "@types/node": "^18.11.9", "@types/react": "^18.0.26", "@types/sanitize-html": "^2.9.0", - "@types/validator": "^13.11.1", "@typescript-eslint/eslint-plugin": "^5.48.1", "@typescript-eslint/parser": "^5.45.0", "autoprefixer": "^10.4.7", From 4d6a8f0476cba604ad4cc69cfd80e2c8470b053e Mon Sep 17 00:00:00 2001 From: Joel Biddle Date: Fri, 25 Aug 2023 01:44:02 +1000 Subject: [PATCH 30/33] 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; }; /** From a99751eb724d71135ec24969d7b782dfa27638ab Mon Sep 17 00:00:00 2001 From: Joel Biddle Date: Fri, 25 Aug 2023 12:36:53 +1000 Subject: [PATCH 31/33] Moved pwd checks into a subfolder --- frontend/src/components/signup/UserInfoStep.tsx | 2 +- .../components/utilities/checks/{ => password}/PasswordCheck.ts | 0 .../utilities/checks/{ => password}/checkIsPasswordBreached.ts | 0 .../components/utilities/checks/{ => password}/checkPassword.ts | 0 .../utilities/checks/{ => password}/passwordRegexes.ts | 0 frontend/src/pages/password-reset.tsx | 2 +- frontend/src/pages/signupinvite.tsx | 2 +- .../ChangePasswordSection/ChangePasswordSection.tsx | 2 +- .../views/Signup/components/UserInfoSSOStep/UserInfoSSOStep.tsx | 2 +- 9 files changed, 5 insertions(+), 5 deletions(-) rename frontend/src/components/utilities/checks/{ => password}/PasswordCheck.ts (100%) rename frontend/src/components/utilities/checks/{ => password}/checkIsPasswordBreached.ts (100%) rename frontend/src/components/utilities/checks/{ => password}/checkPassword.ts (100%) rename frontend/src/components/utilities/checks/{ => password}/passwordRegexes.ts (100%) diff --git a/frontend/src/components/signup/UserInfoStep.tsx b/frontend/src/components/signup/UserInfoStep.tsx index 430d373bd..0e2b02f87 100644 --- a/frontend/src/components/signup/UserInfoStep.tsx +++ b/frontend/src/components/signup/UserInfoStep.tsx @@ -13,7 +13,7 @@ import { fetchOrganizations } from "@app/hooks/api/organization/queries"; import ProjectService from "@app/services/ProjectService"; import InputField from "../basic/InputField"; -import checkPassword from "../utilities/checks/checkPassword"; +import checkPassword from "../utilities/checks/password/checkPassword"; import Aes256Gcm from "../utilities/cryptography/aes-256-gcm"; import { deriveArgonKey } from "../utilities/cryptography/crypto"; import { saveTokenToLocalStorage } from "../utilities/saveTokenToLocalStorage"; diff --git a/frontend/src/components/utilities/checks/PasswordCheck.ts b/frontend/src/components/utilities/checks/password/PasswordCheck.ts similarity index 100% rename from frontend/src/components/utilities/checks/PasswordCheck.ts rename to frontend/src/components/utilities/checks/password/PasswordCheck.ts diff --git a/frontend/src/components/utilities/checks/checkIsPasswordBreached.ts b/frontend/src/components/utilities/checks/password/checkIsPasswordBreached.ts similarity index 100% rename from frontend/src/components/utilities/checks/checkIsPasswordBreached.ts rename to frontend/src/components/utilities/checks/password/checkIsPasswordBreached.ts diff --git a/frontend/src/components/utilities/checks/checkPassword.ts b/frontend/src/components/utilities/checks/password/checkPassword.ts similarity index 100% rename from frontend/src/components/utilities/checks/checkPassword.ts rename to frontend/src/components/utilities/checks/password/checkPassword.ts diff --git a/frontend/src/components/utilities/checks/passwordRegexes.ts b/frontend/src/components/utilities/checks/password/passwordRegexes.ts similarity index 100% rename from frontend/src/components/utilities/checks/passwordRegexes.ts rename to frontend/src/components/utilities/checks/password/passwordRegexes.ts diff --git a/frontend/src/pages/password-reset.tsx b/frontend/src/pages/password-reset.tsx index fa83dbdd8..6d9becb08 100644 --- a/frontend/src/pages/password-reset.tsx +++ b/frontend/src/pages/password-reset.tsx @@ -10,7 +10,7 @@ import queryString from "query-string"; import Button from "@app/components/basic/buttons/Button"; import InputField from "@app/components/basic/InputField"; -import passwordCheck from "@app/components/utilities/checks/PasswordCheck"; +import passwordCheck from "~/components/utilities/checks/password/PasswordCheck"; import Aes256Gcm from "@app/components/utilities/cryptography/aes-256-gcm"; import { useResetPassword, useVerifyPasswordResetCode } from "@app/hooks/api"; import { getBackupEncryptedPrivateKey } from "@app/hooks/api/auth/queries"; diff --git a/frontend/src/pages/signupinvite.tsx b/frontend/src/pages/signupinvite.tsx index c7b04184e..20a093b9c 100644 --- a/frontend/src/pages/signupinvite.tsx +++ b/frontend/src/pages/signupinvite.tsx @@ -16,7 +16,7 @@ import { encodeBase64 } from "tweetnacl-util"; import Button from "@app/components/basic/buttons/Button"; import InputField from "@app/components/basic/InputField"; -import checkPassword from "@app/components/utilities/checks/checkPassword"; +import checkPassword from "~/components/utilities/checks/password/checkPassword"; import Aes256Gcm from "@app/components/utilities/cryptography/aes-256-gcm"; import { deriveArgonKey } from "@app/components/utilities/cryptography/crypto"; import issueBackupKey from "@app/components/utilities/cryptography/issueBackupKey"; diff --git a/frontend/src/views/Settings/PersonalSettingsPage/ChangePasswordSection/ChangePasswordSection.tsx b/frontend/src/views/Settings/PersonalSettingsPage/ChangePasswordSection/ChangePasswordSection.tsx index de83f0c0f..6a141d652 100644 --- a/frontend/src/views/Settings/PersonalSettingsPage/ChangePasswordSection/ChangePasswordSection.tsx +++ b/frontend/src/views/Settings/PersonalSettingsPage/ChangePasswordSection/ChangePasswordSection.tsx @@ -8,7 +8,7 @@ import * as yup from "yup"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; import attemptChangePassword from "@app/components/utilities/attemptChangePassword"; -import checkPassword from "@app/components/utilities/checks/checkPassword"; +import checkPassword from "~/components/utilities/checks/password/checkPassword"; import { Button, FormControl, Input } from "@app/components/v2"; import { useUser } from "@app/context"; diff --git a/frontend/src/views/Signup/components/UserInfoSSOStep/UserInfoSSOStep.tsx b/frontend/src/views/Signup/components/UserInfoSSOStep/UserInfoSSOStep.tsx index 79c502583..cc23933e9 100644 --- a/frontend/src/views/Signup/components/UserInfoSSOStep/UserInfoSSOStep.tsx +++ b/frontend/src/views/Signup/components/UserInfoSSOStep/UserInfoSSOStep.tsx @@ -9,7 +9,7 @@ import nacl from "tweetnacl"; import { encodeBase64 } from "tweetnacl-util"; import InputField from "@app/components/basic/InputField"; -import checkPassword from "@app/components/utilities/checks/checkPassword"; +import checkPassword from "~/components/utilities/checks/password/checkPassword"; import Aes256Gcm from "@app/components/utilities/cryptography/aes-256-gcm"; import { deriveArgonKey } from "@app/components/utilities/cryptography/crypto"; import { saveTokenToLocalStorage } from "@app/components/utilities/saveTokenToLocalStorage"; From f1f64e6ff55b977d48c84bd2438ccfb720b735ef Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Mon, 28 Aug 2023 11:08:00 +0100 Subject: [PATCH 32/33] Fix flaky regex g flag causing unexpected validation password validation issue --- .../src/components/signup/UserInfoStep.tsx | 2 +- .../password/checkIsPasswordBreached.ts | 47 ++++++++++--------- .../checks/password/passwordRegexes.ts | 5 +- 3 files changed, 28 insertions(+), 26 deletions(-) diff --git a/frontend/src/components/signup/UserInfoStep.tsx b/frontend/src/components/signup/UserInfoStep.tsx index 0e2b02f87..c09e43fcb 100644 --- a/frontend/src/components/signup/UserInfoStep.tsx +++ b/frontend/src/components/signup/UserInfoStep.tsx @@ -269,11 +269,11 @@ export default function UserInfoStep({ { - setPassword(pass); await checkPassword({ password: pass, setErrors }); + setPassword(pass); }} type="password" value={password} diff --git a/frontend/src/components/utilities/checks/password/checkIsPasswordBreached.ts b/frontend/src/components/utilities/checks/password/checkIsPasswordBreached.ts index 1a646d387..d978441a6 100644 --- a/frontend/src/components/utilities/checks/password/checkIsPasswordBreached.ts +++ b/frontend/src/components/utilities/checks/password/checkIsPasswordBreached.ts @@ -1,5 +1,22 @@ import axios from "axios"; +// SHA-1 hash the password using the SubtleCrypto API +async function hashPassword(passwordBytes: ArrayBuffer): Promise { + const buffer = await window.crypto.subtle.digest("SHA-1", passwordBytes); + return buffer; +} + +// Convert the hashed password buffer to a hexadecimal string +function bufferToHex(buffer: ArrayBuffer): string { + const byteArray = new Uint8Array(buffer); + const hexParts: string[] = []; + byteArray.forEach((byte) => { + const hex = byte.toString(16).padStart(2, "0"); + hexParts.push(hex); + }); + return hexParts.join(""); +} + // see API details here: https://haveibeenpwned.com/API/v3#SearchingPwnedPasswordsByRange // in short, the pending password is hashed (SHA-1), the first 5 chars are sliced and compared against a ranged hash table // this hash table is formed from the 5 char hash prefix (ie. 00000-FFFFF) so 16^5 results @@ -21,7 +38,7 @@ import axios from "axios"; // thereof." export const checkIsPasswordBreached = async (password: string): Promise => { - const dataBreachCheckAPIBaseURL = "https://api.pwnedpasswords.com/range/"; + const HAVE_I_BEEN_PWNED_API_URL = "https://api.pwnedpasswords.com"; const maxRetryAttempts = 3; let encodedPwd: Uint8Array | undefined; @@ -32,34 +49,18 @@ export const checkIsPasswordBreached = async (password: string): Promise { - const buffer = await crypto.subtle.digest("SHA-1", passwordBytes); - return buffer; - } - - // Convert the hashed password buffer to a hexadecimal string - function bufferToHex(buffer: ArrayBuffer): string { - const byteArray = new Uint8Array(buffer); - const hexParts: string[] = []; - byteArray.forEach((byte) => { - const hex = byte.toString(16).padStart(2, "0"); - hexParts.push(hex); - }); - return hexParts.join(""); - } - // Hash the password and convert it to a useful format for the HIBP API hashedPwdBuffer = await hashPassword(encodedPwd!.buffer); const hashedPwd = bufferToHex(hashedPwdBuffer).toUpperCase(); // ONLY send the first 5 hash chars (over HTTPS) const hashedPwdToSend = hashedPwd.slice(0, 5); const safeHashedPwdToSend = encodeURIComponent(hashedPwdToSend); // Ensure URL safety - const rangedHashTableUri = `${dataBreachCheckAPIBaseURL}${safeHashedPwdToSend}`; + const rangedHashTableUri = `${HAVE_I_BEEN_PWNED_API_URL}/range/${safeHashedPwdToSend}`; let response; let retryAttempt = 0; + /* eslint-disable no-await-in-loop */ while (retryAttempt < maxRetryAttempts) { try { response = await axios.get(rangedHashTableUri, { @@ -75,14 +76,14 @@ export const checkIsPasswordBreached = async (password: string): Promise]|[^\p{L}\p{N}\s]/gu; +export const numAndSpecialCharRegex = /[\d!@#$%^&*(),.?":{}|<>]|[^\p{L}\p{N}\s]/u; // This regex covers 3 repeated consecutive chars (incl. spaces) export const repeatedCharRegex = /(.)\1\1\1|\s{4,}/; @@ -19,7 +20,7 @@ export const lowEntropyRegexes = [ /^(?:(?: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)/, + /(\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/, From a79c6227b145cb6e28933c015fefac4a425f1035 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Mon, 28 Aug 2023 11:25:50 +0100 Subject: [PATCH 33/33] Fix frontend lint issues --- .../utilities/checks/password/PasswordCheck.ts | 13 ++++++------- .../utilities/checks/password/checkPassword.ts | 6 +++--- frontend/src/pages/password-reset.tsx | 8 +++----- frontend/src/pages/signupinvite.tsx | 2 +- .../ChangePasswordSection/ChangePasswordSection.tsx | 2 +- .../components/UserInfoSSOStep/UserInfoSSOStep.tsx | 2 +- 6 files changed, 15 insertions(+), 18 deletions(-) diff --git a/frontend/src/components/utilities/checks/password/PasswordCheck.ts b/frontend/src/components/utilities/checks/password/PasswordCheck.ts index 3804841fc..90ea4c7ea 100644 --- a/frontend/src/components/utilities/checks/password/PasswordCheck.ts +++ b/frontend/src/components/utilities/checks/password/PasswordCheck.ts @@ -1,9 +1,8 @@ -import { letterCharRegex, numAndSpecialCharRegex, repeatedCharRegex, escapeCharRegex, lowEntropyRegexes } from "./passwordRegexes"; import { checkIsPasswordBreached } from "./checkIsPasswordBreached"; +import { escapeCharRegex, letterCharRegex, lowEntropyRegexes,numAndSpecialCharRegex, repeatedCharRegex } from "./passwordRegexes"; interface PasswordCheckProps { password: string; - errorCheck: boolean; setPasswordErrorTooShort: (value: boolean) => void; setPasswordErrorTooLong: (value: boolean) => void; setPasswordErrorNoLetterChar: (value: boolean) => void; @@ -23,9 +22,9 @@ const passwordCheck = async ({ setPasswordErrorRepeatedChar, setPasswordErrorEscapeChar, setPasswordErrorLowEntropy, - setPasswordErrorBreached, - errorCheck + setPasswordErrorBreached }: PasswordCheckProps) => { + let errorCheck = false; const tests = [ { name: "tooShort", @@ -74,15 +73,15 @@ const passwordCheck = async ({ } else { setPasswordErrorBreached(false); } - - for (const test of tests) { + + tests.forEach((test) => { if (!test.validator(password)) { errorCheck = true; test.setError(true); } else { test.setError(false); } - } + }) return errorCheck; }; diff --git a/frontend/src/components/utilities/checks/password/checkPassword.ts b/frontend/src/components/utilities/checks/password/checkPassword.ts index b1b72e797..55fede5a6 100644 --- a/frontend/src/components/utilities/checks/password/checkPassword.ts +++ b/frontend/src/components/utilities/checks/password/checkPassword.ts @@ -1,5 +1,5 @@ -import { letterCharRegex, numAndSpecialCharRegex, repeatedCharRegex, escapeCharRegex, lowEntropyRegexes } from "./passwordRegexes"; import { checkIsPasswordBreached } from "./checkIsPasswordBreached"; +import { escapeCharRegex, letterCharRegex, lowEntropyRegexes,numAndSpecialCharRegex, repeatedCharRegex } from "./passwordRegexes"; type Errors = { tooShort?: string; @@ -86,11 +86,11 @@ const checkPassword = async ({ password, setErrors }: CheckPasswordParams): Prom errors.breached = "Password was found in a data breach."; } - for (const test of tests) { + tests.forEach((test) => { if (test.validator && !test.validator(password)) { errors[test.name as keyof Errors] = test.errorText; } - } + }); setErrors(errors); return Object.keys(errors).length > 0; diff --git a/frontend/src/pages/password-reset.tsx b/frontend/src/pages/password-reset.tsx index 6d9becb08..0f63ef4af 100644 --- a/frontend/src/pages/password-reset.tsx +++ b/frontend/src/pages/password-reset.tsx @@ -10,7 +10,7 @@ import queryString from "query-string"; import Button from "@app/components/basic/buttons/Button"; import InputField from "@app/components/basic/InputField"; -import passwordCheck from "~/components/utilities/checks/password/PasswordCheck"; +import passwordCheck from "@app/components/utilities/checks/password/PasswordCheck"; import Aes256Gcm from "@app/components/utilities/cryptography/aes-256-gcm"; import { useResetPassword, useVerifyPasswordResetCode } from "@app/hooks/api"; import { getBackupEncryptedPrivateKey } from "@app/hooks/api/auth/queries"; @@ -79,8 +79,7 @@ export default function PasswordReset() { setPasswordErrorRepeatedChar, setPasswordErrorEscapeChar, setPasswordErrorLowEntropy, - setPasswordErrorBreached, - errorCheck: false + setPasswordErrorBreached }); if (!errorCheck) { @@ -240,8 +239,7 @@ export default function PasswordReset() { setPasswordErrorRepeatedChar, setPasswordErrorEscapeChar, setPasswordErrorLowEntropy, - setPasswordErrorBreached, - errorCheck: false + setPasswordErrorBreached }); }} type="password" diff --git a/frontend/src/pages/signupinvite.tsx b/frontend/src/pages/signupinvite.tsx index 20a093b9c..0784806c8 100644 --- a/frontend/src/pages/signupinvite.tsx +++ b/frontend/src/pages/signupinvite.tsx @@ -16,7 +16,7 @@ import { encodeBase64 } from "tweetnacl-util"; import Button from "@app/components/basic/buttons/Button"; import InputField from "@app/components/basic/InputField"; -import checkPassword from "~/components/utilities/checks/password/checkPassword"; +import checkPassword from "@app/components/utilities/checks/password/checkPassword"; import Aes256Gcm from "@app/components/utilities/cryptography/aes-256-gcm"; import { deriveArgonKey } from "@app/components/utilities/cryptography/crypto"; import issueBackupKey from "@app/components/utilities/cryptography/issueBackupKey"; diff --git a/frontend/src/views/Settings/PersonalSettingsPage/ChangePasswordSection/ChangePasswordSection.tsx b/frontend/src/views/Settings/PersonalSettingsPage/ChangePasswordSection/ChangePasswordSection.tsx index 6a141d652..0c5012cdb 100644 --- a/frontend/src/views/Settings/PersonalSettingsPage/ChangePasswordSection/ChangePasswordSection.tsx +++ b/frontend/src/views/Settings/PersonalSettingsPage/ChangePasswordSection/ChangePasswordSection.tsx @@ -8,7 +8,7 @@ import * as yup from "yup"; import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; import attemptChangePassword from "@app/components/utilities/attemptChangePassword"; -import checkPassword from "~/components/utilities/checks/password/checkPassword"; +import checkPassword from "@app/components/utilities/checks/password/checkPassword"; import { Button, FormControl, Input } from "@app/components/v2"; import { useUser } from "@app/context"; diff --git a/frontend/src/views/Signup/components/UserInfoSSOStep/UserInfoSSOStep.tsx b/frontend/src/views/Signup/components/UserInfoSSOStep/UserInfoSSOStep.tsx index cc23933e9..23353d7d8 100644 --- a/frontend/src/views/Signup/components/UserInfoSSOStep/UserInfoSSOStep.tsx +++ b/frontend/src/views/Signup/components/UserInfoSSOStep/UserInfoSSOStep.tsx @@ -9,7 +9,7 @@ import nacl from "tweetnacl"; import { encodeBase64 } from "tweetnacl-util"; import InputField from "@app/components/basic/InputField"; -import checkPassword from "~/components/utilities/checks/password/checkPassword"; +import checkPassword from "@app/components/utilities/checks/password/checkPassword"; import Aes256Gcm from "@app/components/utilities/cryptography/aes-256-gcm"; import { deriveArgonKey } from "@app/components/utilities/cryptography/crypto"; import { saveTokenToLocalStorage } from "@app/components/utilities/saveTokenToLocalStorage";