← Semua artikel
Bahasa Melayu

Burn after reading: what actually eats a one-time link

Someone sends a burn-after-reading link over Slack. Slack fetches it to draw the little preview card. Ten minutes later the recipient clicks, and the page says the file is gone. Nobody downloaded anything; Slack’s preview fetch did.

That is the reason “burn after reading” is harder to build than it sounds. The internet is full of things that open links on your behalf: chat apps drawing previews, mail gateways scanning for malware, browsers prefetching the page you might click next. If opening the link is the event that destroys the file, machines will destroy it before a human ever sees it.

So here is how ttl.space decides that a file has been read, and what that means when things go wrong.

Opening the page is not reading the file

A burn-after-reading link points at a page, not at the file. The page is plain HTML: a password box and a warning that the file can be downloaded only once. Loading it does not count as a download. You can open it, close it, send it to yourself, let Slack and WhatsApp and Teams draw their preview cards, let a corporate mail filter poke at it, and the file is still there.

The same goes for the small request the page makes as soon as it opens, before you type anything. It fetches the very start of the encrypted file, at most 314 bytes (the header and the encrypted metadata), so that the password check can happen locally. That request is read-only too.

The reason this is safe is not a clever heuristic about bots. It is that the server will not hand over the file to anyone who does not have the password. When you type it, your browser derives the key that decrypts the file, and from that key a download token. The server stores only a hash of that token. A request that arrives without the right token is refused and changes nothing. A preview bot does not have the password, so it cannot produce the token, so it cannot consume the file. It may fetch the page and cache it all it likes. Any request it makes for the file itself is refused. (How the key and the token are derived is described in how the encryption works.)

The moment the file is consumed

The counter is bumped the moment the server accepts a request that carried a valid token, and it becomes final when the first encrypted byte leaves. Nothing before that touches it: loading the page, typing the password, the small probe request. After it the file is used up, whether or not the download finishes: from then on the page, the probe and every new download answer “not found”.

That rule has a consequence people don’t expect: a download that breaks halfway still counts. If the connection drops after 40 percent, the file is already used up, and starting a second download gets “not found”.

We chose that on purpose. The friendlier rule (roll the counter back if the transfer didn’t finish) looks obvious, and it would break the feature.

The file is encrypted in independent chunks. Each chunk can be decrypted on its own, without the rest. A client that receives 40 percent of the file has 40 percent of the plaintext, not a useless fragment. If a broken transfer reset the counter, a cooperating client could read the file one piece at a time, disconnecting on purpose after each piece, and each time the server would say “nobody has read this yet”. One-time would mean nothing. So the server treats any bytes on the wire as bytes the recipient may already have.

There is one exception. If the request was valid but the server failed before sending anything at all (the storage backend timed out, say), the counter rolls back. Zero bytes sent means zero information out the door, and you get another try.

There is also one allowance, and it is not a rollback. A phone that changes from Wi-Fi to mobile data, or locks its screen, often loses the connection halfway through a large file, and the whole file would be lost with it. So the answer to the first request carries a resume ticket: 256 random bits that no other response ever shows. The server keeps only a hash of it, in memory, and the page holds it in memory too, never in storage and never in a URL.

When the connection breaks, the page comes back with the ticket, the download token and the byte where it stopped, and gets the rest of the file from there. That is the same transfer carrying on, not a second one, and the file stays used up the whole time. The ticket works for ten minutes after the break, at most 20 times, and never later than six hours after the first byte. Anyone without it, even with the link and the password, still gets “not found”.

The copy on our server goes on a timer too: it is deleted two minutes after the download completes, or ten minutes after a break that nothing picks up.

What happens Does it consume the file?
Slack, WhatsApp, Teams, iMessage draw a link preview No
A mail gateway or antivirus fetches the page No
The recipient opens the page and closes it No
The recipient’s browser prefetches the page before they click No
The recipient types a wrong password (checked in the browser, never sent) No
The recipient double-clicks the download button No, the button is disabled while the download runs
The server fails before sending any encrypted bytes No, and you can retry
The first encrypted byte is sent to a request with the right password Yes
The transfer breaks after that Yes, but only that download can pick it up again
The file is never downloaded and the timer runs out It expires anyway

What the recipient sees

The download page for a one-time file says it in plain words: “This file can be downloaded only once: it is used up the moment the download starts, then permanently deleted from the server. If the connection drops briefly, the download picks up where it stopped; it cannot be started again.”

The picking up is done by the page, not by you. When the connection breaks, it asks the server for the rest of the file from the last byte it received and keeps trying for a few minutes, so a phone that changes networks usually just pauses and carries on. The ticket lives only in that open page: close or reload the tab and nothing can pick the download up any more. If the page cannot get the rest, it tells you the download was interrupted and the file has been consumed, and to ask the sender for a new link. After that, a new attempt on the same page comes back with “File not found. It may have already been downloaded or expired”, and reloading the link returns a plain “not found” rather than a download page, because the file is already used up. The only thing that works is to ask the sender for a fresh link.

That sounds harsh until you compare it with the alternative. A service that lets you retry a one-time download has to keep the file around after the first attempt, which means it is not one-time. It is “one-time, mostly, unless something went wrong, in which case it is a few times”. The ticket is not a retry. It lets the first download continue, for minutes, for the one page that holds it, and it never lets a second download begin, not even for someone with the link and the password. When that page is gone, we would rather the file be gone too and the sender spend thirty seconds uploading again.

Two things keep that cheap for the sender. Uploading again is a drag and drop, with no form to fill in, or a single ttl send --burn from the terminal if you use the CLI. And if you leave the password box empty, one is generated for you, sixteen characters, so the new upload gets a new password rather than a reused one.

Where the robots still win

Here is what the one-time rule does not cover.

Burn after reading protects the copy on our server. It does nothing about the copy on the recipient’s device. Once the file is decrypted into their downloads folder, it lives as long as they keep it, and so does any copy they make of it.

The one-time rule is enforced by the token, so whoever has the link and the password is “the recipient” as far as the server is concerned. If you post both in the same channel, the first person to type the password wins. Send the link one way and the password another. Chat for the link, a call or a text for the password.

And the audit row the server keeps says that a download happened, when, from which IP address (kept as a keyed hash plus an encrypted copy) and roughly where that address was, and what the browser called itself. It cannot say who. The password itself is never sent to the server, only a token derived from it, so on the recipient’s side there is no account, no identity, nothing to attach the event to. A service that cannot read your files cannot vouch for who read them either; that is the trade.

So if you got a one-time link and the page said the file was gone, one of a few things happened, and none of them was a preview bot: someone with the link and the password downloaded it before you, a download started and broke partway, the timer ran out first, or the sender deleted it early, from My uploads or the Orbit dashboard. In every case the fix is the same. Ask the sender for a fresh link.

If you need a signed receipt from a named person, this is the wrong tool. If you need a file to reach one person and then disappear, the rules above are how we keep the robots from burning it first.

Diterbitkan pada · Dikemaskini pada .