Engineering writeup

How DocuChan actually works — and how to check

"Trust us" is not a security model. This page is the threat model for DocuChan, written the way we'd want another engineer to write it for us: what's encrypted and where, everything our servers store, the exact things we cannot protect you from, and how to verify the claims yourself with the browser's developer tools.

The problem with "secure" file sharing

Nearly every file-sharing service says it's secure, and nearly every one means the same two things: TLS on the wire, and encryption at rest on their disks. Both are good. Neither stops the provider itself from reading your file — the provider holds the keys. Your contract, your passport scan, your unreleased build sits on someone else's server in a form that server can open, and the download page often pays for itself by showing ads next to it.

End-to-end encryption is the stronger claim: the file is encrypted before it leaves your device, with a key the service never receives. Then it doesn't matter much what happens server-side — a curious admin, a subpoena, a storage-bucket leak — what's there is ciphertext. DocuChan is built so that this is enforced by the design, not promised by a policy.

Two things travel in this story, and they never take the same road. Watch the colors: the key is blue, ciphertext is green.

The key never reaches us — by an old browser rule

When you upload a file, your browser generates a random 256-bit AES-GCM key using the Web Crypto API. That key ends up in the share link — but in a very particular place: the URL fragment, the part after the #.

docuchan.com/d/x7Kq0aH2sent to our server#Zk3vRw…kQ9M.mR8wSaltnever sent — stays in your browser
A DocuChan link. The path identifies the ciphertext; the fragment carries the decryption key (and the password salt, if set).

Fragments predate JavaScript — they were designed to jump to a heading within a page, so browsers were specified to never include them in HTTP requests. Every browser honors this. When a recipient opens the link, our server sees a request for /d/x7Kq0aH2 and nothing more; the JavaScript running on their device reads the fragment locally and decrypts there. This is the same construction Firefox Send used, and it means the "no backdoors" claim is a property of where the key travels, not of our good intentions.

How a file is encrypted

Files are encrypted in 8 MiB chunks, streamed: read a chunk, encrypt it, upload it, move on. Your browser never holds the whole file in memory, which is why a multi-gigabyte upload works on a low-RAM laptop or a phone. Each chunk gets its own random 12-byte IV and is sealed with AES-256-GCM; the wire format is simply [IV | ciphertext + auth tag], repeated.

The file is never encrypted "all at once": a wave of independent seals, each chunk with its own IV, streaming out as it goes.

GCM is an authenticated mode: every chunk carries a tag that the decryption verifies. If anyone flips a single bit of the stored ciphertext — a hostile storage provider, a man-in-the-middle, disk corruption — decryption fails loudly and the download refuses to produce a file. You can't silently receive a tampered document.

The filename gets the same treatment. It's metadata our server has no business reading, so it's encrypted client-side with the same per-file key before it's stored (as an e2e:-prefixed blob). On our side, your upload is a random id, an opaque label, and a byte count. The MIME type we store is application/octet-stream — we don't know if it's a PDF or a photo.

Where the bytes actually go

Two roads: the ciphertext goes up through storage; the key rides only in the link, person to person. They meet again in the recipient's browser — the only place the file exists in the clear.

