ZeroLink 架构概览
April 8, 2026 · View on GitHub
语言: English | 中文
ZeroLink 架构概览
核心架构原则
1. 零知识架构
- 服务器不存明文:所有内容在客户端加密,服务器只存储密文
- 服务器不存私钥:接收方私钥在客户端生成并本地存储(Argon2id 包裹)
- 双路径管理权:Secure Share 使用 WebAuthn(私钥驻留系统/硬件);Quick Share 使用密码包裹的本地 ECDSA 密钥
2. 三方角色模型
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Sender │ │ Server │ │ Receiver │
│ (管理者) │ │ (零知识) │ │ (唯一解密) │
├──────────────┤ ├──────────────┤ ├──────────────┤
│ WebAuthn Key │────────▶│ Ciphertext │◀────────│ RSA-OAEP │
│ (不可导出) │ 管理 │ (无明文) │ 解密 │ 私钥本地 │
│ │ │ │ │ (Argon2id) │
│ 可更新/销毁 │ │ DO 原子性 │ │ 单向密码派生 │
│ 但不能解密 │ │ 防并发覆盖 │ │ │
└──────────────┘ └──────────────┘ └──────────────┘
3. 技术栈
前端
- 运行时:浏览器 Web Crypto API
- 认证:WebAuthn(FIDO2)
- 加密:
- 内容:AES-256-GCM(对称加密)
- 密钥封装:RSA-OAEP-256(混合加密)
- KDF:Argon2id(接收方私钥包裹)
- 存储:IndexedDB(加密私钥)
后端
- Cloudflare 运行时:Workers + Durable Objects + R2
- 自部署运行时:Go API + PostgreSQL + S3 兼容存储(Garage 或外部)
- 状态管理:Durable Objects(串行化、原子性)
- 持久化:channel 元数据落在 DO / SQLite 或 PostgreSQL;multipart 文件分片落在对象存储
- 实时同步:Cloudflare 侧由 DO WebSocket 广播;self-host 侧由进程内 WebSocket hub + HTTP polling fallback 提供
核心协议流程
1. Create(创建)
Sender → 选择 Quick Share 或 Secure Share → 生成 lock_secret
→ Quick Share:本地生成 ECDSA 管理密钥并用 Argon2id 包裹
→ Secure Share:WebAuthn 注册管理凭据
→ 返回两条链接:
- /s/:uuid#k=<lock_secret>[&af=<sender_auth_fpr>] (分享链接;af= 在存在发送者身份指纹时附加)
- /m/:uuid#wk=<wrapped_priv> (管理链接;Quick Share — fragment 携带 Argon2id 包裹的 Admin-Priv)
- /m/:uuid (管理链接;Secure Share — 无需 fragment)
2. Lock(接收方上锁)
Receiver → 访问分享链接(获得 fragment 中的 lock_secret)
→ 输入密码 → 生成 RSA keypair
→ 私钥用 Argon2id(密码) 包裹存本地
→ lock_begin 获取 challenge
→ lock_commit 提交 receiver_pub + lock_proof
→ Server 验证 lock_proof(基于 lock_key)
TOFU 抢占锁定防护:
- lock_secret 只在 URL fragment(不会被 HTTP 请求携带)
- 预加载爬虫无法获得 lock_secret → 无法计算 lock_proof → 无法 lock
3. Deliver(投递内容)
Sender → 获取 receiver_pub(已上锁)
→ 本地混合加密:
- 随机 AES-256 key
- 文本载荷:AES-GCM 加密 padded_plaintext,生成 inline cipher_bundle
- 文件载荷:派生 baseIv/contentKey,对每个 chunk 独立做 AES-GCM
- RSA-OAEP 封装 AES key
→ 文件载荷:
- /api/file/initiate → 上传加密 chunk → /api/file/complete
- 获得 typed fileRef 元数据
→ compound_begin 获取 challenge
→ Secure Share: WebAuthn 签名确认
→ Quick Share: 本地 ECDSA 签名确认
→ compound_commit 原子写入文本 inline cipher_bundle 或 fileRef
4. Update/Delete(管理)
Sender → Secure Share: WebAuthn 签名授权
→ Quick Share: 本地 ECDSA 签名授权
→ DO 验证:version 单调 + nonce 去重
→ 原子性更新/删除
安全机制
1. TOFU 抢占锁定防护(v2.5 核心)
问题:预加载机器人可能先于真实接收方访问链接并上锁
解决方案:
lock_secret(32 bytes 随机)只放在 URL fragment- Fragment 不会被 HTTP 请求携带(RFC 3986)
- Server 存储
lock_key = SHA256("GL-lockkey" || uuid || lock_secret) - Lock 时需要
lock_proof = SHA256("GL-lock" || uuid || challenge_id || challenge || lock_key) - 没有 lock_secret → 无法计算 lock_key → 无法生成有效 lock_proof
2. 密文长度泄露缓解(Padding)
问题:密文长度可能泄露明文长度信息
解决方案:
padded_plaintext = [orig_len(4 bytes)] + [orig_data] + [random_padding]
总长度 = ceil((4 + orig_len) / PAD_BLOCK) * PAD_BLOCK
默认 PAD_BLOCK = 4096 bytes
3. 并发安全(Durable Objects)
问题:多个并发请求可能导致状态不一致
解决方案:
- 所有写操作走 DO(串行化)
- version 单调递增
- nonce 去重(TTL 10min)
- challenge 一次性消费
4. Intent Binding(意图绑定)
问题:WebAuthn 签名可能被诱导签署意外操作
解决方案:两种域分离的 challenge 推导,取决于操作类型:
intent_hash = SHA256(canonical_payload) // payload 包含完整操作细节
// 投递/更新 — 确定性推导,无服务端 nonce;重放保护依赖 challenge 一次性消费
expected_challenge = SHA256("GL-delivery-proof" || uuid || intent_hash)
// 删除 — 包含服务端 nonce(challenge_id + seed)确保新鲜性
expected_challenge = SHA256("GLv2.5" || uuid || challenge_id || intent_hash || seed)
WebAuthn/ECDSA challenge 必须 === expected_challenge
产品模式(Current Profiles)
Quick Share(密码)
- 本地生成 ECDSA P-256 管理密钥
- Admin-Priv 用 Argon2id 包裹后编码在管理链接的 URL fragment 中(不存 IndexedDB)
- 任何拥有管理链接和频道密码的人可从任何设备管理频道
- 默认 4KB padding
Secure Share(Passkey)
- 使用 WebAuthn passkey 管理权
- userVerification = "required"
- residentKey = "discouraged"
- 默认 8KB padding
数据流图
┌─────────────────────────────────────────────────────────────┐
│ Sender 视角 │
├─────────────────────────────────────────────────────────────┤
│ 1. 选择 Quick Share 或 Secure Share │
│ - Quick: 本地 ECDSA 管理密钥 + Argon2id 包裹 │
│ - Secure: WebAuthn 管理私钥(系统/硬件,不可导出) │
│ 2. 获取 lock_secret(仅用于分享链接 fragment) │
│ 3. 等待 Receiver 上锁 │
│ 4. 获得 receiver_pub 后: │
│ - 混合加密内容(AES-GCM + RSA-OAEP) │
│ - 文本载荷保持 inline;文件载荷先上传加密 chunk,再提交 │
│ fileRef │
│ - Padding 到 4KB / 8KB 块 │
│ - Quick: 本地 ECDSA 签名 / Secure: WebAuthn 签名 │
│ - 投递密文到 Server │
│ 5. 可随时更新/删除(按所选模式授权) │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Receiver 视角 │
├─────────────────────────────────────────────────────────────┤
│ 1. 从分享链接 fragment 获得 lock_secret │
│ 2. 输入密码 → 生成 RSA keypair │
│ 3. 私钥用 Argon2id(密码) 包裹存本地 │
│ 4. 计算 lock_proof 上锁 │
│ 5. 展示 Safety Code(Emoji/Color)供核对 │
│ 6. Sender 投递后: │
│ - 输入密码 → 解包私钥 │
│ - RSA-OAEP 解封 AES key │
│ - AES-GCM 解密并去除 padding │
│ - 展示明文 │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Server 视角 │
├─────────────────────────────────────────────────────────────┤
│ - 存储: │
│ * admin_webauthn 或 admin_pub(发送方管理凭据) │
│ * lock_key(用于验证 lock_proof,不可逆回 lock_secret) │
│ * receiver_pub(接收方公钥,仅上锁后存在) │
│ * cipher_bundle(inline 文本载荷)或 fileRef 元数据 │
│ * R2 / S3 兼容存储中的加密 multipart 文件分片 │
│ * version, nonce, challenge(防重放/并发) │
│ - 能力: │
│ * 验证 WebAuthn 签名 │
│ * 验证 lock_proof │
│ * 原子性更新(DO) │
│ * 时间窗口检查(±120s) │
│ - 不能: │
│ * 解密内容(无 receiver_priv) │
│ * 伪造发送方操作(无 admin_priv) │
│ * 知道 lock_secret(只存 lock_key) │
└─────────────────────────────────────────────────────────────┘
状态机
┌─────────┐ lock_commit ┌────────┐ compound_commit ┌───────────┐
│ Waiting ├───────────────▶│ Locked ├──────────────────▶│ Delivered │
└────┬────┘ └────┬───┘ └─────┬─────┘
│ │ │
│ delete_commit │ delete_commit │
└──────────┬───────────────┴──────────────┬──────────────┘
│ │
▼ ▼
┌─────────┐ ┌─────────┐
│ Deleted │ │ Expired │
└─────────┘ └─────────┘
状态转移规则:
- Waiting → Locked:lock_commit(需 lock_proof)
- Locked → Delivered:compound_commit(首次投递)
- Delivered → Delivered:compound_commit(更新)
- 任意 → Deleted:delete_commit(管理授权:WebAuthn 或 ECDSA)
- 任意 → Expired:TTL 到期
不可变性:
- Deleted/Expired 后不可恢复
- version 只能递增
- nonce 不可重用
关键常量
// 标识符
UUID_LENGTH = 21 // nanoid
// 时间窗口
TIMESTAMP_SKEW_MS = 120000 // ±2min
CHALLENGE_TTL_MS = 60000 // 60s
NONCE_TTL_MS = 600000 // 10min
// 密码学
LOCK_SECRET_BYTES = 32 // lock_secret 长度
LOCK_KEY_BYTES = 32 // lock_key 长度 (SHA256 输出)
CHALLENGE_BYTES = 32 // challenge 长度
NONCE_BYTES = 24 // nonce 长度
// Padding
PAD_BLOCK_DEFAULT = 4096 // 默认 4KB 块
PAD_BLOCK_MAX = 65536 // 最大 64KB 块
MAX_PLAINTEXT_BYTES = 2MB // 文本载荷 / legacy 兼容用的 inline 明文上限
// WebAuthn
WEBAUTHN_ALG = -7 // ES256 (ECDSA P-256)
可验证发布链(当前方案)
Signed Manifest
- 每次发布生成 manifest.json(文件 hash + 版本 + commit)
- Ed25519 离线签名 → manifest.sig
- 用户可验证前端完整性
自托管(当前方案)
- Docker Compose 打包,包含 Caddy + Go API + PostgreSQL + Garage(可选 S3 兼容存储)
- 对当前前端协议提供等价实现,包括
/api/file_policy和 multipartfileRef交付 - 完全自主控制密钥、存储和运行时
参考资料
- 完整 PRD:PRD.zh.md
- 安全模型:SECURITY.zh.md
- API 规范:见 PRD 第 10 节
- 协议图:见 PRD 第 15 节