From 9837150827a5c72927b6507add952fa13bc33e66 Mon Sep 17 00:00:00 2001 From: Tasso Evangelista Date: Wed, 10 Mar 2021 23:19:38 -0300 Subject: [PATCH 1/5] Remove missing console.log --- app/ui-message/client/messageBox/messageBox.js | 1 - 1 file changed, 1 deletion(-) diff --git a/app/ui-message/client/messageBox/messageBox.js b/app/ui-message/client/messageBox/messageBox.js index 46856eeecea1..cefe47cbb19c 100644 --- a/app/ui-message/client/messageBox/messageBox.js +++ b/app/ui-message/client/messageBox/messageBox.js @@ -120,7 +120,6 @@ Template.messageBox.onRendered(function() { } $input.on('dataChange', () => { const messages = $input.data('reply') || []; - console.log('dataChange', messages); this.replyMessageData.set(messages); }); } From fedbea12d05b7b2925698a6a6ccfac93d103c738 Mon Sep 17 00:00:00 2001 From: Tasso Evangelista Date: Thu, 11 Mar 2021 16:09:52 -0300 Subject: [PATCH 2/5] Remove dangling references for E2E_ROOM_STATES.PAUSED --- app/e2e/client/rocketchat.e2e.room.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app/e2e/client/rocketchat.e2e.room.js b/app/e2e/client/rocketchat.e2e.room.js index 6178b6ce5055..2b11e973bee6 100644 --- a/app/e2e/client/rocketchat.e2e.room.js +++ b/app/e2e/client/rocketchat.e2e.room.js @@ -28,7 +28,7 @@ import { call } from '../../ui-utils'; import { roomTypes, RoomSettingsEnum } from '../../utils'; import { getConfig } from '../../ui-utils/client/config'; -export const E2E_ROOM_STATES = { +const E2E_ROOM_STATES = { NO_PASSWORD_SET: 'NO_PASSWORD_SET', NOT_STARTED: 'NOT_STARTED', DISABLED: 'DISABLED', @@ -53,7 +53,7 @@ const reduce = (prev, next) => { case E2E_ROOM_STATES.NOT_STARTED: return [E2E_ROOM_STATES.ESTABLISHING, E2E_ROOM_STATES.DISABLED, E2E_ROOM_STATES.KEYS_RECEIVED].includes(next) && next; case E2E_ROOM_STATES.READY: - return [E2E_ROOM_STATES.PAUSED, E2E_ROOM_STATES.DISABLED].includes(next) && next; + return [E2E_ROOM_STATES.DISABLED].includes(next) && next; case E2E_ROOM_STATES.ERROR: return [E2E_ROOM_STATES.KEYS_RECEIVED, E2E_ROOM_STATES.NOT_STARTED].includes(next) && next; case E2E_ROOM_STATES.WAITING_KEYS: @@ -123,10 +123,12 @@ export class E2ERoom extends Emitter { } pause() { + console.log('pause'); this[PAUSED] = true; } unPause() { + console.log('unPause'); this[PAUSED] = false; } @@ -158,7 +160,7 @@ export class E2ERoom extends Emitter { } isReady() { - return [E2E_ROOM_STATES.PAUSED, E2E_ROOM_STATES.READY].includes(this.state); + return this.state === E2E_ROOM_STATES.READY; } isWaitingKeys() { From 4a4394deaac93eca89799e1fb81c3b119fb5e149 Mon Sep 17 00:00:00 2001 From: Tasso Evangelista Date: Thu, 11 Mar 2021 16:50:39 -0300 Subject: [PATCH 3/5] Move E2E client startup --- app/e2e/client/E2ERoomState.ts | 12 +++ app/e2e/client/logger.ts | 19 ++++ app/e2e/client/rocketchat.e2e.js | 136 ++------------------------ app/e2e/client/rocketchat.e2e.room.js | 83 +++++++--------- app/e2e/client/waitUntilFind.ts | 14 +++ client/startup/e2e.js | 113 +++++++++++++++++++++ client/startup/index.js | 1 + 7 files changed, 201 insertions(+), 177 deletions(-) create mode 100644 app/e2e/client/E2ERoomState.ts create mode 100644 app/e2e/client/logger.ts create mode 100644 app/e2e/client/waitUntilFind.ts create mode 100644 client/startup/e2e.js diff --git a/app/e2e/client/E2ERoomState.ts b/app/e2e/client/E2ERoomState.ts new file mode 100644 index 000000000000..5e060816436e --- /dev/null +++ b/app/e2e/client/E2ERoomState.ts @@ -0,0 +1,12 @@ +export enum E2ERoomState { + NO_PASSWORD_SET = 'NO_PASSWORD_SET', + NOT_STARTED = 'NOT_STARTED', + DISABLED = 'DISABLED', + HANDSHAKE = 'HANDSHAKE', + ESTABLISHING = 'ESTABLISHING', + CREATING_KEYS = 'CREATING_KEYS', + WAITING_KEYS = 'WAITING_KEYS', + KEYS_RECEIVED = 'KEYS_RECEIVED', + READY = 'READY', + ERROR = 'ERROR', +} diff --git a/app/e2e/client/logger.ts b/app/e2e/client/logger.ts new file mode 100644 index 000000000000..1efd15d83475 --- /dev/null +++ b/app/e2e/client/logger.ts @@ -0,0 +1,19 @@ +import { getConfig } from '../../ui-utils/client/config'; + +let debug: boolean | undefined = undefined; + +const isDebugEnabled = (): boolean => { + if (debug === undefined) { + debug = getConfig('debug') === 'true' || getConfig('debug-e2e') === 'true'; + } + + return debug; +}; + +export const log = (context: string, ...msg: unknown[]): void => { + isDebugEnabled() && console.log(`[${ context }]`, ...msg); +}; + +export const logError = (context: string, ...msg: unknown[]): void => { + isDebugEnabled() && console.error(`[${ context }]`, ...msg); +}; diff --git a/app/e2e/client/rocketchat.e2e.js b/app/e2e/client/rocketchat.e2e.js index 3be8728ade01..1bfad5665821 100644 --- a/app/e2e/client/rocketchat.e2e.js +++ b/app/e2e/client/rocketchat.e2e.js @@ -1,9 +1,7 @@ import { Meteor } from 'meteor/meteor'; import { Random } from 'meteor/random'; import { ReactiveVar } from 'meteor/reactive-var'; -import { Tracker } from 'meteor/tracker'; import { EJSON } from 'meteor/ejson'; -import { FlowRouter } from 'meteor/kadira:flow-router'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import { Emitter } from '@rocket.chat/emitter'; @@ -23,24 +21,14 @@ import { } from './helper'; import * as banners from '../../../client/lib/banners'; import { Rooms, Subscriptions, Messages } from '../../models'; -import { promises } from '../../promises/client'; -import { settings } from '../../settings'; -import { Notifications } from '../../notifications/client'; -import { Layout, call, modal } from '../../ui-utils'; +import { call, modal } from '../../ui-utils'; import './events.js'; import './tabbar'; -import { getConfig } from '../../ui-utils/client/config'; - -const debug = [getConfig('debug'), getConfig('debug-e2e')].includes('true'); +import { log, logError } from './logger'; +import { waitUntilFind } from './waitUntilFind'; let failedToDecodeKey = false; -const waitUntilFind = (fn) => new Promise((resolve) => { - Tracker.autorun((c) => { - const result = fn(); - return result && resolve(result) && c.stop(); - }); -}); class E2E extends Emitter { constructor() { super(); @@ -60,14 +48,13 @@ class E2E extends Emitter { } log(...msg) { - debug && console.log('[E2E]', ...msg); + log('E2E', ...msg); } error(...msg) { - debug && console.error('[E2E]', ...msg); + logError('E2E', ...msg); } - isEnabled() { return this.enabled.get(); } @@ -84,10 +71,8 @@ class E2E extends Emitter { delete this.instancesByRoomId[rid]; } - async getInstanceByRoomId(roomId) { - const room = await waitUntilFind(() => Rooms.findOne({ - _id: roomId, - })); + async getInstanceByRoomId(rid) { + const room = await waitUntilFind(() => Rooms.findOne({ _id: rid })); if (room.t !== 'd' && room.t !== 'p') { return; @@ -97,9 +82,9 @@ class E2E extends Emitter { return; } - this.instancesByRoomId[roomId] = this.instancesByRoomId[roomId] ?? new E2ERoom(Meteor.userId(), roomId, room.t); + this.instancesByRoomId[rid] = this.instancesByRoomId[rid] ?? new E2ERoom(Meteor.userId(), rid, room.t); - return this.instancesByRoomId[roomId]; + return this.instancesByRoomId[rid]; } async startClient() { @@ -423,106 +408,3 @@ class E2E extends Emitter { } export const e2e = new E2E(); - -const handle = async (roomId, keyId) => { - const e2eRoom = await e2e.getInstanceByRoomId(roomId); - if (!e2eRoom) { - return; - } - - e2eRoom.provideKeyToUser(keyId); -}; - -Meteor.startup(function() { - Tracker.autorun(function() { - if (Meteor.userId()) { - const adminEmbedded = Layout.isEmbedded() && FlowRouter.current().path.startsWith('/admin'); - - if (!adminEmbedded && settings.get('E2E_Enable') && window.crypto) { - e2e.startClient(); - e2e.enabled.set(true); - } else { - e2e.enabled.set(false); - e2e.closeAlert(); - } - } - }); - - let observable = null; - Tracker.autorun(() => { - if (!e2e.isReady()) { - promises.remove('onClientMessageReceived', 'e2e-decript-message'); - Notifications.unUser('e2ekeyRequest', handle); - observable?.stop(); - return promises.remove('onClientBeforeSendMessage', 'e2e'); - } - - - Notifications.onUser('e2ekeyRequest', handle); - - - observable = Subscriptions.find().observe({ - changed: async (doc) => { - if (!doc.encrypted && !doc.E2EKey) { - return e2e.removeInstanceByRoomId(doc.rid); - } - const e2eRoom = await e2e.getInstanceByRoomId(doc.rid); - - if (!e2eRoom) { - return; - } - - - doc.encrypted ? e2eRoom.unPause() : e2eRoom.pause(); - - // Cover private groups and direct messages - if (!e2eRoom.isSupportedRoomType(doc.t)) { - return e2eRoom.disable(); - } - - - if (doc.E2EKey && e2eRoom.isWaitingKeys()) { - return e2eRoom.keyReceived(); - } - if (!e2eRoom.isReady()) { - return; - } - e2eRoom.decryptSubscription(); - }, - added: async (doc) => { - if (!doc.encrypted && !doc.E2EKey) { - return; - } - return e2e.getInstanceByRoomId(doc.rid); - }, - removed: (doc) => { - e2e.removeInstanceByRoomId(doc.rid); - }, - }); - - promises.add('onClientMessageReceived', (msg) => { - const e2eRoom = e2e.getE2ERoom(msg.rid); - if (!e2eRoom || !e2eRoom.shouldConvertReceivedMessages()) { - return msg; - } - return e2e.decryptMessage(msg); - }, promises.priority.HIGH, 'e2e-decript-message'); - - // Encrypt messages before sending - promises.add('onClientBeforeSendMessage', async function(message) { - const e2eRoom = e2e.getE2ERoom(message.rid); - if (!e2eRoom || !e2eRoom.shouldConvertSentMessages()) { - return message; - } - // Should encrypt this message. - return e2eRoom - .encrypt(message) - .then((msg) => { - message.msg = msg; - message.t = 'e2e'; - message.e2e = 'pending'; - return message; - }); - }, promises.priority.HIGH, 'e2e'); - }); -}); diff --git a/app/e2e/client/rocketchat.e2e.room.js b/app/e2e/client/rocketchat.e2e.room.js index 2b11e973bee6..86a8e051beaa 100644 --- a/app/e2e/client/rocketchat.e2e.room.js +++ b/app/e2e/client/rocketchat.e2e.room.js @@ -26,57 +26,40 @@ import { Notifications } from '../../notifications/client'; import { Rooms, Subscriptions, Messages } from '../../models'; import { call } from '../../ui-utils'; import { roomTypes, RoomSettingsEnum } from '../../utils'; -import { getConfig } from '../../ui-utils/client/config'; - -const E2E_ROOM_STATES = { - NO_PASSWORD_SET: 'NO_PASSWORD_SET', - NOT_STARTED: 'NOT_STARTED', - DISABLED: 'DISABLED', - HANDSHAKE: 'HANDSHAKE', - ESTABLISHING: 'ESTABLISHING', - CREATING_KEYS: 'CREATING_KEYS', - WAITING_KEYS: 'WAITING_KEYS', - KEYS_RECEIVED: 'KEYS_RECEIVED', - READY: 'READY', - ERROR: 'ERROR', -}; +import { log, logError } from './logger'; +import { E2ERoomState } from './E2ERoomState'; const KEY_ID = Symbol('keyID'); const PAUSED = Symbol('PAUSED'); const reduce = (prev, next) => { if (prev === next) { - return next === E2E_ROOM_STATES.ERROR; + return next === E2ERoomState.ERROR; } switch (prev) { - case E2E_ROOM_STATES.NOT_STARTED: - return [E2E_ROOM_STATES.ESTABLISHING, E2E_ROOM_STATES.DISABLED, E2E_ROOM_STATES.KEYS_RECEIVED].includes(next) && next; - case E2E_ROOM_STATES.READY: - return [E2E_ROOM_STATES.DISABLED].includes(next) && next; - case E2E_ROOM_STATES.ERROR: - return [E2E_ROOM_STATES.KEYS_RECEIVED, E2E_ROOM_STATES.NOT_STARTED].includes(next) && next; - case E2E_ROOM_STATES.WAITING_KEYS: - return [E2E_ROOM_STATES.KEYS_RECEIVED, E2E_ROOM_STATES.ERROR, E2E_ROOM_STATES.DISABLED].includes(next) && next; - case E2E_ROOM_STATES.ESTABLISHING: - return [E2E_ROOM_STATES.READY, E2E_ROOM_STATES.KEYS_RECEIVED, E2E_ROOM_STATES.ERROR, E2E_ROOM_STATES.DISABLED, E2E_ROOM_STATES.WAITING_KEYS].includes(next) && next; + case E2ERoomState.NOT_STARTED: + return [E2ERoomState.ESTABLISHING, E2ERoomState.DISABLED, E2ERoomState.KEYS_RECEIVED].includes(next) && next; + case E2ERoomState.READY: + return [E2ERoomState.DISABLED].includes(next) && next; + case E2ERoomState.ERROR: + return [E2ERoomState.KEYS_RECEIVED, E2ERoomState.NOT_STARTED].includes(next) && next; + case E2ERoomState.WAITING_KEYS: + return [E2ERoomState.KEYS_RECEIVED, E2ERoomState.ERROR, E2ERoomState.DISABLED].includes(next) && next; + case E2ERoomState.ESTABLISHING: + return [E2ERoomState.READY, E2ERoomState.KEYS_RECEIVED, E2ERoomState.ERROR, E2ERoomState.DISABLED, E2ERoomState.WAITING_KEYS].includes(next) && next; default: return next; } }; -const debug = [getConfig('debug'), getConfig('debug-e2e')].includes('true'); export class E2ERoom extends Emitter { log(...msg) { - if (debug) { - console.log('[E2E ROOM]', `[STATE: ${ this.state }]`, `[RID: ${ this.roomId }]`, ...msg); - } + log('E2E ROOM', `[STATE: ${ this.state }]`, `[RID: ${ this.roomId }]`, ...msg); } error(...msg) { - if (debug) { - console.error('[E2E ROOM]', `[STATE: ${ this.state }]`, `[RID: ${ this.roomId }]`, ...msg); - } + logError('E2E ROOM', `[STATE: ${ this.state }]`, `[RID: ${ this.roomId }]`, ...msg); } setState(state) { @@ -102,24 +85,24 @@ export class E2ERoom extends Emitter { this.typeOfRoom = t; - this.once(E2E_ROOM_STATES.READY, () => this.decryptPendingMessages()); - this.once(E2E_ROOM_STATES.READY, () => this.decryptSubscription()); + this.once(E2ERoomState.READY, () => this.decryptPendingMessages()); + this.once(E2ERoomState.READY, () => this.decryptSubscription()); this.on('STATE_CHANGED', (prev) => { if (this.roomId === Session.get('openedRoom')) { this.log(`[PREV: ${ prev }]`, 'State CHANGED'); } }); this.on('STATE_CHANGED', () => this.handshake()); - this.setState(E2E_ROOM_STATES.NOT_STARTED); + this.setState(E2ERoomState.NOT_STARTED); } disable() { - this.setState(E2E_ROOM_STATES.DISABLED); + this.setState(E2ERoomState.DISABLED); } keyReceived() { - this.setState(E2E_ROOM_STATES.KEYS_RECEIVED); + this.setState(E2ERoomState.KEYS_RECEIVED); } pause() { @@ -137,7 +120,7 @@ export class E2ERoom extends Emitter { } enable() { - ![E2E_ROOM_STATES.READY].includes(this.state) && this.setState(E2E_ROOM_STATES.READY); + ![E2ERoomState.READY].includes(this.state) && this.setState(E2ERoomState.READY); } shouldConvertSentMessages() { @@ -149,7 +132,7 @@ export class E2ERoom extends Emitter { } isDisabled() { - return [E2E_ROOM_STATES.DISABLED].includes(this.state); + return [E2ERoomState.DISABLED].includes(this.state); } wait(state) { @@ -160,11 +143,11 @@ export class E2ERoom extends Emitter { } isReady() { - return this.state === E2E_ROOM_STATES.READY; + return this.state === E2ERoomState.READY; } isWaitingKeys() { - return this.state === E2E_ROOM_STATES.WAITING_KEYS; + return this.state === E2ERoomState.WAITING_KEYS; } get keyID() { @@ -206,17 +189,17 @@ export class E2ERoom extends Emitter { // Initiates E2E Encryption async handshake() { switch (this.state) { - case E2E_ROOM_STATES.KEYS_RECEIVED: - case E2E_ROOM_STATES.NOT_STARTED: - this.setState(E2E_ROOM_STATES.ESTABLISHING); + case E2ERoomState.KEYS_RECEIVED: + case E2ERoomState.NOT_STARTED: + this.setState(E2ERoomState.ESTABLISHING); try { const groupKey = Subscriptions.findOne({ rid: this.roomId }).E2EKey; if (groupKey) { await this.importGroupKey(groupKey); - return this.setState(E2E_ROOM_STATES.READY); + return this.setState(E2ERoomState.READY); } } catch (error) { - this.setState(E2E_ROOM_STATES.ERROR); + this.setState(E2ERoomState.ERROR); // this.error = error; return this.error('Error fetching group key: ', error); } @@ -224,16 +207,16 @@ export class E2ERoom extends Emitter { try { const room = Rooms.findOne({ _id: this.roomId }); if (!room.e2eKeyId) { // TODO CHECK_PERMISSION - this.setState(E2E_ROOM_STATES.CREATING_KEYS); + this.setState(E2ERoomState.CREATING_KEYS); await this.createGroupKey(); - return this.setState(E2E_ROOM_STATES.READY); + return this.setState(E2ERoomState.READY); } - this.setState(E2E_ROOM_STATES.WAITING_KEYS); + this.setState(E2ERoomState.WAITING_KEYS); this.log('Requesting room key'); Notifications.notifyUsersOfRoom(this.roomId, 'e2ekeyRequest', this.roomId, room.e2eKeyId); } catch (error) { // this.error = error; - this.setState(E2E_ROOM_STATES.ERROR); + this.setState(E2ERoomState.ERROR); } } } diff --git a/app/e2e/client/waitUntilFind.ts b/app/e2e/client/waitUntilFind.ts new file mode 100644 index 000000000000..bb07282ef165 --- /dev/null +++ b/app/e2e/client/waitUntilFind.ts @@ -0,0 +1,14 @@ +import { Tracker } from 'meteor/tracker'; + +export const waitUntilFind = (fn: () => T | undefined): Promise => new Promise((resolve) => { + Tracker.autorun((c) => { + const result = fn(); + + if (result === undefined) { + return; + } + + c.stop(); + resolve(result); + }); +}); diff --git a/client/startup/e2e.js b/client/startup/e2e.js new file mode 100644 index 000000000000..968aaf5f76c3 --- /dev/null +++ b/client/startup/e2e.js @@ -0,0 +1,113 @@ +import { FlowRouter } from 'meteor/kadira:flow-router'; +import { Meteor } from 'meteor/meteor'; +import { Tracker } from 'meteor/tracker'; + +import { Layout } from '../../app/ui-utils/client'; +import { settings } from '../../app/settings/client'; +import { promises } from '../../app/promises/client'; +import { Notifications } from '../../app/notifications/client'; +import { e2e } from '../../app/e2e/client/rocketchat.e2e'; +import { Subscriptions } from '../../app/models'; + +const handle = async (roomId, keyId) => { + const e2eRoom = await e2e.getInstanceByRoomId(roomId); + if (!e2eRoom) { + return; + } + + e2eRoom.provideKeyToUser(keyId); +}; + +Meteor.startup(function() { + Tracker.autorun(function() { + if (Meteor.userId()) { + const adminEmbedded = Layout.isEmbedded() && FlowRouter.current().path.startsWith('/admin'); + + if (!adminEmbedded && settings.get('E2E_Enable') && window.crypto) { + e2e.startClient(); + e2e.enabled.set(true); + } else { + e2e.enabled.set(false); + e2e.closeAlert(); + } + } + }); + + let observable = null; + Tracker.autorun(() => { + if (!e2e.isReady()) { + promises.remove('onClientMessageReceived', 'e2e-decript-message'); + Notifications.unUser('e2ekeyRequest', handle); + observable?.stop(); + return promises.remove('onClientBeforeSendMessage', 'e2e'); + } + + + Notifications.onUser('e2ekeyRequest', handle); + + + observable = Subscriptions.find().observe({ + changed: async (doc) => { + if (!doc.encrypted && !doc.E2EKey) { + return e2e.removeInstanceByRoomId(doc.rid); + } + const e2eRoom = await e2e.getInstanceByRoomId(doc.rid); + + if (!e2eRoom) { + return; + } + + + doc.encrypted ? e2eRoom.unPause() : e2eRoom.pause(); + + // Cover private groups and direct messages + if (!e2eRoom.isSupportedRoomType(doc.t)) { + return e2eRoom.disable(); + } + + + if (doc.E2EKey && e2eRoom.isWaitingKeys()) { + return e2eRoom.keyReceived(); + } + if (!e2eRoom.isReady()) { + return; + } + e2eRoom.decryptSubscription(); + }, + added: async (doc) => { + if (!doc.encrypted && !doc.E2EKey) { + return; + } + return e2e.getInstanceByRoomId(doc.rid); + }, + removed: (doc) => { + e2e.removeInstanceByRoomId(doc.rid); + }, + }); + + promises.add('onClientMessageReceived', (msg) => { + const e2eRoom = e2e.getE2ERoom(msg.rid); + if (!e2eRoom || !e2eRoom.shouldConvertReceivedMessages()) { + return msg; + } + return e2e.decryptMessage(msg); + }, promises.priority.HIGH, 'e2e-decript-message'); + + // Encrypt messages before sending + promises.add('onClientBeforeSendMessage', async function(message) { + const e2eRoom = e2e.getE2ERoom(message.rid); + if (!e2eRoom || !e2eRoom.shouldConvertSentMessages()) { + return message; + } + // Should encrypt this message. + return e2eRoom + .encrypt(message) + .then((msg) => { + message.msg = msg; + message.t = 'e2e'; + message.e2e = 'pending'; + return message; + }); + }, promises.priority.HIGH, 'e2e'); + }); +}); diff --git a/client/startup/index.js b/client/startup/index.js index 8fa4bb0160e4..5b33c654ab61 100644 --- a/client/startup/index.js +++ b/client/startup/index.js @@ -12,3 +12,4 @@ import './theme'; import './unread'; import './userSetUtcOffset'; import './usersObserve'; +import './e2e'; From e44a13be02484c4ac1d7fce2ec02816bcd9c74b1 Mon Sep 17 00:00:00 2001 From: Tasso Evangelista Date: Thu, 11 Mar 2021 17:18:16 -0300 Subject: [PATCH 4/5] Remove synchronous method --- app/e2e/client/rocketchat.e2e.js | 22 ++++++++++------------ app/e2e/client/rocketchat.e2e.room.js | 4 ++-- client/startup/e2e.js | 21 ++++++++++----------- 3 files changed, 22 insertions(+), 25 deletions(-) diff --git a/app/e2e/client/rocketchat.e2e.js b/app/e2e/client/rocketchat.e2e.js index 1bfad5665821..a1d19ee45947 100644 --- a/app/e2e/client/rocketchat.e2e.js +++ b/app/e2e/client/rocketchat.e2e.js @@ -63,30 +63,28 @@ class E2E extends Emitter { return this.enabled.get() && this._ready.get(); } - getE2ERoom(rid) { - return this.instancesByRoomId[rid]; - } - - removeInstanceByRoomId(rid) { - delete this.instancesByRoomId[rid]; - } - async getInstanceByRoomId(rid) { const room = await waitUntilFind(() => Rooms.findOne({ _id: rid })); if (room.t !== 'd' && room.t !== 'p') { - return; + return null; } if (room.encrypted !== true && !room.e2eKeyId) { - return; + return null; } - this.instancesByRoomId[rid] = this.instancesByRoomId[rid] ?? new E2ERoom(Meteor.userId(), rid, room.t); + if (!this.instancesByRoomId[rid]) { + this.instancesByRoomId[rid] = new E2ERoom(Meteor.userId(), rid, room.t); + } return this.instancesByRoomId[rid]; } + removeInstanceByRoomId(rid) { + delete this.instancesByRoomId[rid]; + } + async startClient() { if (this.started) { return; @@ -361,7 +359,7 @@ class E2E extends Emitter { return message; } - const e2eRoom = this.getE2ERoom(message.rid); + const e2eRoom = await this.getInstanceByRoomId(message.rid); if (!e2eRoom) { return message; diff --git a/app/e2e/client/rocketchat.e2e.room.js b/app/e2e/client/rocketchat.e2e.room.js index 86a8e051beaa..538f763c6f9c 100644 --- a/app/e2e/client/rocketchat.e2e.room.js +++ b/app/e2e/client/rocketchat.e2e.room.js @@ -70,9 +70,9 @@ export class E2ERoom extends Emitter { this.error(`invalid state ${ prev } -> ${ state }`); return; } - this.state = state; + this.state = next; this.emit('STATE_CHANGED', prev, next, this); - this.emit(state, this); + this.emit(next, this); } constructor(userId, roomId, t) { diff --git a/client/startup/e2e.js b/client/startup/e2e.js index 968aaf5f76c3..18337091ff3f 100644 --- a/client/startup/e2e.js +++ b/client/startup/e2e.js @@ -85,8 +85,8 @@ Meteor.startup(function() { }, }); - promises.add('onClientMessageReceived', (msg) => { - const e2eRoom = e2e.getE2ERoom(msg.rid); + promises.add('onClientMessageReceived', async (msg) => { + const e2eRoom = await e2e.getInstanceByRoomId(msg.rid); if (!e2eRoom || !e2eRoom.shouldConvertReceivedMessages()) { return msg; } @@ -95,19 +95,18 @@ Meteor.startup(function() { // Encrypt messages before sending promises.add('onClientBeforeSendMessage', async function(message) { - const e2eRoom = e2e.getE2ERoom(message.rid); + const e2eRoom = await e2e.getInstanceByRoomId(message.rid); if (!e2eRoom || !e2eRoom.shouldConvertSentMessages()) { return message; } + // Should encrypt this message. - return e2eRoom - .encrypt(message) - .then((msg) => { - message.msg = msg; - message.t = 'e2e'; - message.e2e = 'pending'; - return message; - }); + const msg = await e2eRoom.encrypt(message); + + message.msg = msg; + message.t = 'e2e'; + message.e2e = 'pending'; + return message; }, promises.priority.HIGH, 'e2e'); }); }); From baaf5a6f8ddf51d4c9ddf58cd6e54a0e7034f8e8 Mon Sep 17 00:00:00 2001 From: Tasso Evangelista Date: Fri, 12 Mar 2021 10:44:17 -0300 Subject: [PATCH 5/5] Pause or resume E2E before sending message --- app/e2e/client/rocketchat.e2e.room.js | 228 +++++++++++++++----------- client/startup/e2e.js | 28 ++-- 2 files changed, 148 insertions(+), 108 deletions(-) diff --git a/app/e2e/client/rocketchat.e2e.room.js b/app/e2e/client/rocketchat.e2e.room.js index 538f763c6f9c..bfc10c8c8619 100644 --- a/app/e2e/client/rocketchat.e2e.room.js +++ b/app/e2e/client/rocketchat.e2e.room.js @@ -32,57 +32,59 @@ import { E2ERoomState } from './E2ERoomState'; const KEY_ID = Symbol('keyID'); const PAUSED = Symbol('PAUSED'); -const reduce = (prev, next) => { - if (prev === next) { - return next === E2ERoomState.ERROR; - } - - switch (prev) { - case E2ERoomState.NOT_STARTED: - return [E2ERoomState.ESTABLISHING, E2ERoomState.DISABLED, E2ERoomState.KEYS_RECEIVED].includes(next) && next; - case E2ERoomState.READY: - return [E2ERoomState.DISABLED].includes(next) && next; - case E2ERoomState.ERROR: - return [E2ERoomState.KEYS_RECEIVED, E2ERoomState.NOT_STARTED].includes(next) && next; - case E2ERoomState.WAITING_KEYS: - return [E2ERoomState.KEYS_RECEIVED, E2ERoomState.ERROR, E2ERoomState.DISABLED].includes(next) && next; - case E2ERoomState.ESTABLISHING: - return [E2ERoomState.READY, E2ERoomState.KEYS_RECEIVED, E2ERoomState.ERROR, E2ERoomState.DISABLED, E2ERoomState.WAITING_KEYS].includes(next) && next; - default: - return next; - } +const permitedMutations = { + [E2ERoomState.NOT_STARTED]: [ + E2ERoomState.ESTABLISHING, + E2ERoomState.DISABLED, + E2ERoomState.KEYS_RECEIVED, + ], + [E2ERoomState.READY]: [ + E2ERoomState.DISABLED, + ], + [E2ERoomState.ERROR]: [ + E2ERoomState.KEYS_RECEIVED, + E2ERoomState.NOT_STARTED, + ], + [E2ERoomState.WAITING_KEYS]: [ + E2ERoomState.KEYS_RECEIVED, + E2ERoomState.ERROR, + E2ERoomState.DISABLED, + ], + [E2ERoomState.ESTABLISHING]: [ + E2ERoomState.READY, + E2ERoomState.KEYS_RECEIVED, + E2ERoomState.ERROR, + E2ERoomState.DISABLED, + E2ERoomState.WAITING_KEYS, + ], }; -export class E2ERoom extends Emitter { - log(...msg) { - log('E2E ROOM', `[STATE: ${ this.state }]`, `[RID: ${ this.roomId }]`, ...msg); +const filterMutation = (currentState, nextState) => { + if (currentState === nextState) { + return nextState === E2ERoomState.ERROR; } - error(...msg) { - logError('E2E ROOM', `[STATE: ${ this.state }]`, `[RID: ${ this.roomId }]`, ...msg); + if (!(currentState in permitedMutations)) { + return nextState; } - setState(state) { - const prev = this.state; - - const next = reduce(prev, state); - if (!next) { - this.error(`invalid state ${ prev } -> ${ state }`); - return; - } - this.state = next; - this.emit('STATE_CHANGED', prev, next, this); - this.emit(next, this); + if (permitedMutations[currentState].includes(nextState)) { + return nextState; } + return false; +}; + +export class E2ERoom extends Emitter { + state = undefined; + + [PAUSED] = undefined; + constructor(userId, roomId, t) { super(); - this.state = undefined; - // this.error = undefined; this.userId = userId; this.roomId = roomId; - this.typeOfRoom = t; this.once(E2ERoomState.READY, () => this.decryptPendingMessages()); @@ -93,57 +95,85 @@ export class E2ERoom extends Emitter { } }); this.on('STATE_CHANGED', () => this.handshake()); + this.setState(E2ERoomState.NOT_STARTED); } - - disable() { - this.setState(E2ERoomState.DISABLED); + log(...msg) { + log(`E2E ROOM { state: ${ this.state }, rid: ${ this.roomId } }`, ...msg); } - keyReceived() { - this.setState(E2ERoomState.KEYS_RECEIVED); + error(...msg) { + logError(`E2E ROOM { state: ${ this.state }, rid: ${ this.roomId } }`, ...msg); } - pause() { - console.log('pause'); - this[PAUSED] = true; + setState(requestedState) { + const currentState = this.state; + const nextState = filterMutation(currentState, requestedState); + + if (!nextState) { + this.error(`invalid state ${ currentState } -> ${ requestedState }`); + return; + } + + this.state = nextState; + this.log(currentState, '->', nextState); + this.emit('STATE_CHANGED', currentState, nextState, this); + this.emit(nextState, this); } - unPause() { - console.log('unPause'); - this[PAUSED] = false; + isReady() { + return this.state === E2ERoomState.READY; } - isPaused() { - return this[PAUSED]; + isDisabled() { + return this.state === E2ERoomState.DISABLED; } enable() { - ![E2ERoomState.READY].includes(this.state) && this.setState(E2ERoomState.READY); + if (this.state === E2ERoomState.READY) { + return; + } + + this.setState(E2ERoomState.READY); } - shouldConvertSentMessages() { - return this.isReady() && ! this.isPaused(); + disable() { + this.setState(E2ERoomState.DISABLED); } - shouldConvertReceivedMessages() { - return this.isReady(); + pause() { + this.log('PAUSED', this[PAUSED], '->', true); + this[PAUSED] = true; + this.emit('PAUSED', true); } - isDisabled() { - return [E2ERoomState.DISABLED].includes(this.state); + resume() { + this.log('PAUSED', this[PAUSED], '->', false); + this[PAUSED] = false; + this.emit('PAUSED', false); } - wait(state) { - return new Promise((resolve) => (state === this.state ? resolve(this) : this.once(state, () => resolve(this)))).then((el) => { - this.log(this.state, el); - return el; - }); + keyReceived() { + this.setState(E2ERoomState.KEYS_RECEIVED); } - isReady() { - return this.state === E2ERoomState.READY; + async shouldConvertSentMessages() { + if (!this.isReady() || this[PAUSED]) { + return false; + } + + if (this[PAUSED] === undefined) { + return new Promise((resolve) => { + this.once('PAUSED', resolve); + }); + } + + return true; + } + + shouldConvertReceivedMessages() { + return this.isReady(); } isWaitingKeys() { @@ -159,9 +189,7 @@ export class E2ERoom extends Emitter { } async decryptSubscription() { - const subscription = Subscriptions.findOne({ - rid: this.roomId, - }); + const subscription = Subscriptions.findOne({ rid: this.roomId }); const data = await (subscription.lastMessage?.msg && this.decrypt(subscription.lastMessage.msg)); if (!data?.text) { @@ -188,36 +216,40 @@ export class E2ERoom extends Emitter { // Initiates E2E Encryption async handshake() { - switch (this.state) { - case E2ERoomState.KEYS_RECEIVED: - case E2ERoomState.NOT_STARTED: - this.setState(E2ERoomState.ESTABLISHING); - try { - const groupKey = Subscriptions.findOne({ rid: this.roomId }).E2EKey; - if (groupKey) { - await this.importGroupKey(groupKey); - return this.setState(E2ERoomState.READY); - } - } catch (error) { - this.setState(E2ERoomState.ERROR); - // this.error = error; - return this.error('Error fetching group key: ', error); - } - - try { - const room = Rooms.findOne({ _id: this.roomId }); - if (!room.e2eKeyId) { // TODO CHECK_PERMISSION - this.setState(E2ERoomState.CREATING_KEYS); - await this.createGroupKey(); - return this.setState(E2ERoomState.READY); - } - this.setState(E2ERoomState.WAITING_KEYS); - this.log('Requesting room key'); - Notifications.notifyUsersOfRoom(this.roomId, 'e2ekeyRequest', this.roomId, room.e2eKeyId); - } catch (error) { - // this.error = error; - this.setState(E2ERoomState.ERROR); - } + if (this.state !== E2ERoomState.KEYS_RECEIVED && this.state !== E2ERoomState.NOT_STARTED) { + return; + } + + this.setState(E2ERoomState.ESTABLISHING); + + try { + const groupKey = Subscriptions.findOne({ rid: this.roomId }).E2EKey; + if (groupKey) { + await this.importGroupKey(groupKey); + this.setState(E2ERoomState.READY); + return; + } + } catch (error) { + this.setState(E2ERoomState.ERROR); + this.error('Error fetching group key: ', error); + return; + } + + try { + const room = Rooms.findOne({ _id: this.roomId }); + if (!room.e2eKeyId) { // TODO CHECK_PERMISSION + this.setState(E2ERoomState.CREATING_KEYS); + await this.createGroupKey(); + this.setState(E2ERoomState.READY); + return; + } + + this.setState(E2ERoomState.WAITING_KEYS); + this.log('Requesting room key'); + Notifications.notifyUsersOfRoom(this.roomId, 'e2ekeyRequest', this.roomId, room.e2eKeyId); + } catch (error) { + // this.error = error; + this.setState(E2ERoomState.ERROR); } } diff --git a/client/startup/e2e.js b/client/startup/e2e.js index 18337091ff3f..4f00d7ff09c8 100644 --- a/client/startup/e2e.js +++ b/client/startup/e2e.js @@ -8,6 +8,7 @@ import { promises } from '../../app/promises/client'; import { Notifications } from '../../app/notifications/client'; import { e2e } from '../../app/e2e/client/rocketchat.e2e'; import { Subscriptions } from '../../app/models'; +import { waitUntilFind } from '../../app/e2e/client/waitUntilFind'; const handle = async (roomId, keyId) => { const e2eRoom = await e2e.getInstanceByRoomId(roomId); @@ -45,33 +46,35 @@ Meteor.startup(function() { Notifications.onUser('e2ekeyRequest', handle); - observable = Subscriptions.find().observe({ changed: async (doc) => { if (!doc.encrypted && !doc.E2EKey) { - return e2e.removeInstanceByRoomId(doc.rid); + e2e.removeInstanceByRoomId(doc.rid); + return; } - const e2eRoom = await e2e.getInstanceByRoomId(doc.rid); + const e2eRoom = await e2e.getInstanceByRoomId(doc.rid); if (!e2eRoom) { return; } - - doc.encrypted ? e2eRoom.unPause() : e2eRoom.pause(); - // Cover private groups and direct messages if (!e2eRoom.isSupportedRoomType(doc.t)) { - return e2eRoom.disable(); + e2eRoom.disable(); + return; } - if (doc.E2EKey && e2eRoom.isWaitingKeys()) { - return e2eRoom.keyReceived(); + e2eRoom.keyReceived(); + return; } + if (!e2eRoom.isReady()) { return; } + + doc.encrypted ? e2eRoom.resume() : e2eRoom.pause(); + e2eRoom.decryptSubscription(); }, added: async (doc) => { @@ -96,7 +99,12 @@ Meteor.startup(function() { // Encrypt messages before sending promises.add('onClientBeforeSendMessage', async function(message) { const e2eRoom = await e2e.getInstanceByRoomId(message.rid); - if (!e2eRoom || !e2eRoom.shouldConvertSentMessages()) { + + const subscription = await waitUntilFind(() => Subscriptions.findOne({ rid: message.rid })); + + subscription.encrypted ? e2eRoom.resume() : e2eRoom.pause(); + + if (!e2eRoom || !await e2eRoom.shouldConvertSentMessages()) { return message; }