Files
WebDrop/docs/e2ee-boundary-comparison.md
justme d946fc5e8a feat(api)!: fold devices into users and report member presence
Contract 7.0.0 (breaking). Devices become an attribute of the account;
members are managed per user, and only unauthenticated guests keep a
per-terminal registration.

- rename seats -> max_members, seats_used -> members_used,
  seats_full -> members_full, SeatJoined -> JoinResult, the `seats`
  stream control message -> `members`, and POST /channels/{id}/seats ->
  POST /channels/{id}/join (stored channel settings are migrated on start)
- drop per-terminal encrypted device labels and the seat list/release
  endpoints; add GET/DELETE /channels/{id}/guests for guest terminals
- authenticate account members by account session only: the terminal key
  is optional for logged-in accounts, no seat row is written for them,
  and gated-download grants are counted per user (per terminal for guests)
- GET /channels/{id}/members now returns online, devices and
  online_devices per account; presence is tracked in memory from active
  WebSocket/SSE subscriptions and is not pushed over the stream
- owner invitation list no longer returns accounts that already joined
2026-10-14 22:18:59 +08:00

15 KiB
Raw Permalink Blame History

E2EE 协作架构边界对照

对照来源:用户提供的 D:/Users/JustMe/Downloads/E2EE_Collaboration_Architecture.md。 核查对象:2026-10-13 当前工作区代码,包含尚未提交、尚未部署的后端业务模型改造。此次对照不修改密钥协议,不迁移旧数据。

判断

当前实现已推进为“后端管理业务结构和访问策略、浏览器持有频道数据域密钥并加解密资源”的模型。 参考文档是“后端协调存储、客户端验证授权”的设备身份、频道数据域和 epoch 模型。 两者的主要差距在身份可信来源、密钥分发粒度、授权验证和撤销后的密码学隔离,而非 CRUD 或界面布局。

对照

