The Short Story

  1. After you type your message, your computer generates a random token, like r9q7wwzsza6tqpcbmmdgmemsz8mp33hmm. Your computer encrypts your message using this token.
  2. Your computer sends the encrypted message to SneakyNote.com for storage and then shows you the link for your note, like this: https://sneakynote.com/get#r9q7wwzsza6tqpcbmmdgmemsz8mp33hm
  3. You send the link to your friend over email or chat.
  4. Your friend clicks the link. Their computer grabs the encrypted message from SneakyNote.com and uses the token in the link to decrypt the message. Behind the scenes, SneakyNote.com immediately destroys its copy of the encrypted message so nobody else can access the link.
  5. Both you and your friend will see a code. The code ensures that the link wasn't replaced by a hacker between you and your friend. After verifying the code with you over the phone, your friend can view your message. Done!

Learn By Doing

To see how all this works, try sending a SneakyNote to yourself.

Send a SneakyNote

The Long Story

The truth is in the source code, but here's an English language version.

Goals

The system design had a few goals to improve on the existing self-destruction/expiring password sharing services.

  1. The server would never see a plaintext secret.
  2. The server would never be able to decrypt a secret.
  3. Stored secrets would never touch a hard disk.
  4. Stored secrets would expire.
  5. Stored secrets would be completely erased from RAM on access or expiry.
  6. The cryptography would be sound. (That means no user-generated passwords for protecting the secret.)
  7. Open source.

The original design did not imagine a verification code, but it was added to detect MITM attacks. The service can provide no useful guarantees without MITM detection.

Implementation

SneakyNote.com uses the Stanford Javascript Crypto Library (SJCL) to encrypt and decrypt the secret in the web browser.

The first step is to generate the token for the url. Most browsers these days implement window.crypto.getRandomValues() which exposes a cryptographically secure random number generator to Javascript. If that is available, SJCL will use that. If not, SJCL falls back to gathering entropy from mouse movements, the accelerometer, key presses, and other signals. If less than 256bits of entropy has been gathered before the user presses "Send", they are presented with a mouse movement/finger press challenge to gather more entropy.

When the random number generator is seeded, the sender's browser generates a lowercase base 30 token. The alphabet is the standard alphanumeric base 36, but with 0 1 i l o omitted because they are ambiguous, and with u omitted because there is a high probability it might be preceded by an f and that's a problem if you happen to read the token to someone out loud.

The target entropy for the URL token is 160 bits, but because the number of characters in the alphabet is not a power of two, the actual token strength is about 161.9 bits.

The 256bit encryption/decryption key is generated by prepending "cipherkey" to the URL token like so cipherkeyr9q7wwzsza6tqpcbmmdgmemsz8mp33hm and hashing it with SHA256.

An initialization vector is generated similarly, but by prepending "iv" like so ivr9q7wwzsza6tqpcbmmdgmemsz8mp33hm and hashing it with SHA256.

The cipher key and initialization vector are used to encrypt the secret message using 256-bit AES in CCM mode. Only the first 88-104bits of the initialization vector are used (depending on the length of the message).

The URL token is hashed with SHA256 a third time, prepended by "uuid" like so uuidr9q7wwzsza6tqpcbmmdgmemsz8mp33hm. The result is converted into a valid UUID like 8d6ef9d4-4cd8-4069-be34-f7333986d226.

The browser sends a POST request to https://sneakynote.com/notes/8d6ef9d4-4cd8-4069-be34-f7333986d226 with the encrypted message in the request body.

The SneakyNote.com server is a small app written in Go. For secret storage, it functions as a key-value store. It stores the encrypted secret in a file on a ramfs ram disk and sends back a small, randomly generated base 30 code like 7pv f69 qkkh. This code has about 49.1 bits of entropy.

To prevent remote timing attacks, the file name of the saved encrypted secret is the SHA256 hash of the binary version of the note's UUID.

