Summary
- End-to-end encryption. Secrets are encrypted in the sender's browser with AES-256-GCM and decrypted in the recipient's browser. The key travels only in the link's URL fragment, which browsers do not send to servers.
- Zero-knowledge storage. The server stores ciphertext, a random IV, KDF parameters and a one-way verifier. It never receives plaintext, link keys or passphrases, and cannot decrypt what it stores.
- Exactly-once redemption. Opening a one-time secret locks its row, checks state and expiry, marks it consumed and deletes the ciphertext in a single database transaction. Concurrent requests cannot both succeed.
- Wrong passphrases don't consume secrets. The server can check a passphrase-derived proof without learning the passphrase. After 10 failed attempts (configurable 3–20) the secret is destroyed to stop online guessing.
Cryptographic design
| Step | Construction |
|---|---|
| Link key | 32 bytes from the browser's CSPRNG (crypto.getRandomValues), unique per secret |
| Passphrase stretching | Argon2id, 19 MiB memory, 2 iterations, 1 lane, 16-byte random salt (parameters stored per secret, bounded server-side) |
| Key derivation | HKDF-SHA256 over link key ‖ Argon2id output, with distinct info labels for the encryption key and the redemption proof |
| Encryption | AES-256-GCM, 96-bit random IV, 128-bit tag, fixed associated data; plaintext padded to 256-byte buckets |
| Redemption proof | Server stores SHA-256(proof); the recipient sends the proof; comparison is constant-time |
All primitives come from the browser's Web Crypto API, except Argon2id, which uses the audited @noble/hashes library. No custom ciphers are used. The full design and its analysis are in docs/encryption.md in the repository.
What the server can see
| Data | Visible to VaultDrop? |
|---|---|
| Secret content | No — ciphertext only |
| Link key, passphrase | No |
| Approximate size | Yes, rounded up to 256 bytes |
| Creation, expiry, open and destroy times; view count | Yes |
| Whether a passphrase is required | Yes |
| Private labels (signed-in users) | Yes — labels are not encrypted; never put secrets in them |
| IP addresses | Used transiently for rate limiting as keyed pseudonyms; not stored raw by VaultDrop. Session records keep the sign-in IP for your security page. |
Threat model
Database or backup disclosure
An attacker with a full database copy gets ciphertext without keys. Passphrase-protected secrets additionally require the link key before any guessing is possible.
Network interception
Production deployments must use HTTPS with HSTS. Even a TLS-terminating proxy sees only ciphertext, because keys stay in the URL fragment.
Enumeration and brute force
Identifiers are 128-bit random. Redemption requires a proof derived from the 256-bit link key. Requests are rate-limited per client and per secret, and failed proofs are capped per secret.
Race conditions and replay
Redemption uses a row lock plus a conditional update on the observed state and view count. A replayed proof after consumption receives "already opened". Tests run 25 concurrent redemptions and assert exactly one success.
Cross-site scripting and CSRF
A per-request nonce-based Content Security Policy allows only our own scripts; no third-party scripts, fonts or analytics are loaded. Cookie-authenticated state changes require a same-origin request. Session cookies are HTTP-only and SameSite=Lax.
Accidental logging
Structured logs redact sensitive fields, strip URL fragments and replace secret identifiers in paths. Ciphertext, proofs, keys and tokens are never logged by design.
Insider access
Operators with database access still cannot decrypt secrets. The admin area has no capability to read secret contents or recover destroyed ones.
Deletion and its limits
The encrypted payload row is deleted in the same transaction that consumes, expires or destroys a secret. PostgreSQL may retain deleted data in free pages, the write-ahead log, replicas and backups until those are recycled; backups are kept according to the operator's retention policy. Because that data is ciphertext without keys, residual copies are not readable — but we do not claim instantaneous physical erasure everywhere.
Known limitations
- Anyone who obtains the full link (and passphrase, if any) before the intended recipient can open the secret. The receipt shows when it was opened.
- Browser-based cryptography depends on the integrity of the code served to you. A compromised server could serve malicious JavaScript; the CLI avoids this risk.
- Screenshots, copying and malware on the recipient's device are outside VaultDrop's control.
- Ciphertext size reveals the secret's length to within 256 bytes.
Reporting a vulnerability
Please report security issues privately — see the contact page or /.well-known/security.txt. We ask that you give us reasonable time to fix issues before public disclosure.