diff --git a/app/definitions/IAttachment.ts b/app/definitions/IAttachment.ts index 5001c54e4..da718909a 100644 --- a/app/definitions/IAttachment.ts +++ b/app/definitions/IAttachment.ts @@ -5,19 +5,23 @@ export interface IAttachment { ts?: string | Date; title?: string; type?: string; + size?: number; description?: string; title_link?: string; image_url?: string; image_type?: string; + image_size?: number; + image_dimensions?: { width?: number; height?: number }; + image_preview?: string; video_url?: string; video_type?: string; + video_size?: number; audio_url?: string; + audio_type?: string; + audio_size?: number; title_link_download?: boolean; attachments?: IAttachment[]; fields?: IAttachment[]; - image_dimensions?: { width?: number; height?: number }; - image_preview?: string; - image_size?: number; author_name?: string; author_icon?: string; actions?: { type: string; msg: string; text: string }[]; @@ -29,8 +33,11 @@ export interface IAttachment { color?: string; thumb_url?: string; collapsed?: boolean; - audio_type?: string; translations?: IAttachmentTranslations; + encryption?: { + iv: string; + key: any; // JsonWebKey + }; } export interface IServerAttachment { diff --git a/app/definitions/IUpload.ts b/app/definitions/IUpload.ts index 9931122c2..38cadb0d6 100644 --- a/app/definitions/IUpload.ts +++ b/app/definitions/IUpload.ts @@ -24,3 +24,18 @@ export type TUploadModel = IUpload & Model & { asPlain: () => IUpload; }; + +export interface IUploadFile { + rid: string; + path: string; + name?: string; + tmid?: string; + description?: string; + size: number; + type: string; + // store?: string; + // progress?: number; + msg?: string; + // t?: MessageType; + // e2e?: E2EType; +} diff --git a/app/lib/database/services/Upload.ts b/app/lib/database/services/Upload.ts new file mode 100644 index 000000000..4da1ece9f --- /dev/null +++ b/app/lib/database/services/Upload.ts @@ -0,0 +1,16 @@ +import database from '..'; +import { TAppDatabase } from '../interfaces'; +import { UPLOADS_TABLE } from '../model'; + +const getCollection = (db: TAppDatabase) => db.get(UPLOADS_TABLE); + +export const getUploadByPath = async (path: string) => { + const db = database.active; + const uploadCollection = getCollection(db); + try { + const result = await uploadCollection.find(path); + return result; + } catch (error) { + return null; + } +}; diff --git a/app/lib/encryption/definitions.ts b/app/lib/encryption/definitions.ts new file mode 100644 index 000000000..4b7b1ce77 --- /dev/null +++ b/app/lib/encryption/definitions.ts @@ -0,0 +1,13 @@ +import { IUploadFile } from '../../definitions'; + +export type TGetContent = ( + _id: string, + fileUrl: string +) => Promise<{ + algorithm: 'rc.v1.aes-sha2'; + ciphertext: string; +}>; + +export type TEncryptFileResult = Promise<{ file: IUploadFile; getContent?: TGetContent }>; + +export type TEncryptFile = (rid: string, file: IUploadFile) => TEncryptFileResult; diff --git a/app/lib/encryption/encryption.ts b/app/lib/encryption/encryption.ts index 3c0a4b88c..a47dc87fa 100644 --- a/app/lib/encryption/encryption.ts +++ b/app/lib/encryption/encryption.ts @@ -4,6 +4,7 @@ import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord'; import { Q, Model } from '@nozbe/watermelondb'; import UserPreferences from '../methods/userPreferences'; +import { getSubscriptionByRoomId } from '../database/services/Subscription'; import database from '../database'; import protectedFunction from '../methods/helpers/protectedFunction'; import Deferred from './helpers/deferred'; @@ -16,6 +17,7 @@ import { IMessage, ISubscription, IUpload, + IUploadFile, TMessageModel, TSubscriptionModel, TThreadMessageModel, @@ -31,6 +33,7 @@ import { } from '../constants'; import { Services } from '../services'; import { compareServerVersion } from '../methods/helpers'; +import { TEncryptFile } from './definitions'; class Encryption { ready: boolean; @@ -45,7 +48,7 @@ class Encryption { decrypt: Function; encrypt: Function; encryptText: Function; - encryptFile: Function; + encryptFile: TEncryptFile; encryptUpload: Function; importRoomKey: Function; }; @@ -453,7 +456,7 @@ class Encryption { }; // Encrypt a message - encryptMessage = async (message: IMessage | IUpload) => { + encryptMessage = async (message: IMessage) => { const { rid } = message; const db = database.active; const subCollection = db.get('subscriptions'); @@ -516,35 +519,25 @@ 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) + encryptFile = async (rid: string, file: IUploadFile) => { + const subscription = await getSubscriptionByRoomId(rid); + if (!subscription) { + throw new Error('Subscription not found'); } - // Send a non encrypted message - return attachment; + if (!subscription.encrypted) { + // Send a non encrypted message + return { file }; + } + + // 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, file); }; // Decrypt multiple messages diff --git a/app/lib/encryption/room.ts b/app/lib/encryption/room.ts index c6d12d201..919165f1a 100644 --- a/app/lib/encryption/room.ts +++ b/app/lib/encryption/room.ts @@ -3,9 +3,10 @@ import { Base64 } from 'js-base64'; import SimpleCrypto from 'react-native-simple-crypto'; import ByteBuffer from 'bytebuffer'; import parse from 'url-parse'; +import { sha256 } from 'js-sha256'; import getSingleMessage from '../methods/getSingleMessage'; -import { IMessage, IShareAttachment, IUpload, IUser } from '../../definitions'; +import { IAttachment, IMessage, IShareAttachment, IUpload, IUploadFile, IUser } from '../../definitions'; import Deferred from './helpers/deferred'; import { debounce } from '../methods/helpers'; import database from '../database'; @@ -31,6 +32,7 @@ import { mapMessageFromAPI } from './helpers/mapMessageFromApi'; import { mapMessageFromDB } from './helpers/mapMessageFromDB'; import { createQuoteAttachment } from './helpers/createQuoteAttachment'; import { getMessageById } from '../database/services/Message'; +import { TEncryptFile, TEncryptFileResult, TGetContent } from './definitions'; export default class EncryptionRoom { ready: boolean; @@ -272,75 +274,68 @@ export default class EncryptionRoom { return message; }; - // Encrypt file - encryptFile = async (rid: string, attachment: IShareAttachment) => { - if (!this.ready) { - return attachment; - } + encryptFile = async (rid: string, file: IUploadFile): TEncryptFileResult => { + const { path } = file; + 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)); - 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.name, - type: 'file', - // 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 - }; + const getContent: TGetContent = async (_id, fileUrl) => { + const attachments: IAttachment[] = []; + let att: IAttachment = { + title: file.name, + type: 'file', + size: file.size, + description: file.description, + encryption: { + key: exportedKey, + iv: bufferToB64(vector) } - attachments.push(att); - - const data = EJSON.stringify({ - attachments - }); - - return { - algorithm: 'rc.v1.aes-sha2', - ciphertext: await Encryption.encryptText(rid, data) - }; }; + if (/^image\/.+/.test(file.type)) { + att = { + ...att, + image_url: fileUrl, + image_type: file.type, + image_size: file.size + }; + } else if (/^audio\/.+/.test(file.type)) { + att = { + ...att, + audio_url: fileUrl, + audio_type: file.type, + audio_size: file.size + }; + } else if (/^video\/.+/.test(file.type)) { + att = { + ...att, + video_url: fileUrl, + video_type: file.type, + video_size: file.size + }; + } + attachments.push(att); + + const data = EJSON.stringify({ + attachments + }); return { - encryptedFile, - getContent + algorithm: 'rc.v1.aes-sha2', + ciphertext: await Encryption.encryptText(rid, data) }; - } catch { - // Do nothing - } + }; - return attachment; + return { + file: { + ...file, + type: 'file', + name: sha256(file.name ?? 'File message'), + path: encryptedFile + }, + getContent + }; }; // Decrypt text diff --git a/app/lib/methods/handleMediaDownload.ts b/app/lib/methods/handleMediaDownload.ts index 690c9435f..ee6bbfc04 100644 --- a/app/lib/methods/handleMediaDownload.ts +++ b/app/lib/methods/handleMediaDownload.ts @@ -222,12 +222,15 @@ export function downloadMediaFile({ return reject(); } - const decryptedFile = await decryptAESCTR(result.uri, encryption.key.k, encryption.iv); - console.log('🚀 ~ returnnewPromise ~ decryptedFile:', decryptedFile); - if (decryptedFile) { - return resolve(decryptedFile); + if (encryption) { + const decryptedFile = await decryptAESCTR(result.uri, encryption.key.k, encryption.iv); + console.log('🚀 ~ returnnewPromise ~ decryptedFile:', decryptedFile); + if (decryptedFile) { + return resolve(decryptedFile); + } + return reject(); } - return reject(); + return resolve(result.uri); } catch (e) { console.error(e); return reject(); diff --git a/app/lib/methods/sendFileMessage.ts b/app/lib/methods/sendFileMessage.ts index 5fdc07cbc..96691e03a 100644 --- a/app/lib/methods/sendFileMessage.ts +++ b/app/lib/methods/sendFileMessage.ts @@ -3,10 +3,10 @@ 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 { getUploadByPath } from '../database/services/Upload'; import { Encryption } from '../encryption'; -import { IUpload, IUser, TUploadModel } from '../../definitions'; +import { IUpload, IUploadFile, IUser, TUploadModel } from '../../definitions'; import i18n from '../../i18n'; import database from '../database'; import log from './helpers/log'; @@ -41,13 +41,21 @@ export async function cancelUpload(item: TUploadModel, rid: string): Promise { - const db = database.active; - await db.write(async () => { - await uploadRecord.update(u => { - u.error = true; +const persistUploadError = async (path: string, rid: string) => { + try { + const db = database.active; + const uploadRecord = await getUploadByPath(getUploadPath(path, rid)); + if (!uploadRecord) { + return; + } + await db.write(async () => { + await uploadRecord.update(u => { + u.error = true; + }); }); - }); + } catch { + // Do nothing + } }; const createUploadRecord = async ({ @@ -94,97 +102,75 @@ const createUploadRecord = async ({ const normalizeFilePath = (path: string) => (path.startsWith('file://') ? path.substring(7) : path); -export function sendFileMessage( +export async function sendFileMessage( rid: string, - fileInfo: IUpload, + fileInfo: IUploadFile, tmid: string | undefined, server: string, user: Partial>, isForceTryAgain?: boolean ): Promise { - return new Promise(async (resolve, reject) => { - try { - console.log('sendFileMessage', rid, fileInfo); - const { id, token } = user; - fileInfo.path = normalizeFilePath(fileInfo.path); + try { + console.log('sendFileMessage', rid, fileInfo); + const { id, token } = user; + const headers = { + ...RocketChatSettings.customHeaders, + 'Content-Type': 'multipart/form-data', + 'X-Auth-Token': token, + 'X-User-Id': id + }; + const db = database.active; + fileInfo.path = normalizeFilePath(fileInfo.path); - const [uploadPath, uploadRecord] = await createUploadRecord({ rid, fileInfo, tmid, isForceTryAgain }); - if (!uploadPath || !uploadRecord) { - return; - } - const encryptedFileInfo = await Encryption.encryptFile(rid, fileInfo); - const { encryptedFile, getContent } = encryptedFileInfo; - - // TODO: temp until I bring back non encrypted uploads - if (!encryptedFile) { - await persistUploadError(uploadRecord); - throw new Error('Error while encrypting file'); - } - - const headers = { - ...RocketChatSettings.customHeaders, - 'Content-Type': 'multipart/form-data', - 'X-Auth-Token': token, - 'X-User-Id': id - }; - - const db = database.active; - const data = [ - { - name: 'file', - type: 'file', - filename: sha256(fileInfo.name || 'fileMessage'), - data: RNFetchBlob.wrap(decodeURI(normalizeFilePath(encryptedFile))) - } - ]; - - // @ts-ignore - uploadQueue[uploadPath] = RNFetchBlob.fetch('POST', `${server}/api/v1/rooms.media/${rid}`, headers, data) - .uploadProgress(async (loaded: number, total: number) => { - await db.write(async () => { - await uploadRecord.update(u => { - u.progress = Math.floor((loaded / total) * 100); - }); - }); - }) - .then(async response => { - const json = response.json(); - let content; - if (getContent) { - content = await getContent(json.file._id, json.file.url); - } - fetch(`${server}/api/v1/rooms.mediaConfirm/${rid}/${json.file._id}`, { - method: 'POST', - headers: { - ...headers, - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - // msg: '', TODO: backwards compatibility - tmid: tmid ?? undefined, - description: fileInfo.description, - t: 'e2e', - content - }) - }).then(async () => { - console.log('destroy destroy destroy destroy '); - await db.write(async () => { - await uploadRecord.destroyPermanently(); - }); - }); - resolve(response); - }) - .catch(async error => { - console.log('catch catch catch catch catch '); - await db.write(async () => { - await uploadRecord.update(u => { - u.error = true; - }); - }); - throw error; - }); - } catch (e) { - reject(e); + const [uploadPath, uploadRecord] = await createUploadRecord({ rid, fileInfo, tmid, isForceTryAgain }); + if (!uploadPath || !uploadRecord) { + throw new Error("Couldn't create upload record"); } - }); + const { file, getContent } = await Encryption.encryptFile(rid, fileInfo); + + // @ts-ignore + uploadQueue[uploadPath] = RNFetchBlob.fetch('POST', `${server}/api/v1/rooms.media/${rid}`, headers, [ + { + name: 'file', + type: file.type, + filename: file.name, + data: RNFetchBlob.wrap(decodeURI(normalizeFilePath(file.path))) + } + ]) + .uploadProgress(async (loaded: number, total: number) => { + await db.write(async () => { + await uploadRecord.update(u => { + u.progress = Math.floor((loaded / total) * 100); + }); + }); + }) + .then(async response => { + const json = response.json(); + let content; + if (getContent) { + content = await getContent(json.file._id, json.file.url); + } + fetch(`${server}/api/v1/rooms.mediaConfirm/${rid}/${json.file._id}`, { + method: 'POST', + headers: { + ...headers, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + // msg: '', TODO: backwards compatibility + tmid: tmid ?? undefined, + description: file.description, + t: content ? 'e2e' : undefined, + content + }) + }).then(async () => { + await db.write(async () => { + await uploadRecord.destroyPermanently(); + }); + }); + }); + } catch (e) { + await persistUploadError(fileInfo.path, rid); + throw e; + } }