# 群组模型与密钥

[群组托管协议](../README.md) · [群组核心对象](../core-objects.md)

## 固定托管

每个群在创建时选择一个托管中继，此后不迁移也不自动接管。中继是当前群状态、成员投影、事件顺序和密钥版本顺序的权威来源；客户端保存长期消息历史和自身秘密。群状态不经过账户归属中继，也不使用 DHT。

## 角色权限

群成员角色为 `owner`、`administrator` 或 `member`。每个群始终只有一名 owner。

角色表示群内业务权限。权限如下：

| 操作 | owner | administrator | member |
|---|---|---|---|
| 发送、同步和订阅 | 可以 | 可以 | 可以 |
| 修改群内昵称 | 仅本人 | 仅本人 | 仅本人 |
| 创建邀请 | 可以 | 可以 | 由 `invite_policy` 决定 |
| 查看邀请 | 全部邀请 | 全部邀请 | 自己创建的邀请 |
| 撤销邀请 | 全部邀请 | 自己创建的邀请 | 自己创建的邀请 |
| 审批入群申请 | 可以 | 可以 | 不可以 |
| 修改群属性或角色、转让所有权或关闭群 | 可以 | 不可以 | 不可以 |
| 移除成员 | 任意非 owner 成员 | 仅普通成员 | 不可以 |
| 封禁账户 | 任意非 owner 账户 | 普通成员或非成员 | 不可以 |
| 解除封禁 | 可以 | 可以 | 不可以 |
| 批准成员密钥重置 | 包括自己在内的任意成员 | 仅其他普通成员 | 不可以 |
| 轮换客户端群秘密 | 可以 | 不可以 | 不可以 |
| 主动离群 | 先转让所有权 | 可以 | 可以 |

中继执行方法时必须按当时的当前角色判断权限。事件或列表结果只证明中继曾经接受过某项内容，不会把当时的管理权限延续到以后。

## 客户端秘密与中继秘密

群使用两份 32-byte 随机秘密，均必须由密码学安全随机源生成：

- `client_group_secret` 由当前成员共享，托管中继不得取得；
- `relay_epoch_secret` 由托管中继为每个 `epoch` 独立生成。

### 密钥版本推进

`epoch` 从 0 开始，在群内严格递增但不要求连续。创建、批准申请、成员离开、成员移除、成员密钥重置批准、客户端秘密轮换和中继主动轮换都会建立新的密钥版本。封禁批次包含当前成员时，整批移除这些成员并建立一个新密钥版本；全部目标均非当前成员时不推进密钥。修改群属性或角色、转让所有权、解除封禁和普通消息不会推进密钥版本。

### 客户端秘密承诺

客户端秘密承诺按[网络绑定 JSON 输入](../../../general.md#网络绑定-json-输入)计算：

```text
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 秘密：

1. 发送方为每个盒生成临时 X25519 密钥对，以临时私钥和目标公钥计算共享秘密；接收方使用自己的目标私钥和盒中的 `enc` 计算同一共享秘密。双方均须按 [X25519 共享秘密校验](../../../general.md#x25519-共享秘密校验)拒绝全零结果，通过后才能继续封装或解封。
2. 使用共享秘密作为 HKDF-SHA-256 的 IKM，salt 为 UTF-8 字符串 `Meshline/keybox-salt/v1` 的 SHA-256，info 为下文对应盒类型的[网络绑定 JSON 输入](../../../general.md#网络绑定-json-输入)，派生 32-byte 封装密钥。
3. 使用该封装密钥、独立生成的 12-byte 随机 nonce 和与 info 相同的 AAD，通过 AES-256-GCM 加密秘密，按 `sealed_secret` 的布局封装结果。

临时 X25519 私钥和 nonce 必须来自密码学安全随机源。两类盒必须使用各自的上下文，不得互换。

#### 客户端秘密盒

客户端秘密盒的 KDF info 与 AES-GCM AAD 使用以下网络绑定对象：

```json
{
  "$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`](../methods/keys.md#groupsecretrotationprepare)。更换成员公钥不重新封装历史客户端秘密盒，历史盒仍需对应的旧成员私钥解开。

客户端秘密盒不绑定 `epoch`；客户端秘密和目标成员公钥均未变化时，同一盒可以跨密钥版本复用。盒作为写方法的外层投递材料，不进入签名事件。提交盒的 owner 或 administrator 必须保证秘密和目标上下文正确。

#### 中继秘密盒

中继秘密盒的 KDF info 与 AES-GCM AAD 使用以下网络绑定对象：

```json
{
  "$type": "meshline.group.relay_secret_box.aad",
  "group_id": "grp_...",
  "account": "neo:860833102:...",
  "device_id": "dev_...",
  "epoch": 7
}
```

中继根据 `group.key.sync` 的设备会话确认调用账户、调用设备 ID 和该设备的加密公钥，并据此封装中继秘密；接收方按本设备对应的上下文重建 AAD。

## 群应用秘密派生

客户端取得两份秘密并验证承诺后，按下式派生 32 字节的群应用秘密：

```text
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 节](https://www.rfc-editor.org/rfc/rfc9954.html#section-3.3)描述的固定长度组合方式一致。

## 安全边界

中继单独只有 `relay_epoch_secret`，成员单独只有 `client_group_secret` 和中继发给其设备的中继秘密。中继可以拒绝服务、裁剪有限期历史或向不同设备隐藏材料，但不能只凭自身数据解密消息。中继与任何仍持有客户端秘密的一方串谋时，可以结合中继仍保留的相应版本的中继秘密，派生群应用秘密并解密对应消息。

成员或设备失去访问权后，轮换中继秘密可以阻止其继续取得新的群消息密钥，但不能修复客户端秘密本身的泄露。

发现或怀疑客户端秘密泄露时，建议 owner 执行客户端秘密轮换。轮换时只向当前仍受信任的成员提供新秘密盒。如果某个当前成员已经不再可信，owner 必须先将其移除，再生成全新客户端秘密并完成轮换；新客户端秘密启用后，没有取得该秘密的一方不能再派生后续版本的群应用秘密。轮换不能使此前已经泄露或解密的历史消息重新保密，也不能防止仍获授权的当前成员主动与中继串谋。

若要排除已离群或被移除成员与中继串谋，owner 必须在其成员资格结束后生成全新客户端秘密和承诺，重新准备轮换；继续原准备不能视为已经完成这种安全排除。

封禁同时移除当前成员并禁止其重新加入；其秘密轮换与成员移除遵循相同规则。

新设备的历史起点也是中继访问边界：若它从其他来源取得同一 `epoch` 的旧密文和中继秘密，协议不能阻止解密。

本协议不声明前向保密或入侵后安全性质。
