From 7782ace9c425fbdf2914ff93408162de8c8dbd26 Mon Sep 17 00:00:00 2001 From: Roland Osborne Date: Wed, 26 Apr 2023 13:05:55 -0700 Subject: [PATCH] encrypt and upload asset slices --- net/web/src/context/sealUtil.js | 21 ++++++ net/web/src/context/useUploadContext.hook.js | 38 ++++++++-- .../conversation/addTopic/useAddTopic.hook.js | 71 +++++++++++-------- 3 files changed, 96 insertions(+), 34 deletions(-) diff --git a/net/web/src/context/sealUtil.js b/net/web/src/context/sealUtil.js index 32f83157..5483a501 100644 --- a/net/web/src/context/sealUtil.js +++ b/net/web/src/context/sealUtil.js @@ -56,6 +56,27 @@ export function updateChannelSubject(subject, contentKey) { return { subjectEncrypted, subjectIv }; } +export function encryptBlock(block, contentKey) { + const key = CryptoJS.enc.Hex.parse(contentKey); + const iv = CryptoJS.lib.WordArray.random(128 / 8); + const encrypted = CryptoJS.AES.encrypt(block, key, { iv: iv }); + const blockEncrypted = encrypted.ciphertext.toString(CryptoJS.enc.Base64) + const blockIv = iv.toString(); + + return { blockEncrypted, blockIv }; +} + +export function decryptBlock(blockEncrypted, blockIv, contentKey) { + const iv = CryptoJS.enc.Hex.parse(blockIv); + const key = CryptoJS.enc.Hex.parse(contentKey); + const enc = CryptoJS.enc.Base64.parse(blockEncrypted); + const cipher = CryptoJS.lib.CipherParams.create({ ciphertext: enc, iv: iv }); + const dec = CryptoJS.AES.decrypt(cipher, key, { iv: iv }); + const block = dec.toString(CryptoJS.enc.Utf8); + + return block; +} + export function decryptChannelSubject(subject, contentKey) { const { subjectEncrypted, subjectIv } = JSON.parse(subject); const iv = CryptoJS.enc.Hex.parse(subjectIv); diff --git a/net/web/src/context/useUploadContext.hook.js b/net/web/src/context/useUploadContext.hook.js index 9f2174e5..d685a4b4 100644 --- a/net/web/src/context/useUploadContext.hook.js +++ b/net/web/src/context/useUploadContext.hook.js @@ -1,6 +1,8 @@ import { useState, useRef } from 'react'; import axios from 'axios'; +const ENCRYPTED_BLOCK_SIZE = (128 * 1024); //110k + export function useUploadContext() { const [state, setState] = useState({ @@ -69,7 +71,8 @@ export function useUploadContext() { const controller = new AbortController(); const entry = { index: index.current, - url: `${host}/content/channels/${channelId}/topics/${topicId}/assets?contact=${token}`, + baseUrl: `${host}/content/channels/${channelId}/topics/${topicId}/`, + urlParams: `?contact=${token}`, files, assets: [], current: null, @@ -91,7 +94,8 @@ export function useUploadContext() { const controller = new AbortController(); const entry = { index: index.current, - url: `/content/channels/${channelId}/topics/${topicId}/assets?agent=${token}`, + baseUrl: `/content/channels/${channelId}/topics/${topicId}/`, + urlParams: `?agent=${token}`, files, assets: [], current: null, @@ -154,11 +158,33 @@ async function upload(entry, update, complete) { const file = entry.files.shift(); entry.active = {}; try { - if (file.image) { + if (file.encrypted) { + const { size, getThumb, getEncryptedBlock, position } = file; + const type = file.image ? 'image' : file.video ? 'video' : file.audio ? 'audio' : ''; + const thumb = getThumb(position); + const parts = []; + for (let pos = 0; pos < size; pos += ENCRYPTED_BLOCK_SIZE) { + const { blockEncrypted, blockIv } = await getEncryptedBlock(pos, ENCRYPTED_BLOCK_SIZE); + const partId = await axios.post(`${entry.baseUrl}block${entry.urlParams}`, blockEncrypted, { + signal: entry.cancel.signal, + onUploadProgress: (ev) => { + const { loaded, total } = ev; + const partLoaded = pos + Math.floor(blockEncrypted.length * loaded / total); + entry.active = { partLoaded, size } + update(); + } + }); + parts.push({ blockIv, partId }); + } + entry.assets.push({ + encrypted: { type, thumb, parts } + }); + } + else if (file.image) { const formData = new FormData(); formData.append('asset', file.image); let transform = encodeURIComponent(JSON.stringify(["ithumb;photo", "ilg;photo"])); - let asset = await axios.post(`${entry.url}&transforms=${transform}`, formData, { + let asset = await axios.post(`${entry.baseUrl}assets${entry.urlParams}&transforms=${transform}`, formData, { signal: entry.cancel.signal, onUploadProgress: (ev) => { const { loaded, total } = ev; @@ -178,7 +204,7 @@ async function upload(entry, update, complete) { formData.append('asset', file.video); let thumb = 'vthumb;video;' + file.position; let transform = encodeURIComponent(JSON.stringify(["vlq;video", "vhd;video", thumb])); - let asset = await axios.post(`${entry.url}&transforms=${transform}`, formData, { + let asset = await axios.post(`${entry.baseUrl}assets${entry.urlParams}&transforms=${transform}`, formData, { signal: entry.cancel.signal, onUploadProgress: (ev) => { const { loaded, total } = ev; @@ -198,7 +224,7 @@ async function upload(entry, update, complete) { const formData = new FormData(); formData.append('asset', file.audio); let transform = encodeURIComponent(JSON.stringify(["acopy;audio"])); - let asset = await axios.post(`${entry.url}&transforms=${transform}`, formData, { + let asset = await axios.post(`${entry.baseUrl}assets${entry.urlParams}&transforms=${transform}`, formData, { signal: entry.cancel.signal, onUploadProgress: (ev) => { const { loaded, total } = ev; diff --git a/net/web/src/session/conversation/addTopic/useAddTopic.hook.js b/net/web/src/session/conversation/addTopic/useAddTopic.hook.js index 5b00f46b..610364dd 100644 --- a/net/web/src/session/conversation/addTopic/useAddTopic.hook.js +++ b/net/web/src/session/conversation/addTopic/useAddTopic.hook.js @@ -1,6 +1,6 @@ import { useContext, useState, useRef, useEffect } from 'react'; import { ConversationContext } from 'context/ConversationContext'; -import { encryptTopicSubject } from 'context/sealUtil'; +import { encryptBlock, encryptTopicSubject } from 'context/sealUtil'; import Resizer from "react-image-file-resizer"; export function useAddTopic(contentKey) { @@ -64,20 +64,48 @@ export function useAddTopic(contentKey) { updateState({ enableImage, enableAudio, enableVideo }); }, [conversation.state.channel?.data?.channelDetail]); + const setUrl = async (url, getThumb) => { + if (contentKey) { + const buffer = await url.arrayBuffer(); + const getEncryptedBlock = (pos, len) => { + if (pos + len > buffer.byteLen) { + return null; + } + const block = btoa(String.fromCharCode.apply(null, buffer.slice(pos, len))); + return getEncryptedBlock(block, contentKey); + } + return { url, position: 0, label: '', encrypted: true, size: buffer.byteLength, getEncryptedBlock, getThumb }; + } + else { + return { url, position: 0, label: '', encrypted: false }; + } + } + const actions = { addImage: async (image) => { - let url = URL.createObjectURL(image); - addAsset({ image, url }); + const url = URL.createObjectURL(image); objects.current.push(url); + const getThumb = async () => { + return await getImageThumb(url); + } + const asset = setUrl(url, getThumb); + addAsset({ image, ...asset }); }, addVideo: async (video) => { - let url = URL.createObjectURL(video); - addAsset({ video, url, position: 0 }) + const url = URL.createObjectURL(video); objects.current.push(url); + const getThumb = async (position) => { + return await getVideoThumb(url, position); + } + const asset = setUrl(url, getThumb); + addAsset({ video, ...asset }); }, - addAudio: (audio) => { - let url = URL.createObjectURL(audio); - addAsset({ audio, url, label: '' }) + addAudio: async (audio) => { + const url = URL.createObjectURL(audio); + objects.current.push(url); + const getThumb = async () => { return null }; + const asset = setUrl(url, getThumb); + addAsset({ audio, ...asset }); }, setLabel: (index, label) => { updateAsset(index, { label }); @@ -103,32 +131,19 @@ export function useAddTopic(contentKey) { updateState({ busy: true }); const type = contentKey ? 'sealedtopic' : 'superbasictopic'; const message = (assets) => { - if (contentKey) { - if (assets?.length) { - console.log('assets not yet supported on sealed channels'); - } - const message = { + if (assets?.length) { + return { + assets, text: state.messageText, textColor: state.textColorSet ? state.textColor : null, textSize: state.textSizeSet ? state.textSize : null, } - return encryptTopicSubject({ message }, contentKey); } else { - if (assets?.length) { - return { - assets, - text: state.messageText, - textColor: state.textColorSet ? state.textColor : null, - textSize: state.textSizeSet ? state.textSize : null, - } - } - else { - return { - text: state.messageText, - textColor: state.textColorSet ? state.textColor : null, - textSize: state.textSizeSet ? state.textSize : null, - } + return { + text: state.messageText, + textColor: state.textColorSet ? state.textColor : null, + textSize: state.textSizeSet ? state.textSize : null, } } };