add streaming upload (not supported yet)
This commit is contained in:
parent
9fbd5ccac1
commit
c071c7d56a
@ -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",
|
||||
|
@ -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 };
|
@ -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
|
||||
};
|
@ -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);
|
||||
|
||||
@ -24,7 +41,14 @@ 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
|
||||
|
@ -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));
|
||||
|
Loading…
Reference in New Issue
Block a user