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

fix: Handle encrypted pinned messages #32380

Merged
merged 13 commits into from
Jun 22, 2024
6 changes: 6 additions & 0 deletions .changeset/forty-bikes-check.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@rocket.chat/core-typings': patch
'@rocket.chat/meteor': patch
---

Decrypt pinned encrypted messages in the chat and pinned messages contextual bar.
27 changes: 26 additions & 1 deletion apps/meteor/app/e2e/client/rocketchat.e2e.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import QueryString from 'querystring';
import URL from 'url';

import type { IE2EEMessage, IMessage, IRoom, ISubscription, IUploadWithUser } from '@rocket.chat/core-typings';
import type { IE2EEMessage, IMessage, IRoom, ISubscription, IUploadWithUser, MessageAttachment } from '@rocket.chat/core-typings';
import { isE2EEMessage } from '@rocket.chat/core-typings';
import { Emitter } from '@rocket.chat/emitter';
import EJSON from 'ejson';
Expand Down Expand Up @@ -550,6 +550,31 @@ class E2E extends Emitter {
return decryptedMessageWithQuote;
}

async decryptPinnedMessage(message: IMessage) {
yash-rajpal marked this conversation as resolved.
Show resolved Hide resolved
const pinnedMessage = message?.attachments?.[0]?.text;

if (!pinnedMessage) {
return message;
}

const e2eRoom = await this.getInstanceByRoomId(message.rid);

if (!e2eRoom) {
return message;
}

const data = await e2eRoom.decrypt(pinnedMessage);

if (!data) {
return message;
}

const decryptedPinnedMessage = { ...message } as IMessage & { attachments: MessageAttachment[] };
decryptedPinnedMessage.attachments[0].text = data.text;
yash-rajpal marked this conversation as resolved.
Show resolved Hide resolved

return decryptedPinnedMessage;
}

