Back to work
Personal

locc.it

A zero-knowledge one-time secret sharing tool. The sender encrypts in the browser with a generated passcode, the server stores only the ciphertext on a deadline, and the recipient decrypts in their own browser before the record is deleted. The passcode never touches the server.

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 both encryption and decryption, the server only ever holds opaque ciphertext, and nothing sensitive is persisted on the server, ever.

Life of a secret
Sender's browser
generates passcode
AES encrypt
CryptoJS, in-browser
POST /api/encrypt
ciphertext + expiry
MongoDB
ciphertext + URL + expiry
GET /api/secret/:url
returns ciphertext, marks viewed
Recipient's browser decrypts
CryptoJS, passcode never sent
Plaintext rendered
in-browser only
DELETE /api/secret/:url
record purged
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 the recipient hits the link, the server returns the ciphertext, marks the record as viewed (starting a short grace window for refreshes), and never sees what happens next. The recipient's browser runs the AES decrypt locally; the passcode never leaves their device. Once the plaintext is on screen the client calls DELETE /api/secret/:url and the record is purged. The passcode is never logged, never transmitted, never persisted. The plaintext only ever exists on the two browsers at the ends of the exchange.

A 60-second grace window after the first fetch tolerates page refreshes during the decrypt UX. After that the record self-cleans on the next access attempt, with a cron sweep as belt-and-braces backup. Either way the link works exactly once.

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

Brute-force resistance is structural rather than tracked. An attacker would need both the URL and the passcode, and the URL is one-shot: a single fetch starts a 60-second grace window after which the record is purged regardless of whether anyone managed to decrypt it. That leaves an attacker with at most a minute of offline attempts against a ciphertext they can't re-acquire. 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-knowledge Server cannot read what it stores; passcode never leaves the sender or recipient
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

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. The architecture is there to support that audit; the write-up just isn't.

I'd also harden the recipient UX around lost passcodes. Today a wrong passcode shows a generic "incorrect passcode" message and the recipient keeps trying. With a stronger entropy passcode (longer, more diverse character set) and a small client-side hash check against a known prefix, typos could be distinguished from "the wrong link", which would help the recipient ask the sender for the right thing instead of burning their own attempt window.

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.