End to End Browser Crypto

In my previous post on JavaScript crypto we looked at decrypting simple data in the browser using CryptoJS. In this post we will tackle a more complex end to end file encryption scenario, also taking a look at some nuances of the HTML5 File Api as we go.

Encryption

First of all we need to get a file off the file system.

// create a key and initializationVector .. 
// this can be done by CryptoJS or your own server-side technology

const { key, iv } = await getKeyPerhapsFromServer();
const reader = new FileReader();
const file = document.getElementById('file-input').files[0]; // user selected file

reader.onload = (e) => {
  const encrypted = CryptoJS.AES.encrypt(
    //convert to a word array via CryptoJS. 'this' is the file reader, 'result' is the file.
    arrayBufferToWordArray(this.result),

    //our server generated key happens to be in Base64. 
    //We need to convert it to a word array
    CryptoJS.enc.Base64.parse(key),
    {
      iv: CryptoJS.enc.Base64.parse(iv)
    });

  const blob = new Blob([encrypted], { type: file.type });
  uploadToServer(blob);
};

reader.readAsArrayBuffer(file);

We need to call FileReader.readAsArrayBuffer as using any of the other string based variants will mess around with encoding when encrypting. I found this through much trial and error.

Then we just need to construct a new Blob with the encrypted result, assign it to our blob variable and upload it to the server.

Decryption

When we download a file from the server we need to intercept it, decrypt the contents and then hand it off to the browser as though it is a normal file download.


const file = await getFileFromServer("url/to/file");
const { key, iv } = await getKeyPerhapsFromServer();

const decrypted = CryptoJS.AES.decrypt(
    file,
    CryptoJS.enc.Base64.parse(key), 
    {
        iv: CryptoJS.enc.Base64.parse(iv)
    });

//we need to jump through a hoop or two here
const blob = new Blob([new Uint8Array(toArrayBuffer(decrypted))], 
                        {type: file.mimeType});

const url = (window.webkitURL || window.URL).createObjectURL(blob);

// we'll bust out the yayQuery for brevity.
const a = $("<a>")
    .attr("id", id)
    .attr("download", filename)
    .attr("href", url)
    .attr("textContent", "Download ready")
    .appendTo("body");

$(a)[0].click();
$(a)[0].remove();
(window.webkitURL || window.URL).revokeObjectURL(url);

There are a few things going on here:

  1. Download the encrypted content
  2. We need to decrypt using the same key and initialisation vector as when we encrypted
  3. We then create a new Blob where the contents are converted to a Uint8Array using CryptoJS functions
  4. The mime type is something we determined on upload and is stored on our server.
  5. We need to generate a Url via createObjectURL
  6. Then we are relying on an anchor tag with the download attribute from HTML5 to get a seamless download experience.
  7. Simulate a click and watch the decrypted file pop into your browser's download list.

That's it.