边界 参考文档 当前代码 判断
业务结构 后端提供 Identity、Space、Channel、Chat、File、Sync 模块 账户资料、空间成员、频道、分组、目录、文件归属已放到后端,前端接口类型从 DTO 生成 模块方向一致
资源加密 设备加密,服务端保存密文 Worker + WebCrypto,AES-GCM 分帧,AAD/ID/FINAL/大小校验,持久化密文重试 可沿用基础能力
密码与数据密钥 账户认证不直接充当数据加密根 密码/可用的 Passkey PRF 解锁保险库;随机账号 ECDH 私钥解开账号 Key Envelope;频道数据域密钥独立随机生成 密码不直接生成资源密钥;账号恢复仍与 E2EE 身份私钥恢复耦合
用户身份 Signing Identity 与 Device Keys 账号保险库保存 P-256 签名根密钥;本机保存不可导出的设备签名私钥,根签名设备证书由后端校验并登记 身份密钥基础已落地;尚未用于业务事件
新设备 可信恢复或已有设备审批 用户已决定不要求旧设备审批;新设备以账号密码/支持的 Passkey PRF 解锁同一保险库私钥 与参考文档的设备审批模型不同;账号被攻破会暴露该账号全部空间密钥
设备分发 每个获授权设备各有 Key Envelope 按 username 分发;设备只有 UUID 和 session(v7 起不再有按空间的席位或终端密钥,访客除外),没有设备解包公钥 分发对象不同
Space 主要是授权管理范围 成员、频道目录、日志与同步边界;不再作为频道内容根 业务边界已收束,仍由服务端定义
Channel Chat 与 FileGroup 有独立数据域和密钥 每频道独立 chat/files 随机 epoch key;对象另有随机 key,由域密钥封装 数据域已落地,密钥包仍按账号而非设备
授权链 Owner 签名 Capability,客户端验证 owner/member + bearer 能力令牌 + 服务端成员表,客户端没有验证签名授权链 服务端仍是授权判断的信任方
管理与读取 Admin 可以只管理而没有解密权限 没有 Admin Capability;Owner 管理凭据与频道数据域密钥格式分开,但 Owner 同时能管理并读取其已授权频道 仍缺管理授权与数据读取权限分离
细分权限 按频道、聊天/文件域授予 read/write/upload 等权限 默认频道权限、历史策略、epoch 与收件人计划由后端计算;邀请提交时原子校验域密钥包集合,日志、对象下载和写入按权限检查 初始权限和邀请策略已落地;其他频道授权 UI 和成员撤销后的轮换待补
Epoch 成员或设备撤销触发新 epoch,排除被撤销者 Owner 关闭新成员历史时,邀请流程按 chat/files 分别轮换 epoch;服务端拒绝旧 epoch 写入 新成员历史隔离已实现;成员撤销后轮换、并发与离线恢复未实现
FileKey 每文件随机 FileKey,由 FileGroupKey 封装 每条目(文件、消息、加密元数据)独立随机对象密钥,由对应频道域 epoch key 封装 已具备独立重封装基础,批量 rewrap/撤销流程未做
消息事件 已签名的 create/edit/delete/reaction,客户端 reduce 完整对象的最终帧经账号设备签名并绑定密文 manifest;编辑是签名新对象后再删除旧对象,删除/频道结构命令尚无签名事件 创建/发布事件基础已接入,删除和结构授权仍缺
作者证明 消息签名绑定用户/设备 后端 AuthorAccount 与账号资料决定显示身份 能提供正常服务下的身份归属,不能抵抗恶意服务端改写
前端业务缓存 后端定义账户、空间、频道、权限及 API 契约 DTO 从后端生成;IndexedDB SavedResource 仍缓存服务端设置/角色与本机凭据,在线发送不再根据缓存上传开关授权 服务端是在线权限来源;本地快照仍需与密钥/队列拆分
存储部署 PostgreSQL、Redis、S3 SQLite、本机 blob、进程内 ChannelHub;NativeAOT + WebSocket/SSE/HTTP 已有 部署规模不同,不是首要密码学差距

已收回后端的有:邀请/历史授权计划、默认访问权限、epoch 选择与域密钥包原子提交;上传权限不再读取本地 membersCanUpload 缓存;频道/分组创建直接返回服务端生成的实体 ID;chat/files 与 open/invite 已由后端枚举定义并生成前端联合类型。

关键代码依据

  1. accountCrypto.ts:密码派生登录 verifier 和保险库密钥;账号保险库另含 ECDSA P-256 根签名密钥。generateInvitationKeys() 仍专用于 ECDH 邀请。
  2. account.ts、accountChannels.ts:新设备登录后恢复同一账号私钥,按账号领取空间授权包。
  3. SqliteAccountStore.cs、SqliteAccountSigning.cs:账号保存根签名公钥,设备目录保存根签名证书;尚无设备解包公钥/旧设备审批。
  4. domainCrypto.ts、channelDataKeys.ts:频道 chat/files epoch key 与随机对象 key 在客户端生成,账号 ECDH envelope 与对象封装也在客户端执行。packages/core/kdf.ts 仍服务于访问凭据和独立 Share/旧资源路径,不是新 Workspace 的内容密钥路径。
  5. ChannelWorkspace.cs:后端创建频道、分组、目录并校验资源归属;WorkspaceChannel 保存 chat/files epoch 与新成员历史策略。
  6. ChannelCrypto.cs:后端计算邀请 epoch/收件人计划,在单事务校验并提交域 envelopes、默认访问权和 epoch;按频道、域、epoch 过滤对象并校验历史起点、日志读取、下载与写入权限。客户端只生成密钥并封装到该计划指定的账号。
  7. channelSend.ts:文本编辑目前是新内容上传成功后删除旧条目。
  8. reduce.ts:作者和资源位置来自后端快照,没有作者签名或签名状态验证。

用参考文档的威胁模型审视当前实现

正常服务和被动泄露

加密模式下,数据库和 blob 不保存资源明文或未封装的空间 K。数据密钥随机生成,不能仅凭登录 token 或数据库中的 verifier 直接推出保险库密钥或内容密钥。 机密性仍依赖密码强度、端点安全和实际密钥分发过程;不能将此结论扩展为对主动恶意服务端的完整防护。

主动恶意服务端

  • 邀请时,发送端从服务端目录读取收件账号公钥。当前没有可信签名身份、公钥固定或授权链验证;服务端替换公钥可使新邀请的密钥包被攻击者解开。
  • 服务端可改写未签名的成员、权限、作者和结构数据。客户端即使正确校验资源 AEAD,也不能据此验证作者、授权链或当前业务归属。
  • 已移除成员仍持有此前领取的频道域密钥。成员移除后的 epoch 轮换未实现;若恶意服务端继续向其提供后续密文,当前协议不能保证其无法解密。
  • 客户端没有验证 Owner 签名授权链;恶意服务端可伪造访问快照、作者归属或返回过期签名状态。

因此,当前实现仍没有达到参考文档对“不可信服务器”的完整目标。现在的后端执行频道/域授权,并由客户端只解开获授权的密钥包;这能收束正常服务下的访问边界,但后端仍是身份目录、授权与事件归属的信任方。后端访问控制与客户端独立验证签名授权,是不同的安全保证。

正确的前后端责任划分

后端继续定义、存储和提供业务实体及接口,包括账户资料、设备目录、空间、频道、分组、文件夹、成员和同步游标。

客户端负责生成和保管私钥、建立设备信任、签署并验证授权、校验 Key Envelope/epoch/消息签名、加解密资源,然后把验证通过的数据交给渲染层。

这些客户端职责不应被当作“重复的业务实现”删除。CryptoProtocol 的结构可以统一定义并生成类型,但密码学验证必须在客户端执行,不能由不可信服务器替客户端宣布验证成功。

当前前端可继续收束的部分

已经由后端定义并生成前端 DTO 的有:账户资料、空间/频道/分组/文件夹、资源位置、频道设置与限制、成员和同步接口。ChannelInfo、Item、PreviewKind 等 camelCase 类型是 UI 或解密后的投影,不是第二份后端实体;AES 帧解析、账号私钥、Key Envelope 和对象 key 也必须留在客户端。

仍有几处边界值得收回或明确:

  1. SavedResource 仍把 endpoint secret、链接等本机凭据与频道名称、成员人数上限等服务端展示快照放在同一结构;账号保险库也同步部分快照。建议拆为本机凭据、服务端快照和传输队列,离线快照只用于只读展示。
  2. Worker 从本地 maxDownloads 提供上传表单初始值;服务端已校验每个 GATED 文件的下载上限,缓存仅影响表单显示,不决定授权。
  3. account.ts 仍包含用户名输入规范化和账号本地恢复编排。服务端负责账户验证、设备 session、个人资料和保险库并发版本;浏览器只生成/持有私钥,并执行解锁加密保险库所需的本地 KDF。

文件名、MIME、大小等 E2EE 正文结构和 file.published 不能移到服务器定义,否则服务器需解析或生成加密正文。有一处重复是有意的:加密文件元数据保留上传时原始 thread/folder,供解密后完整性核对;后端 WorkspaceLocation 表示当前可移动的位置。两者应明确命名为“原始来源”和“当前目录位置”,不能混为一个字段。

参考文档把 Server 定义为不可信,同时要求客户端验证身份、签名授权和事件。后端权威管理账户与空间业务状态并不冲突,但这不代表客户端能防止恶意后端篡改状态。若保留“不信任 Server”的安全目标,后端可负责存储与 API 真值,签名身份、Capability 和 epoch 变更仍须客户端验证;在这些协议实现前,产品只能承诺正常服务下的后端权限执行与内容 E2EE,不能声称抵抗主动恶意服务器。

