diff --git a/.gitignore b/.gitignore index 0bdb622..5733fa4 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,5 @@ Debug/ Release/ node_modules/ package-lock.json -out/ \ No newline at end of file +out/ +sw.js \ No newline at end of file diff --git a/package.json b/package.json index 7d11bf6..290f5ca 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "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 && webpack-cli --entry ./src/js/Worker.js --mode production --output ./sw.js", "debug": "webpack-cli --entry ./src/js/index.js --mode development --watch --output ./dist/bundle.js", "debug-worker": "webpack-cli --entry ./src/js/Worker.js --mode development --watch --output ./sw.js" }, diff --git a/src/js/Worker.js b/src/js/Worker.js index f0fe243..f09f0eb 100644 --- a/src/js/Worker.js +++ b/src/js/Worker.js @@ -15,7 +15,18 @@ const VoidFetch = function (event) { let fi = await Api.GetFileInfo(hs.id); if (fi.ok) { let fd = new FileDownloader(fi.data, hs.key, hs.iv); - return await fd.StreamResponse(); + fd.onprogress = function(x) { + if(client !== null && client !== undefined){ + client.postMessage({ + type: 'progress', + x + }); + } + }; + let resp = await fd.StreamResponse(); + resp.headers.set("Content-Type", fd.fileHeader.mime != "" ? fd.fileHeader.mime : "application/octet-stream"); + resp.headers.set("Content-Disposition", `inline; filename="${fd.fileHeader.name}"`); + return resp; } else { return Response.error(); } diff --git a/src/js/modules/FileDownloader.js b/src/js/modules/FileDownloader.js index eeb55c9..9143c09 100644 --- a/src/js/modules/FileDownloader.js +++ b/src/js/modules/FileDownloader.js @@ -81,126 +81,144 @@ function FileDownloader(fileinfo, key, iv) { }); let void_download = { + SetFileHeader: function (fh) { this.fileHeader = fh; }.bind(this), + HandleProgress: this.HandleProgress.bind(this), + downloadStats: this.downloadStats, + isStart: true, + decOffset: 0, + headerLen: 0, + fileHeader: null, + hmacBytes: null, body: response.body, fileinfo: this.fileinfo, aes: new AES_CBC(new Uint8Array(Utils.HexToArray(this.key)), new Uint8Array(Utils.HexToArray(this.iv)), true), hmac: new HmacSha256(new Uint8Array(Utils.HexToArray(this.key))), buff: new Uint8Array(), + start(controller) { + this.reader = this.body.getReader(); + return this.readOnce(controller); + }, pull(controller) { - if (this.reader === undefined) { - this.reader = this.body.getReader(); - } return (async function () { Log.I(`${this.fileinfo.FileId} Starting..`); - var isStart = true; - var decOffset = 0; - var headerLen = 0; - var fileHeader = null; - var hmacBytes = null; while (true) { - let { done, value } = await this.reader.read(); - if (done) { - if (this.buff.byteLength > 0) { - //pad the remaining data with PKCS#7 - var toDecrypt = null; - let padding = 16 - (this.buff.byteLength % 16); - if(padding !== 0){ - let tmpBuff = new Uint8Array(this.buff.byteLength + padding); - tmpBuff.fill(padding); - tmpBuff.set(this.buff, 0); - this.buff = null; - this.buff = tmpBuff; - } - let decBytes = this.aes.AES_Decrypt_process(this.buff); - this.hmac.process(decBytes); - controller.enqueue(decBytes); - this.buff = null; - } - let last = this.aes.AES_Decrypt_finish(); - this.hmac.process(last); - this.hmac.finish(); - controller.enqueue(last); - - //check hmac - let h1 = Utils.ArrayToHex(hmacBytes); - let h2 = Utils.ArrayToHex(this.hmac.result) - if (h1 === h2) { - Log.I(`HMAC verify ok!`); - } else { - Log.E(`HMAC verify failed (${h1} !== ${h2})`); - //controller.cancel(); - //return; - } - Log.I(`${this.fileinfo.FileId} Download complete!`); - controller.close(); - return; - } - - var sliceStart = 0; - var sliceEnd = value.byteLength; - - //!Slice this only once!! - var toDecrypt = value; - if (isStart) { - let header = VBF.ParseStart(value.buffer); - if (header !== null) { - Log.I(`${this.fileinfo.FileId} blob header version is ${header.version} uploaded on ${header.uploaded} (Magic: ${Utils.ArrayToHex(header.magic)})`); - sliceStart = VBF.SliceToEncryptedPart(header.version, value); - } else { - throw "Invalid VBF header"; - } - } else if (fileHeader != null && decOffset + toDecrypt.byteLength + headerLen + 2 >= fileHeader.len) { - sliceEnd -= 32; //hash is on the end (un-encrypted) - hmacBytes = toDecrypt.slice(sliceEnd); - } - - const GetAdjustedLen = function () { - return sliceEnd - sliceStart; - }; - - //decrypt - //append last remaining buffer if any - if (this.buff.byteLength > 0) { - let tmpd = new Uint8Array(this.buff.byteLength + GetAdjustedLen()); - tmpd.set(this.buff, 0); - tmpd.set(toDecrypt.slice(sliceStart, sliceEnd), this.buff.byteLength); - sliceEnd += this.buff.byteLength; - toDecrypt = tmpd; - this.buff = new Uint8Array(); - } - - let blkRem = GetAdjustedLen() % 16; - if (blkRem !== 0) { - //save any remaining data into our buffer - this.buff = toDecrypt.slice(sliceEnd - blkRem, sliceEnd); - sliceEnd -= blkRem; - } - - let encBytes = toDecrypt.slice(sliceStart, sliceEnd); - let decBytes = this.aes.AES_Decrypt_process(encBytes); - decOffset += decBytes.byteLength; - - //read header - if (isStart) { - headerLen = new Uint16Array(decBytes.slice(0, 2))[0]; - let header = new TextDecoder('utf-8').decode(decBytes.slice(2, 2 + headerLen)); - Log.I(`${this.fileinfo.FileId} got header ${header}`); - fileHeader = JSON.parse(header); - decBytes = decBytes.slice(2 + headerLen); - } - - //Log.I(`${this.fileinfo.FileId} Decrypting ${toDecrypt.byteLength} bytes, got ${decBytes.byteLength} bytes`); - this.hmac.process(decBytes); - controller.enqueue(decBytes); - - isStart = false; + await this.readOnce(controller); } }.bind(this))(); + }, + async readOnce(controller) { + let { done, value } = await this.reader.read(); + if (done) { + if (this.buff.byteLength > 0) { + //pad the remaining data with PKCS#7 + var toDecrypt = null; + let padding = 16 - (this.buff.byteLength % 16); + if (padding !== 0) { + let tmpBuff = new Uint8Array(this.buff.byteLength + padding); + tmpBuff.fill(padding); + tmpBuff.set(this.buff, 0); + this.buff = null; + this.buff = tmpBuff; + } + let decBytes = this.aes.AES_Decrypt_process(this.buff); + this.hmac.process(decBytes); + controller.enqueue(decBytes); + this.buff = null; + } + let last = this.aes.AES_Decrypt_finish(); + this.hmac.process(last); + this.hmac.finish(); + controller.enqueue(last); + + //check hmac + let h1 = Utils.ArrayToHex(this.hmacBytes); + let h2 = Utils.ArrayToHex(this.hmac.result) + if (h1 === h2) { + Log.I(`HMAC verify ok!`); + } else { + Log.E(`HMAC verify failed (${h1} !== ${h2})`); + controller.cancel(); + return; + } + Log.I(`${this.fileinfo.FileId} Download complete!`); + controller.close(); + return; + } + + var sliceStart = 0; + var sliceEnd = value.byteLength; + + //!Slice this only once!! + var toDecrypt = value; + if (this.isStart) { + let header = VBF.ParseStart(value.buffer); + if (header !== null) { + Log.I(`${this.fileinfo.FileId} blob header version is ${header.version} uploaded on ${header.uploaded} (Magic: ${Utils.ArrayToHex(header.magic)})`); + sliceStart = VBF.SliceToEncryptedPart(header.version, value); + } else { + throw "Invalid VBF header"; + } + } else if (this.fileHeader != null && this.decOffset + toDecrypt.byteLength + this.headerLen + 2 >= this.fileHeader.len) { + sliceEnd -= 32; //hash is on the end (un-encrypted) + this.hmacBytes = toDecrypt.slice(sliceEnd); + } + + const GetAdjustedLen = function () { + return sliceEnd - sliceStart; + }; + + //decrypt + //append last remaining buffer if any + if (this.buff.byteLength > 0) { + let tmpd = new Uint8Array(this.buff.byteLength + GetAdjustedLen()); + tmpd.set(this.buff, 0); + tmpd.set(toDecrypt.slice(sliceStart, sliceEnd), this.buff.byteLength); + sliceEnd += this.buff.byteLength; + toDecrypt = tmpd; + this.buff = new Uint8Array(); + } + + let blkRem = GetAdjustedLen() % 16; + if (blkRem !== 0) { + //save any remaining data into our buffer + this.buff = toDecrypt.slice(sliceEnd - blkRem, sliceEnd); + sliceEnd -= blkRem; + } + + let encBytes = toDecrypt.slice(sliceStart, sliceEnd); + let decBytes = this.aes.AES_Decrypt_process(encBytes); + this.decOffset += decBytes.byteLength; + + //read header + if (this.isStart) { + this.headerLen = new Uint16Array(decBytes.slice(0, 2))[0]; + let header = new TextDecoder('utf-8').decode(decBytes.slice(2, 2 + this.headerLen)); + Log.I(`${this.fileinfo.FileId} got header ${header}`); + this.fileHeader = JSON.parse(header); + this.SetFileHeader(this.fileHeader); + decBytes = decBytes.slice(2 + this.headerLen); + } + + //Log.I(`${this.fileinfo.FileId} Decrypting ${toDecrypt.byteLength} bytes, got ${decBytes.byteLength} bytes`); + this.hmac.process(decBytes); + controller.enqueue(decBytes); + + //report progress + let now = new Date().getTime(); + let dxLoaded = decBytes.byteLength - this.downloadStats.lastLoaded; + let dxTime = now - this.downloadStats.lastProgress; + + this.downloadStats.lastLoaded = decBytes.byteLength; + this.downloadStats.lastProgress = now; + + this.HandleProgress('progress-speed', `${Utils.FormatBytes(dxLoaded / (dxTime / 1000.0), 2)}/s`); + this.HandleProgress('progress-download', this.decOffset / parseFloat(this.fileHeader.len)); + + this.isStart = false; } } - let sr = new ReadableStream(void_download); - return new Response(sr); + return new Response(new ReadableStream(void_download)); }; /** diff --git a/src/js/modules/ViewManager.js b/src/js/modules/ViewManager.js index f8e5e9c..8e985ae 100644 --- a/src/js/modules/ViewManager.js +++ b/src/js/modules/ViewManager.js @@ -94,7 +94,7 @@ function ViewManager() { if ('serviceWorker' in navigator) { let swreg = await navigator.serviceWorker.getRegistration(); if (swreg !== null) { - Log.I(`Service worker detected, using ${swreg.scope} for download..`); + Log.I(`Service worker detected, using it for download..`); let elm_bar_label = document.querySelector('.view-download-progress div:nth-child(1)'); let elm_bar = document.querySelector('.view-download-progress div:nth-child(2)'); navigator.serviceWorker.addEventListener('message', event => {