Skip to content
Open
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
4 changes: 2 additions & 2 deletions app/definitions/IAttachment.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { IUser } from './IUser';
import { E2EType, IAttachmentTranslations, IMessageE2EEContent } from './IMessage';
import { E2EType, IAttachmentTranslations, TEncryptedContent } from './IMessage';

export type TAttachmentEncryption = {
iv: string;
Expand Down Expand Up @@ -72,7 +72,7 @@ export interface IServerAttachment {
uploading: boolean;
url: string;
user: Pick<IUser, '_id' | 'username' | 'name'>;
content?: IMessageE2EEContent;
content?: TEncryptedContent;
}

export interface IShareAttachment {
Expand Down
29 changes: 25 additions & 4 deletions app/definitions/IMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,31 @@ interface IMessageFile {
type: string;
}

export type IMessageE2EEContent = {
interface IEncryptedContent {
/**
* The encryption algorithm used.
* Currently supported algorithms are:
* - `rc.v1.aes-sha2`: Rocket.Chat E2E Encryption version 1, using AES encryption with SHA-256 hashing.
* - `rc.v2.aes-sha2`: Rocket.Chat E2E Encryption version 2, using AES encryption with SHA-256 hashing and improved key management.
*/
algorithm: string;
ciphertext: string; // base64-encoded encrypted subset JSON of IMessage
}

interface IEncryptedContentV1 extends IEncryptedContent {
/**
* The encryption algorithm used.
*/
algorithm: 'rc.v1.aes-sha2';
ciphertext: string; // Encrypted subset JSON of IMessage
};
}

interface IEncryptedContentV2 extends IEncryptedContent {
algorithm: 'rc.v2.aes-sha2';
iv: string; // base64-encoded initialization vector
kid: string; // ID of the key used to encrypt the message
}

export type TEncryptedContent = IEncryptedContentV1 | IEncryptedContentV2;
export interface IMessageFromServer {
_id: string;
rid: string;
Expand Down Expand Up @@ -112,7 +132,7 @@ export interface IMessageFromServer {
username: string;
};
score?: number;
content?: IMessageE2EEContent;
content?: TEncryptedContent;
}

export interface ILoadMoreMessage {
Expand Down Expand Up @@ -148,6 +168,7 @@ export interface IMessage extends IMessageFromServer {
tmsg?: string;
blocks?: any;
e2e?: E2EType;
e2eMentions?: { e2eUserMentions?: string[]; e2eChannelMentions?: string[] };
tshow?: boolean;
comment?: string;
subscription?: { id: string };
Expand Down
10 changes: 8 additions & 2 deletions app/definitions/rest/v1/chat.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { IMessage, IMessageFromServer, IReadReceipts } from '../../IMessage';
import type { TEncryptedContent, IMessage, IMessageFromServer, IReadReceipts } from '../../IMessage';
import type { IServerRoom } from '../../IRoom';
import { PaginatedResult } from '../helpers/PaginatedResult';

Expand Down Expand Up @@ -75,7 +75,13 @@ export type ChatEndpoints = {
};
};
'chat.update': {
POST: (params: { roomId: IServerRoom['_id']; msgId: string; text: string }) => {
POST: (params: {
roomId: IServerRoom['_id'];
msgId: string;
text?: string;
content?: TEncryptedContent;
e2eMentions?: IMessage['e2eMentions'];
}) => {
messages: IMessageFromServer;
};
};
Expand Down
4 changes: 4 additions & 0 deletions app/lib/encryption/room.ts
Original file line number Diff line number Diff line change
Expand Up @@ -645,6 +645,10 @@ export default class EncryptionRoom {
e2e: 'pending'
}))
};

if (content.text) {
message.msg = content.text;
}
}

const decryptedMessage: IMessage = {
Expand Down
24 changes: 21 additions & 3 deletions app/lib/services/restApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -963,10 +963,28 @@ export function e2eResetRoomKey(rid: string, e2eKey: string, e2eKeyId: string):
return sdk.post('e2e.resetRoomKey', { rid, e2eKey, e2eKeyId });
}

