Charles Engelke's Blog

July 16, 2014

Symmetric Cryptography in the Browser – Conclusion

Filed under: Uncategorized — Charles Engelke @ 8:36 pm
Tags: ,

This series of posts is almost complete. We’ve created, imported, and exported AES keys and used them to encrypt and decrypt files, all inside a standard web browser using the new Web Cryptography API. All that’s left is to put it all together into a single web page. That’s what we’ll do now.

The page and code discussed in here are available on Github, and as a live page you can immediately try out.

I’m going to put together the simplest page I can think of. The top part will cover generating, importing, and exporting keys. The middle part will allow selection of a file on your computer and buttons to encrypt or decrypt that file, and the bottom part will have links to the results of the operations you’ve already done. Here’s a picture of the page:

Image of demonstration web page

As of now, this page works in the current version of Google Chrome, provided you turn on the “Enable Experimental Web Platform Features” flag, and in the current Firefox nightly build. It doesn’t work in Internet Explorer 11 because that implements an older version of the API and uses a vendor prefix for it. However, it should be fairly straightforward to modify this to work there.

Let’s take a look at the HTML:

<!DOCTYPE html>
<html>
<head>
    <title>Symmetric Encryption</title>
    <script src="crypto.js"></script>
</head>
<body>
    <h1>Symmetric Encryption</h1>
    <section id="key-management">
        <input type="text" size="32" id="aes-key"/>
        <button id="generate-key">Generate Random AES Key</button>
    </section>
    <section id="encrypt-and-decrypt">
        <input type="file" id="source-file"/>
        <button id="encrypt">Encrypt File</button>
        <button id="decrypt">Decrypt File</button>
    </section>
    <section id="results">
        Download results:
        <ul id="download-links">
        </ul>
    </section>
</body>
</html>

It’s pretty simple. The head loads the JavaScript that does all the work. The key-management section allows you to enter a hex encoded key, or click a button to generate a random one. The encrypt-and-decrypt section is where you select a file to operate on and click a button to perform the operation, and the results section contains a list of links to download the results of each operation.

The JavaScript is where all the work happens. We don’t want to set up any click handlers until the page is ready, so the content of crypto.js is all wrapped in a function that runs when the page is ready:

document.addEventListener("DOMContentLoaded", function() {
    "use strict";
    if (!window.crypto || !window.crypto.subtle) {
        alert("Your current browser does not support the Web Cryptography API! This page will not work.");
        return;
    }
    // rest of code goes here
});

If the browser doesn’t support web.crypto.subtle this shows the user a message and does nothing else. Otherwise, it continues by attaching listeners to each button:

    document.getElementById("generate-key").addEventListener("click", generateAKey);
    document.getElementById("encrypt").addEventListener("click", encryptTheFile);
    document.getElementById("decrypt").addEventListener("click", decryptTheFile);

And all the work happens in the three listeners. First, the one that generates a new key on demand:

    function generateAKey() {
        window.crypto.subtle.generateKey(
            {name: "AES-CBC", length: 128}, // Algorithm using this key
            true,                           // Allow it to be exported
            ["encrypt", "decrypt"]          // Can use for these purposes
        ).
        then(function(aesKey) {
            window.crypto.subtle.exportKey('raw', aesKey).
            then(function(aesKeyBuffer) {
                document.getElementById("aes-key").value = arrayBufferToHexString(aesKeyBuffer);
            }).
            catch(function(err) {
                alert("Key export failed: " + err.message);
            });
        }).
        catch(function(err) {
            alert("Key generation failed: " + err.message);
        });
    }

We’ve seen code like this in the previous posts. The only new thing here is that once a key is generated it’s exported, converted to a hex string, and that string placed in the input box for use by future operations. Each operation uses the value in that input box instead of a saved key object so that the user can put new keys in at any time. Exporting the key and later importing it to use it is inefficient, but not a big deal.

