技术文档

MLS 设备与加密事件协议

仓库已实现Rust MLS核心、服务端交付接口和iOS独立传输器。当前iOS主ChatStore与Android主聊天仍发送普通text/media,附件也未接入客户端密文封装。本页解释协议状态与恢复要求,不把模块存在等同于全量端到端加密或生产安全审计通过。

身份、编码与职责

Rust固定使用MLS 1.0与MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519。设备credential串联聊天用户ID与设备ID,签名公钥32字节;会话scopeId的UTF-8字节用作MLS GroupId。

对象 关键字段与约束
Device userId、deviceId、signatureKey;deviceId为1–80位字母数字/下划线/短横线
KeyPackage TLS编码MLS字节;1–65536字节;标准base64放入JSON;id为包内容SHA-256
Scope scopeKind、scopeId、mode、creatorUserId、creatorDeviceId;blocked内部另记blocked_after_sequence
Event sequence、clientId、kind、senderUserId、senderDeviceId、payload、createdAt、可选targetUserId/targetDeviceId
Recipient sequence、userId、deviceId、acknowledgedAt;服务器按设备决定收件与确认

服务端验证密钥包的协议签名、套件和账号/设备匹配,以及非Welcome封装的消息类型和群ID;终端负责真正的MLS认证与解密。Welcome封装服务器检查类型、声明收件设备与包可用性;加入终端还必须验证群ID、邀请者credential和签名公钥,不能仅信任HTTP元数据。依据:chat-crypto/src/lib.rs:15-16,48-98,270-309。

MLS内容密文并不隐藏全部交付元数据。服务端仍保存范围ID、发送账号和设备、收件设备、事件种类、密文字节数、时间与确认状态,并利用当前成员目录决定谁能领取事件。协议中的加密内容保护不能推导出匿名通信、隐藏社交关系或服务器完全不知道消息活动;公开文档也不应将这些可见元数据称为已加密正文的一部分。依据:chat-service/src/e2ee-delivery.mjs:36-52,235-255。

注册、目录与claim

设备POST /e2ee/devices/{deviceId}/key-packages注册自己的包。相同用户与deviceId已存在时,签名公钥必须相同,否则DEVICE_KEY_CHANGED;每设备最多25个未用包。GET本设备key-packages返回registered、available。iOS登录后通常补到3个可用包,但注册本身不建立加密聊天,也不允许仅据此显示锁标记。

目录GET /e2ee/{conversations|groups}/{id}/devices?deviceId=...给出当前参与者的全部已注册设备,并为每设备挑最早可用包。目录读取不预留、不claim包;发起者必须准备应对另一个请求先使用同一包。只有成功写入定向Welcome的事务,才把该包claimed_sequence设为对应事件序号。

同一Welcome的完全相同重试先命中事件去重,仍返回原sequence,不会再次消费包;改新clientId却引用已claim的包得到PACKAGE_USED。不能把公开目录返回的keyPackageId当永久可复用邀请凭证。新设备首次注册会把该用户已有active范围改blocked,记录当时边界;此操作没有另写普通事件提醒,客户端需要重查模式。依据:chat-service/src/e2ee-delivery.mjs:76-145,187-255。

初次建立的次序

  1. 每位当前参与者至少注册一个设备,发起设备调用prepare。仅legacy允许,成功后进入preparing并固定协议创建者账号和设备。
  2. 创建者在本机建立MLS群;对每个尚未joined且未invited的目标读取包,核对或固定已知签名公钥。
  3. Rust add_member生成Commit与Welcome;先以独立clientId提交Commit并确认本机pending commit,再提交定向Welcome及包ID。
  4. 目标设备按sequence读取自己的Welcome,验证邀请者和群ID,持久化本地状态,然后发welcome-acks。
  5. 已加入设备处理其收到的后续Commit并持久化,发送commit-acks;创建者发送的Commit由本机完成后推进自己的sequence。
  6. 创建者调用activate。服务器确认所有当前注册设备均已加入并处理必要Commit,返回mode=active;此前不能发送application。

preparing只能由创建者建立群。目录中的joined不等于界面在线:它是创建者身份或Welcome已有确认的结果。一个用户有多台已注册设备时,不是只等每用户任意一台确认;离线旧设备也可能阻止激活。依据:ios-app/Sources/Features/Chat/ChatEncryptedTransport.swift:76-150、chat-service/src/e2ee-delivery.mjs:159-172,301-354。

状态迁移与成员变化

