跳转到内容

安全与加密边界

Hashee 的安全模型不是”我们承诺保密”,而是”我们没法不保密”—— 后端架构上只能看到密文,不能解密、不能验签、不能 forge 用户消息。 这一页讲 SDK 内部和后端契约的分界线、各连接模式的安全差异、以及作为 Agent 开发者 能做什么、不能做什么、丢密钥后能不能恢复。

技术真源:

  • E2EE 规范 (E2EE 6 层栈唯一真源)
  • .claude/rules/security-e2ee.md (强制规则)

6 层加密栈速读

每条 Hashee 消息(H2H / H2A / H2G)穿过 6 层:

算法作用
L1ContentAES-256-GCM + AAD内容加密;CEK 一次性
L2WrapX25519 ECDH + HKDF + AES-256-GCMper-recipient 包裹 CEK
L3SignEd25519签发件人,不可否认
L4RatchetSignal Double Ratchet前向保密 + 后向保密(仅 WS 模式)
L5X3DHSignal X3DH异步初始密钥协商(仅 WS 模式)
L6Grant LedgerPostgres + append-only trigger数据授权审计

群聊(H2G)走简化栈:L1 内容 + L2 wrap group key + L6 grant,不走 L3/L4/L5 (用 group key version rotation 替代 forward secrecy)。

后端能看到的 vs 看不到的

字段后端可见?备注
conversation_id, sender_id, recipient_id✓ 元数据用于路由与计费
created_at, client_message_id幂等去重用
encrypted_payload (整段 wire envelope)✓ 但是密文后端只看字节
attachment_id 引用 R2 对象✓ R2 对象 metadata内容也加密
agent_id, user_id 公钥路由必需
消息明文永不解密
CEK永不见 unwrap 后形态
私钥(X25519 / Ed25519)永不持有
Ed25519 签名是否合法不验,留给客户端
Ratchet state仅在客户端

私钥永不落后端

Agent 持有两对私钥(X25519 wrap + Ed25519 sign),完全不发给 Hashee 后端

HasheeAgent.init() 启动
├─ 检查 onKeyGenerated 回调里持久化的私钥
├─ 没有 → SDK 内部生成(crypto.subtle)
├─ 计算公钥
└─ POST /agents/:id/keys/register
body: { x25519_public, ed25519_public }
✗ 不含任何私钥字节 ✗

后端只持有:

  • agents.x25519_public_key (raw 32 bytes)
  • agents.ed25519_public_key (raw 32 bytes)

如果你 Agent 进程被攻破:攻击者拿到的私钥可以解密未来的消息(直到你旋转 / 删除 Agent), 但拿不到历史消息(如果走 WS 模式,Ratchet 提供前向保密——历史消息 CEK 已 forget)。

Webhook vs WebSocket 安全差异

维度WebSocketWebhook
L1 内容加密✓ AES-256-GCM✓ AES-256-GCM
L2 per-recipient wrap✓ X25519✓ X25519
L3 Ed25519 签名
L4 Double Ratchet
L5 X3DH
L6 Grant Ledger
HMAC 签名 webhook 请求n/a✓ HMAC-SHA256 + ±5min ts

Webhook 跳过 L4/L5 是因为无状态环境无法维护 ratchet state(每次冷启动会丢)。 对绝大多数应用够用——L1-L3 已经达到银行级”per-message 内容加密 + 签发件人不可否认”。

但要注意:L4 提供的 forward secrecy 在 Webhook 模式没有。如果你 Agent 私钥泄露, 攻击者能解密所有历史 inbound 消息(只要他能拿到密文)。

高敏感场景(医疗、金融、隐私顾问)推荐 WS 模式。

CEK 生命周期

Agent 进程内(永不离开):
1. generateCek() → 32 字节随机
2. 用 CEK 加密 plaintext (Layer 1)
3. 用收件人 X25519 公钥 wrap CEK (Layer 2)
4. 用 Agent Ed25519 私钥签 envelope (Layer 3)
5. 把 wire envelope (含 wraps + signed content + signature) 序列化
6. 通过 WS / HTTP 发出去
7. CEK 立即 zero-fill 然后丢弃 (永不存盘)

CEK 永远不写盘、不发后端、不落 log。SDK 在 send 完成后立即 cek.fill(0)

AAD 绑定(防协议混淆)

Layer 1 加密的 Additional Authenticated Data:

content_aad = conversation_id (16B) || epoch_id (8B BE) || "hashee-envelope-v1.1" (20B)
= 44 字节固定结构

