I just read this (excellent) introduction to the Secure Drop protocol, meant for journalists to have a way to safely accept anonymous information from sources.
Using Tor and something like Tails (with a pretty good guide on how to connect to it from an unusual location) you can probably get to the right website in an anonymous manner. With the current version of Secure Drop, you go to a website, such as TechCrunch, and get a .onion address that you can put into the Tor browser. You then get a codename and can upload files and a message.
You are trusting the server to a very large extent, as far as I can see. If the server you are reaching has been compromised, it can just send anything you add directly to the Bad Guys. I mean, the website recommends using GPG to encrypt the messages locally, but then it gives you the GPG key.
For fun, I tested this with the Washington Post. They have a couple of ways to provide information anonymously. One is via Secure Drop and the other is via GPG email. The keys for both are different. That doesn’t inspire confidence.
For that matter, the TechCrunch instance is running on version 2.5.2, whereas the current version is 2.8.0. I assume that those instances are basically set up once and then never maintained. For context, the TechCrunch current version of Secure Drop was released in Feb 2023, quite a while ago.
Let’s assume that there is a security vulnerability (I know of none, just to be clear) in an old version that hasn’t been updated. What happens if it is attacked and taken over by an adversary? Since the current model is to trust the server, you get access to everything.
I think that the new Secure Drop protocol's whole point is to get end-to-end encryption and break the requirement to trust the server. And more to the point, I think that the way they do that is really interesting.
Here are their stated requirements:
- No accounts, and therefore no user authentication.
- No message flow metadata, meaning messages can’t be linked together, and different types of messages are indistinguishable from one another.
- No changes in server state are observable externally.
- No ciphertext collection or information leaks via trial decryption – a given recipient receives pertinent ciphertext only.
Basically, there are three methods:
- Send
- Fetch
- Download
A source needs to send information to a journalist. The journalist publishes their public key somewhere. For example, as a QR code in a physical magazine, etc. Using the journalist's public key, we can start the ball rolling.
They encrypt the data locally and then Send the encrypted file to the server, where it is stored. The next step is for the journalist to Fetch and Download the data.
Each time the source sends information to the server, it is considered a separate action, and there is no way to link them together. To reply back, the sender needs to include their public key in their message.
But there is a problem here. You don’t want the server to know who is the recipient of the messages. Note that this holds both ways, you should not be able to see that a particular journalist got a message or trace a reply to the source.
In other words, the server should have as little information as possible about what is going on. And the way they implemented that feature is absolutely beautiful. Remember, the source will encrypt the message locally using the journalist’s public key.
That is done using an ephemeral key pair, used only for this message. Along with the encrypted message, they send the (ephemeral) public key for the message and also compute a Diffie-Hellman key from the ephemeral private key and the journalist public key.
This is what it would look like:
def send(msg, journalist_public_key):
tmp_key_pair = generate_key_pair()
enc_msg= encrypt(msg, journalist_public_key)
msg_gdh = scalar_mult(tmp_key_pair.private, journalist_public_key)
return enc_msg, msg_gdh, tmp_key_pair.public
The Gdh here refers to Group Diffie-Hellman. Note that this isn’t actually used for anything, we just send that to the server.
When a journalist tries to fetch messages, they aren’t providing any information to the server. The server has no way of knowing who it is talking to. And yet the idea is to send information that only the journalist can read. In order to do that, the server will generate an ephemeral key pair (only for that request) and Diffie-Hellman key from the current request’s private key and Gdh value provided by the source.
The server will compute another Gdh value. This time between the message’s public key and the private key of the request. It will send the encrypted message, the public key of the request and the server computed Gdh value to the journalist. Here is what the code looks like:
def fetch(msg_id, msg_gdh, msg_public_key):
tmp_key_pair= generate_key_pair()
enc_key = scalar_mult(tmp_key_pair.private, msg_gdh)
msg_srv_gdh = scalar_mult(tmp_key_pair.private, msg_public_key)
enc_msg_id = encrypt(msg_id, enc_key)
return enc_msg_id, msg_srv_gdh, tmp_key_pair.public
That looks like utter nonsense, right? What is going on here? Let’s go back to first principles. Classic Diffie-Hellman is based on multiplication on a finite field. Let’s assume that we agreed on Base = 5, and Modulus = 23 (values taken from Wikipedia).
The journalist generates a private key: 13 and computes 513 mod 23 = 21. The journalist thus publishes 21 as its public key.
Journalist key pair |
Private: 13Public: 21 |
The source generates a private key (for a single message): 3 and computes 53 mod 23 = 10. The source also computes a Diffie-Hellman key exchange from the message private key and the journalist public key using: 213 mod 23 = 15. That is marked as the message GDH (Group Diffie-Hellman).
Journalist | Source (per message, ephemeral) | Adversary knows… |
Private: 1Public: 5 | Private: 3Public: 10Message GDH: 15 | Journalist public key: 5 |
Note that usually, the GDH value (15 in our case) would be used as the agreed-upon encryption key between the source and the journalist. Instead of doing that, we are jumping through a few more hoops.
When the source sends a message to the server, it sends it its public key (10) and the GDH (15). This is stored internally and isn’t really doing anything interesting until a journalist needs to fetch messages from the server.
This is where the magic happens. During the processing of the fetch request, the server doesn’t have any idea who the journalist is, there are no keys or authentication happening. A key requirement is that the journalist is able to get new messages without the server knowing what messages they are able to read.
Let’s consider Lois and Clark as two journalists, and assume for simplicity that there is only a single message involved. Both Lois and Clark send a request to the server to get new messages. Since the server cannot tell them apart, it will respond in the same way.
The first step in the server processing the request is to generate an ephemeral private key: 9 and compute 59 mod 23 = 11. It will then compute a Group Diffie-Hellman, between its private key and the message public key: 109 mod 23 = 20, this is called the request GDH. It also computes another Group Diffie-Hellman, this time with the message GDH and its private key: 159 mod 23 = 14. That serves as the encryption key for the message, and the server uses that to send an encrypted message ID to the journalist.
Journalist (Lois) | Journalist (Clark) | Server |
Private: 13Public: 21 | Private: 17Public: 15 | Request Private: 9Request Public: 11Request GDH: 20Encryption key: 14 |
- | - | Server reply: |
- | - | Request GDH: 20encrypted(msg_id, 14) |
Note that in the table above, the reply from the server is computed in the same manner to both journalists (the server cannot tell which is which). However, for each request, the server will generate a new request key pair. So on each request, you’ll get a different result, even for the same data.
Now, what can the journalists do with this information? We have the message public key and the request GDH. Let’s have Lois do Group Diffie-Hellman on them: 2013 mod 23 = 14. That gives us the encryption key that was used by the server. We can use that to decrypt the message the server sent (which by itself is a reference to the actual encrypted message held by the oblivious server).
What about Clark? Using his own key, he can compute 2017 mod 23 = 7. And that means that he cannot get the encryption key to figure out what the server sent.
How does this work? Let’s break it down.
Message GDH | 213 mod 23 = 15 |
Encryption key for server | 159 mod 23 = 14 |
Request GDH | 109 mod 23 = 20 |
Encryption key for journalist | 2013 mod 23 = 14 |
The reason we get the same encryption key is that we actually end up computing:
enc_key_server = pow(
pow(journalist_public_key, message_private_key),
request_private_key
) % 23 # ((21^3)^9) % 23 = 14
enc_key_journalist = pow(
pow(message_public_key, request_private_key),
journalist_private_key
) % 23 # (((10^9)^13) % 23 = 14
In other words, when the server computes the encryption key, it involves:
- Message public key
- Journalist public key ^ message private key
- Request private key
And when the journalist computes it, it involves:
- Request public key
- Journalist public key ^ message private key
- Journalist private key
That is a really cool result. It makes sense, but it was non-obvious. Hence why I wrote it out in detail to understand it properly.
In terms of cryptographic theory, I’m not aware of any other use of something like that (note, not a cryptographer), and it would probably require a thorough review by actual cryptographers to verify.
In practical terms, actually making use of this is pretty hard. It uses a pretty low-level cryptographic primitive that is usually not exposed by modern encryption API. More to the point, I think that it isn’t a good direction to go toward, and I have in mind a much simpler solution instead.