Skip to content

feat: E2E 加密协作 + 工作区共享 (e2e-sharing-v1, Phase 1–5 S0-S3) - #64

Merged
yexiyue merged 14 commits into
developfrom
feature/e2e-sharing-v1
Jun 8, 2026
Merged

feat: E2E 加密协作 + 工作区共享 (e2e-sharing-v1, Phase 1–5 S0-S3)#64
yexiyue merged 14 commits into
developfrom
feature/e2e-sharing-v1

Conversation

@yexiyue

@yexiyue yexiyue commented Jun 7, 2026

Copy link
Copy Markdown
Collaborator

概述

实现 工作区共享 + 端到端加密 + 权限加密内核(OpenSpec change e2e-sharing-v1 的 Phase 1–4)。把正在工作的 P2P 同步从明文广播升级为 E2E 加密:配对设备通过 X25519 Lockbox 共享同一把 workspace key,GossipSub 广播全程加密,未授权 mesh 节点只见密文。

设计定稿见 dev-notes/design/{04-permissions,05-sharing,08-e2e-encryption,11-threat-model}.md

加密主体是设备(per-device Ed25519,无账号系统);业界(Jazz/cojson、p2panda、Tresorit、SecSync)在同类场景的共同正解。

包含(Phase 1–4)

  • 密码学地基 crates/core/src/crypto/:XChaCha20-Poly1305 帧化 key-committing seal/open、HKDF 子用途域分离、Ed25519→X25519 单层复用派生 + ECDH、X25519 Lockbox、Argon2id。
  • DB schema:新增 workspace_key_lockboxespermission_ops 表 + share_invites.commitment 列(migration m20260607_000008)。
  • 密钥模型 + 分发:per-workspace read_key+write_key(独立两把,带 key_version 历史)+ self-Lockbox;peer_id_to_x25519_public(从配对 PeerId 推 X25519,无需改 submodule)+ 给配对设备封/装 Lockbox。
  • 加密同步(翻开):encode/decode_encrypted_gossip(wire=[16B doc_uuid 明文路由][AEAD 帧],AAD 绑 workspace_id‖doc_uuid‖key_version‖msg_type,分通道);publish_* 加密、入站 coordinator 解密(解不开即丢弃);移除 legacy 明文应用分支(防降级)。
  • 密钥投递协议 + join gate:SyncRequest/Response::WorkspaceKey;full_sync step 0 —— joiner 缺 key 时向 owner 请求(走 Noise RR)→ 安装 → reload;open_workspace_for_sync 让 joiner 不 self-init(避免分叉 key)。
  • 权限签名链 workspace/permissions.rs:PermissionOp(blake3 内容哈希 + 设备 Ed25519 签名)+ materialize 重放(genesis 自封 Owner 引导 + 三不变量:签名有效 / issuer 当前 Owner / 两级下仅 Owner 增撤)。

验证

  • 单测:crypto(16)、workspace::keys(含跨设备 A→B 分发往返)、encrypted_sync_end_to_end(A 加密→B 共享 key 后解密→无 key 的 C 解不开)、codec(5)、permissions(5,含伪造提权被拒)、identity —— core 共 99 tests 全绿
  • clippy:cargo clippy --workspace -- -D warnings 干净。
  • 运行时(Tauri MCP,真实 app):启动无 panic;open_workspaceworkspace_key_lockboxes 真有 self-sealed 行(105B = lockbox 帧格式);migration 应用;start_p2p_node → coordinator/event_loop 初始化干净、配对码生成。
  • 构建:前端 pnpm build(tsc 干净)+ 桌面二进制 cargo build -p swarmnote 链接成功。

⚠️ 待真双设备实测

GUI 双实例 + P2P 配对无法在 CI/MCP 环境自动化。合并前建议两台真机过一遍:配对 → B 同步 A 的工作区(应看到内容)→ A 编辑,B 实时可见(= 加密 gossip 往返成功)。其下每一层已验证;未覆盖的仅 libp2p gossip 传输本身(SwarmDrop v0.4.4 已验证)。

不在本 PR(后续)

  • Phase 5:分享 UI + 成员管理 + 链接分享(swarmnote://invite + DHT 签名邀请包 + Argon2id 密码)+ op 广播接入同步。
  • 3.4 topic HMAC 派生(元数据面;明文 topic 暂留)、3.5 libs/core 入站校验钩子 Reject(submodule;当前解密失败=丢弃已安全)、4.5 逐 update 签名(v2 Reader 用)。

威胁模型(诚实声明)

仅传输加密(本地写明文 .md,保 folder-is-truth)· 不防丢设备(交 OS 全盘加密)· 无前向保密(CRDT 需重放全历史)· 撤销是 lazy 的(被移除者对其离开前内容永久可读)。详见 11-threat-model.md

提交

8 个 commit:docs(设计定稿) → crypto 地基+schema → 加密 codec → WorkspaceCore 密钥生命周期 → 密钥分发密码学+存储 → 加密翻开+密钥分发 → 端到端测试 → 权限签名链。

yexiyue added 11 commits June 7, 2026 19:59
…model

把 04-permissions / 05-sharing / 08-e2e-encryption 重写为 v1 定稿,新增
11-threat-model,更新 09-decisions 与 README 索引。核心:加密主体=设备、
per-workspace 对称 key + X25519 Lockbox + lazy 轮换、两级 Owner/Collaborator、
签名 permission_ops、Mega 式链接分享。rust-backend 知识库补密码学踩坑
(hkdf 0.12 配 sha2 0.10、RNG 版本耦合、Ed25519→X25519 派生、key commitment)。
阶段 1(密码学地基,crates/core/src/crypto/):XChaCha20-Poly1305 帧化
key-committing seal/open、HKDF 子用途域分离、Ed25519→X25519 单层复用派生 + ECDH、
X25519 Lockbox、Argon2id;IdentityManager 暴露 x25519_secret/public;AppError::Crypto。

阶段 2(密钥模型 + schema):migration 新增 workspace_key_lockboxes / permission_ops
两表 + share_invites 加 commitment 列;对应 entity;workspace/keys.rs 的 WorkspaceKeys
({key_version→(read,write)} 历史 map)+ initialize/load_workspace_keys(self-Lockbox
落库与解载)。

core 86 tests 通过,clippy -D warnings 干净。
sync/mod.rs 新增 encode/decode_encrypted_gossip:wire = [16B doc_uuid 明文路由]
[XChaCha20-Poly1305 帧],AAD 绑 workspace_id‖doc_uuid‖key_version‖msg_type,
MSG_TYPE_DOC/AWARENESS 分通道,按帧 key_version 选历史 key。纯增量,未触碰实时
publish/handle。WorkspaceKeys::test_single 测试构造器。5/5 codec 测试通过。
WorkspaceCore 持有 keys: RwLock<WorkspaceKeys>,open 时经
load_or_initialize_workspace_keys 加载(无则自封 v1 self-Lockbox,
并发 open 竞态安全),暴露 keys() 访问器;open_workspace 把设备 X25519
secret/public 传入 new()。

非破坏性:publish/handle 仍走明文,wire 行为不变,现有同步零影响。
为加密广播切换铺路。core 91 tests 通过,workspace check + clippy 干净。
identity::peer_id_to_x25519_public —— 从配对设备 ed25519 PeerId(内联 identity
multihash 公钥)推出其 X25519 公钥,无需改 submodule/无需额外交换。
keys::share_workspace_keys_to_device —— 把当前 workspace key 封给 recipient
PeerId 并落 Lockbox 行;load_workspace_keys 补全非自封分支(从 sealed_by
PeerId 推发送方公钥来解)。

跨设备 A→B 分发往返测试通过(真实 keypair/PeerId)。这是分发的密码学+存储
半层;协议投递 + 鉴权 + 加密翻开为后续。core 93 tests,clippy 干净。
…ices

加密翻开:publish_doc_update/publish_awareness 用 encode_encrypted_gossip 加密
广播;event_loop 把原始密文交给 coordinator,handle_ws_gossip_update/
handle_ws_awareness_gossip 用 ws.keys() 解密(解不开即丢弃);移除 legacy 明文
per-doc 应用分支(防降级)。

密钥分发:新增 SyncRequest/Response::WorkspaceKey + SealedWorkspaceKey;
full_sync step 0 —— joiner 缺 key 时向 owner 请求(走 Noise RR)、安装、reload;
coordinator 服务端把当前 key 封给已配对请求方(seal_keys_for_recipient)。
join gate:create_workspace_for_sync 走 open_workspace_for_sync(不 self-init),
WorkspaceCore::new 按 init_keys 区分 owner/joiner;reload_keys 装载收到的 key。

注:topic 仍明文 uuid(3.4 延后);解密失败=丢弃(3.5 校验钩子 submodule 延后);
资产走 RR 已被 Noise 覆盖。需双设备实测验证。core 93 tests,clippy --workspace 干净。
串起完整链路(除 libp2p 传输外):A 初始化 key → seal 给 B 的 PeerId →
B install + load → A encode_encrypted_gossip 加密 → B decode 解出明文;
无 key 的设备 C 解不开。确定性证明加密跨设备同步的核心正确性。
workspace/permissions.rs:PermissionOp(op_id=blake3 内容哈希 + 设备 Ed25519
签名)、build_signed_op、save_op/load_ops、materialize 重放(genesis 自封 Owner
引导 + 三不变量:签名有效/issuer 当前为 Owner/两级下仅 Owner 增撤)+ prev_hash
因果链 BFS 排序(确定性)。
identity.rs:IdentityManager::sign + verify_peer_signature + 抽出 peer_ed25519_public
(去重 peer→pubkey 解码)。

5 单测:genesis 建 Owner、Owner 增/撤 Collaborator、伪造提权被拒(有效签名但
issuer 非 Owner)、篡改签名被丢、跨输入顺序确定性。core 99 tests,clippy 干净。
注:op 广播/接收(4.2b)+ 物化进 permissions 表 + 逐 update 签名(4.5)随 Phase 5 分享流程接。
…phase5 S0)

