More people than ever are relying on systems like Signal and Urbit to provide seamless, reliable end-to-end-encryption (e2ee). But e2ee relies on secret keys, usually stored on your device. What if an attacker pilfers your secret keys? Forward secrecy protects you in such a scenario. In this article, you’ll learn how PFS works in-and-out by implementing a fullstack notifications app with perfect forward secrecy in Python.
Let’s dive straight into some code!
Encrypted notifications in Python
Our app will implement a system for sending notifications to a remote device. Before we worry about any fancy encryption, let’s write the code for our notifications. It’s pretty simple.
First, let’s write a Flask app that will let us listen for new alerts.
Okay, now let’s try sending a notification to this endpoint using curl.
Great! So we have a mechanism for remotely sending alerts to this machine. Note that this code is specific to MacOS – on Windows, Linux, or Urbit, you’d need to change the osascript command to something for your system.
Encrypting our app
This app has no security. Anyone on the network in between the sender of the notification, and the recipient, can read these messages. To show you what I mean, let’s route these requests through Burp Suite, a proxy app for pentesters.
Our notification appears in plain text! To prevent interlopers from snooping on our notifications, let’s implement end-to-end encryption.
We really only need three functions:
- Generate public and private keys
- Encrypt (with public key)
- Decrypt (with private key)
And, of course, we’ll need to add an endpoint to our Flask app that allows notifiers to access our public key.
First, we generate keys using RSA.
Our public key allows others to encrypt information that only we can decrypt, via our private key.
Now let’s write a function to use our public key to encrypt data.
And finally, our decrypt function.
Now, we can modify our Flask app to serve our key and allow others to encrypt messages for us.
Using this manually via curl would be tedious, so let’s write a simple command line client. The client will allow us to specify a recipient to send alerts to via a minimal interface.
In the screenshot above, we have the code for a simple client, as well as an example of running the app, and finally, the alert itself in the top right.
At this point, we’ve implemented e2ee. Now it’s time for the part you’re here for: forward secrecy!
Adding forward secrecy with Python
Why is the app above inadequate? As you can see in the Burp example below, a hacker doing a man-in-the-middle attack can no longer read out messages.
But what if the attacker had been making a log of every communication, waiting for the day when they got your key. Eventually, they manage to steal your key, and now can access your entire notification history! Wouldn’t it be preferable if every message were encrypted with a new key, to prevent this? Of course, you could just create and serve a new public key everytime you get a new request. Technically, this would provide perfect forward secrecy.
However, creating RSA keys is computationally intensive. Traditional symmetric keys are much cheaper, but then you’d have to communicate the secret key, which the attacker could also intercept. If only there were some mechanism to transfer a symmetric key, without passing it over the network…
Diffie Hellman is the algorithm that powers privacy on the internet. It solves our problem, by allowing two parties to calculate a shared secret, without communicating it over the wire. Diffie Hellman is extremely helpful to implement forward secrecy with Python.
For a simple explanation of the math, check out this excellent video: https://www.youtube.com/watch?v=M-0qt6tdHzk.
We’ll implement a simple Diffie Hellman algorithm based on that video.
For starters, let’s remove the /key endpoint and replace it with a new endpoint that performs a Diffie Hellman handshake:
We’ll also need to update the CLI so it speaks this new protocol.
If we try running this, it will print out a shared secret which will be the same as the one on the alert server.
This shared secret serves as our AES key. Speaking of AES, that’s what we need to set up next. Let’s turn our shared secret into a symmetric encryption key!
We’ll do this in our /notification endpoint, which must switch to the new encryption schema.
The code simplifies, since symmetric crypto is simpler than public key. And now we update the client side.
Above all, we should be able to send an alert with forward secrecy! Now that we’ve ostensibly finished our code, let’s try it out and hope the alert arrives correctly!
Voila! Our app is done, we’ve implemented forward secrecy with Python. Well, it works, at least. However, there’s still a lot of work to make this a viable, production ready app. But at the very least, we’ve created a working demo of forward secrecy.
You can find the full code here: https://github.com/darighost/forward-secrecy-poc-app.
When does forward secrecy with Python matter?
When discussing forward secrecy, people tend to think of two use cases.
- TLS (like HTTPS)
- Messaging apps (like Signal)
The first use case is where forward secrecy really shines. With messages, you end up storing all of the ephemeral keys in the same place anyway, so there’s not as much benefit – you may as well just use one key. Since if they can get one, they can probably get them all. Really, the main security boon is that you can delete the keys after some amount of time. Since the keys only existed on your device, the message is truly irrecoverable even if a malicious server had stored the ciphertext.
With ephemeral data like a single web download or alert, forward secrecy really shines. You can throw away the key immediately after you use it, thus making your conversation much safer if you compare it to PGP style encryption, where you have to use the same keys long-term.