AES-256-GCM in the Browser for Zero-Knowledge Secret Sharing (2026)

TL;DR
Zero-knowledge one-time secret sharing uses Web Crypto AES-256-GCM in the browser: generate a random key, encrypt locally, upload ciphertext + IV only, put the key in the URL # fragment (never sent to the server), burn ciphertext after one view. GCM gives authenticated encryption; secure contexts (HTTPS) are required. Honest limits: XSS, bearer URLs, and chat unfurls — not provider-side decryption. VanishingVault implements this pattern on Cloudflare Workers + KV.
Related: how we built zero-knowledge secret sharing on Cloudflare · what is zero-knowledge encryption? · send a password as a one-time link · Slack unfurl burns one-time secrets.
How Does AES-256-GCM Fit the One-Time Secret Courier Threat Model?
The job is not long-term storage — it is move a credential once, then destroy it. The adversaries you care about:
- Provider compromise or database dump (should yield unreadable blobs)
- Network observers (should see only TLS + ciphertext)
- Chat/email retention (should hold a dead link, not plaintext)
AES-256-GCM in the browser addresses the first two when the AES key never reaches your backend. The third is policy + burn-after-read. Generic Web Crypto tutorials rarely tie cipher choice to this courier model — this page does.
Client encrypts
Plaintext exists in JS memory only during create/reveal — never in your DB.
Server stores ciphertext
Workers/KV hold IV + ciphertext + ID + TTL. No keys.
Key in fragment
Recipient browser reads # locally; HTTP requests omit it.
What Does the Browser Encryption Flow Look Like?
Generate key material (Web Crypto)
window.crypto.subtle.generateKey({ name: "AES-GCM", length: 256 }, true, ["encrypt", "decrypt"]) produces a fresh AES-256 key per secret. Export for the URL fragment only when needed.
Generate a random IV (96 bits for GCM)
crypto.getRandomValues(new Uint8Array(12)) — one IV per encryption. With a unique key per secret, random IVs are sufficient. Never reuse IV under the same key.
Encrypt plaintext locally
subtle.encrypt({ name: "AES-GCM", iv }, key, plaintextBytes) returns ciphertext + auth tag. Only ciphertext and IV go in the upload payload.
Upload ciphertext; key stays client-side
POST opaque blob to your API. Build share URL as https://host/s/{id}#{base64url-key}. The fragment is not sent on fetch (RFC 9110).
Recipient decrypts; server burns
Browser parses fragment, GET ciphertext by ID, subtle.decrypt locally, then server deletes KV entry on successful retrieve (or after TTL).
Why AES-GCM — and Why Not Server-Side AES?
| Approach | Best for | Honest limit |
|---|---|---|
| Browser AES-256-GCM + fragment key (ZK courier) | One-time password/API key handoffs; provider-blind storage | Trust the front-end; bearer URL; XSS is game-over |
| Server-side AES (provider-held key) | Platform secrets your infra reads (Workers bindings, DB encryption at rest) | Provider and DB admins can decrypt — wrong model for user-shared credentials |
| TLS only (no app-layer crypto) | Public web pages | Server sees plaintext at termination; logs and DB hold secrets |
| Password manager E2EE sync | Ongoing team vaults with accounts and rotation | Heavier UX; both parties often need org accounts |
GCM specifically: authenticated encryption (AEAD) — tampering with ciphertext fails decrypt. Native in Web Crypto. ChaCha20-Poly1305 is a fine alternative where you control the client matrix; for a public one-time link tool, AES-GCM maximizes browser reach.
What Must Developers Get Right About IVs and Nonces?
GCM security collapses if you reuse an IV with the same AES key. Rules we ship by:
- One random AES key per secret — generated in the browser, never derived from a short password unless you add an explicit KDF path.
- One random 96-bit IV per encryption — store alongside ciphertext; include in the decrypt call.
- Never deterministic IVs from counters alone across users without key separation.
- Do not truncate auth tags — Web Crypto handles the 128-bit tag as part of GCM output.
If you ever reuse a static key (e.g. derived from a site-wide secret — don't), IV uniqueness becomes your only lifeline. Per-secret random keys are simpler and safer for one-time couriers.
Why Does the Key Live in the URL Fragment?
HTTP requests include path and query — not the fragment. Per RFC 9110, the fragment identifier is processed client-side after the resource is retrieved. That means:
- Your Worker logs see
GET /s/abc123— not the AES key. - Referrers and analytics tags should strip fragments (also set Referrer-Policy).
- The URL is a bearer secret — treat link leakage as key leakage.
This is the same fragment-key pattern documented in how we built zero-knowledge secret sharing on Cloudflare. It is what makes KV/Worker storage zero-knowledge despite hosting ciphertext.
What Does the Server Store — and What Never Touches It?
| Data | On server? | Notes |
|---|---|---|
| Plaintext secret | Encrypt before upload | |
| AES-256 key | Fragment only | |
| Ciphertext + auth tag | Opaque blob | |
| IV / nonce | Public alongside ciphertext | |
| Opaque secret ID | Path segment / KV key | |
| TTL / expiry | 7-day max unopened on VanishingVault | |
| Viewer identity | ZK design — no accessor logs |
What Are the Honest Limits Developers Should Document?
XSS / compromised front-end
Malicious JS can read plaintext before encrypt or exfiltrate the fragment key. Mitigate with strict CSP, Subresource Integrity, minimal dependencies, and HTTPS only. ZK does not fix a trojaned bundle.
Bearer URL semantics
First opener wins. Intercepted links, shoulder surfing, or wrong DM thread = credential disclosure. Optional passphrase on a second channel helps; rotate after handoff.
Burn concurrency
get-then-delete on eventually consistent KV is not atomic. Two parallel retrieves might both succeed. Durable Objects serialize per secret if you need strict single-view — we document the tradeoff in our Cloudflare post.
Link preview / unfurl burn
Not a cipher problem — a retrieve-on-GET problem. Use Reveal hardening. See Slack unfurl burns one-time secrets.
Secure Contexts, Web Crypto, and Production Checklist
crypto.subtle is undefined on plain HTTP (except localhost). Production secret UIs must be served over TLS. Checklist we use before shipping crypto changes:
- HTTPS everywhere; HSTS on the marketing + app host
- CSP with tight
script-src; avoid inline script where possible Referrer-Policy: no-referreron reveal pages (fragment leakage)- Random key + random IV per secret; no home-grown CBC+HMAC
- Reveal step before burn API (unfurl resistance)
- Unit tests for encrypt/decrypt round-trip and tampered ciphertext rejection
- Document bearer URL + no recovery in FAQ (users will ask)
How Does Browser GCM Compare to Other Secret-Sharing Patterns?
| Pattern | Crypto | Provider blind? | Best for |
|---|---|---|---|
| VanishingVault | Browser AES-256-GCM; key in # fragment | Guest/contractor one-shot credentials; no accounts | |
| Bitwarden Send | Client AES-256 (vault crypto) | Teams already on Bitwarden; optional access password | |
| Typical hosted Password Pusher | Server-side encryption at rest | Quick drop when you trust the host | |
| PGP email | OpenPGP (often RSA/ECDH + AES) | Recipients with established keys; high friction |
Conceptual foundation: what is zero-knowledge encryption?. End-user steps: send a password as a one-time link.
Frequently Asked Questions
How does AES-256-GCM in the browser work for zero-knowledge one-time secret sharing?
The browser generates a random 256-bit AES-GCM key via the Web Crypto API, encrypts the plaintext locally, and uploads only ciphertext plus IV. The AES key is placed in the URL fragment after # — fragments are not sent in HTTP requests (RFC 9110). The server stores opaque blobs; decryption happens in the recipient’s browser after fetch. On first successful retrieve, ciphertext is deleted (burn-after-read).
Why AES-GCM instead of AES-CBC or ChaCha20-Poly1305 for secret sharing?
AES-GCM is widely available in Web Crypto across modern browsers, provides authenticated encryption (integrity + confidentiality in one primitive), and is the default choice for client-side secret couriers. ChaCha20-Poly1305 is excellent where supported but has narrower legacy browser coverage. AES-CBC requires a separate MAC and is easier to mis-implement. For a one-shot password handoff, GCM’s AEAD properties and Web Crypto support win.
Why not encrypt on the server with a provider-held key?
Server-side AES with a provider key means the service (and anyone who compromises it or receives legal process) can decrypt. That defeats zero-knowledge. Client-side AES-256-GCM with the key only in the URL fragment keeps the provider storing ciphertext it cannot read — the threat model you want for one-time credential couriers.
What is the IV (nonce) and why must it be unique per secret?
GCM requires a unique IV (nonce) for each encryption under the same key. Reusing an IV with the same AES key in GCM is catastrophic — it leaks authentication keys and plaintext XORs. For one-time links you typically generate a fresh random AES key per secret, so a random 96-bit IV per message is standard. Never reuse IV+key pairs.
Does Web Crypto work on HTTP or offline?
Web Crypto’s subtle crypto interface is available in secure contexts only: HTTPS or localhost. Plain HTTP pages cannot use it for production encryption. Offline encryption works once the page is loaded over TLS; upload still needs network.
What does the server actually store?
Only ciphertext, IV, an opaque ID, and TTL metadata. No plaintext, no AES keys, no user accounts. VanishingVault stores those blobs in Cloudflare KV and deletes on first retrieve or after a 7-day TTL — see our Cloudflare architecture post for the full stack.
What are the honest limits of browser AES-256-GCM secret sharing?
Three big ones: (1) XSS or a compromised front-end can read plaintext before encryption or steal the fragment key; (2) the share URL is a bearer secret — whoever opens it first decrypts; (3) burn-after-read is not always atomic on eventually consistent stores (parallel readers). Zero-knowledge protects against provider compromise and database dumps, not malicious JavaScript or leaked links.
Can link previews burn a GCM-encrypted one-time secret?
The encryption is fine — the burn is the problem. If Slack or mail scanners trigger retrieve-on-GET, ciphertext is deleted before the human clicks. Mitigate with a Reveal step so unfurls hit a landing page, not the burn endpoint. See our Slack unfurl explainer.
Should developers use PBKDF2 or Argon2 with AES-GCM for one-time links?
For random per-secret keys (generated in the browser and placed in the fragment), KDFs are unnecessary — the key already has full entropy. KDFs matter when deriving keys from user passwords (optional second factor on the link). If you add passphrase protection, use Argon2id or PBKDF2 with a random salt and send the passphrase on a separate channel.
How does this relate to zero-knowledge encryption?
Zero-knowledge here means the provider never holds a usable decryption key — keys stay client-side (in the URL fragment). AES-256-GCM is the cipher; the architecture (fragment key, ciphertext-only storage, burn-after-read) is what makes the courier zero-knowledge. Read what is zero-knowledge encryption for the broader definition.
See AES-256-GCM zero-knowledge sharing in production
VanishingVault encrypts in your browser, stores ciphertext only, and burns after one Reveal — no account required.
Try VanishingVault