@agentunion/fastaun-browser 0.5.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +56 -0
- package/_packed_docs/CHANGELOG-validators.md +144 -0
- package/_packed_docs/CHANGELOG.md +56 -0
- package/_packed_docs/INDEX.md +198 -181
- package/_packed_docs/KITE_DOCS_GUIDE.md +26 -23
- package/_packed_docs/cli/AUN-CLI/350/256/276/350/256/241/346/226/207/346/241/243.md +4 -3
- package/_packed_docs/cli/CLI/346/211/213/345/206/214.md +331 -0
- package/_packed_docs/protocol/08-AUN-E2EE-Group.md +296 -902
- package/_packed_docs/protocol/10-Group-/345/255/220/345/215/217/350/256/256.md +64 -114
- package/_packed_docs/protocol/11-Storage-/345/255/220/345/215/217/350/256/256.md +7 -1
- package/_packed_docs/protocol/16-/347/263/273/347/273/237/347/233/256/345/275/225/344/277/235/346/212/244/346/226/271/346/241/210.md +177 -0
- package/_packed_docs/protocol/README.md +2 -1
- package/_packed_docs/protocol/index.md +8 -3
- package/_packed_docs/sdk/05-E2EE/345/212/240/345/257/206/351/200/232/344/277/241.md +4 -252
- package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +483 -457
- package/_packed_docs/sdk/09-collab-rpc-manual.md +581 -550
- package/_packed_docs/sdk/09-group-rpc-manual.md +248 -335
- package/_packed_docs/sdk/09-storage-rpc-manual.md +56 -19
- package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +13 -13
- package/_packed_docs/sdk/INDEX.md +14 -14
- package/dist/bundle.js +1560 -1159
- package/dist/client/delivery.d.ts +13 -2
- package/dist/client/delivery.d.ts.map +1 -1
- package/dist/client/delivery.js +251 -46
- package/dist/client/delivery.js.map +1 -1
- package/dist/client/group-state.d.ts.map +1 -1
- package/dist/client/group-state.js +36 -14
- package/dist/client/group-state.js.map +1 -1
- package/dist/client/lifecycle.js +2 -2
- package/dist/client/lifecycle.js.map +1 -1
- package/dist/client/rpc-pipeline.d.ts +1 -0
- package/dist/client/rpc-pipeline.d.ts.map +1 -1
- package/dist/client/rpc-pipeline.js +166 -50
- package/dist/client/rpc-pipeline.js.map +1 -1
- package/dist/client/v2-e2ee.d.ts +14 -1
- package/dist/client/v2-e2ee.d.ts.map +1 -1
- package/dist/client/v2-e2ee.js +300 -121
- package/dist/client/v2-e2ee.js.map +1 -1
- package/dist/client.d.ts +5 -4
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +187 -46
- package/dist/client.js.map +1 -1
- package/dist/collab/client.d.ts +8 -0
- package/dist/collab/client.d.ts.map +1 -1
- package/dist/collab/client.js +12 -0
- package/dist/collab/client.js.map +1 -1
- package/dist/errors.d.ts +0 -20
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +8 -51
- package/dist/errors.js.map +1 -1
- package/dist/facades.d.ts +9 -4
- package/dist/facades.d.ts.map +1 -1
- package/dist/facades.js +192 -31
- package/dist/facades.js.map +1 -1
- package/dist/group-fs.d.ts +17 -0
- package/dist/group-fs.d.ts.map +1 -1
- package/dist/group-fs.js +54 -11
- package/dist/group-fs.js.map +1 -1
- package/dist/group-id.d.ts +9 -12
- package/dist/group-id.d.ts.map +1 -1
- package/dist/group-id.js +41 -63
- package/dist/group-id.js.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/keystore/index.d.ts +2 -54
- package/dist/keystore/index.d.ts.map +1 -1
- package/dist/keystore/indexeddb-identity-store.d.ts +3 -0
- package/dist/keystore/indexeddb-identity-store.d.ts.map +1 -1
- package/dist/keystore/indexeddb-identity-store.js +65 -0
- package/dist/keystore/indexeddb-identity-store.js.map +1 -1
- package/dist/keystore/indexeddb-shared.d.ts +3 -17
- package/dist/keystore/indexeddb-shared.d.ts.map +1 -1
- package/dist/keystore/indexeddb-shared.js +4 -47
- package/dist/keystore/indexeddb-shared.js.map +1 -1
- package/dist/keystore/indexeddb-token-store.d.ts +1 -64
- package/dist/keystore/indexeddb-token-store.d.ts.map +1 -1
- package/dist/keystore/indexeddb-token-store.js +45 -774
- package/dist/keystore/indexeddb-token-store.js.map +1 -1
- package/dist/logger.d.ts +2 -0
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +4 -0
- package/dist/logger.js.map +1 -1
- package/dist/storage/lowlevel.d.ts +9 -1
- package/dist/storage/lowlevel.d.ts.map +1 -1
- package/dist/storage/lowlevel.js +12 -1
- package/dist/storage/lowlevel.js.map +1 -1
- package/dist/storage/vfs.d.ts +22 -0
- package/dist/storage/vfs.d.ts.map +1 -1
- package/dist/storage/vfs.js +54 -0
- package/dist/storage/vfs.js.map +1 -1
- package/dist/tools/cross-sdk-agent.js +336 -49
- package/dist/tools/cross-sdk-agent.js.map +1 -1
- package/dist/transport.d.ts +2 -0
- package/dist/transport.d.ts.map +1 -1
- package/dist/transport.js +96 -3
- package/dist/transport.js.map +1 -1
- package/dist/types.d.ts +39 -56
- package/dist/types.d.ts.map +1 -1
- package/dist/v2/session/session.d.ts +2 -0
- package/dist/v2/session/session.d.ts.map +1 -1
- package/dist/v2/session/session.js +58 -22
- package/dist/v2/session/session.js.map +1 -1
- package/dist/v2/state/commitment.d.ts +1 -1
- package/dist/v2/state/commitment.d.ts.map +1 -1
- package/dist/v2/state/commitment.js +5 -3
- package/dist/v2/state/commitment.js.map +1 -1
- package/dist/validators.d.ts +35 -0
- package/dist/validators.d.ts.map +1 -0
- package/dist/validators.js +127 -0
- package/dist/validators.js.map +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/_packed_docs/collab-gateway-boundary-test-report.md +0 -164
|
@@ -1,902 +1,296 @@
|
|
|
1
|
-
# AUN-E2EE 群组扩展规范
|
|
2
|
-
|
|
3
|
-
> 版本:
|
|
4
|
-
> 状态:规范性文档
|
|
5
|
-
> 适用范围:AUN 客户端 SDK、客户端应用、跨语言实现
|
|
6
|
-
> 不适用范围:Group Service 服务端加解密实现
|
|
7
|
-
> 前置依赖:[08-AUN-E2EE](./08-AUN-E2EE.md)(P2P E2EE
|
|
8
|
-
> 定位:**群组消息端到端加密层**,基于
|
|
9
|
-
|
|
10
|
-
---
|
|
11
|
-
|
|
12
|
-
## 1. 目标与边界
|
|
13
|
-
|
|
14
|
-
本规范定义 AUN
|
|
15
|
-
|
|
16
|
-
### 1.1 目标
|
|
17
|
-
|
|
18
|
-
- 让群组内的 N 个成员在现有 `group.send` / `event/group.message_created` 之上实现端到端加密
|
|
19
|
-
- 让 Group Service 仅看到最小必要路由元数据和密文 payload
|
|
20
|
-
-
|
|
21
|
-
- 为各语言 SDK
|
|
22
|
-
|
|
23
|
-
### 1.2 服务端职责
|
|
24
|
-
|
|
25
|
-
Group Service **只做**:
|
|
26
|
-
|
|
27
|
-
- 认证发送方(JWT token
|
|
28
|
-
- 校验群成员权限
|
|
29
|
-
-
|
|
30
|
-
-
|
|
31
|
-
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
-
|
|
37
|
-
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
|
45
|
-
|
|
|
46
|
-
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
###
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
Membership Commitment
|
|
296
|
-
|
|
297
|
-
- **谁**发起了 epoch 轮换(`initiator_aid`)
|
|
298
|
-
- **哪些成员**被添加或移除(`added`、`removed`)
|
|
299
|
-
- 该操作经过了**合法授权**(ECDSA 签名)
|
|
300
|
-
|
|
301
|
-
### 6A.2 Manifest 结构
|
|
302
|
-
|
|
303
|
-
```json
|
|
304
|
-
{
|
|
305
|
-
"manifest_version": 1,
|
|
306
|
-
"group_id": "g-abc123.agentid.pub",
|
|
307
|
-
"epoch": 2,
|
|
308
|
-
"prev_epoch": 1,
|
|
309
|
-
"member_aids": ["alice.agentid.pub", "bob.agentid.pub", "carol.agentid.pub"],
|
|
310
|
-
"added": ["carol.agentid.pub"],
|
|
311
|
-
"removed": [],
|
|
312
|
-
"initiator_aid": "alice.agentid.pub",
|
|
313
|
-
"issued_at": 1710504000000,
|
|
314
|
-
"signature": "base64(ECDSA-SHA256)"
|
|
315
|
-
}
|
|
316
|
-
```
|
|
317
|
-
|
|
318
|
-
| 字段 | 类型 | 说明 |
|
|
319
|
-
|------|------|------|
|
|
320
|
-
| `manifest_version` | integer | Manifest 格式版本,当前为 `1` |
|
|
321
|
-
| `group_id` | string | 群组标识 |
|
|
322
|
-
| `epoch` | integer | 本次轮换后的 epoch |
|
|
323
|
-
| `prev_epoch` | integer / null | 上一个 epoch(首次创建时为 `null`) |
|
|
324
|
-
| `member_aids` | string[] | 本 epoch 的完整成员列表(排序) |
|
|
325
|
-
| `added` | string[] | 本次新增的成员 |
|
|
326
|
-
| `removed` | string[] | 本次移除的成员 |
|
|
327
|
-
| `initiator_aid` | string | 发起者 AID |
|
|
328
|
-
| `issued_at` | integer | 签发时间戳(ms) |
|
|
329
|
-
| `signature` | string | 发起者对 manifest 内容的 ECDSA-SHA256 签名 |
|
|
330
|
-
|
|
331
|
-
### 6A.3 签名载荷
|
|
332
|
-
|
|
333
|
-
签名覆盖除 `signature` 字段外的所有字段,序列化方式:
|
|
334
|
-
|
|
335
|
-
```
|
|
336
|
-
sign_data = canonical_json(manifest_without_signature)
|
|
337
|
-
// canonical_json: sort_keys=True, separators=(",",":"), ensure_ascii=False
|
|
338
|
-
signature = ECDSA-SHA256(initiator_private_key, sign_data)
|
|
339
|
-
```
|
|
340
|
-
|
|
341
|
-
### 6A.4 验证流程
|
|
342
|
-
|
|
343
|
-
接收方收到包含 `manifest` 的 `e2ee.group_key_distribution` 消息后 **SHOULD**:
|
|
344
|
-
|
|
345
|
-
1. 从本地缓存获取 `initiator_aid` 的证书公钥
|
|
346
|
-
2. 验证 `signature`
|
|
347
|
-
3. 检查 `member_aids` 与 manifest 中 `added`/`removed` 的一致性
|
|
348
|
-
4. 检查 `epoch` == `prev_epoch + 1`(首次创建时 `prev_epoch` 为 `null`)
|
|
349
|
-
5. 验签失败时 **SHOULD** 拒绝该 distribution(返回 `"distribution_rejected"`)
|
|
350
|
-
|
|
351
|
-
> **注意**:Manifest 验证是**建议性的**(SHOULD),不是强制性的。这是因为在某些场景下(如跨域成员加入),接收方可能尚未缓存发起者证书。实现 **MAY** 在验签失败时仍接受 distribution,但 **MUST** 在后台尝试获取发起者证书并进行延迟验证。
|
|
352
|
-
|
|
353
|
-
---
|
|
354
|
-
|
|
355
|
-
## 7. 消息格式与 AAD
|
|
356
|
-
|
|
357
|
-
### 7.1 群组密文 payload
|
|
358
|
-
|
|
359
|
-
```json
|
|
360
|
-
{
|
|
361
|
-
"type": "e2ee.group_encrypted",
|
|
362
|
-
"version": "1",
|
|
363
|
-
"encryption_mode": "epoch_group_key",
|
|
364
|
-
"suite": "P256_HKDF_SHA256_AES_256_GCM",
|
|
365
|
-
"epoch": 3,
|
|
366
|
-
"nonce": "base64(12 bytes)",
|
|
367
|
-
"ciphertext": "base64",
|
|
368
|
-
"tag": "base64(16 bytes)",
|
|
369
|
-
"sender_signature": "base64(ECDSA-SHA256 over ciphertext+tag+aad_bytes)",
|
|
370
|
-
"sender_cert_fingerprint": "sha256:hex",
|
|
371
|
-
"aad": {
|
|
372
|
-
"group_id": "g-abc123.agentid.pub",
|
|
373
|
-
"from": "alice.agentid.pub",
|
|
374
|
-
"message_id": "gm-550e8400-...",
|
|
375
|
-
"timestamp": 1710504000000,
|
|
376
|
-
"epoch": 3,
|
|
377
|
-
"encryption_mode": "epoch_group_key",
|
|
378
|
-
"suite": "P256_HKDF_SHA256_AES_256_GCM"
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
```
|
|
382
|
-
|
|
383
|
-
**发送方签名**:
|
|
384
|
-
|
|
385
|
-
- 发送方 **MUST** 用身份私钥对 `ciphertext_bytes + tag_bytes + aad_bytes` 执行 ECDSA-SHA256 签名
|
|
386
|
-
- `sender_cert_fingerprint` 用于接收方查找发送方证书
|
|
387
|
-
- 接收方 **MUST** 验证 `sender_signature`,缺失或验签失败时 **MUST** 拒绝该消息
|
|
388
|
-
|
|
389
|
-
### 7.2 外层 group.send 信封
|
|
390
|
-
|
|
391
|
-
```json
|
|
392
|
-
{
|
|
393
|
-
"jsonrpc": "2.0",
|
|
394
|
-
"method": "group.send",
|
|
395
|
-
"params": {
|
|
396
|
-
"group_id": "g-abc123.agentid.pub",
|
|
397
|
-
"type": "e2ee.group_encrypted",
|
|
398
|
-
"payload": { "<上述密文 payload>" },
|
|
399
|
-
"encrypted": true
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
```
|
|
403
|
-
|
|
404
|
-
### 7.3 AAD 字段
|
|
405
|
-
|
|
406
|
-
| 字段 | 类型 | 说明 |
|
|
407
|
-
|------|------|------|
|
|
408
|
-
| `group_id` | string | 群组唯一标识 |
|
|
409
|
-
| `from` | string | 发送方 AID |
|
|
410
|
-
| `message_id` | string | 消息唯一标识(发送方客户端生成,参与密钥派生) |
|
|
411
|
-
| `timestamp` | integer | 发送时间戳(ms) |
|
|
412
|
-
| `epoch` | integer | 当前密钥版本号 |
|
|
413
|
-
| `encryption_mode` | string | 固定为 `epoch_group_key` |
|
|
414
|
-
| `suite` | string | 算法套件标识 |
|
|
415
|
-
|
|
416
|
-
AAD 序列化方式与 P2P E2EE(§8.3)一致:递归键排序 + 紧凑格式 + UTF-8 直接输出(`ensure_ascii=False`),详见 P2P E2EE §8.3 的完整规范。
|
|
417
|
-
|
|
418
|
-
### 7.4 明文信封字段(服务端可见)
|
|
419
|
-
|
|
420
|
-
以下字段保持为 Group Service 可见的明文,用于路由和存储:
|
|
421
|
-
|
|
422
|
-
| 字段 | 说明 |
|
|
423
|
-
|------|------|
|
|
424
|
-
| `group_id` | 群组标识(路由用) |
|
|
425
|
-
| `type` | 固定为 `e2ee.group_encrypted` |
|
|
426
|
-
| `encrypted` | 固定为 `true` |
|
|
427
|
-
|
|
428
|
-
`message_id`、`seq`、`sender_aid`、`created_at` 由 Group Service 自动填充到消息记录中。
|
|
429
|
-
|
|
430
|
-
> **外层与 AAD 绑定校验**:接收方 **MUST** 校验外层 `group_id` 与 AAD 中的 `group_id` 一致(防止跨群路由篡改),外层 `from`/`sender_aid` 与 AAD 中的 `from` 一致(防止发送者冒充)。不一致时 **MUST** 拒绝解密。
|
|
431
|
-
|
|
432
|
-
---
|
|
433
|
-
|
|
434
|
-
## 8. 密钥恢复机制
|
|
435
|
-
|
|
436
|
-
### 8.1 场景
|
|
437
|
-
|
|
438
|
-
以下场景可能导致成员缺失当前 epoch 的 group_secret:
|
|
439
|
-
|
|
440
|
-
- P2P 分发消息丢失(网络故障)
|
|
441
|
-
- 客户端重启后本地存储损坏
|
|
442
|
-
- 成员离线期间发生了 epoch 轮换
|
|
443
|
-
|
|
444
|
-
### 8.2 Epoch Key Request
|
|
445
|
-
|
|
446
|
-
缺失密钥的成员 **MAY** 向群内候选成员发送密钥请求(SDK 优先从本地成员列表选择,零状态时退化为向当前消息发送者请求):
|
|
447
|
-
|
|
448
|
-
```json
|
|
449
|
-
{
|
|
450
|
-
"type": "e2ee.group_key_request",
|
|
451
|
-
"group_id": "g-abc123.agentid.pub",
|
|
452
|
-
"epoch": 3,
|
|
453
|
-
"requester_aid": "bob.agentid.pub"
|
|
454
|
-
}
|
|
455
|
-
```
|
|
456
|
-
|
|
457
|
-
此消息 **MUST** 通过 P2P E2EE 通道发送。
|
|
458
|
-
|
|
459
|
-
### 8.3 Epoch Key Response
|
|
460
|
-
|
|
461
|
-
收到请求的成员 **MUST** 先验证请求者确实是当前群成员(通过 `group.get_members` 或本地缓存),然后 **MAY** 回复:
|
|
462
|
-
|
|
463
|
-
```json
|
|
464
|
-
{
|
|
465
|
-
"type": "e2ee.group_key_response",
|
|
466
|
-
"group_id": "g-abc123.agentid.pub",
|
|
467
|
-
"epoch": 3,
|
|
468
|
-
"group_secret": "base64(32 bytes)",
|
|
469
|
-
"commitment": "sha256hex",
|
|
470
|
-
"member_aids": ["alice.agentid.pub", "bob.agentid.pub", "carol.agentid.pub"]
|
|
471
|
-
}
|
|
472
|
-
```
|
|
473
|
-
|
|
474
|
-
此消息 **MUST** 通过 P2P E2EE 通道发送。
|
|
475
|
-
|
|
476
|
-
### 8.4 安全约束
|
|
477
|
-
|
|
478
|
-
- 响应者 **MUST** 验证请求者是群成员后才能回复
|
|
479
|
-
- 请求者 **MUST** 验证 commitment 和 member_aids 的一致性(§6.3)
|
|
480
|
-
- 实现 **SHOULD** 对 key_request 做频率限制,防止被滥用
|
|
481
|
-
|
|
482
|
-
### 8.5 恢复时序
|
|
483
|
-
|
|
484
|
-
密钥恢复是**异步过程**:
|
|
485
|
-
|
|
486
|
-
1. 成员收到无法解密的群消息(epoch 不匹配或无密钥)
|
|
487
|
-
2. SDK 自动向候选成员发送 `e2ee.group_key_request`(优先本地已知成员,零状态时向消息发送者请求)
|
|
488
|
-
3. 在线成员验证请求者身份后回复 `e2ee.group_key_response`
|
|
489
|
-
4. 请求者收到响应后存储 group_secret,后续 pull 或再次收到消息时才能成功解密
|
|
490
|
-
|
|
491
|
-
> `group.use_invite_code` 加入群组后不保证立即拥有 group_secret。SDK 会在后续群消息解密失败时自动发起恢复请求。
|
|
492
|
-
|
|
493
|
-
---
|
|
494
|
-
|
|
495
|
-
## 9. 客户端密钥存储
|
|
496
|
-
|
|
497
|
-
### 9.1 存储位置
|
|
498
|
-
|
|
499
|
-
group_secret **MUST** 持久化到本地存储。推荐存储在 FileKeyStore 的 metadata 中:
|
|
500
|
-
|
|
501
|
-
```
|
|
502
|
-
~/.aun/AIDs/{safe_aid}/tokens/meta.json
|
|
503
|
-
```
|
|
504
|
-
|
|
505
|
-
### 9.2 存储结构
|
|
506
|
-
|
|
507
|
-
在 metadata 中新增 `group_secrets` 字段:
|
|
508
|
-
|
|
509
|
-
```json
|
|
510
|
-
{
|
|
511
|
-
"e2ee_prekeys": { "..." },
|
|
512
|
-
"group_secrets": {
|
|
513
|
-
"g-abc123.agentid.pub": {
|
|
514
|
-
"epoch": 3,
|
|
515
|
-
"secret_protection": {
|
|
516
|
-
"scheme": "dpapi",
|
|
517
|
-
"name": "group_secrets/g-abc123.agentid.pub/secret",
|
|
518
|
-
"persisted": true,
|
|
519
|
-
"blob": "base64(...)"
|
|
520
|
-
},
|
|
521
|
-
"commitment": "sha256hex...",
|
|
522
|
-
"member_aids": ["alice.agentid.pub", "bob.agentid.pub"],
|
|
523
|
-
"updated_at": 1710504000000,
|
|
524
|
-
"old_epochs": [
|
|
525
|
-
{
|
|
526
|
-
"epoch": 2,
|
|
527
|
-
"secret_protection": { "..." },
|
|
528
|
-
"commitment": "sha256hex...",
|
|
529
|
-
"member_aids": ["alice.agentid.pub", "bob.agentid.pub", "carol.agentid.pub"],
|
|
530
|
-
"updated_at": 1710500000000
|
|
531
|
-
}
|
|
532
|
-
]
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
```
|
|
537
|
-
|
|
538
|
-
> **说明**:`old_epochs` 数组保留旧 epoch 的密钥信息,用于解密历史消息。保留期由 `old_epoch_retention_seconds`(默认 7 天)控制,过期后由 SDK 自动清理。
|
|
539
|
-
|
|
540
|
-
### 9.3 敏感字段保护
|
|
541
|
-
|
|
542
|
-
- `group_secret` 的明文 **MUST NOT** 直接写入磁盘
|
|
543
|
-
- **MUST** 通过 SecretStore(DPAPI / Keychain / libsecret)保护
|
|
544
|
-
- 存储格式与 P2P prekey 私钥保护方式一致:明文替换为 `secret_protection` 记录
|
|
545
|
-
|
|
546
|
-
保护流程:
|
|
547
|
-
|
|
548
|
-
```
|
|
549
|
-
写入时:
|
|
550
|
-
secret_name = f"group_secrets/{group_id}/secret"
|
|
551
|
-
record["secret_protection"] = secret_store.protect(scope, secret_name, group_secret)
|
|
552
|
-
// group_secret 明文不落盘
|
|
553
|
-
|
|
554
|
-
读取时:
|
|
555
|
-
group_secret = secret_store.reveal(scope, secret_name, record["secret_protection"])
|
|
556
|
-
```
|
|
557
|
-
|
|
558
|
-
### 9.4 旧 Epoch 密钥保留
|
|
559
|
-
|
|
560
|
-
- 实现 **MAY** 保留旧 epoch 的 group_secret 一段时间(建议 7 天),用于解密在途或离线期间的历史消息
|
|
561
|
-
- 旧 epoch 密钥 **SHOULD** 在保留期过后安全擦除
|
|
562
|
-
- 旧 epoch 密钥 **MUST NOT** 用于加密新消息
|
|
563
|
-
|
|
564
|
-
---
|
|
565
|
-
|
|
566
|
-
## 10. 防重放与防篡改
|
|
567
|
-
|
|
568
|
-
### 10.1 防篡改
|
|
569
|
-
|
|
570
|
-
所有路由关键字段纳入 AAD(§7.3),任何篡改导致 AES-GCM tag 校验失败:
|
|
571
|
-
|
|
572
|
-
- `group_id` 被篡改 → AAD mismatch → 解密异常
|
|
573
|
-
- `from`(sender_aid)被替换 → AAD mismatch → 解密异常
|
|
574
|
-
- `epoch` 被篡改 → HKDF 派生出错误的 msg_key → 解密失败
|
|
575
|
-
- `message_id` 被篡改 → msg_key 派生不同 + AAD mismatch → 解密失败
|
|
576
|
-
|
|
577
|
-
### 10.2 防重放
|
|
578
|
-
|
|
579
|
-
群组消息的防重放与 P2P 一致:
|
|
580
|
-
|
|
581
|
-
- 接收方 **MUST** 维护本地 `seen_messages` 集合
|
|
582
|
-
- 以 `{group_id}:{sender_aid}:{message_id}` 为 key 去重
|
|
583
|
-
- 同一 key 的消息 **MUST** 被拒绝
|
|
584
|
-
|
|
585
|
-
### 10.3 Epoch 降级防护
|
|
586
|
-
|
|
587
|
-
- 接收方 **MUST** 拒绝 epoch 低于本地已知最新 epoch 的加密消息
|
|
588
|
-
- 例外:如果实现保留了旧 epoch 密钥(§9.4),**MAY** 允许解密旧 epoch 消息。实现可选择在解密结果中标记 `historical: true`,但不做强制要求
|
|
589
|
-
|
|
590
|
-
---
|
|
591
|
-
|
|
592
|
-
## 11. 安全约定
|
|
593
|
-
|
|
594
|
-
### 11.1 通用约定
|
|
595
|
-
|
|
596
|
-
- 加密失败时 **MUST NOT** 静默降级为明文
|
|
597
|
-
- 每条消息使用独立的随机 nonce
|
|
598
|
-
- group_secret **MUST** 由密码学安全随机数生成器生成
|
|
599
|
-
- 实现 **MUST NOT** 在日志中输出 group_secret 或 msg_key
|
|
600
|
-
|
|
601
|
-
### 11.2 分发通道安全
|
|
602
|
-
|
|
603
|
-
- group_secret 的分发 **MUST** 通过 P2P E2EE 通道(prekey_ecdh_v2 或 long_term_key 模式)
|
|
604
|
-
- 分发消息的发送方身份 **MUST** 通过 P2P E2EE 的 AAD 机制验证
|
|
605
|
-
- 分发消息 **SHOULD** 附带签名的 Membership Manifest(§6A)
|
|
606
|
-
|
|
607
|
-
### 11.3 客户端操作签名
|
|
608
|
-
|
|
609
|
-
以下群组操作 **MUST** 附加客户端 ECDSA 签名(`client_signature` 字段),服务端强制验签:
|
|
610
|
-
|
|
611
|
-
- `group.send`
|
|
612
|
-
- `group.add_member`
|
|
613
|
-
- `group.kick`
|
|
614
|
-
- `group.leave`
|
|
615
|
-
- `group.update_rules`
|
|
616
|
-
|
|
617
|
-
#### 11.3.1 签名生成
|
|
618
|
-
|
|
619
|
-
签名数据格式:`"{method}|{aid}|{timestamp}|{params_hash}"`
|
|
620
|
-
|
|
621
|
-
其中:
|
|
622
|
-
- `method`:RPC 方法名(如 `group.send`)
|
|
623
|
-
- `aid`:当前认证的 AID
|
|
624
|
-
- `timestamp`:当前 Unix 时间戳(秒,字符串形式)
|
|
625
|
-
- `params_hash`:业务参数的 SHA-256 哈希(十六进制小写),计算方法见 §11.3.2
|
|
626
|
-
|
|
627
|
-
签名算法:ECDSA-SHA256,使用身份私钥签名上述字符串的 UTF-8 编码。
|
|
628
|
-
|
|
629
|
-
`client_signature` 字段结构:
|
|
630
|
-
|
|
631
|
-
```json
|
|
632
|
-
{
|
|
633
|
-
"aid": "alice.agentid.pub",
|
|
634
|
-
"timestamp": "1775541042",
|
|
635
|
-
"params_hash": "a3b2c1d4e5f6...",
|
|
636
|
-
"signature": "<base64 DER-encoded ECDSA signature>"
|
|
637
|
-
}
|
|
638
|
-
```
|
|
639
|
-
|
|
640
|
-
#### 11.3.2 params_hash 计算(Canonical JSON 规范)
|
|
641
|
-
|
|
642
|
-
`params_hash` 的计算输入是业务参数的**规范化 JSON 序列化**(Canonical JSON for AUN),与 AAD 序列化规则(P2P E2EE §8.3)完全一致:
|
|
643
|
-
|
|
644
|
-
1. **字段筛选**:排除 `client_signature` 字段和所有 `_` 前缀字段(`_auth`、`_session_id` 等由网关/服务端注入的内部字段)
|
|
645
|
-
2. **键排序**:所有对象(包括嵌套对象)的键 **MUST** 按 Unicode 码点升序排列(递归排序)
|
|
646
|
-
3. **紧凑格式**:无多余空白,键值对之间用 `,`,键和值之间用 `:` 分隔
|
|
647
|
-
4. **UTF-8 直接输出**:非 ASCII 字符(如中文)**MUST** 直接以 UTF-8 编码输出,**MUST NOT** 转义为 `\uXXXX`
|
|
648
|
-
5. **数值精度**:整数值 **MUST** 序列化为不带小数点的十进制数(如 `42` 而非 `42.0`)
|
|
649
|
-
6. **布尔值**:`true` / `false`(小写)
|
|
650
|
-
7. **空值**:`null`
|
|
651
|
-
|
|
652
|
-
等价的 Python 实现:`json.dumps(params, sort_keys=True, separators=(",", ":"), ensure_ascii=False)`
|
|
653
|
-
|
|
654
|
-
> **设计决策:** `params_hash` 与 AAD 使用完全相同的 Canonical JSON 规范(`ensure_ascii=False`),避免协议内两套序列化规则导致实现混乱。Go 的 `json.Marshal` 和 JavaScript 的 `JSON.stringify` 天然满足 UTF-8 直接输出,无需额外转义处理。
|
|
655
|
-
|
|
656
|
-
> **跨语言注意事项:**
|
|
657
|
-
> - Go 的 `json.Marshal` 默认满足此规范(UTF-8 直接输出 + 自动递归键排序)
|
|
658
|
-
> - Go 的 `json.Unmarshal` 将 JSON 数字解码为 `float64`,序列化时 **MUST** 避免科学计数法(如 `1.775e+12` 应为 `1775540833687`)
|
|
659
|
-
> - JavaScript/TypeScript 的 `JSON.stringify` 满足 UTF-8 直接输出,但 **MUST** 确保嵌套对象键递归排序(需自定义序列化函数)
|
|
660
|
-
|
|
661
|
-
`params_hash = SHA-256(canonical_json_bytes).hex()`
|
|
662
|
-
|
|
663
|
-
#### 11.3.3 服务端验签
|
|
664
|
-
|
|
665
|
-
服务端 **MUST**:
|
|
666
|
-
|
|
667
|
-
1. 从收到的参数中提取 `client_signature`
|
|
668
|
-
2. 验证 `client_signature.aid` 与当前认证 AID 一致
|
|
669
|
-
3. 验证 `client_signature.timestamp` 在 ±300 秒新鲜度窗口内(防重放)
|
|
670
|
-
4. 用收到的实际参数(排除 `client_signature` 和 `_` 前缀字段)按 §11.3.2 重算 `params_hash`
|
|
671
|
-
5. 常量时间比较重算的 hash 与客户端声称的 `params_hash`
|
|
672
|
-
6. 用客户端注册的公钥验证 ECDSA-SHA256 签名
|
|
673
|
-
7. 所有步骤通过后才允许执行操作;任一步骤失败 **MUST** 返回错误码 `-32051`(ClientSignatureError)
|
|
674
|
-
|
|
675
|
-
> Python SDK、Go SDK、TypeScript SDK 均自动附加 `client_signature`,裸客户端必须自行实现。
|
|
676
|
-
|
|
677
|
-
### 11.4 成员移除后的安全保证
|
|
678
|
-
|
|
679
|
-
- 成员被踢出或退出后 **MUST** 立即触发 epoch 轮换
|
|
680
|
-
- 新的 group_secret **MUST NOT** 分发给已离开的成员
|
|
681
|
-
- 旧的 group_secret 仅用于解密历史消息,不用于加密新消息
|
|
682
|
-
|
|
683
|
-
---
|
|
684
|
-
|
|
685
|
-
## 12. 安全属性分析
|
|
686
|
-
|
|
687
|
-
### 12.1 安全属性总览
|
|
688
|
-
|
|
689
|
-
| 属性 | 表现 | 说明 |
|
|
690
|
-
|------|:----:|------|
|
|
691
|
-
| **epoch 间前向安全** | ✅ | 旧 group_secret 安全擦除后,该 epoch 历史消息不可解密 |
|
|
692
|
-
| **epoch 内前向安全** | ❌ | 同一 epoch 内所有消息共享同一 group_secret |
|
|
693
|
-
| **Post-compromise Security** | ⚠️ | 下次 epoch 轮换时恢复;可通过定时轮换缩短窗口 |
|
|
694
|
-
| **防中间人** | ✅ | group_secret 通过已认证的 P2P E2EE 分发 |
|
|
695
|
-
| **防服务端注入** | ✅ 检测 | Membership Commitment 绑定 group_secret(§6),配合 Membership Manifest 签名验证(§6A),提供密钥绑定 + 成员变更授权双重防护 |
|
|
696
|
-
| **防篡改** | ✅ | AAD 覆盖所有路由关键字段 |
|
|
697
|
-
| **防重放** | ✅ | 本地 seen_messages 去重 |
|
|
698
|
-
|
|
699
|
-
### 12.2 与 P2P E2EE 的对比
|
|
700
|
-
|
|
701
|
-
| 维度 | P2P E2EE | Group E2EE |
|
|
702
|
-
|------|---------|-----------|
|
|
703
|
-
| **加密模式** | prekey_ecdh_v2 / long_term_key | epoch_group_key |
|
|
704
|
-
| **密钥来源** | 每消息临时 ECDH | group_secret + HKDF 派生 |
|
|
705
|
-
| **前向安全** | 单消息级(每消息独立临时密钥对) | epoch 级 |
|
|
706
|
-
| **状态** | 零(纯工具类) | 最小(epoch + group_secret) |
|
|
707
|
-
| **可断链** | 不可能 | 不可能 |
|
|
708
|
-
| **密码学操作/条** | 1×ECDH + 1×HKDF + 1×AES-GCM | 1×HKDF + 1×AES-GCM |
|
|
709
|
-
|
|
710
|
-
### 12.3 与其他协议群组方案的对比
|
|
711
|
-
|
|
712
|
-
| 维度 | AUN (Epoch Group Key) | Signal (Sender Keys) | MLS (TreeKEM) | ANP (独立 Sender Keys) |
|
|
713
|
-
|------|:---:|:---:|:---:|:---:|
|
|
714
|
-
| **epoch 轮换分发** | O(n) | O(n²) | O(log n) | O(n²) |
|
|
715
|
-
| **每消息加密代价** | 1×HKDF + AES | 1×HMAC + AES | 1×HMAC + AES | 1×HMAC + AES |
|
|
716
|
-
| **epoch 内前向安全** | ❌ | ✅ | ✅ | ✅ |
|
|
717
|
-
| **状态量** | 2 字段 | n 个 chain state | 二叉树 | n 个 chain state |
|
|
718
|
-
| **断链风险** | 无 | 有 | 有 | 有 |
|
|
719
|
-
| **状态可恢复** | ✅ | ❌ | ❌ | ❌ |
|
|
720
|
-
|
|
721
|
-
### 12.4 设计折中说明
|
|
722
|
-
|
|
723
|
-
本方案选择 **epoch 级前向安全**(而非消息级),换取:
|
|
724
|
-
|
|
725
|
-
1. **零链状态**——不维护 hash chain,不需要 seq 同步
|
|
726
|
-
2. **不可能断链**——消息乱序、丢失、重启均不影响后续消息解密
|
|
727
|
-
3. **状态可恢复**——任何成员均可通过 epoch_key_request 恢复密钥
|
|
728
|
-
4. **O(n) 分发**——无需每个 sender 逐人广播自己的 chain key
|
|
729
|
-
|
|
730
|
-
epoch 内前向安全的缺失通过**缩短 epoch 生命周期**(定时轮换)来弥补。在 Agent 通信场景中,群组通常生命周期短、成员变更频繁,epoch 自然轮换速度快,该折中是合理的。
|
|
731
|
-
|
|
732
|
-
---
|
|
733
|
-
|
|
734
|
-
## 13. 完整交互时序
|
|
735
|
-
|
|
736
|
-
### 13.1 建群并发送加密消息
|
|
737
|
-
|
|
738
|
-
```
|
|
739
|
-
Alice (owner) Group Service Bob (member)
|
|
740
|
-
─────────────────────────────────────────────────────────────────────────
|
|
741
|
-
1. group.create({name: "..."})
|
|
742
|
-
← {group_id: "g-abc123.agentid.pub"}
|
|
743
|
-
|
|
744
|
-
2. group.add_member({group_id, aid: "bob"})
|
|
745
|
-
← {member: {...}}
|
|
746
|
-
|
|
747
|
-
3. 生成 group_secret (epoch=1)
|
|
748
|
-
计算 commitment
|
|
749
|
-
|
|
750
|
-
4. P2P E2EE → Bob:
|
|
751
|
-
{type: "e2ee.group_key_distribution",
|
|
752
|
-
group_id, epoch: 1,
|
|
753
|
-
group_secret, commitment, member_aids}
|
|
754
|
-
5. 验证 commitment
|
|
755
|
-
存储 group_secret
|
|
756
|
-
|
|
757
|
-
6. 发送加密群消息:
|
|
758
|
-
msg_key = HKDF(group_secret,
|
|
759
|
-
info="aun-group:g-abc123.agentid.pub:msg:gm-xxx")
|
|
760
|
-
ciphertext = AES-GCM(msg_key, plaintext, aad)
|
|
761
|
-
|
|
762
|
-
group.send({
|
|
763
|
-
group_id, encrypted: true,
|
|
764
|
-
payload: {type: "e2ee.group_encrypted", ...}
|
|
765
|
-
})
|
|
766
|
-
↓ 透传密文
|
|
767
|
-
→ event/group.message_created
|
|
768
|
-
7. 从信封读取 group_id, message_id
|
|
769
|
-
msg_key = HKDF(group_secret,
|
|
770
|
-
info="aun-group:g-abc123.agentid.pub:msg:gm-xxx")
|
|
771
|
-
plaintext = AES-GCM.decrypt(...)
|
|
772
|
-
```
|
|
773
|
-
|
|
774
|
-
### 13.2 踢人触发 Epoch 轮换
|
|
775
|
-
|
|
776
|
-
```
|
|
777
|
-
Alice (owner) Group Service Bob Carol
|
|
778
|
-
────────────────────────────────────────────────────────────────────────────
|
|
779
|
-
1. group.kick({group_id, aid: "bob"})
|
|
780
|
-
← success
|
|
781
|
-
|
|
782
|
-
2. 生成新 group_secret (epoch=2)
|
|
783
|
-
计算新 commitment(不含 Bob)
|
|
784
|
-
|
|
785
|
-
3. P2P E2EE → Carol:
|
|
786
|
-
{type: "e2ee.group_key_distribution",
|
|
787
|
-
epoch: 2, group_secret, ...}
|
|
788
|
-
4. 验证 commitment
|
|
789
|
-
更新本地 epoch=2
|
|
790
|
-
|
|
791
|
-
× Bob 不会收到新 epoch 密钥
|
|
792
|
-
× Bob 的旧 group_secret (epoch=1) 无法解密新消息
|
|
793
|
-
```
|
|
794
|
-
|
|
795
|
-
### 13.3 密钥恢复
|
|
796
|
-
|
|
797
|
-
```
|
|
798
|
-
Carol (成员) Alice (成员)
|
|
799
|
-
────────────────────────────────────────────────────────────────────
|
|
800
|
-
1. 收到 epoch=3 的群消息
|
|
801
|
-
本地只有 epoch=2 的 group_secret
|
|
802
|
-
解密失败
|
|
803
|
-
|
|
804
|
-
2. P2P E2EE → Alice:
|
|
805
|
-
{type: "e2ee.group_key_request",
|
|
806
|
-
group_id, epoch: 3}
|
|
807
|
-
|
|
808
|
-
3. 验证 Carol 是群成员
|
|
809
|
-
P2P E2EE → Carol:
|
|
810
|
-
{type: "e2ee.group_key_response",
|
|
811
|
-
epoch: 3, group_secret, commitment,
|
|
812
|
-
member_aids}
|
|
813
|
-
|
|
814
|
-
4. 验证 commitment
|
|
815
|
-
存储 group_secret (epoch=3)
|
|
816
|
-
重试解密群消息 → 成功
|
|
817
|
-
```
|
|
818
|
-
|
|
819
|
-
---
|
|
820
|
-
|
|
821
|
-
## 14. SDK 接口建议
|
|
822
|
-
|
|
823
|
-
### 14.1 加密
|
|
824
|
-
|
|
825
|
-
```python
|
|
826
|
-
# 通过 AUNClient 自动处理
|
|
827
|
-
await client.call("group.send", {
|
|
828
|
-
"group_id": "g-abc123.agentid.pub",
|
|
829
|
-
"payload": {"type": "text", "text": "秘密消息"},
|
|
830
|
-
"encrypt": True,
|
|
831
|
-
})
|
|
832
|
-
```
|
|
833
|
-
|
|
834
|
-
SDK 在发送前 **MUST** 检查本地是否持有该群的 group_secret:
|
|
835
|
-
- 有 → 使用 §4 的流程加密 payload
|
|
836
|
-
- 无 → 抛出 `E2EEError`,提示缺少群组密钥
|
|
837
|
-
|
|
838
|
-
### 14.2 解密
|
|
839
|
-
|
|
840
|
-
SDK 收到 `event/group.message_created` 且 `payload.type == "e2ee.group_encrypted"` 时自动解密:
|
|
841
|
-
- 从信封读取 `group_id`、`message_id`
|
|
842
|
-
- 查本地 group_secret(匹配 epoch)
|
|
843
|
-
- 执行 HKDF 派生 + AES-GCM 解密
|
|
844
|
-
- 解密失败时 **MAY** 触发 epoch_key_request
|
|
845
|
-
|
|
846
|
-
### 14.3 配置项
|
|
847
|
-
|
|
848
|
-
| 配置项 | 类型 | 默认值 | 说明 |
|
|
849
|
-
|--------|------|--------|------|
|
|
850
|
-
| `group_e2ee` | bool | `true` | 群组 E2EE 能力声明(必选能力,始终为 true,非用户开关) |
|
|
851
|
-
| `epoch_auto_rotate_interval` | int | `0` | 自动轮换间隔(秒),0 表示禁用 |
|
|
852
|
-
| `old_epoch_retention_seconds` | int | `604800` | 旧 epoch 密钥保留时间(默认 7 天) |
|
|
853
|
-
|
|
854
|
-
---
|
|
855
|
-
|
|
856
|
-
## 15. 错误码
|
|
857
|
-
|
|
858
|
-
| 错误码 | 名称 | 说明 |
|
|
859
|
-
|--------|------|------|
|
|
860
|
-
| -32040 | `E2EE_GROUP_SECRET_MISSING` | 缺少该群的 group_secret |
|
|
861
|
-
| -32041 | `E2EE_GROUP_EPOCH_MISMATCH` | 消息 epoch 与本地不匹配 |
|
|
862
|
-
| -32042 | `E2EE_GROUP_COMMITMENT_INVALID` | Membership Commitment 验证失败 |
|
|
863
|
-
| -32043 | `E2EE_GROUP_NOT_MEMBER` | 密钥请求者不是群成员 |
|
|
864
|
-
| -32044 | `E2EE_GROUP_DECRYPT_FAILED` | 群消息解密失败 |
|
|
865
|
-
|
|
866
|
-
---
|
|
867
|
-
|
|
868
|
-
## 16. 未来扩展方向
|
|
869
|
-
|
|
870
|
-
以下功能不在本规范范围内,但设计时已预留扩展空间:
|
|
871
|
-
|
|
872
|
-
### 16.1 Sender Keys 叠加(可选增强)
|
|
873
|
-
|
|
874
|
-
如需 epoch 内前向安全,可在 Epoch Group Key 基础上叠加 Sender Keys hash chain:
|
|
875
|
-
|
|
876
|
-
```
|
|
877
|
-
sender_chain_key[0] = HKDF(group_secret, info=f"sender:{sender_aid}:epoch:{epoch}")
|
|
878
|
-
sender_chain_key[i+1] = SHA-256(sender_chain_key[i])
|
|
879
|
-
msg_key[i] = HKDF(sender_chain_key[i], info="msg")
|
|
880
|
-
```
|
|
881
|
-
|
|
882
|
-
此方案从 group_secret 确定性派生,无需额外分发(O(0) 网络开销),但需要维护每个 sender 的 chain_index 状态。详见未来的 `08-AUN-E2EE-Group-SenderKeys.md`。
|
|
883
|
-
|
|
884
|
-
### 16.2 大群优化
|
|
885
|
-
|
|
886
|
-
当群规模超过 500 人时,epoch 轮换的 O(n) P2P 分发可能成为瓶颈。可考虑引入树状分发或分层密钥管理。
|
|
887
|
-
|
|
888
|
-
---
|
|
889
|
-
|
|
890
|
-
## 17. 变更记录
|
|
891
|
-
|
|
892
|
-
| 版本 | 日期 | 变更 |
|
|
893
|
-
|------|------|------|
|
|
894
|
-
| 1.0-draft-r5 | 2026-04 | 成员加入改为 MUST 轮换 epoch;服务端以 `min_read_epoch` 约束新成员加入前历史访问;删除加入轮换配置开关 |
|
|
895
|
-
| 1.0-draft-r4 | 2026-04 | 新增 Epoch CAS 轮换 RPC 的 rotation_signature 要求(§5.2.1);新增客户端操作签名要求(§11.3);补充密钥恢复异步语义(§8.5);补充外层与 AAD 绑定校验说明;升级防服务端注入安全属性 |
|
|
896
|
-
| 1.0-draft-r3 | 2026-04 | Membership Commitment 绑定 group_secret 哈希(§6.2);新增 Membership Manifest 签名机制(§6A);群密文消息新增发送方签名(§7.1);更新本地状态模型含 old_epochs(§9.2);服务端角色明确 epoch CAS 协调(§1.2);分发消息增加 manifest 字段(§5.2, §5.3) |
|
|
897
|
-
| 1.0-draft-r2 | 2026-04 | group_e2ee 默认值改为 true;补充成员加入轮换策略;group.leave 明确离开者不执行轮换 |
|
|
898
|
-
| 1.0-draft-r1 | 2026-04 | 修正:Membership Commitment 去掉 "Signed" 命名,明确为哈希一致性检测而非签名防伪;修正 message_id 来源为客户端生成;新增外层路由字段与 AAD 绑定校验要求 |
|
|
899
|
-
| 1.0-draft | 2026-04 | 初始版本:Epoch Group Key 机制;Membership Commitment;密钥恢复协议 |
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
1
|
+
# AUN-E2EE 群组扩展规范
|
|
2
|
+
|
|
3
|
+
> 版本:2.0-draft
|
|
4
|
+
> 状态:规范性文档
|
|
5
|
+
> 适用范围:AUN 客户端 SDK、客户端应用、跨语言实现
|
|
6
|
+
> 不适用范围:Group Service 服务端加解密实现
|
|
7
|
+
> 前置依赖:[08-AUN-E2EE](./08-AUN-E2EE.md)(P2P E2EE 规范,V2 群组复用其 Prekey 体系、AAD 序列化规则)、[10-Group-子协议](./10-Group-子协议.md)
|
|
8
|
+
> 定位:**群组消息端到端加密层**,基于 per-message 密钥 + per-recipient 包裹机制
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## 1. 目标与边界
|
|
13
|
+
|
|
14
|
+
本规范定义 AUN 群组成员之间的端到端消息加密协议(V2,当前唯一在用版本)。
|
|
15
|
+
|
|
16
|
+
### 1.1 目标
|
|
17
|
+
|
|
18
|
+
- 让群组内的 N 个成员在现有 `group.send` / `group.v2.send` / `event/group.message_created` 之上实现端到端加密
|
|
19
|
+
- 让 Group Service 仅看到最小必要路由元数据和密文 payload
|
|
20
|
+
- 每条消息使用独立密钥,接收方按设备分别持有密钥包裹(wrap),不依赖群级共享对称密钥
|
|
21
|
+
- 为各语言 SDK 提供统一的群组密文格式、成员状态签名机制和恢复语义
|
|
22
|
+
|
|
23
|
+
### 1.2 服务端职责
|
|
24
|
+
|
|
25
|
+
Group Service **只做**:
|
|
26
|
+
|
|
27
|
+
- 认证发送方(JWT token / AID 证书)
|
|
28
|
+
- 校验群成员权限
|
|
29
|
+
- 存储共享密文体(`v2_group_messages`)和逐设备密钥包裹(`v2_group_wraps`)
|
|
30
|
+
- 校验消息结构完整性(recipients 排序、digest、必要审计包裹等)
|
|
31
|
+
- 维护群成员状态的签名版本链(`state_version`/`state_chain`),提供分叉检测的比对基准
|
|
32
|
+
- 广播加密消息通知,按设备分发对应的密钥包裹
|
|
33
|
+
|
|
34
|
+
Group Service **绝不做**:
|
|
35
|
+
|
|
36
|
+
- 加解密群消息
|
|
37
|
+
- 持有或管理消息密钥、成员密钥明文
|
|
38
|
+
- 参与密钥协商或密钥派生
|
|
39
|
+
|
|
40
|
+
### 1.3 设计原则
|
|
41
|
+
|
|
42
|
+
| 原则 | 说明 |
|
|
43
|
+
|------|------|
|
|
44
|
+
| **消息级密钥** | 每条消息使用独立随机密钥,发送方一次性会话密钥用完即弃,不维护群级长期对称密钥 |
|
|
45
|
+
| **复用 P2P V2 Prekey 体系** | 接收方设备密钥包裹基于 P2P E2EE([08-AUN-E2EE](./08-AUN-E2EE.md))的 IK/SPK Prekey 机制,缺少群内设备 Prekey 时可回退到该设备已注册的 P2P Prekey |
|
|
46
|
+
| **不信任服务端** | 消息密钥从未经过服务端明文;接收方按设备分别持有的密钥包裹只有对应设备的私钥能解开 |
|
|
47
|
+
| **成员状态可验证** | 群成员集合与权限的变更通过签名的状态版本链(state_version/state_chain)記录,接收方可据此检测状态分叉或篡改 |
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## 2. 与 AUN-Core 的关系
|
|
52
|
+
|
|
53
|
+
AUN-E2EE-Group 建立在以下核心能力之上:
|
|
54
|
+
|
|
55
|
+
- **P2P E2EE V2**([08-AUN-E2EE](./08-AUN-E2EE.md)):设备 Prekey(IK/SPK)注册与管理机制、AAD 序列化规则(Canonical JSON)均直接复用
|
|
56
|
+
- **Group 子协议**([10-Group-子协议](./10-Group-子协议.md)):群组管理、成员管理、消息传输
|
|
57
|
+
- **AID + 证书链身份体系**:成员身份验证
|
|
58
|
+
|
|
59
|
+
群组密文消息通过 `group.v2.send` 提交,`group.send` 承载明文/兼容路径;Group Service 无需识别密文 payload 内部字段。
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## 3. 术语
|
|
64
|
+
|
|
65
|
+
### 3.1 Epoch
|
|
66
|
+
|
|
67
|
+
群密钥版本号(uint32,从 1 开始)。用于在消息 AAD 中标记消息所属的密钥轮换周期,服务端据此拒绝使用过期版本发送的消息,与下方 State Version 是两条独立轨道。
|
|
68
|
+
|
|
69
|
+
### 3.2 State Version / State Chain
|
|
70
|
+
|
|
71
|
+
群成员状态(成员列表、管理员集合、审计接收者等)的签名版本号(uint32,从 0 开始,未签名任何状态时为 0)。每次 owner/admin 提交并确认一次成员状态变更,版本递增。`state_chain` 是历次已确认状态的链式记录,客户端可据此检测服务端是否篡改历史或出现并发分叉。
|
|
72
|
+
|
|
73
|
+
### 3.3 State Commitment
|
|
74
|
+
|
|
75
|
+
对当前群成员集合、管理员集合、审计接收者等状态字段的摘要值,绑定到 `epoch`,写入每条加密消息的 AAD 中,使消息与发送时刻的群状态可验证关联。
|
|
76
|
+
|
|
77
|
+
### 3.4 Recipients Digest
|
|
78
|
+
|
|
79
|
+
一条群消息的所有设备密钥包裹(recipient 行)经排序后计算得到的摘要根值,纳入发送方签名覆盖范围,用于防止服务端在按设备拆分投递时篡改或替换某个接收方的密钥包裹。
|
|
80
|
+
|
|
81
|
+
### 3.5 密文群消息
|
|
82
|
+
|
|
83
|
+
通过 `group.v2.send` 传输的加密群消息,`payload.type` **MUST** 为 `e2ee.group_encrypted`。
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## 4. 密钥体系概述
|
|
88
|
+
|
|
89
|
+
### 4.1 密钥层次
|
|
90
|
+
|
|
91
|
+
| 密钥 | 生成方 | 生命周期 | 说明 |
|
|
92
|
+
|------|--------|---------|------|
|
|
93
|
+
| 消息密钥(对称) | 发送方,每条消息独立生成 | 单条消息 | 用于加密消息正文,从不直接传输,按接收方设备分别用密钥包裹机制加密后随消息一起发送 |
|
|
94
|
+
| 发送方一次性会话密钥 | 发送方,每条消息独立生成 | 单条消息 | 用于与接收方设备的长期身份/Prekey 做密钥协商,加密后即可丢弃 |
|
|
95
|
+
| 接收方设备身份密钥(IK) | 接收方,AID 级别 | 长期 | 与 AID 身份证书绑定 |
|
|
96
|
+
| 接收方设备 Prekey(SPK) | 接收方,设备级别 | 定期轮换 | 群内设备 Prekey(`group_device_prekey`)通过 `group.v2.put_group_pk` 注册;缺失时回退到该设备已注册的 P2P Prekey,此时安全强度降级为仅身份密钥参与协商 |
|
|
97
|
+
|
|
98
|
+
具体的密钥协商组合方式(是否使用 Prekey、密钥派生的哈希函数与输入构成)与 [08-AUN-E2EE](./08-AUN-E2EE.md) 的 P2P V2 加密模式一致,本规范不重复列出精确公式,实现请参照该文档及各语言 SDK 的密码学模块源码。
|
|
99
|
+
|
|
100
|
+
### 4.2 每条消息的密钥包裹
|
|
101
|
+
|
|
102
|
+
一条加密群消息只加密一次消息正文,但会为每个当前应接收该消息的设备分别生成一份密钥包裹(recipient wrap),使得只有对应设备的私钥能解出消息密钥。密钥包裹按角色分类:
|
|
103
|
+
|
|
104
|
+
- `member`:普通群成员设备
|
|
105
|
+
- `self_sync`:发送方自己在其他设备上的同步副本
|
|
106
|
+
- `audit`:群配置的审计接收者(如有),用于合规场景下的旁路可见性,不改变成员关系
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## 5. 消息生命周期
|
|
111
|
+
|
|
112
|
+
### 5.1 Bootstrap(发送前的成员/密钥快照)
|
|
113
|
+
|
|
114
|
+
发送方在加密消息前调用 `group.v2.bootstrap` 获取:
|
|
115
|
+
|
|
116
|
+
- 当前所有(committed)成员的设备身份/Prekey 公钥列表
|
|
117
|
+
- 当前 `epoch`
|
|
118
|
+
- 当前 `state_version` / `state_hash` / `state_chain`
|
|
119
|
+
- `committed_member_aids`:已通过状态签名确认的成员
|
|
120
|
+
- `pending_adds` / `pending_removes`:尚未签名确认的成员变更
|
|
121
|
+
- `e2ee_security_level`:`end_to_end`(正常受控群)或 `transport`(open/邀请码等任何人可自由加入的群,此时密钥包裹接收方集合不与"被授权成员"强绑定,安全强度退化为等价于传输层加密,不构成严格端到端)
|
|
122
|
+
|
|
123
|
+
对于设置了成员审批(private/approval/closed)的群,尚未完成状态签名确认的新成员(pending)不会出现在 bootstrap 返回的设备列表中,发送方因而不会为其生成密钥包裹,实现了"未确认成员收不到密文"的天然隔离。SDK 通常对 bootstrap 结果做短期缓存,成员或状态变更后需要刷新。
|
|
124
|
+
|
|
125
|
+
### 5.2 加密与发送
|
|
126
|
+
|
|
127
|
+
1. 发送方生成本条消息的消息密钥与一次性会话密钥
|
|
128
|
+
2. 构造 AAD(含 `group_id`、`epoch`、`message_id`、发送方身份、`state_commitment` 等,详见 §7)
|
|
129
|
+
3. 用消息密钥对正文做认证加密,AAD 参与认证但不加密
|
|
130
|
+
4. 为 bootstrap 返回的每个目标设备生成密钥包裹(消息密钥被目标设备的密钥协商结果加密)
|
|
131
|
+
5. 对所有密钥包裹排序后计算 Recipients Digest,随同密文一起纳入发送方签名
|
|
132
|
+
6. 调用 `group.v2.send` 提交完整信封(含密文、AAD、Recipients Digest、发送方签名、所有密钥包裹)
|
|
133
|
+
|
|
134
|
+
服务端收到后校验成员资格、`epoch` 是否为当前值、密钥包裹排序与 Recipients Digest 是否自洽、必要审计包裹是否齐全,通过后落库并按设备拆分推送。
|
|
135
|
+
|
|
136
|
+
### 5.3 拉取与解密
|
|
137
|
+
|
|
138
|
+
接收方通过 `group.v2.pull` 按设备拉取属于自己的消息(服务端只返回该设备自己的那一份密钥包裹,可附带 Merkle 证明用于验证该包裹确实是发送方原始签名集合的一部分而未被服务端替换)。接收方:
|
|
139
|
+
|
|
140
|
+
1. 验证发送方签名(覆盖密文、AAD、Recipients Digest)
|
|
141
|
+
2. 用本机私钥解出分配给自己的消息密钥
|
|
142
|
+
3. 用消息密钥解密正文并校验 AAD 一致性
|
|
143
|
+
|
|
144
|
+
解密失败的常见原因:使用了已轮换的旧 Prekey、bootstrap 快照过期导致密钥包裹面向错误的设备、或密文/AAD 被篡改。恢复方式通常是刷新 bootstrap 后等待下一条消息,或依赖服务端 `group.v2.pull` 的历史消息重新拉取。
|
|
145
|
+
|
|
146
|
+
### 5.4 拉取确认
|
|
147
|
+
|
|
148
|
+
接收方通过 `group.v2.ack` 提交已消费到的消息序号游标,用于服务端做 retention 与增量拉取范围计算。
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## 6. 成员状态签名(State Version)
|
|
153
|
+
|
|
154
|
+
### 6.1 目的
|
|
155
|
+
|
|
156
|
+
群成员集合、管理员集合等状态信息如果只由服务端记录,无法防止服务端篡改或伪造。V2 引入 owner/admin 对状态变更进行签名确认的两阶段流程,使状态变更可追溯、可验证、且能检测并发冲突。
|
|
157
|
+
|
|
158
|
+
### 6.2 两阶段流程
|
|
159
|
+
|
|
160
|
+
1. **提案**(`group.v2.propose_state`):owner/admin 提交目标 `state_version`、状态摘要(`state_hash`)、成员快照,并附上覆盖这些字段的签名。首次状态签名(`state_version = 1`)**MUST** 由 owner 发起,此后 owner/admin 均可发起。服务端记录为待确认提案,设有自动确认超时。
|
|
161
|
+
2. **确认**(`group.v2.confirm_state`):发起者(或后续管理员)提交提案 ID 完成确认,服务端以比较并交换(CAS)方式原子推进 `state_version`,若确认前已有其他提案抢先生效则确认失败,需要基于最新状态重新发起。
|
|
162
|
+
|
|
163
|
+
该机制保证并发的状态变更提案中至多一个能生效,避免服务端或多个管理员产生分叉的成员视图。
|
|
164
|
+
|
|
165
|
+
### 6.3 分叉检测
|
|
166
|
+
|
|
167
|
+
客户端在 bootstrap 时获得当前 `state_chain`,与本地已知的历史链做连续性比对;若发现不连续或矛盾,视为潜在的服务端篡改或并发冲突信号,应用层应提示用户或拒绝基于该状态发送敏感消息。
|
|
168
|
+
|
|
169
|
+
### 6.4 Pending 成员语义
|
|
170
|
+
|
|
171
|
+
新加入但尚未被状态签名确认的成员(`pending_adds`)在成员审批类群组(private/approval/closed)中不会出现在 bootstrap 的设备列表里,因而无法接收此前及此后(直到状态确认)发出的加密消息;在开放加入类群组(open/邀请码)中不做此限制,此时群组的 `e2ee_security_level` 会被标记为 `transport`(见 §5.1),提示这类群组的加密强度弱于严格端到端。
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## 7. 消息格式与 AAD
|
|
176
|
+
|
|
177
|
+
### 7.1 AAD 关键字段
|
|
178
|
+
|
|
179
|
+
| 字段 | 说明 |
|
|
180
|
+
|------|------|
|
|
181
|
+
| `group_id` | 群组唯一标识 |
|
|
182
|
+
| `from` / `from_device` | 发送方 AID 与设备标识 |
|
|
183
|
+
| `message_id` | 消息唯一标识,发送方生成,参与认证 |
|
|
184
|
+
| `epoch` | 消息所属的密钥版本号 |
|
|
185
|
+
| `state_commitment` | 绑定发送时刻的成员状态摘要(`state_version`/`state_hash`/`state_chain`) |
|
|
186
|
+
|
|
187
|
+
AAD 采用 Canonical JSON 序列化(递归键排序、UTF-8 直出、紧凑格式),规则与 [08-AUN-E2EE §8.3](./08-AUN-E2EE.md) 完全一致。
|
|
188
|
+
|
|
189
|
+
### 7.2 外层信封
|
|
190
|
+
|
|
191
|
+
密文 payload 通过 `group.v2.send` 的 `envelope` 参数提交;接收方通过 `group.v2.pull` 按设备取回,返回内容包含该设备自己的密钥包裹与(如适用)该包裹在原始签名集合中的完整性证明。
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## 8. 密钥恢复与历史访问
|
|
196
|
+
|
|
197
|
+
- 接收方若因设备 Prekey 轮换、bootstrap 快照过期等原因暂时无法解密某条消息,通常的恢复路径是刷新 bootstrap 快照后重试,而非向对端请求补发密钥(V2 不存在需要跨端口头传递的群级共享密钥)。
|
|
198
|
+
- 新成员加入前发出的历史消息,其密钥包裹集合本就不包含新成员设备,因此新成员天然无法解密加入前的历史消息,无需额外的"历史隔离"机制。
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## 9. 客户端密钥存储
|
|
203
|
+
|
|
204
|
+
- 发送方一次性会话密钥用后即弃,不持久化。
|
|
205
|
+
- 接收方的设备身份密钥(IK)与设备 Prekey(SPK)私钥的存储、保护方式与 [08-AUN-E2EE](./08-AUN-E2EE.md) 的 P2P V2 Prekey 存储机制一致(本地密钥库 + 平台级密钥保护,明文不落盘)。
|
|
206
|
+
- 消息密钥本身不需要持久化——每条消息独立生成、独立解出、用后即弃;除非应用层自行缓存明文消息以支持离线查看。
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## 10. 防重放与防篡改
|
|
211
|
+
|
|
212
|
+
### 10.1 防篡改
|
|
213
|
+
|
|
214
|
+
AAD 覆盖所有路由关键字段(`group_id`、`from`、`epoch`、`message_id`、`state_commitment`),任何篡改导致认证加密校验失败而拒绝解密;发送方签名进一步覆盖密文、AAD 与 Recipients Digest,防止服务端在拆分投递时替换或增删某个接收方的密钥包裹。
|
|
215
|
+
|
|
216
|
+
### 10.2 防重放
|
|
217
|
+
|
|
218
|
+
- 接收方 **MUST** 维护本地去重集合,以 `{group_id}:{from}:{message_id}` 为 key 拒绝重复消息。
|
|
219
|
+
- `group.v2.send` 的时间戳需在合理新鲜度窗口内,超出窗口的请求会被服务端拒绝。
|
|
220
|
+
|
|
221
|
+
### 10.3 Epoch/State 校验
|
|
222
|
+
|
|
223
|
+
- 服务端拒绝 `epoch` 与当前群密钥版本不一致的发送请求,客户端遇到此类拒绝应刷新 bootstrap 后重试。
|
|
224
|
+
- 接收方应校验消息 AAD 中的 `state_commitment` 与本地已知状态链一致,不一致时视为潜在分叉信号。
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
## 11. 安全约定
|
|
229
|
+
|
|
230
|
+
### 11.1 通用约定
|
|
231
|
+
|
|
232
|
+
- 加密失败时 **MUST NOT** 静默降级为明文
|
|
233
|
+
- 每条消息使用独立的随机密钥与随机 nonce
|
|
234
|
+
- 密钥材料 **MUST** 由密码学安全随机数生成器生成
|
|
235
|
+
- 实现 **MUST NOT** 在日志中输出消息密钥或密钥包裹解密结果
|
|
236
|
+
|
|
237
|
+
### 11.2 客户端操作签名
|
|
238
|
+
|
|
239
|
+
以下群组操作 **MUST** 附加客户端 ECDSA 签名(`client_signature` 字段),服务端强制验签:
|
|
240
|
+
|
|
241
|
+
- `group.send` / `group.v2.send`
|
|
242
|
+
- `group.add_member`
|
|
243
|
+
- `group.kick`
|
|
244
|
+
- `group.leave`
|
|
245
|
+
- `group.update_rules`
|
|
246
|
+
|
|
247
|
+
签名生成、`params_hash` 计算规则与验签流程见 [10-Group-子协议](./10-Group-子协议.md) 及 SDK RPC 手册中对应方法的说明,本规范不重复列出。
|
|
248
|
+
|
|
249
|
+
### 11.3 成员移除后的安全保证
|
|
250
|
+
|
|
251
|
+
- 成员被踢出或退出后,后续消息 bootstrap 不再包含其设备,因而无法为其生成新的密钥包裹;已离开成员无法解密移除后发出的消息。
|
|
252
|
+
- 已离开成员此前收到的历史消息密钥包裹不受影响(历史消息本就已经解密完成或本地缓存)。
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
## 12. 安全属性分析
|
|
257
|
+
|
|
258
|
+
### 12.1 安全属性总览
|
|
259
|
+
|
|
260
|
+
| 属性 | 表现 | 说明 |
|
|
261
|
+
|------|:----:|------|
|
|
262
|
+
| **消息级前向安全** | ✅ | 每条消息使用独立密钥,发送方一次性会话密钥用后即弃 |
|
|
263
|
+
| **防中间人** | ✅ | 密钥协商基于已认证的设备身份密钥与 Prekey |
|
|
264
|
+
| **防服务端注入** | ⚠️ | State Commitment 绑定消息到特定的已签名成员状态,State Chain 提供分叉检测,但完整防护依赖应用层对状态链连续性的主动校验 |
|
|
265
|
+
| **防篡改** | ✅ | AAD 覆盖所有路由关键字段,发送方签名覆盖密文与 Recipients Digest |
|
|
266
|
+
| **防重放** | ✅ | 本地去重 + 时间戳新鲜度窗口 |
|
|
267
|
+
| **成员变更后的密钥隔离** | ✅ | 密钥包裹按当前 bootstrap 快照生成,新成员收不到历史消息包裹,被移除成员收不到后续消息包裹 |
|
|
268
|
+
|
|
269
|
+
### 12.2 与 V1(已禁用)的本质差异
|
|
270
|
+
|
|
271
|
+
V1 机制(Epoch Group Key:群级共享对称密钥 `group_secret` + 版本号 `epoch` + 通过 P2P 通道向每个成员分发同一把密钥)已在线上全面禁用,不再是当前协议的一部分。V2 用"每条消息独立密钥 + 逐设备密钥包裹"取代了群级共享密钥模型,因此不再需要 V1 中用于同步 `epoch` 的一次性 CAS 轮换 RPC;群成员/权限的变更改由 §6 的状态签名两阶段流程记录和验证。
|
|
272
|
+
|
|
273
|
+
---
|
|
274
|
+
|
|
275
|
+
## 13. 错误码
|
|
276
|
+
|
|
277
|
+
| 错误码 | 名称 | 说明 |
|
|
278
|
+
|--------|------|------|
|
|
279
|
+
| -32040 | `E2EE_GROUP_KEY_MISSING` | 找不到属于本设备的密钥包裹 |
|
|
280
|
+
| -32041 | `E2EE_GROUP_EPOCH_MISMATCH` | 消息 epoch 与服务端当前值不匹配 |
|
|
281
|
+
| -32042 | `E2EE_GROUP_STATE_MISMATCH` | State Commitment / State Chain 校验失败 |
|
|
282
|
+
| -32044 | `E2EE_GROUP_DECRYPT_FAILED` | 群消息解密失败 |
|
|
283
|
+
|
|
284
|
+
---
|
|
285
|
+
|
|
286
|
+
## 14. 变更记录
|
|
287
|
+
|
|
288
|
+
| 版本 | 日期 | 变更 |
|
|
289
|
+
|------|------|------|
|
|
290
|
+
| 2.0-draft | 2026-07 | 重写为 V2 机制(消息级密钥 + 逐设备密钥包裹 + 状态签名两阶段流程),移除已禁用的 V1 Epoch Group Key 机制描述 |
|
|
291
|
+
| 1.0-draft-r5 | 2026-04 | (V1,已废弃)成员加入改为 MUST 轮换 epoch;服务端以 `min_read_epoch` 约束新成员加入前历史访问 |
|
|
292
|
+
| 1.0-draft-r4 | 2026-04 | (V1,已废弃)新增 Epoch CAS 轮换 RPC 的 rotation_signature 要求 |
|
|
293
|
+
| 1.0-draft-r3 | 2026-04 | (V1,已废弃)Membership Commitment 绑定 group_secret 哈希;新增 Membership Manifest 签名机制 |
|
|
294
|
+
| 1.0-draft-r2 | 2026-04 | (V1,已废弃)group_e2ee 默认值改为 true;补充成员加入轮换策略 |
|
|
295
|
+
| 1.0-draft-r1 | 2026-04 | (V1,已废弃)修正 Membership Commitment 命名与 message_id 来源 |
|
|
296
|
+
| 1.0-draft | 2026-04 | (V1,已废弃)初始版本:Epoch Group Key 机制 |
|