Skip to content

Commit

Permalink
chore: Update types of e2e.room file (#34944)
Browse files Browse the repository at this point in the history
KevLehman authored Jan 29, 2025
1 parent 271894f commit 63df841
Showing 4 changed files with 133 additions and 69 deletions.
8 changes: 4 additions & 4 deletions apps/meteor/app/e2e/client/helper.ts
Original file line number Diff line number Diff line change
@@ -45,19 +45,19 @@ export async function encryptRSA(key: any, data: any) {
return crypto.subtle.encrypt({ name: 'RSA-OAEP' }, key, data);
}

export async function encryptAES(vector: any, key: any, data: any) {
export async function encryptAES(vector: Uint8Array<ArrayBuffer>, key: CryptoKey, data: Uint8Array<ArrayBufferLike>) {
return crypto.subtle.encrypt({ name: 'AES-CBC', iv: vector }, key, data);
}

export async function encryptAESCTR(vector: any, key: any, data: any) {
return crypto.subtle.encrypt({ name: 'AES-CTR', counter: vector, length: 64 }, key, data);
}

export async function decryptRSA(key: any, data: any) {
export async function decryptRSA(key: CryptoKey, data: Uint8Array<ArrayBuffer>) {
return crypto.subtle.decrypt({ name: 'RSA-OAEP' }, key, data);
}

export async function decryptAES(vector: any, key: any, data: any) {
export async function decryptAES(vector: Uint8Array<ArrayBufferLike>, key: CryptoKey, data: Uint8Array<ArrayBufferLike>) {
return crypto.subtle.decrypt({ name: 'AES-CBC', iv: vector }, key, data);
}

@@ -116,7 +116,7 @@ export async function deriveKey(salt: any, baseKey: any, keyUsages: ReadonlyArra
return crypto.subtle.deriveKey({ name: 'PBKDF2', salt, iterations, hash }, baseKey, { name: 'AES-CBC', length: 256 }, true, keyUsages);
}

export async function readFileAsArrayBuffer(file: any) {
export async function readFileAsArrayBuffer(file: File) {
return new Promise<any>((resolve, reject) => {
const reader = new FileReader();
reader.onload = (evt) => {
172 changes: 113 additions & 59 deletions apps/meteor/app/e2e/client/rocketchat.e2e.room.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { Base64 } from '@rocket.chat/base64';
import type { IE2EEMessage, IMessage, IRoom, ISubscription, IUser, IUploadWithUser, AtLeast } from '@rocket.chat/core-typings';
import { Emitter } from '@rocket.chat/emitter';
import type { Optional } from '@tanstack/react-query';
import EJSON from 'ejson';

import { E2ERoomState } from './E2ERoomState';
@@ -34,7 +36,9 @@
const KEY_ID = Symbol('keyID');
const PAUSED = Symbol('PAUSED');

const permitedMutations: any = {
type Mutations = { [k in keyof typeof E2ERoomState]?: (keyof typeof E2ERoomState)[] };

const permitedMutations: Mutations = {
[E2ERoomState.NOT_STARTED]: [E2ERoomState.ESTABLISHING, E2ERoomState.DISABLED, E2ERoomState.KEYS_RECEIVED],
[E2ERoomState.READY]: [E2ERoomState.DISABLED, E2ERoomState.CREATING_KEYS, E2ERoomState.WAITING_KEYS],
[E2ERoomState.ERROR]: [E2ERoomState.KEYS_RECEIVED, E2ERoomState.NOT_STARTED],
@@ -49,46 +53,51 @@
],
};

const filterMutation = (currentState: any, nextState: any): any => {
const filterMutation = (currentState: E2ERoomState | undefined, nextState: E2ERoomState): E2ERoomState | false => {
// When state is undefined, allow it to be moved
if (!currentState) {
return nextState;
}

if (currentState === nextState) {
return nextState === E2ERoomState.ERROR;
return nextState === E2ERoomState.ERROR ? E2ERoomState.ERROR : false;
}

if (!(currentState in permitedMutations)) {
return nextState;
}

if (permitedMutations[currentState].includes(nextState)) {
if (permitedMutations?.[currentState]?.includes(nextState)) {
return nextState;
}

return false;
};

export class E2ERoom extends Emitter {
state: any = undefined;
state: E2ERoomState | undefined = undefined;

[PAUSED]: boolean | undefined = undefined;

[KEY_ID]: any;
[KEY_ID]: string;

userId: any;
userId: string;

roomId: any;
roomId: string;

typeOfRoom: any;
typeOfRoom: string;

roomKeyId: any;
roomKeyId: string | undefined;

groupSessionKey: any;
groupSessionKey: CryptoKey | undefined;

oldKeys: any;
oldKeys: { E2EKey: CryptoKey | null; ts: Date; e2eKeyId: string }[] | undefined;

sessionKeyExportedString: string | undefined;

sessionKeyExported: any;
sessionKeyExported: JsonWebKey | undefined;

constructor(userId: any, room: any) {
constructor(userId: string, room: IRoom) {
super();

this.userId = userId;
@@ -127,7 +136,7 @@
return this.state;
}

setState(requestedState: any) {
setState(requestedState: E2ERoomState) {
const currentState = this.state;
const nextState = filterMutation(currentState, requestedState);

@@ -178,7 +187,7 @@
this.setState(E2ERoomState.KEYS_RECEIVED);
}

async shouldConvertSentMessages(message: any) {
async shouldConvertSentMessages(message: { msg: string }) {
if (!this.isReady() || this[PAUSED]) {
return false;
}
@@ -263,7 +272,7 @@
this.log('decryptOldRoomKeys Done');
}

async exportOldRoomKeys(oldKeys: any) {
async exportOldRoomKeys(oldKeys: ISubscription['oldRoomKeys']) {
this.log('exportOldRoomKeys starting');
if (!oldKeys || oldKeys.length === 0) {
this.log('exportOldRoomKeys nothing to do');
@@ -295,7 +304,7 @@

async decryptPendingMessages() {
return Messages.find({ rid: this.roomId, t: 'e2e', e2e: 'pending' }).forEach(async ({ _id, ...msg }) => {
Messages.update({ _id }, await this.decryptMessage(msg));
Messages.update({ _id }, await this.decryptMessage({ _id, ...msg }));
});
}

@@ -325,6 +334,7 @@
}

try {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const room = Rooms.findOne({ _id: this.roomId })!;
if (!room.e2eKeyId) {
this.setState(E2ERoomState.CREATING_KEYS);
@@ -342,32 +352,39 @@
}
}

isSupportedRoomType(type: any) {
isSupportedRoomType(type: string) {
return roomCoordinator.getRoomDirectives(type).allowRoomSettingChange({}, RoomSettingsEnum.E2E);
}

async decryptSessionKey(key: any) {
async decryptSessionKey(key: string) {
return importAESKey(JSON.parse(await this.exportSessionKey(key)));
}

async exportSessionKey(key: any) {
async exportSessionKey(key: string) {
key = key.slice(12);
key = Base64.decode(key);
const decodedKey = Base64.decode(key);

const decryptedKey = await decryptRSA(e2e.privateKey, key);
if (!e2e.privateKey) {
throw new Error('Private key not found');
}

const decryptedKey = await decryptRSA(e2e.privateKey, decodedKey);
return toString(decryptedKey);
}

async importGroupKey(groupKey: any) {
async importGroupKey(groupKey: string) {
this.log('Importing room key ->', this.roomId);
// Get existing group key
// const keyID = groupKey.slice(0, 12);
groupKey = groupKey.slice(12);
groupKey = Base64.decode(groupKey);
const decodedGroupKey = Base64.decode(groupKey);

// Decrypt obtained encrypted session key
try {
const decryptedKey = await decryptRSA(e2e.privateKey, groupKey);
if (!e2e.privateKey) {
throw new Error('Private key not found');
}
const decryptedKey = await decryptRSA(e2e.privateKey, decodedGroupKey);
this.sessionKeyExportedString = toString(decryptedKey);
} catch (error) {
this.error('Error decrypting group key: ', error);
@@ -382,7 +399,7 @@

// Import session key for use.
try {
const key = await importAESKey(JSON.parse(this.sessionKeyExportedString!));

Check warning on line 402 in apps/meteor/app/e2e/client/rocketchat.e2e.room.ts

GitHub Actions / 🔎 Code Check / Code Lint

Forbidden non-null assertion
// Key has been obtained. E2E is now in session.
this.groupSessionKey = key;
} catch (error) {
@@ -407,12 +424,15 @@
await this.createNewGroupKey();

await sdk.call('e2e.setRoomKeyID', this.roomId, this.keyID);
await sdk.rest.post('/v1/e2e.updateGroupKey', {
rid: this.roomId,
uid: this.userId,
key: await this.encryptGroupKeyForParticipant(e2e.publicKey!),
} as any);
await this.encryptKeyForOtherParticipants();
const myKey = await this.encryptGroupKeyForParticipant(e2e.publicKey!);

Check warning on line 427 in apps/meteor/app/e2e/client/rocketchat.e2e.room.ts

GitHub Actions / 🔎 Code Check / Code Lint

Forbidden non-null assertion
if (myKey) {
await sdk.rest.post('/v1/e2e.updateGroupKey', {
rid: this.roomId,
uid: this.userId,
key: myKey,
});
await this.encryptKeyForOtherParticipants();
}
} catch (error) {
this.error('Error exporting group key: ', error);
throw error;
@@ -442,7 +462,7 @@
}
}

onRoomKeyReset(keyID: any) {
onRoomKeyReset(keyID: string) {
this.log(`Room keyID was reset. New keyID: ${keyID} Previous keyID: ${this.keyID}`);
this.setState(E2ERoomState.WAITING_KEYS);
this.keyID = keyID;
@@ -463,12 +483,27 @@
return;
}

const usersSuggestedGroupKeys = { [this.roomId]: [] as any[] };
const usersSuggestedGroupKeys: Record<
string,
{
_id: IUser['_id'];
key: string;
oldKeys: ISubscription['suggestedOldRoomKeys'];
}[]
> = { [this.roomId]: [] };
for await (const user of users) {
const encryptedGroupKey = await this.encryptGroupKeyForParticipant(user.e2e!.public_key!);

Check warning on line 495 in apps/meteor/app/e2e/client/rocketchat.e2e.room.ts

GitHub Actions / 🔎 Code Check / Code Lint

Forbidden non-null assertion

Check warning on line 495 in apps/meteor/app/e2e/client/rocketchat.e2e.room.ts

GitHub Actions / 🔎 Code Check / Code Lint

Forbidden non-null assertion
const oldKeys = await this.encryptOldKeysForParticipant(user.e2e?.public_key, decryptedOldGroupKeys);

usersSuggestedGroupKeys[this.roomId].push({ _id: user._id, key: encryptedGroupKey, ...(oldKeys && { oldKeys }) });
if (!encryptedGroupKey) {
return;
}
if (decryptedOldGroupKeys) {
const oldKeys = await this.encryptOldKeysForParticipant(user.e2e!.public_key!, decryptedOldGroupKeys);
if (oldKeys) {
usersSuggestedGroupKeys[this.roomId].push({ _id: user._id, key: encryptedGroupKey, oldKeys });
continue;
}
}
usersSuggestedGroupKeys[this.roomId].push({ _id: user._id, key: encryptedGroupKey, oldKeys: undefined });
}

await sdk.rest.post('/v1/e2e.provideUsersSuggestedGroupKeys', { usersSuggestedGroupKeys });
@@ -477,7 +512,7 @@
}
}

async encryptOldKeysForParticipant(publicKey: any, oldRoomKeys: any) {
async encryptOldKeysForParticipant(publicKey: string, oldRoomKeys: { E2EKey: string; e2eKeyId: string; ts: Date }[]) {
if (!oldRoomKeys || oldRoomKeys.length === 0) {
return;
}
@@ -527,7 +562,7 @@
}

// Encrypts files before upload. I/O is in arraybuffers.
async encryptFile(file: any) {
async encryptFile(file: File) {
// if (!this.isSupportedRoomType(this.typeOfRoom)) {
// return;
// }
@@ -562,18 +597,21 @@
}

// Decrypt uploaded encrypted files. I/O is in arraybuffers.
async decryptFile(file: any, key: any, iv: any) {
async decryptFile(file: Uint8Array<ArrayBuffer>, key: JsonWebKey, iv: string) {
const ivArray = Base64.decode(iv);
const cryptoKey = await window.crypto.subtle.importKey('jwk', key, { name: 'AES-CTR' }, true, ['encrypt', 'decrypt']);

return window.crypto.subtle.decrypt({ name: 'AES-CTR', counter: ivArray, length: 64 }, cryptoKey, file);
}

// Encrypts messages
async encryptText(data: any) {
async encryptText(data: Uint8Array<ArrayBufferLike>) {
const vector = crypto.getRandomValues(new Uint8Array(16));

try {
if (!this.groupSessionKey) {
throw new Error('No group session key found.');
}
const result = await encryptAES(vector, this.groupSessionKey, data);
return this.keyID + Base64.encode(joinVectorAndEcryptedData(vector, result));
} catch (error) {
@@ -583,7 +621,9 @@
}

// Helper function for encryption of content
async encryptMessageContent(contentToBeEncrypted: any) {
async encryptMessageContent(
contentToBeEncrypted: Pick<IMessage, 'attachments' | 'files' | 'file'> & Optional<Pick<IMessage, 'msg'>, 'msg'>,
) {
const data = new TextEncoder().encode(EJSON.stringify(contentToBeEncrypted));

return {
@@ -593,21 +633,23 @@
}

// Helper function for encryption of content
async encryptMessage(message: any) {
async encryptMessage(message: AtLeast<IMessage, '_id' | 'rid' | 'msg'>): Promise<IE2EEMessage> {
const { msg, attachments, ...rest } = message;

const content = await this.encryptMessageContent({ msg, attachments });

// E2EMessages remove the `msg` property. It's stored in `content` instead.
// Making the property optional can open a small can of worms, but just ignoring it in here should be fine ;)
return {
...rest,
content,
t: 'e2e',
e2e: 'pending',
};
t: 'e2e' as const,
e2e: 'pending' as const,
} as IE2EEMessage;
}

// Helper function for encryption of messages
encrypt(message: any) {
encrypt(message: IMessage) {
if (!this.isSupportedRoomType(this.typeOfRoom)) {
return;
}
@@ -630,7 +672,7 @@
return this.encryptText(data);
}

async decryptContent(data: any) {
async decryptContent<T extends IUploadWithUser | IE2EEMessage>(data: T) {
if (data.content && data.content.algorithm === 'rc.v1.aes-sha2') {
const content = await this.decrypt(data.content.ciphertext);
Object.assign(data, content);
@@ -640,7 +682,7 @@
}

// Decrypt messages
async decryptMessage(message: any) {
async decryptMessage(message: IMessage | IE2EEMessage): Promise<IE2EEMessage | IMessage> {
if (message.t !== 'e2e' || message.e2e === 'done') {
return message;
}
@@ -657,22 +699,22 @@

return {
...message,
e2e: 'done',
e2e: 'done' as const,
};
}

async doDecrypt(vector: any, key: any, cipherText: any) {
async doDecrypt(vector: Uint8Array<ArrayBufferLike>, key: CryptoKey, cipherText: Uint8Array<ArrayBufferLike>) {
const result = await decryptAES(vector, key, cipherText);
return EJSON.parse(new TextDecoder('UTF-8').decode(new Uint8Array(result)));
}

async decrypt(message: any) {
async decrypt(message: string) {
const keyID = message.slice(0, 12);
message = message.slice(12);

const [vector, cipherText] = splitVectorAndEcryptedData(Base64.decode(message));

let oldKey = '';
let oldKey = null;
if (keyID !== this.keyID) {
const oldRoomKey = this.oldKeys?.find((key: any) => key.e2eKeyId === keyID);
// Messages already contain a keyID stored with them
@@ -682,6 +724,9 @@
// but will be enough to help with some mobile issues.
if (!oldRoomKey) {
try {
if (!this.groupSessionKey) {
throw new Error('No group session key found.');
}
return await this.doDecrypt(vector, this.groupSessionKey, cipherText);
} catch (error) {
this.error('Error decrypting message: ', error, message);
@@ -692,14 +737,20 @@
}

try {
return await this.doDecrypt(vector, oldKey || this.groupSessionKey, cipherText);
if (oldKey) {
return await this.doDecrypt(vector, oldKey, cipherText);
}
if (!this.groupSessionKey) {
throw new Error('No group session key found.');
}
return await this.doDecrypt(vector, this.groupSessionKey, cipherText);
} catch (error) {
this.error('Error decrypting message: ', error, message);
return { msg: t('E2E_Key_Error') };
}
}

provideKeyToUser(keyId: any) {
provideKeyToUser(keyId: string) {
if (this.keyID !== keyId) {
return;
}
@@ -708,12 +759,12 @@
this.setState(E2ERoomState.READY);
}

onStateChange(cb: any) {
onStateChange(cb: () => void) {
this.on('STATE_CHANGED', cb);
return () => this.off('STATE_CHANGED', cb);
}

async encryptGroupKeyForParticipantsWaitingForTheKeys(users: any[]) {
async encryptGroupKeyForParticipantsWaitingForTheKeys(users: { _id: IUser['_id']; public_key: string }[]) {
if (!this.isReady()) {
return;
}
@@ -724,8 +775,11 @@
users.map(async (user) => {
const { _id, public_key } = user;
const key = await this.encryptGroupKeyForParticipant(public_key);
const oldKeys = await this.encryptOldKeysForParticipant(public_key, decryptedOldGroupKeys);
return { _id, key, ...(oldKeys && { oldKeys }) };
if (decryptedOldGroupKeys) {
const oldKeys = await this.encryptOldKeysForParticipant(public_key, decryptedOldGroupKeys);
return { _id, key, oldKeys };
}
return { _id, key };
}),
);

18 changes: 14 additions & 4 deletions apps/meteor/app/e2e/client/rocketchat.e2e.ts
Original file line number Diff line number Diff line change
@@ -239,7 +239,7 @@ class E2E extends Emitter {
return;
}

if (await e2eRoom.importGroupKey(sub.E2ESuggestedKey)) {
if (sub.E2ESuggestedKey && (await e2eRoom.importGroupKey(sub.E2ESuggestedKey))) {
this.log('Imported valid E2E suggested key');
await e2e.acceptSuggestedKey(sub.rid);
e2eRoom.keyReceived();
@@ -264,8 +264,9 @@ class E2E extends Emitter {
return null;
}

if (!this.instancesByRoomId[rid]) {
this.instancesByRoomId[rid] = new E2ERoom(Meteor.userId(), room);
const userId = Meteor.userId();
if (!this.instancesByRoomId[rid] && userId) {
this.instancesByRoomId[rid] = new E2ERoom(userId, room);
}

// When the key was already set and is changed via an update, we update the room instance
@@ -519,6 +520,9 @@ class E2E extends Emitter {

const vector = crypto.getRandomValues(new Uint8Array(16));
try {
if (!masterKey) {
throw new Error('Error getting master key');
}
const encodedPrivateKey = await encryptAES(vector, masterKey, toArrayBuffer(privateKey));

return EJSON.stringify(joinVectorAndEcryptedData(vector, encodedPrivateKey));
@@ -611,6 +615,9 @@ class E2E extends Emitter {
const [vector, cipherText] = splitVectorAndEcryptedData(EJSON.parse(this.db_private_key));

try {
if (!masterKey) {
throw new Error('Error getting master key');
}
const privKey = await decryptAES(vector, masterKey, cipherText);
const privateKey = toString(privKey) as string;

@@ -638,6 +645,9 @@ class E2E extends Emitter {
const [vector, cipherText] = splitVectorAndEcryptedData(EJSON.parse(privateKey));

try {
if (!masterKey) {
throw new Error('Error getting master key');
}
const privKey = await decryptAES(vector, masterKey, cipherText);
return toString(privKey);
} catch (error) {
@@ -673,7 +683,7 @@ class E2E extends Emitter {
return message;
}

const decryptedMessage: IE2EEMessage = await e2eRoom.decryptMessage(message);
const decryptedMessage = (await e2eRoom.decryptMessage(message)) as IE2EEMessage;

const decryptedMessageWithQuote = await this.parseQuoteAttachment(decryptedMessage);

4 changes: 2 additions & 2 deletions apps/meteor/client/lib/chats/flows/uploadFiles.ts
Original file line number Diff line number Diff line change
@@ -159,12 +159,12 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi
size: file.size,
// "format": "png"
},
];
] as IMessage['files'];

return e2eRoom.encryptMessageContent({
attachments,
files,
file: files[0],
file: files?.[0],
});
};

0 comments on commit 63df841

Please sign in to comment.