Skip to content

Commit

Permalink
feat: Setting for enabling files encryption and fix whitelist media t…
Browse files Browse the repository at this point in the history
…ypes stopping E2EE uploads (#33003)

Co-authored-by: Kevin Aleman <[email protected]>
  • Loading branch information
yash-rajpal and KevLehman authored Aug 23, 2024
1 parent c225451 commit 58c0efc
Show file tree
Hide file tree
Showing 10 changed files with 286 additions and 2 deletions.
7 changes: 7 additions & 0 deletions .changeset/stupid-fishes-relate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@rocket.chat/core-typings': minor
'@rocket.chat/i18n': minor
'@rocket.chat/meteor': minor
---

Added a new setting to enable/disable file encryption in an end to end encrypted room.
15 changes: 15 additions & 0 deletions .changeset/violet-radios-begin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
'@rocket.chat/core-typings': minor
'@rocket.chat/i18n': minor
'@rocket.chat/meteor': minor
---

Fixed a bug related to uploading end to end encrypted file.

E2EE files and uploads are uploaded as files of mime type `application/octet-stream` as we can't reveal the mime type of actual content since it is encrypted and has to be kept confidential.

The server resolves the mime type of encrypted file as `application/octet-stream` but it wasn't playing nicely with existing settings related to whitelisted and blacklisted media types.

E2EE files upload was getting blocked if `application/octet-stream` is not a whitelisted media type.

Now this PR solves this issue by always accepting E2EE uploads even if `application/octet-stream` is not whitelisted but it will block the upload if `application/octet-stream` is black listed.
10 changes: 8 additions & 2 deletions apps/meteor/app/file-upload/server/lib/FileUpload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import URL from 'url';
import { hashLoginToken } from '@rocket.chat/account-utils';
import { Apps, AppEvents } from '@rocket.chat/apps';
import { AppsEngineException } from '@rocket.chat/apps-engine/definition/exceptions';
import type { IUpload } from '@rocket.chat/core-typings';
import { isE2EEUpload, type IUpload } from '@rocket.chat/core-typings';
import { Users, Avatars, UserDataFiles, Uploads, Settings, Subscriptions, Messages, Rooms } from '@rocket.chat/models';
import type { NextFunction } from 'connect';
import filesize from 'filesize';
Expand Down Expand Up @@ -170,7 +170,13 @@ export const FileUpload = {
throw new Meteor.Error('error-file-too-large', reason);
}

if (!fileUploadIsValidContentType(file?.type)) {
if (!settings.get('E2E_Enable_Encrypt_Files') && isE2EEUpload(file)) {
const reason = i18n.t('Encrypted_file_not_allowed', { lng: language });
throw new Meteor.Error('error-invalid-file-type', reason);
}

// E2EE files are of type - application/octet-stream, application/octet-stream is whitelisted for E2EE files.
if (!fileUploadIsValidContentType(file?.type, isE2EEUpload(file) ? 'application/octet-stream' : undefined)) {
const reason = i18n.t('File_type_is_not_accepted', { lng: language });
throw new Meteor.Error('error-invalid-file-type', reason);
}
Expand Down
6 changes: 6 additions & 0 deletions apps/meteor/client/lib/chats/flows/uploadFiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { IMessage, FileAttachmentProps, IE2EEMessage, IUpload } from '@rock
import { isRoomFederated } from '@rocket.chat/core-typings';

import { e2e } from '../../../../app/e2e/client';
import { settings } from '../../../../app/settings/client';
import { fileUploadIsValidContentType } from '../../../../app/utils/client';
import { getFileExtension } from '../../../../lib/utils/getFileExtension';
import FileUploadModal from '../../../views/room/modals/FileUploadModal';
Expand Down Expand Up @@ -83,6 +84,11 @@ export const uploadFiles = async (chat: ChatAPI, files: readonly File[], resetFi
return;
}

if (!settings.get('E2E_Enable_Encrypt_Files')) {
uploadFile(file, { description });
return;
}

const shouldConvertSentMessages = await e2eRoom.shouldConvertSentMessages({ msg });

if (!shouldConvertSentMessages) {
Expand Down
8 changes: 8 additions & 0 deletions apps/meteor/server/settings/e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ export const createE2ESettings = () =>
enableQuery: { _id: 'E2E_Enable', value: true },
});

await this.add('E2E_Enable_Encrypt_Files', true, {
type: 'boolean',
i18nLabel: 'E2E_Enable_Encrypt_Files',
i18nDescription: 'E2E_Enable_Encrypt_Files_Description',
public: true,
enableQuery: { _id: 'E2E_Enable', value: true },
});

await this.add('E2E_Enabled_Default_DirectRooms', false, {
type: 'boolean',
public: true,
Expand Down
143 changes: 143 additions & 0 deletions apps/meteor/tests/e2e/e2e-encryption.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,149 @@ test.describe.serial('e2e-encryption', () => {
await expect(poHomeChannel.content.nthMessage(0).locator('.rcx-icon--name-key')).toBeVisible();
});

test.describe('File Encryption', async () => {
test.afterAll(async ({ api }) => {
expect((await api.post('/settings/FileUpload_MediaTypeWhiteList', { value: '' })).status()).toBe(200);
expect((await api.post('/settings/FileUpload_MediaTypeBlackList', { value: 'image/svg+xml' })).status()).toBe(200);
});

test('File and description encryption', async ({ page }) => {
await test.step('create an encrypted channel', async () => {
const channelName = faker.string.uuid();

await poHomeChannel.sidenav.openNewByLabel('Channel');
await poHomeChannel.sidenav.inputChannelName.fill(channelName);
await poHomeChannel.sidenav.advancedSettingsAccordion.click();
await poHomeChannel.sidenav.checkboxEncryption.click();
await poHomeChannel.sidenav.btnCreate.click();

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

await poHomeChannel.dismissToast();

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

await test.step('send a file in channel', async () => {
await poHomeChannel.content.dragAndDropTxtFile();
await poHomeChannel.content.descriptionInput.fill('any_description');
await poHomeChannel.content.fileNameInput.fill('any_file1.txt');
await poHomeChannel.content.btnModalConfirm.click();

await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).toBeVisible();
await expect(poHomeChannel.content.getFileDescription).toHaveText('any_description');
await expect(poHomeChannel.content.lastMessageFileName).toContainText('any_file1.txt');
});
});

test('File encryption with whitelisted and blacklisted media types', async ({ page, api }) => {
await test.step('create an encrypted room', async () => {
const channelName = faker.string.uuid();

await poHomeChannel.sidenav.openNewByLabel('Channel');
await poHomeChannel.sidenav.inputChannelName.fill(channelName);
await poHomeChannel.sidenav.advancedSettingsAccordion.click();
await poHomeChannel.sidenav.checkboxEncryption.click();
await poHomeChannel.sidenav.btnCreate.click();

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

await poHomeChannel.dismissToast();

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

await test.step('send a text file in channel', async () => {
await poHomeChannel.content.dragAndDropTxtFile();
await poHomeChannel.content.descriptionInput.fill('message 1');
await poHomeChannel.content.fileNameInput.fill('any_file1.txt');
await poHomeChannel.content.btnModalConfirm.click();

await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).toBeVisible();
await expect(poHomeChannel.content.getFileDescription).toHaveText('message 1');
await expect(poHomeChannel.content.lastMessageFileName).toContainText('any_file1.txt');
});

await test.step('set whitelisted media type setting', async () => {
expect((await api.post('/settings/FileUpload_MediaTypeWhiteList', { value: 'text/plain' })).status()).toBe(200);
});

await test.step('send text file again with whitelist setting set', async () => {
await poHomeChannel.content.dragAndDropTxtFile();
await poHomeChannel.content.descriptionInput.fill('message 2');
await poHomeChannel.content.fileNameInput.fill('any_file2.txt');
await poHomeChannel.content.btnModalConfirm.click();

await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).toBeVisible();
await expect(poHomeChannel.content.getFileDescription).toHaveText('message 2');
await expect(poHomeChannel.content.lastMessageFileName).toContainText('any_file2.txt');
});

await test.step('set blacklisted media type setting to not accept application/octet-stream media type', async () => {
expect((await api.post('/settings/FileUpload_MediaTypeBlackList', { value: 'application/octet-stream' })).status()).toBe(200);
});

await test.step('send text file again with blacklisted setting set, file upload should fail', async () => {
await poHomeChannel.content.dragAndDropTxtFile();
await poHomeChannel.content.descriptionInput.fill('message 3');
await poHomeChannel.content.fileNameInput.fill('any_file3.txt');
await poHomeChannel.content.btnModalConfirm.click();

await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).toBeVisible();
await expect(poHomeChannel.content.getFileDescription).toHaveText('message 2');
await expect(poHomeChannel.content.lastMessageFileName).toContainText('any_file2.txt');
});
});

test.describe('File encryption setting disabled', async () => {
test.beforeAll(async ({ api }) => {
expect((await api.post('/settings/E2E_Enable_Encrypt_Files', { value: false })).status()).toBe(200);
expect((await api.post('/settings/FileUpload_MediaTypeBlackList', { value: 'application/octet-stream' })).status()).toBe(200);
});

test.afterAll(async ({ api }) => {
expect((await api.post('/settings/E2E_Enable_Encrypt_Files', { value: true })).status()).toBe(200);
expect((await api.post('/settings/FileUpload_MediaTypeBlackList', { value: 'image/svg+xml' })).status()).toBe(200);
});

test('Upload file without encryption in e2ee room', async ({ page }) => {
await test.step('create an encrypted channel', async () => {
const channelName = faker.string.uuid();

await poHomeChannel.sidenav.openNewByLabel('Channel');
await poHomeChannel.sidenav.inputChannelName.fill(channelName);
await poHomeChannel.sidenav.advancedSettingsAccordion.click();
await poHomeChannel.sidenav.checkboxEncryption.click();
await poHomeChannel.sidenav.btnCreate.click();

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

await poHomeChannel.dismissToast();

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

await test.step('send a test encrypted message to check e2ee is working', async () => {
await poHomeChannel.content.sendMessage('This is an encrypted message.');

await expect(poHomeChannel.content.lastUserMessageBody).toHaveText('This is an encrypted message.');
await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).toBeVisible();
});

await test.step('send a text file in channel, file should not be encrypted', async () => {
await poHomeChannel.content.dragAndDropTxtFile();
await poHomeChannel.content.descriptionInput.fill('any_description');
await poHomeChannel.content.fileNameInput.fill('any_file1.txt');
await poHomeChannel.content.btnModalConfirm.click();

await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).not.toBeVisible();
await expect(poHomeChannel.content.getFileDescription).toHaveText('any_description');
await expect(poHomeChannel.content.lastMessageFileName).toContainText('any_file1.txt');
});
});
});
});

test('expect slash commands to be enabled in an e2ee room', async ({ page }) => {
const channelName = faker.string.uuid();

Expand Down
74 changes: 74 additions & 0 deletions apps/meteor/tests/end-to-end/api/rooms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,7 @@ describe('[Rooms]', () => {
.filter((type) => type !== 'image/svg+xml')
.join(',');
await updateSetting('FileUpload_MediaTypeBlackList', newBlockedMediaTypes);
await updateSetting('E2E_Enable_Encrypt_Files', true);
});

after(() =>
Expand All @@ -427,6 +428,7 @@ describe('[Rooms]', () => {
updateSetting('FileUpload_Restrict_to_room_members', true),
updateSetting('FileUpload_ProtectFiles', true),
updateSetting('FileUpload_MediaTypeBlackList', blockedMediaTypes),
updateSetting('E2E_Enable_Encrypt_Files', true),
]),
);

Expand Down Expand Up @@ -708,6 +710,78 @@ describe('[Rooms]', () => {
expect(res.body.message.attachments[0]).to.have.property('description', 'some_file_description');
});
});

it('should correctly save encrypted file', async () => {
let fileId;

await request
.post(api(`rooms.media/${testChannel._id}`))
.set(credentials)
.attach('file', fs.createReadStream(path.join(__dirname, '../../mocks/files/diagram.drawio')), {
contentType: 'application/octet-stream',
})
.field({ content: JSON.stringify({ algorithm: 'rc.v1.aes-sha2', ciphertext: 'something' }) })
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('file');
expect(res.body.file).to.have.property('_id');
expect(res.body.file).to.have.property('url');

fileId = res.body.file._id;
});

await request
.post(api(`rooms.mediaConfirm/${testChannel._id}/${fileId}`))
.set(credentials)
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('message');
expect(res.body.message).to.have.property('files');
expect(res.body.message.files).to.be.an('array').of.length(1);
expect(res.body.message.files[0]).to.have.property('type', 'application/octet-stream');
expect(res.body.message.files[0]).to.have.property('name', 'diagram.drawio');
});
});