当前状态 允许推进 被禁止或要求等待的动作
legacy prepare→preparing prepare前application为NOT_PREPARED
preparing 创建者Commit/Welcome;收件设备ack;创建者activate→active application为NOT_ACTIVE,其他设备不得建立群
active application;创建者新Commit可使其blocked 直接新Welcome会AWAITING_COMMIT
blocked 创建者新Commit、必要Welcome和ack;完成后activate→active application首先得到NOT_ACTIVE;不能回退普通消息发送

群成员增加、移除或退出、删除好友、屏蔽以及新设备注册会阻塞已有范围。blocked恢复需要边界之后的新Commit;每个剩余设备必须处理该Commit,或通过边界之后的新Welcome加入。activate还检查每设备后续未确认Commit。AWAITING_REKEY、AWAITING_WELCOME和AWAITING_COMMIT是不同等待条件,不该通过循环activate掩盖缺失步骤。

协议创建者设备与个人群owner不同,群主转移不会自动转移创建者权限;当前没有公开创建者迁移或设备注销路由。iOS独立prepare遇blocked直接返回模式,未自动实施完整重键协调,不能把服务端恢复契约描述成已交付的客户端自动恢复。依据:chat-service/src/e2ee-delivery.mjs:201-211,301-363、ios-app/Sources/Features/Chat/ChatEncryptedTransport.swift:78-94。

应用消息、游标与去重

POST events的payload最多1048576字节,JSON最多1500000字节,kind只允许welcome/commit/application。事件幂等键为发送用户+设备+clientId,同一次重试还要保持范围、kind、payload摘要、目标与包ID不变,否则CLIENT_ID_REUSED。重新加密同一业务文字会产生不同密文,不能再用原clientId提交。

GET events按当前设备收件、sequence升序、每页最多100。sequence是全表自增值,并非每会话连续无缺口;其他范围和不发给本设备的事件会造成合法跳号。普通/events的id是另一游标,e2ee通知中的messageId可能引用MLS sequence,但仍不能拿它替换普通事件id。

iOS独立文字payload含version=1、kind=text、clientID、text,最多65536个UTF-8字节。设备先把待发送原密文保存,再HTTP提交;响应成功后转为本地已发送消息。接收先验证MLS发送者,再与服务端senderUserId/senderDeviceId比对,解析payload并保存消息与sequence。处理或保存失败不得跳过并推进游标。

Welcome/Commit ack先保存在本地,HTTP确认成功才移出pending;确认丢失可重复提交。自身application回显不会再用接收棘轮解密,而是确认排队消息并推进序号;若既无排队项也无已确认本地消息,按状态异常处理。依据:ios-app/Sources/Features/Chat/ChatEncryptedTransport.swift:153-263、ios-app/Sources/Features/Chat/ChatCryptoDevice.swift:442-510。

信任固定与本地保存失败

iOS把远端账号、设备和签名公钥的对应关系固定在本机Keychain;已知设备的公钥变化时拒绝继续,首次见到设备则返回firstSeen标识。仓库有根据双方身份和公钥生成安全码的函数,但不能把这个辅助函数等同于主聊天已提供完整的用户核对流程,更不能等同于服务端目录透明性或独立身份审计。依据:ios-app/Sources/Features/Chat/ChatCryptoTrustStore.swift:8-81。

加密设备操作通过persisted包装:执行后保存checkpoint,若处理或落盘失败则重新读取上次磁盘状态;连恢复也失败,就把设备标为unusable并拒绝继续使用,避免在内存状态与已保存游标不一致时继续发送。checkpoint同时包含原生MLS状态、消息、游标、待发密文、待确认记录和未完成邀请。不能只备份一个sequence数字再丢掉加密群状态;重新生成设备也不能解开旧状态。依据:ios-app/Sources/Features/Chat/ChatCryptoDevice.swift:553-605。

正常与失败例

正常:甲设备创建preparing,提交C1与发给乙的W1;乙验证W1、保存并ack;甲activate成功后提交A1,响应丢失则沿用同一clientId与同一密文重试,服务端返回原sequence。序号可能为101、104、109,不要求连续。

失败:乙尚未ack Welcome就activate,得到409 AWAITING_WELCOME;换一个已经注册的deviceId却使用不同签名密钥,得到409 DEVICE_KEY_CHANGED;成员变化后直接继续application,得到409 NOT_ACTIVE;把base64编码的普通JSON冒充MLS payload会INVALID_EVENT。错误并不授权回退text/media明文路径。

本页仅说明源码协议。iOS主ChatStore目前只准备包、实际send仍普通messages;Android未接入同等MLS传输;普通文件、新事、位置和音视频均不能据此声明已使用MLS。详见消息与媒体、本地数据格式、安全和隐私与验收矩阵。

源码核对范围:2026-10-23 的当前实现。本轮未执行生产业务请求或原生设备验收。

Markdown 原文完整文档文本