Secure secret sharing for teams
Blog/Technical Tutorial

How We Built a Zero-Knowledge Secret Sharing Tool with Cloudflare in 5 Days

Keith from Cipher Projects12 min read
Building a zero-knowledge secret sharing tool with Cloudflare

We built VanishingVault as a zero-knowledge secret courier on Cloudflare Workers and KV in a five-day sprint: AES-256-GCM in the browser, keys only in the URL fragment, ciphertext with a 7-day TTL, and delete-on-read. This post is the architecture — what we stored, what we deliberately never saw, and the KV concurrency tradeoff.

Built by Cipher Projects. Related: what is zero-knowledge encryption · send a password as a one-time link.

Stack at a Glance

LayerChoiceWhy
CryptoWeb Crypto API · AES-256-GCMBrowser-native, no third-party crypto libs
Key transportURL fragment (#k=...)Never sent to server (RFC 9110)
Edge computeCloudflare WorkersBlind store + retrieve/delete API
StorageCloudflare KV + TTLGlobal, auto-expire (7-day max)
FrontendNext.jsEncrypt/decrypt only in the client

Peer open-source Cloudflare ZK drops often add Durable Objects for the burn path, D1 for metadata, and R2 for larger blobs (see patterns like BurnAfterRead). We kept Workers + KV only — minimum surface that still delivers client-side encryption and delete-on-read.

Threat Model (What ZK Does and Does Not Cover)

ThreatProtected?Notes
Provider or KV dump reads plaintextYesKey never leaves the fragment; server holds ciphertext only
TLS / network observer on API callsYes (for content)Sees ID + ciphertext, not the fragment key
Stolen share URL (full link with #)NoURL is a bearer secret — first opener wins
Malicious or compromised recipient deviceNoAfter decrypt, plaintext is on their machine
Two concurrent opens of a one-view secretBest-effort on KVNon-atomic get→delete; Durable Objects harden this

Hardening peers often add: strict CSP, rate limits on create/read, revoke tokens, and “paranoid” responses that always return not-found (no timing oracle for burned vs missing). Those are worthwhile if you fork the pattern; they are orthogonal to the fragment-key idea.

Technical Implementation

The design constraint was simple: the provider must never be able to read a secret, even with full infrastructure access. That rules out server-side encryption with provider-held keys. Encryption happens exclusively in the browser; the Worker is a courier for opaque blobs.

Architecture Overview

Two tiers, not three. The Next.js frontend owns all crypto via the Web Crypto API. Sensitive data becomes ciphertext before it leaves the device. The Cloudflare Worker receives encrypted blobs and stores them in KV without any ability to decrypt. If the entire backend were compromised, attackers would find ciphertext and IVs — not keys.

What Never Touches the Server

  • Plaintext secrets
  • AES encryption keys (URL fragment only)
  • User accounts or identity for anonymous shares

What does touch the server: ciphertext, IV, a random ID, and a TTL. That is the entire trust boundary.

Backend Implementation with Cloudflare Workers

Core store and retrieve (burn-after-read) handlers:

// Store endpoint - saves an encrypted secret
async function handleStore(request) {
  const { encryptedData, expiresAt } = await request.json();
  
  // Generate a random ID for the secret
  const id = crypto.randomUUID();
  
  // Store the encrypted data in KV with TTL
  await SECRETS_STORE.put(
    id,
    JSON.stringify({ encryptedData }),
    { expirationTtl: 604800 } // 7 days in seconds
  );
  
  return new Response(
    JSON.stringify({ id }),
    { headers: { 'Content-Type': 'application/json' } }
  );
}

// Retrieve endpoint - gets and deletes a secret (one-time access)
async function handleRetrieve(request, id) {
  // Get the secret from KV
  const secretData = await SECRETS_STORE.get(id);
  
  if (!secretData) {
    return new Response(
      JSON.stringify({ error: 'Secret not found or already viewed' }),
      { status: 404, headers: { 'Content-Type': 'application/json' } }
    );
  }
  
  // Delete the secret immediately (one-time access)
  await SECRETS_STORE.delete(id);
  
  return new Response(
    secretData,
    { headers: { 'Content-Type': 'application/json' } }
  );
}

Honest limitation: KV is not atomic

The get-then-delete pattern above is the standard Workers KV burn-after-read approach, but two concurrent requests can both read before either delete completes. Durable Objects serialize work per object (Cloudflare’s input gates and storage semantics), which is why several Cloudflare-based ZK drop projects put the burn path on a DO. We accepted the KV tradeoff for simplicity and latency; most password handoffs are not contested under parallel readers.

Frontend Encryption with Web Crypto API

On the frontend, we use the Web Crypto API (secure contexts only: HTTPS or localhost) to handle encryption and decryption — no third-party crypto libraries:

// Generate a random encryption key
async function generateEncryptionKey() {
  return window.crypto.subtle.generateKey(
    { name: "AES-GCM", length: 256 },
    true,
    ["encrypt", "decrypt"]
  );
}

// Encrypt data with the generated key
async function encryptData(key, data) {
  // Create a random initialization vector
  const iv = window.crypto.getRandomValues(new Uint8Array(12));
  
  // Encode the data as UTF-8
  const encodedData = new TextEncoder().encode(data);
  
  // Encrypt the data
  const encryptedBuffer = await window.crypto.subtle.encrypt(
    { name: "AES-GCM", iv },
    key,
    encodedData
  );
  
  // Convert the encrypted data and IV to base64
  const encryptedData = bufferToBase64(encryptedBuffer);
  const ivBase64 = bufferToBase64(iv);
  
  return { encryptedData, iv: ivBase64 };
}

// Decrypt data with the provided key
async function decryptData(key, encryptedData, iv) {
  // Convert base64 back to ArrayBuffer
  const encryptedBuffer = base64ToBuffer(encryptedData);
  const ivBuffer = base64ToBuffer(iv);
  
  // Decrypt the data
  const decryptedBuffer = await window.crypto.subtle.decrypt(
    { name: "AES-GCM", iv: ivBuffer },
    key,
    encryptedBuffer
  );
  
  // Decode the decrypted data as UTF-8
  return new TextDecoder().decode(decryptedBuffer);
}

Secret URL Generation

When a user creates a secret, we:

  1. Generate a random encryption key
  2. Encrypt the secret with this key
  3. Send the encrypted data to the server
  4. Receive a unique ID from the server
  5. Create a URL with the ID and the encryption key as a fragment

The encryption key is never sent to the server. Instead, it's added as a URL fragment (the part after the # symbol). Browsers do not include fragments in HTTP requests, so Workers and KV never receive the key — even in access logs.

// Create a secret URL
async function createSecretUrl(secret) {
  // Generate a random encryption key
  const key = await generateEncryptionKey();
  
  // Export the key to raw format
  const rawKey = await window.crypto.subtle.exportKey("raw", key);
  
  // Convert the key to base64
  const keyBase64 = bufferToBase64(rawKey);
  
  // Encrypt the secret
  const { encryptedData, iv } = await encryptData(key, secret);
  
  // Send the encrypted data to the server
  const response = await fetch('/api/store', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ encryptedData, iv })
  });
  
  const { id } = await response.json();
  
  // Create the secret URL with the key as a fragment
  return `${window.location.origin}/s/${id}#k=${keyBase64}`;
}

Privacy Benefits of Our Approach

Complete Privacy

Our zero-knowledge architecture ensures that even we cannot access your personal data.

Quick Implementation

The Cloudflare stack allowed us to deploy a privacy-focused solution in just 5 days.

No Data Retention

Secrets automatically vanish after being viewed, leaving no digital trail.

Deployment Process

Deploying our solution to production was straightforward thanks to Cloudflare's developer-friendly tools:

1. Setting Up the KV Namespace

# Create a KV namespace for secrets
wrangler kv:namespace create "SECRETS_STORE"

# Add the namespace to wrangler.toml
# kv_namespaces = [
#   { binding = "SECRETS_STORE", id = "your-namespace-id" }
# ]

2. Configuring CORS for Multiple Domains

Since we operate on two domains (vanishingvault.com and secretdropbox.com), we needed to configure CORS properly:

// CORS headers function
function corsHeaders(request) {
  // Get the origin from the request
  const origin = request.headers.get('Origin');
  const allowedOrigins = [
    'https://secretdropbox.com',
    'https://vanishingvault.com',
    'http://localhost:3000' // For development
  ];
  
  // Check if the origin is allowed
  const allowedOrigin = allowedOrigins.includes(origin) 
    ? origin 
    : allowedOrigins[0];
  
  return {
    'Access-Control-Allow-Origin': allowedOrigin,
    'Access-Control-Allow-Methods': 'GET, POST, DELETE, OPTIONS',
    'Access-Control-Allow-Headers': 'Content-Type',
    'Access-Control-Max-Age': '86400',
  };
}

3. Deploying the Worker

# Deploy the worker
wrangler deploy

# Configure custom domain routes
# In your Cloudflare dashboard:
# - Add a route pattern: secretdropbox.com/api/*
# - Add a route pattern: vanishingvault.com/api/*

Real-World Use Cases

Our zero-knowledge secret sharing tool is designed to address a variety of secure sharing needs:

  • Share sensitive personal information with family members
  • Securely transmit passwords to friends
  • Send private account details to trusted contacts
  • Share temporary access codes without leaving a trace

Frequently Asked Questions

How does zero-knowledge secret sharing work on Cloudflare?

The browser encrypts the secret with AES-256-GCM via the Web Crypto API before upload. Cloudflare Workers store only ciphertext in KV. The decryption key lives in the URL fragment after # and is never sent in HTTP requests, so Workers, KV, and logs never see it.

Why put the encryption key in the URL fragment?

URL fragments (#...) are client-side only. Per HTTP (RFC 9110), browsers do not include the fragment in requests to the server. The Worker therefore receives an opaque ID and ciphertext without the key — a database dump cannot decrypt secrets.

If someone steals the share URL, can they read the secret?

Yes. The fragment key travels with the link, so the URL is a bearer secret — whoever opens it first decrypts. Zero-knowledge protects against the provider and a stolen database; it does not protect against a leaked link. Confirm recipients out of band, avoid public channels, and rotate credentials if the wrong party may have viewed the link.

Does Web Crypto work offline or on plain HTTP?

The Web Crypto API is available in secure contexts only (HTTPS or localhost). Production secret-sharing UIs must be served over TLS. That is a browser platform rule, not a Cloudflare-specific limit.

Why Cloudflare Workers and KV for secret sharing?

Workers run at the edge with no long-lived app servers to patch. KV provides global, TTL-backed storage so ciphertext expires automatically (we use a 7-day max). That combination ships a zero-knowledge courier without managing VMs.

Is Cloudflare KV atomic for burn-after-read?

No. A get-then-delete on Workers KV is not a single atomic transaction. Two simultaneous reads of a one-view secret could theoretically both succeed. Durable Objects serialize access per object (input gates / storage semantics), which is why several open-source Cloudflare ZK drop tools use a DO for the burn path. We accepted the KV tradeoff for simplicity; most password handoffs are not contested under parallel readers.

What does the server actually store?

Only ciphertext, an IV, an ID, and a TTL. No plaintext, no encryption keys, no user accounts, and no view-identity logs. After the first successful retrieve — or after TTL — the KV entry is deleted.

Conclusion

VanishingVault gives privacy-conscious individuals a secure way to share sensitive information. By leveraging Cloudflare's edge infrastructure and client-side encryption, secrets stay under user control while delivery stays global and simple.

Compare the wider landscape in best one-time secret sharing tools.

Ready to protect your personal information? Try VanishingVault today.

Get Started