add streaming upload (not supported yet)

This commit is contained in:
Kieran 2019-05-11 16:03:42 +08:00
parent 9fbd5ccac1
commit c071c7d56a
5 changed files with 202 additions and 7 deletions

View File

@ -7,7 +7,8 @@
"url": "git+https://github.com/v0l/void.cat.git"
},
"scripts": {
"build": "sass src/css/style.scss dist/style.css && webpack-cli --entry ./src/js/index.js --mode production --output ./dist/bundle.js"
"build": "sass src/css/style.scss dist/style.css && webpack-cli --entry ./src/js/index.js --mode production --output ./dist/bundle.js",
"debug": "webpack-cli --entry ./src/js/index.js --mode development --watch --output ./dist/bundle.js"
},
"keywords": [
"upload",

View File

@ -2,7 +2,7 @@ import * as Const from './Const.js';
import { Templates } from './App.js';
import { XHR, Utils, Log, $ } from './Util.js';
import { VBF } from './VBF.js';
import { bytes_to_base64 } from 'asmcrypto.js';
import { bytes_to_base64, HmacSha256, AES_CBC } from 'asmcrypto.js';
/**
* File upload handler class
@ -15,8 +15,7 @@ function FileUpload(file, host) {
this.file = file;
this.host = host;
this.domNode = null;
this.key = null;
this.hmackey = null;
this.key = new Uint8Array(16);
this.iv = new Uint8Array(16);
/**
@ -54,6 +53,7 @@ function FileUpload(file, host) {
/**
* Retruns the formatted hash fragment for this upload
* @param {string} id - The id returned from upload result
* @returns {Promise<string>} The id:key:iv concatenated and converted to base64
*/
this.FormatUrl = async (id) => {
@ -69,6 +69,22 @@ function FileUpload(file, host) {
return bytes_to_base64(ret);
};
/**
* Retruns the formatted hash fragment for this upload
* @param {string} id - The id returned from upload result
* @returns {Promise<string>} The id:key:iv concatenated and converted to base64
*/
this.FormatRawUrl = async (id) => {
let id_hex = new Uint8Array(Utils.HexToArray(id));
let ret = new Uint8Array(id_hex.byteLength + this.key.byteLength + this.iv.byteLength);
ret.set(id_hex, 0);
ret.set(this.key, id_hex.byteLength);
ret.set(this.iv, id_hex.byteLength + this.key.byteLength);
return bytes_to_base64(ret);
};
/**
* Loads the file and SHA256 hashes it
* @return {Promise<ArrayBuffer>}
@ -231,6 +247,18 @@ function FileUpload(file, host) {
$('#uploads').appendChild(nelm);
};
/**
* Generates a new key to use for encrypting the file
* @returns {Uint8Array} The new key
*/
this.GenerateRawKey = function () {
crypto.getRandomValues(this.key);
crypto.getRandomValues(this.iv);
this.domNode.key.textContent = `Key: ${this.TextKey()}`;
return this.key;
};
/**
* Generates a new key to use for encrypting the file
* @returns {Promise<CryptoKey>} The new key
@ -260,6 +288,24 @@ function FileUpload(file, host) {
return encryptedData;
};
/**
* Uploads Blob data to site
* @param {ReadableStream} fileData - The encrypted file data to upload
* @returns {Promise<object>} The json result
*/
this.UploadDataStream = async function (fileData) {
this.uploadStats.lastProgress = new Date().getTime();
this.HandleProgress('state-upload-start');
let request = new Request(`${window.location.protocol}//${this.host}/upload`, {
method: "POST",
body: fileData,
headers: { "Content-Type": "application/octet-stream" }
})
let response = await fetch(request);
return await response.json();
};
/**
* Uploads Blob data to site
* @param {Blob|BufferSource} fileData - The encrypted file data to upload
@ -268,6 +314,7 @@ function FileUpload(file, host) {
this.UploadData = async function (fileData) {
this.uploadStats.lastProgress = new Date().getTime();
this.HandleProgress('state-upload-start');
let uploadResult = await XHR("POST", `${window.location.protocol}//${this.host}/upload`, fileData, { "Content-Type": "application/octet-stream" }, function (ev) {
let now = new Date().getTime();
let dxLoaded = ev.loaded - this.uploadStats.lastLoaded;
@ -346,6 +393,103 @@ function FileUpload(file, host) {
this.domNode.errors.textContent = uploadResult.msg;
}
};
/**
* Stream the file upload
* @return {Promise}
*/
this.StreamUpload = async function () {
Log.I(`Starting upload for ${this.file.name}`);
this.CreateNode();
this.GenerateRawKey();
let header = JSON.stringify(this.CreateHeader());
let vbf_stream = {
type: "bytes",
autoAllocateChunkSize: 16 * 1024,
start(controller) {
this.self.HandleProgress('state-load-start');
this.offset = 0;
this.chunkSize = 16 * 1024;
this.aes = new AES_CBC(this.self.key, this.self.iv);
this.hmac = new HmacSha256(this.self.key);
//encode the header to bytes for encryption
this.header_data = new TextEncoder('utf-8').encode(this.header);
Log.I(`Using header: ${this.header} (length=${this.header_data.byteLength})`);
},
pull(controller) {
let read_now = this.chunkSize;
if(this.offset === 0) {
controller.enqueue(VBF.CreateV2Start());
read_now -= this.header_data.byteLength;
} else if(this.offset === this.self.file.size) {
//done, send last encrypted part and hmac
controller.enqueue(this.self.aes.AES_Encrypt_finish());
this.self.hmac.finish();
controller.enqueue(this.self.hmac.hash);
controller.close();
}
//read file slice
return new Promise((resolve, reject) => {
let file_to_read = this.self.file.slice(this.offset, this.offset + read_now);
let fr = new FileReader();
fr.onload = function(ev) {
let buf = null;
if(ev.target.self.offset === 0){
buf = new Uint8Array(ev.target.self.header_data.byteLength + ev.target.result.byteLength);
buf.set(ev.target.self.header_data, 0);
buf.set(ev.target.result, ev.target.self.header_data.byteLength);
} else {
buf = ev.target.result;
}
//hash the buffer
ev.target.self.hmac.process(buf);
//encrypt the buffer
controller.enqueue(ev.target.self.aes.AES_Encrypt_process(buf));
ev.target.self.offset += buf.byteLength;
resolve();
}
fr.onerror = function(ev) { reject(); }
fr.self = this;
fr.readAsArrayBuffer(file_to_read);
});
},
cancel() {
},
self: this,
header
};
let file_stream = new ReadableStream(vbf_stream);
Log.I(`Uploading file ${this.file.name}`);
let uploadResult = await this.UploadData(file_stream);
Log.I(`Got response for file ${this.file.name}: ${JSON.stringify(uploadResult)}`);
this.domNode.state.parentNode.style.display = "none";
this.domNode.progress.parentNode.style.display = "none";
if (uploadResult.status === 200) {
this.domNode.links.style.display = "";
let nl = document.createElement("a");
nl.target = "_blank";
nl.href = `${window.location.protocol}//${window.location.host}/#${await this.FormatRawUrl(uploadResult.id)}`;
nl.textContent = this.file.name;
this.domNode.links.appendChild(nl);
} else {
this.domNode.errors.style.display = "";
this.domNode.errors.textContent = uploadResult.msg;
}
};
};
export { FileUpload };

View File

@ -168,5 +168,11 @@ export const Utils = {
if (b >= Const.kiB)
return (b / Const.kiB).toFixed(f) + ' KiB';
return b.toFixed(f) + ' B'
}
},
/**
* Gets the text length of x bytes as base64 (including padding)
* @param {number} x - N bytes
*/
Base64Len: (x) => Math.ceil(x / 3) * 4
};

