I learned enough Web Crypto to be dangerous
Will Munslow
Posted on February 6, 2018
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
};
}
And to decrypt:
async function decrypt(data, key, iv) {
return ab2str(await window.crypto.subtle.decrypt({ name: 'AES-GCM', iv: iv }, key, str2ab(data)))
}
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;
}
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
Then convert it to .pem:
pem-jwk private_key.jwk > private_key.pem
Derive the public key from the private key:
openssl rsa -pubout -in private_key.pem -out public_key.pem
Then convert the public key to jwk format:
pem-jwk public_key.pem > public_key.jwk
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']);
}
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);
Posted on February 6, 2018
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.