cipher.ink Write a message

Learn · History

The history of cipher.ink

Built in 2016, broken for eleven weeks by a library update, rebuilt in a day, then slowed to a crawl by its own design. What we learned.

cipher.ink is two sites in one history. We built the first in December 2016, it ran for nearly ten years, and then it broke on August 6, 2026, and stayed broken for eleven weeks. The second was written in a single session on October 22, 2026, and in its first evening it was so slow to type in that it had to be redesigned. We tell both stories as plainly as we can, because the mistakes in them teach more than the successes do: a real example of what goes wrong when a page depends on someone else's code, why a security tool needs tests that actually run, and what a performance bug looks like when you measure it instead of guessing.

2016: the first version

The original went up in December 2016 as two PHP pages, one to write a message and one to read it, sharing the database settings of an older project. Its design was typical of the time:

  • CKEditor 4 as the rich-text editor, jQuery UI for buttons and dialogs, and a typeface from Google Fonts.
  • aes-js 2.x, a pure-JavaScript AES library, loaded from a third-party code host at a pinned commit, doing AES-256 in OFB mode in the page. The key was the passphrase, lowercased and padded to 32 characters, and the initialization vector was the message's own 16-character id.
  • An optional "To e-mail" field that mailed the link to a recipient, with a copy of each going to the site's owner.
  • Google Analytics.

It worked. People sealed messages and opened them, and the server kept only the ciphertext. By today's standards its cryptography was weak. The passphrase went straight into the key with no stretching, nothing authenticated the ciphertext, and the IV came from Math.random (random numbers in the browser). The security lived in the design choice that mattered most: the server never saw the passphrase. The database table from that era still holds 81 messages.

August 6, 2026: it broke

On August 6, 2026, an edit replaced the dead URL the page used to load aes-js with a new one, cdn.jsdelivr.net/npm/[email protected], and raised CKEditor to 4.22.1. It looked like routine maintenance. But aes-js 3 renamed a function the page called: aesjs.util became aesjs.utils. From that day, every attempt to seal or open a message threw a TypeError. The pages still loaded, but the one thing the site exists to do no longer worked, and it stayed that way for eleven weeks, until the rebuild.

Three lessons come out of it, and none of them is "don't update":

  • Pinning isn't enough when the major version changes. The 2016 code pinned an exact commit of version 2. The replacement pinned an exact version, 3.1.2, which was safe from later changes and broken from the day it went in, because nothing checked that the page still worked with it.
  • Load critical code from your own server. A page that fetches its encryption library from someone else's host trusts that host's availability, its versioning and its honesty on every visit. The current site self-hosts everything, and its Content-Security-Policy forbids anything else. A third-party script could also be protected with Subresource Integrity, a hash in the tag that the browser checks, and the old page had no such hash.
  • The critical path needs a test that runs. The one thing the site exists to do, encrypt and decrypt, had no automated check. The rebuild has 930 automated checks (by the numbers), including one that opens messages sealed in the old format byte for byte against the original library.

October 22, 2026: the rebuild

The site's owner asked for it to be made "a lot cooler", with the rich-text editor replaced, and to think big. Everything was rewritten in a single session:

The same evening: too slow to type in

The new site first looked like a full-screen dark "ink" canvas: drifting, blurred color fields, film grain, a frosted-glass control dock and a wall of ciphertext on the reading page. It was beautiful and unusable. The owner's verdict that evening was that the site was extremely slow, with typed letters taking ages to appear. The owner also disliked the text floating full-screen with the interface oddly indented, and asked for a page border with a white background, like Google Docs.

We measured the slowness and didn't guess. A headless-browser benchmark timed the gap between pressing a key and the next painted frame, and found 0.8 to 2.5 seconds. The script itself accounted for about 2 milliseconds per key. Taking the page apart layer by layer showed the cost was the browser painting through the blurred, blended and animated layers behind the text, not any of the JavaScript. With them removed, a keystroke took about 0.11 seconds, the same as an empty text box. The layout changed to what it is today: a white page on a gray desk, the lock controls in the header where Google Docs puts its Share button, and the light theme as the default. The finding became a standing rule of the project: no full-screen blur, blend-mode or animated layer behind the page, and no backdrop filter on anything that overlaps the text, with the benchmark run against any visual change.

The next day: features, a logo and an honest bug

On October 23 the owner picked, from a numbered list, the features to add: a My messages panel that lives only in the browser, read receipts that count only a real unseal, encrypted attachments, an optional passphrase hint, a QR code, per-IP limits with the addresses hashed, and the removal of the email field entirely. The site then gained its logo, a match whose head is a drop of ink, which stands for burn after reading, and its wordmark in a monospaced typeface chosen from ten candidates.

That same evening, an audit of the random-number code found a bug of the kind this site writes about. The passphrase generator picked each word with byte % 288, which made the last 32 of its 288 words impossible to choose, and message ids had the same flaw with byte % 57. Both got fixed with rejection sampling, and the cost turned out to be under a bit of strength. The modulo bias article has the before-and-after code and the exact numbers. It exists because we'd rather show our own mistake than hide it.

What the history teaches

  1. Dependencies are decisions. Each third-party script is a promise by someone else that you never negotiated. Count them, pin them, host them yourself, and test the page that uses them. The product now needs two libraries (DOMPurify and a QR generator).
  2. Test what the product is for. A site that looks right but can't do its job is worse than one that visibly fails.
  3. Measure before you fix. Instinct blamed the slowness on the cryptography. The timer showed it was paint, and the fix had nothing to do with cryptography at all.
  4. Don't strand old data. The 2016 messages stayed readable, in an openly stated weaker format, and nothing new is ever written in it.
  5. Say what went wrong. Eleven weeks of breakage and a modulo bug are in the public documentation, because a site that asks to be trusted with secrets should be honest about its errors.

The details of every decision are in the project's own history document, and the code that runs today is at the code page.

On the timeline

  • December 2016 cipher.ink is built: AES-256 in OFB mode in the browser.
  • 6 August 2026 cipher.ink stops working: an updated aes-js library renames a function the page calls.
  • 22 October 2026 cipher.ink is rebuilt in a day with Web Crypto, AES-256-GCM, secret links and burn after reading.

Further reading

  1. aes-js: the pure JavaScript AES library used in 2016 (Richard Moore)
  2. MDN: Subresource Integrity