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

refactor!: Room's Key ID generation #33329

Merged
merged 18 commits into from
Oct 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/spicy-eggs-march.md
Original file line number Diff line number Diff line change
@@ -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.
7 changes: 7 additions & 0 deletions apps/meteor/app/e2e/client/helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -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('');
}
16 changes: 11 additions & 5 deletions apps/meteor/app/e2e/client/rocketchat.e2e.room.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
readFileAsArrayBuffer,
encryptAESCTR,
generateAESCTRKey,
createSha256Hash,
} from './helper';
import { log, logError } from './logger';
import { e2e } from './rocketchat.e2e';
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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();
Expand Down
80 changes: 41 additions & 39 deletions apps/meteor/app/e2e/client/rocketchat.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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];
Expand Down
Loading