diff --git a/.changeset/flat-tables-applaud.md b/.changeset/flat-tables-applaud.md new file mode 100644 index 0000000000000..457ad7aeaac72 --- /dev/null +++ b/.changeset/flat-tables-applaud.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/core-typings': patch +'@rocket.chat/meteor': patch +--- + +Fixes association of encrypted messages and encrypted files, so that if one of them is removed, the other gets removed as well. diff --git a/apps/meteor/app/api/server/v1/rooms.ts b/apps/meteor/app/api/server/v1/rooms.ts index 48bc71ca5e491..686f76a7476c6 100644 --- a/apps/meteor/app/api/server/v1/rooms.ts +++ b/apps/meteor/app/api/server/v1/rooms.ts @@ -271,7 +271,7 @@ API.v1.addRoute( delete this.bodyParams.description; await applyAirGappedRestrictionsValidation(() => - sendFileMessage(this.userId, { roomId: this.urlParams.rid, file, msgData: this.bodyParams }, { parseAttachmentsForE2EE: false }), + sendFileMessage(this.userId, { roomId: this.urlParams.rid, file, msgData: this.bodyParams }), ); await Uploads.confirmTemporaryFile(this.urlParams.fileId, this.userId); diff --git a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts index ba372a45707cf..ad5c5b4d4e1bf 100644 --- a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts +++ b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts @@ -166,13 +166,6 @@ export const sendFileMessage = async ( file: Partial; msgData?: Record; }, - { - parseAttachmentsForE2EE, - }: { - parseAttachmentsForE2EE: boolean; - } = { - parseAttachmentsForE2EE: true, - }, ): Promise => { const user = await Users.findOneById(userId, { projection: { services: 0 } }); @@ -220,12 +213,10 @@ export const sendFileMessage = async ( groupable: msgData?.groupable ?? false, }; - if (parseAttachmentsForE2EE || msgData?.t !== 'e2e') { - const { files, attachments } = await parseFileIntoMessageAttachments(file, roomId, user); - data.file = files[0]; - data.files = files; - data.attachments = attachments; - } + const { files, attachments } = await parseFileIntoMessageAttachments(file, roomId, user); + data.file = files[0]; + data.files = files; + data.attachments = attachments; const msg = await executeSendMessage(userId, data); diff --git a/apps/meteor/app/lib/server/functions/cleanRoomHistory.ts b/apps/meteor/app/lib/server/functions/cleanRoomHistory.ts index 8d61c0c51088b..2bea0914ee00f 100644 --- a/apps/meteor/app/lib/server/functions/cleanRoomHistory.ts +++ b/apps/meteor/app/lib/server/functions/cleanRoomHistory.ts @@ -48,7 +48,8 @@ export async function cleanRoomHistory({ }); const targetMessageIdsForAttachmentRemoval = new Set(); - const pruneMessageAttachment = { color: NOTIFICATION_ATTACHMENT_COLOR, text }; + // Since we remove every file from the messages, we don't need to specify which fileId has been removed. + const pruneMessageAttachment = { type: 'removed-file', color: NOTIFICATION_ATTACHMENT_COLOR, text }; async function performFileAttachmentCleanupBatch() { if (targetMessageIdsForAttachmentRemoval.size === 0) return; diff --git a/apps/meteor/app/lib/server/functions/notifications/email.js b/apps/meteor/app/lib/server/functions/notifications/email.js index 07cc5c949121b..a27699bc1d111 100644 --- a/apps/meteor/app/lib/server/functions/notifications/email.js +++ b/apps/meteor/app/lib/server/functions/notifications/email.js @@ -54,7 +54,7 @@ export async function getEmailContent({ message, user, room }) { }); } - if (message.t === 'e2e' && !message.file && !message.files?.length) { + if (message.t === 'e2e') { return settings.get('Email_notification_show_message') ? i18n.t('Encrypted_message_preview_unavailable', { lng }) : header; } diff --git a/apps/meteor/app/ui-utils/client/lib/RoomHistoryManager.ts b/apps/meteor/app/ui-utils/client/lib/RoomHistoryManager.ts index f0f207b55cffe..f5008cfa733af 100644 --- a/apps/meteor/app/ui-utils/client/lib/RoomHistoryManager.ts +++ b/apps/meteor/app/ui-utils/client/lib/RoomHistoryManager.ts @@ -23,7 +23,7 @@ const processMessage = async (msg: IMessage & { ignored?: boolean }, { subscript msg.ignored = true; } - if (msg.t === 'e2e' && !msg.file) { + if (msg.t === 'e2e') { msg.e2e = 'pending'; } diff --git a/apps/meteor/client/lib/chats/flows/uploadFiles.ts b/apps/meteor/client/lib/chats/flows/uploadFiles.ts index 2b7a228579182..fee271eebbf3d 100644 --- a/apps/meteor/client/lib/chats/flows/uploadFiles.ts +++ b/apps/meteor/client/lib/chats/flows/uploadFiles.ts @@ -115,6 +115,7 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi hashes: { sha256: encryptedFile.hash, }, + fileId: _id, }; if (/^image\/.+/.test(file.type)) { diff --git a/apps/meteor/client/lib/e2ee/rocketchat.e2e.room.ts b/apps/meteor/client/lib/e2ee/rocketchat.e2e.room.ts index 9d611183e0a31..625e305365af1 100644 --- a/apps/meteor/client/lib/e2ee/rocketchat.e2e.room.ts +++ b/apps/meteor/client/lib/e2ee/rocketchat.e2e.room.ts @@ -9,7 +9,7 @@ import type { EncryptedMessageContent, EncryptedContent, } from '@rocket.chat/core-typings'; -import { isEncryptedMessageContent } from '@rocket.chat/core-typings'; +import { isEncryptedMessageContent, isFileAttachment, isRemovedFileAttachment } from '@rocket.chat/core-typings'; import { Emitter } from '@rocket.chat/emitter'; import type { Optional } from '@tanstack/react-query'; import EJSON from 'ejson'; @@ -660,6 +660,45 @@ export class E2ERoom extends Emitter { return data; } + async decryptMessageContent(message: IE2EEMessage): Promise { + const { attachments } = message; + const deletedAttachments = attachments?.filter((att) => isRemovedFileAttachment(att)) || []; + const deletedAllAttachment = deletedAttachments.find((att) => !att.fileId); + + const content = await this.decrypt(message.content); + Object.assign(message, content); + + // If the encrypted message had deleted files and the decrypted message has files, compare both lists to remove from the final result any file that was flagged as deleted + if (!deletedAttachments.length || !message.attachments?.length || !content.attachments?.length) { + return message; + } + + message.attachments = message.attachments.map((att) => { + if (!isFileAttachment(att)) { + return att; + } + + if (deletedAllAttachment) { + return deletedAllAttachment; + } + + const fileId = att.fileId || message.file?._id; + if (!fileId) { + return att; + } + + for (const removedAttachment of deletedAttachments) { + if (removedAttachment.fileId === fileId) { + return removedAttachment; + } + } + + return att; + }); + + return message; + } + // Decrypt messages async decryptMessage(message: IMessage | IE2EEMessage): Promise { if (message.t !== 'e2e' || message.e2e === 'done') { @@ -668,7 +707,7 @@ export class E2ERoom extends Emitter { if (isEncryptedMessageContent(message)) { return { - ...(await this.decryptContent({ ...message })), + ...(await this.decryptMessageContent({ ...message })), e2e: 'done' as const, }; } diff --git a/apps/meteor/server/services/upload/service.ts b/apps/meteor/server/services/upload/service.ts index f24a084837923..e4cfb5f8c3c2b 100644 --- a/apps/meteor/server/services/upload/service.ts +++ b/apps/meteor/server/services/upload/service.ts @@ -107,7 +107,7 @@ export class UploadService extends ServiceClassInternal implements IUploadServic private async updateMessageRemovingFiles(msg: IMessage, filesToRemove: IUpload['_id'][], user: IUser): Promise { const text = `_${i18n.t('File_removed')}_`; - const newAttachment = { color: NOTIFICATION_ATTACHMENT_COLOR, text }; + const color = NOTIFICATION_ATTACHMENT_COLOR; const newFiles = msg.files?.filter((file) => !filesToRemove.includes(file._id)); const newAttachments = msg.attachments?.map((attachment) => { @@ -120,7 +120,7 @@ export class UploadService extends ServiceClassInternal implements IUploadServic return attachment; } - return newAttachment; + return { type: 'removed-file', color, text, ...(attachment.fileId && { fileId: attachment.fileId }) }; }); const newFile = msg.file?._id && !filesToRemove.includes(msg.file._id) ? msg.file : newFiles?.[0]; diff --git a/packages/core-typings/src/IMessage/MessageAttachment/Files/RemovedFileAttachmentProps.ts b/packages/core-typings/src/IMessage/MessageAttachment/Files/RemovedFileAttachmentProps.ts new file mode 100644 index 0000000000000..a456521f48dcc --- /dev/null +++ b/packages/core-typings/src/IMessage/MessageAttachment/Files/RemovedFileAttachmentProps.ts @@ -0,0 +1,11 @@ +import type { MessageAttachmentBase } from '../MessageAttachmentBase'; +import type { FileProp } from './FileProp'; + +export type RemovedFileAttachmentProps = MessageAttachmentBase & { + type: 'removed-file'; + /* If fileId is missing, then every file in the message has been removed */ + fileId?: FileProp['_id']; +}; + +export const isRemovedFileAttachment = (attachment: MessageAttachmentBase): attachment is RemovedFileAttachmentProps => + 'type' in attachment && attachment.type === 'removed-file'; diff --git a/packages/core-typings/src/IMessage/MessageAttachment/Files/index.ts b/packages/core-typings/src/IMessage/MessageAttachment/Files/index.ts index 5d33ee5cdb9d1..119cb1e7518e7 100644 --- a/packages/core-typings/src/IMessage/MessageAttachment/Files/index.ts +++ b/packages/core-typings/src/IMessage/MessageAttachment/Files/index.ts @@ -2,4 +2,5 @@ export * from './AudioAttachmentProps'; export * from './FileAttachmentProps'; export * from './FileProp'; export * from './ImageAttachmentProps'; +export * from './RemovedFileAttachmentProps'; export * from './VideoAttachmentProps'; diff --git a/packages/core-typings/src/IMessage/MessageAttachment/MessageAttachment.ts b/packages/core-typings/src/IMessage/MessageAttachment/MessageAttachment.ts index 8264b31ba6402..edf668fd70ae9 100644 --- a/packages/core-typings/src/IMessage/MessageAttachment/MessageAttachment.ts +++ b/packages/core-typings/src/IMessage/MessageAttachment/MessageAttachment.ts @@ -1,10 +1,16 @@ import type { FileProp } from './Files'; import type { FileAttachmentProps } from './Files/FileAttachmentProps'; +import type { RemovedFileAttachmentProps } from './Files/RemovedFileAttachmentProps'; import type { MessageAttachmentAction } from './MessageAttachmentAction'; import type { MessageAttachmentDefault } from './MessageAttachmentDefault'; import type { MessageQuoteAttachment } from './MessageQuoteAttachment'; -export type MessageAttachment = MessageAttachmentAction | MessageAttachmentDefault | FileAttachmentProps | MessageQuoteAttachment; +export type MessageAttachment = + | MessageAttachmentAction + | MessageAttachmentDefault + | FileAttachmentProps + | MessageQuoteAttachment + | RemovedFileAttachmentProps; export type FilesAndAttachments = { files: FileProp[];