Actually encrypting the file is quite a bit longer:

    function encryptTheFile() {
        var sourceFile = document.getElementById("source-file").files[0];
        var aesKeyBytes = hexStringToByteArray(document.getElementById("aes-key").value);
        var aesKey; // To be created below
        var reader = new FileReader();
        reader.onload = function() {
            var iv = window.crypto.getRandomValues(new Uint8Array(16));
            window.crypto.subtle.encrypt(
                {name: "AES-CBC", iv: iv},
                aesKey,
                new Uint8Array(reader.result)
            ).
            then(function(result) {
                var blob = new Blob([iv, new Uint8Array(result)], {type: "application/octet-stream"});
                var blobUrl = URL.createObjectURL(blob);
                document.getElementById("download-links").insertAdjacentHTML(
                    'beforeEnd',
                    '<li><a href="' + blobUrl + '" download="' + sourceFile.name + '.encrypted">Encryption of ' + sourceFile.name + '</a></li>'
                    );
            }).
            catch(function(err) {
                alert("Encryption failed: " + err.message);
            });
        };

        window.crypto.subtle.importKey(
            "raw",
            aesKeyBytes,
            {name: "AES-CBC", length: 128},
            true,
            ["encrypt", "decrypt"]
        ).
        then(function(importedKey) {
            aesKey = importedKey;
            reader.readAsArrayBuffer(sourceFile);
        }).
        catch(function(err) {
            alert("Key import and file read failed: " + err.message);
        });
    }

Let’s take this code a step at a time. First we create the variables that will be used in each further step: the sourceFile (from the file selector input box), the aesKey (that will be imported from the aesKeyBytes pulled out of the input box in the page), and the FileReader object reader.

Next we tell what should happen when the file has been read in. That’s identical to what we did in the last post except for inserting a list element in the web page with a link to the blobUrl we create. The steps are: create a random initialization vector iv, encrypt the data that was read in with the aesKey and the iv, then create a blob with the iv followed by the encrypted data and build a URL for that blob. The only problem here is that we haven’t seen any value assigned to aesKey yet.

We need to import the bytes for the AES key into aesKey, and that’s the last step in the code, though it runs before the file has been read. Once the imported key is ready, we kick off the FileReader reader. When it’s done, the handler described in the last paragraph takes over and finishes the job. The use of a callback when reading a file means that the steps are defined out of the order they will actually run. If the FileReader object was a Promise, it might be clearer.

The decryption code is much like the code above, except it pulls the iv out of the beginning of the encrypted file instead of creating a new random one. Again, this was covered in more detail in the last post:

    function decryptTheFile() {
        var sourceFile = document.getElementById("source-file").files[0];
        var aesKeyBytes = hexStringToByteArray(document.getElementById("aes-key").value);
        var aesKey; // To be created below
        var reader = new FileReader();
        reader.onload = function() {
            var iv = new Uint8Array(reader.result.slice(0, 16));
            window.crypto.subtle.decrypt(
                {name: "AES-CBC", iv: iv},
                aesKey,
                new Uint8Array(reader.result.slice(16))
            ).
            then(function(result) {
                var blob = new Blob([new Uint8Array(result)], {type: "application/octet-stream"});
                var blobUrl = URL.createObjectURL(blob);
                document.getElementById("download-links").insertAdjacentHTML(
                    'beforeEnd',
                    '<li><a href="' + blobUrl + '" download="' + sourceFile.name + '.decrypted">Decryption of ' + sourceFile.name + '</a></li>'
                    );
            }).
            catch(function(err) {
                alert("Decryption failed: " + err.message);
            });
        };

        window.crypto.subtle.importKey(
            "raw",
            aesKeyBytes,
            {name: "AES-CBC", length: 128},
            true,
            ["encrypt", "decrypt"]
        ).
        then(function(importedKey) {
            aesKey = importedKey;
            reader.readAsArrayBuffer(sourceFile);
        }).
        catch(function(err) {
            alert("Key import and file read failed: " + err.message);
        });
    }

There’s nothing here that wasn’t seen just above or in the prior blog post.