it('should fail encrypted file upload when files encryption is disabled', async () => {
await updateSetting('E2E_Enable_Encrypt_Files', false);

await request
.post(api(`rooms.media/${testChannel._id}`))
.set(credentials)
.attach('file', fs.createReadStream(path.join(__dirname, '../../mocks/files/diagram.drawio')), {
contentType: 'application/octet-stream',
})
.field({ content: JSON.stringify({ algorithm: 'rc.v1.aes-sha2', ciphertext: 'something' }) })
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('errorType', 'error-invalid-file-type');
});
});

it('should fail encrypted file upload on blacklisted application/octet-stream media type', async () => {
await updateSetting('FileUpload_MediaTypeBlackList', 'application/octet-stream');

await request
.post(api(`rooms.media/${testChannel._id}`))
.set(credentials)
.attach('file', fs.createReadStream(path.join(__dirname, '../../mocks/files/diagram.drawio')), {
contentType: 'application/octet-stream',
})
.field({ content: JSON.stringify({ algorithm: 'rc.v1.aes-sha2', ciphertext: 'something' }) })
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('errorType', 'error-invalid-file-type');
});
});
});

describe('/rooms.favorite', () => {
Expand Down
13 changes: 13 additions & 0 deletions apps/meteor/tests/mocks/files/diagram.drawio
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<mxfile host="app.diagrams.net" modified="2024-05-21T16:10:09.295Z" agent="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36" etag="ZxZRCTHi-kxhlzk7b9_Z" version="24.4.4" type="device">
<diagram name="Página-1" id="9eBILa8281JaQ4yUkDbp">
<mxGraphModel dx="1434" dy="786" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="dopCU4gkJe7Sfp6IDYO1-1" value="&lt;b&gt;&lt;font style=&quot;font-size: 30px;&quot;&gt;Rocket.Chat&lt;/font&gt;&lt;/b&gt;" style="text;html=1;align=center;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;" vertex="1" parent="1">
<mxGeometry x="314" y="350" width="200" height="50" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>
9 changes: 9 additions & 0 deletions packages/core-typings/src/IUpload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,12 @@ export interface IUpload {
}

export type IUploadWithUser = IUpload & { user?: Pick<IUser, '_id' | 'name' | 'username'> };

export type IE2EEUpload = IUpload & {
content: {
algorithm: string; // 'rc.v1.aes-sha2'
ciphertext: string; // Encrypted subset JSON of IUpload
};
};

export const isE2EEUpload = (upload: IUpload): upload is IE2EEUpload => Boolean(upload?.content?.ciphertext && upload?.content?.algorithm);
3 changes: 3 additions & 0 deletions packages/i18n/src/locales/en.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -1801,6 +1801,8 @@
"E2E_Enabled": "E2E Enabled",
"E2E_Enabled_Default_DirectRooms": "Enable encryption for Direct Rooms by default",
"E2E_Enabled_Default_PrivateRooms": "Enable encryption for Private Rooms by default",
"E2E_Enable_Encrypt_Files": "Encrypt files",
"E2E_Enable_Encrypt_Files_Description": "Encrypt files sent inside encrypted rooms. Check for possible conflicts in [file upload settings.](admin/settings/FileUpload)",
"E2E_Encryption_Password_Change": "Change Encryption Password",
"E2E_Encryption_Password_Explanation": "You can now create encrypted private groups and direct messages. You may also change existing private groups or DMs to encrypted.<br/><br/>This is end-to-end encryption so the key to encode/decode your messages will not be saved on the server. For that reason you need to store your password somewhere safe. You will be required to enter it on other devices you wish to use e2e encryption on.",
"E2E_key_reset_email": "E2E Key Reset Notification",
Expand Down Expand Up @@ -1919,6 +1921,7 @@
"Email_verified": "Email verified",
"Enterprise_Only": "Enterprise only",
"Encrypted_field_hint": "Messages are end-to-end encrypted, search will not work and notifications may not show message content",
"Encrypted_file_not_allowed": "Encrypted file not allowed",
"Email_sent": "Email sent",
"Email_verification_isnt_required": "Email verification to login is not required. To require, enable setting in <a href=\"{{url}}\">Accounts</a> > Registration",
"Emoji": "Emoji",
Expand Down

0 comments on commit 58c0efc

Please sign in to comment.