Charles Engelke's Blog

February 14, 2015

Deriving Keys from Passwords with WebCrypto

Filed under: Uncategorized — Charles Engelke @ 1:02 pm
Tags: ,

It has been quite a while since my last post. I got painted into a corner trying to import key pairs that were from the Windows 7 certificate store using the Web Cryptography API. The problem is that the exported keys are encrypted with an algorithm that converts a user-supplied (and required) password to a key using triple DES. But WebCrypto doesn’t support triple DES. So that ended that, and then the holidays and the new year took my attention away. But I’m ready to move forward again. And this post will cover something similar to what blocked my Windows 7 key importation attempts: how to derive keys from passwords,but for use with WebCrypto supported algorithms.

By the way, the Web Cryptography API has become a Candidate Recommendation since my last post. This and future posts will use the API as specified in that version. The changes from the earlier draft are mostly fixed typos and improved consistency.

Password-based Key Derivation

The goal today is to derive 128 bit AES keys from a user-entered password. That’s a common need for simple, person-to-person secrecy. It’s not realistic for people to remember 128 bit (or longer) keys, so we need to derive those keys from normal passwords. You could, of course, just take the password’s binary encoding and add padding or truncate it to get 16 bytes (128 bits), but that would produce a very small, easily explored set of possible keys for people to try to guess. The right way is to use a key derivation function (KDF) that combines a salt unique to your application and performs multiple iterations of obscuring the result.

The Web Cryptography API supports two KDFs: HKDF-CTR and PBKDF2. They are similar to use, and since I’m more familiar with PBKDF2 that’s the one we’ll use here. Also, PBKDF2 has been around a lot longer, so it is easier to find implementations of it in a variety of programming languages that can be used to check the results.

The Sample Application

Note: The API does not mandate implementations to support any specific algorithms. Even though PBKDF2 is included in the specification, not all browsers support it. At least, not yet. This example does not work with the current version of Google Chrome, Opera, or Firefox, but does work with Chrome Canary (version 42) and Opera Developer (29). It does not work even with Firefox Nightly. And it does not with with Internet Explorer 11.

We will run this operation from the simplest possible web page:

PBKDF2WebPage

The user will enter a password (which will display instead of being masked, to make the concepts clearer) and the derived key (in hex form) will be displayed in place of the word “Missing”. The HTML is very simple:

<!DOCTYPE html>
<!-- Copyright 2015 Info Tech, Inc. Provided under the MIT license. See LICENSE file for details. -->
<html>
<head>
    <title>Password Based Key Derivation</title>
    <script src="crypto.js"></script>
</head>

<body>
    <h1>Password Based Key Derivation</h1>

    <section id="key-management">
        <p>Password: <input type="text" size="32" id="password"/>
           <button id="derive-key">Derive Key</button>
        </p>
        <p>
            Derived Key: <span id="aes-key">Missing</span>
        </p>
    </section>
</body>
</html>

The skeleton of the JavaScript is familiar:

// Password Based Key Derivation with Web Cryptography API
//
// Copyright (c) 2015 Info Tech, Inc.
// Provided under the MIT license.
// See LICENSE file for details.

document.addEventListener("DOMContentLoaded", function() {
    "use strict";

    // Check that web crypto is even available
    if (!window.crypto || !window.crypto.subtle) {
        alert("Your browser does not support the Web Cryptography API! This page will not work.");
        return;
    }

    document.getElementById("derive-key").addEventListener("click", deriveAKey);

    // Rest of code goes here.

});

This makes sure that the browser supports the Web Cryptography API and attaches a click listener to the button. Well, it would attache a listener if the deriveAKey function existed. That function will get the password from the form, derive a key from it, and put the hex version of that key into the page. The key derivation will use the PBKDF2 algorithm. That algorithm takes a known (not secret) salt, an SHA hash function, and a count. It will hash the password with the salt, then do the same to the result of that, and repeat as many times as specified. This operation is fast enough for our purposes, but much slower than a simple hash, making it resistant to dictionary attacks.

Deriving a Key

The deriveAKey function starts out simple:

    function deriveAKey() {
        var saltString = "Pick anything you want. This isn't secret.";
        var iterations = 1000;
        var hash = "SHA-256";

        var passwordString = document.getElementById("password").value;
    }

Now that we have all the information needed by the PBKDF2 operation we can perform the operation. I expected this would just use the deriveKey method, but there seems to be a problem: even though the specification’s algorithm/operation table says you can use that method with the PBKDF2 algorithm, the PBKDF2 section of the specification does not include it. The only supported operations are generateKey, deriveBits, importKey, and Get Key Length. No deriveKey. So I figured we’d have to derive 128 bits and then import that into an AES-CBC key.

But in fact, we can use the deriveKey method. That’s because methods and operations are different things. For example, there is a deriveBits method and a deriveBits operation, and they are slightly different things. Operations are abstract procedures, while methods are actual API functions. And, while there is no deriveKey operation, there is a deriveKey method, and it relies on the deriveBits operation. Clear? No, it’s not all that clear to me, either. And from the working group’s mailing list, I see that it’s not that clear to others. But that’s the way the specification is written.

Here’s the specification of the deriveKey method:

Promise deriveKey(AlgorithmIdentifier algorithm,
                  CryptoKey baseKey,
                  AlgorithmIdentifier derivedKeyType,
                  boolean extractable,
                  sequence keyUsages );