修复 broken 的权限链:WorkspaceCore::new 在 owner 首建 key 时(did_init)经
ensure_genesis_owner 种下 genesis Owner op,materialize 从此认得 Owner。
permissions.rs 加 serde(PermissionOp/Role/OpKind)+ load_and_materialize/role_of/
ensure_genesis_owner;CtrlMessage 加 PermissionOpsUpdate;workspace_sync 加
publish_permission_ops;coordinator.handle_ctrl_message 验签后落库。new() 改收
&IdentityManager(派生 peer_id/x25519 + 能签 genesis)。

未动 key 分发 gate(仍 is_paired),现有同步零影响——gate 翻转随 S1 grant 命令。
core 100 tests(含 genesis 种植幂等 + DB 往返),clippy 干净,workspace 编译通过。
… (phase5 S1)

后端 enforcement:build_workspace_key_response 与全部 RR 同步响应(DocList/
StateVector/FullSync/Asset)改按 materialize 角色 gating(只有被授权成员能拿 key
与拉取内容,paired 但未授权拒绝);WorkspaceKey 响应随带 permission ops,
full_sync 安装后可 materialize 自己的角色。

workspace/sharing.rs:grant_collaborator / revoke_member / list_members(MemberInfo
= role+在线+设备名)+ chain_tip;owner-only 校验,grant/revoke 广播签名 op 链。
AppError::PermissionDenied。

src-tauri/commands/share.rs:share_workspace_to_device / list_workspace_members /
revoke_workspace_member 注册 + tauri-specta bindings(Role/MemberInfo)。
core 100 tests,clippy 干净(core+壳)。
…S2-S3)

主窗口 TitleBar 加「共享」按钮(Share2,workspace 存在时),打开 ShareDialog:
- 成员列表(MemberRow,复用 PairedDeviceCard 行规格 + DeviceAvatar + 在线点 + RoleBadge)
- RoleBadge(复用 Badge,Owner=primary / Collaborator=secondary,rounded-full pill)
- owner 可见「添加设备」区(已配对未入成员的设备 → 分享按钮 → grant)
- 撤销走 RevokeMemberDialog(AlertDialog,destructive,文案诚实说明 lazy 撤销)
全部复用 useAsyncAction + ErrorMessage + sonner toast + Lingui + CSS 变量,
与配对/设备 UI 设计语言一致。被授权设备走现有 WorkspaceSyncDialog 拉取。

tsc 无类型错误,biome lint:ci 通过,前端构建通过,i18n 已 extract。
@yexiyue yexiyue changed the title feat: E2E 加密协作内核 (e2e-sharing-v1 Phase 1–4) feat: E2E 加密协作 + 工作区共享 (e2e-sharing-v1, Phase 1–5 S0-S3) Jun 8, 2026
@yexiyue

yexiyue commented Jun 8, 2026

