From 28c1e7269ce873ccc49d71d70e15d9fe239694de Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Mon, 30 Sep 2024 14:58:48 -0600 Subject: [PATCH] Reset room key full cycle --- .../app/e2e/client/rocketchat.e2e.room.js | 29 ++++++- apps/meteor/app/e2e/client/rocketchat.e2e.ts | 6 ++ .../app/e2e/server/functions/resetRoomKey.ts | 22 ++--- .../roomActions/useE2EERoomKeyResetAction.ts | 80 +++++++++++++++++++ apps/meteor/client/ui.ts | 9 ++- 5 files changed, 133 insertions(+), 13 deletions(-) create mode 100644 apps/meteor/client/hooks/roomActions/useE2EERoomKeyResetAction.ts diff --git a/apps/meteor/app/e2e/client/rocketchat.e2e.room.js b/apps/meteor/app/e2e/client/rocketchat.e2e.room.js index f0e78429c033..e41f8ad0edc5 100644 --- a/apps/meteor/app/e2e/client/rocketchat.e2e.room.js +++ b/apps/meteor/app/e2e/client/rocketchat.e2e.room.js @@ -33,7 +33,7 @@ const PAUSED = Symbol('PAUSED'); const permitedMutations = { [E2ERoomState.NOT_STARTED]: [E2ERoomState.ESTABLISHING, E2ERoomState.DISABLED, E2ERoomState.KEYS_RECEIVED], - [E2ERoomState.READY]: [E2ERoomState.DISABLED], + [E2ERoomState.READY]: [E2ERoomState.DISABLED, E2ERoomState.CREATING_KEYS], [E2ERoomState.ERROR]: [E2ERoomState.KEYS_RECEIVED, E2ERoomState.NOT_STARTED], [E2ERoomState.WAITING_KEYS]: [E2ERoomState.KEYS_RECEIVED, E2ERoomState.ERROR, E2ERoomState.DISABLED], [E2ERoomState.ESTABLISHING]: [ @@ -381,6 +381,33 @@ export class E2ERoom extends Emitter { } } + async resetRoomKey() { + this.log('Resetting room key'); + this.setState(E2ERoomState.CREATING_KEYS); + try { + this.groupSessionKey = await generateAESKey(); + } catch (error) { + console.error('Error generating group key: ', error); + throw error; + } + + try { + const sessionKeyExported = await exportJWKKey(this.groupSessionKey); + this.sessionKeyExportedString = JSON.stringify(sessionKeyExported); + this.keyID = Base64.encode(this.sessionKeyExportedString).slice(0, 12); + + const e2eNewKeys = { e2eKeyId: this.keyID, e2eKey: await this.encryptGroupKeyForParticipant(e2e.publicKey) }; + + this.log('Resetting room key ->', this.roomId); + this.setState(E2ERoomState.READY); + + return e2eNewKeys; + } catch (error) { + this.error('Error resetting group key: ', error); + throw error; + } + } + async encryptKeyForOtherParticipants() { // Encrypt generated session key for every user in room and publish to subscription model. try { diff --git a/apps/meteor/app/e2e/client/rocketchat.e2e.ts b/apps/meteor/app/e2e/client/rocketchat.e2e.ts index 944754f46260..ab8ff85741cc 100644 --- a/apps/meteor/app/e2e/client/rocketchat.e2e.ts +++ b/apps/meteor/app/e2e/client/rocketchat.e2e.ts @@ -67,6 +67,8 @@ class E2E extends Emitter { public privateKey: CryptoKey | undefined; + public publicKey: string | undefined; + private keyDistributionInterval: ReturnType | null; private state: E2EEState; @@ -418,6 +420,7 @@ class E2E extends Emitter { Accounts.storageLocation.removeItem('private_key'); this.instancesByRoomId = {}; this.privateKey = undefined; + this.publicKey = undefined; this.started = false; this.keyDistributionInterval && clearInterval(this.keyDistributionInterval); this.keyDistributionInterval = null; @@ -459,6 +462,8 @@ class E2E extends Emitter { this.setState(E2EEState.ERROR); return this.error('Error importing private key: ', error); } + + this.publicKey = public_key; } async createAndLoadKeys(): Promise { @@ -476,6 +481,7 @@ class E2E extends Emitter { try { const publicKey = await exportJWKKey(key.publicKey); + this.publicKey = JSON.stringify(publicKey); Accounts.storageLocation.setItem('public_key', JSON.stringify(publicKey)); } catch (error) { this.setState(E2EEState.ERROR); diff --git a/apps/meteor/app/e2e/server/functions/resetRoomKey.ts b/apps/meteor/app/e2e/server/functions/resetRoomKey.ts index cc94ee037fb9..259c3df25b1d 100644 --- a/apps/meteor/app/e2e/server/functions/resetRoomKey.ts +++ b/apps/meteor/app/e2e/server/functions/resetRoomKey.ts @@ -34,12 +34,7 @@ export async function resetRoomKey(roomId: string, userId: string, newRoomKey: s const keys = replicateMongoSlice(room.e2eKeyId, sub); delete sub.E2ESuggestedKey; delete sub.E2EKey; - - // If you're requesting the reset but you don't have the key, that means you won't have a complete "oldRoomKeys" - // So we'll put you on the list to get one - if (sub.u._id === userId && !sub.E2EKey) { - e2eQueue.push({ userId: sub.u._id, ts: new Date() }); - } + delete sub.suggestedOldRoomKeys; const updateSet = { $set: { @@ -50,20 +45,23 @@ export async function resetRoomKey(roomId: string, userId: string, newRoomKey: s updateOne: { filter: { _id: sub._id }, update: { - $unset: { E2EKey: 1, E2ESuggestedKey: 1 }, + $unset: { E2EKey: 1, E2ESuggestedKey: 1, suggestedOldRoomKeys: 1 }, ...(Object.keys(updateSet.$set).length && updateSet), }, }, }); - // Avoid notifying requesting user as notify will happen at the end - userId !== sub.u._id && + + if (userId !== sub.u._id) { + // Avoid notifying requesting user as notify will happen at the end notifySubs.push({ ...sub, ...(keys && { oldRoomKeys: keys }), }); - // This is for allowing the key distribution process to start inmediately - pushToLimit(e2eQueue, { userId: sub.u._id, ts: new Date() }); + // This is for allowing the key distribution process to start inmediately + pushToLimit(e2eQueue, { userId: sub.u._id, ts: new Date() }); + } + if (updateOps.length >= 100) { await writeAndNotify(updateOps, notifySubs); @@ -83,7 +81,9 @@ export async function resetRoomKey(roomId: string, userId: string, newRoomKey: s // And set the new key to the user that called the func const result = await Subscriptions.setE2EKeyByUserIdAndRoomId(userId, roomId, newRoomKey); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion void notifyOnSubscriptionChanged(result.value!); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion void notifyOnRoomChanged(roomResult.value!); } diff --git a/apps/meteor/client/hooks/roomActions/useE2EERoomKeyResetAction.ts b/apps/meteor/client/hooks/roomActions/useE2EERoomKeyResetAction.ts new file mode 100644 index 000000000000..24c027b3c0ec --- /dev/null +++ b/apps/meteor/client/hooks/roomActions/useE2EERoomKeyResetAction.ts @@ -0,0 +1,80 @@ +import { isRoomFederated } from '@rocket.chat/core-typings'; +import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; +import { useSetting, usePermission, useEndpoint } from '@rocket.chat/ui-contexts'; +import { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { e2e } from '../../../app/e2e/client'; +import { E2EEState } from '../../../app/e2e/client/E2EEState'; +import { OtrRoomState } from '../../../app/otr/lib/OtrRoomState'; +import { dispatchToastMessage } from '../../lib/toast'; +import { useRoom } from '../../views/room/contexts/RoomContext'; +import type { RoomToolboxActionConfig } from '../../views/room/contexts/RoomToolboxContext'; +import { useE2EEState } from '../../views/room/hooks/useE2EEState'; +import { useOTR } from '../useOTR'; + +// Temporal hook for testing whole flow +export const useE2EEResetRoomKeyRoomAction = () => { + const enabled = useSetting('E2E_Enable', false); + const room = useRoom(); + const e2eeState = useE2EEState(); + const isE2EEReady = e2eeState === E2EEState.READY || e2eeState === E2EEState.SAVE_PASSWORD; + const readyToEncrypt = isE2EEReady || room.encrypted; + const permittedToEditRoom = usePermission('edit-room', room._id); + const permitted = (room.t === 'd' || permittedToEditRoom) && readyToEncrypt; + const federated = isRoomFederated(room); + const { t } = useTranslation(); + const { otrState } = useOTR(); + const resetRoomKey = useEndpoint('POST', '/v1/e2e.resetRoomKey'); + + const action = useEffectEvent(async () => { + if (otrState === OtrRoomState.ESTABLISHED || otrState === OtrRoomState.ESTABLISHING || otrState === OtrRoomState.REQUESTED) { + dispatchToastMessage({ type: 'error', message: t('E2EE_not_available_OTR') }); + + return; + } + + const e2eRoom = await e2e.getInstanceByRoomId(room._id); + + if (!e2eRoom) { + return; + } + + const { e2eKey, e2eKeyId } = await e2eRoom.resetRoomKey(); + + if (!e2eKey) { + throw new Error('cannot reset room key'); + } + + try { + await resetRoomKey({ rid: room._id, e2eKeyId, e2eKey }); + + dispatchToastMessage({ + type: 'success', + message: 'Room Key reset successfully', + }); + } catch (error) { + dispatchToastMessage({ type: 'error', message: error }); + } + }); + + return useMemo((): RoomToolboxActionConfig | undefined => { + if (!enabled || !permitted) { + return undefined; + } + + return { + id: 'e2e-reset', + groups: ['direct', 'direct_multiple', 'group', 'team'], + title: 'E2E_Key_Reset', + icon: 'key', + order: 14, + action, + type: 'organization', + ...(federated && { + tooltip: t('core.E2E_unavailable_for_federation'), + disabled: true, + }), + }; + }, [enabled, permitted, federated, t, action]); +}; diff --git a/apps/meteor/client/ui.ts b/apps/meteor/client/ui.ts index 6c7971a8cca0..85740addfa84 100644 --- a/apps/meteor/client/ui.ts +++ b/apps/meteor/client/ui.ts @@ -12,6 +12,7 @@ import { useContactChatHistoryRoomAction } from './hooks/roomActions/useContactC import { useContactProfileRoomAction } from './hooks/roomActions/useContactProfileRoomAction'; import { useDiscussionsRoomAction } from './hooks/roomActions/useDiscussionsRoomAction'; import { useE2EERoomAction } from './hooks/roomActions/useE2EERoomAction'; +import { useE2EEResetRoomKeyRoomAction } from './hooks/roomActions/useE2EERoomKeyResetAction'; import { useExportMessagesRoomAction } from './hooks/roomActions/useExportMessagesRoomAction'; import { useGameCenterRoomAction } from './hooks/roomActions/useGameCenterRoomAction'; import { useKeyboardShortcutListRoomAction } from './hooks/roomActions/useKeyboardShortcutListRoomAction'; @@ -51,6 +52,7 @@ export const roomActionHooks = [ useContactProfileRoomAction, useDiscussionsRoomAction, useE2EERoomAction, + useE2EEResetRoomKeyRoomAction, useExportMessagesRoomAction, useGameCenterRoomAction, useKeyboardShortcutListRoomAction, @@ -79,4 +81,9 @@ export const quickActionHooks = [ useOnHoldChatQuickAction, ] satisfies (() => QuickActionsActionConfig | undefined)[]; -export const roomActionHooksForE2EESetup = [useChannelSettingsRoomAction, useMembersListRoomAction, useE2EERoomAction]; +export const roomActionHooksForE2EESetup = [ + useChannelSettingsRoomAction, + useMembersListRoomAction, + useE2EERoomAction, + useE2EEResetRoomKeyRoomAction, +];