At the end of the request, the server zeros out all of the application buffers that contained the encrypted secret. Go is a garbage-collected language, but GC doesn't erase memory, it only frees it. We must zero the memory because there is no guarantee that the memory will be overwritten any time soon. Due to memory fragmentation, parts of a secret could linger a long time if we did not zero the buffers.

The TLS connection is terminated in-app, not before. (Run an SSL audit.)

After the secret is saved (HTTP status 201 Created) the sender's browser presents the SneakyNote link as https://sneakynote.com/get#r9q7wwzsza6tqpcbmmdgmemsz8mp33hm and begins long polling https://sneakynote.com/notes/8d6ef9d4-4cd8-4069-be34-f7333986d226/status for the note status. To ensure that only the sender (and not any middleman who sees the link) can poll this address, the server requires the request to include the code returned when the note was created. However, the note status and/or code is theoretically available to an attacker via a good timing attack.

When the recipient (or an attacker) clicks the SneakyNote URL, the browser sends a request for https://sneakynote.com/get to the server. URL fragments (the part after the # sign) are not sent in HTTP requests. This means the URL token r9q7wwzsza6tqpcbmmdgmemsz8mp33hm is NOT sent to the server, so the server is unable to calculate the decryption key.

The https://sneakynote.com/get page is static HTML with Javascript. The Javascript immediately looks at the URL token in the fragment and is able to generate a UUID, cipher key, and initialization vector as before.

Having determined the UUID from the URL token, the recipient's browser sends a GET request to https://sneakynote.com/notes/8d6ef9d4-4cd8-4069-be34-f7333986d226. The server returns the encrypted secret along with the 7pv f69 qkkh code.

On secret access, to prevent race conditions the server actually renames the secret file randomly before re-opening the file to read. After reading the encrypted secret, the server zeros the file on the ram disk before unlinking the file, and creates a record noting that this secret was accessed (rather than expiring). The server also zeros the encrypted secret from all application buffers.

Having determined the cipher key and initialization vector from the token, the recipient's browser decrypts the secret but hides it on the page, instead displaying the 7pv f69 qkkh code for verification.

The sender's browser's long poll indicates that the secret was accessed. The sender's browser then also displays the 7pv f69 qkkh code for verification.

When the recipient indicates that the codes match, the browser displays the decrypted secret.

Any further requests to https://sneakynote.com/notes/8d6ef9d4-4cd8-4069-be34-f7333986d226 return HTTP status 403 Forbidden indicating that the secret has already been read. The server does not have the encrypted secret, all that remains is a small file indicating that UUID has been accessed.

To enforce secret expiry, on a GET https://sneakynote.com/notes/8d6ef9d4-4cd8-4069-be34-f7333986d226 request the server looks at the modification time of the encrypted secret file and, if the file is older than 10 minutes, returns HTTP status 410 Gone.

A sweeper runs every minute to zero out and remove any old secrets. A small separate file is left behind to indicate that the secret expired without being accessed.

In practice this means that secrets can only be accessed for 10 minutes, but their lifetime in the server's RAM could be up to 11 minutes.

Well, that's all the important parts. If you want more details, you'll have to read the source or ask me.

Threat Models

Because SneakyNote links will most often travel over untrusted channels, the main goal of a SneakyNote is to detect interception rather than prevent it.

Ideally, robust interception detection will also discourage attackers from trying to intercept messages. I imagine most attackers do not want the other parties to know they are being watched.

MITM Greedy Read

If a middleman accesses the secret before the intended recipient, when the recipient clicks the link they will see a "Consider Secret Compromised" message rather than the secret. Both parties can react accordingly.

If the sender is watching the status page, they can see when the secret is accessed. A secret accessed too quickly can tip off the sender that something is up.

MITM Greedy Read And Replace

A smarter rogue middleman would not merely read the secret. A smarter middleman will read the secret, generate a new SneakyNote link with the same secret, and substitute the new SneakyNote link into the original communication before forwarding that email/SMS/chat message to the recipient.

