Charles Engelke's Blog

September 19, 2014

Saving Cryptographic Keys in the Browser

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

Prior posts here have used the Web Cryptography API to encrypt and decrypt files, and to sign and verify them. But those examples have no practical use because the keys being used are stored in JavaScript variables and disappear as soon as they go out of scope. This post will take the first step toward solving that problem, creating a persistent key store within the browser so that key pairs can be used over multiple sessions and on multiple pages. This is still not any kind of production code, though; it’s an illustration of some of the problems any code you write will have to solve.

The sample code is available on Github, and a live demonstration is also available. But before jumping into coding, consider the risks of storing private keys in the browser. A web browser is a challenging security environment by its very nature, but when proper care is taken it is usually a reasonable choice for an application platform. However, private cryptographic keys are very sensitive. In many uses a private key is a proxy for a user’s actual identity. If they are going to be managed within a web browser, the greatest possible care should be taken with them. In particular, there should be additional safeguards to prevent rogue code (or even just erroneous code) from sending private keys to anyone.

The Web Cryptography API can provide those safeguards by keeping private keys opaque. That is, the API lets applications create and use keys without ever being able to see their actual values. If code inside the browser can’t see the values, it can’t disclose them. All cryptographic keys are stored as type CryptoKey which do not provide access to their values, which are stored outside the browser environment. It is up to the browser vendor to make that storage as secure as possible; in any case, it is inaccessible from inside the browser.

There are cases where code inside the browser might need to access the actual value of key. For example, the public key encryption example created an AES-CBC key and then needed to make it visible so it could be encrypted with RSA-OAEP. That was possible because the AES-CBC key was created with its extractable property set to true. To keep stored keys secure, set their extractable property to false when they are created or imported.

Aside: the API always allows public keys to be exported, regardless of how they are created or imported. That’s because public keys are not sensitive information. They are intended to be shared.

Because CryptoKey objects are opaque they can’t be placed in localStorage, which only supports simple types. Instead the key store will use IndexedDB, which can clone and store opaque variables. IndexedDB is more complex than localStorage, so dealing with it will be take up much of this post.

The Sample Web Page

The page demonstrates key storage by letting the user create signing or encrypting key pairs and storing them persistently. A list of the stored key pairs is always displayed, along with a link to download the public key portion of the saved key pair. There’s very little to the page itself:

<!DOCTYPE html>
<!-- Copyright 2014 Info Tech, Inc. Provided under the MIT license. See LICENSE file for details. -->
<html>
<head>
    <title>Key Management</title>
    <script src="keystore.js"></script>
    <script src="keymanagement.js"></script>
</head>
<body>
    <h1>Key Management</h1>
    <section id="create-keys">
        <h1>Create New Key Pair</h1>
        Key Name: <input type="text" id="created-key-name"/><br/>
        Purpose:
            <input type="radio" name="created-key-type" value="Signing">Signing</input>
            <input type="radio" name="created-key-type" value="Encrypting">Encrypting</input>
            <br/>
        <button id="create-key">Create Key Pair</button>
    </section>
    <section id="list-keys">
        <h1>Stored Keys</h1>
        <ul id="key-list">
        </ul>
    </section>
</body>
</html>

Key storage is provided by a KeyStore object whose code is in keystore.js. The page itself is managed using code in keymanagement.js. The key storage code will be deferred for now. All we need to know to use it is how to create a KeyStore object and use the methods it provides:

  • Create the object with new KeyStore().
  • All object methods return Promises because almost all IndexedDB operations are asynchronous.
  • Open and close the key store with the open and close methods.
  • Save a key pair with the saveKey method, providing the public key, the private key (or null if not known), and a user-supplied name to identify it.
  • Fetch a key pair with the getKey method, providing either the database supplied id, the assigned name, or the exported public key in spki format.
  • Get a list of all key pairs with the listKeys method.

The overall structure of the page’s JavaScript is very similar to all the previous examples:

// Copyright 2014 Info Tech, Inc.
// Provided under the MIT license.
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;
    }
    if (!window.indexedDB) {
        alert("Your current browser does not support IndexedDB. This page will not work.");
        return;
    }
    // All the work happens here.
}

