feat: Setting for enabling files encryption and fix whitelist media types stopping E2EE uploads (#33003)

Co-authored-by: Kevin Aleman <kaleman960@gmail.com>
pull/33169/head
Yash Rajpal 2 years ago committed by GitHub
parent c2254514f2
commit 58c0efc732
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 7
      .changeset/stupid-fishes-relate.md
  2. 15
      .changeset/violet-radios-begin.md
  3. 10
      apps/meteor/app/file-upload/server/lib/FileUpload.ts
  4. 6
      apps/meteor/client/lib/chats/flows/uploadFiles.ts
  5. 8
      apps/meteor/server/settings/e2e.ts
  6. 143
      apps/meteor/tests/e2e/e2e-encryption.spec.ts
  7. 74
      apps/meteor/tests/end-to-end/api/rooms.ts
  8. 13
      apps/meteor/tests/mocks/files/diagram.drawio
  9. 9
      packages/core-typings/src/IUpload.ts
  10. 3
      packages/i18n/src/locales/en.i18n.json

@ -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.

@ -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,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';
@ -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);
}

@ -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';
@ -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) {

@ -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,

@ -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();

@ -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(() =>
@ -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),
]),
);
@ -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', () => {

@ -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>

@ -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);

@ -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",
@ -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",

Loading…
Cancel
Save