群组模型与密钥
固定托管
每个群在创建时选择一个托管中继,此后不迁移也不自动接管。中继是当前群状态、成员投影、事件顺序和密钥版本顺序的权威来源;客户端保存长期消息历史和自身秘密。群状态不经过账户归属中继,也不使用 DHT。
角色权限
群成员角色为 owner、administrator 或 member。每个群始终只有一名 owner。
角色表示群内业务权限。权限如下:
| 操作 | owner | administrator | member |
|---|---|---|---|
| 发送、同步和订阅 | 可以 | 可以 | 可以 |
| 修改群内昵称 | 仅本人 | 仅本人 | 仅本人 |
| 创建邀请 | 可以 | 可以 | 由 invite_policy 决定 |
| 查看邀请 | 全部邀请 | 全部邀请 | 自己创建的邀请 |
| 撤销邀请 | 全部邀请 | 自己创建的邀请 | 自己创建的邀请 |
| 审批入群申请 | 可以 | 可以 | 不可以 |
| 修改群属性或角色、转让所有权或关闭群 | 可以 | 不可以 | 不可以 |
| 移除成员 | 任意非 owner 成员 | 仅普通成员 | 不可以 |
| 封禁账户 | 任意非 owner 账户 | 普通成员或非成员 | 不可以 |
| 解除封禁 | 可以 | 可以 | 不可以 |
| 批准成员密钥重置 | 包括自己在内的任意成员 | 仅其他普通成员 | 不可以 |
| 轮换客户端群秘密 | 可以 | 不可以 | 不可以 |
| 主动离群 | 先转让所有权 | 可以 | 可以 |
中继执行方法时必须按当时的当前角色判断权限。事件或列表结果只证明中继曾经接受过某项内容,不会把当时的管理权限延续到以后。
客户端秘密与中继秘密
群使用两份 32-byte 随机秘密,均必须由密码学安全随机源生成:
client_group_secret由当前成员共享,托管中继不得取得;relay_epoch_secret由托管中继为每个epoch独立生成。
密钥版本推进
epoch 从 0 开始,在群内严格递增但不要求连续。创建、批准申请、成员离开、成员移除、成员密钥重置批准、客户端秘密轮换和中继主动轮换都会建立新的密钥版本。封禁批次包含当前成员时,整批移除这些成员并建立一个新密钥版本;全部目标均非当前成员时不推进密钥。修改群属性或角色、转让所有权、解除封禁和普通消息不会推进密钥版本。
客户端秘密承诺
客户端秘密承诺按网络绑定 JSON 输入计算:
client_secret_commitment =
network_bound_json_hash({
$type: "meshline.group.client_secret.commitment",
group_id,
secret: base64url(client_group_secret)
})
创建群时保存初始承诺,只有客户端秘密轮换成功时才能替换它。批准申请、成员变化和中继秘密轮换可以推进 epoch,但继续沿用当前客户端秘密承诺。
中继不知道 client_group_secret,因此不能自行重新计算承诺,也不能确认客户端秘密盒中实际封装的秘密;取得盒的成员负责完成这项密码学验证。中继处理入群申请批准或成员密钥重置批准请求时,检查请求携带的承诺是否与群的当前承诺一致;不一致时拒绝请求。
群秘密盒与封装规则
GroupSecretBox
| 字段 | 类型 | 必需 | 语义与约束 |
|---|---|---|---|
alg |
string | 是 | 密钥封装构造,固定为 X25519-HKDF-SHA256-AES256GCM;接收方不支持或不匹配时必须拒绝该盒 |
enc |
string | 是 | 发送方为该盒生成的 32-byte 临时 X25519 公钥,使用无 padding base64url |
sealed_secret |
string | 是 | 12-byte AES-GCM nonce、32-byte ciphertext 和 16-byte tag 的顺序拼接,使用无 padding base64url;解密结果必须恰为 32-byte 秘密 |
客户端秘密盒和中继秘密盒均使用 GroupSecretBox,按以下步骤封装各自的 32-byte 秘密:
- 发送方为每个盒生成临时 X25519 密钥对,以临时私钥和目标公钥计算共享秘密;接收方使用自己的目标私钥和盒中的
enc计算同一共享秘密。双方均须按 X25519 共享秘密校验拒绝全零结果,通过后才能继续封装或解封。 - 使用共享秘密作为 HKDF-SHA-256 的 IKM,salt 为 UTF-8 字符串
Meshline/keybox-salt/v1的 SHA-256,info 为下文对应盒类型的网络绑定 JSON 输入,派生 32-byte 封装密钥。 - 使用该封装密钥、独立生成的 12-byte 随机 nonce 和与 info 相同的 AAD,通过 AES-256-GCM 加密秘密,按
sealed_secret的布局封装结果。
临时 X25519 私钥和 nonce 必须来自密码学安全随机源。两类盒必须使用各自的上下文,不得互换。
客户端秘密盒
客户端秘密盒的 KDF info 与 AES-GCM AAD 使用以下网络绑定对象:
{
"$type": "meshline.group.client_secret_box.aad",
"group_id": "grp_...",
"account": "neo:860833102:...",
"member_encryption_public_key": "base64url...",
"client_secret_commitment": "sha256:..."
}
生成盒时,客户端必须按相应方法确定目标账户和 member_encryption_public_key。批准新成员或重置公钥时,批准者先验证成员完整签名请求及设备证书,账户取自已验证设备证书,公钥取自完整签名请求。管理链、批准权限、请求签名或账户绑定验证失败时,不得封装或上传秘密。
中继处理包含客户端秘密盒的请求时,按方法要求检查目标账户和密钥盒覆盖范围,以及每个盒的算法标识和编码长度。中继没有成员私钥,不能解开客户端秘密盒,也不能确认其明文、AAD 目标或客户端秘密承诺是否正确。
解密时,接收方必须使用封装目标对应的成员私钥派生公钥,并依据预期上下文按上述定义重建 AAD。历史盒使用生成时的目标公钥。AEAD 验证或解密后的客户端秘密承诺校验失败时,必须拒绝该盒。
客户端秘密轮换时的密钥盒准备规则见 group.secret.rotation.prepare。更换成员公钥不重新封装历史客户端秘密盒,历史盒仍需对应的旧成员私钥解开。
客户端秘密盒不绑定 epoch;客户端秘密和目标成员公钥均未变化时,同一盒可以跨密钥版本复用。盒作为写方法的外层投递材料,不进入签名事件。提交盒的 owner 或 administrator 必须保证秘密和目标上下文正确。
中继秘密盒
中继秘密盒的 KDF info 与 AES-GCM AAD 使用以下网络绑定对象:
{
"$type": "meshline.group.relay_secret_box.aad",
"group_id": "grp_...",
"account": "neo:860833102:...",
"device_id": "dev_...",
"epoch": 7
}
中继根据 group.key.sync 的设备会话确认调用账户、调用设备 ID 和该设备的加密公钥,并据此封装中继秘密;接收方按本设备对应的上下文重建 AAD。
群应用秘密派生
客户端取得两份秘密并验证承诺后,按下式派生 32 字节的群应用秘密:
epoch_application_secret = HKDF-SHA-256(
IKM = client_group_secret || relay_epoch_secret,
salt = SHA-256(UTF8("Meshline/group-epoch-salt/v1")),
info = network_bound_json_bytes({
$type: "meshline.group.epoch_secret",
group_id,
epoch,
client_secret_commitment
}),
L = 32
)
拼接两份固定长度共享秘密后再使用 HKDF,与 RFC 9954 第 3.3 节描述的固定长度组合方式一致。
安全边界
中继单独只有 relay_epoch_secret,成员单独只有 client_group_secret 和中继发给其设备的中继秘密。中继可以拒绝服务、裁剪有限期历史或向不同设备隐藏材料,但不能只凭自身数据解密消息。中继与任何仍持有客户端秘密的一方串谋时,可以结合中继仍保留的相应版本的中继秘密,派生群应用秘密并解密对应消息。
成员或设备失去访问权后,轮换中继秘密可以阻止其继续取得新的群消息密钥,但不能修复客户端秘密本身的泄露。
发现或怀疑客户端秘密泄露时,建议 owner 执行客户端秘密轮换。轮换时只向当前仍受信任的成员提供新秘密盒。如果某个当前成员已经不再可信,owner 必须先将其移除,再生成全新客户端秘密并完成轮换;新客户端秘密启用后,没有取得该秘密的一方不能再派生后续版本的群应用秘密。轮换不能使此前已经泄露或解密的历史消息重新保密,也不能防止仍获授权的当前成员主动与中继串谋。
若要排除已离群或被移除成员与中继串谋,owner 必须在其成员资格结束后生成全新客户端秘密和承诺,重新准备轮换;继续原准备不能视为已经完成这种安全排除。
封禁同时移除当前成员并禁止其重新加入;其秘密轮换与成员移除遵循相同规则。
新设备的历史起点也是中继访问边界:若它从其他来源取得同一 epoch 的旧密文和中继秘密,协议不能阻止解密。
本协议不声明前向保密或入侵后安全性质。