Allow upload of non-encrypted files again
This commit is contained in:
parent
628c3fc764
commit
b8db541231
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
};
|
|
@ -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;
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue