End-to-end encryption between Node.js and webcrypto

Written by Jeroen van Veen on 30th April 2016

Creating a Javascript p2p web framework has been a personal project, almost an obsession, of mine for almost two years now. Two months ago I hit a brick wall when I was trying to implement end-to-end encryption using the webcrypto API and Node.js crypto. Here is a short explanation of what I am trying to accomplish, and what progress I made so far.

Background

The open source project I work on is called High5. Its purpose is to allow creation of an overlay network of universal/isomorphic Javascript peers (nodes), that are interconnected by using suitable transports. This could be a websocket transport, a memory transport (to provide an HTML snapshot state to other nodes) or a webRTC transport (browser to browser, not implemented yet). Each node reasons about the network and routing using a graph. The figure above represents an imaginary overlay network, where the yellow nodes are running inside Node.js and the blue nodes are running in Chromium/Chrome. A simple preliminary protocol allows nodes to route JSON messages to each other.

If Node C wants to send a chat message to node F, then it has to route all the way through node A and B (or through A-D-G-B-F) in order to arrive at node F. The node figures out the route by using Dijkstra’s shortest path algorithm. These routing messages can be used to exchange peer-to-peer web application data with, and are also useful to setup direct webRTC transports between browser nodes with. It then uses the overlay network as a decentralized signalling channel for webRTC SDP messages. Take for example the webRTC transport between node D and G. By using route D-A-B-G as a signalling channel, it’s quite straightforward to setup a connection between these browsers, which would otherwise not be aware of each other.

Asymmetric encryption

Now this is all fine and dandy, but node C doesn’t want node A and B to read a message that’s meant for node F! Then how do we get the message from C to F without A and B being able to read it? An option would be to use an encryption method like AES to scramble the message with a common secret key; the session key. AES is a fast and effective way to encrypt and decrypt larger messages with. But how do we get the session key to node F without node A and B learning about it? If A or B knows the session key, then encrypting the text wouldn’t make much sense anymore, because anybody with the session key can decrypt the message! As it turns out, this problem was already solved in the early 1970’s with the invention of asymmetric cryptography methods like RSA and Diffie Hellman key exchange.

Asymmetric cryptography uses two keys; a private key and a public key. A message from Alice to Bob can be encrypted by Alice using Bob’s public key. Bob receives the encrypted message from Alice and is the only one being able to decrypt it using his private key. Asymmetric cryptography is rather slow and has limitations on the encrypted message size, but is very useful to exchange a common secret, the AES session key, between Alice and Bob without having to fear that an MITM can read it as well.

RSA encryption

After learning a bit more about cryptography methods, I initially thought I needed to create this RSA key exchange myself. On the Node.js side I initially used node-rsa to generate a key pair in Node.js. Using some webcrypto examples, I managed to encrypt some text using RSA-OAEP-256 (OAEP padding with SHA-256 hashing) and the Node.js generated public key. At that point, I thought I would be able to decrypt it in Node.js using the private key. Wrong! I couldn’t get it to work, because the node-rsa decrypt method kept on complaining about invalid OAEP padding. I tried other libraries like ursa to see if that would fix the problem, but it didn’t. After countless hours of messing around, not really knowing what I was doing, I finally found the reason. The used OAEP padding mechanism in node-rsa and in ursa’s OpenSSL bindings is hardcoded to use SHA1 hashing! Knowing this, I finally managed to decrypt the message using node-forge, which allowed me to use SHA-256 hashing for OAEP.

Diffie Hellman key exchange

Since then, a lot of people who know a thing or two about cryptography advised me not to ‘roll-my-own’ key exchange mechanism. So I continued reading and learned a bit about Diffie Hellman key exchange. DH is probably the right solution to safely exchange a common session key. With DH, Alice and Bob still exchange two public keys, just like with RSA. The difference is that with DH, Alice can derive the session key directly from her private key and Bob’s public key. Bob can do the same with his private key and Alice’s public key. They both end up with the same secret, without an MITM to be able to learn about it. This is better than ‘rolling-my-own’, since it doesn’t require to send the session key (encrypted) over the wire at all. Alice and Bob can from then on use the negotiated AES session key to encrypt follow-up messages. There is also the topic of message authentication, but I’ll leave that outside the scope of this article. Webcrypto allows using DH by using ECDH. Now I’m back at where I began! Generating an ECDH key pair with webcrypto is quite straightforward, but how to do the same thing in Node.js, and properly generate the session key with the right parameters?

Node.js Subtle

Node.js built-in crypto library doesn’t resemble the webcrypto API at all. There are some third-party libs, but none of them explain how to interop with webcrypto. Happily I found the Subtle project, which aims to be a ‘WebCrypto Polyfill for node/browsers’. Awesome! This would make life on the Node.js side a lot easier, because then we can stick to the webcrypto API and perform the same operations in the browser and on Node.js. Bummer! It failed to compile on Node.js 5.x. Subtle depends on a compiled elliptic curve extension called ecc, which in turn depends on an older version of nan. After searching around for some hours about nan, I managed to get ecc working with nan 2.1.0. With Subtle up and running, I tested it to do the same RSA key exchange as described at the beginning of this post. Importing and exporting the keys was a breeze using spki/pkcs8, but now I got the same OAEP padding error again! (Error: Invalid RSAES-OAEP padding.]). This may have to do with the SHA1 hashing issue again. Finally, I tried ECDH key exchange between webcrypto and Node.js Subtle, but sadly this failed as well using my example code. Back to the drawing board…

The way to go?

I still have to figure out how to properly apply RSA encryption and ECDH key exchange using the Node.js Subtle library, as well do some more research on message authentication (digital signatures) and ephemeral ECDH. Overall, I feel that this Subtle library may be on the right track to make end-to-end encryption accessible for Node.js web developers without having to know all the ins and outs of cryptography. Another interesting direction to this may be the TweetNaCl-js project, but I didn’t try this yet. This is it for now! I need to read a lot more about the subject first, and then I’ll be able to write the next post, in which I will describe how to implement a complete end-to-end message encryption method.

Background information

If you enjoy this subject and want to know more, I found these resources very helpful when trying to learn a bit more about cryptography:

Do you know other resources which could be useful or have any other tips to help me on my quest, let me know in the comments below!