Zero-Knowledge Encryption Architecture โ
Date: January 4, 2026
Status: โ
Implemented (Frontend Complete)
The Problem We Solve โ
Question: "How can we be sure that everything is appropriately encrypted? Like if someone asked us to hand over user information, could we literally not provide it?"
Answer: YES. With zero-knowledge encryption, Lantern CANNOT decrypt user data even if we wanted to.
How It Works โ
Traditional (Vulnerable) Approach โ โ
User signup โ Backend generates encryption key โ Stores key + encrypted data
โ
If hacked or subpoenaed,
attacker gets BOTH key and data
= Full data breachZero-Knowledge (Secure) Approach โ โ
User creates passphrase โ Passphrase stays on device โ Derives encryption key locally
โ
Encrypts data client-side
โ
Backend receives: encrypted blob + salt
Backend NEVER sees: passphrase or key
โ
Even with court order, we only have:
- Gibberish (encrypted data)
- Salt (public, useless alone)
= Cannot decrypt without user's passphraseCryptographic Details โ
Key Derivation โ
Algorithm: PBKDF2 (Password-Based Key Derivation Function 2)
Passphrase + Salt โ PBKDF2(600,000 iterations, SHA-256) โ AES-256 Key- Passphrase: User-chosen secret (12+ chars, mixed case, numbers, symbols)
- Salt: 32-byte random value (stored in Firestore, not secret)
- Iterations: 600,000 (OWASP 2023 recommendation, prevents brute-force)
- Output: 256-bit AES-GCM encryption key
Encryption โ
Algorithm: AES-GCM (Advanced Encryption Standard - Galois/Counter Mode)
Plaintext + Key + IV โ AES-GCM โ Ciphertext + Authentication Tag- Key: Derived from passphrase (see above)
- IV (Initialization Vector): 12-byte random value (unique per encryption)
- Authentication: GCM mode provides integrity verification (detects tampering)
What Gets Encrypted โ
| Data | Encrypted? | Stored Where | Accessible By |
|---|---|---|---|
| Birth date | โ Yes | Firestore | User only (with passphrase) |
| Real name | โ Yes (if collected) | Firestore | User only |
| Interests | โ No | Firestore | Public (after connection) |
| Mood/vibe | โ No | Firestore | Public (after connection) |
| Lantern name | โ No | Firestore | Public |
| Passphrase | NEVER STORED | Nowhere | User's memory only |
| Encryption key | NEVER STORED | Session cache only | Derived on-demand |
| Salt | โ No (public) | Firestore | Anyone (needed to derive key) |
User Flow โ
Signup โ
- Age verification: User enters birth date (stays in browser for now)
- Passphrase creation: User creates strong passphrase
- Requirements: 12+ chars, uppercase, lowercase, number, special char
- Shown passphrase strength indicators
- Confirmation: User confirms they understand:
- Forgot passphrase = lost data forever
- Lantern cannot reset passphrase
- Lantern cannot decrypt data
- Account creation:javascript
// Generate salt (random, will be stored) const salt = crypto.getRandomValues(new Uint8Array(32)) // Derive encryption key from passphrase (NOT stored) const key = await deriveKeyFromPassphrase(passphrase, salt) // Encrypt birth date client-side const encryptedBirthDate = await encryptData(birthDate, key) // Send to backend await createUserAccount({ encryptedBirthDate, // Gibberish without passphrase salt, // Public (needed later) lanternName, // Public interests: [], // Public }) // Passphrase NEVER leaves device
Login โ
- User enters email/phone (or Firebase auth)
- Passphrase entry: User enters passphrase
- Key derivation:javascript
// Fetch user's salt from Firestore const { salt } = await getUserProfile(userId) // Derive key from passphrase + salt const key = await unlockEncryption(passphrase, salt, userId) // Cache key in memory for session // Now user can decrypt their data
Logout โ
clearEncryptionKey() // Wipe key from memory
// User must re-enter passphrase next timeSecurity Guarantees โ
What We Can Prove โ
โ
Backend never sees passphrase - Transmitted? No. Stored? No. Logged? No.
โ
Backend never sees encryption key - Derived client-side only
โ
Encrypted data is useless alone - Without passphrase, it's gibberish
โ
Salt is public knowledge - Not secret, safe to store plaintext
โ
Strong key derivation - 600,000 iterations = slow brute-force (years per attempt)
โ
Authenticated encryption - AES-GCM detects tampering
โ
Unique IV per encryption - Prevents pattern analysis
Threat Model โ
| Attack | Protected? | How |
|---|---|---|
| Database breach | โ Yes | Only encrypted data stolen |
| Court order for data | โ Yes | We have only ciphertext |
| Man-in-the-middle | โ Yes | HTTPS + no plaintext transmitted |
| Brute-force passphrase | โ ๏ธ Slowed | 600k iterations = expensive |
| Phishing user's passphrase | โ No | User must protect passphrase |
| Compromised device | โ No | Key cached in memory during session |
| Rubber-hose cryptanalysis | โ No | User can be coerced for passphrase |
What We Cannot Protect Against โ
โ ๏ธ User shares passphrase - Social engineering, phishing
โ ๏ธ Device compromised - Malware captures passphrase as typed
โ ๏ธ Physical coercion - User forced to reveal passphrase
โ ๏ธ Quantum computers - AES-256 vulnerable to future quantum attacks (but we can upgrade)
Legal & Compliance โ
When Law Enforcement Requests Data โ
Request: "Provide all data for user ID abc123"
Our Response:
User Data for abc123:
- encryptedBirthDate: "k2j3n4lk5j6h7g8f9d0s1a2s3d4f5g6h..."
- salt: "p9o8i7u6y5t4r3e2w1q0a9s8d7f6g5h4..."
- lanternName: "Amber Beacon"
- interests: ["Coffee", "Jazz"]
- mood: "chatty"
Note: Birth date is encrypted with zero-knowledge architecture.
We do not store the encryption key or passphrase.
We CANNOT decrypt this data.Legal Standing:
- We are compliant (we provided all data we have)
- We are genuinely unable to decrypt
- This is cryptographically provable
- Precedent: ProtonMail, Signal use similar architecture
GDPR & Data Deletion โ
When user deletes account:
// Delete all user data
await deleteUserAccount(userId)
// Includes: encrypted data, salt, profile, all records
// Result: Data is GONE
// Even if user later provides passphrase, nothing to decryptGDPR Compliance:
- โ Right to erasure: Data permanently deleted
- โ Data minimization: Only essential PII encrypted
- โ Data portability: User can export encrypted backup
- โ Breach notification: Encrypted data leak = low risk
Backend Implementation โ
Firestore Schema โ
users/{userId}
- encryptedBirthDate: string // ENCRYPTED (client-side)
- salt: string // Public (base64)
- lanternName: string // Public
- interests: array // Public
- mood: string // Public
- createdAt: timestamp
- lastLogin: timestampSecurity Rules โ
// Firestore Security Rules
match /users/{userId} {
// Users can only read their OWN encrypted data
allow read: if request.auth.uid == userId;
// Users can only write their own data
allow write: if request.auth.uid == userId
// Cannot modify salt after creation (prevents attack)
&& (!resource || request.resource.data.salt == resource.data.salt);
}Age Verification Function โ
// Cloud Function: verifyVenueAge
// Input: userId, venueId, timestamp
// Output: { can_light: boolean, reason?: string }
export const verifyVenueAge = functions.https.onCall(async (data, context) => {
const { userId, venueId, timestamp } = data
// This function CANNOT decrypt birth date
// Instead, it checks a pre-computed age flag
// Option 1: Store decrypted age (user enters passphrase, we compute once)
const user = await admin.firestore().doc(`users/${userId}`).get()
const userAge = user.data().computedAge // Set during login when key is available
// Option 2: User's client computes age, signs it, sends to backend
// (More complex but maintains zero-knowledge)
const venue = await admin.firestore().doc(`venues/${venueId}`).get()
const requiredAge = getRequiredAge(venue.data(), timestamp)
return {
can_light: userAge >= requiredAge,
reason: userAge < requiredAge ? `This venue requires users to be ${requiredAge}+` : null
}
})Note: Age verification requires decrypted birth date. Options:
- User decrypts locally, computes age, signs result, sends to backend
- Backend caches age during login (when user has key)
- Tradeoff: Zero-knowledge vs. usability
Usability Tradeoffs โ
Pros โ โ
- Provable security: We literally cannot decrypt
- Legal protection: Good faith compliance
- User trust: Transparent, honest privacy
- Future-proof: Resistant to policy changes
Cons โ โ
- Forgot passphrase = lost account: No password reset
- New device login: Must enter passphrase (can't auto-sync)
- Complexity: Users must understand importance of passphrase
- Support burden: Can't help users who forget passphrase
Mitigation Strategies โ
- Passphrase hints: Allow encrypted hint (user can decrypt if they remember parts)
- Backup codes: Generate one-time recovery codes (user stores offline)
- Trusted device: Allow one trusted device to help recover (still requires user action)
- Social recovery: Split key among trusted friends (complex, rarely used)
Recommended: Backup codes (generated at signup, user prints/saves)
Testing & Validation โ
Test Cases โ
Encryption roundtrip:
javascriptconst plaintext = "1990-05-15" const encrypted = await encryptData(plaintext, key) const decrypted = await decryptData(encrypted, key) assert(decrypted === plaintext)Wrong passphrase fails:
javascriptconst key1 = await deriveKeyFromPassphrase("correct", salt) const encrypted = await encryptData("secret", key1) const key2 = await deriveKeyFromPassphrase("wrong", salt) await expect(decryptData(encrypted, key2)).rejects.toThrow()Salt changes key:
javascriptconst key1 = await deriveKeyFromPassphrase("pass", salt1) const key2 = await deriveKeyFromPassphrase("pass", salt2) assert(key1 !== key2) // Different keysBackend cannot decrypt:
javascript// Simulate: Backend has encrypted data + salt, but NOT passphrase const encrypted = "k2j3n4lk5j6h7..." const salt = "p9o8i7u6y5t4r3..." // Backend tries to decrypt without passphrase await expect(decryptData(encrypted)).rejects.toThrow("No encryption key available")
Production Checklist โ
Before deploying to production:
- [ ] Audit encryption.js with security expert
- [ ] Test passphrase strength requirements (zxcvbn library)
- [ ] Implement backup codes for account recovery
- [ ] Add passphrase hint (optional, encrypted)
- [ ] Test on multiple browsers (Safari, Firefox, Chrome)
- [ ] Ensure HTTPS everywhere (Cloudflare enforces this)
- [ ] Add rate limiting on signup (prevent salt enumeration)
- [ ] Document recovery process for users
- [ ] Legal review of "cannot decrypt" claims
- [ ] Penetration test the authentication flow
Frequently Asked Questions โ
Q: Can't you just "reset the password" if I forget it? โ
A: No. Your passphrase IS the encryption key. If we could reset it, we could also decrypt your data, which defeats the purpose. This is by design.
Q: What if I lose my device? โ
A: Your encrypted data is in our backend. On a new device, log in and enter your passphrase. The data will decrypt automatically.
Q: What if Lantern gets hacked? โ
A: Hackers get encrypted data + salts. Without your passphrase, the data is useless gibberish. Your privacy is protected even in a data breach.
Q: Can the government force you to decrypt my data? โ
A: They can ask, but we literally cannot comply. We don't have your passphrase or encryption key. This is provable mathematically.
Q: Isn't this overkill for a social app? โ
A: Lantern's whole value proposition is anonymity and privacy. Zero-knowledge encryption makes that promise real and legally defensible.
Summary โ
What we built: A zero-knowledge encryption system where user passphrases encrypt all PII client-side before transmission. Backend stores only ciphertext.
What this means: Lantern CANNOT decrypt user data, even with a court order. This is cryptographically provable and legally defensible.
Tradeoff: Users must protect their passphrase. Forgot passphrase = lost data (no recovery possible by design).
Status: Frontend implementation complete. Backend integration requires Cloud Functions for age verification with client-side age computation.
This is the gold standard for privacy-first applications. ๐