import * as crypto from 'crypto'; import { Attachment } from '../../types/Attachment'; import { AttachmentPointer, AttachmentPointerWithUrl, PreviewWithAttachmentUrl, Quote, QuotedAttachmentWithUrl, } from '../messages/outgoing/visibleMessage/VisibleMessage'; import { addAttachmentPadding } from '../crypto/BufferPadding'; import _ from 'lodash'; import { encryptAttachment } from '../../util/crypto/attachmentsEncrypter'; import { uploadFileToFsWithOnionV4 } from '../apis/file_server_api/FileServerApi'; interface UploadParams { attachment: Attachment; isAvatar?: boolean; isRaw?: boolean; shouldPad?: boolean; } export interface RawPreview { url: string; title?: string; image: Attachment; } export interface RawQuoteAttachment { contentType?: string; fileName?: string; thumbnail?: Attachment; } export interface RawQuote { id: number; author: string; text?: string; attachments?: Array; } async function uploadToFileServer(params: UploadParams): Promise { const { attachment, isRaw = false, shouldPad = false } = params; if (typeof attachment !== 'object' || attachment == null) { throw new Error('Invalid attachment passed.'); } if (!(attachment.data instanceof ArrayBuffer)) { throw new TypeError( `\`attachment.data\` must be an \`ArrayBuffer\`; got: ${typeof attachment.data}` ); } const pointer: AttachmentPointer = { contentType: attachment.contentType || undefined, size: attachment.size, fileName: attachment.fileName, flags: attachment.flags, caption: attachment.caption, width: attachment.width, height: attachment.height, }; let attachmentData: ArrayBuffer; if (isRaw) { attachmentData = attachment.data; } else { pointer.key = new Uint8Array(crypto.randomBytes(64)); const iv = new Uint8Array(crypto.randomBytes(16)); const dataToEncrypt = !shouldPad ? attachment.data : addAttachmentPadding(attachment.data); const data = await encryptAttachment(dataToEncrypt, pointer.key.buffer, iv.buffer); pointer.digest = new Uint8Array(data.digest); attachmentData = data.ciphertext; } // use file server v2 const uploadToV2Result = await uploadFileToFsWithOnionV4(attachmentData); if (uploadToV2Result) { const pointerWithUrl: AttachmentPointerWithUrl = { ...pointer, id: uploadToV2Result.fileId, url: uploadToV2Result.fileUrl, }; return pointerWithUrl; } window?.log?.warn('upload to file server v2 failed'); throw new Error(`upload to file server v2 of ${attachment.fileName} failed`); } export async function uploadAttachmentsToFileServer( attachments: Array ): Promise> { const promises = (attachments || []).map(async attachment => uploadToFileServer({ attachment, shouldPad: true, }) ); return Promise.all(promises); } export async function uploadLinkPreviewToFileServer( preview: RawPreview | null ): Promise { // some links do not have an image associated, and it makes the whole message fail to send if (!preview?.image) { window.log.warn('tried to upload file to FileServer without image.. skipping'); return preview as any; } const image = await uploadToFileServer({ attachment: preview.image, }); return { ...preview, image, id: image.id, }; } export async function uploadQuoteThumbnailsToFileServer( quote?: RawQuote ): Promise { if (!quote) { return undefined; } const promises = (quote.attachments ?? []).map(async attachment => { let thumbnail: AttachmentPointer | undefined; if (attachment.thumbnail) { thumbnail = await uploadToFileServer({ attachment: attachment.thumbnail, }); } if (!thumbnail) { return attachment; } return { ...attachment, thumbnail, url: thumbnail.url, id: thumbnail.id, } as QuotedAttachmentWithUrl; }); const attachments = _.compact(await Promise.all(promises)); return { ...quote, attachments, }; }