Blog
Design decisions

The 20-character proof code, and why it is random

By the Let’s Seal team5 min read

Every seal gets a short code you can read over the phone or type off a printed stamp, something like 5X9H-KWF3-JTBC-55CS-6Q6X. The interesting part is where it comes from. It is minted from randomness, with no link to the file, and that single decision buys two properties at once.

A file already has an identity

Every sealed file already has a perfect name: its SHA-256 digest, 64 hexadecimal characters that identify it uniquely and change completely if a single byte moves. That is the cryptographic identity we anchor and sign. The trouble is that it overwhelms any human who tries to read it aloud, type it off a stamp, or remember it. A hash is a superb key and a terrible handle.

So each seal also gets a short code, shown at letsseal.org/v/<code>. It is twenty characters, printed as a typable line under the stamp and encoded into the QR beside it. The question that shapes the whole design is a simple one: where should those twenty characters come from?

1.3 x 10^30
distinct codes (32 to the 20th)
20
Crockford base32 characters
random
the code is minted from randomness, independent of the file

Why the code is minted from randomness

The tempting answer is to compute the code from the file, take some of the hash and shorten it. It would be neat, and it would be a mistake. A code derived from the document is a function of the document, which means it quietly carries information about it. Anyone who sees the code, on a stamp, in an email, over someone's shoulder, would be holding a fragment of the file's fingerprint. Two files that happened to share a prefix would announce that fact through their codes. We wanted the friendly handle to keep the private thing behind it private.

So the code is random. It is generated from cryptographically secure random bytes, independent of the file and its hash. As independent random data, it stays silent about the document's contents, size, and digest. We assign the name ourselves.

Your sealed file
Track 1
SHA-256 (cryptographic identity)
9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08
Track 2
Random 20-char code (friendly handle + capability)
5X9H-KWF3-JTBC-55CS-6Q6X
letsseal.org/v/5X9H-KWF3-JTBC-55CS-6Q6X/d/9f86d081...0a08
One file, two identifiers on separate tracks. The SHA-256 is the cryptographic identity we sign and anchor. The 20-character code is a random handle assigned to it, independent of it. Resolving the code just forwards to the canonical proof page.

Twenty characters, and the alphabet behind them

The code is written in Crockford base32, the alphabet 0123456789ABCDEFGHJKMNPQRSTVWXYZ. That is thirty-two symbols, and it deliberately omits I, L, O and U. Dropping I, L and O removes the characters people confuse with 1 and 0 when reading a code off paper, and dropping U makes it far less likely that a random string spells an accidental word. The result is twenty characters that survive being read aloud, faxed, photographed and retyped.

Twenty symbols from a thirty-two character alphabet gives 32 to the power 20, which is roughly 1.3 x 10^30 distinct codes. That size earns its keep. Even at internet scale, imagine 700 million users each holding many seals, the space stays almost entirely empty. The live codes are a vanishingly thin scattering across an enormous field. To keep that field uniform we sample random bytes and mask each down to the alphabet cleanly, because thirty-two divides evenly into the byte range, so every symbol is equally likely to appear. Every position in the code is a fair, independent draw.

Sparse enough to be a key

That emptiness is the second property, and it comes for free once the code is random. When live codes are a needle-thin fraction of the space, guessing one that resolves is infeasible, and working backwards from a file to its code is equally hopeless, because the two are independent. A derived code would have handed an attacker a shortcut; a random one across a space this large leaves only brute force, which the size defeats.

So knowing a code is itself evidence. It means you were given it, by someone holding the seal. The handle doubles as an unguessable capability token: possession of the code is possession of a link to the proof. A friendly name and an access key, from the same twenty characters, at no extra cost.

For that to hold, uniqueness has to be guaranteed. It is: at mint time a database check confirms the fresh code is available, and only then is it kept. Collisions are astronomically unlikely given the size of the space, and the check settles the matter for certain regardless.

Reading, folding and resolving

Stored, a code is exactly twenty uppercase characters in a single unbroken run. Shown, it is grouped for the eye as XXXX-XXXX-XXXX-XXXX-XXXX. On the way back in, we are forgiving about how a human types it. Input is uppercased, spaces and dashes are stripped, and the classic look-alikes are folded home: I and L become 1, O becomes 0, U becomes V. Whatever remains must be exactly twenty valid characters to reach the database.

A resolved code is a redirect. Visiting /v/<code> canonicalises the input, looks it up (sealed documents and standalone anchors share one namespace), and forwards to the canonical proof page at /d/<sha256>:

letsseal.org/v/5X9H-KWF3-JTBC-55CS-6Q6X

One more detail protects the capability property. Because a code doubles as an access key, we have to stop a scraper sweeping the space for live ones. So the resolver is rate limited per client address, at roughly sixty lookups a minute. Cross that line and the response is a 404, where most services would return a 429. A blocked probe looks exactly like a genuine miss, so a scraper stays in the dark about whether the code it tried exists.

There is a quiet consequence of all this in the order things happen. The code is minted before the file is sealed, before the sealed file's hash even exists. That works precisely because the code is independent of the hash. Once minted, it is baked into the QR and printed on the stamp, ready to carry a proof that it predates.

Every claim here is something you can check yourself, offline, with tools we do not control. That is the whole point.