diff --git a/app/api.js b/app/api.js index f32b5abf..0219d006 100644 --- a/app/api.js +++ b/app/api.js @@ -197,6 +197,69 @@ export function uploadWs(encrypted, info, metadata, verifierB64, onprogress) { }; } +//////////////////////// + +async function downloadS(id, keychain, onprogress, signal) { + const auth = await keychain.authHeader(); + try { + const response = await fetch(`/api/download/${id}`, { + signal: signal , + method: 'GET', + headers: {'Authorization': auth} + }); + + if (response.status !== 200) { + throw new Error(response.status); + } + + const authHeader = response.headers.get('WWW-Authenticate'); + if (authHeader) { + keychain.nonce = parseNonce(authHeader); + } + + const fileSize = response.headers.get('Content-Length'); + onprogress([0, fileSize]); + + console.log(response.body); + if (response.body) { + return response.body; + } + return response.blob(); + + } catch (err) { + if (err.name === 'AbortError') { + throw new Error('0'); + } else { + throw err; + } + } +} + +async function tryDownloadStream(id, keychain, onprogress, signal, tries = 1) { + try { + const result = await downloadS(id, keychain, onprogress, signal); + return result; + } catch (e) { + if (e.message === '401' && --tries > 0) { + return tryDownloadStream(id, keychain, onprogress, signal, tries); + } + throw e; + } +} + +export function downloadStream(id, keychain, onprogress) { + const controller = new AbortController(); + function cancel() { + controller.abort(); + } + return { + cancel, + result: tryDownloadStream(id, keychain, onprogress, controller.signal, 2) + }; +} + +////////////////// + function download(id, keychain, onprogress, canceller) { const xhr = new XMLHttpRequest(); canceller.oncancel = function() { diff --git a/app/ece.js b/app/ece.js index eb86c6c0..12605811 100644 --- a/app/ece.js +++ b/app/ece.js @@ -1,5 +1,5 @@ require('buffer'); -import { ReadableStream, TransformStream } from 'web-streams-polyfill'; +import { TransformStream } from 'web-streams-polyfill'; const NONCE_LENGTH = 12; const TAG_LENGTH = 16; @@ -258,14 +258,67 @@ class BlobSlicer { } } -class BlobSliceStream extends ReadableStream { - constructor(blob, size, mode) { - super(new BlobSlicer(blob, size, mode)); +class StreamSlicer { + constructor(rs, mode) { + this.mode = mode; + this.rs = rs; + this.chunkSize = mode === MODE_ENCRYPT ? rs - 17 : 21; + this.partialChunk = new Uint8Array(this.chunkSize); //where partial chunks are saved + this.offset = 0; + } + + send(buf, controller) { + //console.log("sent a record") + controller.enqueue(buf); + if (this.chunkSize === 21) { + this.chunkSize = this.rs; + this.partialChunk = new Uint8Array(this.chunkSize); + } + } + + //reslice input uint8arrays into record sized chunks + transform(chunk, controller) { + //console.log('Received chunk') // with %d bytes.', chunk.byteLength) + let i = 0; + + if (this.offset > 0) { //send off the partial chunk + const len = Math.min(chunk.byteLength, (this.chunkSize - this.offset)); + this.partialChunk.set((chunk.slice(0, len)), this.offset); + this.offset += len; + i += len; + + if (this.offset === this.chunkSize) { + this.send(this.partialChunk, controller); + this.offset = 0; + } + } + + while (i < chunk.byteLength) { //send off whole records and stick last bit in partialChunk + if ((chunk.byteLength - i) > this.chunkSize) { + const record = chunk.slice(i, i + this.chunkSize); + i += this.chunkSize; + this.send(record, controller); + } else { + const end = chunk.slice(i, end); + this.partialChunk.set(end); + this.offset = end.length; + i += end.length; + } + } + } + + flush(controller) { + if (this.offset > 0) { + console.log("sent a partial record") + controller.enqueue(this.partialChunk.slice(0, this.offset)); + } } } + + /* -input: a blob containing data to be transformed +input: a blob or a readable stream containing data to be transformed key: Uint8Array containing key of size KEY_LENGTH mode: string, either 'encrypt' or 'decrypt' rs: int containing record size, optional @@ -280,11 +333,17 @@ export default class ECE { salt = generateSalt(KEY_LENGTH); } - this.streamInfo = { - recordSize: rs, - fileSize: 21 + input.size + 16 * Math.floor(input.size / (rs - 17)) - }; - const inputStream = new BlobSliceStream(input, rs, mode); + let inputStream; + if (input instanceof Blob) { + this.streamInfo = { + recordSize: rs, + fileSize: 21 + input.size + 16 * Math.floor(input.size / (rs - 17)) + }; + inputStream = new ReadableStream(new BlobSlicer(input, rs, mode)); + } else { + const sliceStream = new TransformStream(new StreamSlicer(rs, mode)); + inputStream = input.pipeThrough(sliceStream); + } const ts = new TransformStream(new ECETransformer(mode, key, rs, salt)); this.stream = inputStream.pipeThrough(ts); diff --git a/app/fileReceiver.js b/app/fileReceiver.js index aef142b0..5ca9f62b 100644 --- a/app/fileReceiver.js +++ b/app/fileReceiver.js @@ -1,7 +1,7 @@ import Nanobus from 'nanobus'; import Keychain from './keychain'; import { bytes } from './utils'; -import { metadata, downloadFile } from './api'; +import { metadata, downloadFile, downloadStream} from './api'; export default class FileReceiver extends Nanobus { constructor(fileInfo) { @@ -51,24 +51,64 @@ export default class FileReceiver extends Nanobus { this.state = 'ready'; } - async streamToArrayBuffer(stream, streamSize) { - const reader = stream.getReader(); - const result = new Uint8Array(streamSize); - let offset = 0; + /* + async streamToArrayBuffer(stream, streamSize) { + try { + var finish; + const promise = new Promise((resolve) => { + finish = resolve; + }); + const result = new Uint8Array(streamSize); + let offset = 0; - let state = await reader.read(); - while (!state.done) { - result.set(state.value, offset); - offset += state.value.length; - state = await reader.read(); + + const writer = new WritableStream( + { + write(chunk) { + result.set(state.value, offset); + offset += state.value.length; + }, + close() { + //resolve a promise or something + finish.resolve(); + } + } + ); + + stream.pipeTo(writer); + + await promise; + return result.slice(0, offset).buffer; + + } catch (e) { + console.log(e) } + } + */ - return result.slice(0, offset).buffer; + async streamToArrayBuffer(stream, streamSize) { + try { + const result = new Uint8Array(streamSize); + let offset = 0; + console.log("reading...") + const reader = stream.getReader(); + let state = await reader.read(); + console.log("read done") + while (!state.done) { + result.set(state.value, offset); + offset += state.value.length; + state = await reader.read(); + } + + return result.slice(0, offset).buffer; + } catch (e) { + console.log(e) + } } async download(noSave = false) { this.state = 'downloading'; - this.downloadRequest = await downloadFile( + this.downloadRequest = await downloadStream( this.fileInfo.id, this.keychain, p => { @@ -78,18 +118,22 @@ export default class FileReceiver extends Nanobus { ); try { + const ciphertext = await this.downloadRequest.result; this.downloadRequest = null; this.msg = 'decryptingFile'; this.state = 'decrypting'; this.emit('decrypting'); - const dec = await this.keychain.decryptStream(ciphertext); - const plaintext = await this.streamToArrayBuffer( + const dec = this.keychain.decryptStream(ciphertext); + + let plaintext = await this.streamToArrayBuffer( dec.stream, this.fileInfo.size ); + if (plaintext === undefined) { plaintext = (new Uint8Array(1)).buffer; } + if (!noSave) { await saveFile({ plaintext, @@ -97,8 +141,10 @@ export default class FileReceiver extends Nanobus { type: this.fileInfo.type }); } + this.msg = 'downloadFinish'; this.state = 'complete'; + } catch (e) { this.downloadRequest = null; throw e;