Skip to content

E2EE 密钥全生命周期

对应 gap-matrix D4(生命周期各环节散落各 ADR)。审计场景下的单一入口: 每一类密钥材料,从生成到销毁,各环节由哪段代码执行、在哪里能验证。 最后核对:2026-08-02

取代 e2ee-key-rotation-policy.md —— 该文档写于 RSA 时代, 其协议规格、存储清单与恢复方案均已失真,详见本文末尾"被取代文档"一节。

0. 一句话结论

服务端从不接触任何明文私钥。 服务端只存放公开材料(公钥、一次性公钥、fallback 公钥) 与用户口令派生密钥加密后的备份密文。所有私钥生成、封装与解封装都在客户端完成。

1. 密钥清单 × 生命周期矩阵

1.1 客户端持有(私密材料)

密钥生成存储轮换可备份销毁
Olm identity(Curve25519 + Ed25519)首次启用 E2EE 时 vodozemac 生成olm_* account pickle @ secure storage不轮换(= 设备身份)✅ 随备份登出清理 / 设备吊销 / 注销
Olm one-time keys(OTK)批量生成后上传公钥account pickle 内低水位 5 → 补到 50✅ 随备份同上;被 claim 即服务端删
Olm fallback key同上account pickle 内7 天✅ 随备份同上
Olm session(双棘轮)X3DH 首次握手olm_* session pickle棘轮逐条前进有意不备份同上
Megolm outbound session首次群发时内存 + pickle三触发器(见 §2.2)轮换即弃
Megolm inbound session收到 room key 时megolm_inbound_*随 outbound 轮换新增随备份登出清理 / 注销
备份口令派生密钥PBKDF2 310,000不落盘(用完即弃)随用户改口令内存生命周期内
SQLCipher DB key首次建库db_cipher_key_<uid>不轮换登出清理(删后旧库永久打不开)
RSA legacy历史遗留e2ee_*不再生成登出清理
mls_*预留前缀,尚无写入方登出清理已覆盖

前缀权威清单:imboyapp/lib/service/e2ee/e2ee_secret_inventory.dart secretKeyPrefixes新增任何 E2EE 存储键必须落在这些前缀之下,否则登出清理与其测试都覆盖不到。

为什么 Olm session 有意不备份:Olm session 状态含发送侧棘轮位置。跨设备恢复 会造成密钥重用 + 棘轮分叉,前向保密与后向保密同时失效。Megolm inbound session 是只读的(无发送棘轮),备份安全。这也是单聊历史换设备后不可恢复的根因, 详见 history-recoverability.md。Signal 与 Matrix 同此取舍。

1.2 服务端持有(公开材料 + 密文)

内容敏感度
olm_identity设备身份公开
olm_one_time_key一次性钥池公开,claim 即删
olm_fallback_keyfallback 钥 + Ed25519 签名公开
olm_otk_claim_requestclaim 幂等/租约记录(迁移 49)元数据
e2ee_key_backups口令派生密钥加密后的密文(迁移 36)密文,服务端无法解
e2ee_local_backups客户端本地备份上传的密文同上
user_devicedevice_id / public_key / key_id公开
trust_audit设备信任决策流水(actor_uid / target_uid)PII 元数据

2. 各环节实现位置

