# 群组密钥方法

[群组托管协议](../README.md) · [群组模型与密钥](../concepts/model-and-keys.md) · [方法公共约定](conventions.md)

## `group.key.sync`

`group.key.sync` 向当前设备返回其访问区间内仍在保留的群密钥材料。

客户端按所需群和 `epoch` 调用本方法补齐本地缺少的材料。尚未完成验证的材料不得提前启用。

| 项目 | 约定 |
|---|---|
| HTTP | `GET /meshline/v1/group/key/sync` |
| 会话要求 | 设备会话 |
| WSS | `group.key.sync` |
| HTTP 成功状态 | `200 OK` |

### 请求参数

| 字段 | 类型 | 必需 | 语义与约束 |
|---|---|---|---|
| `group_id` | string | 是 | 要同步密钥材料的群 ID；响应中的全部密钥条目都必须属于该群和调用账户 |
| `after` | integer | 否 | 本次读取的密钥版本下界；省略时为 `-1`，值不得小于 `-1`，且不得超过群的当前密钥版本。只返回访问区间内严格大于该值的条目 |
| `limit` | integer | 否 | 本页最多返回的密钥条目数；必须为正安全整数，遵循[分页约定](../../methods/conventions.md#分页) |

### 响应对象

| 字段 | 类型 | 必需 | 语义与约束 |
|---|---|---|---|
| `keys` | array&lt;GroupKeyEntry&gt; | 是 | 当前设备访问区间内、版本严格大于请求位置且仍被中继保留的最早一页密钥条目；按 `epoch` 严格升序，可以为空且不要求版本连续 |
| `has_more` | boolean | 是 | 中继生成本页时，当前设备仍可读取的保留条目中是否还有版本大于本页最后一项；空页时必须为 `false` |

`GroupKeyEntry` 字段：

| 字段 | 类型 | 必需 | 语义与约束 |
|---|---|---|---|
| `epoch` | integer | 是 | 这组密钥材料所属的密钥版本 |
| `client_secret_box` | GroupSecretBox | 条件 | 为调用账户封装本版本客户端秘密的盒；历史条目返回该版本原有的盒，不因成员公钥更换而重新封装。每页第一项必须提供；后续条目的客户端秘密承诺或盒对应的目标成员公钥与本页前一返回条目不同时，也必须提供；两者均相同时省略 |
| `relay_secret_box` | GroupSecretBox | 是 | 中继使用调用设备的加密公钥封装本版本中继秘密的盒；构造规则见[中继秘密盒](../concepts/model-and-keys.md#中继秘密盒) |

客户端按每条 `epoch` 从已验证管理历史取得该版本的成员公钥和客户端秘密承诺，用于判断客户端秘密盒的省略条件、重建其 AAD 及派生群应用秘密。盒的目标成员公钥和承诺按该历史版本确定；上述省略条件比较本页相邻返回条目，不要求其 `epoch` 连续，也不跨页复用省略条件。

客户端必须按同一规则检查盒是否提供，不能以承诺相同为由接受成员公钥变化处缺失的盒。使用盒对应的成员私钥解开客户端秘密后，必须重算承诺，并与已验证管理历史中该版本的承诺核对。后续省略客户端秘密盒的条目继续使用已验证、承诺相同的客户端秘密。

客户端解开两个秘密后派生群应用秘密。缺少旧成员私钥时，对应历史客户端秘密盒无法解开，但不影响后续条目的读取和独立验证；未通过适用的来源、密钥盒和承诺校验的秘密不得用于派生密钥。

### 处理与错误

中继必须验证参数、设备会话和当前成员资格。设备访问区间的建立和起点遵循[设备访问区间](../concepts/membership-and-access.md#设备访问区间)。建立访问起点和返回密钥材料属于本次成功调用的处理结果，参数或授权校验失败时不得建立新区间。

中继只选择访问区间内仍在保留的版本，并按 `epoch` 升序分页。版本号不连续、请求位置指向已经清理或从未分配的版本，都不构成错误；同步从其后的下一项可读材料继续。当前版本及恢复它所需的密钥材料必须按[群组数据保留与访问规则](../concepts/timeline-and-sync.md#群组数据保留与访问规则)持续保留并可读。

请求参数非法，或者 `after` 超过群的当前密钥版本时返回 `bad_request`；群不存在时返回 `not_found`；账户不是当前成员、账户已被封禁，或者读取期间所依据的访问区间已经关闭时返回 `forbidden`。

密钥同步不推进密钥版本，也不追加群事件。设备失效后的中继秘密轮换遵循[设备访问区间](../concepts/membership-and-access.md#设备访问区间)中的规则。

## `group.secret.rotation.prepare`

当前 owner 通过设备会话固定本次轮换的信息，并分批上传覆盖所有当前成员的客户端秘密盒。prepare 只准备材料，不提交签名轮换声明；最终授权由 [`group.secret.rotation.commit`](#groupsecretrotationcommit) 的签名请求提供。

| 项目 | 约定 |
|---|---|
| HTTP | `PATCH /meshline/v1/group/secret/rotation/prepare` |
| 会话要求 | 设备会话 |
| WSS | `group.secret.rotation.prepare` |
| HTTP 成功状态 | `200 OK` |

### 请求参数

| 字段 | 类型 | 必需 | 语义与约束 |
|---|---|---|---|
| `group_id` | string | 是 | 要准备客户端群秘密轮换的群 ID |
| `base_commitment` | string | 是 | 准备轮换所依据的客户端秘密承诺，采用 `sha256:` 文本表示；必须等于群的当前承诺 |
| `client_secret_commitment` | string | 是 | 新 32-byte 客户端群秘密的网络绑定 SHA-256 承诺，使用 `sha256:` 文本表示；必须不同于旧承诺并与本次全部盒的明文一致 |
| `client_secret_boxes` | object&lt;string, GroupSecretBox&gt; | 是 | 本批要暂存的客户端秘密盒，以账户为键且至少包含一项；账户必须是当前成员，每个盒封装本次新客户端秘密；目标公钥规则见下文 |

同一轮换的各批次除 `client_secret_boxes` 外，表中其他参数必须保持一致。

owner 必须为每次新轮换使用密码学安全随机源生成新的 32-byte 客户端群秘密，并按[客户端秘密承诺](../concepts/model-and-keys.md#客户端秘密与中继秘密)的计算规则取得新承诺。因暂存到期或改变固定轮换参数而重新开始准备时，也必须生成新秘密和新承诺，并重新生成相应密钥盒；正常分批上传、为新增成员补盒和重试继续复用原秘密和承诺。

客户端先同步并验证管理链，以本地成员状态中的账户和公钥封装密钥盒；准备期间新增成员或公钥变更的补盒必须先验证相应批准事件。管理链缺失、签名或授权不成立时，不得向未验证的公钥封装或上传秘密。

如果群主计划在 commit 时同时更换自身成员公钥，必须在生成自己的盒前选定新密钥对，以新公钥封装自己的盒。新公钥只在 commit 的 `owner_encryption_public_key` 中提交，受完整轮换请求签名保护。其他成员使用本地已验证的当前公钥。

建议客户端在发送首次 prepare 请求前保存新的 `client_group_secret`、本次轮换的固定请求参数以及可选的新 owner 成员私钥，以便中断后继续处理。重试时建议复用已生成的盒，也允许使用新的密码学安全随机数重新封装同一秘密；秘密和目标上下文仍须符合本次轮换要求。

候选值不得覆盖本地当前可用的秘密和成员私钥。候选状态的验证和启用必须遵循 [`group.secret.rotation.commit`](#groupsecretrotationcommit) 的规定。

### 响应对象

| 字段 | 类型 | 必需 | 语义与约束 |
|---|---|---|---|
| `prepared` | integer | 是 | 当前成员中已经保存编码结构合法的客户端秘密盒的不同账户数；不表示中继已经验证盒的密文或明文 |
| `expires_at` | integer | 是 | 中继在本次暂存轮换建立或替换时确定的 UTC Unix 秒到期时间；同一暂存轮换的到期时间保持不变，达到后不能 commit |

### 处理与错误

#### 暂存轮换与批次处理

中继按请求的群和准备启用的客户端秘密承诺区分暂存轮换。

中继通过设备会话确认调用账户是当前 owner，并确认轮换所依据的旧承诺仍是群的当前权威承诺；随后验证完整请求大小、盒映射非空且账户均为当前成员，并按[客户端秘密盒](../concepts/model-and-keys.md#客户端秘密盒)规则检查密钥盒。继续当前暂存轮换时还须确认准备仍有效。

没有暂存项时，中继直接建立暂存轮换；请求中的新承诺与当前暂存轮换记录的新承诺不同且请求通过校验时，中继原子替换现有暂存轮换。建立或替换时，中继固定调用账户和本次轮换的新旧客户端秘密承诺，重新确定 `expires_at`，以当前成员账户和公钥开始准备，并保存本批密钥盒；不继承旧轮换的密钥盒。校验或替换失败时，原暂存轮换保持不变。

请求中的新承诺与当前暂存轮换记录的新承诺相同时，中继确认其他固定参数与首次请求一致，再按账户保存本批密钥盒。同一账户后接受的盒覆盖先前的盒；本批未包含的账户盒保持不变。整个批次通过校验后原子生效，失败时原暂存材料保持不变。

#### 成员与公钥变更

准备期间新增成员不使准备失效，已有成员的盒保持有效；须按新增成员的账户和公钥补充盒。

成员通过 [`group.member.leave`](members-and-bans.md#groupmemberleave) 主动离群、被 [`group.member.remove`](members-and-bans.md#groupmemberremove) 移除或因 [`group.member.ban`](members-and-bans.md#groupmemberban) 封禁而被移除，均不使准备失效。中继必须在成员变更生效时原子删除这些账户的暂存盒。其余成员的盒及本轮固定参数保持不变，候选秘密和承诺继续使用。这些成员变更不恢复已经到期或因其他原因失效的准备。安全边界见[客户端秘密轮换的安全边界](../concepts/model-and-keys.md#安全边界)。

上传批次仍必须只包含操作生效时的当前成员。迟到或重试的批次包含已不属于当前成员的账户时，整批返回 `bad_request`，不得恢复其暂存盒，也不得使原本有效的准备失效；客户端同步并验证成员变化后，可删除已不属于当前成员的账户的盒，继续上传非空批次。同一账户重新加入时不恢复原暂存盒，须按新批准的公钥补盒。

准备期间成员公钥发生变化时，中继必须在公钥变更生效时原子删除受影响成员的暂存盒；其余成员的盒、本轮固定参数和原到期时间保持不变。owner 同步并验证相应管理事件后，用新公钥重新封装同一候选秘密，只补传受影响成员的盒；owner 自身的盒仍须符合上文 commit 时可选换钥的规则。公钥变化不恢复已经到期或因其他原因失效的准备。

客户端确认目标公钥变更后，必须停止上传和重试该账户的旧盒。提交后接收成员发现解密或承诺校验失败时，必须拒绝该盒，并可通过 [`group.member.recovery.submit`](member-recovery.md#groupmemberrecoverysubmit) 发起新的成员密钥重置以恢复。

#### 错误返回与状态影响

请求格式、群 ID、承诺、盒账户或编码结构不合法时返回 `bad_request`；群不存在时返回 `not_found`；调用账户不是当前 owner 或已被封禁时返回 `forbidden`；群已关闭、旧承诺不是当前承诺、继续当前暂存轮换时固定参数不一致、暂存项已经到期，或者发生并发写入冲突时返回 `state_conflict`。

prepare 只持久化候选材料，不更新当前成员公钥、客户端秘密承诺、`epoch`、时间线或设备访问区间，也不发送群事件通知。

## `group.secret.rotation.commit`

当前 owner 提交签名的 `GroupClientSecretRotation`，授权启用已经完整准备的客户端秘密；成功后，该声明写入群事件。

| 项目 | 约定 |
|---|---|
| HTTP | `POST /meshline/v1/group/secret/rotation/commit` |
| 会话要求 | 设备会话 |
| WSS | `group.secret.rotation.commit` |
| HTTP 成功状态 | `204 No Content` |

### 请求参数

请求参数直接是 `GroupClientSecretRotation`，不再上传客户端秘密盒。

| 字段 | 类型 | 必需 | 语义与约束 |
|---|---|---|---|
| `$type` | string | 是 | 固定为 `meshline.group.secret.rotation` |
| `group_id` | string | 是 | 要启用暂存客户端秘密并写入轮换事件的群 ID |
| `prev_hash` | string | 是 | 上一项[管理链](../core-objects.md#参与事件与前序引用)摘要；提交时必须仍是当前链头 |
| `client_secret_commitment` | string | 是 | 本次要启用的新客户端群秘密承诺，采用 `sha256:` 文本表示；必须与 prepare 时固定的新承诺一致，并与全部暂存客户端秘密盒的明文一致 |
| `owner_encryption_public_key` | string | 否 | owner 的新 32-byte X25519 公钥，使用无 padding base64url；提供时必须不同于当前成员公钥，并与已上传的自身密钥盒目标一致；省略时保留原公钥 |
| `device_signature` | string | 是 | 由当前 owner 使用本次调用设备签署，签名输入是排除本字段后的完整 `GroupClientSecretRotation` |

更换自身公钥时，`owner_encryption_public_key` 必须是先前选定且用于生成自身盒的公钥，owner 必须持有对应私钥。commit 使用的公钥必须与已上传自身盒的目标一致。准备仍有效时，owner 可以改选本次 commit 要启用的新公钥，但必须先用新公钥重新封装自身密钥盒，并通过 prepare 覆盖原盒；其他成员的盒保持不变。改变承诺时，必须生成新秘密和新承诺，通过新的 prepare 替换暂存轮换并重新上传盒。

### 响应对象

无。

### 处理与错误

#### 提交校验与错误

中继必须验证完整请求大小、固定字段、群 ID、承诺和可选新成员公钥的格式，通过设备会话确认调用账户是当前 owner，并验证声明由本次调用设备签署。声明准备启用的客户端秘密承诺必须与该群当前暂存轮换匹配，且调用账户仍是建立该轮换的 owner。所有权转让会按 [`group.owner.transfer`](properties-and-roles.md#groupownertransfer) 的规则删除原 owner 的暂存轮换。

中继必须确认操作生效时暂存项未到期、prepare 时固定在暂存信息中的旧承诺仍是群的当前权威承诺，并确认密钥盒已覆盖提交时的全部当前成员。轮换声明的签名不代替接收成员对密钥盒的验证，验证规则见[客户端秘密盒](../concepts/model-and-keys.md#客户端秘密盒)。

消息、角色更新、成员离群、成员移除、成员公钥变化、封禁、解除封禁和仅轮换中继秘密不使准备失效。新增成员或公钥变化后，密钥盒尚未覆盖全部当前成员时，commit 返回 `state_conflict`，原准备继续有效。成员变化后决定继续轮换时，客户端同步并验证新管理事件，按当前成员集合使用同一秘密补齐缺少的盒，再依据新链头重新签名提交。

固定字段、群 ID、承诺、公钥或请求格式不合法，或者提供的 `owner_encryption_public_key` 与当前成员公钥相同时返回 `bad_request`；群不存在时返回 `not_found`；账户不是当前 owner 或已被封禁时返回 `forbidden`；群已关闭、暂存轮换不存在、已到期、新承诺与暂存信息不一致、暂存信息中的旧承诺已不是当前值、尚未覆盖全部成员，或者并发状态变化时返回 `state_conflict`。

#### 原子提交与通知

中继必须原子完成以下变更：启用新客户端承诺和可选的新 owner 公钥，保存全体成员的客户端秘密盒，生成新中继秘密，推进 `epoch`，删除暂存项，并追加以本次签名声明为正文的事件；如果同时替换 owner 成员公钥，还必须在同一次操作中删除 owner 的任何待审批密钥重置请求。其他未过期的密钥重置请求继续保留，可以基于新承诺批准。

成功提交后发送 `group.timeline.changed`；本次实际删除了 owner 的待审批密钥重置请求时，还须按[删除通知的接收范围](../notifications/README.md#groupmemberrecoverychanged)发送 `group.member.recovery.changed`。

失败不得部分启用候选秘密，也不得删除仍可继续提交的暂存项。

#### 客户端验证与启用

启用候选状态前，客户端必须通过 `group.sync` 验证与本地签名请求一致的轮换事件、管理链及 owner 权限，再按 `group.key.sync` 验证密钥条目和盒。若该事件已被后续轮换取代，不能启用过时候选值。候选公钥、承诺仍与本地重建的当前状态一致时，才能启用本地候选状态。

若同时更换 owner 成员私钥，再通过 [`AccountGroupPrivateStateSync`](../concepts/account-sync.md#accountgroupprivatestatesync) 同步已生效的私钥；群秘密由其他设备从相应密钥盒取得。