All that’s left are a couple of utility functions to convert between hex encoded strings and arrays of bytes. We saw them two posts ago, but here they are again for completeness:

    function arrayBufferToHexString(arrayBuffer) {
        var byteArray = new Uint8Array(arrayBuffer);
        var hexString = "";
        var nextHexByte;
        for (var i=0; i<byteArray.byteLength; i++) {
            nextHexByte = byteArray[i].toString(16);  // Integer to base 16
            if (nextHexByte.length < 2) {
                nextHexByte = "0" + nextHexByte;     // Otherwise 10 becomes just a instead of 0a
            }
            hexString += nextHexByte;
        }
        return hexString;
    }

    function hexStringToByteArray(hexString) {
        if (hexString.length % 2 !== 0) {
            throw Error("Must have an even number of hex digits to convert to bytes");
        }
        var numBytes = hexString.length / 2;
        var byteArray = new Uint8Array(numBytes);
        for (var i=0; i<numBytes; i++) {
            byteArray[i] = parseInt(hexString.substr(i*2, 2), 16);
        }
        return byteArray;
    }

The entire example is available on GitHub. I hope you find it a useful starting point for using this new API. It worked well for me, encrypting or decrypting a 100MB file in about a second.

Of course, speed doesn’t help if it’s wrong, so I used OpenSSL to check the results. It was kind of a pain because OpenSSL wants the IV as a command line parameter and only the rest of the encrypted payload as its input. But I did managed to do that and it worked. Here are the steps.

My original file is called original, the key from the web page is 9ad96448523485f6f2936c9259eca6e3, and the encrypted version from the web page is called original.encrypted. Here’s the file listing:

-rw-r--r-- 1 charles domain users 105235836 Jul 14 15:13 original
-rw-r--r-- 1 charles domain users 105235856 Jul 14 15:32 original.encrypted

Note that the encrypted file is 20 bytes longer than the original one. That’s due to adding a 16 byte initialization vector, and padding it to be a multiple of 16 bytes long. To use OpenSSL I need to separate the IV from the rest of the ciphertext first, using the intuitive command:

head -c 16 original.encrypted | od -t x1

That x1 instead of just x (for hex) is vital! I left it off at first and got the bytes scrambled because it processed 16-bit words instead of separate bytes. This shows the IV in hex as:

0000000 99 3f 00 6e 1c 30 64 72 5c 7b 35 fe 96 a9 44 25

The IV is just the actual digits starting with 99, with the spaces removed. Now we need to get the raw ciphertext (with the IV stripped from the start):

tail -c 105235840 original.encrypted > raw_ciphertext

That magic number 105235840 is just the size of original.encrypted less 16 bytes for the leading IV. Finally we can use OpenSSL. I’ve stretched out over two lines for a bit more clarity:

openssl enc -aes-128-cbc -d -K 9ad96448523485f6f2936c9259eca6e3 \
-iv 993f006e1c3064725c7b35fe96a94425 -in raw_ciphertext -out plaintext

The resulting plaintext is identical to original, checked with cmp original plaintext.

This ends the series of posts on Symmetric Encryption in the browser. Next time I’ll start looking at using public key cryptography with this API. The basic operations don’t look too hard, but compatibility with existing standard file structures may be really tough. We’ll see.

Advertisement

July 13, 2014

Symmetric Cryptography in the Browser – Part 3

Filed under: Uncategorized — Charles Engelke @ 6:47 pm
Tags: ,

This post is part of a series on cryptography in the browser. Previous posts have used the new Web Cryptography API to create and manage AES keys, and encrypt and decrypt strings. Now we will read a file, encrypt or decrypt it, and allow the result to be saved back in a new file. The next post will finish this first part of the series (that deals with symmetric cryptography) by putting all the pieces so far together into a working web page.

We start with an AES key object called aesKey already created, and a File object sourceFile, perhaps from an HTML input element. We will end up with a URL resultUrl that can be used to fetch and download the encrypted or decrypted file.

Step 1 – Declare variable to hold the URL when created, and set up a FileReader object:

var resultUrl;
var reader = new FileReader();

Step 2 – Specify what should happen when the file has been read in:

reader.onload = encryptTheFile;

or

reader.onload = decryptTheFile;

depending on which operation you want to perform.

Step 3 – Trigger the file to be read as an ArrayBuffer (which can be easily converted to a Uint8Array for processing).

read.readAsArrayBuffer(sourceFile);

All the real work happens in encryptReaderResult or decryptReaderResult. They’re similar, but with some important differences. We need to create a random initialization vector for encryption and save it with the encrypted file, then extract it and use it later for decryption. A common convention is to write the 16 byte initialization at the start of the encrypted file, so it can be read first and used later for decryption. That’s what we’ll do.

