From 000d306bd1e391aa7429bbd1954c664a30fc9cae Mon Sep 17 00:00:00 2001 From: Roland Osborne Date: Thu, 27 Apr 2023 10:20:25 -0700 Subject: [PATCH] sending encrypted asset blocks from mobile app --- app/mobile/src/context/sealUtil.js | 21 ++++++ .../src/context/useUploadContext.hook.js | 45 +++++++++++- .../conversation/addTopic/useAddTopic.hook.js | 68 +++++++++++-------- .../conversation/addTopic/useAddTopic.hook.js | 2 +- 4 files changed, 107 insertions(+), 29 deletions(-) diff --git a/app/mobile/src/context/sealUtil.js b/app/mobile/src/context/sealUtil.js index 47027d8b..8ab8658d 100644 --- a/app/mobile/src/context/sealUtil.js +++ b/app/mobile/src/context/sealUtil.js @@ -57,6 +57,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/app/mobile/src/context/useUploadContext.hook.js b/app/mobile/src/context/useUploadContext.hook.js index 20ddbd7e..5510fa8a 100644 --- a/app/mobile/src/context/useUploadContext.hook.js +++ b/app/mobile/src/context/useUploadContext.hook.js @@ -1,5 +1,10 @@ import { useState, useRef } from 'react'; import axios from 'axios'; +import { createThumbnail } from "react-native-create-thumbnail"; +import ImageResizer from '@bam.tech/react-native-image-resizer'; +import RNFS from 'react-native-fs'; + +const ENCRYPTED_BLOCK_SIZE = (128 * 1024); //100k export function useUploadContext() { @@ -116,6 +121,23 @@ export function useUploadContext() { return { state, actions } } +async function getThumb(file, type, position) { + if (type === 'image') { + const thumb = await ImageResizer.createResizedImage(file, 192, 192, "JPEG", 50, 0, null); + const base = await RNFS.readFile(thumb.path, 'base64') + return `data:image/jpeg;base64,${base}`; + } + else if (type === 'video') { + const shot = await createThumbnail({ url: url, timeStamp: position * 1000 }) + const thumb = await ImageResizer.createResizedImage('file://' + shot.path, 192, 192, "JPEG", 50, 0, null); + const base = await RNFS.readFile(thumb.path, 'base64') + return `data:image/jpeg;base64,${base}`; + } + else { + return null + } +} + async function upload(entry, update, complete) { if (!entry.files?.length) { try { @@ -133,7 +155,28 @@ async function upload(entry, update, complete) { const file = entry.files.shift(); entry.active = {}; try { - if (file.type === 'image') { + if (file.encrypted) { + const { data, type, size, getEncryptedBlock, position } = file; + const thumb = await getThumb(data, type, 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.type === 'image') { const formData = new FormData(); if (file.data.startsWith('file:')) { formData.append("asset", {uri: file.data, name: 'asset', type: 'application/octent-stream'}); diff --git a/app/mobile/src/session/conversation/addTopic/useAddTopic.hook.js b/app/mobile/src/session/conversation/addTopic/useAddTopic.hook.js index 3d92ed2d..59759b29 100644 --- a/app/mobile/src/session/conversation/addTopic/useAddTopic.hook.js +++ b/app/mobile/src/session/conversation/addTopic/useAddTopic.hook.js @@ -3,10 +3,8 @@ import { UploadContext } from 'context/UploadContext'; import { ConversationContext } from 'context/ConversationContext'; import { Image } from 'react-native'; import Colors from 'constants/Colors'; -import { getChannelSeals, getContentKey, encryptTopicSubject } from 'context/sealUtil'; +import { encryptBlock, decryptBlock, getChannelSeals, getContentKey, encryptTopicSubject } from 'context/sealUtil'; import { AccountContext } from 'context/AccountContext'; -import { createThumbnail } from "react-native-create-thumbnail"; -import ImageResizer from '@bam.tech/react-native-image-resizer'; import RNFS from 'react-native-fs'; export function useAddTopic(contentKey) { @@ -57,6 +55,10 @@ export function useAddTopic(contentKey) { updateState({ conflict }); }, [state.assets, state.locked, state.enableImage, state.enableAudio, state.enableVideo]); + useEffect(() => { + updateState({ assets: [] }); + }, [contentKey]); + useEffect(() => { const cardId = conversation.state.card?.card?.cardId; const channelId = conversation.state.channel?.channelId; @@ -103,39 +105,51 @@ export function useAddTopic(contentKey) { updateState({ enableImage, enableAudio, enableVideo, locked }); }, [conversation.state]); + const setAsset = async (file) => { + const url = file.startsWith('file:') ? file : `file://${file}`; + if (contentKey) { + const stat = await RNFS.stat(url); + const getEncryptedBlock = async (pos, len) => { + if (pos + len > stat.size) { + return null; + } + const block = await RNFS.read(file, len, pos, 'base64'); + return getEncryptedBlock(block, contentKey); + } + return { data: url, encrypted: true, size: stat.size, getEncryptedBlock }; + } + else { + return { data: url, encrypted: false }; + } + } + const actions = { setMessage: (message) => { updateState({ message }); }, addImage: async (data) => { - const url = data.startsWith('file:') ? data : 'file://' + data; - assetId.current++; - Image.getSize(url, (width, height) => { - const asset = { key: assetId.current, type: 'image', data: url, ratio: width/height }; - updateState({ assets: [ ...state.assets, asset ] }); - }) - }, - addVideo: async (data) => { - const url = data.startsWith('file:') ? data : 'file://' + data - - const shot = await createThumbnail({ url: url, timeStamp: 5000, }) - console.log(shot); - - const thumb = await ImageResizer.createResizedImage('file://' + shot.path, 192, 192, "JPEG", 50, 0, null); - console.log(thumb); - - const base = await RNFS.readFile(thumb.path, 'base64') - console.log('data:image/jpeg;base64,' + base); - - assetId.current++; - const asset = { key: assetId.current, type: 'video', data: url, ratio: 1, duration: 0, position: 0 }; + const asset = await setAsset(data); + asset.key = assetId.current; + asset.type = 'image'; + asset.ratio = 1; updateState({ assets: [ ...state.assets, asset ] }); }, - addAudio: (data, label) => { - const url = data.startsWith('file:') ? data : 'file://' + data + addVideo: async (data) => { assetId.current++; - const asset = { key: assetId.current, type: 'audio', data: url, label }; + const asset = await setAsset(data); + asset.key = assetId.current; + asset.type = 'video'; + asset.position = 0; + asset.ratio = 1; + updateState({ assets: [ ...state.assets, asset ] }); + }, + addAudio: async (data, label) => { + assetId.current++; + const asset = await setAsset(data); + asset.key = assetId.current; + asset.type = 'audio'; + asset.label = label; updateState({ assets: [ ...state.assets, asset ] }); }, setVideoPosition: (key, position) => { diff --git a/net/web/src/session/conversation/addTopic/useAddTopic.hook.js b/net/web/src/session/conversation/addTopic/useAddTopic.hook.js index 610364dd..ad7d8d75 100644 --- a/net/web/src/session/conversation/addTopic/useAddTopic.hook.js +++ b/net/web/src/session/conversation/addTopic/useAddTopic.hook.js @@ -56,7 +56,7 @@ export function useAddTopic(contentKey) { useEffect(() => { updateState({ assets: [] }); - return () => { console.log("RETURN CLEAR"); clearObjects() }; + return () => { clearObjects() }; }, [contentKey]); useEffect(() => {