NoteKillnotes that do not survive

Every claim, where it's enforced, how you can verify it

This page exists to be checked, not believed. Each guarantee below names the place it is enforced and gives you a way to test it.

This service stores self-destructing notes, encrypted files up to 25 MB, and chat rooms that are deleted once nobody has them open. It is built so that we cannot read any of it. This page states each guarantee, where it is enforced, and how you can check it — including the one limit we cannot engineer away. Where a claim cannot be verified from outside, we say so.

The guarantees

Security guarantees: each claim, the mechanism that enforces it, and how a visitor can verify it.
ClaimWhere it is enforcedHow you can verify
AES-256-GCM, authenticated encryption In your browser, via the native Web Crypto API. No third-party crypto library is loaded. Read /assets/app.js — unminified, no build step. Search for crypto.subtle.
The key cannot reach the server The 256-bit key sits in the URL fragment, after #. Browsers never transmit fragments in HTTP requests, so the key cannot reach our logs or any proxy's. It does reach other places on your own device — see the note below this table, which we would rather you read than discover. DevTools → Network, open any share link: the request carries the 32-hex path only. Nothing after # is sent.
Message lengths are rounded, within limits Notes and chat messages are padded to a whole number of 256-byte blocks before encryption, so "ok" and a two-line reply are stored at exactly the same size. This hides small differences, not large ones. A three-page note still occupies three pages: rounding to 256 bytes conceals whether a message was short or medium, never whether it was short or long. File contents are not padded at all — a 9 MB upload would have to become 16 MB to hide its size, and we judged that not worth the bandwidth. Send two short notes of different lengths and compare the size of what the network panel uploads. They match. Send a very long one and it plainly does not.
Reading destroys, so reading is never a GET Opening a share link loads a static page and asks the server only whether something is still there. The note itself is fetched — and deleted — by a POST, sent when you press the button. Link scanners, antivirus proxies and message previews issue GET requests, so none of them can spend a note by visiting the link. curl https://notekill.com/api/notes.php?uid=<id> answers 405, and the note is still readable afterwards.
An optional passphrase, so the link alone is not enough In your browser. With a passphrase set, the key is derived from the link and the passphrase together — HKDF(link key ‖ PBKDF2(passphrase, 600,000 iterations)) — so an intercepted link decrypts nothing. We never receive the passphrase, and never learn whether one was set: that marker lives in the URL fragment, which browsers do not send. Create a note with a passphrase, then open the link and leave the field blank. It refuses. Watch the network panel while doing it: neither the passphrase nor the marker appears in any request.
A wrong passphrase never destroys the content The note is fetched — and burned server-side — before the passphrase is asked for, then held in the open tab. Retries happen on your device with no further request. Asking first would have meant one typo destroyed a note permanently. Open a passphrase-protected note, get it wrong several times, then get it right. It opens. The network panel shows a single request throughout.
Burn-after-read cannot race Server-side, inside a database transaction using SELECT … FOR UPDATE. Our test: 8 simultaneous readers of a one-view note — exactly one got the note, seven got 404. Create a one-view note and request it from two clients at once. One succeeds.
File bytes are not directly fetchable Ciphertext is stored outside the web root under a random 32-hex name; a server-side script streams it and enforces expiry and the download counter. The download URL is handled by a script, not served as a file. Once spent or expired, the same URL stops serving.
Filenames are encrypted too The original filename and MIME type are encrypted in your browser, separately from the file bytes. Upload with DevTools open: no plaintext filename appears anywhere in the request.
We cannot tell who is talking in a chat Messages are ciphertext rows in the database. Your chosen display name travels inside the ciphertext. Watch the network panel while chatting: the name you typed never appears outside the encrypted payload.
No access logs The web server is configured not to write an access log for this site. No record exists of who read which note. Not verifiable from outside. Stated here and in our privacy policy; its consequences are enumerated under legal orders below.
Rate limiting stores no IP We store a truncated HMAC-SHA256(IP, per-window salt). The salt is random, belongs to that hour alone, and is destroyed together with the counters it keyed — so once an hour closes, its hashes cannot be linked back to any address by anyone, including us. The raw IP is never written to the database. The limiter fails open: if the database is unreachable, requests are allowed through uncounted rather than refused. That is a deliberate choice of availability over strictness, and it means rate limiting is a defence against casual abuse, not a guarantee that holds during an outage. Not verifiable from outside. What this does and does not protect is set out below.
CSP allows nothing by default Content-Security-Policy: default-src 'none', with no unsafe-inline and no unsafe-eval. curl -sI https://notekill.com/ | grep -i content-security
HSTS for two years max-age=63072000; includeSubDomains on every response. The header also carries the preload token, but carrying the token is not the same as being on the list: preloading requires submitting the domain to the browser vendors' list separately, and we have not done that yet. Treat this as HSTS, not as preloading, until it appears at hstspreload.org. curl -sI https://notekill.com/ | grep -i strict-transport
No Referer leakage Referrer-Policy: no-referrer: a site you open from one of our pages never learns the URL you came from. curl -sI https://notekill.com/ | grep -i referrer-policy
Every script is pinned Subresource Integrity on the stylesheet and every script, plus modulepreload integrity for the imported ES modules. View source, then compare the hashes against /integrity.json.
No third party in the TLS path TLS terminates on the server we run. No CDN, no reverse proxy, nothing between your browser and us that could read the connection. Inspect the certificate in your browser: it is issued to this domain, not to a content delivery network.