function encryptReaderResult() {
    var iv = window.crypto.getRandomValues(new Uint8Array(16));
    window.crypto.subtle.encrypt(
        {name: "AES-CBC", iv: iv},
        aesKey,
        new Uint8Array(reader.result)
    ).
    then(function(result) {
        var blob = new Blob([iv, new Uint8Array(result)], {type: "application/octet-stream"});
        resultUrl = URL.createObjectURL(blob);
    }).
    catch(function(err) {
        alert("Encryption failed: " + err.message);
    });
}

There are a couple of new things in this code. First, note the coercion of read.result (an ArrayBuffer because that’s what we asked the FileReader to provide) to an array of bytes that we can encrypt. Second, we are creating a Blob out of two byte arrays (iv and the result of the encryption) and specifying its content type as application/octet-stream. The browser gives us a URL for that blob, which we can put into a link in the page in order to download its contents.

The decryption is very similar, except instead of putting the iv together with the rest of the file, we start by separating it from the encrypted file:

function decryptReaderResult() {
    var iv = new Uint8Array(reader.result.slice(0, 16));
    window.crypto.subtle.decrypt(
        {name: "AES-CBC", iv: iv},
        aesKey,
        new Uint8Array(reader.result.slice(16))
    ).
    then(function(result) {
        var blob = new Blob([new Uint8Array(result)], {type: "application/octet-stream"});
        resultUrl = URL.createObjectURL(blob);
    }).
    catch(function(err) {
        alert("Decryption failed: " + err.message);
    });
}

The new thing here is the use of the Blob.slice method to address different parts of an ArrayBuffer, so we can pull the iv out of the beginning of the file.

That’s all the pieces we need. Next time (sooner than a week from now, I hope) I’ll show a complete web page to perform these operations.

July 5, 2014

Symmetric Cryptography in the Browser – Part 2

Filed under: Uncategorized — Charles Engelke @ 12:31 pm
Tags: ,

This post is part of a series on cryptography in the browser. My last post covered the basics of encrypting and decrypting with the Web Cryptography API, but had no practical use. That’s because you couldn’t save and later load the key you used, and you couldn’t get meaningful amounts of data into and out of the software. We’ll address the first of those needs now. When we created our encryption key we set the exportable parameter to true. If we hadn’t, the actual key would forever be hidden from us, which would be a good idea if there were an outside-the-browser way to manage it. As of now, there isn’t such a way, so we’ll manage keys in the browser. That requires exporting them to a format that can be saved and transported and importing them from those formats. The format we’ll use is a hexadecimal string, so our 128 bit (16 byte) key will be a 32 character string. We can export a key to a byte array using the window.crypto.subtle.exportKey method. This is a pretty easy method to use. It takes two parameters: the format you want to export to, and the key to export. It returns a promise that passes the exported key (as an ArrayBuffer) to its then method’s parameter. Assuming our AES key is in the variable aesKey, here’s how to get it into a viewable form:

var aesKeyBytes;

window.crypto.subtle.exportKey('raw', aesKey).
then(function(result) {aesKeyBytes = new Uint8Array(result);}).
catch(function(err) {alert("Something went wrong: " + err.message);});

When I try that in my browser with a defined aesKey, I get the following bytes: [51, 155, 145, 34, 55, 159, 162, 158, 253, 202, 19, 78, 139, 186, 51, 118] So I can see the actual key, but I’d rather look at it in hex. For example, 51 is 33 in hex, 155 is 9b, 145 is 91, and so on. I can convert a Uint8ByteArray to a hexadecimal string by converting each byte and concatenating them:

function byteArrayToHexString(byteArray) {
    var hexString = '';
    var nextHexByte;
    for (var i=0; i<byteArray.byteLength; i++) {
        nextHexByte = byteArray[i].toString(16);  // Integer to base 16
        if (nextHexByte.length < 2) {
            nextHexByte = "0" + nextHexByte;     // Otherwise 10 becomes just a instead of 0a
        }
        hexString += nextHexByte;
    }
    return hexString;
}

