ADR 0007:端到端加密——服务端只转发密文
- 状态:已采纳(2026-09-28)
- 决策者:zhibo li
- 延续:ADR 0005(大模型调用放到用户设备上)、ADR 0006(服务端只做中转)
用户在手机或另一台电脑上接电话时,设备之间没法直接连接:家里的电脑在 NAT 后面,不开入站端口;手机退到后台会被系统挂起,锁屏时响铃还要靠推送。所以服务端这个中转必须保留。
ADR 0006 之后,服务端虽然在来电结束后删掉内容,但待接听期间仍然保存汇报原文、简报、工作目录,以及用户的回复全文。这些都是用户的隐私。
三类数据:
- 简报和汇报:daemon 生成,经服务端送到客户端。
- 挂断后的回复:客户端写好,经服务端送回 daemon,再发到 Multica 或接着原会话运行 Agent。
- 通话中和大模型的问答:ADR 0005 起就由客户端直接请求用户自己的大模型,不经过服务端,这里不涉及。
- 第 1、2 类都在用户设备之间端到端加密。 daemon 和客户端持有同一把密钥,服务端只存、只转发密文,没有密钥。
- 算法:AES-256-GCM。每条消息随机 12 字节 IV,16 字节认证标签附在密文后。AAD 标明用途,防止把一种密文当成另一种重放:
- 汇报:
outbrief:report:v1 - 某个事件的回复:
outbrief:reply:v1:<eventId> - 某个回复的失败原因:
outbrief:reply-error:v1:<replyId>
- 汇报:
- 格式:
ob1.<keyId>.<iv>.<密文>。IV 和密文用 base64url 编码;keyId取 SHA-256(“outbrief-key-id” ‖ key) 的前 8 字节,用 hex 表示。客户端据此区分「密钥不一致」和「数据损坏」,不必逐个尝试解密。 - 密钥: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_…密钥。 - 不采用「内置固定密钥」:代码和安装包都能拆开,内置密钥等于没有加密。
- 默认:daemon 第一次
- 密文里放什么、明文留下什么:
- 汇报密文
SealedReport:标题、原文、cwd、sessionId、简报、Multica issue 信息(工作区、issue、标题、所属项目、优先级和更新时间、Agent、评论 id)。 - 回复密文
SealedReply:回复内容,以及回到哪个会话或哪条评论。 - 明文只留服务端路由要用的:来源、时间、上报的机器、Multica 任务 id(去重,同一任务只来一次电)、状态、投递状态、Multica 回复的评论 id。
- 回复失败的原因由 daemon 加密。daemon 自己解不开回复时(密钥不一致),会回一个不含任何内容的明文错误,否则用了另一把密钥的客户端也看不到原因。
- 汇报密文
- 失败时不降级为明文:客户端解不开的来电不响铃,空闲页显示「无法解密」和原因,可以「忽略」;服务端拒收一切明文字段。
- 删除服务端兜底 TTS(
/v1/tts、tts_usage表、AZURE_SPEECH_*):它会把简报的每一句明文发给服务端。客户端直接调用 Azure 合成失败时,照原逻辑提示失败,由用户选择重试或忽略。 - 迁移
010:把还没接听的旧明文来电记为已拒绝,把还在排队的旧回复记为失败;删除明文列和briefs表,新增sealed列。
- daemon:
src/e2e/crypto.ts(Nodecrypto)、src/e2e/keyring.ts。 - 客户端:
src/e2e/crypto.ts(WebCrypto)、src/e2e/events.ts。 - 两边的单测使用同一组固定的测试向量(固定密钥、IV、口令),保证逐字节一致。
- 服务端被攻破或数据库泄露时,拿到的只有密文和路由信息。
- 换密钥后,旧密钥加密、还没接听的来电无法解密。已经存到本机的历史不受影响,因为本地存的是解密后的内容。
- 在没有 daemon 的设备(以后的手机端)上,要手动输入同一口令或密钥;扫码配对以后再做。
- 服务端无法再校验简报的结构,由 daemon 和客户端各自按
SealedReport校验。