From 4fd9c4cbaae442d9994e7f075e4fe692c47d3933 Mon Sep 17 00:00:00 2001 From: Yash Rajpal <58601732+yash-rajpal@users.noreply.github.com> Date: Sat, 25 May 2024 01:38:10 +0530 Subject: [PATCH] feat: Un-encrypted messages not allowed in E2EE rooms (#32040) Co-authored-by: gabriellsh <40830821+gabriellsh@users.noreply.github.com> Co-authored-by: Hugo Costa <20212776+hugocostadev@users.noreply.github.com> --- .changeset/slow-cars-press.md | 8 + apps/meteor/app/e2e/client/E2EEState.ts | 9 + .../app/e2e/client/rocketchat.e2e.room.js | 15 ++ apps/meteor/app/e2e/client/rocketchat.e2e.ts | 225 +++++++++++++----- .../app/lib/server/methods/sendMessage.ts | 8 + .../hooks/roomActions/useE2EERoomAction.ts | 10 +- apps/meteor/client/startup/e2e.ts | 14 +- .../client/views/e2e/SaveE2EPasswordModal.tsx | 2 +- .../room/E2EESetup/RoomE2EENotAllowed.tsx | 58 +++++ .../views/room/E2EESetup/RoomE2EESetup.tsx | 69 ++++++ apps/meteor/client/views/room/Room.tsx | 8 +- .../EditRoomInfo/useEditRoomPermissions.ts | 8 +- .../client/views/room/hooks/useE2EERoom.ts | 9 + .../views/room/hooks/useE2EERoomState.ts | 20 ++ .../client/views/room/hooks/useE2EEState.ts | 9 + .../views/room/providers/ChatProvider.tsx | 4 +- .../hooks/useChatMessagesInstance.ts | 12 +- apps/meteor/server/settings/e2e.ts | 6 + apps/meteor/tests/e2e/e2e-encryption.spec.ts | 216 +++++++++++++++-- .../page-objects/fragments/home-sidenav.ts | 11 + apps/meteor/tests/end-to-end/api/03-groups.js | 72 ++++++ packages/i18n/src/locales/en.i18n.json | 13 +- 22 files changed, 700 insertions(+), 106 deletions(-) create mode 100644 .changeset/slow-cars-press.md create mode 100644 apps/meteor/app/e2e/client/E2EEState.ts create mode 100644 apps/meteor/client/views/room/E2EESetup/RoomE2EENotAllowed.tsx create mode 100644 apps/meteor/client/views/room/E2EESetup/RoomE2EESetup.tsx create mode 100644 apps/meteor/client/views/room/hooks/useE2EERoom.ts create mode 100644 apps/meteor/client/views/room/hooks/useE2EERoomState.ts create mode 100644 apps/meteor/client/views/room/hooks/useE2EEState.ts diff --git a/.changeset/slow-cars-press.md b/.changeset/slow-cars-press.md new file mode 100644 index 000000000000..de4d08ff52ff --- /dev/null +++ b/.changeset/slow-cars-press.md @@ -0,0 +1,8 @@ +--- +'@rocket.chat/i18n': minor +'@rocket.chat/meteor': minor +--- + +Introduced a new setting which doesn't allow users to access encrypted rooms until E2EE is configured and also doesn't allow users to send un-encrypted messages in encrypted rooms. + +New room setup for E2EE feature which helps users to setup their E2EE keys and introduced states to E2EE feature. diff --git a/apps/meteor/app/e2e/client/E2EEState.ts b/apps/meteor/app/e2e/client/E2EEState.ts new file mode 100644 index 000000000000..0e505ec4a1bd --- /dev/null +++ b/apps/meteor/app/e2e/client/E2EEState.ts @@ -0,0 +1,9 @@ +export enum E2EEState { + NOT_STARTED = 'NOT_STARTED', + DISABLED = 'DISABLED', + LOADING_KEYS = 'LOADING_KEYS', + READY = 'READY', + SAVE_PASSWORD = 'SAVE_PASSWORD', + ENTER_PASSWORD = 'ENTER_PASSWORD', + ERROR = 'ERROR', +} diff --git a/apps/meteor/app/e2e/client/rocketchat.e2e.room.js b/apps/meteor/app/e2e/client/rocketchat.e2e.room.js index bd0863d691a9..554cd6a327a8 100644 --- a/apps/meteor/app/e2e/client/rocketchat.e2e.room.js +++ b/apps/meteor/app/e2e/client/rocketchat.e2e.room.js @@ -41,6 +41,7 @@ const permitedMutations = { E2ERoomState.ERROR, E2ERoomState.DISABLED, E2ERoomState.WAITING_KEYS, + E2ERoomState.CREATING_KEYS, ], }; @@ -92,6 +93,10 @@ export class E2ERoom extends Emitter { logError(`E2E ROOM { state: ${this.state}, rid: ${this.roomId} }`, ...msg); } + getState() { + return this.state; + } + setState(requestedState) { const currentState = this.state; const nextState = filterMutation(currentState, requestedState); @@ -208,6 +213,10 @@ export class E2ERoom extends Emitter { // Initiates E2E Encryption async handshake() { + if (!e2e.isReady()) { + return; + } + if (this.state !== E2ERoomState.KEYS_RECEIVED && this.state !== E2ERoomState.NOT_STARTED) { return; } @@ -459,5 +468,11 @@ export class E2ERoom extends Emitter { } this.encryptKeyForOtherParticipants(); + this.setState(E2ERoomState.READY); + } + + onStateChange(cb) { + this.on('STATE_CHANGED', cb); + return () => this.off('STATE_CHANGED', cb); } } diff --git a/apps/meteor/app/e2e/client/rocketchat.e2e.ts b/apps/meteor/app/e2e/client/rocketchat.e2e.ts index 1a98ce857f01..aeb51c292715 100644 --- a/apps/meteor/app/e2e/client/rocketchat.e2e.ts +++ b/apps/meteor/app/e2e/client/rocketchat.e2e.ts @@ -6,8 +6,7 @@ import { isE2EEMessage } from '@rocket.chat/core-typings'; import { Emitter } from '@rocket.chat/emitter'; import EJSON from 'ejson'; import { Meteor } from 'meteor/meteor'; -import type { ReactiveVar as ReactiveVarType } from 'meteor/reactive-var'; -import { ReactiveVar } from 'meteor/reactive-var'; +import { Tracker } from 'meteor/tracker'; import * as banners from '../../../client/lib/banners'; import type { LegacyBannerPayload } from '../../../client/lib/banners'; @@ -24,6 +23,7 @@ import { settings } from '../../settings/client'; import { getUserAvatarURL } from '../../utils/client'; import { sdk } from '../../utils/client/lib/SDKClient'; import { t } from '../../utils/lib/i18n'; +import { E2EEState } from './E2EEState'; import { toString, toArrayBuffer, @@ -49,36 +49,39 @@ type KeyPair = { private_key: string | null; }; +const E2EEStateDependency = new Tracker.Dependency(); + class E2E extends Emitter { private started: boolean; - public enabled: ReactiveVarType; - - private _ready: ReactiveVarType; - private instancesByRoomId: Record; - private db_public_key: string | null; + private db_public_key: string | null | undefined; - private db_private_key: string | null; + private db_private_key: string | null | undefined; public privateKey: CryptoKey | undefined; + private state: E2EEState; + constructor() { super(); this.started = false; - this.enabled = new ReactiveVar(false); - this._ready = new ReactiveVar(false); this.instancesByRoomId = {}; - this.on('ready', async () => { - this._ready.set(true); - this.log('startClient -> Done'); - this.log('decryptSubscriptions'); + this.on('E2E_STATE_CHANGED', ({ prevState, nextState }) => { + this.log(`${prevState} -> ${nextState}`); + }); + + this.on(E2EEState.READY, async () => { + await this.onE2EEReady(); + }); - await this.decryptSubscriptions(); - this.log('decryptSubscriptions -> Done'); + this.on(E2EEState.SAVE_PASSWORD, async () => { + await this.onE2EEReady(); }); + + this.setState(E2EEState.NOT_STARTED); } log(...msg: unknown[]) { @@ -89,12 +92,46 @@ class E2E extends Emitter { logError('E2E', ...msg); } + getState() { + return this.state; + } + isEnabled(): boolean { - return this.enabled.get(); + return this.state !== E2EEState.DISABLED; } isReady(): boolean { - return this.enabled.get() && this._ready.get(); + E2EEStateDependency.depend(); + + // Save_Password state is also a ready state for E2EE + return this.state === E2EEState.READY || this.state === E2EEState.SAVE_PASSWORD; + } + + async onE2EEReady() { + this.log('startClient -> Done'); + this.log('decryptSubscriptions'); + this.initiateHandshake(); + await this.decryptSubscriptions(); + this.log('decryptSubscriptions -> Done'); + await this.initiateDecryptingPendingMessages(); + this.log('DecryptingPendingMessages -> Done'); + } + + shouldAskForE2EEPassword() { + const { private_key } = this.getKeysFromLocalStorage(); + return this.db_private_key && !private_key; + } + + setState(nextState: E2EEState) { + const prevState = this.state; + + this.state = nextState; + + E2EEStateDependency.changed(); + + this.emit('E2E_STATE_CHANGED', { prevState, nextState }); + + this.emit(nextState); } async getInstanceByRoomId(rid: IRoom['_id']): Promise { @@ -155,6 +192,35 @@ class E2E extends Emitter { }; } + initiateHandshake() { + Object.keys(this.instancesByRoomId).map((key) => this.instancesByRoomId[key].handshake()); + } + + async initiateDecryptingPendingMessages() { + await Promise.all(Object.keys(this.instancesByRoomId).map((key) => this.instancesByRoomId[key].decryptPendingMessages())); + } + + openSaveE2EEPasswordModal(randomPassword: string) { + imperativeModal.open({ + component: SaveE2EPasswordModal, + props: { + randomPassword, + onClose: imperativeModal.close, + onCancel: () => { + this.closeAlert(); + imperativeModal.close(); + }, + onConfirm: () => { + Meteor._localStorage.removeItem('e2e.randomPassword'); + this.setState(E2EEState.READY); + dispatchToastMessage({ type: 'success', message: t('End_To_End_Encryption_Enabled') }); + this.closeAlert(); + imperativeModal.close(); + }, + }, + }); + } + async startClient(): Promise { if (this.started) { return; @@ -172,9 +238,10 @@ class E2E extends Emitter { public_key = this.db_public_key; } - if (!private_key && this.db_private_key) { + if (this.shouldAskForE2EEPassword()) { try { - private_key = await this.decodePrivateKey(this.db_private_key); + this.setState(E2EEState.ENTER_PASSWORD); + private_key = await this.decodePrivateKey(this.db_private_key as string); } catch (error) { this.started = false; failedToDecodeKey = true; @@ -195,44 +262,29 @@ class E2E extends Emitter { if (public_key && private_key) { await this.loadKeys({ public_key, private_key }); + this.setState(E2EEState.READY); } else { await this.createAndLoadKeys(); + this.setState(E2EEState.READY); } if (!this.db_public_key || !this.db_private_key) { + this.setState(E2EEState.LOADING_KEYS); await this.persistKeys(this.getKeysFromLocalStorage(), await this.createRandomPassword()); } const randomPassword = Meteor._localStorage.getItem('e2e.randomPassword'); if (randomPassword) { + this.setState(E2EEState.SAVE_PASSWORD); this.openAlert({ title: () => t('Save_your_encryption_password'), html: () => t('Click_here_to_view_and_copy_your_password'), modifiers: ['large'], closable: false, icon: 'key', - action: () => { - imperativeModal.open({ - component: SaveE2EPasswordModal, - props: { - randomPassword, - onClose: imperativeModal.close, - onCancel: () => { - this.closeAlert(); - imperativeModal.close(); - }, - onConfirm: () => { - Meteor._localStorage.removeItem('e2e.randomPassword'); - this.closeAlert(); - dispatchToastMessage({ type: 'success', message: t('End_To_End_Encryption_Set') }); - imperativeModal.close(); - }, - }, - }); - }, + action: () => this.openSaveE2EEPasswordModal(randomPassword), }); } - this.emit('ready'); } async stopClient(): Promise { @@ -243,9 +295,8 @@ class E2E extends Emitter { Meteor._localStorage.removeItem('private_key'); this.instancesByRoomId = {}; this.privateKey = undefined; - this.enabled.set(false); - this._ready.set(false); this.started = false; + this.setState(E2EEState.DISABLED); } async changePassword(newPassword: string): Promise { @@ -258,11 +309,13 @@ class E2E extends Emitter { async loadKeysFromDB(): Promise { try { + this.setState(E2EEState.LOADING_KEYS); const { public_key, private_key } = await sdk.rest.get('/v1/e2e.fetchMyKeys'); this.db_public_key = public_key; this.db_private_key = private_key; } catch (error) { + this.setState(E2EEState.ERROR); return this.error('Error fetching RSA keys: ', error); } } @@ -275,17 +328,20 @@ class E2E extends Emitter { Meteor._localStorage.setItem('private_key', private_key); } catch (error) { + this.setState(E2EEState.ERROR); return this.error('Error importing private key: ', error); } } async createAndLoadKeys(): Promise { // Could not obtain public-private keypair from server. + this.setState(E2EEState.LOADING_KEYS); let key; try { key = await generateRSAKey(); this.privateKey = key.privateKey; } catch (error) { + this.setState(E2EEState.ERROR); return this.error('Error generating key: ', error); } @@ -294,6 +350,7 @@ class E2E extends Emitter { Meteor._localStorage.setItem('public_key', JSON.stringify(publicKey)); } catch (error) { + this.setState(E2EEState.ERROR); return this.error('Error exporting public key: ', error); } @@ -302,6 +359,7 @@ class E2E extends Emitter { Meteor._localStorage.setItem('private_key', JSON.stringify(privateKey)); } catch (error) { + this.setState(E2EEState.ERROR); return this.error('Error exporting private key: ', error); } @@ -327,6 +385,7 @@ class E2E extends Emitter { return EJSON.stringify(joinVectorAndEcryptedData(vector, encodedPrivateKey)); } catch (error) { + this.setState(E2EEState.ERROR); return this.error('Error encrypting encodedPrivateKey: ', error); } } @@ -341,6 +400,7 @@ class E2E extends Emitter { try { baseKey = await importRawKey(toArrayBuffer(password)); } catch (error) { + this.setState(E2EEState.ERROR); return this.error('Error creating a key based on user password: ', error); } @@ -348,30 +408,34 @@ class E2E extends Emitter { try { return await deriveKey(toArrayBuffer(Meteor.userId()), baseKey); } catch (error) { + this.setState(E2EEState.ERROR); return this.error('Error deriving baseKey: ', error); } } - async requestPassword(): Promise { + openEnterE2EEPasswordModal(onEnterE2EEPassword?: (password: string) => void) { + imperativeModal.open({ + component: EnterE2EPasswordModal, + props: { + onClose: imperativeModal.close, + onCancel: () => { + failedToDecodeKey = false; + dispatchToastMessage({ type: 'info', message: t('End_To_End_Encryption_Not_Enabled') }); + this.closeAlert(); + imperativeModal.close(); + }, + onConfirm: (password) => { + onEnterE2EEPassword?.(password); + this.closeAlert(); + imperativeModal.close(); + }, + }, + }); + } + + async requestPasswordAlert(): Promise { return new Promise((resolve) => { - const showModal = () => { - imperativeModal.open({ - component: EnterE2EPasswordModal, - props: { - onClose: imperativeModal.close, - onCancel: () => { - failedToDecodeKey = false; - this.closeAlert(); - imperativeModal.close(); - }, - onConfirm: (password) => { - resolve(password); - this.closeAlert(); - imperativeModal.close(); - }, - }, - }); - }; + const showModal = () => this.openEnterE2EEPasswordModal((password) => resolve(password)); const showAlert = () => { this.openAlert({ @@ -394,8 +458,42 @@ class E2E extends Emitter { }); } + async requestPasswordModal(): Promise { + return new Promise((resolve) => this.openEnterE2EEPasswordModal((password) => resolve(password))); + } + + async decodePrivateKeyFlow() { + const password = await this.requestPasswordModal(); + const masterKey = await this.getMasterKey(password); + + if (!this.db_private_key) { + return; + } + + const [vector, cipherText] = splitVectorAndEcryptedData(EJSON.parse(this.db_private_key)); + + try { + const privKey = await decryptAES(vector, masterKey, cipherText); + const privateKey = toString(privKey) as string; + + if (this.db_public_key && privateKey) { + await this.loadKeys({ public_key: this.db_public_key, private_key: privateKey }); + this.setState(E2EEState.READY); + } else { + await this.createAndLoadKeys(); + this.setState(E2EEState.READY); + } + dispatchToastMessage({ type: 'success', message: t('End_To_End_Encryption_Enabled') }); + } catch (error) { + this.setState(E2EEState.ENTER_PASSWORD); + dispatchToastMessage({ type: 'error', message: t('Your_E2EE_password_is_incorrect') }); + dispatchToastMessage({ type: 'info', message: t('End_To_End_Encryption_Not_Enabled') }); + throw new Error('E2E -> Error decrypting private key'); + } + } + async decodePrivateKey(privateKey: string): Promise { - const password = await this.requestPassword(); + const password = await this.requestPasswordAlert(); const masterKey = await this.getMasterKey(password); @@ -405,6 +503,9 @@ class E2E extends Emitter { const privKey = await decryptAES(vector, masterKey, cipherText); return toString(privKey); } catch (error) { + this.setState(E2EEState.ENTER_PASSWORD); + dispatchToastMessage({ type: 'error', message: t('Your_E2EE_password_is_incorrect') }); + dispatchToastMessage({ type: 'info', message: t('End_To_End_Encryption_Not_Enabled') }); throw new Error('E2E -> Error decrypting private key'); } } diff --git a/apps/meteor/app/lib/server/methods/sendMessage.ts b/apps/meteor/app/lib/server/methods/sendMessage.ts index 5749daa980f3..c78407782059 100644 --- a/apps/meteor/app/lib/server/methods/sendMessage.ts +++ b/apps/meteor/app/lib/server/methods/sendMessage.ts @@ -81,6 +81,14 @@ export async function executeSendMessage(uid: IUser['_id'], message: AtLeast('E2E_Enable') && !settings.get('E2E_Allow_Unencrypted_Messages')) { + if (message.t !== 'e2e' || message.e2e !== 'pending') { + throw new Meteor.Error('error-not-allowed', 'Not allowed to send un-encrypted messages in an encrypted room', { + method: 'sendMessage', + }); + } + } + metrics.messagesSent.inc(); // TODO This line needs to be moved to it's proper place. See the comments on: https://github.com/RocketChat/Rocket.Chat/pull/5736 return await sendMessage(user, message, room, false, previewUrls); } catch (err: any) { diff --git a/apps/meteor/client/hooks/roomActions/useE2EERoomAction.ts b/apps/meteor/client/hooks/roomActions/useE2EERoomAction.ts index 73b0f34836e1..abc84f372594 100644 --- a/apps/meteor/client/hooks/roomActions/useE2EERoomAction.ts +++ b/apps/meteor/client/hooks/roomActions/useE2EERoomAction.ts @@ -1,20 +1,22 @@ import { isRoomFederated } from '@rocket.chat/core-typings'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import { useSetting, usePermission, useEndpoint } from '@rocket.chat/ui-contexts'; -import { useCallback, useMemo } from 'react'; +import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { e2e } from '../../../app/e2e/client/rocketchat.e2e'; +import { E2EEState } from '../../../app/e2e/client/E2EEState'; import { dispatchToastMessage } from '../../lib/toast'; import { useRoom, useRoomSubscription } from '../../views/room/contexts/RoomContext'; import type { RoomToolboxActionConfig } from '../../views/room/contexts/RoomToolboxContext'; -import { useReactiveValue } from '../useReactiveValue'; +import { useE2EEState } from '../../views/room/hooks/useE2EEState'; export const useE2EERoomAction = () => { const enabled = useSetting('E2E_Enable', false); const room = useRoom(); const subscription = useRoomSubscription(); - const readyToEncrypt = useReactiveValue(useCallback(() => e2e.isReady(), [])) || room.encrypted; + const e2eeState = useE2EEState(); + const isE2EEReady = e2eeState === E2EEState.READY || e2eeState === E2EEState.SAVE_PASSWORD; + const readyToEncrypt = isE2EEReady || room.encrypted; const permittedToToggleEncryption = usePermission('toggle-room-e2e-encryption', room._id); const permittedToEditRoom = usePermission('edit-room', room._id); const permitted = (room.t === 'd' || (permittedToEditRoom && permittedToToggleEncryption)) && readyToEncrypt; diff --git a/apps/meteor/client/startup/e2e.ts b/apps/meteor/client/startup/e2e.ts index f9cf156f8d8b..0cf5dca41ca4 100644 --- a/apps/meteor/client/startup/e2e.ts +++ b/apps/meteor/client/startup/e2e.ts @@ -2,6 +2,7 @@ import type { AtLeast, IMessage, ISubscription } from '@rocket.chat/core-typings import { Meteor } from 'meteor/meteor'; import { Tracker } from 'meteor/tracker'; +import { E2EEState } from '../../app/e2e/client/E2EEState'; import { e2e } from '../../app/e2e/client/rocketchat.e2e'; import { Subscriptions, ChatRoom } from '../../app/models/client'; import { settings } from '../../app/settings/client'; @@ -28,9 +29,8 @@ Meteor.startup(() => { if (enabled && !adminEmbedded) { e2e.startClient(); - e2e.enabled.set(true); } else { - e2e.enabled.set(false); + e2e.setState(E2EEState.DISABLED); e2e.closeAlert(); } }); @@ -39,6 +39,8 @@ Meteor.startup(() => { let offClientMessageReceived: undefined | (() => void); let offClientBeforeSendMessage: undefined | (() => void); let unsubNotifyUser: undefined | (() => void); + let listenersAttached = false; + Tracker.autorun(() => { if (!e2e.isReady()) { offClientMessageReceived?.(); @@ -46,6 +48,11 @@ Meteor.startup(() => { unsubNotifyUser = undefined; observable?.stop(); offClientBeforeSendMessage?.(); + listenersAttached = false; + return; + } + + if (listenersAttached) { return; } @@ -141,11 +148,12 @@ Meteor.startup(() => { // Should encrypt this message. const msg = await e2eRoom.encrypt(message); - message.msg = msg; message.t = 'e2e'; message.e2e = 'pending'; return message; }); + + listenersAttached = true; }); }); diff --git a/apps/meteor/client/views/e2e/SaveE2EPasswordModal.tsx b/apps/meteor/client/views/e2e/SaveE2EPasswordModal.tsx index f642e68e09af..b16837bc3d17 100644 --- a/apps/meteor/client/views/e2e/SaveE2EPasswordModal.tsx +++ b/apps/meteor/client/views/e2e/SaveE2EPasswordModal.tsx @@ -14,7 +14,7 @@ type SaveE2EPasswordModalProps = { onConfirm: () => void; }; -const DOCS_URL = 'https://rocket.chat/docs/user-guides/end-to-end-encryption/'; +const DOCS_URL = 'https://go.rocket.chat/i/e2ee-guide'; const SaveE2EPasswordModal = ({ randomPassword, onClose, onCancel, onConfirm }: SaveE2EPasswordModalProps): ReactElement => { const t = useTranslation(); diff --git a/apps/meteor/client/views/room/E2EESetup/RoomE2EENotAllowed.tsx b/apps/meteor/client/views/room/E2EESetup/RoomE2EENotAllowed.tsx new file mode 100644 index 000000000000..03beebb8487e --- /dev/null +++ b/apps/meteor/client/views/room/E2EESetup/RoomE2EENotAllowed.tsx @@ -0,0 +1,58 @@ +import { + Box, + Button, + States, + StatesAction, + StatesActions, + StatesIcon, + StatesLink, + StatesSubtitle, + StatesTitle, +} from '@rocket.chat/fuselage'; +import type { Keys as IconName } from '@rocket.chat/icons'; +import { useRouter, useTranslation } from '@rocket.chat/ui-contexts'; +import type { ReactElement } from 'react'; +import React from 'react'; + +const DOCS_URL = 'https://go.rocket.chat/i/e2ee-guide'; + +type RoomE2EENotAllowedProps = { + title: string; + subTitle: string; + action?: () => void; + btnText?: string; + icon: IconName; +}; + +const RoomE2EENotAllowed = ({ title, subTitle, action, btnText, icon }: RoomE2EENotAllowedProps): ReactElement => { + const router = useRouter(); + const t = useTranslation(); + const handleGoHomeClick = () => { + router.navigate('/home'); + }; + + return ( + + + + {title} + {subTitle} + {action && ( + + + + {btnText} + + + )} + + {t('Learn_more_about_E2EE')} + + + + ); +}; + +export default RoomE2EENotAllowed; diff --git a/apps/meteor/client/views/room/E2EESetup/RoomE2EESetup.tsx b/apps/meteor/client/views/room/E2EESetup/RoomE2EESetup.tsx new file mode 100644 index 000000000000..71604da54b67 --- /dev/null +++ b/apps/meteor/client/views/room/E2EESetup/RoomE2EESetup.tsx @@ -0,0 +1,69 @@ +import { useTranslation } from '@rocket.chat/ui-contexts'; +import React, { useCallback } from 'react'; + +import { e2e } from '../../../../app/e2e/client'; +import { E2EEState } from '../../../../app/e2e/client/E2EEState'; +import { E2ERoomState } from '../../../../app/e2e/client/E2ERoomState'; +import RoomBody from '../body/RoomBody'; +import { useRoom } from '../contexts/RoomContext'; +import { useE2EERoomState } from '../hooks/useE2EERoomState'; +import { useE2EEState } from '../hooks/useE2EEState'; +import RoomE2EENotAllowed from './RoomE2EENotAllowed'; + +const RoomE2EESetup = () => { + const room = useRoom(); + + const e2eeState = useE2EEState(); + const e2eRoomState = useE2EERoomState(room._id); + + const t = useTranslation(); + const randomPassword = window.localStorage.getItem('e2e.randomPassword'); + + const onSavePassword = useCallback(() => { + if (!randomPassword) { + return; + } + + e2e.openSaveE2EEPasswordModal(randomPassword); + }, [randomPassword]); + + const onEnterE2EEPassword = useCallback(() => e2e.decodePrivateKeyFlow(), []); + + if (e2eeState === E2EEState.SAVE_PASSWORD) { + return ( + + ); + } + + if (e2eeState === E2EEState.ENTER_PASSWORD) { + return ( + + ); + } + + if (e2eRoomState === E2ERoomState.WAITING_KEYS) { + return ( + + ); + } + + return ; +}; + +export default RoomE2EESetup; diff --git a/apps/meteor/client/views/room/Room.tsx b/apps/meteor/client/views/room/Room.tsx index 8dcc76a20e57..6bbeb9f9e230 100644 --- a/apps/meteor/client/views/room/Room.tsx +++ b/apps/meteor/client/views/room/Room.tsx @@ -1,10 +1,11 @@ -import { useTranslation } from '@rocket.chat/ui-contexts'; +import { useTranslation, useSetting } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React, { createElement, lazy, memo, Suspense } from 'react'; import { FocusScope } from 'react-aria'; import { ErrorBoundary } from 'react-error-boundary'; import { ContextualbarSkeleton } from '../../components/Contextualbar'; +import RoomE2EESetup from './E2EESetup/RoomE2EESetup'; import Header from './Header'; import MessageHighlightProvider from './MessageList/providers/MessageHighlightProvider'; import RoomBody from './body/RoomBody'; @@ -23,6 +24,9 @@ const Room = (): ReactElement => { const room = useRoom(); const toolbox = useRoomToolbox(); const contextualBarView = useAppsContextualBar(); + const isE2EEnabled = useSetting('E2E_Enable'); + const unencryptedMessagesAllowed = useSetting('E2E_Allow_Unencrypted_Messages'); + const shouldDisplayE2EESetup = room?.encrypted && !unencryptedMessagesAllowed && isE2EEnabled; return ( @@ -37,7 +41,7 @@ const Room = (): ReactElement => { : t('Channel__roomName__', { roomName: room.name }) } header={
} - body={} + body={shouldDisplayE2EESetup ? : } aside={ (toolbox.tab?.tabComponent && ( diff --git a/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/useEditRoomPermissions.ts b/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/useEditRoomPermissions.ts index 7b9e8c353941..579a649f7090 100644 --- a/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/useEditRoomPermissions.ts +++ b/apps/meteor/client/views/room/contextualBar/Info/EditRoomInfo/useEditRoomPermissions.ts @@ -2,9 +2,10 @@ import type { IRoom, IRoomWithRetentionPolicy } from '@rocket.chat/core-typings' import { usePermission, useAtLeastOnePermission, useRole } from '@rocket.chat/ui-contexts'; import { useMemo } from 'react'; -import { e2e } from '../../../../../../app/e2e/client/rocketchat.e2e'; +import { E2EEState } from '../../../../../../app/e2e/client/E2EEState'; import { RoomSettingsEnum } from '../../../../../../definition/IRoomTypeConfig'; import { roomCoordinator } from '../../../../../lib/rooms/roomCoordinator'; +import { useE2EEState } from '../../../hooks/useE2EEState'; const getCanChangeType = (room: IRoom | IRoomWithRetentionPolicy, canCreateChannel: boolean, canCreateGroup: boolean, isAdmin: boolean) => (!room.default || isAdmin) && ((room.t === 'p' && canCreateChannel) || (room.t === 'c' && canCreateGroup)); @@ -13,7 +14,8 @@ export const useEditRoomPermissions = (room: IRoom | IRoomWithRetentionPolicy) = const isAdmin = useRole('admin'); const canCreateChannel = usePermission('create-c'); const canCreateGroup = usePermission('create-p'); - + const e2eeState = useE2EEState(); + const isE2EEReady = e2eeState === E2EEState.READY || e2eeState === E2EEState.SAVE_PASSWORD; const canChangeType = getCanChangeType(room, canCreateChannel, canCreateGroup, isAdmin); const canSetReadOnly = usePermission('set-readonly', room._id); const canSetReactWhenReadOnly = usePermission('set-react-when-readonly', room._id); @@ -22,7 +24,7 @@ export const useEditRoomPermissions = (room: IRoom | IRoomWithRetentionPolicy) = useMemo(() => ['archive-room', 'unarchive-room'], []), room._id, ); - const canToggleEncryption = usePermission('toggle-room-e2e-encryption', room._id) && (room.encrypted || e2e.isReady()); + const canToggleEncryption = usePermission('toggle-room-e2e-encryption', room._id) && (room.encrypted || isE2EEReady); const [ canViewName, diff --git a/apps/meteor/client/views/room/hooks/useE2EERoom.ts b/apps/meteor/client/views/room/hooks/useE2EERoom.ts new file mode 100644 index 000000000000..3645491ca9d4 --- /dev/null +++ b/apps/meteor/client/views/room/hooks/useE2EERoom.ts @@ -0,0 +1,9 @@ +import { useQuery } from '@tanstack/react-query'; + +import { e2e } from '../../../../app/e2e/client'; + +export const useE2EERoom = (rid: string) => { + const { data } = useQuery(['e2eRoom', rid], () => e2e.getInstanceByRoomId(rid)); + + return data; +}; diff --git a/apps/meteor/client/views/room/hooks/useE2EERoomState.ts b/apps/meteor/client/views/room/hooks/useE2EERoomState.ts new file mode 100644 index 000000000000..c541aad121d4 --- /dev/null +++ b/apps/meteor/client/views/room/hooks/useE2EERoomState.ts @@ -0,0 +1,20 @@ +import { useMemo } from 'react'; +import { useSyncExternalStore } from 'use-sync-external-store/shim'; + +import type { E2ERoomState } from '../../../../app/e2e/client/E2ERoomState'; +import { useE2EERoom } from './useE2EERoom'; + +export const useE2EERoomState = (rid: string) => { + const e2eRoom = useE2EERoom(rid); + + const subscribeE2EERoomState = useMemo( + () => + [ + (callback: () => void): (() => void) => (e2eRoom ? e2eRoom.onStateChange(callback) : () => undefined), + (): E2ERoomState | undefined => (e2eRoom ? e2eRoom.getState() : undefined), + ] as const, + [e2eRoom], + ); + + return useSyncExternalStore(...subscribeE2EERoomState); +}; diff --git a/apps/meteor/client/views/room/hooks/useE2EEState.ts b/apps/meteor/client/views/room/hooks/useE2EEState.ts new file mode 100644 index 000000000000..79cd5893159d --- /dev/null +++ b/apps/meteor/client/views/room/hooks/useE2EEState.ts @@ -0,0 +1,9 @@ +import { useSyncExternalStore } from 'use-sync-external-store/shim'; + +import { e2e } from '../../../../app/e2e/client'; +import type { E2EEState } from '../../../../app/e2e/client/E2EEState'; + +const subscribe = (callback: () => void): (() => void) => e2e.on('E2E_STATE_CHANGED', callback); +const getSnapshot = (): E2EEState => e2e.getState(); + +export const useE2EEState = (): E2EEState => useSyncExternalStore(subscribe, getSnapshot); diff --git a/apps/meteor/client/views/room/providers/ChatProvider.tsx b/apps/meteor/client/views/room/providers/ChatProvider.tsx index 5b17e3e881a6..2e89c1c75702 100644 --- a/apps/meteor/client/views/room/providers/ChatProvider.tsx +++ b/apps/meteor/client/views/room/providers/ChatProvider.tsx @@ -11,8 +11,8 @@ type ChatProviderProps = { }; const ChatProvider = ({ children, tmid }: ChatProviderProps): ReactElement => { - const { _id: rid } = useRoom(); - const value = useChatMessagesInstance({ rid, tmid }); + const { _id: rid, encrypted } = useRoom(); + const value = useChatMessagesInstance({ rid, tmid, encrypted }); return {children}; }; diff --git a/apps/meteor/client/views/room/providers/hooks/useChatMessagesInstance.ts b/apps/meteor/client/views/room/providers/hooks/useChatMessagesInstance.ts index 290b1c4be680..69fe6e8f968f 100644 --- a/apps/meteor/client/views/room/providers/hooks/useChatMessagesInstance.ts +++ b/apps/meteor/client/views/room/providers/hooks/useChatMessagesInstance.ts @@ -9,7 +9,15 @@ import { useUiKitActionManager } from '../../../../uikit/hooks/useUiKitActionMan import { useRoomSubscription } from '../../contexts/RoomContext'; import { useInstance } from './useInstance'; -export function useChatMessagesInstance({ rid, tmid }: { rid: IRoom['_id']; tmid?: IMessage['_id'] }): ChatAPI { +export function useChatMessagesInstance({ + rid, + tmid, + encrypted, +}: { + rid: IRoom['_id']; + tmid?: IMessage['_id']; + encrypted: IRoom['encrypted']; +}): ChatAPI { const uid = useUserId(); const subscription = useRoomSubscription(); const actionManager = useUiKitActionManager(); @@ -17,7 +25,7 @@ export function useChatMessagesInstance({ rid, tmid }: { rid: IRoom['_id']; tmid const instance = new ChatMessages({ rid, tmid, uid, actionManager }); return [instance, () => instance.release()]; - }, [rid, tmid, uid]); + }, [rid, tmid, uid, encrypted]); useEffect(() => { if (subscription) { diff --git a/apps/meteor/server/settings/e2e.ts b/apps/meteor/server/settings/e2e.ts index 2c9f0c0446ac..0ff31090f5d8 100644 --- a/apps/meteor/server/settings/e2e.ts +++ b/apps/meteor/server/settings/e2e.ts @@ -10,6 +10,12 @@ export const createE2ESettings = () => alert: 'E2E_Enable_alert', }); + await this.add('E2E_Allow_Unencrypted_Messages', true, { + type: 'boolean', + public: true, + enableQuery: { _id: 'E2E_Enable', value: true }, + }); + await this.add('E2E_Enabled_Default_DirectRooms', false, { type: 'boolean', public: true, diff --git a/apps/meteor/tests/e2e/e2e-encryption.spec.ts b/apps/meteor/tests/e2e/e2e-encryption.spec.ts index c1a1ba0d2872..55de92ef8bad 100644 --- a/apps/meteor/tests/e2e/e2e-encryption.spec.ts +++ b/apps/meteor/tests/e2e/e2e-encryption.spec.ts @@ -7,15 +7,6 @@ import { Users, storeState, restoreState } from './fixtures/userStates'; import { AccountProfile, HomeChannel } from './page-objects'; import { test, expect } from './utils/test'; -// OK Enable e2ee on admin -// OK Test banner and check password, logout and use password -// OK Set new password, logout and use the password -// OK Reset key, should logout, login and check banner -// OK Create channel encrypted and send message -// OK Disable encryption and send message -// OK Enable encryption and send message -// OK Create channel not encrypted, encrypt end send message - test.use({ storageState: Users.admin.state }); test.describe.serial('e2e-encryption initial setup', () => { @@ -79,7 +70,7 @@ test.describe.serial('e2e-encryption initial setup', () => { await page.locator('role=banner >> text="Enter your E2E password"').click(); - await page.locator('#modal-root input').type(password); + await page.locator('#modal-root input').fill(password); await page.locator('#modal-root .rcx-button--primary').click(); @@ -96,8 +87,8 @@ test.describe.serial('e2e-encryption initial setup', () => { await poAccountProfile.securityE2EEncryptionSection.click(); await poAccountProfile.securityE2EEncryptionPassword.click(); - await poAccountProfile.securityE2EEncryptionPassword.type(newPassword); - await poAccountProfile.securityE2EEncryptionPasswordConfirmation.type(newPassword); + await poAccountProfile.securityE2EEncryptionPassword.fill(newPassword); + await poAccountProfile.securityE2EEncryptionPasswordConfirmation.fill(newPassword); await poAccountProfile.securityE2EEncryptionSavePasswordButton.click(); await poAccountProfile.btnClose.click(); @@ -112,13 +103,13 @@ test.describe.serial('e2e-encryption initial setup', () => { await page.locator('role=banner >> text="Enter your E2E password"').click(); - await page.locator('#modal-root input').type(password); + await page.locator('#modal-root input').fill(password); await page.locator('#modal-root .rcx-button--primary').click(); await page.locator('role=banner >> text="Wasn\'t possible to decode your encryption key to be imported."').click(); - await page.locator('#modal-root input').type(newPassword); + await page.locator('#modal-root input').fill(newPassword); await page.locator('#modal-root .rcx-button--primary').click(); @@ -139,19 +130,19 @@ test.describe.serial('e2e-encryption', () => { await page.goto('/home'); }); - test.afterAll(async ({ api }) => { - const statusCode = (await api.post('/settings/E2E_Enable', { value: false })).status(); + test.beforeAll(async ({ api }) => { + expect((await api.post('/settings/E2E_Allow_Unencrypted_Messages', { value: true })).status()).toBe(200); + }); - await expect(statusCode).toBe(200); + test.afterAll(async ({ api }) => { + expect((await api.post('/settings/E2E_Enable', { value: false })).status()).toBe(200); + expect((await api.post('/settings/E2E_Allow_Unencrypted_Messages', { value: false })).status()).toBe(200); }); test('expect create a private channel encrypted and send an encrypted message', async ({ page }) => { const channelName = faker.string.uuid(); - await poHomeChannel.sidenav.openNewByLabel('Channel'); - await poHomeChannel.sidenav.inputChannelName.type(channelName); - await poHomeChannel.sidenav.checkboxEncryption.click(); - await poHomeChannel.sidenav.btnCreate.click(); + await poHomeChannel.sidenav.createEncryptedChannel(channelName); await expect(page).toHaveURL(`/group/${channelName}`); @@ -192,7 +183,7 @@ test.describe.serial('e2e-encryption', () => { const channelName = faker.string.uuid(); await poHomeChannel.sidenav.openNewByLabel('Channel'); - await poHomeChannel.sidenav.inputChannelName.type(channelName); + await poHomeChannel.sidenav.inputChannelName.fill(channelName); await poHomeChannel.sidenav.btnCreate.click(); await expect(page).toHaveURL(`/group/${channelName}`); @@ -217,10 +208,7 @@ test.describe.serial('e2e-encryption', () => { test('expect placeholder text in place of encrypted message, when E2EE is not setup', async ({ page }) => { const channelName = faker.string.uuid(); - await poHomeChannel.sidenav.openNewByLabel('Channel'); - await poHomeChannel.sidenav.inputChannelName.fill(channelName); - await poHomeChannel.sidenav.checkboxEncryption.click(); - await poHomeChannel.sidenav.btnCreate.click(); + await poHomeChannel.sidenav.createEncryptedChannel(channelName); await expect(page).toHaveURL(`/group/${channelName}`); @@ -285,3 +273,179 @@ test.describe.serial('e2e-encryption', () => { }); }); }); + +test.describe.serial('e2ee room setup', () => { + let poAccountProfile: AccountProfile; + let poHomeChannel: HomeChannel; + let e2eePassword: string; + + test.beforeEach(async ({ page }) => { + poAccountProfile = new AccountProfile(page); + poHomeChannel = new HomeChannel(page); + }); + + test.beforeAll(async ({ api }) => { + expect((await api.post('/settings/E2E_Enable', { value: true })).status()).toBe(200); + expect((await api.post('/settings/E2E_Allow_Unencrypted_Messages', { value: false })).status()).toBe(200); + }); + + test.afterAll(async ({ api }) => { + expect((await api.post('/settings/E2E_Enable', { value: false })).status()).toBe(200); + expect((await api.post('/settings/E2E_Allow_Unencrypted_Messages', { value: false })).status()).toBe(200); + }); + + test('expect save password state on encrypted room', async ({ page }) => { + await page.goto('/account/security'); + await poAccountProfile.securityE2EEncryptionSection.click(); + await poAccountProfile.securityE2EEncryptionResetKeyButton.click(); + + await page.locator('role=button[name="Login"]').waitFor(); + + await page.reload(); + + await page.locator('role=button[name="Login"]').waitFor(); + + await injectInitialData(); + await restoreState(page, Users.admin); + + await page.goto('/home'); + + await page.locator('role=banner >> text="Save your encryption password"').waitFor(); + await expect(page.locator('role=banner >> text="Save your encryption password"')).toBeVisible(); + + const channelName = faker.string.uuid(); + + await poHomeChannel.sidenav.openNewByLabel('Channel'); + await poHomeChannel.sidenav.inputChannelName.fill(channelName); + await poHomeChannel.sidenav.checkboxEncryption.click(); + await poHomeChannel.sidenav.btnCreate.click(); + + await expect(page).toHaveURL(`/group/${channelName}`); + + await poHomeChannel.dismissToast(); + + await expect(poHomeChannel.content.encryptedRoomHeaderIcon).toBeVisible(); + + await page.locator('role=button[name="Save E2EE password"]').waitFor(); + await expect(page.locator('role=button[name="Save E2EE password"]')).toBeVisible(); + + await expect(poHomeChannel.content.inputMessage).not.toBeVisible(); + + await page.locator('role=button[name="Save E2EE password"]').click(); + + e2eePassword = (await page.evaluate(() => localStorage.getItem('e2e.randomPassword'))) || 'undefined'; + + await expect(page.locator('role=dialog[name="Save your encryption password"]')).toBeVisible(); + await expect(page.locator('#modal-root')).toContainText(e2eePassword); + + await page.locator('#modal-root >> button:has-text("I saved my password")').click(); + + await poHomeChannel.content.inputMessage.waitFor(); + + await poHomeChannel.content.sendMessage('hello world'); + + await expect(poHomeChannel.content.lastUserMessageBody).toHaveText('hello world'); + await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).toBeVisible(); + }); + + test('expect enter password state on encrypted room', async ({ page }) => { + await page.goto('/home'); + + // Logout to remove e2ee keys + await poHomeChannel.sidenav.logout(); + + await page.locator('role=button[name="Login"]').waitFor(); + await page.reload(); + await page.locator('role=button[name="Login"]').waitFor(); + + await injectInitialData(); + await restoreState(page, Users.admin, { except: ['private_key', 'public_key'] }); + + const channelName = faker.string.uuid(); + + await poHomeChannel.sidenav.openNewByLabel('Channel'); + await poHomeChannel.sidenav.inputChannelName.fill(channelName); + await poHomeChannel.sidenav.checkboxEncryption.click(); + await poHomeChannel.sidenav.btnCreate.click(); + + await expect(page).toHaveURL(`/group/${channelName}`); + + await poHomeChannel.dismissToast(); + + await expect(poHomeChannel.content.encryptedRoomHeaderIcon).toBeVisible(); + + await page.locator('role=button[name="Enter your E2E password"]').waitFor(); + + await expect(page.locator('role=banner >> text="Enter your E2E password"')).toBeVisible(); + await expect(poHomeChannel.content.inputMessage).not.toBeVisible(); + + await page.locator('role=button[name="Enter your E2E password"]').click(); + + await page.locator('#modal-root input').fill(e2eePassword); + + await page.locator('#modal-root .rcx-button--primary').click(); + + await expect(page.locator('role=banner >> text="Enter your E2E password"')).not.toBeVisible(); + + await poHomeChannel.content.inputMessage.waitFor(); + // For E2EE to complete init setup + await page.waitForTimeout(300); + + await poHomeChannel.content.sendMessage('hello world'); + + await expect(poHomeChannel.content.lastUserMessageBody).toHaveText('hello world'); + await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).toBeVisible(); + + await storeState(page, Users.admin); + }); + + test('expect waiting for room keys state', async ({ page }) => { + await page.goto('/home'); + + const channelName = faker.string.uuid(); + + await poHomeChannel.sidenav.openNewByLabel('Channel'); + await poHomeChannel.sidenav.inputChannelName.fill(channelName); + await poHomeChannel.sidenav.checkboxEncryption.click(); + await poHomeChannel.sidenav.btnCreate.click(); + + await expect(page).toHaveURL(`/group/${channelName}`); + + await poHomeChannel.dismissToast(); + + await expect(poHomeChannel.content.encryptedRoomHeaderIcon).toBeVisible(); + + await poHomeChannel.content.sendMessage('hello world'); + + await expect(poHomeChannel.content.lastUserMessageBody).toHaveText('hello world'); + await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).toBeVisible(); + + await poHomeChannel.sidenav.userProfileMenu.click(); + await poHomeChannel.sidenav.accountProfileOption.click(); + + await page.locator('role=navigation >> a:has-text("Security")').click(); + + await poAccountProfile.securityE2EEncryptionSection.click(); + await poAccountProfile.securityE2EEncryptionResetKeyButton.click(); + + await page.locator('role=button[name="Login"]').waitFor(); + + await page.reload(); + + await page.locator('role=button[name="Login"]').waitFor(); + + await injectInitialData(); + await restoreState(page, Users.admin); + + await page.locator('role=navigation >> role=button[name=Search]').click(); + await page.locator('role=search >> role=searchbox').fill(channelName); + await page.locator(`role=search >> role=listbox >> role=link >> text="${channelName}"`).click(); + + await page.locator('role=button[name="Save E2EE password"]').click(); + await page.locator('#modal-root >> button:has-text("I saved my password")').click(); + + await expect(poHomeChannel.content.inputMessage).not.toBeVisible(); + await expect(page.locator('.rcx-states__title')).toContainText('Check back later'); + }); + +}); diff --git a/apps/meteor/tests/e2e/page-objects/fragments/home-sidenav.ts b/apps/meteor/tests/e2e/page-objects/fragments/home-sidenav.ts index 361db6af0455..1e7c70baff71 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/home-sidenav.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/home-sidenav.ts @@ -49,6 +49,10 @@ export class HomeSidenav { return this.page.getByRole('toolbar', { name: 'Sidebar actions' }); } + get accountProfileOption(): Locator { + return this.page.locator('role=menuitemcheckbox[name="Profile"]'); + } + getSidebarItemByName(name: string): Locator { return this.page.locator(`[data-qa="sidebar-item"][aria-label="${name}"]`); } @@ -140,4 +144,11 @@ export class HomeSidenav { await this.inputChannelName.type(name); await this.btnCreate.click(); } + + async createEncryptedChannel(name: string) { + await this.openNewByLabel('Channel'); + await this.inputChannelName.type(name); + await this.checkboxEncryption.click(); + await this.btnCreate.click(); + } } diff --git a/apps/meteor/tests/end-to-end/api/03-groups.js b/apps/meteor/tests/end-to-end/api/03-groups.js index 07b03494900f..68fcbc4c455a 100644 --- a/apps/meteor/tests/end-to-end/api/03-groups.js +++ b/apps/meteor/tests/end-to-end/api/03-groups.js @@ -122,6 +122,16 @@ describe('[Groups]', function () { }); describe('validate E2E rooms', () => { + before(async () => { + await Promise.all([updateSetting('E2E_Enable', true), updateSetting('E2E_Allow_Unencrypted_Messages', false)]); + }); + + after(async () => { + await Promise.all([updateSetting('E2E_Enable', false), updateSetting('E2E_Allow_Unencrypted_Messages', true)]); + }); + + let rid; + it('should create a new encrypted group', async () => { await request .post(api('groups.create')) @@ -140,6 +150,68 @@ describe('[Groups]', function () { expect(res.body).to.have.nested.property('group.t', 'p'); expect(res.body).to.have.nested.property('group.msgs', 0); expect(res.body).to.have.nested.property('group.encrypted', true); + rid = res.body.group._id; + }); + }); + + it('should send an encrypted message in encrypted group', async () => { + await request + .post(api('chat.sendMessage')) + .set(credentials) + .send({ + message: { + text: 'Encrypted Message', + t: 'e2e', + e2e: 'pending', + rid, + }, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('message'); + expect(res.body).to.have.nested.property('message.text', 'Encrypted Message'); + expect(res.body).to.have.nested.property('message.t', 'e2e'); + expect(res.body).to.have.nested.property('message.e2e', 'pending'); + }); + }); + + it('should give an error on sending un-encrypted message in encrypted room', async () => { + await request + .post(api('chat.sendMessage')) + .set(credentials) + .send({ + message: { + text: 'Unencrypted Message', + rid, + }, + }) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('error').that.is.a('string'); + }); + }); + + it('should allow sending un-encrypted messages in encrypted room when setting is enabled', async () => { + await updateSetting('E2E_Allow_Unencrypted_Messages', true); + await request + .post(api('chat.sendMessage')) + .set(credentials) + .send({ + message: { + text: 'Unencrypted Message', + rid, + }, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('message'); + expect(res.body).to.have.nested.property('message.text', 'Unencrypted Message'); }); }); }); diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index c59f3887106c..8e5c80ab1043 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -18,6 +18,8 @@ "__count__without__assignee__": "{{count}} without assignee", "__roomName__was_added_to_favorites": "{{roomName}} was added to favorites", "__roomName__was_removed_from_favorites": "{{roomName}} was removed from favorites", + "__roomName__is_encrypted": "{{roomName}} is encrypted", + "__roomName__encryption_keys_need_to_be_updated": "{{roomName}} encryption keys need to be updated to give you access. Another room member needs to be online for this to happen.", "removed__username__as__role_": "removed {{username}} as {{role}}", "set__username__as__role_": "set {{username}} as {{role}}", "sequential_message": "sequential message", @@ -767,6 +769,7 @@ "Back_to_applications": "Back to applications", "Back_to_calendar": "Back to calendar", "Back_to_chat": "Back to chat", + "Back_to_home": "Back to home", "Back_to_imports": "Back to imports", "Back_to_integration_detail": "Back to the integration detail", "Back_to_integrations": "Back to integrations", @@ -1003,6 +1006,7 @@ "Chat_Duration": "Chat Duration", "Chats_removed": "Chats Removed", "Check_All": "Check All", + "Check_back_later": "Check back later", "Check_if_the_spelling_is_correct": "Check if the spelling is correct", "Check_Progress": "Check Progress", "Check_device_activity": "Check device activity", @@ -1774,6 +1778,8 @@ "Duplicated_Email_address_will_be_ignored": "Duplicated email address will be ignored.", "Markdown_Marked_Tables": "Enable Marked Tables", "duplicated-account": "Duplicated account", + "E2E_Allow_Unencrypted_Messages": "Unencrypted messages in encrypted rooms", + "E2E_Allow_Unencrypted_Messages_Description": "Allow plain text messages to be sent in encrypted rooms. These messages will not be encrypted.", "E2E Encryption": "E2E Encryption", "E2E_Encryption_enabled_for_room": "End-to-end encryption enabled for #{{roomName}}", "E2E_Encryption_disabled_for_room": "End-to-end encryption disabled for #{{roomName}}", @@ -1942,7 +1948,8 @@ "End_suspicious_sessions": "End any suspicious sessions", "End_call": "End call", "End_conversation": "End conversation", - "End_To_End_Encryption_Set": "End-to-end encryption is set", + "End_To_End_Encryption_Enabled": "End-to-end encryption is enabled", + "End_To_End_Encryption_Not_Enabled": "End-to-end encryption is not enabled", "Expand_view": "Expand view", "Explore": "Explore", "Explore_marketplace": "Explore Marketplace", @@ -1974,6 +1981,7 @@ "Enter_to": "Enter to", "Enter_TOTP_password": "Enter TOTP password", "Enter_your_E2E_password": "Enter your E2E password", + "Enter_your_E2E_password_to_access": "Enter your end-to-end encryption password to access", "Enter_your_password_to_delete_your_account": "Enter your password to delete your account. This cannot be undone.", "Enter_your_username_to_delete_your_account": "Enter your username to delete your account. This cannot be undone.", "Premium_capabilities": "Premium capabilities", @@ -4672,10 +4680,12 @@ "Saturday": "Saturday", "Save": "Save", "Save_changes": "Save changes", + "Save_E2EE_password": "Save E2EE password", "Save_Mobile_Bandwidth": "Save Mobile Bandwidth", "Save_to_enable_this_action": "Save to enable this action", "Save_To_Webdav": "Save to WebDAV", "Save_your_encryption_password": "Save your encryption password", + "Save_your_encryption_password_to_access": "Save your end-to-end encryption password to access", "save-all-canned-responses": "Save All Canned Responses", "save-all-canned-responses_description": "Permission to save all canned responses", "save-canned-responses": "Save Canned Responses", @@ -6017,6 +6027,7 @@ "Your_new_email_is_email": "Your new email address is [email].", "Your_E2EE_password_is": "Your E2EE password is:", "Your_password_is_wrong": "Your password is wrong!", + "Your_E2EE_password_is_incorrect": "Your E2EE password is incorrect", "Your_password_was_changed_by_an_admin": "Your password was changed by an admin.", "Your_push_was_sent_to_s_devices": "Your push was sent to %s devices", "Your_request_to_join__roomName__has_been_made_it_could_take_up_to_15_minutes_to_be_processed": "Your request to join {{roomName}} has been made, it could take up to 15 minutes to be processed. You'll be notified when it's ready to go.",