Given the aesKey and aesKeyBytes shown above, byteArrayToHexString(aesKeyBytes) returns "339b9122379fa29efdca134e8bba3376", which I can easily display and save. Going the other way is pretty easy now. We will use the window.crypto.subtle.importKey method, after first converting a hex string to a byte array with this function:

function hexStringToByteArray(hexString) {
    if (hexString.length % 2 !== 0) {
        throw "Must have an even number of hex digits to convert to bytes";
    }
    var numBytes = hexString.length / 2;
    var byteArray = new Uint8Array(numBytes);
    for (var i=0; i<numBytes; i++) {
        byteArray[i] = parseInt(hexString.substr(i*2, 2), 16);
    }
    return byteArray;
}

Trying it out, hexStringToByteArray("339b9122379fa29efdca134e8bba3376") returns [51, 155, 145, 34, 55, 159, 162, 158, 253, 202, 19, 78, 139, 186, 51, 118], which is what I started with. Now that we have the array of bytes, we’re ready to import it to create a key. The importKey method takes all the same parameters as createKey, plus the key’s bytes and format of those bytes, so the steps to use them are almost the same:

var importedAesKey;

window.crypto.subtle.importKey(
    "raw",                          // Exported key format
    aesKeyBytes,                    // The exported key
    {name: "AES-CBC", length: 128}, // Algorithm the key will be used with
    true,                           // Can extract key value to binary string
    ["encrypt", "decrypt"]          // Use for these operations
).
then(function(key) {importedAesKey = key;}).
catch(function(err) {alert("Something went wrong: " + err.message);});

Now that we can get keys in and out of our code in a human readable form, we’re ready to actually encrypt and decrypt files. The next post will encrypt or decrypt a File into a Blob that can be downloaded. Then we’ll put it all together into a complete web page that performs all these functions.

June 22, 2014

Symmetric Cryptography in the Browser – Part 1

Filed under: Uncategorized — Charles Engelke @ 2:07 pm
Tags: ,

I’m going to start exploring the Web Cryptography API with just about the simplest use case I can think of: symmetric encryption with AES. The user will select a file, have the browser encrypt it, and download the encrypted file. Or select an encrypted file, have the browser decrypt it, and download the plaintext.

The cryptography API is provided in the browser by the window.crypto object. Almost all of the functionality currently available is provided by methods of the window.crypto.subtle object, so named “to reflect the fact that many of these algorithms have subtle usage requirements in order to provide the required algorithmic security guarantees.” That means that this API will do the crypto right, but you’re on your own to use it properly in order to have a secure solution.

Encryption is performed with the encrypt method (guess what method does decryption), but we can’t just jump in and use it. The encrypt method takes a specification of the algorithm to use, a key, and the original plaintext as parameters, and returns a promise. The ciphertext is provided to the function given to that promise’s then method. So before we can do anything else, we need a key. For this API, keys are opaque objects that must be manipulated through API methods. You can’t just use a binary string. We can either import an existing key or generate a new one. We’ll do the latter first.

Keys are created using the generateKey method, which returns another promise. The first parameter describes the kind of key to generate, the second says whether or not you can extract the actual binary key from it, and the third is an array of the purposes of the key. So, to get a 128 bit key to use with the Advanced Encryption Standard in Cipher Block Chaining mode, we’d use:

var keyPromise = window.crypto.subtle.generateKey(
    {name: "AES-CBC", length: 128}, // Algorithm the key will be used with
    true,                           // Can extract key value to binary string
    ["encrypt", "decrypt"]          // Use for these operations
);

This will try to create a new, random key, and pass it to the promise’s then method. We will just save it for now:

var aesKey;   // Global variable for saving
keyPromise.then(function(key) {aesKey = key;});
keyPromise.catch(function(err) {alert("Something went wrong: " + err.message);});

Assuming nothing went wrong, we now have a key stored in the aesKey variable that we can use to encrypt and decrypt data. So let’s try it with some dummy data. The encrypt method takes three parameters. The key we just generated is one of them. The plaintext to encrypt is another. And the remaining parameter is an object that specifies the algorithm to use and provides any needed options for encryption. For AES-CBC that object has the name “AES-CBC” and a property called iv, which is the initialization vector.

