From 510af6f6a31988c2eafa09efce73a24b19a60053 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Wed, 2 Oct 2024 10:19:21 -0600 Subject: [PATCH] refactor!: Room's Key ID generation (#33329) Co-authored-by: Hugo Costa Co-authored-by: Guilherme Gazzo --- .changeset/spicy-eggs-march.md | 8 ++ apps/meteor/app/e2e/client/helper.js | 7 ++ .../app/e2e/client/rocketchat.e2e.room.js | 16 ++-- apps/meteor/app/e2e/client/rocketchat.e2e.ts | 80 ++++++++++--------- 4 files changed, 67 insertions(+), 44 deletions(-) create mode 100644 .changeset/spicy-eggs-march.md diff --git a/.changeset/spicy-eggs-march.md b/.changeset/spicy-eggs-march.md new file mode 100644 index 000000000000..a88098afd96e --- /dev/null +++ b/.changeset/spicy-eggs-march.md @@ -0,0 +1,8 @@ +--- +"@rocket.chat/meteor": major +--- + +Randomizes `e2eKeyId` generation instead of derive it from encoded key. Previously, we used the stringified & encoded version of the key to extract a keyID, however this generated the same keyID for all rooms. As we didn't use this keyID, and rooms didn't have the capability of having multiple keys, this was harmless. +This PR introduces a new way of generating that identifier, making it random and unique, so multiple room keys can be used on the same room as long as the keyID is different. + +NOTE: new E2EE rooms created _after_ this PR is merged will not be compatible with older versions of Rocket.Chat. Old rooms created before this update will continue to be compatible. diff --git a/apps/meteor/app/e2e/client/helper.js b/apps/meteor/app/e2e/client/helper.js index 49b157c5ccf4..ddf49b262b91 100644 --- a/apps/meteor/app/e2e/client/helper.js +++ b/apps/meteor/app/e2e/client/helper.js @@ -146,3 +146,10 @@ export async function generateMnemonicPhrase(n, sep = ' ') { } return result.join(sep); } + +export async function createSha256Hash(data) { + const hash = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(data)); + return Array.from(new Uint8Array(hash)) + .map((b) => b.toString(16).padStart(2, '0')) + .join(''); +} diff --git a/apps/meteor/app/e2e/client/rocketchat.e2e.room.js b/apps/meteor/app/e2e/client/rocketchat.e2e.room.js index fe61156240a8..1b2357067028 100644 --- a/apps/meteor/app/e2e/client/rocketchat.e2e.room.js +++ b/apps/meteor/app/e2e/client/rocketchat.e2e.room.js @@ -24,6 +24,7 @@ import { readFileAsArrayBuffer, encryptAESCTR, generateAESCTRKey, + createSha256Hash, } from './helper'; import { log, logError } from './logger'; import { e2e } from './rocketchat.e2e'; @@ -67,12 +68,13 @@ export class E2ERoom extends Emitter { [PAUSED] = undefined; - constructor(userId, roomId, t) { + constructor(userId, room) { super(); this.userId = userId; - this.roomId = roomId; - this.typeOfRoom = t; + this.roomId = room._id; + this.typeOfRoom = room.t; + this.roomKeyId = room.e2eKeyId; this.once(E2ERoomState.READY, () => this.decryptPendingMessages()); this.once(E2ERoomState.READY, () => this.decryptSubscription()); @@ -280,7 +282,11 @@ export class E2ERoom extends Emitter { return false; } - this.keyID = Base64.encode(this.sessionKeyExportedString).slice(0, 12); + // When a new e2e room is created, it will be initialized without an e2e key id + // This will prevent new rooms from storing `undefined` as the keyid + if (!this.keyID) { + this.keyID = this.roomKeyId || (await createSha256Hash(this.sessionKeyExportedString)).slice(0, 12); + } // Import session key for use. try { @@ -308,7 +314,7 @@ export class E2ERoom extends Emitter { try { const sessionKeyExported = await exportJWKKey(this.groupSessionKey); this.sessionKeyExportedString = JSON.stringify(sessionKeyExported); - this.keyID = Base64.encode(this.sessionKeyExportedString).slice(0, 12); + this.keyID = (await createSha256Hash(this.sessionKeyExportedString)).slice(0, 12); await sdk.call('e2e.setRoomKeyID', this.roomId, this.keyID); await this.encryptKeyForOtherParticipants(); diff --git a/apps/meteor/app/e2e/client/rocketchat.e2e.ts b/apps/meteor/app/e2e/client/rocketchat.e2e.ts index 50224cb89dbb..5485fe31fc15 100644 --- a/apps/meteor/app/e2e/client/rocketchat.e2e.ts +++ b/apps/meteor/app/e2e/client/rocketchat.e2e.ts @@ -145,52 +145,54 @@ class E2E extends Emitter { this.log('observing subscriptions'); } - observeSubscriptions() { - this.observable?.stop(); + async onSubscriptionChanged(sub: ISubscription) { + this.log('Subscription changed', sub); + if (!sub.encrypted && !sub.E2EKey) { + this.removeInstanceByRoomId(sub.rid); + return; + } - this.observable = Subscriptions.find().observe({ - changed: (sub: ISubscription) => { - setTimeout(async () => { - this.log('Subscription changed', sub); - if (!sub.encrypted && !sub.E2EKey) { - this.removeInstanceByRoomId(sub.rid); - return; - } + const e2eRoom = await this.getInstanceByRoomId(sub.rid); + if (!e2eRoom) { + return; + } - const e2eRoom = await this.getInstanceByRoomId(sub.rid); - if (!e2eRoom) { - return; - } + if (sub.E2ESuggestedKey) { + if (await e2eRoom.importGroupKey(sub.E2ESuggestedKey)) { + await this.acceptSuggestedKey(sub.rid); + e2eRoom.keyReceived(); + } else { + console.warn('Invalid E2ESuggestedKey, rejecting', sub.E2ESuggestedKey); + await this.rejectSuggestedKey(sub.rid); + } + } - if (sub.E2ESuggestedKey) { - if (await e2eRoom.importGroupKey(sub.E2ESuggestedKey)) { - await this.acceptSuggestedKey(sub.rid); - e2eRoom.keyReceived(); - } else { - console.warn('Invalid E2ESuggestedKey, rejecting', sub.E2ESuggestedKey); - await this.rejectSuggestedKey(sub.rid); - } - } + sub.encrypted ? e2eRoom.resume() : e2eRoom.pause(); - sub.encrypted ? e2eRoom.resume() : e2eRoom.pause(); + // Cover private groups and direct messages + if (!e2eRoom.isSupportedRoomType(sub.t)) { + e2eRoom.disable(); + return; + } - // Cover private groups and direct messages - if (!e2eRoom.isSupportedRoomType(sub.t)) { - e2eRoom.disable(); - return; - } + if (sub.E2EKey && e2eRoom.isWaitingKeys()) { + e2eRoom.keyReceived(); + return; + } - if (sub.E2EKey && e2eRoom.isWaitingKeys()) { - e2eRoom.keyReceived(); - return; - } + if (!e2eRoom.isReady()) { + return; + } - if (!e2eRoom.isReady()) { - return; - } + await e2eRoom.decryptSubscription(); + } - await e2eRoom.decryptSubscription(); - }, 0); + observeSubscriptions() { + this.observable?.stop(); + + this.observable = Subscriptions.find().observe({ + changed: (sub: ISubscription) => { + setTimeout(() => this.onSubscriptionChanged(sub), 0); }, added: (sub: ISubscription) => { setTimeout(async () => { @@ -263,7 +265,7 @@ class E2E extends Emitter { } if (!this.instancesByRoomId[rid]) { - this.instancesByRoomId[rid] = new E2ERoom(Meteor.userId(), rid, room.t); + this.instancesByRoomId[rid] = new E2ERoom(Meteor.userId(), room); } return this.instancesByRoomId[rid];