ZeroLink Architecture Overview
April 8, 2026 · View on GitHub
Language: English | 中文
ZeroLink Architecture Overview
Core Architecture Principles
1. Zero-Knowledge Architecture
- Server never stores plaintext: All content is encrypted client-side; the server only stores ciphertext
- Server never stores private keys: Receiver private keys are generated client-side and stored locally (wrapped with Argon2id)
- Dual-path admin authority: Secure Share uses WebAuthn (private key resides in system/hardware); Quick Share uses a password-wrapped local ECDSA key
2. Three-Party Role Model
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Sender │ │ Server │ │ Receiver │
│ (Admin) │ │ (Zero-know.) │ │(Sole decrypt)│
├──────────────┤ ├──────────────┤ ├──────────────┤
│ WebAuthn Key │────────▶│ Ciphertext │◀────────│ RSA-OAEP │
│(non-export.) │ Manage │(no plaintext)│ Decrypt │ Private key │
│ │ │ │ │ (local) │
│ Can update/ │ │ DO atomicity │ │ (Argon2id) │
│ delete but │ │ Prevents │ │ One-way pwd │
│ can't decrypt│ │ concurrent │ │ derivation │
│ │ │ overwrites │ │ │
└──────────────┘ └──────────────┘ └──────────────┘
3. Technology Stack
Frontend
- Runtime: Browser Web Crypto API
- Authentication: WebAuthn (FIDO2)
- Encryption:
- Content: AES-256-GCM (symmetric encryption)
- Key wrapping: RSA-OAEP-256 (hybrid encryption)
- KDF: Argon2id (receiver private key wrapping)
- Storage: IndexedDB (encrypted private keys)
Backend
- Cloudflare runtime: Workers + Durable Objects + R2
- Self-hosted runtime: Go API + PostgreSQL + S3-compatible storage (Garage or external)
- State management: Durable Objects (serialization, atomicity)
- Persistence: DO storage / SQLite or PostgreSQL rows for channel metadata, plus object storage for multipart file chunks
- Realtime: DO WebSocket fan-out on Cloudflare; process-local WebSocket hub with HTTP polling fallback in self-hosted mode
Core Protocol Flows
1. Create
Sender → Choose Quick Share or Secure Share → Generate lock_secret
→ Quick Share: Generate local ECDSA admin key and wrap with Argon2id
→ Secure Share: Register WebAuthn admin credential
→ Return two links:
- /s/:uuid#k=<lock_secret>[&af=<sender_auth_fpr>] (share link; af= appended when sender auth fingerprint exists)
- /m/:uuid#wk=<wrapped_priv> (manage link; Quick Share — fragment carries Argon2id-wrapped Admin-Priv)
- /m/:uuid (manage link; Secure Share — no fragment needed)
2. Lock (Receiver Locks)
Receiver → Visit share link (obtain lock_secret from fragment)
→ Enter password → Generate RSA keypair
→ Wrap private key with Argon2id(password) and store locally
→ lock_begin to obtain challenge
→ lock_commit to submit receiver_pub + lock_proof
→ Server verifies lock_proof (based on lock_key)
TOFU Preemptive Lock Protection:
- lock_secret is only in the URL fragment (never sent with HTTP requests)
- Preload crawlers cannot obtain lock_secret → cannot compute lock_proof → cannot lock
3. Deliver (Deliver Content)
Sender → Fetch receiver_pub (already locked)
→ Client-side hybrid encryption:
- Random AES-256 key
- Text payloads: AES-GCM encrypt padded_plaintext into inline cipher_bundle
- File payloads: derive baseIv/contentKey, AES-GCM encrypt each chunk independently
- RSA-OAEP wrap AES key
→ File payloads:
- /api/file/initiate → upload encrypted chunks → /api/file/complete
- Receive typed fileRef metadata
→ compound_begin to obtain challenge
→ Secure Share: WebAuthn signature confirmation
→ Quick Share: Local ECDSA signature confirmation
→ compound_commit to atomically write inline text cipher_bundle or fileRef
4. Update/Delete (Management)
Sender → Secure Share: WebAuthn signature authorization
→ Quick Share: Local ECDSA signature authorization
→ DO verification: monotonic version + nonce deduplication
→ Atomic update/delete
Security Mechanisms
1. TOFU Preemptive Lock Protection (v2.5 Core)
Problem: Preload bots may visit the link and lock it before the real receiver
Solution:
lock_secret(32 bytes random) is placed only in the URL fragment- Fragments are never sent with HTTP requests (RFC 3986)
- Server stores
lock_key = SHA256("GL-lockkey" || uuid || lock_secret) - Locking requires
lock_proof = SHA256("GL-lock" || uuid || challenge_id || challenge || lock_key) - Without lock_secret → cannot compute lock_key → cannot generate valid lock_proof
2. Ciphertext Length Leakage Mitigation (Padding)
Problem: Ciphertext length may leak information about plaintext length
Solution:
padded_plaintext = [orig_len(4 bytes)] + [orig_data] + [random_padding]
Total length = ceil((4 + orig_len) / PAD_BLOCK) * PAD_BLOCK
Default PAD_BLOCK = 4096 bytes
3. Concurrency Safety (Durable Objects)
Problem: Multiple concurrent requests may cause state inconsistency
Solution:
- All write operations go through DO (serialized)
- Monotonically increasing version
- Nonce deduplication (TTL 10min)
- Single-use challenge consumption
4. Intent Binding
Problem: WebAuthn signatures could be tricked into signing unintended operations
Solution: Two domain-separated challenge derivations depending on the operation:
intent_hash = SHA256(canonical_payload) // payload contains full operation details
// Deliver/Update — deterministic, no server nonce; replay protection via single-use challenge consumption
expected_challenge = SHA256("GL-delivery-proof" || uuid || intent_hash)
// Delete — includes server nonce (challenge_id + seed) for freshness
expected_challenge = SHA256("GLv2.5" || uuid || challenge_id || intent_hash || seed)
WebAuthn/ECDSA challenge must === expected_challenge
Product Modes (Current Profiles)
Quick Share (Password)
- Locally generated ECDSA P-256 admin key
- Admin-Priv is wrapped with Argon2id and encoded in the manage link's URL fragment (not stored in IndexedDB)
- Anyone with the manage link and channel password can manage the channel from any device
- Default 4KB padding
Secure Share (Passkey)
- Uses WebAuthn passkey for admin authority
- userVerification = "required"
- residentKey = "discouraged"
- Default 8KB padding
Data Flow Diagrams
┌─────────────────────────────────────────────────────────────┐
│ Sender Perspective │
├─────────────────────────────────────────────────────────────┤
│ 1. Choose Quick Share or Secure Share │
│ - Quick: Local ECDSA admin key + Argon2id wrapping │
│ - Secure: WebAuthn admin key (system/hardware, │
│ non-exportable) │
│ 2. Obtain lock_secret (only for share link fragment) │
│ 3. Wait for Receiver to lock │
│ 4. After obtaining receiver_pub: │
│ - Hybrid encrypt content (AES-GCM + RSA-OAEP) │
│ - Text payloads stay inline; file payloads upload │
│ encrypted chunks first and then commit a fileRef │
│ - Pad to 4KB / 8KB blocks │
│ - Quick: Local ECDSA signature / Secure: WebAuthn │
│ signature │
│ - Deliver ciphertext to Server │
│ 5. Can update/delete at any time (authorized per chosen │
│ mode) │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Receiver Perspective │
├─────────────────────────────────────────────────────────────┤
│ 1. Obtain lock_secret from share link fragment │
│ 2. Enter password → Generate RSA keypair │
│ 3. Wrap private key with Argon2id(password) and store │
│ locally │
│ 4. Compute lock_proof to lock │
│ 5. Display Safety Code (Emoji/Color) for verification │
│ 6. After Sender delivers: │
│ - Enter password → Unwrap private key │
│ - RSA-OAEP unwrap AES key │
│ - AES-GCM decrypt and remove padding │
│ - Display plaintext │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Server Perspective │
├─────────────────────────────────────────────────────────────┤
│ - Stores: │
│ * admin_webauthn or admin_pub (sender admin credential) │
│ * lock_key (used to verify lock_proof; cannot reverse │
│ to lock_secret) │
│ * receiver_pub (receiver public key; exists only after │
│ locking) │
│ * cipher_bundle (inline text payloads) or fileRef │
│ metadata (file deliveries) │
│ * encrypted multipart chunks in │
│ R2 / S3-compatible storage │
│ * version, nonce, challenge (anti-replay/concurrency) │
│ - Can: │
│ * Verify WebAuthn signatures │
│ * Verify lock_proof │
│ * Atomic updates (DO) │
│ * Time window checks (±120s) │
│ - Cannot: │
│ * Decrypt content (no receiver_priv) │
│ * Forge sender operations (no admin_priv) │
│ * Know lock_secret (only stores lock_key) │
└─────────────────────────────────────────────────────────────┘
State Machine
┌─────────┐ lock_commit ┌────────┐ compound_commit ┌───────────┐
│ Waiting ├───────────────▶│ Locked ├──────────────────▶│ Delivered │
└────┬────┘ └────┬───┘ └─────┬─────┘
│ │ │
│ delete_commit │ delete_commit │
└──────────┬───────────────┴──────────────┬──────────────┘
│ │
▼ ▼
┌─────────┐ ┌─────────┐
│ Deleted │ │ Expired │
└─────────┘ └─────────┘
State Transition Rules:
- Waiting → Locked: lock_commit (requires lock_proof)
- Locked → Delivered: compound_commit (first delivery)
- Delivered → Delivered: compound_commit (update)
- Any → Deleted: delete_commit (admin authorization: WebAuthn or ECDSA)
- Any → Expired: TTL expiration
Immutability:
- Cannot recover after Deleted/Expired
- Version can only increment
- Nonce cannot be reused
Key Constants
// Identifiers
UUID_LENGTH = 21 // nanoid
// Time Windows
TIMESTAMP_SKEW_MS = 120000 // ±2min
CHALLENGE_TTL_MS = 60000 // 60s
NONCE_TTL_MS = 600000 // 10min
// Cryptography
LOCK_SECRET_BYTES = 32 // lock_secret length
LOCK_KEY_BYTES = 32 // lock_key length (SHA256 output)
CHALLENGE_BYTES = 32 // challenge length
NONCE_BYTES = 24 // nonce length
// Padding
PAD_BLOCK_DEFAULT = 4096 // Default 4KB block
PAD_BLOCK_MAX = 65536 // Maximum 64KB block
MAX_PLAINTEXT_BYTES = 2MB // Inline plaintext ceiling for text payloads / legacy compatibility
// WebAuthn
WEBAUTHN_ALG = -7 // ES256 (ECDSA P-256)
Verifiable Release Chain (Current Approach)
Signed Manifest
- Each release generates manifest.json (file hashes + version + commit)
- Ed25519 offline signing → manifest.sig
- Users can verify frontend integrity
Self-Hosting (Current)
- Docker Compose package with Caddy + Go API + PostgreSQL + Garage (optional S3-compatible storage)
- Protocol-equivalent implementation for the current frontend contract, including
/api/file_policyand multipartfileRefdelivery - Full autonomous control over keys, storage, and runtime
References
- Full PRD: PRD.md
- Security model: SECURITY.md
- API specification: See PRD Section 10
- Protocol diagrams: See PRD Section 15