Security model
The v1 scheme, parameter by parameter
The parameters below are the ones the shipped client uses, so you can check the claims against the primitives rather than against our description of them.
What the server receives
A share is a single JSON document. The browser produces it before anything is uploaded, and it is the entire contents of what we store. A note blob looks like this:
{
"v": 1,
"alg": "A256GCM",
"ciphertext": "<base64url>",
"iv": "<base64url, 12 bytes>",
"hasPassword": false,
"wrappedKey": null,
"kdf": null
} There is no title field, no filename field, no sender field and no recipient field, because the service never asks for any of them. The iv is a nonce rather than a secret: it is required to decrypt and is useless without the key. When you set no password, wrappedKey and kdf stay null, and the key exists only in your link.
Parameters
| Element | Value | Reasoning |
|---|---|---|
| Link secret | 128 bits from crypto.getRandomValues | Short enough to keep links manageable, far beyond brute-force reach. The extra characters a 256-bit secret would add to every link buy nothing against any realistic adversary. |
| Key derivation | HKDF-SHA-256, empty salt, info string classified/v1 aes-gcm content key | Expands the 128-bit secret to a full AES-256 key. The salt is empty deliberately: the secret is already uniformly random, which is the condition under which RFC 5869 permits it. The info string separates this key from any future use of the same secret. |
| Content cipher | AES-256-GCM, 96-bit nonce, fresh per share | Authenticated encryption, so a tampered ciphertext fails to decrypt rather than returning plausible garbage. 96 bits is the nonce length NIST SP 800-38D recommends for GCM. |
| Password mode | PBKDF2-SHA-512, 310,000 iterations, random per-share salt | Wraps the link secret under a key derived from your password, so the link alone decrypts nothing. The iteration count follows OWASP's PBKDF2-SHA-512 guidance. |
| Share identifier | 72 bits of randomness, 12 base64url characters | Independent of the key. Guessing an ID yields ciphertext the guesser cannot read, and consumes the share's single read in the attempt. |
Where the key goes
For a share without a password, the 128-bit secret is base64url-encoded and placed after the # in the link. Fragments are resolved by the client and are not part of the request-URI sent to the server, per RFC 3986 section 3.5. That is a property of HTTP itself rather than a policy we apply, which is why the design leans on it: the key is not part of the request our servers receive, so it is not in the request logs. The guarantee is narrower than it first sounds, though. A fragment is fully readable by any JavaScript running on the page, analytics and third-party scripts included. It is withheld from the network layer, not from the page. That is why the limit on browser-delivered code, further down, is the one that actually bounds this design.
The key does travel wherever the link travels. Paste a link into a chat product and that product now holds the key. The fragment protects the key from our infrastructure, not from the channel you choose.
Password mode changes the trust model
With a password set, the browser generates the same 128-bit secret, encrypts that secret under a PBKDF2-derived key, and stores the result in wrappedKey alongside the salt and iteration count in kdf. Nothing usable goes in the link.
Use this when the link and the password can travel by different routes, such as link by email and password by phone.
Be precise about what it buys. The wrapped key, the salt and the iteration count are all stored in the envelope and returned to whoever fetches the share. Anyone holding that envelope can therefore try passwords offline, at whatever rate their hardware allows, with no rate limit available to us. The 310,000 PBKDF2-SHA-512 iterations make each attempt expensive, and that cost is the entire defence. A key derivation function multiplies an attacker's work; it cannot create entropy your password never had. Pick a password with real entropy rather than a memorable one.
What we can still see
A zero-knowledge design constrains content, not metadata. We can observe, and in some cases must observe:
- The size of the ciphertext, which bounds the size of the plaintext.
- When a share was created, when it expires, and when it was read.
- Whether a share is password-protected. The client records that flag in the envelope, so it is visible to us and countable in aggregate.
- Standard request metadata handled transiently at the Cloudflare edge, such as IP address, timestamp and user agent, for delivery and abuse prevention.
None of that reveals content, and none of it is nothing. A claim of "we store nothing" would be false. The privacy policy covers the account-level records in more detail.
Where the design stops
One-time delivery is best-effort, not atomic
Reading a share is a fetch followed by a delete against an eventually-consistent key-value store. Those are two operations, not one transaction. Two requests arriving close enough together can both receive the payload before either delete lands. The stored TTL is the backstop that guarantees eventual removal, so the single read is a strong default rather than a cryptographic guarantee. Anything that must be read exactly once needs a protocol we do not claim to provide.
An automated first reader can consume the share
Link previewers, security scanners and mail gateways follow URLs. Any of them can be the first reader and burn the share before your recipient opens it. This is inherent to delete-on-read over HTTP rather than specific to us. When a recipient reports a dead link they never opened, this is almost always the cause.
Browser-delivered code is delivered by us
The encryption runs in JavaScript our servers send you. A provider able to serve modified client code to a targeted visitor could capture plaintext before encryption ever happens. Publishing source would help reviewers understand the intended design, but even that does not prove any particular visitor received that exact build. This limit applies to every browser-based end-to-end system. The mitigations are reproducible builds, signed native clients and independent audits. To be direct about where we stand: our source is not currently public and we have not commissioned an external audit, so this limit is one you have to take on trust today.
What none of this controls. A recipient who can read the content can screenshot it, photograph the screen or forward the plaintext. One-time sharing governs the copy on our infrastructure. It cannot govern a person.
Primary sources
- NIST SP 800-38D, Recommendation for Block Cipher Modes of Operation: Galois/Counter Mode (GCM) and GMAC, for nonce length and authentication tag guidance.
- RFC 5869, HMAC-based Extract-and-Expand Key Derivation Function (HKDF), including when an empty salt is acceptable.
- RFC 3986, URI Generic Syntax, section 3.5, Fragment.
- W3C, Web Cryptography API, for the
SubtleCryptooperations used throughout. - OWASP, Password Storage Cheat Sheet, for PBKDF2 iteration guidance by hash function.