Client side encryption (WIP)

This commit is contained in:
Kieran 2022-09-09 17:16:43 +01:00
parent 4f76c81bb7
commit d94f7aeb06
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
5 changed files with 125 additions and 10 deletions

View File

@ -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",

View File

@ -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 ?
<dl>
<dt>Link:</dt>
<dd><a target="_blank" href={`/${result.id}`}>{result.id}</a></dd>
<dd><a target="_blank" href={link}>{result.id}</a></dd>
</dl>
: <b>{result}</b>;
} 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 (

View File

@ -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);

View File

@ -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<bl/8; i++) {
if ((i&3) === 0) {
tmp = arr[i/4];
}
out.push(tmp >>> 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<bytes.length; i++) {
tmp = tmp << 8 | bytes[i];
if ((i&3) === 3) {
out.push(tmp);
tmp = 0;
}
}
if (i&3) {
out.push(sjcl.bitArray.partial(8*(i&3), tmp));
}
return out;
}
};

View File

@ -7726,6 +7726,11 @@ sisteransi@^1.0.5:
resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed"
integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==
sjcl@^1.0.8:
version "1.0.8"
resolved "https://registry.yarnpkg.com/sjcl/-/sjcl-1.0.8.tgz#f2ec8d7dc1f0f21b069b8914a41a8f236b0e252a"
integrity sha512-LzIjEQ0S0DpIgnxMEayM1rq9aGwGRG4OnZhCdjx7glTaJtf4zRfpg87ImfjSJjoW9vKpagd82McDOwbRT5kQKQ==
slash@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"