22

I want to download an encrypted file from my server, decrypt it and save it locally. I want to decrypt the file and write it locally as it is being downloaded rather than waiting for the download to finish, decrypting it and then putting the decrypted file in an anchor tag. The main reason I want to do this is so that with large files the browser does not have to store hundreds of megabytes or several gigabytes in memory.

Hephaestious
  • 431
  • 1
  • 5
  • 12
  • Do you have decryption algorithm for the data? – guest271314 Sep 25 '16 at 01:07
  • I'm planning to use AES. – Hephaestious Sep 25 '16 at 01:08
  • You can't write to files on a users computer in browser Javascript. So you can't do what you are trying to achieve unless you are working with, say, Electron or something similar. – Luke Joshua Park Sep 25 '16 at 01:08
  • @LukePark _"You can't write to files on a users computer in browser Javascript."_ Technically, it is possible to write to user filesystem. – guest271314 Sep 25 '16 at 01:09
  • What about with the method [this post](http://stackoverflow.com/questions/2897619/using-html5-javascript-to-generate-and-save-a-file) describes? Would it be possible to edit the content as it is being downloaded? – Hephaestious Sep 25 '16 at 01:09
  • @guest271314 No it isn't. Unless you consider cache, local storage etc to be "writing to the filesystem". Which it isn't. – Luke Joshua Park Sep 25 '16 at 01:10
  • @LukePark See [How to Write in file (user directory) using JavaScript?](http://stackoverflow.com/questions/36098129/how-to-write-in-file-user-directory-using-javascript/) . – guest271314 Sep 25 '16 at 01:10
  • @Hephaestious You can use `ReadableStream`, see [JS Promise - instantly retrieve some data from a function that returns a Promise](http://stackoverflow.com/questions/39140670/js-promise-instantly-retrieve-some-data-from-a-function-that-returns-a-promise/) – guest271314 Sep 25 '16 at 01:13
  • Didnt know this existed. Still not sure if it will solve the problem though. – Luke Joshua Park Sep 25 '16 at 01:15
  • _"Still not sure if it will solve the problem though"_ What is the problem? – guest271314 Sep 25 '16 at 01:17
  • It looks like that allows you to read the stream as it is being downloaded but I don't see how to save the stream locally as it is being processed – Hephaestious Sep 25 '16 at 01:19
  • What do you mean by _"modify the data being saved"_? Use the decryption method that you select – guest271314 Sep 25 '16 at 01:20
  • How would you then save that data locally as you process it? – Hephaestious Sep 25 '16 at 01:21
  • You can store the data as a `Blob` or `ArrayBuffer`, as the data is streaming, following any decryption processes; then use `createObjectURL` or `data URI` of data for user to download when stream is complete – guest271314 Sep 25 '16 at 01:22
  • 1
    @guest271314 - that would require the whole download to complete prior to the file being written to the local filesystem - I think the OP wants to "pipe" the incoming data through some decryption component and "pipe" the decrypted data to filesystem - such that `the browser does not have to store hundreds of megabytes or several gigabytes in memory.` – Jaromanda X Sep 25 '16 at 01:24
  • @JaromandaX Correct, I need to do the decryption and saving on the fly. – Hephaestious Sep 25 '16 at 01:25
  • How can a file be saved "on the fly"? – guest271314 Sep 25 '16 at 01:25
  • By writing to a download stream or file, the latter of which I'm aware JavaScript doesn't really allow for. – Hephaestious Sep 25 '16 at 01:25
  • @Hephaestious - is an addon or web extension a viable option for you (requires clients to choose to install the addon/extension - which means no support for IE at all of course) – Jaromanda X Sep 25 '16 at 01:27
  • Should be possible using `nodejs`. Here, would want for entire stream to be completed and verified before offering a file for download. What if 100MB of file is ok, though last byte is corrupted? – guest271314 Sep 25 '16 at 01:27
  • I'm going to have a C# client that can do the same thing but I'd really like to be able to have a web client that does not have external dependencies. – Hephaestious Sep 25 '16 at 01:28
  • @JaromandaX Why would it be a problem? An attacker would still need access to the user's browser or system to view the data, no? – Hephaestious Sep 25 '16 at 01:30
  • @Hephaestious See [stream-handbook](https://github.com/substack/stream-handbook) . Note, you can, generally, utilize use `browserify` to use `nodejs` at browser. – guest271314 Sep 25 '16 at 01:30
  • @guest271314 I don't see any way to download data from that stream to the client's filesystem. – Hephaestious Sep 25 '16 at 01:35
  • @JaromandaX I'm going to be using the Stanford Crypto Library most likely, so I don't think it would matter if people can see how the data is encrypted or decrypted when using a secure algorithm such as AES. The user will still need to enter a password to decrypt anything. – Hephaestious Sep 25 '16 at 01:36
  • @Hephaestious - no problems - I'll remove the "noise" in the comments about this aspect :p – Jaromanda X Sep 25 '16 at 01:37
  • @Hephaestious Yes, `nodejs` is not, generally, intended to be used at browser. Was attempting to link to illustrations of using `.pipe()`. Process data at server, or even `Worker`, then offer download. Again, not certain how a file can be downloaded as a stream. Closest have viewed is a `.zip` file being populated as it downloads. What is the size of the file that will be downloaded? – guest271314 Sep 25 '16 at 01:40
  • @guest271314 - `What is the size of the file that will be downloaded?` - **it's in the question** – Jaromanda X Sep 25 '16 at 01:42
  • @JaromandaX Ok. Got it. – guest271314 Sep 25 '16 at 01:43
  • @Hephaestious Have you tried without attempting to stream the download? Does browser freeze? – guest271314 Sep 25 '16 at 01:43
  • Without streaming the download it would be very easy: Just do an ajax.get(url), decrypt the data and put it in an anchor tag or one of the other ways you can download a specified string with JavaScript. I want to decrypt it client side as if I decrypt it server side there's not really any point in using encryption at all. – Hephaestious Sep 25 '16 at 01:47
  • Not focusing, here, on decryption portion, only technical viability of streaming a download. You could probably stream to user filesystem using `requestFileSystem`, then use `.toURL()` to offer download. Though, admittedly, have not tried to append characters to a `data URI` set at `a` element `href` after user has clicked anchor – guest271314 Sep 25 '16 at 01:49
  • Not all browsers support requestFileSystem. – Hephaestious Sep 25 '16 at 01:51
  • Yes, that is true. Just trying to offer possible options to meet requirement. – guest271314 Sep 25 '16 at 01:52
  • All browsers have `Blob` and `ArrayBuffer` defined; though, again, have not tried appending bytes to a `Blob` or `ArrayBuffer` as the bytes are being downloaded to a local file. – guest271314 Sep 25 '16 at 01:53
  • Thanks you! I'm going to test this right now. – Hephaestious Sep 25 '16 at 02:01
  • @Hephaestious Tried with `i < 100000`, tab crashed – guest271314 Sep 25 '16 at 02:04
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/124131/discussion-between-hephaestious-and-guest271314). – Hephaestious Sep 25 '16 at 02:06

2 Answers2

23

This is only going to be possible with a combination of service worker + fetch + stream A few browser has worker and fetch but even fewer support fetch with streaming (Blink)

new Response(new ReadableStream({...}))

I have built a streaming file saver lib to communicate with a service worker in other to intercept network request: StreamSaver.js

It's a little bit different from node's stream here is an example

function unencrypt(){
    // should return Uint8Array
    return new Uint8Array()
}

// We use fetch instead of xhr that has streaming support
fetch(url).then(res => {
    // create a writable stream + intercept a network response
    const fileStream = streamSaver.createWriteStream('filename.txt')
    const writer = fileStream.getWriter()

    // stream the response
    const reader = res.body.getReader()
    const pump = () => reader.read()
        .then(({ value, done }) => {
            let chunk = unencrypt(value)

            // Write one chunk, then get the next one
            writer.write(chunk) // returns a promise

            // While the write stream can handle the watermark,
            // read more data
            return writer.ready.then(pump)
        )

    // Start the reader
    pump().then(() =>
        console.log('Closed the stream, Done writing')
    )
})

There are also two other way you can get streaming response with xhr, but it's not standard and doesn't mather if you use them (responseType = ms-stream || moz-chunked-arrayBuffer) cuz StreamSaver depends on fetch + ReadableStream any ways and can't be used in any other way

Later you will be able to do something like this when WritableStream + Transform streams gets implemented as well

fetch(url).then(res => {
    const fileStream = streamSaver.createWriteStream('filename.txt')

    res.body
        .pipeThrogh(unencrypt)
        .pipeTo(fileStream)
        .then(done)
})

It's also worth mentioning that the default download manager is commonly associated with background download so ppl sometimes close the tab when they see the download. But this is all happening in the main thread so you need to warn the user when they leave

window.onbeforeunload = function(e) {
  if( download_is_done() ) return

  var dialogText = 'Download is not finish, leaving the page will abort the download'
  e.returnValue = dialogText
  return dialogText
}
Endless
  • 34,080
  • 13
  • 108
  • 131
  • Is implementation of unencrypt methos missing? – Naveen Ramawat Feb 17 '20 at 07:51
  • yes, cuz unencrypt can work diffrent, have a look at webcrypto do implement your own decryption – Endless Feb 17 '20 at 08:39
  • Hi, I am using angular and I tries the same solution, I am not able to get download popup even I have not seen any error on console. – Naveen Ramawat Feb 17 '20 at 10:29
  • I could able to download in stream, Its lifesaver, Thanks a lot – Naveen Ramawat Feb 17 '20 at 14:18
  • Using the StreamSaver JS on client side to stream the data. The file being download is encrypted and data in base64 format. The stream data returned from fetch is in Uint8Array. Is is there anyway way to change it string format ? The reason I am asking once the data is in Unit8Array format, I am finding it difficult to get the string (bas64 encoded data) from Unit8Array when the data involves the multi byte charaters – Ranganatha Feb 18 '20 at 10:17
  • HI I am facing another problem, When I download file around 1 GB its downloaded twice but when file is small it download once, At large file it makes three calls in background 1)https://localhost:8443/api/v1.0/ui/appliance/download_logs?archived_file=8948060_logs.tar.gz 2)https://jimmywarting.github.io/StreamSaver.js/mitm.html?version=2.0.0 3)https://jimmywarting.github.io/StreamSaver.js/localhost:8443/965011/8948060_logs.tar.gz| But on small file only two calls made (1) and (3), Any idea why its happening? – Naveen Ramawat Feb 18 '20 at 11:37
  • 1
    could you make a new issue in my repo or post a new SO question with a example and what goes wrong? – Endless Feb 18 '20 at 14:17
  • Thanks for quick response Jimmy Warting, There is one more issue I am having. My system running on self signed certificate. And I want to serve mitm.html from my server because at client setup outer internet might be unavailable. I tried but facing issue to register service worker. Is it possible to use mitm from local server which is running using self sign certificate ? – Naveen Ramawat Feb 19 '20 at 06:18
5

New solution has arrived: showSaveFilePicker/FileSystemWritableFileStream, supported in Chrome and all major derivatives (including Edge and Opera) since the end of 2020, and with a shim (written by the author of the other major answer!) for Firefox and Safari, will allow you to do this directly:

async function streamDownloadDecryptToDisk(url, DECRYPT) {

    // create readable stream for ciphertext
    let rs_src = fetch(url).then(response => response.body);

    // create writable stream for file
    let ws_dest = window.showSaveFilePicker().then(handle => handle.createWritable());

    // create transform stream for decryption
    let ts_dec = new TransformStream({
        async transform(chunk, controller) {
            controller.enqueue(await DECRYPT(chunk));
        }
    });

    // stream cleartext to file
    let rs_clear = rs_src.then(s => s.pipeThrough(ts_dec));
    return (await rs_clear).pipeTo(await ws_dest);

}

Depending on performance—if you're trying to compete with MEGA, for instance—you might also consider modifying DECRYPT(chunk) to allow you to use ReadableStreamBYOBReader with it:

…zero-copy reading from an underlying byte source. It is used for efficient copying from underlying sources where the data is delivered as an "anonymous" sequence of bytes, such as files.

  • Hi James, I am working on something similar and unable to figure out how to read chunk of a specific size. When uploading I make chunks of 5MB using slice and encrypt each of them and send it via multipart upload to S3. However when downing, I am unable to define the chunk size. – Samay Aug 26 '22 at 16:24
  • And when we are processing chunk of a specific size, would it not create issues as the speed of stream generate from fetch would not match the processing. – Samay Aug 26 '22 at 16:25
  • @Samay *If* the disk and CPU cannot keep up with decrypting and storing the files at full network speed, I believe that the browser will [throttle the `fetch` stream appropriately](https://developer.mozilla.org/en-US/docs/Web/API/Streams_API/Concepts#backpressure), *just as it does when downloading files normally*. – JamesTheAwesomeDude Aug 26 '22 at 16:35
  • @Samay The `reader` in this case is _nothing more than_ [the `ReadableStream` produced by Fetch itself](https://developer.mozilla.org/en-US/docs/Web/API/Response#response.body). It spits out data in whatever chunk size is efficient for downloading, probably to do with the network, at the browser's discretion. If you need to process the data in 32B or 5MB or whatever chunk sizes, you'll need to [package such units up yourself](https://codereview.stackexchange.com/a/58877/242503), from the stream Fetch gives you. – JamesTheAwesomeDude Aug 26 '22 at 17:03
  • Thanks @James. That was useful. I create a wrapper around TransformStream that works in the browser. https://gist.github.com/samaybhavsar/97d2674536c6f64de8b6d2c43085a347 – Samay Aug 30 '22 at 14:36