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

feat(E2EEncryption): File encryption support #32316

Merged
merged 77 commits into from
Jun 22, 2024
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
77 commits
Select commit Hold shift + click to select a range
4ee6d2f
POC
rodrigok Apr 25, 2024
a1c0569
Initial service worker implementation
rodrigok Apr 26, 2024
640ed9e
Start changing the upload to two calls
rodrigok Apr 27, 2024
59d99a6
Start using content property in place of e2e
rodrigok Apr 28, 2024
d84ce58
Readd e2e property
rodrigok Apr 28, 2024
e6a4330
Client side attachment generation
rodrigok Apr 28, 2024
677f0ef
Cleanup
rodrigok Apr 28, 2024
7c7eb29
Encrypt content
rodrigok Apr 28, 2024
a9add79
Fix file description
rodrigok Apr 28, 2024
69f815e
Small improvements
rodrigok Apr 28, 2024
ebbe64f
Cleanup
rodrigok Apr 28, 2024
b43f57b
Cleanup
rodrigok Apr 28, 2024
ec06eb3
Improve TS
rodrigok Apr 28, 2024
f70d2e1
Change content data structure
rodrigok Apr 30, 2024
445c528
Move attachment key info to inside encryption property
rodrigok Apr 30, 2024
6daab1a
Set encrypted file name as a hash sha-256 of the name
rodrigok Apr 30, 2024
d14a0e0
Merge remote-tracking branch 'origin/develop' into feat/encrypted-files
rodrigok Apr 30, 2024
fd5fcd2
Fix lint and TS
rodrigok Apr 30, 2024
2e1f78a
Fix regression
rodrigok Apr 30, 2024
d307519
Merge branch 'develop' into feat/encrypted-files
rodrigok May 9, 2024
1e25638
Mark uploads as temporary and confirm on send message
rodrigok May 9, 2024
574169f
Fix API test
rodrigok May 9, 2024
7154756
Implement cronjob
rodrigok May 10, 2024
222cbf3
Merge branch 'develop' into feat/encrypted-files
rodrigok May 10, 2024
aff80f3
Skip UI attachment test for now
rodrigok May 10, 2024
de5f0f2
Do not generate attachment on backend for e2ee messages
rodrigok May 10, 2024
939940d
Download through serviceworker
ggazzo May 14, 2024
7fb9618
Merge remote-tracking branch 'origin/develop' into feat/encrypted-files
rodrigok May 14, 2024
26e2eb8
👀
ggazzo May 15, 2024
3234c56
Merge branch 'develop' into feat/encrypted-files
MarcosSpessatto May 25, 2024
d894d30
Merge branch 'develop' into feat/encrypted-files
rodrigok Jun 10, 2024
ee3fe8a
Move encryption of msg to inside content
rodrigok Jun 12, 2024
956316e
Merge branch 'develop' into feat/encrypted-files
rodrigok Jun 12, 2024
48a97f7
Fix placeholder message for threads
rodrigok Jun 13, 2024
446b1a7
Unskip test
rodrigok Jun 13, 2024
35363b6
Merge branch 'develop' into feat/encrypted-files
rodrigok Jun 13, 2024
c258b21
Fix ts
rodrigok Jun 13, 2024
989c61c
Fix tests
rodrigok Jun 13, 2024
403ced3
Merge branch 'develop' into feat/encrypted-files
rodrigok Jun 14, 2024
de94f83
Merge branch 'develop' into feat/encrypted-files
rodrigok Jun 14, 2024
9da9640
Add test for old e2ee msg format
rodrigok Jun 14, 2024
8c7310f
Merge branch 'develop' into feat/encrypted-files
rodrigok Jun 18, 2024
5228bdb
Add tests for new upload API
rodrigok Jun 18, 2024
9e83634
Save encrypted content info to the file upload
rodrigok Jun 19, 2024
8f81edd
Add dimensions to image attachments
rodrigok Jun 19, 2024
b9ff006
Fix TS
rodrigok Jun 19, 2024
9a39914
Merge branch 'develop' into feat/encrypted-files
rodrigok Jun 19, 2024
20962c4
Fix tests
rodrigok Jun 19, 2024
74cfee1
Fix image preview
rodrigok Jun 19, 2024
6508ebe
Prevent keys to be set on top of existent keys
rodrigok Jun 19, 2024
1a1788e
Merge branch 'develop' into feat/encrypted-files
rodrigok Jun 19, 2024
64a27cb
Fix API tests
rodrigok Jun 19, 2024
b5e1ba5
Fix e2ee change password
rodrigok Jun 19, 2024
f108fd4
Fix TS
rodrigok Jun 19, 2024
ae76481
Fix API tests
rodrigok Jun 19, 2024
18d0e87
Decrypt room's last message correctly
rodrigok Jun 19, 2024
e984ad2
Merge remote-tracking branch 'origin/develop' into feat/encrypted-files
rodrigok Jun 19, 2024
c251e17
Merge branch 'develop' into feat/encrypted-files
rodrigok Jun 19, 2024
a96fa61
Try to fix tests
rodrigok Jun 19, 2024
3f480b0
Merge branch 'develop' into feat/encrypted-files
rodrigok Jun 19, 2024
fa2456a
Fix tests
rodrigok Jun 20, 2024
365cef8
Fix preview of encrypted files
rodrigok Jun 20, 2024
bd90357
Merge branch 'develop' into feat/encrypted-files
rodrigok Jun 20, 2024
81dad69
Fix download button
rodrigok Jun 20, 2024
8402305
Fix download from files list
rodrigok Jun 20, 2024
503218e
Merge branch 'develop' into feat/encrypted-files
rodrigok Jun 20, 2024
09e3bb7
Force cors on SW
rodrigok Jun 20, 2024
cc07b26
Merge branch 'develop' into feat/encrypted-files
rodrigok Jun 21, 2024
4d1c7a1
Prevent error when file contains non utf-8 chars
rodrigok Jun 21, 2024
72dc127
Fix progress bar not showing enc file name
rodrigok Jun 21, 2024
0d41aed
Fix error not allowing enc file upload when restricted
rodrigok Jun 21, 2024
588f939
Fix tests
rodrigok Jun 21, 2024
815bb21
Deprecate API and fix tests
rodrigok Jun 21, 2024
e4ea847
Fix tests
rodrigok Jun 21, 2024
6cb3043
Create metal-cats-suffer.md
rodrigok Jun 21, 2024
47302e6
Merge branch 'develop' into feat/encrypted-files
rodrigok Jun 21, 2024
1b8e725
Merge branch 'develop' into feat/encrypted-files
ggazzo Jun 22, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"typescript.tsdk": "./node_modules/typescript/lib",
"cSpell.words": [
"autotranslate",
"ciphertext",
"Contextualbar",
"fname",
"Gazzodown",
Expand Down
84 changes: 84 additions & 0 deletions apps/meteor/app/api/server/v1/rooms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { isGETRoomsNameExists, isRoomsImagesProps, isRoomsMuteUnmuteUserProps }
import { Meteor } from 'meteor/meteor';

import { isTruthy } from '../../../../lib/isTruthy';
import { omit } from '../../../../lib/utils/omit';
import * as dataExport from '../../../../server/lib/dataExport';
import { eraseRoom } from '../../../../server/methods/eraseRoom';
import { muteUserInRoom } from '../../../../server/methods/muteUserInRoom';
Expand Down Expand Up @@ -139,6 +140,7 @@ API.v1.addRoute(
},
);

// TODO: deprecate API
API.v1.addRoute(
'rooms.upload/:rid',
{ authRequired: true },
Expand Down Expand Up @@ -194,6 +196,88 @@ API.v1.addRoute(
},
);

API.v1.addRoute(
'rooms.media/:rid',
{ authRequired: true },
{
async post() {
if (!(await canAccessRoomIdAsync(this.urlParams.rid, this.userId))) {
return API.v1.unauthorized();
}

const file = await getUploadFormData(
{
request: this.request,
},
{ field: 'file', sizeLimit: settings.get<number>('FileUpload_MaxFileSize') },
);

if (!file) {
throw new Meteor.Error('invalid-field');
}

let { fileBuffer } = file;

const details = {
name: file.filename,
size: fileBuffer.length,
type: file.mimetype,
rid: this.urlParams.rid,
userId: this.userId,
};

const stripExif = settings.get('Message_Attachments_Strip_Exif');
if (stripExif) {
// No need to check mime. Library will ignore any files without exif/xmp tags (like BMP, ico, PDF, etc)
fileBuffer = await Media.stripExifFromBuffer(fileBuffer);
}

const fileStore = FileUpload.getStore('Uploads');
const uploadedFile = await fileStore.insert(details, fileBuffer);

await Uploads.updateFileComplete(uploadedFile._id, this.userId, omit(uploadedFile, '_id'));

const fileUrl = FileUpload.getPath(`${uploadedFile._id}/${encodeURI(uploadedFile.name || '')}`);

return API.v1.success({
file: {
_id: uploadedFile._id,
url: fileUrl,
},
});
},
},
);

