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;
|
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 {
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
@ -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 { 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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue