Back to work
Personal

locc.it

A web app for sharing one-time secrets: the sender encrypts in the browser with a generated passcode, the server stores only the ciphertext on a deadline, and a recipient with the passcode opens the link exactly once before it's deleted.

Role
Sole engineer & designer
Timeframe
2019 – Present
Stack
React · Express · MongoDB · CryptoJS

Every team shares secrets, a database password, an API key, a login for a contractor. Almost nobody shares them safely. locc.it is the tool I built so I wouldn't have to: paste a secret, get a link and a passcode, and know the secret was encrypted before it left the browser, can be opened exactly once, and will expire whether or not anyone opens it.

The problem

Watch how secrets actually move between people and it's grim. A password gets pasted into a Slack DM. An API key goes out by email. A production token lands in an iMessage thread because it was the fastest window open. The secret reaches the right person, and then it just stays there.

Every one of those channels keeps a copy. Slack retains message history and syncs it to every device on the account. Email lives in two mailboxes and two sets of backups. Nothing expires. A credential shared once in 2019 is still sitting in a searchable archive today, long after anyone remembers it's there. The leak isn't the moment of sharing, it's the indefinite retention afterward.

I wanted a tool with three properties: encryption in the browser (a secret is ciphertext before it leaves the sender's machine), one-time access (a link opens once, then the secret is gone), and a required expiry (every secret has a deadline, there is no option to create one that lasts forever).

The architecture

locc.it is a React frontend and an Express + MongoDB backend. The shape of the security model matters more than the stack: the browser handles the encryption, the server handles storage and the decrypt step, and nothing sensitive is persisted on the server beyond a short, bounded window.

Life of a secret
Sender's browser
generates passcode
AES encrypt
CryptoJS, in-browser
POST /api/encrypt
ciphertext + expiry
MongoDB
ciphertext + URL + expiry
POST /api/decrypt
passcode submitted
Server decrypts
in-memory, then deletes
Plaintext returned
HTTPS response
Record purged
single-use
Link is dead
refresh finds nothing

Encryption in the browser

When a sender submits a secret, the browser generates a random passcode and runs CryptoJS AES over the payload before anything is transmitted. Only the ciphertext (and the URL identifier, and the chosen expiry) are sent to the server. The passcode never touches the server at write time; the sender shares it with the recipient out-of-band, alongside the link.

What the server actually does

The server's standing ability to read anything is zero. There is no master key, no per-user key, no recovery flow. The ciphertext sits in MongoDB alongside the URL identifier and the expiry, and that's it. When a recipient submits the passcode, the server uses it once to run the AES decrypt in memory, returns the plaintext to the recipient over HTTPS, and immediately deletes the record. The passcode is never persisted; the plaintext exists for the duration of the request and never afterward.

This is the security trade-off worth being clear about: the server is trusted to perform decryption on request, even though it has no standing ability to read what it stores. Moving the decrypt step into the recipient's browser (so the passcode never reaches the server at all) is the most important future change. More on that below.

One-time access, mandatory expiry

A locc.it link works exactly once. The first successful open returns the secret and destroys it; a refresh or a second click finds nothing. Every secret also carries an expiry, and it isn't optional: a link that's created and never opened doesn't linger, because once its deadline passes a cleanup job purges the record whether it was viewed or not. Between single use and required expiry, every secret has a short, guaranteed lifetime instead of an open-ended one. A locc.it link is only ever live, used, or expired.

Brute-force protection and bot defence

Three failed passcode attempts lock a record for an hour. The decrypt and encrypt routes are individually rate-limited (5 and 10 requests per minute respectively) to slow scraping. Forms run behind Google reCAPTCHA, and a cron job sweeps expired records on a schedule. None of these are exotic, but together they close the obvious back doors around the cryptography rather than relying on the encryption to do all the work.

Accounts are optional

A first-time sender can use the tool without signing up. OAuth via Google, GitHub, or Facebook is there for people who want a small dashboard of secrets they've sent, but it's never required to share or receive one. The default path is anonymous on purpose: the higher the bar to send a secret safely, the more people just paste it into Slack.

Key decisions

Encrypt on the client, store ciphertext only

The simplest way to build a secret-sharing tool is to encrypt on the server with a key the server holds. It's easy to build and it means a database dump is a secrets dump. Doing the encryption in the browser, with a passcode that's never written down on the server, makes the stored data inert by itself. A copy of the database alone is just a pile of locked boxes; the keys aren't anywhere on disk.

Expiry is mandatory, not a setting

Most sharing tools make expiry optional and default it generously. locc.it makes it required and bounded. The cost is that a sender can't create a permanent link, even if they want one. The gain is that there is no such thing as a forgotten locc.it secret: the failure mode where a credential quietly outlives its purpose can't happen, because the default behaviour is the safe behaviour and there's no way to opt out of it.

A web app, not a developer tool

locc.it could have been a CLI or a library, cleaner to build, and a natural fit for the engineers most fluent in why it matters. I made it a plain web page instead: paste a secret, get a link. The people who most need to share credentials safely aren't all developers, and a tool only improves security if the least technical person in the exchange will actually use it.

Outcomes

Zero Passcodes or plaintext persisted on the server
One view Then the record is deleted
Required Every secret carries an expiry
Since 2019 Live and in use for over six years

locc.it has been live since 2019 and still runs today, years of quiet uptime for something that has never asked for much attention. It does one thing: move a secret from one person to another without leaving a copy behind. It's still the link I send whenever someone needs a credential from me, and building it was the cleanest way I know to think hard about a real threat model end to end: what the server holds, what survives a view, and for how long.

What I'd do differently

The biggest improvement I'd ship is moving the decrypt step into the recipient's browser. Today the passcode is submitted to the server, used once, and discarded; the cryptography is correct but the server is a trust boundary that doesn't need to exist. A version where the recipient's browser fetches the ciphertext, decrypts it locally with the passcode the sender shared, and the server is never asked to perform the AES step, would shrink the trusted-server window to zero. This is the change that moves locc.it from "the server has no standing ability to read" to "the server cannot read."

Alongside that, I'd document the security model openly: publish exactly how a secret is protected end-to-end, so the model is something a visitor can verify rather than take on faith. For a tool whose entire pitch is trust, "it's encrypted" should be a claim anyone can check, not one they have to believe.

Six years in, the surrounding app has accumulated features around the edges of its one real job. A tool this focused is usually strongest stripped back to exactly what it does, and deciding what still earns its place is overdue.