async decryptPendingMessages(): Promise<void> {
return Messages.find({ t: 'e2e', e2e: 'pending' }).forEach(async ({ _id, ...msg }: IMessage) => {
Messages.update({ _id }, await this.decryptMessage(msg as IE2EEMessage));
Expand Down
4 changes: 3 additions & 1 deletion apps/meteor/app/message-pin/server/pinMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,9 @@ Meteor.methods<ServerMethods>({
// App IPostMessagePinned event hook
await Apps.self?.triggerEvent(AppEvents.IPostMessagePinned, originalMessage, await Meteor.userAsync(), originalMessage.pinned);

const msgId = await Message.saveSystemMessage('message_pinned', originalMessage.rid, '', me, {
const pinMessageType = originalMessage.t === 'e2e' ? 'message_pinned_e2e' : 'message_pinned';

const msgId = await Message.saveSystemMessage(pinMessageType, originalMessage.rid, '', me, {
attachments: [
{
text: originalMessage.msg,
Expand Down
4 changes: 2 additions & 2 deletions apps/meteor/client/components/message/StatusIndicators.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { IMessage, ITranslatedMessage } from '@rocket.chat/core-typings';
import { isEditedMessage, isE2EEMessage, isOTRMessage, isOTRAckMessage } from '@rocket.chat/core-typings';
import { isEditedMessage, isE2EEMessage, isOTRMessage, isOTRAckMessage, isE2EEPinnedMessage } from '@rocket.chat/core-typings';
import { MessageStatusIndicator, MessageStatusIndicatorItem } from '@rocket.chat/fuselage';
import { useUserId, useTranslation } from '@rocket.chat/ui-contexts';
import type { ReactElement } from 'react';
Expand All @@ -17,7 +17,7 @@ const StatusIndicators = ({ message }: StatusIndicatorsProps): ReactElement => {
const starred = useShowStarred({ message });
const following = useShowFollowing({ message });

const isEncryptedMessage = isE2EEMessage(message);
const isEncryptedMessage = isE2EEMessage(message) || isE2EEPinnedMessage(message);
const isOtrMessage = isOTRMessage(message) || isOTRAckMessage(message);

const uid = useUserId();
Expand Down
6 changes: 6 additions & 0 deletions apps/meteor/client/startup/e2e.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { IMessage, ISubscription } from '@rocket.chat/core-typings';
import { isE2EEPinnedMessage } from '@rocket.chat/core-typings';
import { Meteor } from 'meteor/meteor';
import { Tracker } from 'meteor/tracker';

Expand Down Expand Up @@ -125,6 +126,11 @@ Meteor.startup(() => {
if (!e2eRoom?.shouldConvertReceivedMessages()) {
return msg;
}

if (isE2EEPinnedMessage(msg)) {
return e2e.decryptPinnedMessage(msg);
}

return e2e.decryptMessage(msg);
});

Expand Down
6 changes: 6 additions & 0 deletions apps/meteor/client/startup/messageTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,10 @@ Meteor.startup(() => {
system: true,
message: 'Pinned_a_message',
});

MessageTypes.registerType({
id: 'message_pinned_e2e',
system: true,
message: 'Pinned_a_message',
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useQuery } from '@tanstack/react-query';
import type { ReactElement } from 'react';
import React from 'react';

import { onClientMessageReceived } from '../../../lib/onClientMessageReceived';
import { mapMessageFromApi } from '../../../lib/utils/mapMessageFromApi';
import { useRoom } from '../contexts/RoomContext';
import MessageListTab from './MessageListTab';
Expand All @@ -25,7 +26,7 @@ const PinnedMessagesTab = (): ReactElement => {
messages.push(...result.messages.map(mapMessageFromApi));
}

return messages;
return Promise.all(messages.map(onClientMessageReceived));
});

const t = useTranslation();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useEndpoint, useTranslation } from '@rocket.chat/ui-contexts';
import { useQuery } from '@tanstack/react-query';
import React from 'react';

import { onClientMessageReceived } from '../../../lib/onClientMessageReceived';
import { mapMessageFromApi } from '../../../lib/utils/mapMessageFromApi';
import { useRoom } from '../contexts/RoomContext';
import MessageListTab from './MessageListTab';
Expand All @@ -24,7 +25,7 @@ const StarredMessagesTab = () => {
messages.push(...result.messages.map(mapMessageFromApi));
}

return messages;
return Promise.all(messages.map(onClientMessageReceived));
});

const t = useTranslation();
Expand Down
45 changes: 45 additions & 0 deletions apps/meteor/tests/e2e/e2e-encryption.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,51 @@ test.describe.serial('e2e-encryption', () => {
await expect(sidebarChannel.locator('span')).toContainText(encriptedMessage1);
});

test('expect create a private encrypted channel and pin/star an encrypted message', async ({ page }) => {
const channelName = faker.string.uuid();

await poHomeChannel.sidenav.createEncryptedChannel(channelName);

await expect(page).toHaveURL(`/group/${channelName}`);

await poHomeChannel.dismissToast();

await expect(poHomeChannel.content.encryptedRoomHeaderIcon).toBeVisible();

await poHomeChannel.content.sendMessage('This message should be pinned and stared.');

await expect(poHomeChannel.content.lastUserMessageBody).toHaveText('This message should be pinned and stared.');
await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).toBeVisible();

await poHomeChannel.content.openLastMessageMenu();
await page.locator('role=menuitem[name="Star"]').click();

await expect(poHomeChannel.toastSuccess).toBeVisible();
await poHomeChannel.dismissToast();

await poHomeChannel.content.openLastMessageMenu();
await page.locator('role=menuitem[name="Pin"]').click();
await page.locator('#modal-root >> button:has-text("Yes, pin message")').click();

await poHomeChannel.tabs.kebab.click();
await poHomeChannel.tabs.btnPinnedMessagesList.click();

await expect(page.getByRole('dialog', { name: 'Pinned Messages' })).toBeVisible();
await expect(page.getByRole('dialog', { name: 'Pinned Messages' }).locator('[data-qa-type="message"]').last()).toContainText(
'This message should be pinned and stared.',
);

await poHomeChannel.btnContextualbarClose.click();

await poHomeChannel.tabs.kebab.click();
await poHomeChannel.tabs.btnStarredMessageList.click();

await expect(page.getByRole('dialog', { name: 'Starred Messages' })).toBeVisible();
await expect(page.getByRole('dialog', { name: 'Starred Messages' }).locator('[data-qa-type="message"]').last()).toContainText(
'This message should be pinned and stared.',
);
});

test.describe('reset keys', () => {
let anotherClientPage: Page;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,12 @@ export class HomeFlextab {
get userInfoUsername(): Locator {
return this.page.locator('[data-qa="UserInfoUserName"]');
}

get btnPinnedMessagesList(): Locator {
return this.page.locator('[data-key="pinned-messages"]');
}

get btnStarredMessageList(): Locator {
return this.page.locator('[data-key="starred-messages"]');
}
}
6 changes: 6 additions & 0 deletions packages/core-typings/src/IMessage/IMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ export type MessageTypesValues =
| 'command'
| 'videoconf'
| 'message_pinned'
| 'message_pinned_e2e'
| 'new-moderator'
| 'moderator-removed'
| 'new-owner'
Expand Down Expand Up @@ -364,6 +365,10 @@ export type IE2EEMessage = IMessage & {
e2e: 'pending' | 'done';
};

export type IE2EEPinnedMessage = IMessage & {
t: 'message_pinned_e2e';
};

export interface IOTRMessage extends IMessage {
t: 'otr';
otrAck?: string;
Expand All @@ -378,6 +383,7 @@ export type IVideoConfMessage = IMessage & {
};

export const isE2EEMessage = (message: IMessage): message is IE2EEMessage => message.t === 'e2e';
export const isE2EEPinnedMessage = (message: IMessage): message is IE2EEPinnedMessage => message.t === 'message_pinned_e2e';
export const isOTRMessage = (message: IMessage): message is IOTRMessage => message.t === 'otr';
export const isOTRAckMessage = (message: IMessage): message is IOTRAckMessage => message.t === 'otr-ack';
export const isVideoConfMessage = (message: IMessage): message is IVideoConfMessage => message.t === 'videoconf';
Expand Down
Loading