Allow upload of non-encrypted files again

This commit is contained in:
Diego Mello 2024-05-22 18:08:42 -03:00
parent 628c3fc764
commit b8db541231
8 changed files with 222 additions and 194 deletions

View File

@ -5,19 +5,23 @@ export interface IAttachment {
ts?: string | Date; ts?: string | Date;
title?: string; title?: string;
type?: string; type?: string;
size?: number;
description?: string; description?: string;
title_link?: string; title_link?: string;
image_url?: string; image_url?: string;
image_type?: string; image_type?: string;
image_size?: number;
image_dimensions?: { width?: number; height?: number };
image_preview?: string;
video_url?: string; video_url?: string;
video_type?: string; video_type?: string;
video_size?: number;
audio_url?: string; audio_url?: string;
audio_type?: string;
audio_size?: number;
title_link_download?: boolean; title_link_download?: boolean;
attachments?: IAttachment[]; attachments?: IAttachment[];
fields?: IAttachment[]; fields?: IAttachment[];
image_dimensions?: { width?: number; height?: number };
image_preview?: string;
image_size?: number;
author_name?: string; author_name?: string;
author_icon?: string; author_icon?: string;
actions?: { type: string; msg: string; text: string }[]; actions?: { type: string; msg: string; text: string }[];
@ -29,8 +33,11 @@ export interface IAttachment {
color?: string; color?: string;
thumb_url?: string; thumb_url?: string;
collapsed?: boolean; collapsed?: boolean;
audio_type?: string;
translations?: IAttachmentTranslations; translations?: IAttachmentTranslations;
encryption?: {
iv: string;
key: any; // JsonWebKey
};
} }
export interface IServerAttachment { export interface IServerAttachment {

View File

@ -24,3 +24,18 @@ export type TUploadModel = IUpload &
Model & { Model & {
asPlain: () => IUpload; 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;
}

View File

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

View File

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

View File

@ -4,6 +4,7 @@ import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord';
import { Q, Model } from '@nozbe/watermelondb'; import { Q, Model } from '@nozbe/watermelondb';
import UserPreferences from '../methods/userPreferences'; import UserPreferences from '../methods/userPreferences';
import { getSubscriptionByRoomId } from '../database/services/Subscription';
import database from '../database'; import database from '../database';
import protectedFunction from '../methods/helpers/protectedFunction'; import protectedFunction from '../methods/helpers/protectedFunction';
import Deferred from './helpers/deferred'; import Deferred from './helpers/deferred';
@ -16,6 +17,7 @@ import {
IMessage, IMessage,
ISubscription, ISubscription,
IUpload, IUpload,
IUploadFile,
TMessageModel, TMessageModel,
TSubscriptionModel, TSubscriptionModel,
TThreadMessageModel, TThreadMessageModel,
@ -31,6 +33,7 @@ import {
} from '../constants'; } from '../constants';
import { Services } from '../services'; import { Services } from '../services';
import { compareServerVersion } from '../methods/helpers'; import { compareServerVersion } from '../methods/helpers';
import { TEncryptFile } from './definitions';
class Encryption { class Encryption {
ready: boolean; ready: boolean;
@ -45,7 +48,7 @@ class Encryption {
decrypt: Function; decrypt: Function;
encrypt: Function; encrypt: Function;
encryptText: Function; encryptText: Function;
encryptFile: Function; encryptFile: TEncryptFile;
encryptUpload: Function; encryptUpload: Function;
importRoomKey: Function; importRoomKey: Function;
}; };
@ -453,7 +456,7 @@ class Encryption {
}; };
// Encrypt a message // Encrypt a message
encryptMessage = async (message: IMessage | IUpload) => { encryptMessage = async (message: IMessage) => {
const { rid } = message; const { rid } = message;
const db = database.active; const db = database.active;
const subCollection = db.get('subscriptions'); const subCollection = db.get('subscriptions');
@ -516,18 +519,15 @@ class Encryption {
return roomE2E.decrypt(message); return roomE2E.decrypt(message);
}; };
encryptFile = async (rid: string, attachment: IAttachment) => { encryptFile = async (rid: string, file: IUploadFile) => {
const db = database.active; const subscription = await getSubscriptionByRoomId(rid);
const subCollection = db.get('subscriptions'); if (!subscription) {
throw new Error('Subscription not found');
}
try { if (!subscription.encrypted) {
// Find the subscription
const subRecord = await subCollection.find(rid);
// Subscription is not encrypted at the moment
if (!subRecord.encrypted) {
// Send a non encrypted message // Send a non encrypted message
return attachment; return { file };
} }
// If the client is not ready // If the client is not ready
@ -537,14 +537,7 @@ class Encryption {
} }
const roomE2E = await this.getRoomInstance(rid); const roomE2E = await this.getRoomInstance(rid);
return roomE2E.encryptFile(rid, attachment); return roomE2E.encryptFile(rid, file);
} catch {
// Subscription not found
// or client can't be initialized (missing password)
}
// Send a non encrypted message
return attachment;
}; };
// Decrypt multiple messages // Decrypt multiple messages

View File

@ -3,9 +3,10 @@ import { Base64 } from 'js-base64';
import SimpleCrypto from 'react-native-simple-crypto'; import SimpleCrypto from 'react-native-simple-crypto';
import ByteBuffer from 'bytebuffer'; import ByteBuffer from 'bytebuffer';
import parse from 'url-parse'; import parse from 'url-parse';
import { sha256 } from 'js-sha256';
import getSingleMessage from '../methods/getSingleMessage'; 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 Deferred from './helpers/deferred';
import { debounce } from '../methods/helpers'; import { debounce } from '../methods/helpers';
import database from '../database'; import database from '../database';
@ -31,6 +32,7 @@ import { mapMessageFromAPI } from './helpers/mapMessageFromApi';
import { mapMessageFromDB } from './helpers/mapMessageFromDB'; import { mapMessageFromDB } from './helpers/mapMessageFromDB';
import { createQuoteAttachment } from './helpers/createQuoteAttachment'; import { createQuoteAttachment } from './helpers/createQuoteAttachment';
import { getMessageById } from '../database/services/Message'; import { getMessageById } from '../database/services/Message';
import { TEncryptFile, TEncryptFileResult, TGetContent } from './definitions';
export default class EncryptionRoom { export default class EncryptionRoom {
ready: boolean; ready: boolean;
@ -272,52 +274,45 @@ export default class EncryptionRoom {
return message; return message;
}; };
// Encrypt file encryptFile = async (rid: string, file: IUploadFile): TEncryptFileResult => {
encryptFile = async (rid: string, attachment: IShareAttachment) => { const { path } = file;
if (!this.ready) {
return attachment;
}
try {
const { path } = attachment;
const vector = await SimpleCrypto.utils.randomBytes(16); const vector = await SimpleCrypto.utils.randomBytes(16);
const key = await generateAESCTRKey(); const key = await generateAESCTRKey();
const exportedKey = await exportAESCTR(key); const exportedKey = await exportAESCTR(key);
const encryptedFile = await encryptAESCTR(path, exportedKey.k, bufferToB64(vector)); const encryptedFile = await encryptAESCTR(path, exportedKey.k, bufferToB64(vector));
const getContent = async (_id: string, fileUrl: string) => { const getContent: TGetContent = async (_id, fileUrl) => {
const attachments = []; const attachments: IAttachment[] = [];
let att = { let att: IAttachment = {
title: attachment.name, title: file.name,
type: 'file', type: 'file',
// mime: attachment.type, size: file.size,
size: attachment.size, description: file.description,
description: attachment.description,
encryption: { encryption: {
key: exportedKey, key: exportedKey,
iv: bufferToB64(vector) iv: bufferToB64(vector)
} }
}; };
if (/^image\/.+/.test(attachment.type)) { if (/^image\/.+/.test(file.type)) {
att = { att = {
...att, ...att,
image_url: fileUrl, image_url: fileUrl,
image_type: attachment.type, image_type: file.type,
image_size: attachment.size image_size: file.size
}; };
} else if (/^audio\/.+/.test(attachment.type)) { } else if (/^audio\/.+/.test(file.type)) {
att = { att = {
...att, ...att,
audio_url: fileUrl, audio_url: fileUrl,
audio_type: attachment.type, audio_type: file.type,
audio_size: attachment.size audio_size: file.size
}; };
} else if (/^video\/.+/.test(attachment.type)) { } else if (/^video\/.+/.test(file.type)) {
att = { att = {
...att, ...att,
video_url: fileUrl, video_url: fileUrl,
video_type: attachment.type, video_type: file.type,
video_size: attachment.size video_size: file.size
}; };
} }
attachments.push(att); attachments.push(att);
@ -333,14 +328,14 @@ export default class EncryptionRoom {
}; };
return { return {
encryptedFile, file: {
...file,
type: 'file',
name: sha256(file.name ?? 'File message'),
path: encryptedFile
},
getContent getContent
}; };
} catch {
// Do nothing
}
return attachment;
}; };
// Decrypt text // Decrypt text

View File

@ -222,12 +222,15 @@ export function downloadMediaFile({
return reject(); return reject();
} }
if (encryption) {
const decryptedFile = await decryptAESCTR(result.uri, encryption.key.k, encryption.iv); const decryptedFile = await decryptAESCTR(result.uri, encryption.key.k, encryption.iv);
console.log('🚀 ~ returnnewPromise ~ decryptedFile:', decryptedFile); console.log('🚀 ~ returnnewPromise ~ decryptedFile:', decryptedFile);
if (decryptedFile) { if (decryptedFile) {
return resolve(decryptedFile); return resolve(decryptedFile);
} }
return reject(); return reject();
}
return resolve(result.uri);
} catch (e) { } catch (e) {
console.error(e); console.error(e);
return reject(); return reject();

View File

@ -3,10 +3,10 @@ import { settings as RocketChatSettings } from '@rocket.chat/sdk';
import isEmpty from 'lodash/isEmpty'; import isEmpty from 'lodash/isEmpty';
import RNFetchBlob, { FetchBlobResponse, StatefulPromise } from 'rn-fetch-blob'; import RNFetchBlob, { FetchBlobResponse, StatefulPromise } from 'rn-fetch-blob';
import { Alert } from 'react-native'; import { Alert } from 'react-native';
import { sha256 } from 'js-sha256';
import { getUploadByPath } from '../database/services/Upload';
import { Encryption } from '../encryption'; import { Encryption } from '../encryption';
import { IUpload, IUser, TUploadModel } from '../../definitions'; import { IUpload, IUploadFile, IUser, TUploadModel } from '../../definitions';
import i18n from '../../i18n'; import i18n from '../../i18n';
import database from '../database'; import database from '../database';
import log from './helpers/log'; import log from './helpers/log';
@ -41,13 +41,21 @@ export async function cancelUpload(item: TUploadModel, rid: string): Promise<voi
} }
} }
const persistUploadError = async (uploadRecord: TUploadModel) => { const persistUploadError = async (path: string, rid: string) => {
try {
const db = database.active; const db = database.active;
const uploadRecord = await getUploadByPath(getUploadPath(path, rid));
if (!uploadRecord) {
return;
}
await db.write(async () => { await db.write(async () => {
await uploadRecord.update(u => { await uploadRecord.update(u => {
u.error = true; u.error = true;
}); });
}); });
} catch {
// Do nothing
}
}; };
const createUploadRecord = async ({ const createUploadRecord = async ({
@ -94,52 +102,41 @@ const createUploadRecord = async ({
const normalizeFilePath = (path: string) => (path.startsWith('file://') ? path.substring(7) : path); const normalizeFilePath = (path: string) => (path.startsWith('file://') ? path.substring(7) : path);
export function sendFileMessage( export async function sendFileMessage(
rid: string, rid: string,
fileInfo: IUpload, fileInfo: IUploadFile,
tmid: string | undefined, tmid: string | undefined,
server: string, server: string,
user: Partial<Pick<IUser, 'id' | 'token'>>, user: Partial<Pick<IUser, 'id' | 'token'>>,
isForceTryAgain?: boolean isForceTryAgain?: boolean
): Promise<FetchBlobResponse | void> { ): Promise<FetchBlobResponse | void> {
return new Promise(async (resolve, reject) => {
try { try {
console.log('sendFileMessage', rid, fileInfo); console.log('sendFileMessage', rid, fileInfo);
const { id, token } = user; const { id, token } = user;
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 = { const headers = {
...RocketChatSettings.customHeaders, ...RocketChatSettings.customHeaders,
'Content-Type': 'multipart/form-data', 'Content-Type': 'multipart/form-data',
'X-Auth-Token': token, 'X-Auth-Token': token,
'X-User-Id': id 'X-User-Id': id
}; };
const db = database.active; const db = database.active;
const data = [ fileInfo.path = normalizeFilePath(fileInfo.path);
{
name: 'file', const [uploadPath, uploadRecord] = await createUploadRecord({ rid, fileInfo, tmid, isForceTryAgain });
type: 'file', if (!uploadPath || !uploadRecord) {
filename: sha256(fileInfo.name || 'fileMessage'), throw new Error("Couldn't create upload record");
data: RNFetchBlob.wrap(decodeURI(normalizeFilePath(encryptedFile)))
} }
]; const { file, getContent } = await Encryption.encryptFile(rid, fileInfo);
// @ts-ignore // @ts-ignore
uploadQueue[uploadPath] = RNFetchBlob.fetch('POST', `${server}/api/v1/rooms.media/${rid}`, headers, data) 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) => { .uploadProgress(async (loaded: number, total: number) => {
await db.write(async () => { await db.write(async () => {
await uploadRecord.update(u => { await uploadRecord.update(u => {
@ -162,29 +159,18 @@ export function sendFileMessage(
body: JSON.stringify({ body: JSON.stringify({
// msg: '', TODO: backwards compatibility // msg: '', TODO: backwards compatibility
tmid: tmid ?? undefined, tmid: tmid ?? undefined,
description: fileInfo.description, description: file.description,
t: 'e2e', t: content ? 'e2e' : undefined,
content content
}) })
}).then(async () => { }).then(async () => {
console.log('destroy destroy destroy destroy ');
await db.write(async () => { await db.write(async () => {
await uploadRecord.destroyPermanently(); 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) { } catch (e) {
reject(e); await persistUploadError(fileInfo.path, rid);
throw e;
} }
});
} }