If the sender is watching the status page, they can see when the secret is accessed. A secret accessed too quickly can tip off the sender that something is up.

When the recipient opens the message, they would see the same secret. The message codes, however, will not match.

Of course, the parties should verify the code by voice. If the sender and recipient verify the code over the same channel, the middleman can simply swap the code in transit and the codes will look like they match.

MITM Clever Read And Replace

A bad-guy middleman that does not want the note to immediately appear as opened to the sender could replace the SneakyNote link in the communication, long poll the status of the new link, but wait to open the original link until just after the replacement note is opened.

The sender will not see the note open early.

However, the middleman will not be able to see the original note's code nor the original's secret before creating the replacement. The replacement note will contain a false secret and an entirely different code.

If the secret was, for example, a password, then the attacker's only hope for non-detection is to immediately use the password to sign into the service and then change the password to the replacement note's secret. The receiver will then be able to sign into the service.

Again, the note verification codes will not match. And, if the middleman changes the password, the sender will not be able to sign into the service under the original password.

Can Verification Codes Be Brute-forced?

Verification codes are generated randomly by the server (from a cryptographic RNG). The middleman could (with some delay in forwarding the message), attempt to create a lot of SneakyNotes and wait for a code that matches the original. SneakyNote.com may be able to handle 10,000 requests per second. Over 10 minutes of creating SneakyNotes as fast as possible, an attacker has only about a 1 in 100 million chance of getting a code that matches the original message. The attacker only has a 10 minute window before each note expires, so they cannot increase these odds. They could only ever cover 1/100,000,000 of the code space.

Compromised Server, Eavesdropping Only

If an attacker gains access to the SneakyNote.com server, they could begin copying off encrypted secrets as they arrive at the server.

The attacker may be able to see which IP addresses are talking to the server and when, but without the decryption key, the attacker cannot read the secrets.

If, however, the attacker also has broad access to snoop global internet traffic, they might begin collecting SneakyNote links they see. They could decrypt any secret for which they also intercepted the SneakyNote link.

Bug me to add something here about the most likely methods for an attacker to gain access to the server box.

Compromised Server, New Javascript

If the attacker does not have broad access to snoop other internet traffic, an attacker might instead modify the SneakyNote server to send Javascript that tells the browser to store the secret unencrypted instead—or perhaps more subtle: encrypt the secret with the UUID so the secret still looks encrypted but the attacker can recover the decryption key.

In this case, it's a matter of eyes on the Javascript. The attack is in plain sight for anyone who would audit the Javascript or run a diff against GitHub. To make auditing easier, SneakyNote.com does not minify the Javascript.

Search Warrants Etc.

SneakyNote.com is not able to arbitrarily provide plaintext secrets without compromising our service similar to what an attacker would do.

Philosophically, I have no problem per se with cooperating with search warrants that went through due process—there are real bad guys in the world and I don't want SneakyNote.com to be their friend.

However, forcing an organization to write new code to compromise their own cryptosystem to honor a search warrant could be a bad precedent.

As of yet, SneakyNote.com has received no warrants or similar wiretapping requests.

In Case of A Compromised Server

If I ever become aware of an attacker in the SneakyNote.com server, news will be published on the home page and and on the SneakyNote send and get pages.

The service will be taken offline while the attack is resolved. After that, service may or may not resume.

Reporting Security Issues

If you notice a vulnerability in SneakyNote.com, I use S/MIME. We can send a few emails back and forth to get that handshake set up, then you can report the vulnerability. Comodo and StartCom offer free S/MIME certificates.

If the issue is less serious, open an issue or prepare a pull request.

Acknowledgements

Cryptography provided by the Stanford Javascript Crypto Library (SJCL)

Images by Unsplash

Built with Skeleton

Checkmark, X, and arrow icons used unchanged from IcoMoon

Design inspired by Photon from HTML5 UP

Steve Richert identified a few typos.

Let's Do This

I'm ready to Rijndael all over some UTF-8.

Send a SneakyNote