Zero-Knowledge Encryption Explained for Developers
"Zero-knowledge" is a phrase every privacy-focused SaaS uses, and most of them shouldn't. The term has a specific technical meaning that's been diluted to the point of marketing noise. This article is the version I wish I'd read when I first encountered it — written for developers who want to understand the actual cryptographic property, evaluate whether a vendor's claim holds up, and implement it correctly if building their own system.
If a service claims zero-knowledge encryption and can answer "what's in my data?" with anything other than "we have no idea," they're misusing the term. Here's how to tell the difference.
What zero-knowledge actually means
In its strict cryptographic sense, zero-knowledge proof is a protocol where a prover convinces a verifier that a statement is true without revealing anything beyond the truth of the statement itself. That's not what most consumer products mean when they say zero-knowledge.
In SaaS context, zero-knowledge has come to mean: the service operator cannot read user data, even if compelled by subpoena, and even if their entire infrastructure is breached. The operator stores ciphertext. The decryption key never reaches them.
This is a property of the system, not a marketing claim. It's verifiable — you can inspect what gets sent over the wire and confirm whether the operator has the necessary inputs to decrypt.
The four common encryption models, ranked by trust required
Most "encrypted" SaaS falls into one of four buckets:
Model 1: Encryption in transit only. TLS between client and server. Server stores plaintext. The operator can read everything. Calling this "encrypted" is technically true and practically meaningless.
Model 2: Encryption at rest. Server encrypts data with a key the server holds. The operator can decrypt at will. AWS RDS encryption, S3 SSE — useful against a stolen disk, useless against a malicious operator or a subpoena.
Model 3: Server-side encryption with user-supplied key. Client sends both data and key to the server. The server encrypts and stores ciphertext. In transit, the operator briefly held both. Many "end-to-end encrypted" services live here. Better than Model 2, but still requires trusting that the operator doesn't log the key as it passes through.
Model 4: True client-side encryption (zero-knowledge). Client encrypts before transmission. Only ciphertext leaves the client. The decryption key never traverses the network or touches the server. The operator literally cannot decrypt — not "won't," can't.
Only Model 4 deserves "zero-knowledge."
How a real zero-knowledge system works
The architecture pattern that makes Model 4 possible:
- The client generates an encryption key in the browser using
crypto.subtle.generateKey(). - The client encrypts the data using AES-256-GCM (or a similar AEAD cipher) with that key.
- The client uploads only the ciphertext to the server.
- The server returns an opaque ID identifying the stored ciphertext.
- The client constructs a URL of the form
https://service.com/share/{id}#{key}. - The fragment portion (after
#) is never sent in HTTP requests by browsers. It stays client-side. - Recipient receives the URL. Their browser fetches
https://service.com/share/{id}(no key in this request). - The server returns ciphertext.
- The recipient's JavaScript reads the fragment from
window.location.hash, decrypts locally.
The operator sees: an opaque ID and ciphertext. The operator never sees: the key, the plaintext, or anything that would let them recover either.
A working example
Here's a minimal version of the encrypt-and-share flow:
// Sender side
async function encryptAndShare(plaintext) {
// Generate a fresh key
const key = await crypto.subtle.generateKey(
{ name: 'AES-GCM', length: 256 },
true,
['encrypt', 'decrypt']
);
// Random IV per encryption
const iv = crypto.getRandomValues(new Uint8Array(12));
// Encrypt
const ciphertext = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv },
key,
new TextEncoder().encode(plaintext)
);
// Upload ciphertext + IV only
const response = await fetch('/api/store', {
method: 'POST',
body: JSON.stringify({
ciphertext: bufferToBase64(ciphertext),
iv: bufferToBase64(iv)
})
});
const { id } = await response.json();
// Export key, put it in the URL fragment
const exportedKey = await crypto.subtle.exportKey('raw', key);
const keyBase64 = bufferToBase64(exportedKey);
return `https://service.com/share/${id}#${keyBase64}`;
}
Recipient side:
async function fetchAndDecrypt() {
const url = window.location;
const id = url.pathname.split('/').pop();
const keyBase64 = url.hash.slice(1); // never sent to server
// Fetch ciphertext (no key in this request)
const { ciphertext, iv } = await fetch(`/api/fetch/${id}`).then(r => r.json());
// Reconstruct key
const key = await crypto.subtle.importKey(
'raw', base64ToBuffer(keyBase64),
{ name: 'AES-GCM' }, false, ['decrypt']
);
// Decrypt
const plaintext = await crypto.subtle.decrypt(
{ name: 'AES-GCM', iv: base64ToBuffer(iv) },
key,
base64ToBuffer(ciphertext)
);
return new TextDecoder().decode(plaintext);
}
The server in this flow has only two endpoints: store ciphertext, retrieve ciphertext. It doesn't see keys. It can't decrypt.
How to verify a vendor's zero-knowledge claim
If a service claims zero-knowledge, you can audit it:
Step 1: Open DevTools, watch the network tab during share creation. If you see the plaintext or the encryption key in the request body, it's not zero-knowledge. The only thing that should leave the browser is ciphertext and an IV.
Step 2: Check whether the URL contains a fragment. If the share URL is something like service.com/abc123 with nothing after #, the key has to be stored on the server somewhere — meaning the operator can decrypt. A real zero-knowledge service has the key in the fragment: service.com/abc123#aBc....
Step 3: Read the architecture documentation. Every legitimate zero-knowledge service publishes their model. If the vendor's security page is full of "military-grade encryption" and zero technical details, be skeptical.
Step 4: Inspect the JavaScript. Open the page source. The encryption code is client-side; you can read it. Look for crypto.subtle.encrypt calls and verify the data being encrypted is the user's plaintext, not just metadata.
Limitations of zero-knowledge architectures
Honesty: zero-knowledge systems aren't a panacea. Real limitations:
- Recipient compromise still leaks data. If the recipient's browser is compromised when they decrypt, the plaintext is exposed. Zero-knowledge protects against operator/server compromise, not endpoint compromise.
- No server-side search or sharing. The operator can't help you with "I lost my link" because they have no way to find or decrypt your data.
- Metadata can still leak. Timestamps, file sizes, IP addresses, request patterns. Zero-knowledge encrypts content, not the fact that you used the service.
- JavaScript delivery is the weak point. If the operator's frontend is compromised and serves malicious JS, the encryption can be subverted at runtime. Mitigated by code signing, subresource integrity, and reproducible builds — but the operator could in theory MITM their own users.
These aren't reasons to dismiss zero-knowledge — they're reasons to understand its scope. It defeats one threat model (operator/server compromise) very strongly. It does not defeat all threats.
Where SnapSend fits in
SnapSend is a zero-knowledge secret sharing tool that you can audit yourself using the steps above. Open DevTools, share a secret, and you'll see only ciphertext and an IV in the request body. The encryption key lives in the URL fragment, never transmitted to the SnapSend server. The Web Crypto API does the encryption — no homegrown crypto, no proprietary protocols.
If you're evaluating zero-knowledge claims and want a reference implementation to compare against, SnapSend is open to inspection. We welcome scrutiny.
Build with the right model
If you're building a system that handles user secrets, default to Model 4. It's harder to design — you give up server-side features in exchange for the cryptographic guarantee — but it's the only model that lets you tell users "we can't read your data" and mean it.
Try SnapSend free at snapsend.site — no account needed.