Security Without TLS

Cody Eilar
Feb 23 · 7 min read

I know what you’re thinking, that I’m another one of those people out there that think they can role their own protocols better than the contributors of SSL/TLS. I’m here to tell you that I am not! I’ve recently stumbled upon a really nice framework that allows to you safely define your own protocol that is still supported by the best practices used in Diffie-Hellman cryptography. The name of the framework is called “The Noise Protocol”.

The Rationale

So why would you want to use some other framework when you already have TLS/SSL? Well it really comes down to a question of what type of application you are building that needs strong encryption over a network. If you’re starting from scratch, and you have custom requirements, starting with something like TLS can feel a bit heavy weight. There is a lot of documentation and so many options, it can take a really long time to figure out what parts of the protocol you need to accomplish your goals. Whereas with the Noise Protocol Framework, you can define exactly what you need from the ground up without removing a bunch of features you don’t need. This also means that you can keep your messages very small and succinct without a bunch of headers and other data that you aren’t taking advantage of. And in other cases, existing protocols such as TLS, SSH, and IPsec may not be capable of accomplishing the protocol goals you’ve defined for your application. With Noise, you can accomplish many different protocol goals with a single framework by starting from the ground up. Additionally, Noise attempts to keep things simple. Simplicity, some would argue, is even more secure than protocols that carry around a bunch of extra data that is ultimately not being used by the protocol itself. By having all these other flags and options in a protocol, an attacker has a much larger surface area to attack the protocol.

The Noise Framework attempts to balance the need for some application designers to pick and choose the exact features they need by providing small layers that can be combined to create a complex and rich feature set for a protocol design. You can think of Noise as a bunch of legos that can be combined in interesting and useful ways, but that are easy enough to put together that you don’t have to be an expert in cryptography to navigate the API. Noise was designed such that you can create a huge number of protocols with relative ease and can be tailored for your specific use case.

Let’s see an example!

If you’re anything like me, sometimes words are really boring and code is less so, so let’s take a look at a use case that I dreamed up!

I love doing electronic and software projects around the house. Before I had discovered peer-to-peer libraries, I would use services like AWS IoT and some other centralized infrastructure so I could chat with my devices even when I wasn’t connected to my local area network. However, more recently I was introduced to a really simple and easy to use P2P networking stack called hyperswarm. It’s really easy to use and to setup, so it was a good starting point for me to use to communicate with random devices that I setup around my house. There is one problem though, there is no secure communication built around this library. That’s ok because we have the noise protocol to the rescue! I wanted to setup a situation where I knew what the public key was for the device, but the device didn’t know about my remote device. That way, as long as I knew the public key to my home device, I could establish a connection. Furthermore, I could secure the device even more with a preshared key (psk).

So the first step was reading through the specs of the noise protocol framework and figuring out how to setup the pattern. So in noise protocol speak, my device is going to called the “responder” and the client device (i.e. my phone) is going to be the “initiator”. So I want the case where my device (i.e. my responder) doesn’t know anything about my client (it doesn’t know its public key) and my phone or laptop (or my initiator) knows the public key for the device.

So let’s take a look how to set things up. First and foremost we need to instantiate the web assembly object:

