Secure secret sharing for teams
Blog/Security

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

Keith from Cipher Projects11 min read
Browser Web Crypto AES-256-GCM encrypting a secret before ciphertext upload to the server

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.

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?

1

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.

2

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.

3

Encrypt plaintext locally

subtle.encrypt({ name: "AES-GCM", iv }, key, plaintextBytes) returns ciphertext + auth tag. Only ciphertext and IV go in the upload payload.

4

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).

5

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?

ApproachBest forHonest limit
Browser AES-256-GCM + fragment key (ZK courier)One-time password/API key handoffs; provider-blind storageTrust 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 pagesServer sees plaintext at termination; logs and DB hold secrets
Password manager E2EE syncOngoing team vaults with accounts and rotationHeavier 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?

DataOn server?Notes
Plaintext secretEncrypt before upload
AES-256 keyFragment only
Ciphertext + auth tagOpaque blob
IV / noncePublic alongside ciphertext
Opaque secret IDPath segment / KV key
TTL / expiry7-day max unopened on VanishingVault
Viewer identityZK 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-referrer on 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?

PatternCryptoProvider blind?Best for
VanishingVaultBrowser AES-256-GCM; key in # fragmentGuest/contractor one-shot credentials; no accounts
Bitwarden SendClient AES-256 (vault crypto)Teams already on Bitwarden; optional access password
Typical hosted Password PusherServer-side encryption at restQuick drop when you trust the host
PGP emailOpenPGP (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