Copy link
Copy Markdown
Collaborator Author

追加 Phase 5 S0–S3:权限签名链激活(genesis owner op + 广播)、权限 enforcement(key 分发 + RR 全量同步均按 materialize 角色 gating,未授权设备拉不到内容)、share/list_members/revoke 命令、主窗口「共享」按钮 + 成员管理 UI(复用 PairedDeviceCard/Badge/AlertDialog 等现有组件)。core 100 tests + 前端 tsc/biome/build 全绿。剩 S4–S5 链接分享(给陌生人,独立子系统)待定 v1 / v1.1。

yexiyue added 3 commits June 8, 2026 18:14
build_workspace_list(ListWorkspaces 响应)只返回请求方被授权(role_of=Some)的
工作区,与 key 分发/RR 同步的权限 gating 一致——B 的同步对话框不再列出未授权
工作区(消除"看得到拉不动")。core 100 tests,clippy 干净。
…pass)

materialize() accepted any self-signed genesis op (prev_hash=None,
self-grant Owner) as a trusted root, with no uniqueness or authority
binding. A paired-but-unauthorized device could self-sign a genesis,
broadcast it via PermissionOpsUpdate (persisted after op.verify() only),
and materialize itself as Owner on a key holder's device — which then
sealed the workspace key to the attacker. This defeated the whole E2E
sharing confidentiality.

Fix: bind the genesis to the workspace's authoritative creator.
- materialize(ops, expected_owner): genesis bootstraps Owner only when
  issuer == target == expected_owner (workspaces.created_by). Any other
  self-signed genesis is dropped. Missing row -> "" -> fails closed.
- The authoritative owner can never be revoked or demoted (protects the
  founder; guarantees at least one Owner survives) — closes the
  "co-owner revokes founder" gap.
- load_and_materialize reads created_by from the workspace row; all
  authorization checks (role_of / is_authorized /
  build_workspace_key_response / list_members / discovery) flow through
  it, so no path bypasses the binding.
- A joiner's local row is created with itself as created_by; once it
  receives the owner's signed chain over the Noise-authenticated sync
  channel (ensure_workspace_key), it pins created_by to the chain's sole
  genesis issuer (genesis_owner) so its own binding matches the owner's.

Adds regression tests: forged genesis cannot self-bootstrap Owner (unit
+ DB-level), owner cannot be revoked/demoted, genesis_owner uniqueness.
Documents the joiner-pin TOFU boundary and remaining DoS-only followups
in the threat model. core: 105 tests green, clippy clean.
@yexiyue

yexiyue commented Jun 8, 2026

Copy link
Copy Markdown
Collaborator Author

合并前安全审查 + 修复

对本 PR 做了对抗式审查(5 维度 → 逐发现独立验证)。绝大多数发现为 nit/low(纵深加固/文档/i18n),但权限 DAG 有一个 critical + 一个 high 阻塞项,已在 5b43c42 修复:

  • critical — 伪造 genesis 越权成 Owner / 骗取 E2E 密钥:materialize() 接受任意自签 genesis(prev_hash=None self-grant Owner)作可信根,无唯一性/权威绑定。仅配对、未授权的设备可自签 genesis、经 PermissionOpsUpdate 广播(仅 op.verify() 即落库),在 key 持有者设备上把自己 materialize 成 Owner → 对方把 workspace key 封给它,击穿 E2E 机密性。
  • high — 可 revoke 创始 Owner:materialize 对非 genesis op 仅校验"issuer 当前为 Owner",Owner 间平权,无"创始/最后一个 Owner 保护"。

修复:把 genesis 绑定到工作区权威创建者 workspaces.created_by——materialize(ops, expected_owner) 仅当 issuer==target==expected_owner 才认 Owner,其余自签 genesis 丢弃(行缺失=空串=fail closed);owner 永不可被 revoke/降级;joiner 首次同步时从经 Noise 认证的链里把本地 created_by 钉成真 owner。新增回归测试(伪造 genesis 单测+DB 级、owner 不可 revoke、genesis 唯一性)。core 105 测试全绿,clippy 干净

joiner-pin 的 TOFU 边界与入站 op 来源校验(DoS)等非阻塞项已记入 dev-notes/design/11-threat-model.md,后续 v1.1 硬化。

@yexiyue
yexiyue merged commit f0daabb into develop Jun 8, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant