From d94f7aeb062774656ebbd97ea14e9e54f9c9239e Mon Sep 17 00:00:00 2001 From: Kieran Date: Fri, 9 Sep 2022 17:16:43 +0100 Subject: [PATCH] Client side encryption (WIP) --- VoidCat/spa/package.json | 3 +- .../src/Components/FileUpload/FileUpload.js | 48 +++++++++++++++---- VoidCat/spa/src/Pages/FilePreview.js | 37 ++++++++++++++ VoidCat/spa/src/codecBytes.js | 42 ++++++++++++++++ VoidCat/spa/yarn.lock | 5 ++ 5 files changed, 125 insertions(+), 10 deletions(-) create mode 100644 VoidCat/spa/src/codecBytes.js diff --git a/VoidCat/spa/package.json b/VoidCat/spa/package.json index 12d2354..4e9f695 100644 --- a/VoidCat/spa/package.json +++ b/VoidCat/spa/package.json @@ -18,7 +18,8 @@ "react-redux": "^7.2.6", "react-router-dom": "^6.2.1", "react-scripts": "5.0.0", - "recharts": "^2.1.10" + "recharts": "^2.1.10", + "sjcl": "^1.0.8" }, "scripts": { "start": "react-scripts start", diff --git a/VoidCat/spa/src/Components/FileUpload/FileUpload.js b/VoidCat/spa/src/Components/FileUpload/FileUpload.js index 1dde1d4..8bfb8da 100644 --- a/VoidCat/spa/src/Components/FileUpload/FileUpload.js +++ b/VoidCat/spa/src/Components/FileUpload/FileUpload.js @@ -2,6 +2,8 @@ import "./FileUpload.css"; import {useEffect, useState} from "react"; import * as CryptoJS from 'crypto-js'; import {useSelector} from "react-redux"; +import sjcl from "sjcl"; +import {sjclcodec} from "../../codecBytes"; import {ConstName, FormatBytes} from "../Shared/Util"; import {RateCalculator} from "../Shared/RateCalculator"; @@ -18,6 +20,7 @@ const UploadState = { }; export const DigestAlgo = "SHA-256"; +const BlockSize = 16; export function FileUpload(props) { const auth = useSelector(state => state.login.jwt); @@ -27,6 +30,7 @@ export function FileUpload(props) { const [result, setResult] = useState(); const [uState, setUState] = useState(UploadState.NotStarted); const [challenge, setChallenge] = useState(); + const [encryptionKey, setEncryptionKey] = useState(); const calc = new RateCalculator(); function handleProgress(e) { @@ -39,7 +43,19 @@ export function FileUpload(props) { } } + function generateEncryptionKey() { + let key = { + key: sjclcodec.toBits(window.crypto.getRandomValues(new Uint8Array(16))), + iv: sjclcodec.toBits(window.crypto.getRandomValues(new Uint8Array(12))) + }; + setEncryptionKey(key); + return key; + } + async function doStreamUpload() { + let key = generateEncryptionKey(); + let aes = new sjcl.cipher.aes(key.key); + setUState(UploadState.Hashing); let hash = await digest(props.file); calc.Reset(); @@ -56,13 +72,27 @@ export function FileUpload(props) { return new Uint8Array(data); } + async function readEncryptedChunk(size) { + if (offset >= props.file.size) { + return new Uint8Array(0); + } + size -= size % BlockSize; + + let end = Math.min(offset + size, props.file.size); + let blob = props.file.slice(offset, end, props.file.type); + let data = new Uint8Array(await blob.arrayBuffer()); + offset += data.byteLength; + let encryptedData = sjcl.mode.gcm.encrypt(aes, sjclcodec.toBits(data), key.iv); + return new Uint8Array(sjclcodec.fromBits(encryptedData)); + } + let rs = new ReadableStream({ - start: (controller) => { - console.log(controller); + start: () => { setUState(UploadState.Uploading); }, pull: async (controller) => { - let chunk = await readChunk(controller.desiredSize); + let chunkSize = controller.desiredSize; + let chunk = key ? await readEncryptedChunk(chunkSize) : await readChunk(chunkSize); if (chunk.byteLength === 0) { controller.close(); return; @@ -71,7 +101,6 @@ export function FileUpload(props) { calc.ReportProgress(chunk.byteLength); setSpeed(calc.RateWindow(5)); setProgress(offset / props.file.size); - controller.enqueue(chunk); }, cancel: (reason) => { @@ -194,7 +223,7 @@ export function FileUpload(props) { } } - function getChromeVersion () { + function getChromeVersion() { let raw = navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./); return raw ? parseInt(raw[2], 10) : false; } @@ -217,10 +246,11 @@ export function FileUpload(props) { function renderStatus() { if (result) { + let link = encryptionKey ? `/${result.id}#${sjcl.codec.hex.fromBits(encryptionKey.key)}:${sjcl.codec.hex.fromBits(encryptionKey.iv)}` : `/${result.id}`; return uState === UploadState.Done ?
Link:
-
{result.id}
+
{result.id}
: {result}; } else { @@ -245,13 +275,13 @@ export function FileUpload(props) { useEffect(() => { console.log(props.file); - + let chromeVersion = getChromeVersion(); - if(chromeVersion >= 105) { + if (chromeVersion >= 105) { doStreamUpload().catch(console.error); } else { doXHRUpload().catch(console.error); - } + } }, []); return ( diff --git a/VoidCat/spa/src/Pages/FilePreview.js b/VoidCat/spa/src/Pages/FilePreview.js index 6e54b68..324ce0f 100644 --- a/VoidCat/spa/src/Pages/FilePreview.js +++ b/VoidCat/spa/src/Pages/FilePreview.js @@ -10,6 +10,8 @@ import {Helmet} from "react-helmet"; import {FormatBytes} from "../Components/Shared/Util"; import {ApiHost} from "../Components/Shared/Const"; import {InlineProfile} from "../Components/Shared/InlineProfile"; +import sjcl from "sjcl"; +import {sjclcodec} from "../codecBytes"; export function FilePreview() { const {Api} = useApi(); @@ -143,6 +145,41 @@ export function FilePreview() { useEffect(() => { if (info) { let fileLink = info.metadata?.url ?? `${ApiHost}/d/${info.id}`; + + // detect encrypted file link + let hashKey = window.location.hash.match(/#([0-9a-z]{32}):([0-9a-z]{24})/); + if (hashKey.length === 3) { + let [key, iv] = [sjcl.codec.hex.toBits(hashKey[1]), sjcl.codec.hex.toBits(hashKey[2])]; + console.log(key, iv); + let aes = new sjcl.cipher.aes(key); + + async function load() { + let decryptStream = new window.TransformStream({ + transform: async (chunk, controller) => { + chunk = await chunk; + console.log("Transforming chunk:", chunk); + + let buff = sjclcodec.toBits(chunk); + let decryptedBuff = sjclcodec.fromBits(sjcl.mode.gcm.decrypt(aes, buff, iv)); + console.log("Decrypted data:", decryptedBuff); + controller.enqueue(new Uint8Array(decryptedBuff)); + } + }); + let rsp = await fetch(fileLink); + if (rsp.ok) { + let reader = rsp.body + .pipeThrough(decryptStream); + + console.log("Pipe reader", reader); + let newResponse = new Response(reader); + setLink(window.URL.createObjectURL(await newResponse.blob(), {type: info.metadata.mimeType})); + } + } + + load(); + return; + } + let order = window.localStorage.getItem(`payment-${info.id}`); if (order) { let orderObj = JSON.parse(order); diff --git a/VoidCat/spa/src/codecBytes.js b/VoidCat/spa/src/codecBytes.js new file mode 100644 index 0000000..48c026b --- /dev/null +++ b/VoidCat/spa/src/codecBytes.js @@ -0,0 +1,42 @@ +import sjcl from "sjcl"; +/** @fileOverview Bit array codec implementations. + * + * @author Emily Stark + * @author Mike Hamburg + * @author Dan Boneh + */ + +/** + * Arrays of bytes + * @namespace + */ +export const sjclcodec = { + /** Convert from a bitArray to an array of bytes. */ + fromBits: function (arr) { + var out = [], bl = sjcl.bitArray.bitLength(arr), i, tmp; + for (i=0; i>> 24); + tmp <<= 8; + } + return out; + }, + /** Convert from an array of bytes to a bitArray. */ + /** @return {bitArray} */ + toBits: function (bytes) { + var out = [], i, tmp=0; + for (i=0; i