Encrypted document exchange. Files are encrypted in your browser with AES-256-GCM before they leave your device. The server stores only ciphertext. The decryption key exists only in the URL you share.
- Drop a file. It's encrypted in your browser.
- The encrypted blob is uploaded to the server. The key is not.
- You get a link:
https://vault.example.com/d/abc123#K7xP2mN9qR4sT6vW - The part after
#is the encryption key. It's never sent to the server. - The recipient opens the link. Their browser downloads the ciphertext and decrypts it locally.
- The server deletes the blob after download (or after expiry).
| Component | Choice | Rationale |
|---|---|---|
| Encryption | AES-256-GCM (Web Crypto API) | Only native AEAD in browsers. Hardware-accelerated. |
| Key | 128-bit random (crypto.getRandomValues) |
In the URL fragment. Exceeds GCM birthday bound. |
| Key expansion | HKDF-SHA256 | Derives encryption key from URL key. Clean key separation. |
| Nonce | 96-bit random per file | One key per file = zero nonce reuse risk. |
| Password (optional) | Authentication only, not encryption | Server-side gate. Doesn't weaken encryption if absent. |
Reference: Bitwarden Send uses the same key delivery model.
- The encrypted blob (ciphertext)
- File size
- Upload/download IP addresses and timestamps
- Whether a password was set (not the password itself)
- The file contents
- The file name (encrypted client-side)
- The encryption key (only in the URL fragment)
- The password (only a hash)
These apply to all browser-based encryption tools. Most don't state them.
-
The server delivers the code. A compromised server could serve modified JavaScript that captures keys. For high-assurance use, verify against the source code.
-
The key is in the URL. Browser history, cloud sync, and extensions with page access can read it.
-
PBKDF2 is GPU-friendly. If you add a password, it's authentication only. The encryption key is the random URL value.
-
File size is visible. Ciphertext reveals plaintext size (to within 28 bytes).
-
IP addresses are visible. The server sees uploader and downloader IPs.
-
Deletion is best-effort. Storage backups and browser caches may retain copies.
-
JavaScript can't wipe memory. Keys persist in heap until garbage collection.
-
Extensions can read everything. A browser extension with page access can read the key and decrypted content.
-
Screenshots exist. Once decrypted, content can be screen-captured.
-
Abuse is possible. Zero-knowledge means no content scanning. Rate limits and takedowns are the only mitigation.
Full research: /research/
- Frontend: Vanilla TypeScript, Vite, Web Crypto API (zero external crypto deps)
- Backend: Cloudflare Workers (Hono), R2 (blob storage), D1 (metadata)
cd apps/web && npm test23 tests covering key generation, HKDF derivation, encrypt/decrypt round-trips (empty, 1 byte, 1MB), failure modes (wrong key, tampered data, truncated ciphertext), base64url encoding, and memory wiping.
MIT