"The key never reaches the server" is true, and on its own it invites a conclusion that is not. The key is part of the link, and links go places.

Your browser history. The full URL, key included, is written to history when the page loads. We strip the fragment from the address bar once a note has been read, but the visit was recorded before that. And if you use Chrome with sync switched on, your history is uploaded to Google — so the key lands on somebody's server after all. Not ours, which is exactly the point worth being precise about: our not having it does not mean nobody has it.

Your clipboard. Pressing "Copy" puts the link, with the key, into the system clipboard — shared with every application, and on Windows retained in clipboard history if that feature is on.

Wherever you sent it. A link pasted into WhatsApp, Slack or email is stored by those services exactly like any other message. That is the one a passphrase is for: with one set, the intercepted link is not enough.

None of this has a technical fix from our side — the key travels in the link because that is what keeps it away from us. What we can do is say so, so that "the server cannot read it" is not mistaken for "nothing can".

Two things a passphrase does not do, said here rather than left to be discovered. It does not help if you send it in the same message as the link — that is one secret travelling one road, and an intermediary who reads the message has both. And a short one does not hold: anyone with the link and the stored ciphertext can try passphrases offline, and while each attempt is made deliberately slow, slow is not impossible.

We use PBKDF2 with 600,000 iterations of SHA-256. Argon2id would resist graphics-card cracking better, and we do not use it, because no browser provides it natively — adding it would mean shipping a third-party library and a build step, and then the code on this site could no longer be read end to end. We would rather keep the frontend auditable and tell you where the floor is.

The honest limit

One thing you must take on trust — or verify.

The encryption code arrives from our server. Whoever controls a server can, in principle, serve altered code to future visitors — code that leaks keys. Subresource Integrity and our Content-Security-Policy do not prevent this, because the same server publishes the hashes. This is true of every in-browser encryption tool ever built. Most of them don't mention it.

Two things limit the damage. First, it is forward-looking only: notes already encrypted and sent stay sealed no matter what code is served later. Second, it is detectable: every script we serve is pinned with Subresource Integrity, the hashes are published at /integrity.json, and you — or anyone — can archive them and check that the code served tomorrow is the code that was served today. Our frontend is hand-written vanilla JavaScript with no build step, so what you audit is what runs.

Be precise about what that detection is worth, because the distinction is the whole thing. Our integrity checking compares this server against itself. It catches a file that changed between two of your visits — because the record your browser kept last week is the one thing here we cannot reach. It does not catch a server that rewrites the code and the list of hashes together: to somebody arriving for the first time, an honest server and a thoroughly compromised one look identical, and no amount of checking performed by code that server sent you can tell them apart.

So the value is in repetition, and in comparing notes with other people. Save a baseline on the verification page, come back, and check it again — ideally from a different network. If something changed and we did not announce a release, ask us why before you use the service.

