Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[IMPROVE] Require acceptance when setting new E2E Encryption key for another user #27556

Merged
merged 15 commits into from Jan 16, 2023
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
34 changes: 34 additions & 0 deletions apps/meteor/app/api/server/v1/e2e.ts
Expand Up @@ -195,3 +195,37 @@ API.v1.addRoute(
},
},
);

API.v1.addRoute(
'e2e.acceptSuggestedGroupKey',
{
authRequired: true,
validateParams: ise2eGetUsersOfRoomWithoutKeyParamsGET,
},
{
post() {
const { rid } = this.bodyParams;

Meteor.call('e2e.acceptSuggestedGroupKey', rid);

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

API.v1.addRoute(
'e2e.rejectSuggestedGroupKey',
{
authRequired: true,
validateParams: ise2eGetUsersOfRoomWithoutKeyParamsGET,
},
{
post() {
const { rid } = this.bodyParams;

Meteor.call('e2e.rejectSuggestedGroupKey', rid);

return API.v1.success();
},
},
);
8 changes: 6 additions & 2 deletions apps/meteor/app/e2e/client/rocketchat.e2e.room.js
Expand Up @@ -264,7 +264,8 @@ export class E2ERoom extends Emitter {
const decryptedKey = await decryptRSA(e2e.privateKey, groupKey);
this.sessionKeyExportedString = toString(decryptedKey);
} catch (error) {
return this.error('Error decrypting group key: ', error);
this.error('Error decrypting group key: ', error);
return false;
}

this.keyID = Base64.encode(this.sessionKeyExportedString).slice(0, 12);
Expand All @@ -275,8 +276,11 @@ export class E2ERoom extends Emitter {
// Key has been obtained. E2E is now in session.
this.groupSessionKey = key;
} catch (error) {
return this.error('Error importing group key: ', error);
this.error('Error importing group key: ', error);
return false;
}

return true;
}

async createGroupKey() {
Expand Down
12 changes: 12 additions & 0 deletions apps/meteor/app/e2e/client/rocketchat.e2e.ts
Expand Up @@ -136,6 +136,18 @@ class E2E extends Emitter {
});
}

async acceptSuggestedKey(rid: string): Promise<void> {
await APIClient.post('/v1/e2e.acceptSuggestedGroupKey', {
rid,
});
}

async rejectSuggestedKey(rid: string): Promise<void> {
await APIClient.post('/v1/e2e.rejectSuggestedGroupKey', {
rid,
});
}

getKeysFromLocalStorage(): KeyPair {
return {
public_key: Meteor._localStorage.getItem('public_key'),
Expand Down
28 changes: 28 additions & 0 deletions apps/meteor/app/e2e/server/functions/handleSuggestedGroupKey.ts
@@ -0,0 +1,28 @@
import { Meteor } from 'meteor/meteor';
import { Subscriptions } from '@rocket.chat/models';

export async function handleSuggestedGroupKey(handle: 'accept' | 'reject', rid: string, userId: string | null, method: string) {
if (!userId) {
throw new Meteor.Error('error-invalid-user', 'Invalid user', { method });
}

const sub = await Subscriptions.findOneByRoomIdAndUserId(rid, userId);
if (!sub) {
throw new Meteor.Error('error-subscription-not-found', 'Subscription not found', { method });
}

const suggestedKey = String(sub.E2ESuggestedKey ?? '').trim();
if (!suggestedKey) {
throw new Meteor.Error('error-no-suggested-key-available', 'No suggested key available', { method });
}

if (handle === 'accept') {
await Subscriptions.setGroupE2EKey(sub._id, suggestedKey);
}

await Subscriptions.unsetGroupE2ESuggestedKey(sub._id);

return {
success: true,
};
}
2 changes: 2 additions & 0 deletions apps/meteor/app/e2e/server/index.js
Expand Up @@ -11,6 +11,8 @@ import './methods/setRoomKeyID';
import './methods/fetchMyKeys';
import './methods/resetOwnE2EKey';
import './methods/requestSubscriptionKeys';
import './methods/acceptSuggestedGroupKey';
import './methods/rejectSuggestedGroupKey';

callbacks.add(
'afterJoinRoom',
Expand Down
11 changes: 11 additions & 0 deletions apps/meteor/app/e2e/server/methods/acceptSuggestedGroupKey.ts
@@ -0,0 +1,11 @@
import { Meteor } from 'meteor/meteor';

import { handleSuggestedGroupKey } from '../functions/handleSuggestedGroupKey';

const method = 'e2e.acceptSuggestedGroupKey';

Meteor.methods({
async [method](rid) {
return handleSuggestedGroupKey('accept', rid, Meteor.userId(), method);
},
});
11 changes: 11 additions & 0 deletions apps/meteor/app/e2e/server/methods/rejectSuggestedGroupKey.ts
@@ -0,0 +1,11 @@
import { Meteor } from 'meteor/meteor';

import { handleSuggestedGroupKey } from '../functions/handleSuggestedGroupKey';

const method = 'e2e.rejectSuggestedGroupKey';

Meteor.methods({
async [method](rid) {
return handleSuggestedGroupKey('reject', rid, Meteor.userId(), method);
},
});
17 changes: 0 additions & 17 deletions apps/meteor/app/e2e/server/methods/updateGroupKey.js

This file was deleted.

27 changes: 27 additions & 0 deletions apps/meteor/app/e2e/server/methods/updateGroupKey.ts
@@ -0,0 +1,27 @@
import { Meteor } from 'meteor/meteor';
import { Subscriptions } from '@rocket.chat/models';

Meteor.methods({
async 'e2e.updateGroupKey'(rid, uid, key) {
const userId = Meteor.userId();
if (!userId) {
throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'e2e.acceptSuggestedGroupKey' });
}

// I have a subscription to this room
const mySub = await Subscriptions.findOneByRoomIdAndUserId(rid, userId);

if (mySub) {
// Setting the key to myself, can set directly to the final field
if (userId === uid) {
return Subscriptions.setGroupE2EKey(mySub._id, key);
}

// uid also has subscription to this room
const userSub = await Subscriptions.findOneByRoomIdAndUserId(rid, uid);
if (userSub) {
return Subscriptions.setGroupE2ESuggestedKey(userSub._id, key);
}
}
},
});
7 changes: 0 additions & 7 deletions apps/meteor/app/models/server/models/Subscriptions.js
Expand Up @@ -347,13 +347,6 @@ export class Subscriptions extends Base {
return this.find(query, options);
}

updateGroupE2EKey(_id, key) {
const query = { _id };
const update = { $set: { E2EKey: key } };
this.update(query, update);
return this.findOne({ _id });
}

/**
* @param {IRole['_id'][]} roles
* @param {string} scope the value for the role scope (room id)
Expand Down
33 changes: 21 additions & 12 deletions apps/meteor/client/startup/e2e.ts
Expand Up @@ -53,26 +53,35 @@ Meteor.startup(() => {
Notifications.onUser('e2ekeyRequest', handle);

observable = Subscriptions.find().observe({
changed: async (doc: ISubscription) => {
if (!doc.encrypted && !doc.E2EKey) {
e2e.removeInstanceByRoomId(doc.rid);
changed: async (sub: ISubscription) => {
if (!sub.encrypted && !sub.E2EKey) {
e2e.removeInstanceByRoomId(sub.rid);
return;
}

const e2eRoom = await e2e.getInstanceByRoomId(doc.rid);
const e2eRoom = await e2e.getInstanceByRoomId(sub.rid);
if (!e2eRoom) {
return;
}

doc.encrypted ? e2eRoom.resume() : e2eRoom.pause();
if (sub.E2ESuggestedKey) {
if (await e2eRoom.importGroupKey(sub.E2ESuggestedKey)) {
e2e.acceptSuggestedKey(sub.rid);
} else {
console.warn('Invalid E2ESuggestedKey, rejecting', sub.E2ESuggestedKey);
e2e.rejectSuggestedKey(sub.rid);
}
}

sub.encrypted ? e2eRoom.resume() : e2eRoom.pause();

// Cover private groups and direct messages
if (!e2eRoom.isSupportedRoomType(doc.t)) {
if (!e2eRoom.isSupportedRoomType(sub.t)) {
e2eRoom.disable();
return;
}

if (doc.E2EKey && e2eRoom.isWaitingKeys()) {
if (sub.E2EKey && e2eRoom.isWaitingKeys()) {
e2eRoom.keyReceived();
return;
}
Expand All @@ -83,14 +92,14 @@ Meteor.startup(() => {

e2eRoom.decryptSubscription();
},
added: async (doc: ISubscription) => {
if (!doc.encrypted && !doc.E2EKey) {
added: async (sub: ISubscription) => {
if (!sub.encrypted && !sub.E2EKey) {
return;
}
return e2e.getInstanceByRoomId(doc.rid);
return e2e.getInstanceByRoomId(sub.rid);
},
removed: (doc: ISubscription) => {
e2e.removeInstanceByRoomId(doc.rid);
removed: (sub: ISubscription) => {
e2e.removeInstanceByRoomId(sub.rid);
},
});

Expand Down
18 changes: 18 additions & 0 deletions apps/meteor/server/models/raw/Subscriptions.ts
Expand Up @@ -449,4 +449,22 @@ export class SubscriptionsRaw extends BaseRaw<ISubscription> implements ISubscri
};
return this.updateMany(query, update);
}

async setGroupE2EKey(_id: string, key: string): Promise<ISubscription | null> {
const query = { _id };
const update = { $set: { E2EKey: key } };
await this.updateOne(query, update);
return this.findOneById(_id);
}

setGroupE2ESuggestedKey(_id: string, key: string): Promise<UpdateResult | Document> {
const query = { _id };
const update = { $set: { E2ESuggestedKey: key } };
return this.updateOne(query, update);
}

unsetGroupE2ESuggestedKey(_id: string): Promise<UpdateResult | Document> {
const query = { _id };
return this.updateOne(query, { $unset: { E2ESuggestedKey: 1 } });
}
}
1 change: 1 addition & 0 deletions apps/meteor/server/modules/watchers/publishFields.ts
Expand Up @@ -36,6 +36,7 @@ export const subscriptionFields = {
muteGroupMentions: 1,
ignored: 1,
E2EKey: 1,
E2ESuggestedKey: 1,
tunread: 1,
tunreadGroup: 1,
tunreadUser: 1,
Expand Down
1 change: 1 addition & 0 deletions packages/core-typings/src/ISubscription.ts
Expand Up @@ -40,6 +40,7 @@ export interface ISubscription extends IRocketChatRecord {
onHold?: boolean;
encrypted?: boolean;
E2EKey?: string;
E2ESuggestedKey?: string;
unreadAlert?: 'default' | 'all' | 'mentions' | 'nothing';

fname?: string;
Expand Down
6 changes: 6 additions & 0 deletions packages/model-typings/src/models/ISubscriptionsModel.ts
Expand Up @@ -77,4 +77,10 @@ export interface ISubscriptionsModel extends IBaseModel<ISubscription> {
setAlertForRoomIdExcludingUserId(roomId: IRoom['_id'], userId: IUser['_id']): Promise<UpdateResult | Document>;

setOpenForRoomIdExcludingUserId(roomId: IRoom['_id'], userId: IUser['_id']): Promise<UpdateResult | Document>;

setGroupE2EKey(_id: string, key: string): Promise<ISubscription | null>;

setGroupE2ESuggestedKey(_id: string, key: string): Promise<UpdateResult | Document>;

unsetGroupE2ESuggestedKey(_id: string): Promise<UpdateResult | Document>;
}
6 changes: 6 additions & 0 deletions packages/rest-typings/src/v1/e2e.ts
Expand Up @@ -99,6 +99,12 @@ export type E2eEndpoints = {
'/v1/e2e.updateGroupKey': {
POST: (params: E2eUpdateGroupKeyProps) => void;
};
'/v1/e2e.acceptSuggestedGroupKey': {
POST: (params: E2eGetUsersOfRoomWithoutKeyProps) => void;
};
'/v1/e2e.rejectSuggestedGroupKey': {
POST: (params: E2eGetUsersOfRoomWithoutKeyProps) => void;
};
'/v1/e2e.setRoomKeyID': {
POST: (params: E2eSetRoomKeyIdProps) => void;
};
Expand Down