The only new thing here is the check for window.indexedDB to be defined, along with the previously shown check for window.crypto.subtle.

The body of the code needs to create and open a key store, set a click handler on the only button on the page, and add a list of all stored keys to the page:

    var keyStore = new KeyStore();
    keyStore.open().
    then(function() {
        document.getElementById("create-key").addEventListener("click", handleCreateKeyPairClick);
        populateKeyListing(keyStore);
    }).
    catch(function(err) {
        alert("Could not open key store: " + err.message)
    });

Note that the keyStore object is never closed. An IndexedDB database will close automatically when the variable goes out of scope. The close method is provided only so the developer can force it to close earlier if desired.

The populateKeyListing function uses the new key storage listKeys method. The Promise it returns passes an array of objects to the resolver. Each object has an id property and the value of the actual stored key. The value is an object with publicKey, privateKey, name, and spki properties. (The name of the standard export format for public keys is spki (for Subject Public Key Info), which is why that property has an odd name.)

    function populateKeyListing(keyStore) {
        keyStore.listKeys().
        then(function(list) {
            for (var i=0; i<list.length; i++) {
                addToKeyList(list[i].value);
            }
        }).
        catch(function(err) {
            alert("Could not list keys: " + err.message);
        });
    }

    function addToKeyList(savedObject) {
        var dataUrl = createDataUrlFromByteArray(new Uint8Array(savedObject.spki));
        var name = escapeHTML(savedObject.name);
        document.getElementById("list-keys").insertAdjacentHTML(
            'beforeEnd',
            '<li><a download="' + name + '.publicKey" href="' + dataUrl + '">' + name + '</a></li>');
    }

The addToKeyList function adds a single key to the list of keys and makes each one a link that can be used to download the public key in spki format. To do that job, it relies on two utility functions: createDataUrlFromByteArray and escapeHTML:

    function escapeHTML(s) {
        return s.toString().replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
    }

    function createDataUrlFromByteArray(byteArray) {
        var binaryString = '';
        for (var i=0; i<byteArray.byteLength; i++) {
            binaryString += String.fromCharCode(byteArray[i]);
        }
        return "data:application/octet-stream;base64," + btoa(binaryString);
    }

Prior examples provided download links by creating a URL for a Blob. Exported public keys are relatively short, so the simpler dataURI link is used here instead.

The button click handler is the only code left to write for the page. It needs to check that a name is provided and determine whether the user selected a signing key or an encrypting key. Those two kinds of keys use different algorithms and have different usages, so the correct values are saved in variables. Then it creates the key pair, saves it in the key store, and adds it to the displayed list of keys:

    function handleCreateKeyPairClick() {
        var algorithmName, usages;

        var name = document.getElementById("created-key-name").value;
        if (!name) {
            alert("Must specify a name for the new key.");
            return;
        }

        var selection = document.getElementsByName("created-key-type");
        if (selection[0].checked) { // Signing key
            algorithmName = "RSASSA-PKCS1-v1_5";
            usages = ["sign", "verify"];
        } else if (selection[1].checked) { // Encrypting key
            algorithmName = "RSA-OAEP";
            usages = ["encrypt", "decrypt"];
        } else {
            alert("Must select kind of key first.");
            return;
        }

        window.crypto.subtle.generateKey(
            {
                name: algorithmName,
                modulusLength: 2048,
                publicExponent: new Uint8Array([1, 0, 1]),  // 24 bit representation of 65537
                hash: {name: "SHA-256"}
            },
            false,  // Cannot extract new key
            usages
        ).
        then(function(keyPair) {
            return keyStore.saveKey(keyPair.publicKey, keyPair.privateKey, name);
        }).
        then(addToKeyList).
        catch(function(err) {
            alert("Could not create and save new key pair: " + err.message);
        });
    }

Note that the code relies on the key store’s saveKey method passing an object representing the saved key to its resolver.

Key Store Definition

The key store will be an object, created by calling the KeyStore constructor.

function KeyStore() {
    "use strict";
    var self = this;
    self.db = null;  // Filled in when the open method succeeds
    self.dbName = "KeyStore";      // Arbitrarily selected
    self.objectStoreName = "keys"; // Arbitrarily selected

    // Method definitions go here
}

