Skip to content

Commit

Permalink
feat(E2EE): Async E2EE keys exchange (#32197)
Browse files Browse the repository at this point in the history
  • Loading branch information
yash-rajpal authored Jun 22, 2024
1 parent 1240c87 commit f75a2cb
Show file tree
Hide file tree
Showing 23 changed files with 636 additions and 96 deletions.
8 changes: 8 additions & 0 deletions .changeset/orange-clocks-raise.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@rocket.chat/model-typings': minor
'@rocket.chat/core-typings': minor
'@rocket.chat/rest-typings': minor
'@rocket.chat/meteor': minor
---

Async End-to-End Encrypted rooms key distribution process. Users now don't need to be online to get the keys of their subscribed encrypted rooms, the key distribution process is now async and users can recieve keys even when they are not online.
51 changes: 51 additions & 0 deletions apps/meteor/app/api/server/v1/e2e.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import type { IUser } from '@rocket.chat/core-typings';
import { Subscriptions } from '@rocket.chat/models';
import {
ise2eGetUsersOfRoomWithoutKeyParamsGET,
ise2eSetRoomKeyIDParamsPOST,
ise2eSetUserPublicAndPrivateKeysParamsPOST,
ise2eUpdateGroupKeyParamsPOST,
isE2EProvideUsersGroupKeyProps,
isE2EFetchUsersWaitingForGroupKeyProps,
} from '@rocket.chat/rest-typings';
import { Meteor } from 'meteor/meteor';

import { handleSuggestedGroupKey } from '../../../e2e/server/functions/handleSuggestedGroupKey';
import { provideUsersSuggestedGroupKeys } from '../../../e2e/server/functions/provideUsersSuggestedGroupKeys';
import { settings } from '../../../settings/server';
import { API } from '../api';

API.v1.addRoute(
Expand Down Expand Up @@ -188,6 +193,9 @@ API.v1.addRoute(
{
authRequired: true,
validateParams: ise2eUpdateGroupKeyParamsPOST,
deprecation: {
version: '8.0.0',
},
},
{
async post() {
Expand Down Expand Up @@ -233,3 +241,46 @@ API.v1.addRoute(
},
},
);

API.v1.addRoute(
'e2e.fetchUsersWaitingForGroupKey',
{
authRequired: true,
validateParams: isE2EFetchUsersWaitingForGroupKeyProps,
},
{
async get() {
if (!settings.get('E2E_Enable')) {
return API.v1.success({ usersWaitingForE2EKeys: {} });
}

const { roomIds = [] } = this.queryParams;
const usersWaitingForE2EKeys = (await Subscriptions.findUsersWithPublicE2EKeyByRids(roomIds, this.userId).toArray()).reduce<
Record<string, { _id: string; public_key: string }[]>
>((acc, { rid, users }) => ({ [rid]: users, ...acc }), {});

return API.v1.success({
usersWaitingForE2EKeys,
});
},
},
);

API.v1.addRoute(
'e2e.provideUsersSuggestedGroupKeys',
{
authRequired: true,
validateParams: isE2EProvideUsersGroupKeyProps,
},
{
async post() {
if (!settings.get('E2E_Enable')) {
return API.v1.success();
}

await provideUsersSuggestedGroupKeys(this.userId, this.bodyParams.usersSuggestedGroupKeys);

return API.v1.success();
},
},
);
44 changes: 38 additions & 6 deletions apps/meteor/app/e2e/client/rocketchat.e2e.room.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,10 @@ export class E2ERoom extends Emitter {
logError(`E2E ROOM { state: ${this.state}, rid: ${this.roomId} }`, ...msg);
}

hasSessionKey() {
return !!this.groupSessionKey;
}

getState() {
return this.state;
}
Expand Down Expand Up @@ -317,17 +321,29 @@ export class E2ERoom extends Emitter {
async encryptKeyForOtherParticipants() {
// Encrypt generated session key for every user in room and publish to subscription model.
try {
const { users } = await sdk.call('e2e.getUsersOfRoomWithoutKey', this.roomId);
users.forEach((user) => this.encryptForParticipant(user));
const users = (await sdk.call('e2e.getUsersOfRoomWithoutKey', this.roomId)).users.filter((user) => user?.e2e?.public_key);

if (!users.length) {
return;
}

const usersSuggestedGroupKeys = { [this.roomId]: [] };
for await (const user of users) {
const encryptedGroupKey = await this.encryptGroupKeyForParticipant(user.e2e.public_key);

usersSuggestedGroupKeys[this.roomId].push({ _id: user._id, key: encryptedGroupKey });
}

await sdk.rest.post('/v1/e2e.provideUsersSuggestedGroupKeys', { usersSuggestedGroupKeys });
} catch (error) {
return this.error('Error getting room users: ', error);
}
}

async encryptForParticipant(user) {
async encryptGroupKeyForParticipant(public_key) {
let userKey;
try {
userKey = await importRSAKey(JSON.parse(user.e2e.public_key), ['encrypt']);
userKey = await importRSAKey(JSON.parse(public_key), ['encrypt']);
} catch (error) {
return this.error('Error importing user key: ', error);
}
Expand All @@ -336,8 +352,8 @@ export class E2ERoom extends Emitter {
// Encrypt session key for this user with his/her public key
try {
const encryptedUserKey = await encryptRSA(userKey, toArrayBuffer(this.sessionKeyExportedString));
// Key has been encrypted. Publish to that user's subscription model for this room.
await sdk.call('e2e.updateGroupKey', this.roomId, user._id, this.keyID + Base64.encode(new Uint8Array(encryptedUserKey)));
const encryptedUserKeyToString = this.keyID + Base64.encode(new Uint8Array(encryptedUserKey));
return encryptedUserKeyToString;
} catch (error) {
return this.error('Error encrypting user key: ', error);
}
Expand Down Expand Up @@ -510,4 +526,20 @@ export class E2ERoom extends Emitter {
this.on('STATE_CHANGED', cb);
return () => this.off('STATE_CHANGED', cb);
}

async encryptGroupKeyForParticipantsWaitingForTheKeys(users) {
if (!this.isReady()) {
return;
}

const usersWithKeys = await Promise.all(
users.map(async (user) => {
const { _id, public_key } = user;
const key = await this.encryptGroupKeyForParticipant(public_key);
return { _id, key };
}),
);

return usersWithKeys;
}
}
Loading

0 comments on commit f75a2cb

Please sign in to comment.