I learned enough Web Crypto to be dangerous

subterrane

Will Munslow

Posted on February 6, 2018

I learned enough Web Crypto to be dangerous

Disclaimer: security is hard. Keep your private keys private. I'm not a security expert, there's reasons not to do this. I'm not sure what they all are so proceed at your own peril.

I was asked to encrypt some data in a browser before sending it to a server. Sounds simple enough: I figured I'd get someone's public key, encrypt some data with it and send it on its way. They'd decrypt it with their private key, easy-peasy. Nope.

I learned quickly that asymmetric key pairs are (usually) used to encrypt symmetric keys and a symmetric key is used to encrypt the data. This is due to speed and the amount of data that can be encrypted is dependent on key length and zzzzzz...sorry, I fell asleep.

So, you make up your own key. Which explains why Web Crypto gives you this handy function: generateKey()

Here is an example function to encrypt some data:

    // encrypt form input
    let cypher = await encrypt(input.value);
    console.dir('cyphertext: ' + cypher.data);

    async function encrypt(data) {
        const key = await window.crypto.subtle.generateKey({ name: 'AES-GCM', length: 256 }, true, ['encrypt', 'decrypt']);
        const iv = window.crypto.getRandomValues(new Uint8Array(12));
        const cypher = ab2str(await window.crypto.subtle.encrypt({ name: 'AES-GCM', iv: iv }, key, str2ab(data)));

        return {
            data: cypher,
            iv: iv,
            key: key
        };
    }
Enter fullscreen mode Exit fullscreen mode

And to decrypt:

    async function decrypt(data, key, iv) {
        return ab2str(await window.crypto.subtle.decrypt({ name: 'AES-GCM', iv: iv }, key, str2ab(data)))
    }
Enter fullscreen mode Exit fullscreen mode

The second thing you learn is that the Web Crypto functions work on a BufferSource, not a string. There are APIs available to encode and decode strings to buffers (TextEncoder) but I had some difficulty using them so I used a couple of functions by Renato Mangini.

    function ab2str(buf) {
        return String.fromCharCode.apply(null, new Uint16Array(buf));
    }

    function str2ab(str) {
        let buf = new ArrayBuffer(str.length * 2);
        let bufView = new Uint16Array(buf);
        for (let i = 0, strLen = str.length; i < strLen; i++) {
            bufView[i] = str.charCodeAt(i);
        }
        return buf;
    }
Enter fullscreen mode Exit fullscreen mode

Now we have some encrypted data, a key and an initialization vector (Pro Tip: saying 'initialization vector' makes you sound smart in meetings). We need to encrypt the key we made with someone's public key. That way, we can send the encrypted data, the initialization vector and the encrypted symmetric key to them and only they can decrypt they symmetric key to decrypt the data.

It's similar to how your realtor is able to get into that house you want to see. The house has a key and the key is placed in a lockbox on the front door. Your realtor knows the code to the lockbox, so s/he opens it up, gets the key, unlocks the house and shows you around. You decide you really would prefer an open-concept kitchen and a master ensuite so you leave and the realtor puts the key in the lockbox. The lockbox is a terrible analogy for a public/private key pair but you get the idea that the key to open the house gets secured in some manner.

For fun, we can make our own key pair with a command line tool. For extra fun, we can convert it to JSON Web Key format to make it easy to deal with. The Web Crypto API had methods to allow you to create and export key pairs in JWK format. I used the generateKey method above to make a symmetric key. But I needed to be able to use a public key that someone else created so I went through these steps to see if I could make it work.

I used this package by dannycoates. First, make a key:

openssl genrsa 2048 | pem-jwk > private_key.jwk
Enter fullscreen mode Exit fullscreen mode

Then convert it to .pem:

pem-jwk private_key.jwk > private_key.pem
Enter fullscreen mode Exit fullscreen mode

Derive the public key from the private key:

openssl rsa -pubout -in private_key.pem -out public_key.pem
Enter fullscreen mode Exit fullscreen mode

Then convert the public key to jwk format:

pem-jwk public_key.pem > public_key.jwk
Enter fullscreen mode Exit fullscreen mode

You end up with 4 files:

  • private_key.jwk
  • private_key.pem
  • public_key.jwk
  • public_key.pem

I wrote a couple more functions

    async function importPublicKey() {
        const key = /* contents of public_key.jwk */ ;
        const algo = {
            name: 'RSA-OAEP',
            hash: { name: 'SHA-256' }
        };
        return await window.crypto.subtle.importKey('jwk', key, algo, false, ['wrapKey']);
    }

    async function importPrivateKey() {
        const key = /* contents of private_key.jwk */;
        const algo = {
            name: 'RSA-OAEP',
            hash: { name: 'SHA-256' }
        };
        return await window.crypto.subtle.importKey('jwk', key, algo, false, ['unwrapKey']);
    }
Enter fullscreen mode Exit fullscreen mode

Disclaimer: Again, keep your private key private. This is just for kicks, man, don't do this in real life.

Web Crypto gives you the tools to encrypt and decrypt a key: wrapKey and unwrapKey and with a key, you can decrypt your BufferSource:

        // import public key
        const publicKey = await importPublicKey();

        // wrap symmetric key
        const wrappedKey =  ab2str(await window.crypto.subtle.wrapKey('raw', cypher.key, publicKey, { name: 'RSA-OAEP' }));
        console.log('wrappedKey: ' + wrappedKey);

        // import private key
        const privateKey = await importPrivateKey();

        // unwrap symmetric key
        const unwrappedKey =  await window.crypto.subtle.unwrapKey('raw', str2ab(wrappedKey), privateKey, { name: 'RSA-OAEP' }, { name: 'AES-GCM', length: 256 }, false, ['decrypt']);
        console.log('unwrappedKey: ' + unwrappedKey);

        // decrypt encrypted data
        let plaintext = await decrypt(cypher.data, unwrappedKey, cypher.iv);
        console.log('plaintext: ' + plaintext);
Enter fullscreen mode Exit fullscreen mode
💖 💪 🙅 🚩
subterrane
Will Munslow

Posted on February 6, 2018

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related