sending encrypted asset blocks from mobile app

This commit is contained in:
Roland Osborne 2023-04-27 10:20:25 -07:00
parent ea349dffbf
commit 000d306bd1
4 changed files with 107 additions and 29 deletions

View File

@ -57,6 +57,27 @@ export function updateChannelSubject(subject, contentKey) {
return { subjectEncrypted, subjectIv }; 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) { export function decryptChannelSubject(subject, contentKey) {
const { subjectEncrypted, subjectIv } = JSON.parse(subject); const { subjectEncrypted, subjectIv } = JSON.parse(subject);
const iv = CryptoJS.enc.Hex.parse(subjectIv); const iv = CryptoJS.enc.Hex.parse(subjectIv);

View File

@ -1,5 +1,10 @@
import { useState, useRef } from 'react'; import { useState, useRef } from 'react';
import axios from 'axios'; 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() { export function useUploadContext() {
@ -116,6 +121,23 @@ export function useUploadContext() {
return { state, actions } 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) { async function upload(entry, update, complete) {
if (!entry.files?.length) { if (!entry.files?.length) {
try { try {
@ -133,7 +155,28 @@ async function upload(entry, update, complete) {
const file = entry.files.shift(); const file = entry.files.shift();
entry.active = {}; entry.active = {};
try { 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(); const formData = new FormData();
if (file.data.startsWith('file:')) { if (file.data.startsWith('file:')) {
formData.append("asset", {uri: file.data, name: 'asset', type: 'application/octent-stream'}); formData.append("asset", {uri: file.data, name: 'asset', type: 'application/octent-stream'});

View File

@ -3,10 +3,8 @@ import { UploadContext } from 'context/UploadContext';
import { ConversationContext } from 'context/ConversationContext'; import { ConversationContext } from 'context/ConversationContext';
import { Image } from 'react-native'; import { Image } from 'react-native';
import Colors from 'constants/Colors'; 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 { 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'; import RNFS from 'react-native-fs';
export function useAddTopic(contentKey) { export function useAddTopic(contentKey) {
@ -57,6 +55,10 @@ export function useAddTopic(contentKey) {
updateState({ conflict }); updateState({ conflict });
}, [state.assets, state.locked, state.enableImage, state.enableAudio, state.enableVideo]); }, [state.assets, state.locked, state.enableImage, state.enableAudio, state.enableVideo]);
useEffect(() => {
updateState({ assets: [] });
}, [contentKey]);
useEffect(() => { useEffect(() => {
const cardId = conversation.state.card?.card?.cardId; const cardId = conversation.state.card?.card?.cardId;
const channelId = conversation.state.channel?.channelId; const channelId = conversation.state.channel?.channelId;
@ -103,39 +105,51 @@ export function useAddTopic(contentKey) {
updateState({ enableImage, enableAudio, enableVideo, locked }); updateState({ enableImage, enableAudio, enableVideo, locked });
}, [conversation.state]); }, [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 = { const actions = {
setMessage: (message) => { setMessage: (message) => {
updateState({ message }); updateState({ message });
}, },
addImage: async (data) => { addImage: async (data) => {
const url = data.startsWith('file:') ? data : 'file://' + data;
assetId.current++; assetId.current++;
Image.getSize(url, (width, height) => { const asset = await setAsset(data);
const asset = { key: assetId.current, type: 'image', data: url, ratio: width/height }; asset.key = assetId.current;
updateState({ assets: [ ...state.assets, asset ] }); asset.type = 'image';
}) asset.ratio = 1;
},
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 };
updateState({ assets: [ ...state.assets, asset ] }); updateState({ assets: [ ...state.assets, asset ] });
}, },
addAudio: (data, label) => { addVideo: async (data) => {
const url = data.startsWith('file:') ? data : 'file://' + data
assetId.current++; 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 ] }); updateState({ assets: [ ...state.assets, asset ] });
}, },
setVideoPosition: (key, position) => { setVideoPosition: (key, position) => {

View File

@ -56,7 +56,7 @@ export function useAddTopic(contentKey) {
useEffect(() => { useEffect(() => {
updateState({ assets: [] }); updateState({ assets: [] });
return () => { console.log("RETURN CLEAR"); clearObjects() }; return () => { clearObjects() };
}, [contentKey]); }, [contentKey]);
useEffect(() => { useEffect(() => {