var createNoise = require('noise-c.wasm');createNoise(function (noise) {
// Our code will go here
}

Next we realize what pattern we want to use, which is going to be XK. The first position is for the initiator, and in this case it is X. X means that the initiator is going to transmit its key to the responder. The second position, which is K for the responder, means that the static key is known to the initiator. This is a great start for security, but ideally for this case you would want mutual authentication, i.e. both the responder and the initiator know about each other’s static keys. So the first handshake can be just to send a validation e-mail to the device system admin for approval, once that has been done, the responder could record the initiator’s keys in a database on disk so that mutual authentication can be done to perform more sensitive transmissions such as opening a garage door or watering a plant. The pattern for that type of communication is “KK”. So this is the next bit of code we need to define our protocol:

  let pattern = 'XK'
let curve = '25519'
let curve_id = noise.constants.NOISE_DH_CURVE25519
let cipher = 'ChaChaPoly'
let hash = 'BLAKE2b'
let protocol_name = `Noise_${pattern}_${curve}_${cipher}_${hash}`
console.log(`Using protocol: ${protocol_name} `)

I’m not going to go into the details regarding each of these algorithms, so feel free to look them up!

For our test case, I’m not going to go through the trouble of setting up a client and server, I’m simply going to create everything in one file to keep the code as succinct as possible. But in the code, you can imagine what parts you would put in the device, and what parts you would in the client.

So now that we have established our patterns and our encryption, hashing, and cipher algorithms, we can create the keys for both the responder and the initiator:

// Setup the keys
let [initiator_private, initiator_public] = noise.CreateKeyPair(curve_id)
let [responder_private, responder_public] = noise.CreateKeyPair(curve_id)
console.log("Initiator public key = ", Buffer.from(initiator_public).toString('hex') )
console.log("Responder public key = ", Buffer.from(responder_public).toString('hex') )

Since this is just an example, we would probably want to store these keys for each the responder and the client and load them in next time so we don’t have to establish a new key pair each time.

Next we can setup the handshake. Keep in mind that the way we named our protocol matters. You’ll notice that if you try to change the patter to something like “Jackie Chan”, you will get an error!

// Setup the handshake
let initiator_hs = noise.HandshakeState(protocol_name, noise.constants.NOISE_ROLE_INITIATOR)
let responder_hs = noise.HandshakeState(protocol_name, noise.constants.NOISE_ROLE_RESPONDER)

Now we can get into setting up the actual handshake on both sides. You’ll notice that in the following code I don’t include a prologue or a pre shared key. Adding these things will definitely make your communication even more secure.

// Initialize
// Preshare data so man-in-the middle is less likely. Both ends must have this
// data identical. Not required though
let prologue = null
// There is an ability to use a pre-shared key mechanism as well. But in this
// use case are not going to use it.
let psk = null
initiator_hs.Initialize(prologue, initiator_private, responder_public, psk)
// Note: In the scenario I've defined above, we assuming that we don't know
// the public key of the initiator, hence why the responder doesn't have it!
responder_hs.Initialize(prologue, responder_private, null /*public key of initiator for mutual auth*/, psk)

Now that we have initialized both parties, we are ready to begin the handshake. Like I mentioned above, this would normally be done over some sort of a network socket. I wrote a simple loop that demonstrates how the handshake is done. Basically the responder and the initiator send messages back and forth to each other until the “Split” state is reached or if there is handshake error, which could mean that someone is attempting to connect that doesn’t have the proper information! The code below demonstrates this:

// perform the handshake. We know we are ready to start communicaiton once
// both the intiator and the responder are in the split state!
let ready = false
let msgToResponder
let msgToInitiator
while(initiator_hs.GetAction != noise.constants.NOISE_ACTION_SPLIT && responder_hs.GetAction() != noise.constants.NOISE_ACTION_SPLIT) {
switch(initiator_hs.GetAction())
{
case noise.constants.NOISE_ACTION_WRITE_MESSAGE:
console.log('Initiator writing message to responder..')
msgToResponder = initiator_hs.WriteMessage()
break;
case noise.constants.NOISE_ACTION_READ_MESSAGE:
console.log('Initiator reading message from responder...')
if (msgToInitiator) {
initiator_hs.ReadMessage(msgToInitiator, true)
msgToInitiator = null
}
break;
}
switch(responder_hs.GetAction())
{
case noise.constants.NOISE_ACTION_WRITE_MESSAGE:
console.log('Responder writing message to initiator..')
msgToInitiator = responder_hs.WriteMessage()
break;
case noise.constants.NOISE_ACTION_READ_MESSAGE:
console.log('Responder reading message from initiator...')
if (msgToResponder) {
responder_hs.ReadMessage(msgToResponder, true)
msgToResponder = null
}
break;
}
}

Once the handshake is complete, we can now begin sharing information with forward secrecy! Pretty sweet right? This is an example of how to pass messages back and forth:

// At this point, we are ready to split and start sending messages.
// The key here is to understand that we need to get everything into the split
// state.
// Ready to split and start sending messages
let [initiator_send, initiator_receive] = initiator_hs.Split()
let [responder_send, responder_receive] = responder_hs.Split()
// Initiator is going to send message
let ad = new Uint8Array()
let messageToResponder = Uint8Array.from(Buffer.from("Hiya Responder!"))
let cipherToResponder = initiator_send.EncryptWithAd(ad, messageToResponder)
let messageFromInitiator = responder_receive.DecryptWithAd(ad, cipherToResponder)
console.log('Decrypted message received by the responder: ', Buffer.from(messageFromInitiator).toString() )
let messageToInitiator = Uint8Array.from(Buffer.from("Well hello, initiator!"))
let cipherToInitiator = responder_send.EncryptWithAd(ad, messageToInitiator)
let messageFromResponder = initiator_receive.DecryptWithAd(ad, cipherToInitiator)
console.log('Decrypted message received by the initiator: ', Buffer.from(messageFromResponder).toString())

Conclusion

Well with a little bit of work, and a lot of fun we can create our own secure protocols to meet the needs for our own projects! We don’t have to rely on protocols that have thousands of options we don’t need and we can easily build support for protocols that haven’t been implemented in other libraries. I’m definitely looking forward to using this protocol more for future projects.

Also, I realize that the code syntax highlighting is lacking in Medium, so you can find the entire code listing on my github page: https://github.com/AcidLeroy/noise-example.

Thanks for reading and happy hacking!!

Cody Eilar

Written by

Computer Engineer for the heck of it

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade