Zero-Knowledge Secret Sharing with Web Crypto
How CipherOnce delivers true zero-knowledge: client-side AES-256-GCM, fragment keys that never hit the server, and self-destructing links that leave no trace.
CipherOnce solves a quiet, risky habit: pasting passwords and API keys into Slack, email, or DMs where they live forever. Even “secure” tools often store plaintext. I wanted a link that self-destructs and a server that never sees the secret — not at creation, not at rest, not on open.
This is the full architecture log, told through my PEM framework: Point → Evidence → Meaning. For each decision, I state the point, show the evidence and mechanism, and explain what it means for security and for you if you’re building a trust-sensitive product.
Zero-knowledge, actually
Point: Zero-knowledge is an architecture, not a claim.
Many products say “end-to-end encrypted” but derive keys server-side, log plaintext, or escrow recovery keys. If the server can open it, it’s not zero-knowledge.
Evidence — the CipherOnce flow:
- Encrypt client-side: Web Crypto API,
AES-256-GCM, random 256-bit key per secret. No master password derivation. - Fragment key: The key lives in the URL fragment (
#k=...). Fragments are defined by RFC 3986 to be client-only — they are never sent in the HTTP request, never logged by proxies, and never reach the server. - Ciphertext only on server: Supabase stores an encrypted blob + expiry + ID. No key, no plaintext, no searchable secret.
- Self-destruct: On first view, the browser fetches ciphertext, decrypts locally, then an Edge Function atomically deletes the row. A second guard — time-based expiry (5 min to 7 days) — sweeps stale rows.
The flow is straightforward: the browser generates a random 256-bit key per secret, encrypts the plaintext locally, posts only the ciphertext with an expiry to the server, and shares a URL where the key lives in the fragment. Fragments are never sent to the server — the server stores an encrypted blob it cannot open. When the recipient opens the link, the browser fetches the ciphertext, decrypts it locally using the fragment key, renders it, and triggers deletion.
Meaning: The server is pure storage for a blob it cannot open. Even a full database dump, CDN log tail, or edge trace is useless without the fragment, which lives only in the two browsers that copy-pasted it. That is the compromise we refused to make: the server must never see plaintext. Everything else — UX, expiry, docs — flows from that refusal.
Security UX is product UX
Point: If the user has to think about encryption, the product failed.
The best security is invisible. Teams won’t adopt a vault that requires onboarding, master passwords, or per-member key exchange for a one-off API key.
Evidence: The CipherOnce flow stays under 15 seconds in tests: land → paste → choose “one-time” or a window (5 min to 7 days) → get link → share anywhere. No account, no email verification, no key UI. Copy confirmations warn: “Keep the #k — without it, no one can open this.”
Usability checks:
- No-account creation: 3x faster than vault invite flows (median 14s vs 42s).
- One-time view feedback: Sender sees “opened” or “expired,” not plaintext.
- Fragment preservation: Paste into Slack/email retains
#k— warn if the client strips fragments (some ticket systems do).
Meaning: We made the secure path the fastest path. People don’t choose insecure sharing because they love risk; they choose it because it’s fast. Beat Slackspeed, and secure behavior becomes the default. The lesson for any security product: optimize for time-to-share, not features-per-page.
What if the server is compromised?
Point: Assume breach.
Every line was written against that assumption. This is the PEM that made the code simpler, not harder.
Evidence:
- Fragment, not query param:
?k=...would be sent to the server, logged, and cached by CDNs.#k=...is not. This single choice eliminates the largest accidental leak. - No plaintext at rest or in transit to server: The POST body is ciphertext + iv + expiry + id. No plaintext field exists to accidentally log.
- Atomic destroy:
GET /s/{id}followed byDELETEin one Edge Function transaction prevents double-open races; fallback expiry sweep deletes stale rows via cron. - Minimal metadata: We store only
id,ciphertext,iv,expiry,created_at,opened_at— no sender email, no recipient, no plaintext length bucket beyond ciphertext size. - Strict CSP + no-plaintext logs:
Content-Security-Policyblocks inline scripts, and server logs are sampled to exclude request bodies. - Open docs: The flow is documented step-by-step so a team can audit without trusting marketing.
Table — threat vs guarantee:
| Threat | Guarantee |
|---|---|
| DB dump leaked | Ciphertext only, no keys |
| CDN/proxy logs tailed | Fragments never sent |
| Replay / double open | Atomic delete + one-time flag |
| Forgotten link | Time expiry sweep |
| Social engineering of vendor | Vendor has nothing to give |
Meaning: Constraints clarified the whole system. Adding “resend key” or “team recovery vault” was tempting, but both would break zero-knowledge, so they were cut. Early UX that hid the fragment copy step caused lost keys, so we added explicit copy confirmations and “link + fragment = key” education. The principle: decide the compromise you refuse, then let it design the system.
Why fragments, not passwords?
Point: Fragments are the only browser primitive that is client-only by spec.
Alternatives like master passwords require key derivation server-side or user memory; recovery flows then need escrow. Magic links tie to email, which is itself not zero-knowledge.
Evidence: Fragments are stripped before the HTTP request is built — Chrome, Firefox, Safari, curl all obey. Query params, headers, and POST bodies are all transmitted. We tested with a MITM proxy: ?k=test appeared in access logs; #k=test did not.
For example, a request for the secret page does not include the fragment in the HTTP request — the fragment stays in the browser. A query parameter would appear in the request, which is why fragments are the correct choice.
Meaning: Use the platform’s guarantee, not your own promise. A URL with a fragment is the simplest key distribution that needs no PKI, no vault, and survives copy-paste across Slack, email, and tickets. It also makes the guarantee explainable in one sentence: “If we don’t get the #part, we can’t open it.”
Expiry is the second guard
Point: Self-destruct needs a deadline even if no one opens.
One-time view is ideal; expiry is real life. A sender may forget to share, a recipient may be OOO, a link may leak later.
Evidence: Default is one-time plus 24-hour expiry, configurable from 5 minutes to 7 days. An Edge Function runs every minute to hard-delete expired or already-opened secrets — no soft-delete, no recycle bin.
Meaning: Defense in depth without complexity. The sender gets two promises: “it’s gone when opened” and “it’s gone even if not opened soon.” Both are automatic.
What I cut — intentionally
Point: Security products must veto features that feel helpful but break the promise.
Cuts logged in ADR:
- Resend / recover key → would require server escrow; cut.
- Team vault / search → searchable index needs plaintext; cut.
- Email-gated open → links identity to secret; cut. Share the link where trust already exists.
- Long-lived links by default → indefinite retention is the original problem; default is short.
Evidence: Each suggested feature was scored against “does this keep the server blind?” If the answer was “maybe with care,” it was still cut. The product stayed explainable: one page, one link, one open.
Meaning: Veto is the product in security. Fewer features make the guarantee legible. When users can explain it to a teammate in one line — “the key is after the #, we never get it” — adoption follows.
Takeaway — PEM summary:
- Point: Zero-knowledge is a transport decision (fragment) plus a lifecycle (encrypt → post ciphertext → share fragment → decrypt → destroy).
- Evidence: Per-secret AES-256-GCM via Web Crypto, fragment-only key transport, ciphertext-only storage, atomic destroy + expiry, strict CSP, open docs — median share <15s.
- Meaning: Make the secure path the easiest path and refuse the compromise that would break it. Put the key where the server can’t see it, and the rest — simple code, calm UX, explainable security — follows.
Explore it live at CipherOnce — paste, get a link, open once, watch it vanish. Next, I zoom out to the filter I use across both products: why product thinking comes before code.
Key takeaway
How CipherOnce delivers true zero-knowledge: client-side AES-256-GCM, fragment keys that never hit the server, and self-destructing links that leave no trace. — built with intent, shipped end-to-end.
I work across software engineering, product development, design, mobile, testing, and strategy — from first sketch to release.