I’ve always been on the lookout for new, innovative ways to empower secret sharing between non-technical folks. For the longest time, I used services like One Time Secret because it allowed for large secrets, automatic expiration, and an optional passphrase to protect the secret.
But how secure are these services? Really?
Whenever you use a system like One Time Secret (or the numerous clones out there on the web) you have to trust the third party running the system to not eavesdrop on your secrets. With One Time Secret, the system uses your passphrase to encrypt the secret when it’s stored on disk. However, both your secret and your passphrase are sent to the server in plaintext, meaning the site owner can see them!
So again, how secure are secret sharing services? Really?
Secure secret sharing
If you can’t trust the third party exchanging or storing the secret data, you need to take further precautions. In this case, end-to-end encryption is the best approach as your data is encrypted by you before the server sees it, and it’s only ever decrypted by the recipient.
Even if the server wanted to spy on you, they’d be blocked by the encryption around the secret!
A few months ago, a friend and colleague introduced me to a fully end-to-end encrypted secret sharing app his team had built. The app worked as advertised, but it repurposed a system they’d built for a completely different purpose. A bit like driving an M1 Abrams tank to pick up groceries – you’ll definitely get there, but is this really the best way to run errands?
On his urging, I took it upon myself to build an alternative.
A little while later, I sent him an Ngrok link to demo a service running on my local machine. This service was comprised of three elements:
- A PHP application used to power an API for both creating and retrieving secrets
- An in-memory Redis datastore used to store the secrets
The goal was to allow him to store an encrypted secret and send me a secret link directly, then communicate his user-selected password out of band (i.e. send me the link in email and text me the password). The Redis datastore holds only encrypted data, and the PHP application merely provides an interface to that data. Nothing in the server can see his password, encryption key, plaintext data, or any information that could be used to violate the secrecy of the information.
I’ve also taken steps to fully containerize this project and have now launched it as a publicly available project – Project Swordfish.
How it works
The system is built atop several, well-studied cryptographic primitives:
- Password-based key derivation (PBKDF2) to expand human-readable passphrases into keys appropriate for use in other cryptographic operations
- Secure hashing algorithm (SHA-256), the hashing algorithm underlying each round of the above PBKDF2 operation
- Bcrypt, the default password hashing algorithm supported by PHP’s
- The Advanced Encryption Standard (AES-265), specifically in Galois/Counter Mode (GCM) to support authenticated encryption and decryption (AEAD)
These aren’t the only cryptographic choices one could make to build such a system. I selected them due to the commonalities between the in-browser SubtleCrypto API and the cryptographic primitives available to vanilla PHP so I could build a command line interface to the same system. Using the same primitives between two languages (JS and PHP) also allowed me to validate I had everything implemented properly.
Under the hood, the main page allows you to enter a secret for storage and a password with which to protect it. But we don’t use the password as-is – using user-defined passwords directly as encryption keys is inherently insecure! Instead, we use the password to derive two separate keys:
- The first key is used as a verifier and is generated using the password, salted by a static “pepper,” run through 10000 rounds of PBDKF2.
- The second is used as an encryption key and is also generated using the password, but with a random salt, and another 10000 rounds of PBKDF2.
Secrets are encrypted using AES-256 GCM using the derived encryption key. Then the random salt, derived verification key, and encrypted payload are sent to the server.
The server treats the derived verification key as a password and only ever stores a hash of the key. This hash and the encrypted payload are stored in Redis mapped to a completely random string generated by the server. This string is used as the secret ID and returned to the person who stored the secret.
All of the data bound to your secret is also set to automatically expire and be purged from Redis after 24 hours.
The server never sees your passphrase or your plaintext data. Ever. Even if the server itself is breached, the data it contains is completely unusable by a third party attacker. Any data flowing over the wire is similarly useless to an attacker or eavesdropper!
When you request a secret, all you need to provide is the secret ID and your passphrase. Because the server doesn’t know your passphrase, the browser will again derive the verification key. It then sends that derived key (along with the secret ID) to retrieve data.
The server verifies you have the right password. If things match, returns your original salt and the encrypted data payload. In the browser, your passphrase is again expanded to an encryption key leveraging PBKD2 and finally used to decrypt the secret!
Project Swordfish is, today, hosted on AWS using CloudFront for static content and load-balanced docker containers for the API backend. It’s locked down to prevent script injection (and block browser extensions) so secrets are safe from the prying eyes of third parties or trackers. Which means I can’t monitor traffic via Google Analytics (though I can monitor CloudFront metrics). And I can’t pay for the site with advertising.
In all, my goal with the project was to prove I could build something. A truly anonymous, purpose-built, end-to-end encrypted secret sharing tool. I’ve now done that and want to share it with the world.
Why did I build it? Because it’s a tool we need. It’s not one that readily existed already. And mostly because I can.