边界原则:除端到端密钥生成、封装、解密与签名验证外,所有可由请求参数决定的业务规则都必须由后端计算和校验。前端校验、禁用按钮和隐藏菜单只能改善交互,不能决定账户角色、频道成员范围、权限、默认值、资源归属或状态转换。

参考文档还需补足的协议细节

已确认的实施目标(尚未部署)

  • 账号根签名密钥签发设备证书;设备私钥独立生成。新设备通过密码或 Passkey 解锁保险库恢复根密钥,不要求旧设备批准。

  • 首次收到的账号根公钥采用 TOFU 并展示“未核验”;之后密钥变化必须阻止静默更新。可选使用独立渠道/二维码核对指纹。TOFU 不防服务端在首次接触时替换密钥。

  • 第一阶段覆盖频道结构命令与聊天 create/edit/delete 事件。服务端拒绝无效签名并执行权限规则;浏览器同样验证签名、证书和授权链后才更新投影。

  • 现有服务端仍保存/排序/推送密文与事件;明文加解密继续只在浏览器。服务端分配的 seq 不由客户端签名,事件需绑定客户端 event ID、作者设备、类型、频道/epoch 和密文摘要。

  • 可信根:首次确认用户/Owner 签名公钥的方式,避免服务端替换整个身份及授权链。

  • 设备恢复:项目当前选择不要求旧设备审批;需明确密码、支持 PRF 的 Passkey 和不支持 PRF 的 Passkey 各自能恢复哪些账号/频道密钥,以及账号失陷的影响范围。

  • 签名格式:确定签名覆盖的 subject/device/scope/permissions/version/epoch/密钥指纹等字段、规范编码和验证规则。

  • 状态一致性:签名不能单独防止服务端返回旧的合法授权。需要定义版本、前序状态、可信检查点,以及跨设备回滚/分叉的检测范围。

  • 轮换过程:原子提交新 epoch 与剩余设备的 envelopes,拒绝撤销后的旧 epoch 写入,处理离线设备、并发轮换和上传中的文件。

  • 历史读取策略:新成员能否读历史、移除成员后哪些旧 FileKey 需要重新封装。重新封装不能撤回已被保存的旧密钥或明文。

  • 元数据可见性:明确账号资料、频道/目录名称、资源归属和访问时间哪些允许服务端看到。文档中的“用户数据”不能替代字段级隐私范围。

Epoch 轮换可以隔离正确切换后新数据的读取权限,不自动提供完整前向保密。服务端拒绝服务及删除密文仍属于无法单靠加密消除的可用性风险。

按依赖推进的顺序

  1. 确定签名身份、可信根、设备证书与设备解包密钥;定义新协议版本,不迁移旧数据。
  2. 设计 Owner/Admin/Member 的签名 Capability,将管理能力和数据读取权拆开,并让后端给出可验证、原子提交的授权变更计划。
  3. 将账号级 envelopes 升级为设备级 envelopes,明确项目选择无旧设备审批后,Passkey 恢复、设备失陷与私钥复制的边界。
  4. 为成员/设备撤销实现每个 ChannelChat/ChannelFileGroup 的原子 epoch 轮换,覆盖离线成员、并发邀请和上传中的资源。
  5. 引入签名 ChatEvent、作者验证、可信同步检查点,再接入客户端投影和界面。
  6. 拆分 SavedResource 中的后端快照与本机密钥/队列,移除 Worker 对缓存权限字段的业务判断,在线操作以服务端授权为准。
  7. 根据多实例和存储规模需求替换数据库、事件通知和对象存储适配器。

已完成的后端 CRUD、契约生成、Worker 分层、频道数据域、对象 key 封装、分帧 AEAD、完整性检查及持久化重试可以作为基础;下一阶段重点应转向可验证授权、设备级密钥分发、撤销轮换和前端缓存收束。