View File

@ -1,6 +1,16 @@
/**
* @constant {object} - Creates and decodes VBF file format
*/
const VBF = {
Version: 2,
/**
* Creates a VBF file with the specified encryptedData using the current version
* @param {BufferSource} hash
* @param {BufferSource} encryptedData
* @param {number} version
* @returns {Uint8Array} VBF formatted file
*/
Create: function (hash, encryptedData, version) {
version = typeof version === "number" ? version : VBF.Version;
switch (version) {
@ -11,6 +21,13 @@ const VBF = {
}
},
/**
* Creates a VBF1 file with the specified encryptedData
* @param {BufferSource} hash
* @param {BufferSource} encryptedData
* @param {number} version
* @returns {Uint8Array} VBF formatted file
*/
CreateV1: function (hash, encryptedData) {
let upload_payload = new Uint8Array(37 + encryptedData.byteLength);
@ -25,6 +42,13 @@ const VBF = {
return upload_payload;
},
/**
* Creates a VBF2 file with the specified encryptedData
* @param {BufferSource} hash
* @param {BufferSource} encryptedData
* @param {number} version
* @returns {Uint8Array} VBF formatted file
*/
CreateV2: function (hash, encryptedData) {
let header_len = 12;
let upload_payload = new Uint8Array(header_len + encryptedData.byteLength + hash.byteLength);
@ -40,6 +64,26 @@ const VBF = {
return upload_payload;
},
/**
* Creates the header part of VBF_V2
* @param {BufferSource} hash
* @param {BufferSource} encryptedData
* @param {number} version
* @returns {Uint8Array} VBF2 header
*/
CreateV2Start: function () {
let header_len = 12;
let start = new Uint8Array(header_len);
let created = new ArrayBuffer(4);
new DataView(created).setUint32(0, parseInt(new Date().getTime() / 1000), true);
start.set(new Uint8Array([0x02, 0x4f, 0x49, 0x44, 0xf0, 0x9f, 0x90, 0xb1]), 0);
start.set(new Uint8Array(created), 8);
return start;
},
/**
* Returns the encrypted part of the VBF blob
* @param {number} version

View File

@ -16,7 +16,7 @@ export function ViewManager() {
this.id = hs[0];
this.key = hs[1];
this.iv = hs[2];
} else if (window.location.hash.length === 73) { //base64 encoded #id:key:iv
} else if (window.location.hash.length === Utils.Base64Len(52) + 1) { //base64 encoded #id:key:iv
let hs = base64_to_bytes(window.location.hash.substr(1));
this.id = Utils.ArrayToHex(hs.slice(0, 20));
this.key = Utils.ArrayToHex(hs.slice(20, 36));