The dbName and objectStoreName properties are the persistent names of the IndexedDB database and the IndexedDB object store that will hold the keys. A database contains one or more object stores and each object store is a collection of data objects; in this case, those objects include the CryptoKey objects we want to store. In theory the user could provide the database and object store names to the constructor, but at this stage there seems to be little benefit from doing that so they are hard-coded.

That object’s open method will create or connect to the IndexedDB database holding the keys. If the open operation succeeds the key store’s db property will be updated. There’s no need for any value to be passed on, but since it might be handy in the future the key store object itself is passed to the Promise’s resolver.

    self.open = function() {
        return new Promise(function(fulfill, reject) {
            // code to come, including "fulfill(self)" at some point
        });
    };

Opening an IndexedDB database a bit tricky: you have to create a new request to open the database and have callbacks to handle four cases: opens successfully, fails to open at all, is blocked from opening (because it’s already opened elsewhere), or is being created or upgraded in response to your request. The actual database open also needs to provide a version number. The structure of the database can only be changed when that version number changes. This code sets the version number to 1 so if there’s an existing database with that version it just opens it, otherwise it opens it and then executes the handler for an upgrade.

    var req = indexedDB.open(self.dbName, 1);
    req.onsuccess = function(evt) {
        // Work with the database in evt.target.result
    };
    req.onfailure = function(evt) {
        // Deal with the error in evt.error
    };
    req.onblocked = function(evt) {
        // Create an Error describing the problem.
    };
    req.onupgradeneeded = function(evt) {
        // "Upgrade" or initialize the database in evt.target.result
    };

The easiest handlers are for the two error conditions:

    req.onerror = function(evt) {
        reject(evt.error);
    };
    req.onblocked = function(evt) {
        reject(new Error("Database already open."));
    };

Success is easy to handle, too. The event includes the opened database, so the handler needs to save that in the object for future use and then call the Promise’s resolver:

    req.onsuccess = function(evt) {
        self.db = evt.target.result;
        fulfill(self);
    };

The longest handler is for onupgradeneeded. If this is the first time the database is opened the code needs to create an object store to actually hold the keys. Since it’s possible for this event to occur on an existing database that may or may not have the necessary object store already defined, the code checks for it first:

    req.onupgradeneeded = function(evt) {
        self.db = evt.target.result;
        if (!self.db.objectStoreNames.contains(self.objectStoreName)) {
            var objectStore = self.db.createObjectStore(self.objectStoreName, {autoIncrement: true});
            objectStore.createIndex("name", "name", {unique: false});
            objectStore.createIndex("spki", "spki", {unique: false});
        }
    };

The object store and two indexes are created if the store does not already exist. Every object store needs to have a unique primary key that IndexedDB can use to identify each record. Since we are so often dealing with cryptographic keys the primary key is called the id in this code. The call to createObjectStore tells IndexedDB to create that id itself by auto-incrementing an integer. Each call to createIndex takes three parameters: the name of the index to create, the name of the object property to use for the index, and an optional object that in this case specifies that the indexed properties do not have to be be unique. It is common to use the same name for the index and the indexed property, as shown here.

The simplest method is close:

    self.close = function() {
        return new Promise(function(fulfill, reject){
            self.db.close();
            self.db = null;
            fulfill();
        });
    };

The IndexedDB close() operation returns immediately, returning nothing. But this method returns a Promise for consistency with all the other methods.

The saveKey method creates and stores an object with four properties: publicKey, privateKey (which is allowed to be null), name, and spki. Three of those four properties are passed to it as arguments. The spki property value is created by exporting the public key:

    self.saveKey = function(publicKey, privateKey, name) {
        return new Promise(function(fulfill, reject) {
            if (!self.db) {  // No operation can be performed.
                reject(new Error("KeyStore is not open."));
            }

            window.crypto.subtle.exportKey('spki', publicKey).
            then(function(spki) {
                var savedObject = {
                    publicKey:  publicKey,
                    privateKey: privateKey,
                    name:       name,
                    spki:       spki
                };

                var transaction = self.db.transaction([self.objectStoreName], "readwrite");
                transaction.onerror = function(evt) {reject(evt.error);};
                transaction.onabort = function(evt) {reject(evt.error);};
                transaction.oncomplete = function(evt) {fulfill(savedObject);};

                var objectStore = transaction.objectStore(self.objectStoreName);
                var request = objectStore.add(savedObject);
            }).
            catch(function(err) {
                reject(err);
            });
        });
    }

