Craig Bruce

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.