half-locket

Half-Locket · Technical overview

Inside Half-Locket

How a file gets sealed with a key that does not exist yet, why the key is torn in two, and what each party can and cannot see.

What it is

The one-paragraph version

Half-Locket seals photos and video into a single .locket file that cannot be opened until a date the sender chooses. The encryption happens entirely in the browser. The file never leaves the sender's device except by their own hand — they send it however they like, and the recipient opens it in a browser too.

Two things enforce the date. The first is mathematics, and cannot be persuaded, bribed, or reconfigured. The second is a small server that holds one half of the key and will not release it early — and that the sender can order to destroy its half at any time before the date, which ends the locket permanently for everyone.

Neither half is any use alone. Not to the recipient, not to an attacker, and not to us.

The part that sounds impossible

Timelock encryption, and the honest version of the trick

The obvious objection to a time-locked file is that a key must exist to encrypt with, and if the key exists then somebody has it. If somebody has it, the date is a promise rather than a fact.

Timelock encryption gets around this, and the resolution is genuinely surprising:

You never encrypt with the future key. You encrypt with its address — and the mathematics accepts an address as a lock while accepting only the real key as the key.

The clock tower

There is a public network called drand, run by a couple of dozen independent organisations — universities, research labs, infrastructure companies — scattered across different countries and jurisdictions. Every three seconds, together, they publish one number. The numbers come out in strict order, forever, and each is permanently public the moment it appears.

Think of it as a clock tower that, instead of chiming, announces a number. Announcement 12,345,678 will happen at a time anyone can calculate, because the rate never changes. What that number is cannot be known until it happens.

No single organisation can produce a future announcement, because no single organisation holds the whole stamp — each holds a fragment, and a threshold of them must act together to produce each number. That is the security assumption, stated plainly: a locket's date holds unless a majority of that network colludes to run the clock forward in secret.

…677public…678public…679public…680the unlock date…681…682every 3 seconds, foreverseal to this one
Sealing picks the announcement whose scheduled moment is the unlock date, and locks to its position in the sequence. The announcement itself is the key, and arrives on time whether or not anyone is watching.

Why an address is enough

The cryptography underneath is identity-based encryption on a pairing-friendly curve. Its defining property is that you can encrypt to an arbitrary label using only a master public key — and the private key for that label can be derived only by whoever holds the master secret.

drand's arrangement is exactly that shape. The network's public key is the master public key. The number it publishes for round N is the private key for the label N. So sealing a file to next June is: encrypt to the label "round 12,345,680", using a public key that is already published. Opening it is: wait until the network publishes round 12,345,680, and use it.

What this buys

Nobody has to be trusted to withhold anything, and no countdown runs on the sender's device or ours. Changing a phone's clock does nothing. Losing our server does nothing to the date. The file is bound to a moment in a public sequence that will arrive on its own.

Why the key is torn in two

A 2-of-2 split, and why it is XOR rather than a cut

Timelock alone would give a file that opens on schedule and cannot be stopped. That is a bad property for a keepsake. People change their minds; relationships change; a message written for a fifteen-year-old is not always one you want a twenty-five-year-old to receive.

So the key is split. The real encryption key K is 32 random bytes. A second value B is 32 more random bytes, drawn independently. The first half is whatever makes the two reconcile:

K — the real keysplitA = K ⊕ BB — randomtimelocked, written into the filethe date cannot be rushedheld by the server, or by the sendercan be destroyed on request
Each half, on its own, is indistinguishable from random noise. Only the pair reconstructs the key, and reconstruction is a single exclusive-or.

The choice of exclusive-or rather than cutting the key in half is the whole point, and it is the kind of detail that looks like a nitpick until you work it through. If you cut a 256-bit key down the middle, each holder has 128 real bits of it — the search space for the rest collapses from impossible to merely expensive, and worse, one party genuinely holds half the secret. With XOR, A is uniformly random and reveals nothing: for any key you care to guess, there is a B that makes A consistent with it. One half alone is not a weakened key. It is not a key at all.

Half A enforces the date

It travels inside the .locket file, wrapped by the timelock. Everyone who holds the file holds it, and nobody can unwrap it before the round arrives.

Half B is the veto

It goes to the server, which releases it only after the date — or, in manual mode, stays with the sender, who simply chooses whether to send it.

The life of a locket

Sealing, in order — and the order is load-bearing

  1. The sender picks files, an optional private note, and a date.

    Everything from here happens in the browser. Nothing is uploaded.

  2. The browser draws K and B from the operating system's random source, and computes A = K ⊕ B.

    Two separate draws. Deriving one from the other by any function at all would destroy the independence the split depends on.

  3. Server mode only. A record is created: the unlock date, half B, and a fresh capsule ID. The server returns a cancel token, once, and keeps only its hash.

    The token is the sender's proof of ownership. The server cannot recover it if the sender loses it.

  4. Half B is zeroed from memory immediately, and the cancel record is written to the browser's own storage before anything long-running starts.

    The token exists nowhere else at this instant, so it is made durable before encryption begins, and a failure to store it fails the whole seal.

  5. Half A is timelock-encrypted to the drand round matching the unlock date.

  6. The payload is encrypted with K in fixed 4 MiB chunks, AES-256-GCM, one chunk held in memory at a time.

    Each chunk gets its own nonce and is authenticated against the capsule ID, its own index, and whether it is the last — so chunks cannot be reordered, duplicated, or truncated without detection.

  7. The .locket is assembled: a cleartext header, then an encrypted manifest, then the encrypted chunks.

  8. The key and both halves are wiped, and the finished file is handed to the sender to send however they like.

    In manual mode the sender must save the .lockethalf sidecar first — it is the only copy of half B in existence, and the app will not release the locket until it has been saved.

And then, on the day

The recipient chooses the file. The app reads the cleartext header, sees the unlock date, and shows a countdown; nothing is sent anywhere at this point. When the date arrives it unwraps half A from the now-public drand round, obtains half B — from the server, or from the sidecar the sender sent separately — reconstructs K, and decrypts. The manifest first, then files one at a time, on demand. Leaving the page drops the key.

What is in the clear, and what is not

The .locket file, honestly itemised

A locket has to be openable years later by software that only has the file, so some of it must be readable without a key. That part is worth stating precisely rather than glossing.

In the clearEncrypted
Format version, capsule ID, exact unlock time, drand chain and round, the wrapped half A, whether half B is on a server and which one, chunk size, nonce prefixEvery filename, every MIME type, every file size, the time it was sealed, the sender's private note, and all of the content

So a locket in transit reveals its own size, its unlock date, and that it is a locket. It does not reveal what is inside it, how many files there are, or what they are called.

What the server knows

Five values, and no way to reach the contents

This is the part most easily got wrong by apps that claim end-to-end encryption, so it is worth being exact. In server mode the backend stores five things per locket, split deliberately across two different storage systems:

ValueWhereWhy
unlockAtDurable ObjectThe release decision
sha256(cancelToken)Durable ObjectVerify a cancel request without holding the credential
createdAtDurable ObjectBookkeeping
cancelledDurable ObjectA one-way flag
halfBKey-value store32 bytes — see below

The ciphertext never arrives. Neither does half A, or the key, or a filename, or the note. The entire upload for a locket is about a hundred bytes regardless of whether the file is two megabytes or two gigabytes.

Why half B lives somewhere else

Durable Objects keep thirty days of point-in-time recovery history, and that cannot be switched off. Overwriting a value there does not destroy the old one at the storage layer, so someone with account-level access could restore a snapshot from before a cancellation and read back a half B the sender had already destroyed. The key-value store has no customer-facing recovery, so a delete there is a delete. Cancellation would otherwise be a promise with a thirty-day asterisk.

Cancellation, in order

The sender presents the cancel token. The server compares its hash, marks the record permanently non-releasable first, and only then deletes half B — so the guarantee holds from the moment of the write even if the delete needs retrying. Afterwards nobody can reconstruct the key, including the sender.

A wrong token, an already-cancelled locket, and one that never existed all return exactly the same refusal. The server will not confirm whether a capsule ID is real to somebody who cannot prove they own it.

The limit worth stating

Cancellation works before the unlock date. After it, if the recipient has already fetched half B, they have what they need — deleting the server's copy stops anyone fetching it from that point on, but it does not reach back and take anything away. The app says so in those words rather than implying a recall.

Manual mode

The same file, with no server at all

A sender can seal with no server involvement whatsoever. The app produces the .locket plus a small .lockethalf file holding half B, and the sender keeps that second file. The recipient gets the locket now and the half whenever the sender decides to send it.

The trade is exact: no record exists anywhere, and no request is ever made to us — but the sender's veto is now a file they have to keep safe for as long as the locket runs, and there is no way to recover it. Opening a manual locket still contacts the public drand network, because half A is still timelocked. That is the one network call manual mode cannot avoid.

Deliberate limits

Where the guarantees stop

Any honest description of a system like this has to include the parts it does not solve. These are not open bugs; they are properties of the approach.

The site that serves the code could change the code

This is the structural weakness of every browser-delivered encryption tool. The app's protections are enforced by JavaScript served from a domain, and whoever controls that domain could serve different JavaScript tomorrow. A strict content security policy stops third-party code from running and stops the app talking to anywhere it should not — but it cannot defend against the origin itself, because the origin is exactly what it trusts. Native apps solve this with code signing; a web page has no equivalent.

The device is the boundary, not the page

Once a file is decrypted it is plaintext in a browser process. A browser extension with page access, or malware, sees what the person sees. Nothing in a web app can prevent that, and claiming otherwise would be dishonest.

Saved files are ordinary files

A decrypted photo saved to a phone is a photo on a phone: unencrypted, backed up to whatever cloud that device syncs with. The same is true of the .lockethalf and cancel-card sidecars, which are real secrets stored as plain files. This is inherent to letting people keep what they were sent.

Metadata is not content, but it is not nothing

Opening a server-mode locket tells our API a capsule ID and an IP address. Opening any locket tells a drand relay which round is being requested, which implies the unlock date. No contents leak either way, but the fact of an unlock does.

Storage is not a vault

The cancel token lives in browser storage, which browsers are entitled to evict — Safari discards script-writable storage for sites left unvisited for a week. For a locket sealed ten years out, that matters, and it is why the app also offers a portable cancel card. A sender who keeps neither has lost the veto, silently.

Half-Locket is built with tlock-js against drand's quicknet, WebCrypto for AES-256-GCM, and a Cloudflare Worker whose entire job is to hold thirty-two bytes and refuse to hand them over early. halflocket.app