If a tool in this category tells you it has no such limit, it is describing its marketing, not its architecture.

VERIFY

curl -s https://notekill.com/assets/app.js | sha256sum

Record the hash, then run the same command tomorrow from a different network and compare: serving altered code selectively to one visitor is exactly what that comparison catches.

What a legal order could actually obtain

We answer this precisely, because "we have nothing" is never entirely true. Served with valid legal process, we could produce three things.

  • CIPHERTEXT The stored blob and its metadata: creation time, byte size, expiry, view counter. We cannot decrypt it — the key never reached us.
  • ERROR LOG Access logging is switched off, but the server still keeps an error log. A failed request can leave a line containing an IP address.
  • RATE-LIMITER HASHES We keep truncated HMACs of client IP addresses for the current hour, deleted when that hour closes. For as long as an hour is open, its salt exists on the machine, and with that salt the IPv4 address space is small enough to search exhaustively offline — so the hashes of the current hour could be worked back to the addresses that produced them. Once the hour closes, the salt is destroyed with the counters, and that becomes impossible for anyone holding anything: the database, our configuration secret, or both. An earlier design keyed these with a permanent server secret, which meant a database copy plus that one value was enough to unwind every hash it ever produced; that is no longer how it works, and we changed the mechanism rather than soften the sentence. The residual exposure is one open hour, and it is real: those rows are stamped with the hour and separated by which endpoint was called, while every note, file and room carries its own creation time in the same database, so within that hour someone holding both could line them up.

The following does not exist and therefore cannot be produced: access logs (there is no record of who read which note), accounts, email addresses, any means of decryption, and any way to warn an affected user — we do not know who our users are.

We respond to valid legal process only, and we contest requests that are overbroad. We cannot hand over what was never recorded.

There is no warrant canary. Under the confidentiality duties that EU legal orders can carry, a canary that lapses is a legal risk for the operator rather than a reliable signal for you, so we do not publish one.

Infrastructure

A single server inside the European Union. TLS terminates on it directly — no CDN, no reverse proxy, no third party anywhere in the path between your browser and us. Nothing sits in the middle that could read the connection even if it wanted to.

A page load fetches four first-party files and nothing else. No cookie is set for anyone using the service, no analytics, no third-party scripts, no fonts pulled from someone else's domain. The operator's settings page at /admin is the one place a cookie exists, scoped to that path — a visitor's browser never receives it and never sends it. The only browser storage used is a single sessionStorage entry holding the chat display name you typed, and it dies with the tab. Expired notes, files and chat rooms are removed by a scheduled sweep every minute, not only when the next visitor happens to arrive.

We do not publish the hosting provider, the region or the software versions we run. Those are operational details; naming them would help someone attacking this service more than it would help you, and no law requires us to disclose them. What you can hold us to is on this page — the guarantees, where each is enforced, and how to check.

How encryption actually runs, step by step

  1. Key generation. Your browser calls crypto.getRandomValues to produce a 256-bit key. Native Web Crypto; no third-party crypto library.
  2. IV. A fresh random 96-bit IV for every encryption. Tested: 500 encryptions under one key produced 500 distinct IVs.
  3. Encrypt. AES-256-GCM through crypto.subtle. GCM is authenticated encryption: in our tests, flipping a single bit in the body, the tag, or the IV always makes decryption fail. Tampering produces an error, never garbage output.
  4. Pack. IV, ciphertext and authentication tag are concatenated as IV||ciphertext||tag and base64-encoded. For files, the filename and MIME type are encrypted separately from the bytes.
  5. POST. The browser sends the ciphertext and the retention settings the server must enforce — expiry and view limit. Neither the key nor the plaintext is in the request.
  6. Store. The server assigns a 32-hex identifier from random_bytes — 128 bits from a CSPRNG, never a weak PRNG. Encrypted file bytes go to a directory outside the web root.
  7. Link. The share link is https://notekill.com/n/<32 hex chars>#<43 base64 chars>. Everything after # stays in your browser. Decryption reverses these steps locally; what we hold is ciphertext we cannot read.

The code that does all of this is served unminified at /assets/app.js. Shorter answers live in the FAQ; what we retain and why is in the privacy policy.