From b0ab2cc0ef2f90ca15b7bd9bb2761bf454a31a2d Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Thu, 9 Oct 2025 16:27:29 -0300 Subject: [PATCH 1/4] chore(federation): accept and store encrypted messages --- .../server/services/messages/service.ts | 13 +- .../federation-matrix/src/events/message.ts | 114 ++++++++++++++++++ .../src/types/IMessageService.ts | 9 +- 3 files changed, 132 insertions(+), 4 deletions(-) diff --git a/apps/meteor/server/services/messages/service.ts b/apps/meteor/server/services/messages/service.ts index ec28bc8ef6fe8..db522292182a8 100644 --- a/apps/meteor/server/services/messages/service.ts +++ b/apps/meteor/server/services/messages/service.ts @@ -88,8 +88,9 @@ export class MessageService extends ServiceClassInternal implements IMessageServ async saveMessageFromFederation({ fromId, rid, - msg, federation_event_id, + msg, + e2e_content, file, files, attachments, @@ -97,8 +98,12 @@ export class MessageService extends ServiceClassInternal implements IMessageServ }: { fromId: string; rid: string; - msg: string; federation_event_id: string; + msg?: string; + e2e_content?: { + algorithm: string, + ciphertext: string, + }, file?: IMessage['file']; files?: IMessage['files']; attachments?: IMessage['attachments']; @@ -115,6 +120,10 @@ export class MessageService extends ServiceClassInternal implements IMessageServ ...(file && { file }), ...(files && { files }), ...(attachments && { attachments }), + ...(e2e_content && { + t: 'e2e', + content: e2e_content, + }), }); } diff --git a/ee/packages/federation-matrix/src/events/message.ts b/ee/packages/federation-matrix/src/events/message.ts index aff347d2240f5..0b02324c8525c 100644 --- a/ee/packages/federation-matrix/src/events/message.ts +++ b/ee/packages/federation-matrix/src/events/message.ts @@ -262,6 +262,120 @@ export function message(emitter: Emitter, serverName: } }); + emitter.on('homeserver.matrix.encrypted', async (data) => { + try { + if (!data.content.ciphertext) { + logger.debug('No message content found in event'); + return; + } + + // at this point we know for sure the user already exists + const user = await Users.findOneByUsername(data.sender); + if (!user) { + throw new Error(`User not found for sender: ${data.sender}`); + } + + const room = await Rooms.findOne({ 'federation.mrid': data.room_id }); + if (!room) { + throw new Error(`No mapped room found for room_id: ${data.room_id}`); + } + + const relation = data.content['m.relates_to']; + + // SPEC: For example, an m.thread relationship type denotes that the event is part of a “thread” of messages and should be rendered as such. + const hasRelation = relation && 'rel_type' in relation; + + const isThreadMessage = hasRelation && relation.rel_type === 'm.thread'; + + const threadRootEventId = isThreadMessage && relation.event_id; + + // SPEC: Though rich replies form a relationship to another event, they do not use rel_type to create this relationship. + // Instead, a subkey named m.in_reply_to is used to describe the reply’s relationship, + const isRichReply = relation && !('rel_type' in relation) && 'm.in_reply_to' in relation; + + const quoteMessageEventId = isRichReply && relation['m.in_reply_to']?.event_id; + + const thread = threadRootEventId ? await getThreadMessageId(threadRootEventId) : undefined; + + const isEditedMessage = hasRelation && relation.rel_type === 'm.replace'; + if (isEditedMessage && relation.event_id) { + logger.debug('Received edited message from Matrix, updating existing message'); + const originalMessage = await Messages.findOneByFederationId(relation.event_id); + if (!originalMessage) { + logger.error('Original message not found for edit:', relation.event_id); + return; + } + if (originalMessage.federation?.eventId !== relation.event_id) { + return; + } + if (originalMessage.content?.ciphertext === data.content.ciphertext) { + logger.debug('No changes in message content, skipping update'); + return; + } + + if (quoteMessageEventId) { + await Message.updateMessage( + { + ...originalMessage, + content: { + algorithm: data.content.algorithm, + ciphertext: data.content.ciphertext, + }, + }, + user, + originalMessage, + ); + return; + } + + await Message.updateMessage( + { + ...originalMessage, + content: { + algorithm: data.content.algorithm, + ciphertext: data.content.ciphertext, + }, + }, + user, + originalMessage, + ); + return; + } + + if (quoteMessageEventId) { + const originalMessage = await Messages.findOneByFederationId(quoteMessageEventId); + if (!originalMessage) { + logger.error('Original message not found for quote:', quoteMessageEventId); + return; + } + await Message.saveMessageFromFederation({ + fromId: user._id, + rid: room._id, + e2e_content: { + algorithm: data.content.algorithm, + ciphertext: data.content.ciphertext, + }, + federation_event_id: data.event_id, + thread, + }); + return; + } + + await Message.saveMessageFromFederation({ + fromId: user._id, + rid: room._id, + e2e_content: { + algorithm: data.content.algorithm, + ciphertext: data.content.ciphertext, + }, + federation_event_id: data.event_id, + thread, + }); + } catch (error) { + logger.error(error, 'Error processing Matrix message:'); + } + }); + emitter.on('homeserver.matrix.redaction', async (data) => { try { const redactedEventId = data.redacts; diff --git a/packages/core-services/src/types/IMessageService.ts b/packages/core-services/src/types/IMessageService.ts index 793781bcfe070..646c575f61507 100644 --- a/packages/core-services/src/types/IMessageService.ts +++ b/packages/core-services/src/types/IMessageService.ts @@ -12,8 +12,9 @@ export interface IMessageService { saveMessageFromFederation({ fromId, rid, - msg, federation_event_id, + msg, + e2e_content, file, files, attachments, @@ -21,8 +22,12 @@ export interface IMessageService { }: { fromId: string; rid: string; - msg: string; federation_event_id: string; + msg?: string; + e2e_content?: { + algorithm: string, + ciphertext: string, + }, file?: IMessage['file']; files?: IMessage['files']; attachments?: IMessage['attachments']; From cb79ed788adcb9767df0f746fa28505294c024ef Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 9 Oct 2025 17:56:13 -0300 Subject: [PATCH 2/4] chore(deps): update @rocket.chat/federation-sdk to version 0.1.23 in package.json and yarn.lock --- ee/packages/federation-matrix/package.json | 2 +- packages/core-services/package.json | 2 +- yarn.lock | 12 ++++++------ 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/ee/packages/federation-matrix/package.json b/ee/packages/federation-matrix/package.json index 61bfb8f5b3da1..8b51664ef4290 100644 --- a/ee/packages/federation-matrix/package.json +++ b/ee/packages/federation-matrix/package.json @@ -38,7 +38,7 @@ "@rocket.chat/core-services": "workspace:^", "@rocket.chat/core-typings": "workspace:^", "@rocket.chat/emitter": "^0.31.25", - "@rocket.chat/federation-sdk": "0.1.21", + "@rocket.chat/federation-sdk": "0.1.23", "@rocket.chat/http-router": "workspace:^", "@rocket.chat/license": "workspace:^", "@rocket.chat/models": "workspace:^", diff --git a/packages/core-services/package.json b/packages/core-services/package.json index 72feb130e4c2c..975ce2958eb9b 100644 --- a/packages/core-services/package.json +++ b/packages/core-services/package.json @@ -35,7 +35,7 @@ }, "dependencies": { "@rocket.chat/core-typings": "workspace:^", - "@rocket.chat/federation-sdk": "0.1.21", + "@rocket.chat/federation-sdk": "0.1.23", "@rocket.chat/http-router": "workspace:^", "@rocket.chat/icons": "^0.43.0", "@rocket.chat/media-signaling": "workspace:^", diff --git a/yarn.lock b/yarn.lock index f619215824931..a715006cb4159 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7324,7 +7324,7 @@ __metadata: "@rocket.chat/apps-engine": "workspace:^" "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/eslint-config": "workspace:^" - "@rocket.chat/federation-sdk": "npm:0.1.21" + "@rocket.chat/federation-sdk": "npm:0.1.23" "@rocket.chat/http-router": "workspace:^" "@rocket.chat/icons": "npm:^0.43.0" "@rocket.chat/jest-presets": "workspace:~" @@ -7535,7 +7535,7 @@ __metadata: "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/emitter": "npm:^0.31.25" "@rocket.chat/eslint-config": "workspace:^" - "@rocket.chat/federation-sdk": "npm:0.1.21" + "@rocket.chat/federation-sdk": "npm:0.1.23" "@rocket.chat/http-router": "workspace:^" "@rocket.chat/license": "workspace:^" "@rocket.chat/models": "workspace:^" @@ -7561,9 +7561,9 @@ __metadata: languageName: unknown linkType: soft -"@rocket.chat/federation-sdk@npm:0.1.21": - version: 0.1.21 - resolution: "@rocket.chat/federation-sdk@npm:0.1.21" +"@rocket.chat/federation-sdk@npm:0.1.23": + version: 0.1.23 + resolution: "@rocket.chat/federation-sdk@npm:0.1.23" dependencies: "@datastructures-js/priority-queue": "npm:^6.3.3" "@noble/ed25519": "npm:^3.0.0" @@ -7576,7 +7576,7 @@ __metadata: zod: "npm:^3.22.4" peerDependencies: typescript: ~5.9.2 - checksum: 10/348ca6461759434132c6ca1ba92bdda698f7ef4605c33d2479491c518b5b73fe80d7f9c9d51e93877fe377120e2f31497fb492bf82be40bed92c88f31e5ec5af + checksum: 10/8d475d7f7d30cb8dc5db5239c13d0ee39a11fc24b5e9ece460334abdfb8b5fde0321dd6bef16470b22d09a4451327e3ab71052b09db895a103477c57e8458f2c languageName: node linkType: hard From 23dcbd830a5200fb2db27e7270acc3dbda2391d8 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 9 Oct 2025 17:56:30 -0300 Subject: [PATCH 3/4] fix(federation): prevent processing for non-local users in room event handling --- apps/meteor/server/services/messages/service.ts | 6 +++--- ee/packages/federation-matrix/src/events/room.ts | 5 +++++ packages/core-services/src/types/IMessageService.ts | 6 +++--- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/apps/meteor/server/services/messages/service.ts b/apps/meteor/server/services/messages/service.ts index db522292182a8..d48b97c040e02 100644 --- a/apps/meteor/server/services/messages/service.ts +++ b/apps/meteor/server/services/messages/service.ts @@ -101,9 +101,9 @@ export class MessageService extends ServiceClassInternal implements IMessageServ federation_event_id: string; msg?: string; e2e_content?: { - algorithm: string, - ciphertext: string, - }, + algorithm: string; + ciphertext: string; + }; file?: IMessage['file']; files?: IMessage['files']; attachments?: IMessage['attachments']; diff --git a/ee/packages/federation-matrix/src/events/room.ts b/ee/packages/federation-matrix/src/events/room.ts index a71d1751c5e73..369bd8193235e 100644 --- a/ee/packages/federation-matrix/src/events/room.ts +++ b/ee/packages/federation-matrix/src/events/room.ts @@ -53,6 +53,11 @@ export function room(emitter: Emitter, services: Home const [allegedUsernameLocal, , allegedUserLocalIsLocal] = getUsernameServername(userId, services.config.serverName); const localUserId = allegedUserLocalIsLocal && (await Users.findOneByUsername(allegedUsernameLocal, { projection: { _id: 1 } })); + + if (!allegedUserLocalIsLocal) { + return; + } + if (!localUserId) { throw new Error('mapped user not found'); } diff --git a/packages/core-services/src/types/IMessageService.ts b/packages/core-services/src/types/IMessageService.ts index 646c575f61507..5532a553f6d6f 100644 --- a/packages/core-services/src/types/IMessageService.ts +++ b/packages/core-services/src/types/IMessageService.ts @@ -25,9 +25,9 @@ export interface IMessageService { federation_event_id: string; msg?: string; e2e_content?: { - algorithm: string, - ciphertext: string, - }, + algorithm: string; + ciphertext: string; + }; file?: IMessage['file']; files?: IMessage['files']; attachments?: IMessage['attachments']; From f974305100de6b629d6d8f5e74fb6aa142e408e1 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 9 Oct 2025 19:35:42 -0300 Subject: [PATCH 4/4] fix(federation): correct backoff delay calculation in error handling --- ee/packages/federation-matrix/src/api/_matrix/invite.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ee/packages/federation-matrix/src/api/_matrix/invite.ts b/ee/packages/federation-matrix/src/api/_matrix/invite.ts index 7467e321db8a5..ce72f74418366 100644 --- a/ee/packages/federation-matrix/src/api/_matrix/invite.ts +++ b/ee/packages/federation-matrix/src/api/_matrix/invite.ts @@ -144,8 +144,8 @@ async function runWithBackoff(fn: () => Promise, delaySec = 5) { try { await fn(); } catch (e) { - const delay = Math.max(625, delaySec ** 2); - console.error(`error occurred, retrying in ${delay}ms`, e); + const delay = Math.min(625, delaySec ** 2); + console.error(`error occurred, retrying in ${delay}s`, e); setTimeout(() => { runWithBackoff(fn, delay); }, delay * 1000);