技术文档

消息、媒体与 MLS 数据流

星线 App 当前主聊天界面通过普通消息 API 发送文字或媒体引用。仓库另外实现了 Rust MLS 核心、服务端加密事件流和 iOS 独立加密传输器。本页分别说明实际消息路径与 MLS 协议,尤其明确附件是否经过客户端加密,避免把已有加密模块扩大描述为所有聊天数据已经端到端加密。

当前普通消息路径

客户端在单聊调用 /conversations/{id}/messages,在群聊调用 /groups/{id}/messages。请求必须包含合法 UUID clientId,并在非空 text 和一个 mediaId 中二选一。文字会 trim,最多 2000 个 Unicode 码点。消息写入后返回 {message};首次写入为 201,完全相同的重试为 200。

客户端应为一次用户发送动作生成一次 clientId,失败重试沿用该 ID。服务端不仅检查 ID,还检查范围、正文和媒体是否一致;复用 ID 发送不同内容得到 409 CLIENT_ID_REUSED。客户端可先展示 sending,再按回执或同步结果合并,网络失败展示 failed 并提供重试。HTTP 成功代表服务端已接受,不能单凭它显示“对方已读”。

单聊发送要求当前仍是好友且双方均未屏蔽;群发送要求当前是成员。普通消息表保存服务端可读正文和媒体引用。当前 iOS 主 ChatStore 与 Android 发送路径均使用这些接口;注册 MLS 密钥包不会自动改变这条链路。

分页、已读与删除

单聊列表支持 after 增量或 before 旧消息分页,非零 after 不能与 before 同时使用;群消息只支持 before。默认 50 条、最多 100 条,返回顺序为时间线升序。最新页和 before 页内部从数据库倒序取数后再反转,因此界面不需要额外反转。第一页不要传 before=0 代替“没有游标”。

已读接口发送 {lastMessageId},必须指向当前范围的实际消息,已读位置只前进。单聊回执使用对方 peerReadMessageId,群的已读信息按群模型处理。删除消息只对当前用户隐藏;删除聊天还会推进个人历史可见边界。恢复聊天不会恢复已越过的旧历史,也不会向其他参与者发起撤回。

附件实际上传与访问

先用原始二进制请求体调用 POST /media?kind=file&name=...,成功返回 {media:{id,kind,name,mimeType,size,createdAt}},再把 mediaId 放入消息发送请求。请求体不是 multipart,也不是 base64 JSON。当前实现直接保存上传字节;客户端未生成独立附件密钥,未先把照片或文件封装为密文,也没有下载后的附件解密步骤。因此不能将这些附件称为 MLS 加密附件。

类型 单项限制 其他限制
普通文件、聊天照片 10 MiB,即 10*1024*1024 字节 非空;MIME 需符合类型格式
自定义贴纸 512 KiB 有效 PNG/JPEG;PNG 宽高均为 1–2048;每账号最多 100 个自定义贴纸
聊天媒体总量 每账号 100 MiB 普通文件与贴纸共同计入字节配额
文件名 最多 120 个 Unicode 码点 禁止控制字符、斜杠与反斜杠

GET /media/{id} 要求 Bearer token。所有者可下载,其他用户必须能看到引用该媒体的消息,且不能越过个人历史边界或单条隐藏记录。返回 Content-Disposition: attachment、nosniff 和私有缓存头。当前普通附件下载不实现 Range 分片。UUID 不是访问许可。普通单聊媒体的可见消息授权分支不重新检查好友或屏蔽,因此删好友、屏蔽不等于撤销全部历史媒体访问;群消息授权分支要求当前成员,媒体所有者分支则独立存在。依据:chat-service/src/database.mjs:239-254。

iOS 上传照片会先缩放到最长边不超过 2048 像素并转 JPEG,普通文件检查是否为非空规则文件及大小;这属于客户端预处理,不改变服务端可读性。城市贴纸通过 /stickers/city 选择服务器内置素材,按账号与素材键去重;GET /stickers 列出自定义贴纸,不包含所有内置城市贴纸。

新事媒体使用另一套契约

新事图片允许 JPEG/PNG,最大 10 MiB;视频允许 MP4/MOV,最大 30 MiB,检查 ftyp 文件标识。每账号新事媒体配额为 500 MiB。一个帖子最多 9 张图片,或仅 1 个视频,不能混发图片和视频。媒体必须属于当前用户、尚未绑定其他帖子。

新事媒体读取依据帖子可见性,支持单个 bytes=start-end Range,返回 206 或 416;未绑定帖子的媒体仅所有者可读,超过 24 小时会清理。新事图文与评论保存在服务端,不属于 MLS 聊天事件流。

MLS 设备与会话建立

加密核心使用 MLS 1.0 和固定套件 MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519。设备身份包含聊天用户 ID、设备 ID 和签名公钥,密钥包必须通过 OpenMLS 签名、有效性及套件检查,并与已认证用户和路径设备匹配。相同设备不能静默替换签名公钥,每设备最多保存 25 个未使用密钥包。

  1. 设备注册密钥包,服务端保存可验证的公钥材料;私钥留在设备。
  2. 范围当前是 legacy 时,创建者调用 prepare。每位当前参与者至少要有一个已注册设备,范围变为 preparing。
  3. 创建者在本机创建 MLS 群,按设备获取可用密钥包,生成 Commit 与定向 Welcome,使用不同 clientId 依次提交。
  4. 目标设备处理 Welcome、核对邀请者身份并固定签名公钥,完成本地持久化后提交 welcome-ack;已有设备处理后续 Commit 并提交 commit-ack。
  5. 创建者 activate。服务端确认当前所有设备已加入并处理所需 Commit,范围才变为 active。
  6. active 范围接受 application 密文事件。客户端处理 MLS 消息,核对加密层认证的发送用户、设备与服务端元数据一致,然后解析应用载荷。

密钥包 TLS 字节最大 65,536 字节,事件 MLS payload 最大 1,048,576 字节,使用规范标准 base64 置于 JSON。事件 JSON 请求体上限为 1,500,000 字节。iOS 独立文字加密载荷为 version=1、kind=text、clientID、text,文字上限另为 65,536 UTF-8 字节;这不等于普通文字接口的 2000 字限制,也不构成大附件传输方案。

模式、重试与恢复

一旦存在 preparing、active 或 blocked 范围,旧 POST messages 接口就返回 E2EE_REQUIRED,防止改走明文。注册新设备、群成员变化、删除好友或屏蔽可使已存在加密范围 blocked;完成新的成员 Commit、Welcome 和必要确认后,创建者才可再次 activate。不要把等待设备上线解释成消息已经交付。

服务端对每个发送用户、设备、clientId 去重,并比较密文摘要、类型和目标。iOS 把待发送密文、pending invite、ack 和 sequence 连同 MLS 状态保存,重试时使用原密文,不重新加密同一排队消息。独立传输器存在不代表主聊天界面已完成接入;Android 当前未见相应 MLS 发送链路。

当前不能宣称所有文字、附件、新事、实时位置或音视频均已端到端加密,也不能宣称已完成生产安全审计。接口细节见API 参考,密钥与威胁边界见安全和隐私。

密钥包claim、设备确认、非连续sequence与blocked恢复的详细次序见MLS协议;重试边界见限制与错误。

Markdown 原文完整文档文本