One detail worth spelling out: encrypted chunks are uploaded directly to object storage with presigned URLs. Our application server authorizes the upload and hands out URLs, but file bytes never pass through it — there is no midpoint where plaintext could exist even by accident. The storage provider, in turn, stores ciphertext it has no key for. Downloads are the same path in reverse, decrypted in a stream as chunks arrive (on browsers without the File System Access API, through a service worker, so large files still don't balloon in memory).

Everything our server stores — the complete list

Honesty about metadata matters more than slogans about encryption, so here is the entire per-transfer record, verbatim from the schema:

  • a random, unguessable link id (also the storage object's name)
  • the encrypted filename label (opaque to us)
  • the ciphertext size in bytes
  • expiry time, burn-after-download flag, download counter
  • if you set a password: a PBKDF2 verifier hash — never the password (next section)
  • creation timestamp, and your account id if you were signed in

Operationally we also keep salted hashes of IP addresses with short retention for rate limiting and abuse control — not the addresses themselves — and standard web-server logs. Our analytics is self-hosted and cookieless and never sees a filename or link id. If we received a subpoena tomorrow, the sum total available about your file is the list above plus ciphertext. We can confirm that something was shared; we cannot produce what.

Passwords are a gate, not the key

You can put a password on a link. It's worth being precise about what that does: the password is not where the encryption key comes from — the key is in the fragment regardless. The password is an access gate: our server won't hand out the ciphertext without it. That protects you when a link leaks somewhere the password didn't (a forwarded email, a chat log, a screenshot).

We never learn the password either. Your browser derives a verifier from it with PBKDF2 (SHA-256, 210,000 iterations, random salt — the salt rides in the fragment next to the key), and only that verifier is stored and compared, in constant time, on download. What a database leak would yield is a slow-to-brute-force hash, not your password.

Self-destruct, precisely

One completed save, then nothing left to leak — not for us, not for anyone who finds the link later.

"Burn after download" deletes the file after the first completed download — the recipient's browser confirms the full save before the burn triggers, so a dropped connection at 90% doesn't destroy the file unread. A cancelled download doesn't burn it either. Timed links are deleted at expiry by a background janitor. Deletion means the ciphertext object is removed from storage and the link stops resolving; since only ciphertext ever existed on our side, there is no plaintext copy anywhere to chase.

Beam: when files shouldn't touch a server at all

For device-to-device sending (docuchan.com/beam), the file travels over a WebRTC data channel directly between the two browsers. Our server only relays the connection handshake (SDP/ICE — network coordinates, a few KB). But WebRTC's built-in DTLS encryption has a known weak spot: the party relaying the handshake could, in principle, sit in the middle. So Beam runs its own end-to-end layer on top, designed so that we are in the threat model:

  1. Each device generates an ephemeral ECDH P-256 key pair and first sends a commitment — a SHA-256 hash of its public key.
  2. Only after receiving the peer's commitment does each side reveal the actual key, which is checked against the commitment. Then both derive a shared AES-256-GCM session key.
  3. Both screens show a 6-character code (a Short Authentication String); the humans confirm it matches once, and can pin the device so next time is automatic.
The handshake, in order. Each side locks in a hash of its key before either key is shown — then the humans compare one short code.

The commit-before-reveal step is what makes the short code safe: a malicious relay would have to choose its substituted keys before seeing yours, so it can't grind for a pair of keys that shows the same code on both screens. It gets one blind shot in about a billion. This is the same construction ZRTP used to secure voice calls. When a direct connection is impossible and a relay (TURN) carries the traffic, the relay sees only this already-encrypted stream.

What we can't protect you from

A security page that lists no limitations is a marketing page. Here are ours:

  • You're trusting the JavaScript we serve. The encryption runs in code that arrives from our server on every page load. If we turned malicious, or were compelled or compromised, that code could be altered. This is true of every browser-based E2E tool — including the web versions of the encrypted messengers — and we'd rather say it than have you find it in a comment thread. Opening the client source for inspection is under active consideration; it's the honest mitigation.
  • No third-party audit yet. The design follows well-studied constructions (Firefox Send's fragment scheme, ZRTP-style SAS), but nobody independent has audited this implementation. When that changes, the report will be linked here.
  • The link is the secret. Anyone holding the full link (and password, if set) can decrypt the file. Share it over a channel you trust, and use burn-after-download so a leaked link is dead after first use.
  • Your device is out of scope. Malware or an over-the-shoulder camera defeats any encryption, ours included.
  • Lost keys are lost files. We cannot recover, reset, or regenerate a link's key — we never had it. This is the design working as intended, but it surprises people used to "forgot password" flows.

Found a hole? Tell us.

If you find a vulnerability, we genuinely want to hear it — that's not a platitude; scrutiny is the whole reason this page exists. Email abuse@docuchan.com with "Security" in the subject (also listed in security.txt). We read every report, respond, and credit finders who want credit. Please don't test against other people's files.

— Beki, builder of DocuChan · the friendlier walkthrough · try it with a real file

Security — how DocuChan actually works — DocuChan