意义:

  • 同一 CEK + 同一密文,换 conversation_id 就解不出来 → 防”密文搬运”。
  • 换 epoch_id 也解不出来 → 支持密钥轮换的 forward secrecy。
  • wire 字面量绑死 → 防协议混淆攻击(v1.1 密文不能被解析为 v1)。

群聊有自己的 AAD:

group_aad = conversation_id (16B) || group_key_version (8B BE) || "hashee-group-v1.2" (20B)

key version 增加时旧 group key 无法解新消息 → 移除被踢成员后必须轮换。

SDK 用户能做 / 不能做

能做

  • ✓ 生成自己的 X25519 + Ed25519 keypair → 注册到 Agent
  • ✓ 接收入站消息(SDK 自动解密)→ 应用业务逻辑 → 发送回复
  • ✓ 上传文件(SDK 自动加密)到 R2
  • ✓ 发送 artifact、artifact_update、tool_call
  • ✓ 声明 Capability Manifest + 处理 tool_response
  • ✓ 流式输出(delta 模式)
  • ✓ 在 WebSocket / Webhook / Polling 模式间切换
  • ✓ 用 CLI 进行本地测试
  • ✓ 显式调用底层 L1-L3 API(非标准场景,如自定义 transport)

不能做

  • ✗ 改写或包装用户消息(违反盲管道不变量;客户端会拒)
  • ✗ 直接调用 Ratchet API(Webhook agent 没有 ratchet state;WS agent 由 SDK 内部管理)
  • ✗ 绕过 aadContext 强制(v0.2.0 会 throw AadContextRequiredError
  • ✗ 伪造签名或修改已签名的消息(Ed25519 验签会失败)
  • ✗ 访问其他 Agent 的私钥或对话
  • ✗ 在后端存储 private keys(CEK / identity key 永不离开客户端)
  • ✗ 用 SDK 从后端读取用户消息明文(后端本身不持有明文)
  • ✗ 让 Agent 替用户在群里”宣告”内容(mention / artifact 必须从用户触发的对话出发)

私钥丢失能不能恢复

不能——盲管道前提下后端没法做托管恢复。

如果两份备份(热 + 冷)都丢了:

  1. 在 Hashee app 让系统 Agent 删除现有 Agent
  2. 用户对你 Agent 的 H2A 关系会被同步取消并收到通知(cascade delete with notification)。
  3. 重新创建 Agent → 拿到新 agent_id + agent_token + 新私钥。
  4. 历史消息无法恢复(前向保密的代价)。

为了避免这个,请按 Agent 身份与密钥 里的”备份与恢复”做好两层备份。

Ratchet 状态(只 WS 模式)

L4 Double Ratchet 的状态:

  • DH ratchet keypair:每条消息一对新 ephemeral X25519
  • Sending chain key:派生发出消息的 CEK
  • Receiving chain key:派生解入站消息的 CEK
  • Skipped message keys:留着解乱序到达的旧消息

SDK 把这些状态存在内存(WS agent 长驻)。重启 → 状态丢 → 仍能解新消息(X3DH 重做), 但乱序到达的旧消息可能解不出来。这是为什么我们推荐你做”持久化 ratchet state” (V1.1 路线图)。

V1 当前 acceptable trade-off:少量乱序未解消息 → 客户端会重发 + 显示”无法解密” banner 让用户重新触发。

哪些行为会被 CI / hook 阻断

.claude/rules/security-e2ee.md §2 有完整禁字识别表。简言之你不应该:

  • import 任何 legacy-cryptoencryptH2HdecryptH2Hx25519-aes256gcm-v1 等老 API
  • 在 backend 代码(apps/api / apps/queue-consumer / apps/cron)里 import 解密函数
  • 在 mobile facade(apps/mobile/src/{hooks,screens,stores,components})外 import L1-L5 原语

如果你 fork 了 SDK 或扩展自定义功能,CI 会在这些边界报错。

检查清单(Agent 上线前)

  • 私钥放在 Secret Manager / Vault / 加密文件,不在 git
  • .gitignore.env / keystore.json
  • 部署到 prod 前用 staging API 跑一遍密钥注册(避免 prod 出 AGENT_KEYS_MISMATCH
  • 写了”两层私钥备份”的 runbook(hot store 自动 + cold store 手动)
  • 监控 decrypt_failure rate,> 1% 时报警
  • 监控 status 长期 != connected 时报警
  • 处理 relation.revoked 事件时及时清理本地用户数据(GDPR)
  • 处理 agent.capability_changed 事件时记录 diff(合规审计)

下一步