feat: E2E 加密协作 + 工作区共享 (e2e-sharing-v1, Phase 1–5 S0-S3) - #64
Conversation
…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。
|
追加 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。 |
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.
合并前安全审查 + 修复对本 PR 做了对抗式审查(5 维度 → 逐发现独立验证)。绝大多数发现为 nit/low(纵深加固/文档/i18n),但权限 DAG 有一个 critical + 一个 high 阻塞项,已在
修复:把 genesis 绑定到工作区权威创建者 joiner-pin 的 TOFU 边界与入站 op 来源校验(DoS)等非阻塞项已记入 |
概述
实现 工作区共享 + 端到端加密 + 权限 的加密内核(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。包含(Phase 1–4)
crates/core/src/crypto/:XChaCha20-Poly1305 帧化 key-committing seal/open、HKDF 子用途域分离、Ed25519→X25519 单层复用派生 + ECDH、X25519 Lockbox、Argon2id。workspace_key_lockboxes、permission_ops表 +share_invites.commitment列(migrationm20260607_000008)。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 明文应用分支(防降级)。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 全绿。cargo clippy --workspace -- -D warnings干净。open_workspace→workspace_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(后续)
swarmnote://invite+ DHT 签名邀请包 + Argon2id 密码)+ op 广播接入同步。威胁模型(诚实声明)
仅传输加密(本地写明文
.md,保 folder-is-truth)· 不防丢设备(交 OS 全盘加密)· 无前向保密(CRDT 需重放全历史)· 撤销是 lazy 的(被移除者对其离开前内容永久可读)。详见11-threat-model.md。提交
8 个 commit:docs(设计定稿) → crypto 地基+schema → 加密 codec → WorkspaceCore 密钥生命周期 → 密钥分发密码学+存储 → 加密翻开+密钥分发 → 端到端测试 → 权限签名链。