How the key stays out of our database
Everything after the # in a URL is never sent to the server. That one detail is the whole architecture - and it is worth being precise about what it does and does not buy you.
When you hand someone a credential through a web service, the obvious question is what the service itself can see. Most tools answer this with a promise. The more useful answer is an architecture where the promise is hard to break even if we wanted to break it.
The fragment never leaves the browser
A URL splits into parts, and one of them behaves differently from the rest. Given this link:
https://oobsecret.com/s/8Kq2mNvR7xPfL3wD#b8BuzfVRuwaX6a5cXcUW
\_____________________________________/ \____________________/
sent to the server never sent
The part after # is the fragment. It was designed to point at a location
within a document, so the browser resolves it locally and strips it from the
HTTP request. It is not in the request line, not in the Referer header,
and not in our access logs - because it never arrives.
So we put the encryption key there. The flow is:
- Your browser generates a random 256-bit key. It never asks us for one.
- It encrypts your secret locally with AES-256-GCM.
- It sends us only the ciphertext. We store that and return an opaque id.
- It builds the link as
/s/<id>#<key>and hands it to you.
The recipient's browser does the reverse. It requests /s/<id>, which
tells us which row to serve but nothing about how to read it, then reads the key out of
location.hash in JavaScript and decrypts the ciphertext locally.
What sits in our database is a blob we have no key for. If we were compelled to hand over every row, or if someone walked off with the whole table, the useful content is not in it.
Why a passphrase is stretched, not hashed
When you add a passphrase, it is not used as a key directly - human-chosen strings have far too little entropy for that. It goes through a slow key-derivation function first, so that guessing costs real time per attempt rather than being bounded only by how fast an attacker's hardware can run a hash.
The derived value is combined with the fragment key, which means a passphrase-protected secret needs both the link and the phrase. That is the whole point of sending them separately, and it is why the passphrase should never travel in the same message as the link.
What you still need to do
Fragment-held keys close one specific hole. A few things stay in your hands, and they are worth knowing before you send anything important.
- The link is the secret. Anyone who gets the whole URL can read the payload until it burns. Pasting it into a channel that logs messages puts it right back where it started.
- Browser history keeps fragments. The recipient's address bar and history hold the key. On a shared machine that matters.
- We still see metadata. Not the secret, but we see that a row was created, how large it was, and when it was fetched.
- A passphrase is only as good as the passphrase. It is stretched before use, but a weak one is still a weak one, and it needs to travel on a different channel from the link or it adds nothing at all.
The short version
We hold ciphertext and metadata. The key lives in a part of the URL that browsers are specified never to transmit, so it stays with the two people who have the link. Look after the link the way you would look after the secret inside it, and put a passphrase on anything that would hurt to lose.