I’m not going to get into how AES-CBC works, just that it operates on blocks of 128 bits (16 bytes) each, and you need to provide a random 16 byte (128 bit) chunk called the initialization vector for it to be secure. So let’s start by getting a random block of data using window.crypto.getRandomValues:

var iv = new Uint8Array(16);
window.crypto.getRandomValues(iv);

Because getRandomValues returns the array it fills in, too, this could be written more consisely as:

var iv = window.crypto.getRandomValues(new Uint8Array(16));

Unlike most of this API’s operations, getRandomValues is not asynchronous so it doesn’t require a promise. It just gets some cryptographically strong data and puts it in the array provided. Now we could perform encryption, if we just had something to encrypt. Let’s just put some text in a string for that:

var plainTextString = "This is very sensitive stuff.";

Unfortunately, encrypt doesn’t take JavaScript strings, just blocks of memory, so we’ve got to copy the contents of the string to an array of bytes:

var plainTextBytes = new Uint8Array(plainTextString.length);
for (var i=0; i<plainTextString.length; i++) {
    plainTextBytes[i] = plainTextString.charCodeAt(i);
}

And now we can encrypt it, saving the ciphertext to a global variable:

var cipherTextBytes;
var encryptPromise = window.crypto.subtle.encrypt(
    {name: "AES-CBC", iv: iv}, // Random data for security
    aesKey,                    // The key to use 
    plainTextBytes             // Data to encrypt
);
encryptPromise.then(function(result) {cipherTextBytes = new Uint8Array(result);});
encryptPromise.catch(function(err) {alert("Problem encrypting: " + err.message);});

Note that I have to convert the result into a Uint8Array. That’s because the encrypt operation returns an ArrayBuffer, which is just a chunk of memory. If I want to see what’s in it, I have to have a real typed array.

When I run this in the JavaScript console on my Chrome browser I get the following values for cipherTextBytes:

[93, 197, 31, 64, 100, 122, 144, 131, 57, 185, 92, 198, 185, 152, 106, 27,
151, 244, 48, 204, 12, 195, 49, 97, 148, 26, 165, 173, 127, 178, 56, 38]

A couple of things to note here. First, if you run the same code, you should get a totally different answer. That’s because we seeded the operation with a random initialization vector in order avoid some cryptanalysis techniques that could crack our encryption if we encrypted multiple plaintexts with the same key. Second, the string we started with was 29 characters long, but the cipherTextBytes is 32 bytes long. That’s because AES always encrypts 16 byte blocks, so the encrypt operation padded our original text before encrypting it.

Decrypting is nearly identical:

var decryptPromise = window.crypto.subtle.decrypt(
    {name: "AES-CBC", iv: iv}, // Same IV as for encryption
    aesKey,                    // The key to use
    cipherTextBytes            // Data to decrypt
);
var decryptedBytes;
decryptPromise.then(function(result) {decryptedBytes = new Uint8Array(result);});
decryptPromise.catch(function(err) {alert("Problem decrypting: " + err.message); });

After running this, I see the contents of decryptedBytes is:

[84, 104, 105, 115, 32, 105, 115, 32, 118, 101, 114, 121, 32, 115, 101, 110,
115, 105, 116, 105, 118, 101, 32, 115, 116, 117, 102, 102, 46]

We have to convert it back to a string to read it:

var decryptedString = "";
for (var i=0; i<decryptedBytes.byteLength; i++) {
    decryptedString += String.fromCharCode(decryptedBytes[i]);
}

And now when I look at that string, I see:

"This is very sensitive stuff."

Which is what we started with. The decrypt operation removed the padding that the encrypt operation had added, so we’re back to 29 characters.

This code works, but it’s not very useful. You’ve got to put the plainText into the program as a literal and the cipherText isn’t something you can easily display and save. Much worse is that the key and initialization vector aren’t saved, so you can only decrypt things if you haven’t closed the browser window. If you close it and reopen it you’ll get a new key and new initialization vector, which are useless for decrypting old ciphertexts.

My next blog entry will deal with these problems, creating a web page and actually useful JavaScript code for encrypting and decrypting files.

Blog at WordPress.com.