diff --git a/app/definitions/IAttachment.ts b/app/definitions/IAttachment.ts index e945b0324..5001c54e4 100644 --- a/app/definitions/IAttachment.ts +++ b/app/definitions/IAttachment.ts @@ -69,4 +69,8 @@ export interface IShareAttachment { canUpload: boolean; error?: any; uri: string; + encryption?: { + key: any; + iv: string; + }; } diff --git a/app/lib/encryption/encryption.ts b/app/lib/encryption/encryption.ts index 39054d099..3c0a4b88c 100644 --- a/app/lib/encryption/encryption.ts +++ b/app/lib/encryption/encryption.ts @@ -12,6 +12,7 @@ import { store } from '../store/auxStore'; import { joinVectorData, randomPassword, splitVectorData, toString, utf8ToBuffer } from './utils'; import { EncryptionRoom } from './index'; import { + IAttachment, IMessage, ISubscription, IUpload, @@ -44,6 +45,7 @@ class Encryption { decrypt: Function; encrypt: Function; encryptText: Function; + encryptFile: Function; encryptUpload: Function; importRoomKey: Function; }; @@ -514,6 +516,37 @@ class Encryption { return roomE2E.decrypt(message); }; + encryptFile = async (rid: string, attachment: IAttachment) => { + const db = database.active; + const subCollection = db.get('subscriptions'); + + try { + // Find the subscription + const subRecord = await subCollection.find(rid); + + // Subscription is not encrypted at the moment + if (!subRecord.encrypted) { + // Send a non encrypted message + return attachment; + } + + // If the client is not ready + if (!this.ready) { + // Wait for ready status + await this.establishing; + } + + const roomE2E = await this.getRoomInstance(rid); + return roomE2E.encryptFile(rid, attachment); + } catch { + // Subscription not found + // or client can't be initialized (missing password) + } + + // Send a non encrypted message + return attachment; + }; + // Decrypt multiple messages decryptMessages = (messages: Partial[]) => Promise.all(messages.map((m: Partial) => this.decryptMessage(m as IMessage))); diff --git a/app/lib/encryption/room.ts b/app/lib/encryption/room.ts index 18080f14e..541959c97 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, IUpload, IUser } from '../../definitions'; +import { IMessage, IShareAttachment, IUpload, IUser } from '../../definitions'; import Deferred from './helpers/deferred'; import { debounce } from '../methods/helpers'; import database from '../database'; @@ -15,6 +15,9 @@ import { bufferToB64, bufferToB64URI, bufferToUtf8, + encryptAESCTR, + exportAESCTR, + generateAESCTRKey, joinVectorData, splitVectorData, toString, @@ -269,6 +272,77 @@ export default class EncryptionRoom { return message; }; + // Encrypt file + encryptFile = async (rid: string, attachment: IShareAttachment) => { + if (!this.ready) { + return attachment; + } + + try { + const { path } = attachment; + const vector = await SimpleCrypto.utils.randomBytes(16); + const key = await generateAESCTRKey(); + const exportedKey = await exportAESCTR(key); + const encryptedFile = await encryptAESCTR(path, exportedKey.k, bufferToB64(vector)); + + const getContent = async (_id: string, fileUrl: string) => { + const attachments = []; + let att = { + title: attachment.filename, + type: attachment.type, + mime: attachment.type, + size: attachment.size, + description: attachment.description, + encryption: { + key: exportedKey, + iv: bufferToB64(vector) + } + }; + if (/^image\/.+/.test(attachment.type)) { + att = { + ...att, + image_url: fileUrl, + image_type: attachment.type, + image_size: attachment.size + }; + } else if (/^audio\/.+/.test(attachment.type)) { + att = { + ...att, + audio_url: fileUrl, + audio_type: attachment.type, + audio_size: attachment.size + }; + } else if (/^video\/.+/.test(attachment.type)) { + att = { + ...att, + video_url: fileUrl, + video_type: attachment.type, + video_size: attachment.size + }; + } + attachments.push(att); + + const data = EJSON.stringify({ + attachments + }); + + return { + algorithm: 'rc.v1.aes-sha2', + ciphertext: await Encryption.encryptText(rid, data) + }; + }; + + return { + encryptedFile, + getContent + }; + } catch { + // Do nothing + } + + return attachment; + }; + // Decrypt text decryptText = async (msg: string | ArrayBuffer) => { if (!msg) { diff --git a/app/lib/methods/sendFileMessage.ts b/app/lib/methods/sendFileMessage.ts index bc741d80a..315c9b06d 100644 --- a/app/lib/methods/sendFileMessage.ts +++ b/app/lib/methods/sendFileMessage.ts @@ -3,6 +3,7 @@ import { settings as RocketChatSettings } from '@rocket.chat/sdk'; import isEmpty from 'lodash/isEmpty'; import RNFetchBlob, { FetchBlobResponse, StatefulPromise } from 'rn-fetch-blob'; import { Alert } from 'react-native'; +import { sha256 } from 'js-sha256'; import { Encryption } from '../encryption'; import { IUpload, IUser, TUploadModel } from '../../definitions'; @@ -45,59 +46,69 @@ export async function cancelUpload(item: TUploadModel, rid: string): Promise { + const db = database.active; + const uploadsCollection = db.get('uploads'); + const uploadPath = getUploadPath(fileInfo.path, rid); + let uploadRecord: TUploadModel; + try { + uploadRecord = await uploadsCollection.find(uploadPath); + if (uploadRecord.id && !isForceTryAgain) { + return Alert.alert(i18n.t('FileUpload_Error'), i18n.t('Upload_in_progress')); + } + } catch (error) { + try { + await db.write(async () => { + uploadRecord = await uploadsCollection.create(u => { + u._raw = sanitizedRaw({ id: uploadPath }, uploadsCollection.schema); + Object.assign(u, fileInfo); + if (tmid) { + u.tmid = tmid; + } + if (u.subscription) { + u.subscription.id = rid; + } + }); + }); + } catch (e) { + throw e; + } + } +}; + export function sendFileMessage( rid: string, fileInfo: IUpload, tmid: string | undefined, server: string, user: Partial>, - isForceTryAgain?: boolean, - getContent?: Function + isForceTryAgain?: boolean ): Promise { return new Promise(async (resolve, reject) => { try { const { id, token } = user; - - const uploadUrl = `${server}/api/v1/rooms.media/${rid}`; - fileInfo.rid = rid; - - const db = database.active; - const uploadsCollection = db.get('uploads'); - const uploadPath = getUploadPath(fileInfo.path, rid); - let uploadRecord: TUploadModel; - try { - uploadRecord = await uploadsCollection.find(uploadPath); - if (uploadRecord.id && !isForceTryAgain) { - return Alert.alert(i18n.t('FileUpload_Error'), i18n.t('Upload_in_progress')); - } - } catch (error) { - try { - await db.write(async () => { - uploadRecord = await uploadsCollection.create(u => { - u._raw = sanitizedRaw({ id: uploadPath }, uploadsCollection.schema); - Object.assign(u, fileInfo); - if (tmid) { - u.tmid = tmid; - } - if (u.subscription) { - u.subscription.id = rid; - } - }); - }); - } catch (e) { - return log(e); - } - } - - // const encryptedFileInfo = await Encryption.encryptMessage(fileInfo); + fileInfo.path = fileInfo.path.startsWith('file://') ? fileInfo.path.substring(7) : fileInfo.path; + await createUploadRecord({ rid, fileInfo, tmid, isForceTryAgain }); + const encryptedFileInfo = await Encryption.encryptFile(rid, fileInfo); + const { encryptedFile, getContent } = encryptedFileInfo; const formData: IFileUpload[] = []; formData.push({ name: 'file', - type: fileInfo.type, - filename: fileInfo.name || 'fileMessage', - uri: fileInfo.path + type: 'file', + filename: sha256(fileInfo.name || 'fileMessage'), + uri: encryptedFile }); // if (fileInfo.description) { @@ -152,13 +163,12 @@ export function sendFileMessage( } return item; }); - const response = await RNFetchBlob.fetch('POST', uploadUrl, headers, data); + const response = await RNFetchBlob.fetch('POST', `${server}/api/v1/rooms.media/${rid}`, headers, data); const json = response.json(); let content; if (getContent) { content = await getContent(json.file._id, json.file.url); - console.log('🚀 ~ returnnewPromise ~ content:', content); } const mediaConfirm = await fetch(`${server}/api/v1/rooms.mediaConfirm/${rid}/${json.file._id}`, { diff --git a/app/views/ShareView/index.tsx b/app/views/ShareView/index.tsx index fc67ffdda..e8efb8918 100644 --- a/app/views/ShareView/index.tsx +++ b/app/views/ShareView/index.tsx @@ -5,9 +5,6 @@ import { Text, View } from 'react-native'; import { connect } from 'react-redux'; import ShareExtension from 'rn-extensions-share'; import { Q } from '@nozbe/watermelondb'; -import SimpleCrypto from 'react-native-simple-crypto'; -import EJSON from 'ejson'; -import { sha256 } from 'js-sha256'; import { IMessageComposerRef, MessageComposerContainer } from '../../containers/MessageComposer'; import { InsideStackParamList } from '../../stacks/types'; @@ -38,8 +35,6 @@ import { import { sendFileMessage, sendMessage } from '../../lib/methods'; import { hasPermission, isAndroid, canUploadFile, isReadOnly, isBlocked } from '../../lib/methods/helpers'; import { RoomContext } from '../RoomView/context'; -import { Encryption } from '../../lib/encryption'; -import { bufferToB64, encryptAESCTR, exportAESCTR, generateAESCTRKey } from '../../lib/encryption/utils'; interface IShareViewState { selected: IShareAttachment; @@ -257,70 +252,27 @@ class ShareView extends Component { // Send attachment if (attachments.length) { await Promise.all( - attachments.map(async ({ filename: name, mime: type, description, size, canUpload }) => { + attachments.map(({ filename: name, mime: type, description, size, path, canUpload }) => { if (!canUpload) { return Promise.resolve(); } - try { - const { path } = attachments[0]; - const vector = await SimpleCrypto.utils.randomBytes(16); - const key = await generateAESCTRKey(); - const exportedKey = await exportAESCTR(key); - const encryptedFile = await encryptAESCTR(path, exportedKey.k, bufferToB64(vector)); - - const getContent = async (_id: string, fileUrl: string) => { - const attachments = []; - - const attachment = { - title: name, - type: 'file', - description, - // title_link: fileUrl, - // title_link_download: true, - encryption: { - key: exportedKey, - iv: bufferToB64(vector) - }, - image_url: fileUrl, - image_type: type, - image_size: size - }; - attachments.push(attachment); - - const data = EJSON.stringify({ - attachments - }); - - return { - algorithm: 'rc.v1.aes-sha2', - ciphertext: await Encryption.encryptText(room.rid, data) - }; - }; - - // Send the file message with the encrypted path - return sendFileMessage( - room.rid, - { - rid: room.rid, - name: sha256(name), - description, - size, - type: 'file', - path: encryptedFile, - store: 'Uploads', - msg - }, - thread?.id, - server, - { id: user.id, token: user.token }, - undefined, - getContent - ); - } catch (e) { - console.error(e); - return Promise.reject(e); - } + return sendFileMessage( + room.rid, + { + rid: room.rid, + name, + description, + size, + type, + path, + store: 'Uploads', + msg + }, + thread?.id, + server, + { id: user.id, token: user.token } + ); }) );