After the public key has been exported this code first creates the savedObject, which will be put into the database. Operations that access information in the database are performed as parts of a transaction. Each transaction operates on one or more object stores so they must be created with an array of the names of affected object stores, and the creation operation has to specify whether the transaction should be allowed to write to the database (readwrite) or not (readonly). Event handlers on the transaction object deal with errors, aborts, or successful completion of the transaction. Note that each of these handlers calls the Promise’s fulfill or reject method. When that happens the transaction goes out of scope and is automatically closed. Actually saving the object is simple. The code gets the object store from the transaction and calls its add method.

The getKey function is logically simpler but a bit longer because the caller might want to look it up by id, name, or spki. In each case, the actual database fetch will be done with a get method. If the property to be searched is the id (which is the primary key for the database) then this is a method on the object store itself. If it’s a property that’s indexed (name or spki) then the get is a method on the index itself. The get method is asynchronous, and needs handlers for success and failure:

    self.getKey = function(propertyName, propertyValue) {
        return new Promise(function(fulfill, reject) {
            if (!self.db) { // No operation can be performed.
                reject(new Error("KeyStore is not open."));
            }

            var transaction = self.db.transaction([self.objectStoreName], "readonly");
            var objectStore = transaction.objectStore(self.objectStoreName);
            var request;
            if (propertyName === "id") {
                request = objectStore.get(propertyValue);
            } else if (propertyName === "name") {
                request = objectStore.index("name").get(propertyValue);
            } else if (propertyName === "spki") {
                request = objectStore.index("spki").get(propertyValue);
            } else {
                reject(new Error("No such property: " + propertyName));
            }

            request.onsuccess = function(evt) {
                fulfill(evt.target.result);
            };
            request.onerror = function(evt) {
                reject(evt.error);
            };
        });
    };

That leaves one more method: listKeys. This method introduces the use of a cursor. This is similar to a request, but after it provides a single value to the onsuccess handler the event result’s continue method can be invoked to start fetching the next one. A success with a null value for the result indicates that the entire list has been traversed. This would make it easy to create an iterator for the list of keys, but this method will keep it simple by building an array of keys and returning the whole thing when ready:

    self.listKeys = function() {
        return new Promise(function(fulfill, reject) {
            if (!self.db) {
                reject(new Error("KeyStore is not open."));
            }

            var list = [];

            var transaction = self.db.transaction([self.objectStoreName], "readonly");
            transaction.onerror = function(evt) {reject(evt.error);};
            transaction.onabort = function(evt) {reject(evt.error);};

            var objectStore = transaction.objectStore(self.objectStoreName);
            var cursor = objectStore.openCursor();

            cursor.onsuccess = function(evt) {
                if (evt.target.result) {
                    list.push({id: evt.target.result.key, value: evt.target.result.value});
                    evt.target.result.continue();
                } else {
                    fulfill(list);
                }
            }
        });
    };

This completes the key storage functionality, at least for now. These basic operations can provide a lot of utility.

Issues

This example provides a way for a user to get a copy of a public key and take it elsewhere, but no way to import such a public key when that occurs. If that capability existed it would be possible to use extended versions of the sample applications shown so far to encrypt messages to other users (with their public keys) and verify digitally signed files (again, with a public key). So the ability to import a public key would be useful.

A bigger problem is that the browser is allowed to delete this key storage database whenever it wants to, such as needing more space. That’s not likely to happen but it would be prudent to have a way to back up entire key pairs when they are first created and restore them later. That would also allow users to keep their keys in more than one browser. Backing up the private key would require it to be marked as extractable, which we do not want to do. The solution is a bit of a hack: create the key pair as extractable, export the private key and have the user download it, create a new private key by importing that data and marking the imported private key as not extractable. Then, after all that, store the non-extractable private key in the key store.

Finally, there are other programs out there that use public key pairs and it would be nice to interoperate with them to the extent possible. The next post will make a small start toward that by importing a public key from an X.509 certificate.

Blog at WordPress.com.