跳转到内容

ADR 0007:端到端加密——服务端只转发密文

  • 状态:已采纳(2026-09-28)
  • 决策者:zhibo li
  • 延续:ADR 0005(大模型调用放到用户设备上)、ADR 0006(服务端只做中转)

用户在手机或另一台电脑上接电话时,设备之间没法直接连接:家里的电脑在 NAT 后面,不开入站端口;手机退到后台会被系统挂起,锁屏时响铃还要靠推送。所以服务端这个中转必须保留。

ADR 0006 之后,服务端虽然在来电结束后删掉内容,但待接听期间仍然保存汇报原文、简报、工作目录,以及用户的回复全文。这些都是用户的隐私。

三类数据:

  1. 简报和汇报:daemon 生成,经服务端送到客户端。
  2. 挂断后的回复:客户端写好,经服务端送回 daemon,再发到 Multica 或接着原会话运行 Agent。
  3. 通话中和大模型的问答:ADR 0005 起就由客户端直接请求用户自己的大模型,不经过服务端,这里不涉及。
  1. 第 1、2 类都在用户设备之间端到端加密。 daemon 和客户端持有同一把密钥,服务端只存、只转发密文,没有密钥。
  2. 算法:AES-256-GCM。每条消息随机 12 字节 IV,16 字节认证标签附在密文后。AAD 标明用途,防止把一种密文当成另一种重放:
    • 汇报:outbrief:report:v1
    • 某个事件的回复:outbrief:reply:v1:<eventId>
    • 某个回复的失败原因:outbrief:reply-error:v1:<replyId>
  3. 格式:ob1.<keyId>.<iv>.<密文>。IV 和密文用 base64url 编码;keyId 取 SHA-256(“outbrief-key-id” ‖ key) 的前 8 字节,用 hex 表示。客户端据此区分「密钥不一致」和「数据损坏」,不必逐个尝试解密。
  4. 密钥:32 字节,文本形式为 obk1_<base64url>。
    • 默认:daemon 第一次 run 时随机生成,保存在 ~/.outbrief/daemon.json 的 e2e(权限 600)。本机桌面端通过本机接口 GET http://127.0.0.1:8790/e2e/key 自动获取。该接口需要服务端令牌,并且只接受回环地址的 Host。
    • 用户自定义:在客户端「设置 → 加密」输入口令(至少 12 个字符),用 PBKDF2-HMAC-SHA256(固定盐 outbrief-e2e-v1,60 万次)派生出密钥。同一口令在每台设备上得到同一把密钥。客户端同时通过 PUT /e2e/key 把本机 daemon 换成同一把密钥;只保存派生出的密钥,不保存口令。也可以粘贴另一台设备上的 obk1_… 密钥。
    • 不采用「内置固定密钥」:代码和安装包都能拆开,内置密钥等于没有加密。
  5. 密文里放什么、明文留下什么:
    • 汇报密文 SealedReport:标题、原文、cwd、sessionId、简报、Multica issue 信息(工作区、issue、标题、所属项目、优先级和更新时间、Agent、评论 id)。
    • 回复密文 SealedReply:回复内容,以及回到哪个会话或哪条评论。
    • 明文只留服务端路由要用的:来源、时间、上报的机器、Multica 任务 id(去重,同一任务只来一次电)、状态、投递状态、Multica 回复的评论 id。
    • 回复失败的原因由 daemon 加密。daemon 自己解不开回复时(密钥不一致),会回一个不含任何内容的明文错误,否则用了另一把密钥的客户端也看不到原因。
  6. 失败时不降级为明文:客户端解不开的来电不响铃,空闲页显示「无法解密」和原因,可以「忽略」;服务端拒收一切明文字段。
  7. 删除服务端兜底 TTS(/v1/tts、tts_usage 表、AZURE_SPEECH_*):它会把简报的每一句明文发给服务端。客户端直接调用 Azure 合成失败时,照原逻辑提示失败,由用户选择重试或忽略。
  8. 迁移 010:把还没接听的旧明文来电记为已拒绝,把还在排队的旧回复记为失败;删除明文列和 briefs 表,新增 sealed 列。
  • daemon:src/e2e/crypto.ts(Node crypto)、src/e2e/keyring.ts。
  • 客户端:src/e2e/crypto.ts(WebCrypto)、src/e2e/events.ts。
  • 两边的单测使用同一组固定的测试向量(固定密钥、IV、口令),保证逐字节一致。
  • 服务端被攻破或数据库泄露时,拿到的只有密文和路由信息。
  • 换密钥后,旧密钥加密、还没接听的来电无法解密。已经存到本机的历史不受影响,因为本地存的是解密后的内容。
  • 在没有 daemon 的设备(以后的手机端)上,要手动输入同一口令或密钥;扫码配对以后再做。
  • 服务端无法再校验简报的结构,由 daemon 和客户端各自按 SealedReport 校验。