API.v1.addRoute(
'rooms.mediaConfirm/:rid/:fileId',
{ authRequired: true },
{
async post() {
if (!(await canAccessRoomIdAsync(this.urlParams.rid, this.userId))) {
return API.v1.unauthorized();
}

const file = await Uploads.findOneById(this.urlParams.fileId);

if (!file) {
throw new Meteor.Error('invalid-file');
}

file.description = this.bodyParams.description;
delete this.bodyParams.description;

await sendFileMessage(this.userId, { roomId: this.urlParams.rid, file, msgData: this.bodyParams });

const message = await Messages.getMessageByFileIdAndUsername(file._id, this.userId);

return API.v1.success({
message,
});
},
},
);

API.v1.addRoute(
'rooms.saveNotification',
{ authRequired: true },
Expand Down
8 changes: 8 additions & 0 deletions apps/meteor/app/e2e/client/helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ export async function encryptAES(vector, key, data) {
return crypto.subtle.encrypt({ name: 'AES-CBC', iv: vector }, key, data);
}

export async function encryptAESCTR(vector, key, data) {
return crypto.subtle.encrypt({ name: 'AES-CTR', counter: vector, length: 64 }, key, data);
}

export async function decryptRSA(key, data) {
return crypto.subtle.decrypt({ name: 'RSA-OAEP' }, key, data);
}
Expand All @@ -65,6 +69,10 @@ export async function generateAESKey() {
return crypto.subtle.generateKey({ name: 'AES-CBC', length: 128 }, true, ['encrypt', 'decrypt']);
}

export async function generateAESCTRKey() {
return crypto.subtle.generateKey({ name: 'AES-CTR', length: 256 }, true, ['encrypt', 'decrypt']);
}

export async function generateRSAKey() {
return crypto.subtle.generateKey(
{
Expand Down
90 changes: 38 additions & 52 deletions apps/meteor/app/e2e/client/rocketchat.e2e.room.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Base64 } from '@rocket.chat/base64';
import { Emitter } from '@rocket.chat/emitter';
import { Random } from '@rocket.chat/random';
import EJSON from 'ejson';

import { RoomManager } from '../../../client/lib/RoomManager';
Expand All @@ -23,6 +22,8 @@ import {
importAESKey,
importRSAKey,
readFileAsArrayBuffer,
encryptAESCTR,
generateAESCTRKey,
} from './helper';
import { log, logError } from './logger';
import { e2e } from './rocketchat.e2e';
Expand Down Expand Up @@ -333,69 +334,72 @@ export class E2ERoom extends Emitter {
}
}

async sha256Hash(text) {
const encoder = new TextEncoder();
const data = encoder.encode(text);
const hashArray = Array.from(new Uint8Array(await crypto.subtle.digest('SHA-256', data)));
return hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');
}

// Encrypts files before upload. I/O is in arraybuffers.
async encryptFile(file) {
if (!this.isSupportedRoomType(this.typeOfRoom)) {
return;
}
// if (!this.isSupportedRoomType(this.typeOfRoom)) {
// return;
// }

const fileArrayBuffer = await readFileAsArrayBuffer(file);

const vector = crypto.getRandomValues(new Uint8Array(16));
const key = await generateAESCTRKey();
diegolmello marked this conversation as resolved.
Show resolved Hide resolved
let result;
try {
result = await encryptAES(vector, this.groupSessionKey, fileArrayBuffer);
result = await encryptAESCTR(vector, key, fileArrayBuffer);
} catch (error) {
console.log(error);
return this.error('Error encrypting group key: ', error);
}

const output = joinVectorAndEcryptedData(vector, result);
const exportedKey = await window.crypto.subtle.exportKey('jwk', key);

const fileName = await this.sha256Hash(file.name);

const encryptedFile = new File([toArrayBuffer(EJSON.stringify(output))], file.name);
const encryptedFile = new File([toArrayBuffer(result)], fileName);

return encryptedFile;
return {
file: encryptedFile,
key: exportedKey,
iv: Base64.encode(vector),
type: file.type,
};
}

// Decrypt uploaded encrypted files. I/O is in arraybuffers.
async decryptFile(message) {
if (message[0] !== '{') {
return;
}

const [vector, cipherText] = splitVectorAndEcryptedData(EJSON.parse(message));
async decryptFile(file, key, iv) {
const ivArray = Base64.decode(iv);
const cryptoKey = await window.crypto.subtle.importKey('jwk', key, { name: 'AES-CTR' }, true, ['encrypt', 'decrypt']);

try {
return await decryptAES(vector, this.groupSessionKey, cipherText);
} catch (error) {
this.error('Error decrypting file: ', error);

return false;
}
return window.crypto.subtle.decrypt({ name: 'AES-CTR', counter: ivArray, length: 64 }, cryptoKey, file);
}

// Encrypts messages
async encryptText(data) {
if (!(typeof data === 'function' || (typeof data === 'object' && !!data))) {
data = new TextEncoder('UTF-8').encode(EJSON.stringify({ text: data, ack: Random.id((Random.fraction() + 1) * 20) }));
}

if (!this.isSupportedRoomType(this.typeOfRoom)) {
return data;
}

const vector = crypto.getRandomValues(new Uint8Array(16));
let result;

try {
result = await encryptAES(vector, this.groupSessionKey, data);
const result = await encryptAES(vector, this.groupSessionKey, data);
return this.keyID + Base64.encode(joinVectorAndEcryptedData(vector, result));
} catch (error) {
return this.error('Error encrypting message: ', error);
this.error('Error encrypting message: ', error);
throw error;
}

return this.keyID + Base64.encode(joinVectorAndEcryptedData(vector, result));
}

// Helper function for encryption of messages
encrypt(message) {
if (!this.isSupportedRoomType(this.typeOfRoom)) {
return;
}

const ts = new Date();

const data = new TextEncoder('UTF-8').encode(
Expand All @@ -410,20 +414,6 @@ export class E2ERoom extends Emitter {
return this.encryptText(data);
}

encryptAttachmentDescription(description, _id) {
const ts = new Date();

const data = new TextEncoder('UTF-8').encode(
EJSON.stringify({
userId: this.userId,
text: description,
_id,
ts,
}),
);
return this.encryptText(data);
}

// Decrypt messages

async decryptMessage(message) {
Expand All @@ -445,10 +435,6 @@ export class E2ERoom extends Emitter {
}

async decrypt(message) {
if (!this.isSupportedRoomType(this.typeOfRoom)) {
return message;
}

const keyID = message.slice(0, 12);

if (keyID !== this.keyID) {
Expand Down
54 changes: 8 additions & 46 deletions apps/meteor/app/e2e/client/rocketchat.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import QueryString from 'querystring';
import URL from 'url';

import type { IE2EEMessage, IMessage, IRoom, ISubscription } from '@rocket.chat/core-typings';
import { isE2EEMessage, isFileAttachment } from '@rocket.chat/core-typings';
import { isE2EEMessage } from '@rocket.chat/core-typings';
import { Emitter } from '@rocket.chat/emitter';
import EJSON from 'ejson';
import { Meteor } from 'meteor/meteor';
Expand Down Expand Up @@ -422,60 +422,22 @@ class E2E extends Emitter {

const data = await e2eRoom.decrypt(message.msg);

if (message.content && message.content.algorithm === 'rc.v1.aes-sha2') {
const content = await e2eRoom.decrypt(message.content.ciphertext);
Object.assign(message, content);
}

const decryptedMessage: IE2EEMessage = {
...message,
e2e: 'done',
...(data && {
msg: data.text,
e2e: 'done',
}),
};

const decryptedMessageWithQuote = await this.parseQuoteAttachment(decryptedMessage);

const decryptedMessageWithAttachments = await this.decryptMessageAttachments(decryptedMessageWithQuote);

return decryptedMessageWithAttachments;
}

async decryptMessageAttachments(message: IMessage): Promise<IMessage> {
const { attachments } = message;

if (!attachments || !attachments.length) {
return message;
}

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

if (!e2eRoom) {
return message;
}

const decryptedAttachments = await Promise.all(
attachments.map(async (attachment) => {
if (!isFileAttachment(attachment)) {
return attachment;
}

if (!attachment.description) {
return attachment;
}

const data = await e2eRoom.decrypt(attachment.description);

if (!data) {
return attachment;
}

attachment.description = data.text;
return attachment;
}),
);

return {
...message,
attachments: decryptedAttachments,
e2e: 'done',
};
return decryptedMessageWithQuote;
}

async decryptPendingMessages(): Promise<void> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,12 @@ export const sendFileMessage = async (
tmid: Match.Optional(String),
customFields: Match.Optional(String),
t: Match.Optional(String),
e2e: Match.Optional(String),
content: Match.Optional(
Match.ObjectIncluding({
algorithm: String,
ciphertext: String,
}),
),
}),
);

Expand Down
Loading
Loading