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

View File

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

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

View File

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

View File

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

View File

@ -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<voi
}
}
const persistUploadError = async (uploadRecord: TUploadModel) => {
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<Pick<IUser, 'id' | 'token'>>,
isForceTryAgain?: boolean
): Promise<FetchBlobResponse | void> {
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;
}
}