From a746188d0049eed27d6767c3d6f1f126a7888849 Mon Sep 17 00:00:00 2001 From: Matheus Cardoso Date: Tue, 28 Oct 2025 13:51:05 -0300 Subject: [PATCH 01/19] feat(e2ee): passphrase requirements --- .../views/account/security/EndToEnd.tsx | 10 ++-- .../account/security/PassphraseVerifier.tsx | 53 +++++++++++++++++++ .../security/PassphraseVerifierItem.tsx | 43 +++++++++++++++ .../account/security/useVerifyPassphrase.ts | 24 +++++++++ .../e2ee-passphrase-management.spec.ts | 7 ++- 5 files changed, 133 insertions(+), 4 deletions(-) create mode 100644 apps/meteor/client/views/account/security/PassphraseVerifier.tsx create mode 100644 apps/meteor/client/views/account/security/PassphraseVerifierItem.tsx create mode 100644 apps/meteor/client/views/account/security/useVerifyPassphrase.ts diff --git a/apps/meteor/client/views/account/security/EndToEnd.tsx b/apps/meteor/client/views/account/security/EndToEnd.tsx index 6285ac8476fac..24f653af72896 100644 --- a/apps/meteor/client/views/account/security/EndToEnd.tsx +++ b/apps/meteor/client/views/account/security/EndToEnd.tsx @@ -7,6 +7,8 @@ import { useId, useEffect } from 'react'; import { Controller, useForm } from 'react-hook-form'; import { Trans, useTranslation } from 'react-i18next'; +import { PassphraseVerifier } from './PassphraseVerifier'; +import { useValidatePassphrase } from './useVerifyPassphrase'; import { e2e } from '../../../lib/e2ee/rocketchat.e2e'; import { useResetE2EPasswordMutation } from '../../hooks/useResetE2EPasswordMutation'; @@ -40,7 +42,7 @@ const EndToEnd = (props: ComponentProps): ReactElement => { */ const keysExist = Boolean(publicKey && privateKey); - const hasTypedPassword = Boolean(password?.trim().length); + const passwordIsValid = useValidatePassphrase(password); const saveNewPassword = async (data: { password: string; passwordConfirm: string }) => { try { @@ -61,6 +63,7 @@ const EndToEnd = (props: ComponentProps): ReactElement => { const passwordId = useId(); const e2ePasswordExplanationId = useId(); const passwordConfirmId = useId(); + const passphraseVerifierId = useId(); return ( @@ -94,6 +97,7 @@ const EndToEnd = (props: ComponentProps): ReactElement => { )} /> + {keysExist && } {!keysExist && ( @@ -117,7 +121,7 @@ const EndToEnd = (props: ComponentProps): ReactElement => { )} - {hasTypedPassword && ( + {passwordIsValid && ( {t('Confirm_new_E2EE_password')} @@ -149,7 +153,7 @@ const EndToEnd = (props: ComponentProps): ReactElement => { + + + ); +}; diff --git a/apps/meteor/client/views/account/security/EndToEnd.tsx b/apps/meteor/client/views/account/security/EndToEnd.tsx index 340921ef40aa7..d406d6e774304 100644 --- a/apps/meteor/client/views/account/security/EndToEnd.tsx +++ b/apps/meteor/client/views/account/security/EndToEnd.tsx @@ -1,184 +1,15 @@ -import { Box, PasswordInput, Field, FieldGroup, FieldLabel, FieldRow, FieldError, FieldHint, Button, Divider } from '@rocket.chat/fuselage'; -import { PasswordVerifierList } from '@rocket.chat/ui-client'; -import { useToastMessageDispatch } from '@rocket.chat/ui-contexts'; -import DOMPurify from 'dompurify'; -import type { ComponentProps, ReactElement } from 'react'; -import { useId, useEffect } from 'react'; -import { Controller, useForm } from 'react-hook-form'; -import { Trans, useTranslation } from 'react-i18next'; +import { Box, Divider } from '@rocket.chat/fuselage'; +import type { ComponentPropsWithoutRef } from 'react'; -import { usePasswordPolicy } from './usePasswordPolicy'; -import { e2e } from '../../../lib/e2ee/rocketchat.e2e'; -import { useResetE2EPasswordMutation } from '../../hooks/useResetE2EPasswordMutation'; -import { useE2EEState } from '../../room/hooks/useE2EEState'; - -const PASSWORD_POLICY = Object.freeze({ - enabled: true, - minLength: 30, - mustContainAtLeastOneLowercase: true, - mustContainAtLeastOneUppercase: true, - mustContainAtLeastOneNumber: true, - mustContainAtLeastOneSpecialCharacter: true, - forbidRepeatingCharacters: false, -}); - -const EndToEnd = (props: ComponentProps): ReactElement => { - const { t } = useTranslation(); - const dispatchToastMessage = useToastMessageDispatch(); - - const e2eeState = useE2EEState(); - - const resetE2EPassword = useResetE2EPasswordMutation(); - const verify = usePasswordPolicy(PASSWORD_POLICY); - - const { - handleSubmit, - watch, - resetField, - formState: { errors, isValid }, - control, - } = useForm({ - defaultValues: { - password: '', - passwordConfirm: '', - }, - }); - - const { password } = watch(); - - const keysExist = e2eeState === 'READY' || e2eeState === 'SAVE_PASSWORD'; - - const { valid, validations } = verify(password); - - const saveNewPassword = async (data: { password: string; passwordConfirm: string }) => { - try { - await e2e.changePassword(data.password); - resetField('password'); - dispatchToastMessage({ type: 'success', message: t('Encryption_key_saved_successfully') }); - } catch (error) { - dispatchToastMessage({ type: 'error', message: error }); - } - }; - - useEffect(() => { - if (password?.trim() === '') { - resetField('passwordConfirm'); - } - }, [password, resetField]); - - const passwordId = useId(); - const e2ePasswordExplanationId = useId(); - const passwordConfirmId = useId(); - const passphraseVerifierId = useId(); +import { ChangePassphrase } from './ChangePassphrase'; +import { ResetPassphrase } from './ResetPassphrase'; +const EndToEnd = (props: ComponentPropsWithoutRef): JSX.Element => { return ( - - - - {t('Change_E2EE_password')} - - - - {t('New_E2EE_password')} - - ( - - )} - /> - - {keysExist && } - {!keysExist && ( - - - To set a new password, first - { - e.preventDefault(); - await e2e.decodePrivateKeyFlow(); - }} - > - enter your current E2EE password. - - - - )} - {errors?.password && ( - - {errors.password.message} - - )} - - {valid && ( - - {t('Confirm_new_E2EE_password')} - - (password !== value ? t('Passwords_do_not_match') : true), - }} - render={({ field }) => ( - - )} - /> - - {errors.passwordConfirm && ( - - {errors.passwordConfirm.message} - - )} - - )} - - - + - - - {t('Reset_E2EE_password')} - - - {t('Reset_E2EE_password_description')} - - - + ); }; diff --git a/apps/meteor/client/views/account/security/ResetPassphrase.tsx b/apps/meteor/client/views/account/security/ResetPassphrase.tsx new file mode 100644 index 0000000000000..990f66ab91cee --- /dev/null +++ b/apps/meteor/client/views/account/security/ResetPassphrase.tsx @@ -0,0 +1,22 @@ +import { Box, Button } from '@rocket.chat/fuselage'; +import { useTranslation } from 'react-i18next'; + +import { useResetE2EPasswordMutation } from '../../hooks/useResetE2EPasswordMutation'; + +export const ResetPassphrase = (): JSX.Element => { + const { t } = useTranslation(); + const resetE2EPassword = useResetE2EPasswordMutation(); + return ( + + + {t('Reset_E2EE_password')} + + + {t('Reset_E2EE_password_description')} + + + + ); +}; From bb14a395e84ad881e705220302643eea7bb4d112 Mon Sep 17 00:00:00 2001 From: Matheus Cardoso Date: Thu, 30 Oct 2025 17:43:45 -0300 Subject: [PATCH 06/19] refactor(e2ee): change password hook --- .../account/security/ChangePassphrase.tsx | 28 +++++++++---------- .../hooks/useChangeE2EPasswordMutation.ts | 24 ++++++++++++++++ 2 files changed, 37 insertions(+), 15 deletions(-) create mode 100644 apps/meteor/client/views/hooks/useChangeE2EPasswordMutation.ts diff --git a/apps/meteor/client/views/account/security/ChangePassphrase.tsx b/apps/meteor/client/views/account/security/ChangePassphrase.tsx index 7a8ff96ea46b6..be29223ecdc37 100644 --- a/apps/meteor/client/views/account/security/ChangePassphrase.tsx +++ b/apps/meteor/client/views/account/security/ChangePassphrase.tsx @@ -1,6 +1,5 @@ import { Box, PasswordInput, Field, FieldGroup, FieldLabel, FieldRow, FieldError, FieldHint, Button } from '@rocket.chat/fuselage'; import { PasswordVerifierList } from '@rocket.chat/ui-client'; -import { useToastMessageDispatch } from '@rocket.chat/ui-contexts'; import DOMPurify from 'dompurify'; import { useId, useEffect } from 'react'; import { Controller, useForm } from 'react-hook-form'; @@ -8,6 +7,7 @@ import { Trans, useTranslation } from 'react-i18next'; import { usePasswordPolicy } from './usePasswordPolicy'; import { e2e } from '../../../lib/e2ee/rocketchat.e2e'; +import { useChangeE2EPasswordMutation } from '../../hooks/useChangeE2EPasswordMutation'; import { useE2EEState } from '../../room/hooks/useE2EEState'; const PASSWORD_POLICY = Object.freeze({ @@ -22,8 +22,8 @@ const PASSWORD_POLICY = Object.freeze({ export const ChangePassphrase = (): JSX.Element => { const { t } = useTranslation(); - const dispatchToastMessage = useToastMessageDispatch(); const verify = usePasswordPolicy(PASSWORD_POLICY); + const changeE2EEPasswordMutation = useChangeE2EPasswordMutation(); const { handleSubmit, @@ -48,16 +48,6 @@ export const ChangePassphrase = (): JSX.Element => { const keysExist = e2eeState === 'READY' || e2eeState === 'SAVE_PASSWORD'; const hasTypedPassword = password.trim().length > 0; - const saveNewPassword = async (data: { password: string; passwordConfirm: string }) => { - try { - await e2e.changePassword(data.password); - resetField('password'); - dispatchToastMessage({ type: 'success', message: t('Encryption_key_saved_successfully') }); - } catch (error) { - dispatchToastMessage({ type: 'error', message: error }); - } - }; - const e2ePasswordExplanationId = useId(); const passwordId = useId(); const passwordConfirmId = useId(); @@ -84,7 +74,13 @@ export const ChangePassphrase = (): JSX.Element => { { + const { valid } = verify(value); + return valid || t('Password_does_not_meet_requirements'); + }, + }} render={({ field }) => ( { )} - {hasTypedPassword && ( + {hasTypedPassword && valid && ( {t('Confirm_new_E2EE_password')} @@ -154,7 +150,9 @@ export const ChangePassphrase = (): JSX.Element => { - + ); }; From cb536c880ea4ae11e78cbdd7d632ff020862c4aa Mon Sep 17 00:00:00 2001 From: Matheus Cardoso Date: Tue, 4 Nov 2025 14:56:56 -0300 Subject: [PATCH 17/19] fix: aria-required and aria-invalid --- .../client/views/account/security/ChangePassphrase.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/meteor/client/views/account/security/ChangePassphrase.tsx b/apps/meteor/client/views/account/security/ChangePassphrase.tsx index 14eab55154ec3..c265a58a9b715 100644 --- a/apps/meteor/client/views/account/security/ChangePassphrase.tsx +++ b/apps/meteor/client/views/account/security/ChangePassphrase.tsx @@ -133,7 +133,7 @@ export const ChangePassphrase = (): JSX.Element => { ] .filter(Boolean) .join(' ')} - aria-invalid={!!errors.passphrase} + aria-invalid={errors.passphrase ? 'true' : 'false'} /> )} /> @@ -181,8 +181,8 @@ export const ChangePassphrase = (): JSX.Element => { error={errors.confirmationPassphrase?.message} flexGrow={1} disabled={!keysExist || !valid} - aria-required={!passphrase} - aria-invalid={!!errors.confirmationPassphrase} + aria-required={passphrase ? 'true' : 'false'} + aria-invalid={errors.confirmationPassphrase ? 'true' : 'false'} aria-describedby={errors.confirmationPassphrase ? confirmPassphraseErrorId : undefined} /> )} From 578a6726c3da132f2515871e2ad68826f60cae02 Mon Sep 17 00:00:00 2001 From: Matheus Cardoso Date: Tue, 4 Nov 2025 15:23:40 -0300 Subject: [PATCH 18/19] chore: apply review suggestions --- .../account/security/ChangePassphrase.tsx | 2 +- .../PasswordVerifier/PasswordVerifierItem.tsx | 48 ++++++++++--------- 2 files changed, 27 insertions(+), 23 deletions(-) diff --git a/apps/meteor/client/views/account/security/ChangePassphrase.tsx b/apps/meteor/client/views/account/security/ChangePassphrase.tsx index c265a58a9b715..752d34ee6f47e 100644 --- a/apps/meteor/client/views/account/security/ChangePassphrase.tsx +++ b/apps/meteor/client/views/account/security/ChangePassphrase.tsx @@ -139,7 +139,7 @@ export const ChangePassphrase = (): JSX.Element => { /> {errors.passphrase && ( -