From 94845cbfd2f0d80661f25a83c2b8a428988ceb7e Mon Sep 17 00:00:00 2001 From: Diego Mello Date: Fri, 19 Apr 2024 17:19:30 -0300 Subject: [PATCH] feat: Encrypt file descriptions on E2EE rooms (#5599) --- .../components/RecordAudio/RecordAudio.tsx | 17 +++++---- app/containers/message/Attachments.tsx | 4 +-- app/containers/message/index.tsx | 3 +- app/definitions/IUpload.ts | 6 +++- app/lib/encryption/encryption.ts | 24 ++++++++++--- app/lib/encryption/room.ts | 36 ++++++++++++++++++- app/lib/methods/sendFileMessage.ts | 17 ++++++++- .../RoomSettings/SwitchItemEncrypted.test.tsx | 6 ++-- app/views/ShareView/index.tsx | 5 ++- 9 files changed, 97 insertions(+), 21 deletions(-) diff --git a/app/containers/MessageComposer/components/RecordAudio/RecordAudio.tsx b/app/containers/MessageComposer/components/RecordAudio/RecordAudio.tsx index 0ec27650e..f93a3208d 100644 --- a/app/containers/MessageComposer/components/RecordAudio/RecordAudio.tsx +++ b/app/containers/MessageComposer/components/RecordAudio/RecordAudio.tsx @@ -95,16 +95,21 @@ export const RecordAudio = (): ReactElement | null => { try { if (!rid) return; setRecordingAudio(false); - const fileURI = recordingRef.current?.getURI(); - const fileData = await getInfoAsync(fileURI as string); - const fileInfo = { + const fileURI = recordingRef.current?.getURI() as string; + const fileData = await getInfoAsync(fileURI); + + if (!fileData.exists) { + return; + } + + const fileInfo: IUpload = { + rid, name: `${Date.now()}${RECORDING_EXTENSION}`, - mime: 'audio/aac', type: 'audio/aac', store: 'Uploads', path: fileURI, - size: fileData.exists ? fileData.size : null - } as IUpload; + size: fileData.size + }; if (fileInfo) { if (permissionToUpload) { diff --git a/app/containers/message/Attachments.tsx b/app/containers/message/Attachments.tsx index 1e96dbaac..d9cfcfa42 100644 --- a/app/containers/message/Attachments.tsx +++ b/app/containers/message/Attachments.tsx @@ -55,14 +55,14 @@ const AttachedActions = ({ attachment, getCustomEmoji }: { attachment: IAttachme const Attachments: React.FC = React.memo( ({ attachments, timeFormat, showAttachment, style, getCustomEmoji, isReply, author }: IMessageAttachments) => { - const { translateLanguage } = useContext(MessageContext); + const { translateLanguage, isEncrypted } = useContext(MessageContext); if (!attachments || attachments.length === 0) { return null; } const attachmentsElements = attachments.map((file: IAttachment, index: number) => { - const msg = getMessageFromAttachment(file, translateLanguage); + const msg = isEncrypted ? '' : getMessageFromAttachment(file, translateLanguage); if (file && file.image_url) { return ( {/* @ts-ignore*/} diff --git a/app/definitions/IUpload.ts b/app/definitions/IUpload.ts index a2935575b..187e34881 100644 --- a/app/definitions/IUpload.ts +++ b/app/definitions/IUpload.ts @@ -1,8 +1,10 @@ import Model from '@nozbe/watermelondb/Model'; +import { E2EType, MessageType } from './IMessage'; + export interface IUpload { id?: string; - rid?: string; + rid: string; path: string; name?: string; tmid?: string; @@ -14,6 +16,8 @@ export interface IUpload { error?: boolean; subscription?: { id: string }; msg?: string; + t?: MessageType; + e2e?: E2EType; } export type TUploadModel = IUpload & Model; diff --git a/app/lib/encryption/encryption.ts b/app/lib/encryption/encryption.ts index 05071d074..9ac6de218 100644 --- a/app/lib/encryption/encryption.ts +++ b/app/lib/encryption/encryption.ts @@ -11,7 +11,15 @@ import log from '../methods/helpers/log'; import { store } from '../store/auxStore'; import { joinVectorData, randomPassword, splitVectorData, toString, utf8ToBuffer } from './utils'; import { EncryptionRoom } from './index'; -import { IMessage, ISubscription, TMessageModel, TSubscriptionModel, TThreadMessageModel, TThreadModel } from '../../definitions'; +import { + IMessage, + ISubscription, + IUpload, + TMessageModel, + TSubscriptionModel, + TThreadMessageModel, + TThreadModel +} from '../../definitions'; import { E2E_BANNER_TYPE, E2E_MESSAGE_TYPE, @@ -34,6 +42,7 @@ class Encryption { handshake: Function; decrypt: Function; encrypt: Function; + encryptUpload: Function; importRoomKey: Function; }; }; @@ -275,7 +284,7 @@ class Encryption { ]; toDecrypt = (await Promise.all( toDecrypt.map(async message => { - const { t, msg, tmsg } = message; + const { t, msg, tmsg, attachments } = message; let newMessage: TMessageModel = {} as TMessageModel; if (message.subscription) { const { id: rid } = message.subscription; @@ -284,7 +293,8 @@ class Encryption { t, rid, msg: msg as string, - tmsg + tmsg, + attachments }); } @@ -434,7 +444,7 @@ class Encryption { }; // Encrypt a message - encryptMessage = async (message: IMessage) => { + encryptMessage = async (message: IMessage | IUpload) => { const { rid } = message; const db = database.active; const subCollection = db.get('subscriptions'); @@ -456,6 +466,10 @@ class Encryption { } const roomE2E = await this.getRoomInstance(rid); + + if ('path' in message) { + return roomE2E.encryptUpload(message); + } return roomE2E.encrypt(message); } catch { // Subscription not found @@ -467,7 +481,7 @@ class Encryption { }; // Decrypt a message - decryptMessage = async (message: Pick) => { + decryptMessage = async (message: Pick) => { const { t, e2e } = message; // Prevent create a new instance if this room was encrypted sometime ago diff --git a/app/lib/encryption/room.ts b/app/lib/encryption/room.ts index 090582a81..55f90e349 100644 --- a/app/lib/encryption/room.ts +++ b/app/lib/encryption/room.ts @@ -5,7 +5,7 @@ import ByteBuffer from 'bytebuffer'; import parse from 'url-parse'; import getSingleMessage from '../methods/getSingleMessage'; -import { IMessage, IUser } from '../../definitions'; +import { IMessage, IUpload, IUser } from '../../definitions'; import Deferred from './helpers/deferred'; import { debounce } from '../methods/helpers'; import database from '../database'; @@ -243,8 +243,38 @@ export default class EncryptionRoom { return message; }; + // Encrypt upload + encryptUpload = async (message: IUpload) => { + if (!this.ready) { + return message; + } + + try { + let description = ''; + + if (message.description) { + description = await this.encryptText(EJSON.stringify({ text: message.description })); + } + + return { + ...message, + t: E2E_MESSAGE_TYPE, + e2e: E2E_STATUS.PENDING, + description + }; + } catch { + // Do nothing + } + + return message; + }; + // Decrypt text decryptText = async (msg: string | ArrayBuffer) => { + if (!msg) { + return null; + } + msg = b64ToBuffer(msg.slice(12) as string); const [vector, cipherText] = splitVectorData(msg); @@ -275,6 +305,10 @@ export default class EncryptionRoom { tmsg = await this.decryptText(tmsg); } + if (message.attachments?.length) { + message.attachments[0].description = await this.decryptText(message.attachments[0].description as string); + } + const decryptedMessage: IMessage = { ...message, tmsg, diff --git a/app/lib/methods/sendFileMessage.ts b/app/lib/methods/sendFileMessage.ts index 8886f6064..8c1460752 100644 --- a/app/lib/methods/sendFileMessage.ts +++ b/app/lib/methods/sendFileMessage.ts @@ -4,12 +4,14 @@ import isEmpty from 'lodash/isEmpty'; import { FetchBlobResponse, StatefulPromise } from 'rn-fetch-blob'; import { Alert } from 'react-native'; +import { Encryption } from '../encryption'; import { IUpload, IUser, TUploadModel } from '../../definitions'; import i18n from '../../i18n'; import database from '../database'; import FileUpload from './helpers/fileUpload'; import { IFileUpload } from './helpers/fileUpload/interfaces'; import log from './helpers/log'; +import { E2E_MESSAGE_TYPE } from '../constants'; const uploadQueue: { [index: string]: StatefulPromise } = {}; @@ -85,6 +87,8 @@ export function sendFileMessage( } } + const encryptedFileInfo = await Encryption.encryptMessage(fileInfo); + const formData: IFileUpload[] = []; formData.push({ name: 'file', @@ -96,7 +100,7 @@ export function sendFileMessage( if (fileInfo.description) { formData.push({ name: 'description', - data: fileInfo.description + data: encryptedFileInfo.description }); } @@ -114,6 +118,17 @@ export function sendFileMessage( }); } + if (encryptedFileInfo.t === E2E_MESSAGE_TYPE) { + formData.push({ + name: 't', + data: encryptedFileInfo.t + }); + formData.push({ + name: 'e2e', + data: encryptedFileInfo.e2e + }); + } + const headers = { ...RocketChatSettings.customHeaders, 'Content-Type': 'multipart/form-data', diff --git a/app/views/CreateChannelView/RoomSettings/SwitchItemEncrypted.test.tsx b/app/views/CreateChannelView/RoomSettings/SwitchItemEncrypted.test.tsx index a5f536d5a..1f18b44a6 100644 --- a/app/views/CreateChannelView/RoomSettings/SwitchItemEncrypted.test.tsx +++ b/app/views/CreateChannelView/RoomSettings/SwitchItemEncrypted.test.tsx @@ -45,7 +45,7 @@ describe('SwitchItemEncrypted', () => { const component = screen.queryByTestId(testEncrypted.testSwitchID); expect(component).toBeTruthy(); }); - + it('should change value of switch', () => { render( { expect(onPressMock).toHaveReturnedWith({ value: !testEncrypted.encrypted }); } }); - + it('label when encrypted and isTeam are false and is a public channel', () => { render( { const component = screen.queryByTestId(testEncrypted.testLabelID); expect(component?.props.children).toBe(i18n.t('Channel_hint_encrypted_not_available')); }); - + it('label when encrypted and isTeam are true and is a private team', () => { testEncrypted.isTeam = true; testEncrypted.type = true; diff --git a/app/views/ShareView/index.tsx b/app/views/ShareView/index.tsx index b76866a8c..be7b8af50 100644 --- a/app/views/ShareView/index.tsx +++ b/app/views/ShareView/index.tsx @@ -126,7 +126,9 @@ class ShareView extends Component { // if is share extension show default back button if (!this.isShareExtension) { - options.headerLeft = () => ; + options.headerLeft = () => ( + + ); } if (!attachments.length && !readOnly) { @@ -255,6 +257,7 @@ class ShareView extends Component { return sendFileMessage( room.rid, { + rid: room.rid, name, description, size,