To call this method we need the algorithm to use to derive the key (just {"name": "PBKDF2"}), a baseKey, the type of the derived key we want ({"name": "AES-CBC, "length": 128}), whether the derived key is extractable (true, so we can display the hex value on the page), and keyUsages for the derived key (["encrypt", "decrypt"]). All that’s clear, except for the baseKey parameter. And where do we enter the password? This is called a password-based key derivation function, after all.

This stumped me for quite a while. I figured the baseKey must mean the password since that’s what the basis for the derivation is. But the password is just a string, while baseKey has to be a CryptoKey. It turns out the baseKey does represent the password, but you have to convert it into a CryptoKey object in order to use it, via the importKey method. Actually, it doesn’t seem to do any conversion, just essentially coerce the value to the correct type. That method is defined by:

Promise importKey(KeyFormat format,
                  (BufferSource or JsonWebKey) keyData,
                  AlgorithmIdentifier algorithm,
                  boolean extractable,
                  sequence keyUsages );

The keyData is the password. However, it has to a BufferSource object, which means an ArrayBuffer or an ArrayBufferView (such as Uint8Array). For now, assume there is a function stringToArrayBuffer that will do that conversion. Now we can write the main code: use importKey to convert the password to a base key, use deriveKey to create a key from that base key, then use exportKey to see the key contents, which will be inserted in the aes-key span on the page. Here’s that code:

        // First, create a PBKDF2 "key" containing the password
        window.crypto.subtle.importKey(
            "raw",
            stringToArrayBuffer(password),
            {"name": "PBKDF2"},
            false,
            ["deriveKey"]).
        // Derive a key from the password
        then(function(baseKey){
            return window.crypto.subtle.deriveKey(
                {
                    "name": "PBKDF2",
                    "salt": stringToArrayBuffer(salt),
                    "iterations": iterations,
                    "hash": hash
                },
                baseKey,
                {"name": "AES-CBC", "length": 128}, // Key we want
                true,                               // Extrable
                ["encrypt", "decrypt"]              // For new key
                );
        }).
        // Export it so we can display it
        then(function(aesKey) {
            return window.crypto.subtle.exportKey("raw", aesKey);
        }).
        // Display it in hex format
        then(function(keyBytes) {
            var hexKey = arrayBufferToHexString(keyBytes);
            document.getElementById("aes-key").innerText = hexKey;
        }).
        catch(function(err) {
            alert("Key derivation failed: " + err.message);
            });

That’s it, except for the two utility conversion functions that were used.

Utility Functions

The arrayBufferToHexString function is identical to the one shown in the Symmetric Cryptography in the Browser – Conclusion post:

    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);
            if (nextHexByte.length < 2) {
                nextHexByte = "0" + nextHexByte;
            }
            hexString += nextHexByte;
        }
        return hexString;
    }

The stringToArrayBuffer function is quite a bit trickier. At first glance, it seems easy: just iterate over the characters in the string, convert each one to an integer using the charCodeAt method, and put each integer into a byte in a Uint8Array. But what happens if the string is something like “Я ♥ secrets”? The first character has code 1071 and the third has 9829. Those won’t fit into a byte. We could use a Uint16Array, though. Until someone uses a 𐎘 in their password (that is, of course, UGARITIC LETTER THANNA, with code 0x10398 = 66456, which won’t fit into 16 bits; it might not display properly in your browser unless you have very complete Unicode fonts).

Clearly, we want to put the string’s UTF-8 encoding into a Uint8Array. For example, 𐎘 will take four bytes: hex f0 90 8e 98. But how do we do that? UTF-8 encoding is extremely elegant, but not trivial to derive. Luckily, there is an encoding API that can handle that. Unfortunately, it’s quite new and not supported in all browsers yet. But then, neither is the Web Cryptography API. My cursory check seems to show that recent browsers either support both APIs or neither, so we will change the check at the start of the code to make sure both APIs are available and use the new API.

The check goes right after the web crypto check:

    if (!window.TextEncoder || !window.TextDecoder) {
        alert("Your browser does not support the Encoding API! This page will not work.");
        return;
    }

Now it’s easy to write the proper stringToArrayBuffer function:

    function stringToArrayBuffer(string) {
        var encoder = new TextEncoder("utf-8");
        return encoder.encode(string);
    }

And the application is complete.

Source and Live Demo

The source code is available on GitHub, and you can run a live version from there, too. Let’s give it a try by deriving a password from the string “My secret!”

Screen grab of application

The derived key is ed4b134e89eff7e9366af4abd5c6fb38. I gave this a sanity check by using the Python PyCrypto library to also derive a key with the same salt and password, and got the same answer. That’s good enough for me!

Advertisements

6 Comments

  1. Do you have experience with the wrap and unwrap function? I am getting an error while unwrapping with code 0, so I am not getting any type of information about the caught exception, woulb be nice if you have some advice on that.

    Comment by David — October 7, 2015 @ 1:13 pm

    • Sorry, I had trouble with them early on and never went back to see how well they worked.

      Comment by Charles Engelke — October 7, 2015 @ 4:30 pm

  2. Using SHA-1 in deriveAKey() with iteration > 100 is not working, have you an idea why?

    Comment by David — October 9, 2015 @ 3:25 am

  3. Ok, you can forget my last comment, it was a bug.

    Comment by David — October 12, 2015 @ 8:06 am

    • Glad to hear it worked out. Sorry I didn’t reply earlier, but I was out of town and not connected.

      Comment by Charles Engelke — October 12, 2015 @ 8:15 am

  4. Hello Sir, do you know a good pbkdf2 polyfill? Since Microsoft Edge/IE don’t support it. I found a JS implementation (http://anandam.name/pbkdf2/) but it takes like 1 Minute to derive the key!

    Comment by David — December 23, 2015 @ 3:38 am


RSS feed for comments on this post.

Blog at WordPress.com.

%d bloggers like this: