What this protects.
And what it does not.
A security page that only lists strengths is marketing. The second half of this one is everything this service cannot do for you, including the parts that are inconvenient to admit.
What the service can and cannot see
Your message is encrypted in your browser with AES-256-GCM. The ciphertext travels in the URL fragment — the part after # — which browsers do not transmit. It is not in the request line, not in the Referer header, and not in any server log, ours or anyone else’s along the way.
So the server never receives the plaintext. It also never receives the ciphertext. There is no endpoint that accepts one and no column that could hold one. This is not a policy we are promising to keep; it is a shape the system does not have.
What the server does hold is one of the two halves of the key.
The split
| Half | Where it lives | Who has it |
|---|---|---|
| Client share | 256 random bits, inside the link fragment | Whoever holds the link |
| Server share | HMAC-SHA256(secret, noteId) | Recomputed on demand; released once |
HKDF-SHA256 over their concatenation produces the AES-256-GCM key, salted with the note identifier. An optional passphrase contributes a third share via PBKDF2-SHA256 at 600,000 iterations over a random 128-bit salt carried in the fragment.
The note identifier is bound in as GCM additional authenticated data. A ciphertext lifted out of one link and pasted under a different identifier will not decrypt — it fails the authentication tag rather than producing garbage.
Why the expiry can sit in the identifier in the clear
A note identifier is 128 random bits plus its expiry. The expiry is readable by anyone. It is also an input to the HMAC, which means editing it produces a different identifier, therefore a different server share, therefore a key that decrypts nothing. Tampering is self-defeating. The server independently refuses any identifier whose expiry is in the past or further out than the longest lifetime it offers.
One-time release
Reveal performs a single atomic SET <hash> 1 NX EX <ttl>. The first caller creates the marker and receives the share; every subsequent caller, including ones arriving in the same millisecond in different regions, is told the note is gone. There is no read-then-write window.
If storage is unreachable, reveal returns a 503 and no share. A note that cannot be marked burned is not opened. The failure mode is “temporarily unavailable”, never “quietly readable twice”.
What is stored
- Burn markers
- One byte of value under a key derived from a keyed hash of the note identifier — not the identifier itself. Expires when the note would have expired anyway.
- Rate-limit counters
- An integer per endpoint per ten-minute window, keyed by a hash of the caller’s IP address rather than the address itself. Also expires on its own.
- Nothing else
- No user table, no message table, no ciphertext, no key share, and no log of what was created. Creating a note writes nothing at all.
What this does not protect you from
Anyone who sees the link can read the note
The link is the credential. There is no second factor unless you add a passphrase. If the link passes through a channel someone else can read — a shared inbox, a logged corporate proxy performing TLS interception, a group chat, a screen behind you — that person can open the note. Your recipient will then find it already burned, which is the only signal you get that it happened.
Corollary: a burned note that the recipient never opened is evidence of interception. It is worth treating as such.
The link persists in places you do not control
The fragment is part of a URL, and URLs are sticky:
- It is in the sender’s browser history and address-bar autocomplete.
- It is in whatever app was used to send it, including that app’s backups, its cloud sync, and its server-side message store.
- It may be in the recipient’s history until the page clears it, which happens only after a successful reveal.
We clear the fragment from the address bar once a note is decrypted, but that only touches the current tab. It cannot reach into the sender’s history or the messaging app that carried the link.
A reader can keep the note
Burning destroys the key, not the reader’s memory, their screenshot, their clipboard, or the text file they pasted it into. “Self-destructing” describes the service, not the recipient. If you would not want it written down, a note that deletes itself does not change that.
Whoever controls this site could serve different code
All the cryptography runs in JavaScript this site serves. Anyone able to modify what is served — a compromised deployment, a stolen hosting credential, a malicious commit — could serve a version that exfiltrates the fragment before decrypting it. This is the fundamental limitation of browser-delivered cryptography and no amount of client-side design fixes it.
What is done about it: no third-party scripts, no analytics, no fonts or assets from other origins, and a Content-Security-Policy with connect-src 'self' that leaves an injected script nowhere to send anything. What is not: there is no code-signing or reproducible-build story, and you are trusting the deployment.
Metadata is not hidden
We do not log request bodies or note identifiers, but the hosting platform records HTTP requests as every host does. The existence and timing of a note being opened is observable to someone with access to those logs, along with the address that opened it and the identifier in the request path.
That is a privacy leak, not a confidentiality one: the identifier alone cannot decrypt anything, because the client share never appears in any request. But someone with log access could open a note before its recipient, which would burn it. The threshold for that is “already has access to our infrastructure logs”.
Ciphertext is padded to size buckets, so the length of a link reveals only which bucket a message fell into, not its size.
The passphrase is PBKDF2, not Argon2id
Argon2id is the better choice against GPU-accelerated cracking, and we are not using it. The honest reason is dependency and policy cost: the audited implementations are WebAssembly, which would require 'wasm-unsafe-eval' in the script policy and add a binary to the trusted bundle of an app whose entire pitch is that it is small.
PBKDF2-SHA256 at 600,000 iterations is OWASP’s current floor. It is also not the only thing standing between an attacker and the plaintext — they would need the link’s client share and the one-time server share as well. Choose a passphrase accordingly: it is a third factor, not a password protecting a database dump.
Not covered at all
- Traffic analysis, or a global adversary correlating timing.
- A compromised endpoint on either side. Malware on the reader’s machine sees what the reader sees.
- Denial of service beyond per-IP rate limiting.
- Anyone who obtained the link and is willing to burn the note to read it.
Reporting something
If you find a flaw in any of this, the service itself is a reasonable way to tell us about it: write it up, and send the link to the address on the domain’s WHOIS contact. If you believe it is a working exploit, please describe the class of problem rather than publishing a recipe.