2.1 生成与分发

  • 身份与 OTK 发布:客户端生成 → POST /api/v1/olm/* 上传公钥。
  • OTK 领取claim_one_time_key 取走即删(一次性语义),幂等由 olm_otk_claim_request 保证。
  • room key 分发:Megolm room key 经 Olm 一对一信道逐设备加密下发(ADR 13 v2/13-room-key-over-olm.md),服务端只见密文。分发列表上限 4096 条,防超大列表 DoS。
  • fallback key 验签report_fallback_key/5 做 canonical 单射守卫 → 查已注册 ed25519 → 验签。 ⚠️ 签名当前仍非必填(见 §4 残留)。

2.2 轮换

对象触发位置
Megolm outbound①成员/设备集合变化 ②消息数 ≥ 100 ③存活 ≥ 7 天group_session_service.dart _maxMessagesPerSession / _maxSessionAgeMs
OTK 池余量 < 5 → 补到 50olm_session_service.dart:30,33 + e2ee/otk_refill_policy.dart
Fallback key7 天e2ee/fallback_rotation_policy.dart:11
Olm session每条消息棘轮前进(协议内建)vodozemac

任何成员/设备集合变化都整体轮换 + 全量重分发,不做"仅新增设备定向补发"。

2.3 备份与恢复

口令 → PBKDF2(310,000) → 加密 → 上传密文。服务端全程只见密文,口令丢失不可恢复,这是设计。 备份纳入 Megolm inbound session,故换设备后群聊历史可恢复、单聊历史不可(§1.1 注)。 备份内容的前缀白名单及其反向断言测试见 megolm_backup_section.dart

2.4 销毁(三条路径,互不重叠)

场景入口行为
登出 / 换号E2eeSecretInventory.purgeAll内存缓存 → 临时产物 → 按前缀删持久化键 → 复核零残留;任一失败抛 E2eeSecretPurgeException,调用方必须阻止建立新会话(fail-closed)
设备吊销user_device_ds:delete/2先删设备行(= token 吊销),再 olm_identity_repo:delete_by_device/2 级联清 Olm 三表。顺序不可颠倒:反过来若删行失败,会变成"密钥没了但 token 还有效"的最坏组合
账号注销user_ds:delete_all_related_data/2事务内显式删 olm 三表 + e2ee_key_backups + e2ee_local_backups

⚠️ 注销路径不触发设备吊销级联:该函数删 user_device 走的是本函数内的直接 SQL, 不经 user_device_ds:delete/2。所以 olm 三表必须在注销路径里再显式删一次。 少了它,账号注销后别人仍能 claim 到它的 OTK、与一个不存在的账号建立 Olm 会话。 这一点由 test/ds/user_ds_tests.erl 的删除维度断言钉住(每张 olm 表只能用本人 uid 作条件—— 写错维度会清掉别人的密钥,光断言"调用发生过"抓不到)。

3. 可验证性(审计员怎么自己查)

主张验证方式
服务端不碰明文私钥olm 三表 schema 无任何 private/secret 列grep -i "private|secret" priv/migrations/*olm*.up.sql 零命中);grep -rn "private_key" src/ 的命中全部是支付宝网关 / APNS 推送 / SSO 配置,与 E2EE 无关
登出确实清干净E2eeSecretInventory.purgeAll 删完 readAll() 复核残留;对应测试断言残留为空
吊销确实级联test/ds/device_revocation_tests.erl 路径 4(4 例),含"先删设备行再清 Olm"的顺序断言
注销确实级联test/ds/user_ds_tests.erl delete_all_related_data_cascades_olm_tables_test_
单聊历史确实不在备份里megolm_backup_section.dart 前缀白名单 + 其反向断言测试

4. 残留缺口(如实列出)

缺口状态
trust_audit 在账号注销时未清🔒 审计留存 vs 被遗忘权是政策判断,需显式拍板,不在代码里默默删
fallback key 签名非必填今天若强制必填,无客户端发签名 → 所有设备发布不了 fallback key → 每次耗尽变 no_prekey_available(可用性事故)。用 olm_fallback_unsigned_total 指标判断第二阶段启动时机(E2EE-062)
换设备的备份提醒 UI 未加P3-1 残留
交叉签名 / SAS 验证算法在、UI 零接线(gap-matrix B1,P3-4/P3-5)

5. 被取代文档

e2ee-key-rotation-policy.md(333 行,写于 RSA 时代)以下内容已不成立, 保留原文仅为追溯历史决策,不可作为当前实现的依据:

  • §1.1 把 RSA-2048 / RSA-OAEP-256+AES-256-GCM 描述为主协议 —— v2 已 Olm-only cutover, RSA 降为仅解密(ADR 24 v2/24-unified-olm-only-and-rsa-decrypt-only.md)。
  • §1.2 存储清单只列 RSA 公私钥 + device_id/key_id,漏掉了几乎全部真实密钥材料: Olm account pickle、Olm session pickle、Megolm inbound session、OTK、fallback key、SQLCipher DB key。
  • §5.2 社交恢复(Shamir Secret Sharing) —— 已从代码删除(grep -rn "splitSecret\|Shamir" lib/ 零命中)。
  • §4 设备迁移流程引用的 e2ee_transfer_handler —— 已删除(迁移 38 drop_e2ee_social_transfer)。

IMBoy — 企业私有化即时通讯平台