Skip to content

Commit

Permalink
Reset room key full cycle
Browse files Browse the repository at this point in the history
  • Loading branch information
KevLehman committed Sep 30, 2024
1 parent 0d7a6b3 commit 28c1e72
Show file tree
Hide file tree
Showing 5 changed files with 133 additions and 13 deletions.
29 changes: 28 additions & 1 deletion apps/meteor/app/e2e/client/rocketchat.e2e.room.js
Original file line number Diff line number Diff line change
Expand Up @@ -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]: [
Expand Down Expand Up @@ -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 {
Expand Down
6 changes: 6 additions & 0 deletions apps/meteor/app/e2e/client/rocketchat.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ class E2E extends Emitter {

public privateKey: CryptoKey | undefined;

public publicKey: string | undefined;

private keyDistributionInterval: ReturnType<typeof setInterval> | null;

private state: E2EEState;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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<void> {
Expand All @@ -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);
Expand Down
22 changes: 11 additions & 11 deletions apps/meteor/app/e2e/server/functions/resetRoomKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand All @@ -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);
Expand All @@ -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!);
}

Expand Down
80 changes: 80 additions & 0 deletions apps/meteor/client/hooks/roomActions/useE2EERoomKeyResetAction.ts
Original file line number Diff line number Diff line change
@@ -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]);
};
9 changes: 8 additions & 1 deletion apps/meteor/client/ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -51,6 +52,7 @@ export const roomActionHooks = [
useContactProfileRoomAction,
useDiscussionsRoomAction,
useE2EERoomAction,
useE2EEResetRoomKeyRoomAction,
useExportMessagesRoomAction,
useGameCenterRoomAction,
useKeyboardShortcutListRoomAction,
Expand Down Expand Up @@ -79,4 +81,9 @@ export const quickActionHooks = [
useOnHoldChatQuickAction,
] satisfies (() => QuickActionsActionConfig | undefined)[];

export const roomActionHooksForE2EESetup = [useChannelSettingsRoomAction, useMembersListRoomAction, useE2EERoomAction];
export const roomActionHooksForE2EESetup = [
useChannelSettingsRoomAction,
useMembersListRoomAction,
useE2EERoomAction,
useE2EEResetRoomKeyRoomAction,
];

0 comments on commit 28c1e72

Please sign in to comment.