export const editMessage = async (message: Pick<IMessage, 'id' | 'msg' | 'rid'>) => {
const { rid, msg } = await Encryption.encryptMessage(message as IMessage);
export const editMessage = async (message: Pick<IMessage, 'id' | 'msg' | 'rid' | 'content' | 'e2eMentions'>) => {
const result = await Encryption.encryptMessage(message as IMessage);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tested it on the Mobile workspace (7.9.0), and editMessage doesn’t work, encryptMessage always returns content and fails on chat.update.
Screenshot 2025-10-14 at 17 30 33

if (!result) {
throw new Error('Failed to encrypt message');
}

if (result.content) {
// RC 0.49.0
return sdk.post('chat.update', {
roomId: message.rid,
msgId: message.id,
content: result.content,
e2eMentions: result.e2eMentions
});
}

// RC 0.49.0
return sdk.post('chat.update', { roomId: rid, msgId: message.id, text: msg });
return sdk.post('chat.update', {
roomId: message.rid,
msgId: message.id,
text: message.msg || ''
});
};
Comment on lines +966 to 988
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add fallback for servers that don’t support content in chat.update.

Currently, edits in E2EE rooms may fail on older servers (where text is required). Try content first, then fallback to text with the ciphertext.

 export const editMessage = async (message: Pick<IMessage, 'id' | 'msg' | 'rid' | 'content' | 'e2eMentions'>) => {
   const result = await Encryption.encryptMessage(message as IMessage);
   if (!result) {
     throw new Error('Failed to encrypt message');
   }

   if (result.content) {
-    // RC 0.49.0
-    return sdk.post('chat.update', {
-      roomId: message.rid,
-      msgId: message.id,
-      content: result.content,
-      e2eMentions: result.e2eMentions
-    });
+    try {
+      // Prefer new API with encrypted content
+      return await sdk.post('chat.update', {
+        roomId: message.rid,
+        msgId: message.id,
+        content: result.content,
+        e2eMentions: result.e2eMentions
+      });
+    } catch (e) {
+      // Fallback for older servers: send ciphertext as text
+      const ciphertext = (result as any).msg ?? result.content.ciphertext ?? message.msg ?? '';
+      return sdk.post('chat.update', {
+        roomId: message.rid,
+        msgId: message.id,
+        text: ciphertext
+      });
+    }
   }

   // RC 0.49.0
   return sdk.post('chat.update', {
     roomId: message.rid,
     msgId: message.id,
     text: message.msg || ''
   });
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const editMessage = async (message: Pick<IMessage, 'id' | 'msg' | 'rid' | 'content' | 'e2eMentions'>) => {
const result = await Encryption.encryptMessage(message as IMessage);
if (!result) {
throw new Error('Failed to encrypt message');
}
if (result.content) {
// RC 0.49.0
return sdk.post('chat.update', {
roomId: message.rid,
msgId: message.id,
content: result.content,
e2eMentions: result.e2eMentions
});
}
// RC 0.49.0
return sdk.post('chat.update', { roomId: rid, msgId: message.id, text: msg });
return sdk.post('chat.update', {
roomId: message.rid,
msgId: message.id,
text: message.msg || ''
});
};
export const editMessage = async (message: Pick<IMessage, 'id' | 'msg' | 'rid' | 'content' | 'e2eMentions'>) => {
const result = await Encryption.encryptMessage(message as IMessage);
if (!result) {
throw new Error('Failed to encrypt message');
}
if (result.content) {
try {
// Prefer new API with encrypted content
return await sdk.post('chat.update', {
roomId: message.rid,
msgId: message.id,
content: result.content,
e2eMentions: result.e2eMentions
});
} catch (e) {
// Fallback for older servers: send ciphertext as text
const ciphertext = (result as any).msg ?? result.content.ciphertext ?? message.msg ?? '';
return sdk.post('chat.update', {
roomId: message.rid,
msgId: message.id,
text: ciphertext
});
}
}
// RC 0.49.0
return sdk.post('chat.update', {
roomId: message.rid,
msgId: message.id,
text: message.msg || ''
});
};


export const registerPushToken = () =>
Expand Down
Loading