@agentunion/fastaun 0.2.13

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.
Files changed (69) hide show
  1. package/LICENSE +17 -0
  2. package/README.md +78 -0
  3. package/dist/auth.d.ts +287 -0
  4. package/dist/auth.js +1668 -0
  5. package/dist/auth.js.map +1 -0
  6. package/dist/client.d.ts +359 -0
  7. package/dist/client.js +3918 -0
  8. package/dist/client.js.map +1 -0
  9. package/dist/config.d.ts +43 -0
  10. package/dist/config.js +119 -0
  11. package/dist/config.js.map +1 -0
  12. package/dist/crypto.d.ts +41 -0
  13. package/dist/crypto.js +85 -0
  14. package/dist/crypto.js.map +1 -0
  15. package/dist/discovery.d.ts +22 -0
  16. package/dist/discovery.js +110 -0
  17. package/dist/discovery.js.map +1 -0
  18. package/dist/e2ee-group.d.ts +192 -0
  19. package/dist/e2ee-group.js +1134 -0
  20. package/dist/e2ee-group.js.map +1 -0
  21. package/dist/e2ee.d.ts +120 -0
  22. package/dist/e2ee.js +890 -0
  23. package/dist/e2ee.js.map +1 -0
  24. package/dist/errors.d.ts +115 -0
  25. package/dist/errors.js +253 -0
  26. package/dist/errors.js.map +1 -0
  27. package/dist/events.d.ts +39 -0
  28. package/dist/events.js +82 -0
  29. package/dist/events.js.map +1 -0
  30. package/dist/index.d.ts +23 -0
  31. package/dist/index.js +32 -0
  32. package/dist/index.js.map +1 -0
  33. package/dist/keystore/aid-db.d.ts +79 -0
  34. package/dist/keystore/aid-db.js +621 -0
  35. package/dist/keystore/aid-db.js.map +1 -0
  36. package/dist/keystore/file.d.ts +82 -0
  37. package/dist/keystore/file.js +395 -0
  38. package/dist/keystore/file.js.map +1 -0
  39. package/dist/keystore/index.d.ts +88 -0
  40. package/dist/keystore/index.js +7 -0
  41. package/dist/keystore/index.js.map +1 -0
  42. package/dist/keystore/sqlite-backup.d.ts +40 -0
  43. package/dist/keystore/sqlite-backup.js +379 -0
  44. package/dist/keystore/sqlite-backup.js.map +1 -0
  45. package/dist/logger.d.ts +6 -0
  46. package/dist/logger.js +53 -0
  47. package/dist/logger.js.map +1 -0
  48. package/dist/namespaces/auth.d.ts +49 -0
  49. package/dist/namespaces/auth.js +248 -0
  50. package/dist/namespaces/auth.js.map +1 -0
  51. package/dist/namespaces/custody.d.ts +47 -0
  52. package/dist/namespaces/custody.js +231 -0
  53. package/dist/namespaces/custody.js.map +1 -0
  54. package/dist/secret-store/file-store.d.ts +25 -0
  55. package/dist/secret-store/file-store.js +124 -0
  56. package/dist/secret-store/file-store.js.map +1 -0
  57. package/dist/secret-store/index.d.ts +28 -0
  58. package/dist/secret-store/index.js +19 -0
  59. package/dist/secret-store/index.js.map +1 -0
  60. package/dist/seq-tracker.d.ts +29 -0
  61. package/dist/seq-tracker.js +221 -0
  62. package/dist/seq-tracker.js.map +1 -0
  63. package/dist/transport.d.ts +60 -0
  64. package/dist/transport.js +355 -0
  65. package/dist/transport.js.map +1 -0
  66. package/dist/types.d.ts +170 -0
  67. package/dist/types.js +12 -0
  68. package/dist/types.js.map +1 -0
  69. package/package.json +42 -0
@@ -0,0 +1,1134 @@
1
+ /**
2
+ * GroupE2EEManager — 群组端到端加密管理器 + 纯函数
3
+ *
4
+ * 与 E2EEManager 平行:所有网络操作(P2P 发送、RPC 调用)由调用方负责。
5
+ * 内置防重放、epoch 降级防护、密钥请求频率限制。
6
+ */
7
+ import * as crypto from 'node:crypto';
8
+ import { E2EEError, E2EEGroupSecretMissingError, } from './errors.js';
9
+ import { isJsonObject, } from './types.js';
10
+ // ── 辅助:证书 SHA-256 指纹 ──────────────────────────────────────
11
+ /** PEM 证书 → sha256:{hex} 指纹(与 Python/Go/JS SDK 一致) */
12
+ function certSha256Fingerprint(certPem) {
13
+ const pem = typeof certPem === 'string' ? certPem : certPem.toString('utf-8');
14
+ const b64 = pem.replace(/-----[^-]+-----/g, '').replace(/\s+/g, '');
15
+ const der = Buffer.from(b64, 'base64');
16
+ return `sha256:${crypto.createHash('sha256').update(der).digest('hex')}`;
17
+ }
18
+ // ── 常量 ───────────────────────────────────────────────────────
19
+ const SUITE = 'P256_HKDF_SHA256_AES_256_GCM';
20
+ const MODE_EPOCH_GROUP_KEY = 'epoch_group_key';
21
+ /** 群组消息 AAD 字段 */
22
+ const AAD_FIELDS_GROUP = [
23
+ 'group_id', 'from', 'message_id', 'timestamp',
24
+ 'epoch', 'encryption_mode', 'suite',
25
+ ];
26
+ /** 群组消息 AAD 匹配字段 */
27
+ const AAD_MATCH_FIELDS_GROUP = [
28
+ 'group_id', 'from', 'message_id',
29
+ 'epoch', 'encryption_mode', 'suite',
30
+ ];
31
+ /** 旧 epoch 默认保留 7 天 */
32
+ const OLD_EPOCH_RETENTION_SECONDS = 7 * 24 * 3600;
33
+ // ── Epoch Transcript Chain ────────────────────────────────────
34
+ const EPOCH_CHAIN_GENESIS_PREFIX = Buffer.from('aun-epoch-chain:genesis', 'utf-8');
35
+ /**
36
+ * 计算 Epoch Transcript Chain。
37
+ * genesis(prev_chain=null)使用固定前缀;后续 epoch 使用上一个 chain 的字节。
38
+ */
39
+ export function computeEpochChain(prevChain, epoch, commitment, rotatorAid) {
40
+ const prefix = prevChain === null
41
+ ? EPOCH_CHAIN_GENESIS_PREFIX
42
+ : Buffer.from(prevChain, 'hex');
43
+ const epochBuf = Buffer.alloc(4);
44
+ epochBuf.writeUInt32BE(epoch, 0);
45
+ const data = Buffer.concat([
46
+ prefix,
47
+ epochBuf,
48
+ Buffer.from(commitment, 'utf-8'),
49
+ Buffer.from(rotatorAid, 'utf-8'),
50
+ ]);
51
+ return crypto.createHash('sha256').update(data).digest('hex');
52
+ }
53
+ /**
54
+ * 验证 Epoch Transcript Chain(常量时间比较)。
55
+ */
56
+ export function verifyEpochChain(epochChain, prevChain, epoch, commitment, rotatorAid) {
57
+ const expected = computeEpochChain(prevChain, epoch, commitment, rotatorAid);
58
+ const a = Buffer.from(expected, 'hex');
59
+ const b = Buffer.from(epochChain, 'hex');
60
+ if (a.length !== b.length)
61
+ return false;
62
+ return crypto.timingSafeEqual(a, b);
63
+ }
64
+ function loadKeyStoreGroupEpoch(keystore, aid, groupId, epoch) {
65
+ if (typeof keystore.loadGroupSecretEpoch === 'function') {
66
+ return keystore.loadGroupSecretEpoch(aid, groupId, epoch);
67
+ }
68
+ throw new Error(`keystore ${keystore.constructor?.name ?? 'unknown'} missing loadGroupSecretEpoch method`);
69
+ }
70
+ function loadKeyStoreGroupEpochs(keystore, aid, groupId) {
71
+ if (typeof keystore.loadGroupSecretEpochs === 'function') {
72
+ return keystore.loadGroupSecretEpochs(aid, groupId);
73
+ }
74
+ throw new Error(`keystore ${keystore.constructor?.name ?? 'unknown'} missing loadGroupSecretEpochs method`);
75
+ }
76
+ function storeKeyStoreGroupTransition(keystore, aid, groupId, opts) {
77
+ if (typeof keystore.storeGroupSecretTransition !== 'function')
78
+ return null;
79
+ return keystore.storeGroupSecretTransition(aid, groupId, {
80
+ ...opts,
81
+ oldEpochRetentionMs: OLD_EPOCH_RETENTION_SECONDS * 1000,
82
+ });
83
+ }
84
+ function storeKeyStoreGroupEpoch(keystore, aid, groupId, opts) {
85
+ if (typeof keystore.storeGroupSecretEpoch !== 'function')
86
+ return null;
87
+ return keystore.storeGroupSecretEpoch(aid, groupId, {
88
+ ...opts,
89
+ oldEpochRetentionMs: OLD_EPOCH_RETENTION_SECONDS * 1000,
90
+ });
91
+ }
92
+ function cleanupKeyStoreGroupOldEpochs(keystore, aid, groupId, cutoffMs) {
93
+ if (typeof keystore.cleanupGroupOldEpochsState === 'function') {
94
+ return keystore.cleanupGroupOldEpochsState(aid, groupId, cutoffMs);
95
+ }
96
+ throw new Error(`keystore ${keystore.constructor?.name ?? 'unknown'} missing cleanupGroupOldEpochsState method`);
97
+ }
98
+ function deleteKeyStoreGroupState(keystore, aid, groupId) {
99
+ if (typeof keystore.deleteGroupSecretState === 'function') {
100
+ keystore.deleteGroupSecretState(aid, groupId);
101
+ return;
102
+ }
103
+ throw new Error(`keystore ${keystore.constructor?.name ?? 'unknown'} missing deleteGroupSecretState method`);
104
+ }
105
+ // ── 内部工具函数 ───────────────────────────────────────────────
106
+ /** HKDF-SHA256 派生密钥 */
107
+ function hkdfDeriveSync(ikm, info, length) {
108
+ const derived = crypto.hkdfSync('sha256', ikm, Buffer.alloc(0), info, length);
109
+ return Buffer.from(derived);
110
+ }
111
+ /** AES-256-GCM 加密 */
112
+ function aesGcmEncrypt(key, plaintext, aad) {
113
+ const nonce = crypto.randomBytes(12);
114
+ const cipher = crypto.createCipheriv('aes-256-gcm', key, nonce);
115
+ cipher.setAAD(aad);
116
+ const encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()]);
117
+ const tag = cipher.getAuthTag();
118
+ return { ciphertext: encrypted, tag, nonce };
119
+ }
120
+ /** AES-256-GCM 解密 */
121
+ function aesGcmDecrypt(key, ciphertext, tag, nonce, aad) {
122
+ const decipher = crypto.createDecipheriv('aes-256-gcm', key, nonce);
123
+ decipher.setAuthTag(tag);
124
+ decipher.setAAD(aad);
125
+ return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
126
+ }
127
+ /** ECDSA-SHA256 签名 */
128
+ function ecdsaSign(privateKeyPem, data) {
129
+ const signer = crypto.createSign('SHA256');
130
+ signer.update(data);
131
+ signer.end();
132
+ return signer.sign(privateKeyPem);
133
+ }
134
+ /** ECDSA-SHA256 验签 */
135
+ function ecdsaVerify(publicKey, signature, data) {
136
+ const verifier = crypto.createVerify('SHA256');
137
+ verifier.update(data);
138
+ verifier.end();
139
+ return verifier.verify(publicKey, signature);
140
+ }
141
+ function groupKeyResponseSignData(payload) {
142
+ const fields = [
143
+ String(payload.response_version ?? 1),
144
+ String(payload.group_id ?? ''),
145
+ String(payload.epoch ?? 0),
146
+ String(payload.requester_aid ?? ''),
147
+ String(payload.request_id ?? ''),
148
+ String(payload.responder_aid ?? ''),
149
+ String(payload.commitment ?? ''),
150
+ [...(payload.member_aids ?? [])].sort().join('|'),
151
+ String(payload.issued_at ?? 0),
152
+ ];
153
+ return Buffer.from(fields.join('\n'), 'utf-8');
154
+ }
155
+ export function signGroupKeyResponse(payload, privateKeyPem) {
156
+ const signed = {
157
+ ...payload,
158
+ response_version: payload.response_version ?? 1,
159
+ issued_at: payload.issued_at ?? Date.now(),
160
+ };
161
+ signed.response_signature = ecdsaSign(privateKeyPem, groupKeyResponseSignData(signed)).toString('base64');
162
+ return signed;
163
+ }
164
+ export function verifyGroupKeyResponseSignature(payload, responderCertPem) {
165
+ const sigB64 = payload.response_signature;
166
+ if (!sigB64)
167
+ return false;
168
+ try {
169
+ return ecdsaVerify(pemToCertPublicKey(responderCertPem), Buffer.from(sigB64, 'base64'), groupKeyResponseSignData(payload));
170
+ }
171
+ catch {
172
+ return false;
173
+ }
174
+ }
175
+ /** PEM 证书 → 公钥 KeyObject */
176
+ function pemToCertPublicKey(certPem) {
177
+ const pem = typeof certPem === 'string' ? certPem : certPem.toString('utf-8');
178
+ const x509 = new crypto.X509Certificate(pem);
179
+ return x509.publicKey;
180
+ }
181
+ // ── 群组 AAD 工具 ─────────────────────────────────────────────
182
+ /** 群组 AAD 序列化(sorted keys, compact JSON) */
183
+ function aadBytesGroup(aad) {
184
+ const obj = {};
185
+ for (const field of AAD_FIELDS_GROUP) {
186
+ obj[field] = aad[field] ?? null;
187
+ }
188
+ const sorted = {};
189
+ for (const k of Object.keys(obj).sort()) {
190
+ sorted[k] = obj[k];
191
+ }
192
+ return Buffer.from(JSON.stringify(sorted), 'utf-8');
193
+ }
194
+ /** 群组 AAD 字段匹配检查 */
195
+ function aadMatchesGroup(expected, actual) {
196
+ for (const f of AAD_MATCH_FIELDS_GROUP) {
197
+ if (String(expected[f] ?? '') !== String(actual[f] ?? '')) {
198
+ return false;
199
+ }
200
+ }
201
+ return true;
202
+ }
203
+ // ── 群消息密钥派生 ────────────────────────────────────────────
204
+ /** 从 group_secret 派生单条群消息的加密密钥 */
205
+ function deriveGroupMsgKey(groupSecret, groupId, messageId) {
206
+ return hkdfDeriveSync(groupSecret, Buffer.from(`aun-group:${groupId}:msg:${messageId}`, 'utf-8'), 32);
207
+ }
208
+ // ── 纯函数:群组消息加解密 ────────────────────────────────────
209
+ /**
210
+ * 加密群组消息,返回 e2ee.group_encrypted 信封。
211
+ * senderPrivateKeyPem: 可选,传入时为密文附加发送方 ECDSA 签名。
212
+ */
213
+ export function encryptGroupMessage(groupId, epoch, groupSecret, payload, opts) {
214
+ const msgKey = deriveGroupMsgKey(groupSecret, groupId, opts.messageId);
215
+ const plaintext = Buffer.from(JSON.stringify(payload), 'utf-8');
216
+ const aad = {
217
+ group_id: groupId,
218
+ from: opts.fromAid,
219
+ message_id: opts.messageId,
220
+ timestamp: opts.timestamp,
221
+ epoch,
222
+ encryption_mode: MODE_EPOCH_GROUP_KEY,
223
+ suite: SUITE,
224
+ };
225
+ const aadBytes = aadBytesGroup(aad);
226
+ const { ciphertext, tag, nonce } = aesGcmEncrypt(msgKey, plaintext, aadBytes);
227
+ const envelope = {
228
+ type: 'e2ee.group_encrypted',
229
+ version: '1',
230
+ encryption_mode: MODE_EPOCH_GROUP_KEY,
231
+ suite: SUITE,
232
+ epoch,
233
+ nonce: nonce.toString('base64'),
234
+ ciphertext: ciphertext.toString('base64'),
235
+ tag: tag.toString('base64'),
236
+ aad,
237
+ };
238
+ // 发送方签名
239
+ if (opts.senderPrivateKeyPem) {
240
+ const signPayload = Buffer.concat([ciphertext, tag, aadBytes]);
241
+ const sig = ecdsaSign(opts.senderPrivateKeyPem, signPayload);
242
+ envelope.sender_signature = sig.toString('base64');
243
+ // 证书指纹(与 Python/Go/JS SDK 一致)
244
+ if (opts.senderCertPem) {
245
+ envelope.sender_cert_fingerprint = certSha256Fingerprint(opts.senderCertPem);
246
+ }
247
+ }
248
+ return envelope;
249
+ }
250
+ /**
251
+ * 解密群组消息。
252
+ * groupSecrets: {epoch: groupSecretBytes} 映射。
253
+ * requireSignature: 为 true 时(默认),若消息含签名但无证书可验证,或缺少签名,则拒绝。
254
+ */
255
+ export function decryptGroupMessage(message, groupSecrets, senderCertPem, opts) {
256
+ const requireSignature = opts?.requireSignature ?? true;
257
+ const payload = isJsonObject(message.payload) ? message.payload : null;
258
+ if (payload === null)
259
+ return null;
260
+ if (payload.type !== 'e2ee.group_encrypted')
261
+ return null;
262
+ const epoch = payload.epoch;
263
+ if (epoch == null)
264
+ return null;
265
+ const groupSecret = groupSecrets.get(epoch);
266
+ if (!groupSecret)
267
+ return null;
268
+ try {
269
+ // 优先从 AAD 读取 group_id 和 message_id
270
+ const aad = isJsonObject(payload.aad) ? payload.aad : undefined;
271
+ const outerGroupId = message.group_id || '';
272
+ let groupId;
273
+ let messageId;
274
+ let aadFrom = '';
275
+ if (aad) {
276
+ groupId = aad.group_id || outerGroupId;
277
+ messageId = aad.message_id || message.message_id || '';
278
+ aadFrom = aad.from || '';
279
+ // 外层路由字段与 AAD 绑定校验
280
+ if (outerGroupId && groupId !== outerGroupId)
281
+ return null;
282
+ if (aadFrom) {
283
+ const outerFrom = message.from || '';
284
+ const outerSender = message.sender_aid || '';
285
+ if (outerFrom && outerFrom !== aadFrom)
286
+ return null;
287
+ if (outerSender && outerSender !== aadFrom)
288
+ return null;
289
+ }
290
+ }
291
+ else {
292
+ groupId = outerGroupId;
293
+ messageId = message.message_id || '';
294
+ }
295
+ if (!groupId || !messageId)
296
+ return null;
297
+ const msgKey = deriveGroupMsgKey(groupSecret, groupId, messageId);
298
+ const nonce = Buffer.from(payload.nonce, 'base64');
299
+ const ciphertext = Buffer.from(payload.ciphertext, 'base64');
300
+ const tag = Buffer.from(payload.tag, 'base64');
301
+ const aadBytes = aad ? aadBytesGroup(aad) : Buffer.alloc(0);
302
+ const plaintext = aesGcmDecrypt(msgKey, ciphertext, tag, nonce, aadBytes);
303
+ const decoded = JSON.parse(plaintext.toString('utf-8'));
304
+ const result = {
305
+ ...message,
306
+ payload: decoded,
307
+ encrypted: true,
308
+ e2ee: {
309
+ encryption_mode: MODE_EPOCH_GROUP_KEY,
310
+ suite: SUITE,
311
+ epoch,
312
+ sender_verified: false,
313
+ },
314
+ };
315
+ // 发送方签名验证
316
+ const senderSigB64 = payload.sender_signature;
317
+ if (requireSignature) {
318
+ if (!senderSigB64)
319
+ return null;
320
+ if (!senderCertPem)
321
+ return null;
322
+ try {
323
+ const senderPub = pemToCertPublicKey(senderCertPem);
324
+ const sigBytes = Buffer.from(senderSigB64, 'base64');
325
+ const verifyPayload = Buffer.concat([ciphertext, tag, aadBytes]);
326
+ if (!ecdsaVerify(senderPub, sigBytes, verifyPayload))
327
+ return null;
328
+ if (isJsonObject(result.e2ee))
329
+ result.e2ee.sender_verified = true;
330
+ }
331
+ catch {
332
+ return null;
333
+ }
334
+ }
335
+ else if (senderCertPem) {
336
+ // 非零信任模式但提供了证书:有证书时强制验签
337
+ if (!senderSigB64)
338
+ return null;
339
+ try {
340
+ const senderPub = pemToCertPublicKey(senderCertPem);
341
+ const sigBytes = Buffer.from(senderSigB64, 'base64');
342
+ const verifyPayload = Buffer.concat([ciphertext, tag, aadBytes]);
343
+ if (!ecdsaVerify(senderPub, sigBytes, verifyPayload))
344
+ return null;
345
+ if (isJsonObject(result.e2ee))
346
+ result.e2ee.sender_verified = true;
347
+ }
348
+ catch {
349
+ return null;
350
+ }
351
+ }
352
+ return result;
353
+ }
354
+ catch {
355
+ return null;
356
+ }
357
+ }
358
+ // ── Membership Commitment ─────────────────────────────────────
359
+ /**
360
+ * 计算 Membership Commitment。
361
+ * commitment = SHA-256(sort(aids).join("|") + "|" + epoch + "|" + groupId + "|" + SHA256(groupSecret).hex())
362
+ */
363
+ export function computeMembershipCommitment(memberAids, epoch, groupId, groupSecret) {
364
+ const sortedAids = [...memberAids].sort();
365
+ const secretHash = crypto.createHash('sha256').update(groupSecret).digest().toString('hex');
366
+ const data = sortedAids.join('|') + '|' + epoch + '|' + groupId + '|' + secretHash;
367
+ return crypto.createHash('sha256').update(data, 'utf-8').digest().toString('hex');
368
+ }
369
+ /**
370
+ * 验证 Membership Commitment。
371
+ * 1. 重算 commitment 是否匹配
372
+ * 2. 检查 myAid 是否在 memberAids 中
373
+ */
374
+ export function verifyMembershipCommitment(commitment, memberAids, epoch, groupId, myAid, groupSecret) {
375
+ if (!memberAids.includes(myAid))
376
+ return false;
377
+ const expected = computeMembershipCommitment(memberAids, epoch, groupId, groupSecret);
378
+ // 恒定时间比较(长度不同时直接返回 false)
379
+ const a = Buffer.from(expected, 'utf-8');
380
+ const b = Buffer.from(commitment, 'utf-8');
381
+ if (a.length !== b.length)
382
+ return false;
383
+ return crypto.timingSafeEqual(a, b);
384
+ }
385
+ // ── Membership Manifest ───────────────────────────────────────
386
+ /** 构建 Membership Manifest(未签名) */
387
+ export function buildMembershipManifest(groupId, epoch, prevEpoch, memberAids, opts) {
388
+ return {
389
+ manifest_version: 1,
390
+ group_id: groupId,
391
+ epoch,
392
+ prev_epoch: prevEpoch,
393
+ member_aids: [...memberAids].sort(),
394
+ added: [...(opts?.added ?? [])].sort(),
395
+ removed: [...(opts?.removed ?? [])].sort(),
396
+ initiator_aid: opts?.initiatorAid ?? '',
397
+ issued_at: Date.now(),
398
+ };
399
+ }
400
+ /** 序列化 manifest 为签名输入 */
401
+ function manifestSignData(manifest) {
402
+ const fields = [
403
+ String(manifest.manifest_version ?? 1),
404
+ manifest.group_id ?? '',
405
+ String(manifest.epoch ?? 0),
406
+ String(manifest.prev_epoch ?? ''),
407
+ (manifest.member_aids ?? []).join('|'),
408
+ (manifest.added ?? []).join('|'),
409
+ (manifest.removed ?? []).join('|'),
410
+ manifest.initiator_aid ?? '',
411
+ String(manifest.issued_at ?? 0),
412
+ ];
413
+ return Buffer.from(fields.join('\n'), 'utf-8');
414
+ }
415
+ /** 对 Membership Manifest 签名,返回带 signature 字段的新 manifest */
416
+ export function signMembershipManifest(manifest, privateKeyPem) {
417
+ const signData = manifestSignData(manifest);
418
+ const sig = ecdsaSign(privateKeyPem, signData);
419
+ return { ...manifest, signature: sig.toString('base64') };
420
+ }
421
+ /** 验证 Membership Manifest 签名 */
422
+ export function verifyMembershipManifest(manifest, initiatorCertPem) {
423
+ const sigB64 = manifest.signature;
424
+ if (!sigB64)
425
+ return false;
426
+ try {
427
+ const pubKey = pemToCertPublicKey(initiatorCertPem);
428
+ const sigBytes = Buffer.from(sigB64, 'base64');
429
+ const signData = manifestSignData(manifest);
430
+ return ecdsaVerify(pubKey, sigBytes, signData);
431
+ }
432
+ catch {
433
+ return false;
434
+ }
435
+ }
436
+ // ── Group Secret 生命周期管理 ─────────────────────────────────
437
+ /** 存储 group_secret 到 keystore metadata */
438
+ export function storeGroupSecret(keystore, aid, groupId, epoch, groupSecret, commitment, memberAids, epochChain, pendingRotationId = '', epochChainUnverified, epochChainUnverifiedReason) {
439
+ const transitionResult = storeKeyStoreGroupTransition(keystore, aid, groupId, {
440
+ epoch,
441
+ secret: groupSecret.toString('base64'),
442
+ commitment,
443
+ memberAids: [...memberAids].sort(),
444
+ epochChain,
445
+ pendingRotationId,
446
+ epochChainUnverified,
447
+ epochChainUnverifiedReason,
448
+ });
449
+ if (transitionResult !== null)
450
+ return transitionResult;
451
+ throw new Error(`keystore ${keystore.constructor?.name ?? 'unknown'} missing storeGroupSecretTransition method`);
452
+ }
453
+ /** 保存指定 epoch key;低于 current 时写入 old epoch,不覆盖 current。 */
454
+ export function storeGroupSecretEpoch(keystore, aid, groupId, epoch, groupSecret, commitment, memberAids, epochChain, pendingRotationId = '', epochChainUnverified, epochChainUnverifiedReason) {
455
+ const secret = groupSecret.toString('base64');
456
+ const members = [...memberAids].sort();
457
+ const rowResult = storeKeyStoreGroupEpoch(keystore, aid, groupId, {
458
+ epoch,
459
+ secret,
460
+ commitment,
461
+ memberAids: members,
462
+ epochChain,
463
+ pendingRotationId,
464
+ epochChainUnverified,
465
+ epochChainUnverifiedReason,
466
+ });
467
+ if (rowResult !== null)
468
+ return rowResult;
469
+ throw new Error(`keystore ${keystore.constructor?.name ?? 'unknown'} missing storeGroupSecretEpoch method`);
470
+ }
471
+ /** 读取 group_secret */
472
+ export function loadGroupSecret(keystore, aid, groupId, epoch) {
473
+ const entry = loadKeyStoreGroupEpoch(keystore, aid, groupId, epoch) ?? undefined;
474
+ if (!entry)
475
+ return null;
476
+ const secretStr = entry.secret;
477
+ if (!secretStr)
478
+ return null;
479
+ const loaded = {
480
+ epoch: entry.epoch,
481
+ secret: Buffer.from(secretStr, 'base64'),
482
+ commitment: String(entry.commitment ?? ''),
483
+ member_aids: Array.isArray(entry.member_aids) ? entry.member_aids.map((item) => String(item)) : [],
484
+ };
485
+ if (typeof entry.epoch_chain === 'string')
486
+ loaded.epoch_chain = entry.epoch_chain;
487
+ if (typeof entry.pending_rotation_id === 'string')
488
+ loaded.pending_rotation_id = entry.pending_rotation_id;
489
+ if (typeof entry.epoch_chain_unverified === 'boolean')
490
+ loaded.epoch_chain_unverified = entry.epoch_chain_unverified;
491
+ if (typeof entry.epoch_chain_unverified_reason === 'string') {
492
+ loaded.epoch_chain_unverified_reason = entry.epoch_chain_unverified_reason;
493
+ }
494
+ return loaded;
495
+ }
496
+ function assessIncomingEpochChain(keystore, aid, groupId, epoch, commitment, incomingChain, rotationId, rotatorAid, source) {
497
+ const chain = (incomingChain ?? '').trim();
498
+ const rid = rotationId.trim();
499
+ const rotator = rotatorAid.trim();
500
+ if (rid && !chain) {
501
+ console.warn(`[e2ee-group] 拒绝缺少 epoch_chain 的新 rotation key source=${source} group=${groupId} epoch=${epoch} rotation=${rid}`);
502
+ return { ok: false };
503
+ }
504
+ const current = loadGroupSecret(keystore, aid, groupId);
505
+ if (current?.epoch === epoch) {
506
+ const currentChain = current.epoch_chain ?? '';
507
+ const currentPendingRotationId = current.pending_rotation_id ?? '';
508
+ if (chain && currentChain === chain)
509
+ return { ok: true };
510
+ if (rid && chain && currentChain && currentChain !== chain) {
511
+ if (!(currentPendingRotationId && currentPendingRotationId !== rid)) {
512
+ console.warn(`[e2ee-group] 拒绝同 epoch 分叉 chain source=${source} group=${groupId} epoch=${epoch} rotation=${rid}`);
513
+ return { ok: false };
514
+ }
515
+ }
516
+ }
517
+ const prev = loadGroupSecret(keystore, aid, groupId, epoch - 1);
518
+ const prevChain = prev?.epoch_chain ?? '';
519
+ if (!chain)
520
+ return { ok: true, unverified: true, reason: 'missing_epoch_chain' };
521
+ if (!prevChain)
522
+ return { ok: true, unverified: true, reason: 'missing_prev_chain' };
523
+ if (!rotator) {
524
+ if (rid) {
525
+ console.warn(`[e2ee-group] 拒绝缺少 rotator_aid 的新 rotation key source=${source} group=${groupId} epoch=${epoch} rotation=${rid}`);
526
+ return { ok: false };
527
+ }
528
+ return { ok: true, unverified: true, reason: 'missing_rotator_aid' };
529
+ }
530
+ if (!verifyEpochChain(chain, prevChain, epoch, commitment, rotator)) {
531
+ if (rid) {
532
+ console.warn(`[e2ee-group] 拒绝 epoch_chain 验证失败的新 rotation key source=${source} group=${groupId} epoch=${epoch} rotation=${rid}`);
533
+ return { ok: false };
534
+ }
535
+ console.warn(`[e2ee-group] epoch_chain 验证失败,按兼容档接收并标记未验证 source=${source} group=${groupId} epoch=${epoch}`);
536
+ return { ok: true, unverified: true, reason: 'chain_mismatch_legacy' };
537
+ }
538
+ if (!rid)
539
+ return { ok: true, unverified: true, reason: 'missing_rotation_id' };
540
+ return { ok: true, unverified: false };
541
+ }
542
+ /** 加载某群组所有 epoch 的 group_secret */
543
+ export function loadAllGroupSecrets(keystore, aid, groupId) {
544
+ const result = new Map();
545
+ for (const entry of loadKeyStoreGroupEpochs(keystore, aid, groupId)) {
546
+ const secretStr = entry.secret;
547
+ if (secretStr && entry.epoch != null) {
548
+ result.set(entry.epoch, Buffer.from(secretStr, 'base64'));
549
+ }
550
+ }
551
+ return result;
552
+ }
553
+ /** 清理过期的旧 epoch 记录。返回清理数量。 */
554
+ export function cleanupOldEpochs(keystore, aid, groupId, retentionSeconds = OLD_EPOCH_RETENTION_SECONDS) {
555
+ const cutoffMs = Date.now() - retentionSeconds * 1000;
556
+ return cleanupKeyStoreGroupOldEpochs(keystore, aid, groupId, cutoffMs);
557
+ }
558
+ /** 仅回滚指定 rotation 写入的本地 pending epoch key。 */
559
+ export function discardPendingGroupSecret(keystore, aid, groupId, epoch, rotationId) {
560
+ const rid = rotationId.trim();
561
+ if (!rid)
562
+ return false;
563
+ if (typeof keystore.discardPendingGroupSecretState === 'function') {
564
+ return keystore.discardPendingGroupSecretState(aid, groupId, epoch, rid);
565
+ }
566
+ throw new Error(`keystore ${keystore.constructor?.name ?? 'unknown'} missing discardPendingGroupSecretState method`);
567
+ }
568
+ /** 删除群组的所有密钥数据(群组解散时使用) */
569
+ export function deleteGroupSecret(keystore, aid, groupId) {
570
+ deleteKeyStoreGroupState(keystore, aid, groupId);
571
+ }
572
+ // ── GroupReplayGuard ──────────────────────────────────────────
573
+ /** 群组消息防重放守卫 */
574
+ export class GroupReplayGuard {
575
+ _seen = new Map();
576
+ _maxSize;
577
+ constructor(maxSize = 50000) {
578
+ this._maxSize = maxSize;
579
+ }
580
+ /** 检查并记录。返回 true 表示首次(通过),false 表示重放(拒绝)。 */
581
+ checkAndRecord(groupId, senderAid, messageId) {
582
+ const key = `${groupId}:${senderAid}:${messageId}`;
583
+ if (this._seen.has(key))
584
+ return false;
585
+ this._seen.set(key, true);
586
+ this.trim();
587
+ return true;
588
+ }
589
+ /** 仅检查是否已记录 */
590
+ isSeen(groupId, senderAid, messageId) {
591
+ return this._seen.has(`${groupId}:${senderAid}:${messageId}`);
592
+ }
593
+ /** 仅记录,不检查 */
594
+ record(groupId, senderAid, messageId) {
595
+ this._seen.set(`${groupId}:${senderAid}:${messageId}`, true);
596
+ this.trim();
597
+ }
598
+ /** LRU 裁剪(供外部调用) */
599
+ trim() {
600
+ if (this._seen.size > this._maxSize) {
601
+ const trimCount = this._seen.size - Math.floor(this._maxSize * 0.8);
602
+ const iter = this._seen.keys();
603
+ for (let i = 0; i < trimCount; i++) {
604
+ const next = iter.next();
605
+ if (next.done)
606
+ break;
607
+ this._seen.delete(next.value);
608
+ }
609
+ }
610
+ }
611
+ get size() {
612
+ return this._seen.size;
613
+ }
614
+ }
615
+ // ── GroupKeyRequestThrottle ──────────────────────────────────
616
+ /** 群组密钥请求/响应频率限制 */
617
+ export class GroupKeyRequestThrottle {
618
+ _last = new Map();
619
+ _cooldown;
620
+ constructor(cooldown = 30.0) {
621
+ this._cooldown = cooldown;
622
+ }
623
+ /** 检查是否允许操作 */
624
+ allow(key) {
625
+ const now = Date.now() / 1000;
626
+ const last = this._last.get(key);
627
+ if (last != null && (now - last) < this._cooldown)
628
+ return false;
629
+ this._last.set(key, now);
630
+ return true;
631
+ }
632
+ reset(key) {
633
+ this._last.delete(key);
634
+ }
635
+ }
636
+ // ── Group Key 分发与恢复协议 ──────────────────────────────────
637
+ /** 生成 32 字节随机 group_secret */
638
+ export function generateGroupSecret() {
639
+ return crypto.randomBytes(32);
640
+ }
641
+ /** 构建 group key 分发消息 payload */
642
+ export function buildKeyDistribution(groupId, epoch, groupSecret, memberAids, distributedBy, manifest, epochChain) {
643
+ const commitment = computeMembershipCommitment(memberAids, epoch, groupId, groupSecret);
644
+ const result = {
645
+ type: 'e2ee.group_key_distribution',
646
+ group_id: groupId,
647
+ epoch,
648
+ group_secret: groupSecret.toString('base64'),
649
+ commitment,
650
+ member_aids: [...memberAids].sort(),
651
+ distributed_by: distributedBy,
652
+ distributed_at: Date.now(),
653
+ };
654
+ if (manifest) {
655
+ result.manifest = manifest;
656
+ }
657
+ if (epochChain !== undefined) {
658
+ result.epoch_chain = epochChain;
659
+ }
660
+ return result;
661
+ }
662
+ /** 处理收到的 group key 分发消息 */
663
+ export function handleKeyDistribution(message, keystore, aid, initiatorCertPem) {
664
+ const payload = 'group_id' in message
665
+ ? message
666
+ : (isJsonObject(message.payload) ? message.payload : message);
667
+ const groupId = payload.group_id;
668
+ const epoch = payload.epoch;
669
+ const groupSecretB64 = payload.group_secret;
670
+ const commitment = payload.commitment;
671
+ const memberAids = payload.member_aids ?? [];
672
+ if (!groupId || epoch == null || !groupSecretB64 || !commitment)
673
+ return false;
674
+ // 验证 Membership Manifest 签名
675
+ const manifest = isJsonObject(payload.manifest) ? payload.manifest : undefined;
676
+ if (initiatorCertPem) {
677
+ if (!manifest)
678
+ return false;
679
+ if (!verifyMembershipManifest(manifest, initiatorCertPem))
680
+ return false;
681
+ if (manifest.group_id !== groupId || manifest.epoch !== epoch)
682
+ return false;
683
+ const manifestMembers = [...(manifest.member_aids ?? [])].sort();
684
+ const payloadMembers = [...memberAids].sort();
685
+ if (JSON.stringify(manifestMembers) !== JSON.stringify(payloadMembers))
686
+ return false;
687
+ }
688
+ else if (manifest) {
689
+ if (manifest.group_id !== groupId || manifest.epoch !== epoch)
690
+ return false;
691
+ const manifestMembers = [...(manifest.member_aids ?? [])].sort();
692
+ const payloadMembers = [...memberAids].sort();
693
+ if (JSON.stringify(manifestMembers) !== JSON.stringify(payloadMembers))
694
+ return false;
695
+ }
696
+ const groupSecret = Buffer.from(groupSecretB64, 'base64');
697
+ // 验证 commitment
698
+ if (!verifyMembershipCommitment(commitment, memberAids, epoch, groupId, aid, groupSecret)) {
699
+ return false;
700
+ }
701
+ const incomingChain = typeof payload.epoch_chain === 'string' ? payload.epoch_chain : undefined;
702
+ const rotationId = typeof payload.rotation_id === 'string' ? payload.rotation_id : '';
703
+ const chainAssessment = assessIncomingEpochChain(keystore, aid, groupId, epoch, commitment, incomingChain, rotationId, String(payload.distributed_by ?? payload.rotator_aid ?? ''), 'key_distribution');
704
+ if (!chainAssessment.ok)
705
+ return false;
706
+ return storeGroupSecret(keystore, aid, groupId, epoch, groupSecret, commitment, memberAids, incomingChain, rotationId, chainAssessment.unverified, chainAssessment.reason);
707
+ }
708
+ /** 构建密钥请求 payload */
709
+ export function buildKeyRequest(groupId, epoch, requesterAid, requestId) {
710
+ return {
711
+ type: 'e2ee.group_key_request',
712
+ group_id: groupId,
713
+ epoch,
714
+ requester_aid: requesterAid,
715
+ request_id: requestId ?? crypto.randomUUID(),
716
+ requested_at: Date.now(),
717
+ };
718
+ }
719
+ /** 处理收到的密钥请求 */
720
+ export function handleKeyRequest(request, keystore, aid, currentMembers, privateKeyPem) {
721
+ const payload = 'group_id' in request
722
+ ? request
723
+ : (isJsonObject(request.payload) ? request.payload : request);
724
+ const requesterAid = payload.requester_aid;
725
+ const groupId = payload.group_id;
726
+ const epoch = payload.epoch;
727
+ if (!requesterAid || !groupId || epoch == null)
728
+ return null;
729
+ // 验证请求者是群成员
730
+ if (!currentMembers.includes(requesterAid))
731
+ return null;
732
+ // 查本地密钥
733
+ const secretData = loadGroupSecret(keystore, aid, groupId, epoch);
734
+ if (!secretData)
735
+ return null;
736
+ let commitmentStr = secretData.commitment ?? '';
737
+ const memberAids = (secretData.member_aids ?? []).map(String).filter(Boolean).sort();
738
+ const currentMemberAids = currentMembers.map(String).filter(Boolean).sort();
739
+ let responseMemberAids = memberAids.length > 0 ? memberAids : currentMemberAids;
740
+ let includeEpochChain = true;
741
+ if (currentMemberAids.includes(requesterAid) && !responseMemberAids.includes(requesterAid)) {
742
+ responseMemberAids = currentMemberAids;
743
+ commitmentStr = computeMembershipCommitment(responseMemberAids, epoch, groupId, secretData.secret);
744
+ includeEpochChain = false;
745
+ }
746
+ else if (!commitmentStr) {
747
+ commitmentStr = computeMembershipCommitment(responseMemberAids, epoch, groupId, secretData.secret);
748
+ }
749
+ const response = {
750
+ type: 'e2ee.group_key_response',
751
+ group_id: groupId,
752
+ epoch,
753
+ group_secret: secretData.secret.toString('base64'),
754
+ commitment: commitmentStr,
755
+ member_aids: responseMemberAids,
756
+ requester_aid: requesterAid,
757
+ request_id: String(payload.request_id ?? ''),
758
+ responder_aid: aid,
759
+ issued_at: Date.now(),
760
+ };
761
+ if (includeEpochChain && secretData.epoch_chain !== undefined) {
762
+ response.epoch_chain = secretData.epoch_chain;
763
+ }
764
+ return privateKeyPem ? signGroupKeyResponse(response, privateKeyPem) : response;
765
+ }
766
+ /** 处理收到的密钥响应 */
767
+ export function handleKeyResponse(response, keystore, aid, opts) {
768
+ const payload = 'group_id' in response
769
+ ? response
770
+ : (isJsonObject(response.payload) ? response.payload : response);
771
+ const groupId = payload.group_id;
772
+ const epoch = payload.epoch;
773
+ const groupSecretB64 = payload.group_secret;
774
+ const commitment = payload.commitment;
775
+ const memberAids = payload.member_aids ?? [];
776
+ if (!groupId || epoch == null || !groupSecretB64 || !commitment)
777
+ return false;
778
+ const expected = opts?.expectedRequest ?? null;
779
+ if (expected) {
780
+ if (payload.requester_aid !== aid)
781
+ return false;
782
+ const expectedResponder = String(expected._expected_responder_aid ?? '');
783
+ if (expectedResponder && payload.responder_aid !== expectedResponder)
784
+ return false;
785
+ if (payload.request_id !== expected.request_id)
786
+ return false;
787
+ if (payload.group_id !== expected.group_id)
788
+ return false;
789
+ if (Number(payload.epoch ?? 0) !== Number(expected.epoch ?? 0))
790
+ return false;
791
+ }
792
+ const responderAid = String(payload.responder_aid ?? '');
793
+ if (opts?.strict) {
794
+ if (!responderAid || !opts.responderCertPem)
795
+ return false;
796
+ if ((opts.currentMembers?.length ?? 0) > 0 && !opts.currentMembers.includes(responderAid))
797
+ return false;
798
+ if (!verifyGroupKeyResponseSignature(payload, opts.responderCertPem))
799
+ return false;
800
+ }
801
+ else if (opts?.responderCertPem && payload.response_signature) {
802
+ if (!verifyGroupKeyResponseSignature(payload, opts.responderCertPem))
803
+ return false;
804
+ }
805
+ const groupSecret = Buffer.from(groupSecretB64, 'base64');
806
+ if (!verifyMembershipCommitment(commitment, memberAids, epoch, groupId, aid, groupSecret)) {
807
+ return false;
808
+ }
809
+ const incomingChain = typeof payload.epoch_chain === 'string' ? payload.epoch_chain : undefined;
810
+ const rotationId = typeof payload.rotation_id === 'string' ? payload.rotation_id : '';
811
+ const chainAssessment = assessIncomingEpochChain(keystore, aid, groupId, epoch, commitment, incomingChain, rotationId, String(payload.distributed_by ?? payload.rotator_aid ?? payload.responder_aid ?? ''), 'key_response');
812
+ if (!chainAssessment.ok)
813
+ return false;
814
+ return storeGroupSecretEpoch(keystore, aid, groupId, epoch, groupSecret, commitment, memberAids, incomingChain, rotationId, chainAssessment.unverified, chainAssessment.reason);
815
+ }
816
+ // ── GroupE2EEManager 类 ───────────────────────────────────────
817
+ export class GroupE2EEManager {
818
+ _identityFn;
819
+ _keystore;
820
+ _replayGuard;
821
+ _requestThrottle;
822
+ _responseThrottle;
823
+ _senderCertResolver;
824
+ _initiatorCertResolver;
825
+ _pendingKeyRequests = new Map();
826
+ constructor(opts) {
827
+ this._identityFn = opts.identityFn;
828
+ this._keystore = opts.keystore;
829
+ this._replayGuard = new GroupReplayGuard();
830
+ this._requestThrottle = new GroupKeyRequestThrottle(opts.requestCooldown ?? 30);
831
+ this._responseThrottle = new GroupKeyRequestThrottle(opts.responseCooldown ?? 30);
832
+ this._senderCertResolver = opts.senderCertResolver ?? null;
833
+ this._initiatorCertResolver = opts.initiatorCertResolver ?? null;
834
+ }
835
+ // ── 密钥管理 ──────────────────────────────────────────
836
+ /** 用当前身份私钥签名 manifest */
837
+ _signManifest(manifest) {
838
+ const identity = this._identityFn();
839
+ const pkPem = identity.private_key_pem;
840
+ if (!pkPem)
841
+ return manifest;
842
+ return signMembershipManifest(manifest, pkPem);
843
+ }
844
+ /** 创建首个 epoch。返回 {epoch, commitment, distributions: [{to, payload}]} */
845
+ createEpoch(groupId, memberAids) {
846
+ const aid = this._currentAid();
847
+ const gs = generateGroupSecret();
848
+ const epoch = 1;
849
+ const commitment = computeMembershipCommitment(memberAids, epoch, groupId, gs);
850
+ const epochChain = computeEpochChain(null, epoch, commitment, aid);
851
+ storeGroupSecret(this._keystore, aid, groupId, epoch, gs, commitment, memberAids, epochChain);
852
+ const manifest = this._signManifest(buildMembershipManifest(groupId, epoch, null, memberAids, { initiatorAid: aid }));
853
+ const distPayload = buildKeyDistribution(groupId, epoch, gs, memberAids, aid, manifest, epochChain);
854
+ return {
855
+ epoch,
856
+ commitment,
857
+ distributions: memberAids
858
+ .filter(m => m !== aid)
859
+ .map(m => ({ to: m, payload: distPayload })),
860
+ };
861
+ }
862
+ /** 轮换 epoch(踢人/退出后调用) */
863
+ rotateEpoch(groupId, memberAids) {
864
+ const aid = this._currentAid();
865
+ const current = loadGroupSecret(this._keystore, aid, groupId);
866
+ const prevEpoch = current ? Number(current.epoch) : null;
867
+ const newEpoch = (prevEpoch ?? 0) + 1;
868
+ const gs = generateGroupSecret();
869
+ const commitment = computeMembershipCommitment(memberAids, newEpoch, groupId, gs);
870
+ const prevChain = current?.epoch_chain ?? null;
871
+ const epochChain = computeEpochChain(prevChain, newEpoch, commitment, aid);
872
+ const stored = storeGroupSecret(this._keystore, aid, groupId, newEpoch, gs, commitment, memberAids, epochChain);
873
+ if (!stored) {
874
+ throw new Error(`group ${groupId} epoch ${newEpoch} secret already exists or is newer; abort distribution`);
875
+ }
876
+ const manifest = this._signManifest(buildMembershipManifest(groupId, newEpoch, prevEpoch, memberAids, { initiatorAid: aid }));
877
+ const distPayload = buildKeyDistribution(groupId, newEpoch, gs, memberAids, aid, manifest, epochChain);
878
+ return {
879
+ epoch: newEpoch,
880
+ commitment,
881
+ distributions: memberAids
882
+ .filter(m => m !== aid)
883
+ .map(m => ({ to: m, payload: distPayload })),
884
+ };
885
+ }
886
+ /** 指定目标 epoch 号轮换(配合服务端 CAS 使用) */
887
+ rotateEpochTo(groupId, newEpoch, memberAids, opts) {
888
+ const aid = this._currentAid();
889
+ const current = loadGroupSecret(this._keystore, aid, groupId, newEpoch - 1)
890
+ ?? loadGroupSecret(this._keystore, aid, groupId);
891
+ const gs = generateGroupSecret();
892
+ const commitment = computeMembershipCommitment(memberAids, newEpoch, groupId, gs);
893
+ const prevChain = current?.epoch_chain ?? null;
894
+ const epochChain = computeEpochChain(prevChain, newEpoch, commitment, aid);
895
+ const rotationId = opts?.rotationId ?? '';
896
+ const stored = storeGroupSecret(this._keystore, aid, groupId, newEpoch, gs, commitment, memberAids, epochChain, rotationId);
897
+ if (!stored) {
898
+ throw new Error(`group ${groupId} epoch ${newEpoch} secret already exists or is newer; abort distribution`);
899
+ }
900
+ const manifest = this._signManifest(buildMembershipManifest(groupId, newEpoch, newEpoch - 1, memberAids, { initiatorAid: aid }));
901
+ const distPayload = buildKeyDistribution(groupId, newEpoch, gs, memberAids, aid, manifest, epochChain);
902
+ if (rotationId) {
903
+ distPayload.rotation_id = rotationId;
904
+ }
905
+ return {
906
+ epoch: newEpoch,
907
+ commitment,
908
+ distributions: memberAids
909
+ .filter(m => m !== aid)
910
+ .map(m => ({ to: m, payload: distPayload })),
911
+ };
912
+ }
913
+ discardPendingSecret(groupId, epoch, rotationId) {
914
+ return discardPendingGroupSecret(this._keystore, this._currentAid(), groupId, epoch, rotationId);
915
+ }
916
+ /** 加密群消息(含发送方签名)。无密钥时抛 E2EEGroupSecretMissingError。 */
917
+ encrypt(groupId, payload) {
918
+ const aid = this._currentAid();
919
+ const secretData = loadGroupSecret(this._keystore, aid, groupId);
920
+ if (!secretData) {
921
+ throw new E2EEGroupSecretMissingError(`no group secret for ${groupId}`);
922
+ }
923
+ const identity = this._identityFn();
924
+ const senderPkPem = identity ? identity.private_key_pem ?? null : null;
925
+ // TS-017: 签名失败必须阻止发送,不允许发出无签名的群消息
926
+ if (!senderPkPem) {
927
+ throw new E2EEError('sender identity private key unavailable for group message signing');
928
+ }
929
+ const senderCertPem = identity ? identity.cert ?? null : null;
930
+ return encryptGroupMessage(groupId, secretData.epoch, secretData.secret, payload, {
931
+ fromAid: aid,
932
+ messageId: `gm-${crypto.randomUUID()}`,
933
+ timestamp: Date.now(),
934
+ senderPrivateKeyPem: senderPkPem,
935
+ senderCertPem,
936
+ });
937
+ }
938
+ /** 使用指定 epoch 加密群消息。 */
939
+ encryptWithEpoch(groupId, epoch, payload) {
940
+ const aid = this._currentAid();
941
+ const secretData = loadGroupSecret(this._keystore, aid, groupId, epoch);
942
+ if (!secretData) {
943
+ throw new E2EEGroupSecretMissingError(`no group secret for ${groupId} epoch ${epoch}`);
944
+ }
945
+ const identity = this._identityFn();
946
+ const senderPkPem = identity ? identity.private_key_pem ?? null : null;
947
+ if (!senderPkPem) {
948
+ throw new E2EEError('sender identity private key unavailable for group message signing');
949
+ }
950
+ const senderCertPem = identity ? identity.cert ?? null : null;
951
+ return encryptGroupMessage(groupId, secretData.epoch, secretData.secret, payload, {
952
+ fromAid: aid,
953
+ messageId: `gm-${crypto.randomUUID()}`,
954
+ timestamp: Date.now(),
955
+ senderPrivateKeyPem: senderPkPem,
956
+ senderCertPem,
957
+ });
958
+ }
959
+ /** 解密单条群消息。内置防重放 + 发送方验签。 */
960
+ decrypt(message, opts) {
961
+ const payload = isJsonObject(message.payload) ? message.payload : null;
962
+ if (payload === null || payload.type !== 'e2ee.group_encrypted') {
963
+ return message;
964
+ }
965
+ const groupId = message.group_id || '';
966
+ const sender = message.from || message.sender_aid || '';
967
+ // 防重放预检:优先使用 AAD 内 message_id
968
+ const aad = isJsonObject(payload.aad) ? payload.aad : undefined;
969
+ const aadMsgId = aad ? (aad.message_id ?? '') : '';
970
+ const msgId = aadMsgId || message.message_id || '';
971
+ if (!opts?.skipReplay && groupId && sender && msgId) {
972
+ if (this._replayGuard.isSeen(groupId, sender, msgId)) {
973
+ // 返回原消息(不含 e2ee 字段),调用方可通过缺失 e2ee 识别 replay
974
+ return message;
975
+ }
976
+ }
977
+ // 解析发送方证书(零信任:无证书则拒绝)
978
+ let senderCertPem = null;
979
+ if (this._senderCertResolver && sender) {
980
+ senderCertPem = this._senderCertResolver(sender);
981
+ }
982
+ if (!senderCertPem)
983
+ return null;
984
+ const allSecrets = loadAllGroupSecrets(this._keystore, this._currentAid(), groupId);
985
+ if (allSecrets.size === 0)
986
+ return null;
987
+ const result = decryptGroupMessage(message, allSecrets, senderCertPem);
988
+ // 解密成功后记录防重放
989
+ if (result != null) {
990
+ const finalMsgId = aadMsgId || message.message_id || '';
991
+ if (!opts?.skipReplay && groupId && sender && finalMsgId) {
992
+ this._replayGuard.record(groupId, sender, finalMsgId);
993
+ }
994
+ }
995
+ return result;
996
+ }
997
+ /** 批量解密 */
998
+ decryptBatch(messages, opts) {
999
+ return messages.map(m => this.decrypt(m, opts) ?? m);
1000
+ }
1001
+ // ── 密钥协议消息处理 ──────────────────────────────────
1002
+ /**
1003
+ * 处理已解密的 P2P 密钥消息。
1004
+ * 返回 "distribution"/"request"/"response" 表示已成功处理。
1005
+ * 返回 "distribution_rejected"/"response_rejected" 表示被拒绝。
1006
+ * 返回 null 表示不是密钥消息。
1007
+ */
1008
+ handleIncoming(payload) {
1009
+ const msgType = payload.type || '';
1010
+ const aid = this._currentAid();
1011
+ if (msgType === 'e2ee.group_key_distribution') {
1012
+ let initiatorCert = null;
1013
+ const distributedBy = payload.distributed_by || '';
1014
+ if (this._initiatorCertResolver && distributedBy) {
1015
+ initiatorCert = this._initiatorCertResolver(distributedBy);
1016
+ }
1017
+ const ok = handleKeyDistribution(payload, this._keystore, aid, initiatorCert);
1018
+ return ok ? 'distribution' : 'distribution_rejected';
1019
+ }
1020
+ if (msgType === 'e2ee.group_key_response') {
1021
+ const pendingKey = `${String(payload.group_id ?? '')}:${String(payload.epoch ?? '')}:${String(payload.request_id ?? '')}`;
1022
+ const expected = this._pendingKeyRequests.get(pendingKey) ?? null;
1023
+ if (expected === null)
1024
+ return 'response_rejected';
1025
+ const responderAid = String(payload.responder_aid ?? '');
1026
+ const responderCertPem = responderAid && this._initiatorCertResolver
1027
+ ? this._initiatorCertResolver(responderAid)
1028
+ : null;
1029
+ const ok = handleKeyResponse(payload, this._keystore, aid, {
1030
+ expectedRequest: expected,
1031
+ responderCertPem,
1032
+ currentMembers: this.getMemberAids(String(payload.group_id ?? '')),
1033
+ strict: true,
1034
+ });
1035
+ if (ok && expected)
1036
+ this._pendingKeyRequests.delete(pendingKey);
1037
+ return ok ? 'response' : 'response_rejected';
1038
+ }
1039
+ if (msgType === 'e2ee.group_key_request') {
1040
+ return 'request';
1041
+ }
1042
+ return null;
1043
+ }
1044
+ /** 构建恢复请求。返回 {to, payload} 或 null(限流/无目标)。 */
1045
+ buildRecoveryRequest(groupId, epoch, senderAid) {
1046
+ const aid = this._currentAid();
1047
+ if (!this._requestThrottle.allow(`request:${groupId}:${epoch}`)) {
1048
+ return null;
1049
+ }
1050
+ let candidates = [];
1051
+ const secretData = loadGroupSecret(this._keystore, aid, groupId);
1052
+ if (secretData && secretData.member_aids?.length) {
1053
+ candidates = secretData.member_aids.filter(m => m !== aid);
1054
+ }
1055
+ if (candidates.length === 0 && senderAid && senderAid !== aid) {
1056
+ candidates = [senderAid];
1057
+ }
1058
+ if (candidates.length === 0)
1059
+ return null;
1060
+ const payload = buildKeyRequest(groupId, epoch, aid);
1061
+ this.rememberKeyRequest(payload, candidates[0]);
1062
+ return { to: candidates[0], payload };
1063
+ }
1064
+ rememberKeyRequest(payload, expectedResponderAid) {
1065
+ if (payload.type !== 'e2ee.group_key_request')
1066
+ return;
1067
+ const requestId = String(payload.request_id ?? '');
1068
+ if (!requestId)
1069
+ return;
1070
+ this._pendingKeyRequests.set(`${String(payload.group_id ?? '')}:${String(payload.epoch ?? '')}:${requestId}`, expectedResponderAid ? { ...payload, _expected_responder_aid: expectedResponderAid } : { ...payload });
1071
+ }
1072
+ /** 处理密钥请求。返回响应 payload(受频率限制 + 成员资格验证)。 */
1073
+ handleKeyRequestMsg(requestPayload, members) {
1074
+ const requester = requestPayload.requester_aid || '';
1075
+ const groupId = requestPayload.group_id || '';
1076
+ if (!requester || !groupId)
1077
+ return null;
1078
+ // 成员资格验证
1079
+ if (!members.includes(requester))
1080
+ return null;
1081
+ if (!this._responseThrottle.allow(`response:${groupId}:${requester}`)) {
1082
+ return null;
1083
+ }
1084
+ const identity = this._identityFn();
1085
+ const privateKeyPem = identity.private_key_pem;
1086
+ if (!privateKeyPem)
1087
+ return null;
1088
+ return handleKeyRequest(requestPayload, this._keystore, this._currentAid(), members, privateKeyPem);
1089
+ }
1090
+ // ── 状态查询 ──────────────────────────────────────────
1091
+ /** 检查是否有群组密钥 */
1092
+ hasSecret(groupId) {
1093
+ return loadGroupSecret(this._keystore, this._currentAid(), groupId) != null;
1094
+ }
1095
+ /** 获取当前 epoch */
1096
+ currentEpoch(groupId) {
1097
+ const s = loadGroupSecret(this._keystore, this._currentAid(), groupId);
1098
+ return s ? s.epoch : null;
1099
+ }
1100
+ /** 获取群组成员 AID 列表 */
1101
+ getMemberAids(groupId) {
1102
+ const s = loadGroupSecret(this._keystore, this._currentAid(), groupId);
1103
+ return s ? (s.member_aids ?? []) : [];
1104
+ }
1105
+ /** 加载群组密钥数据 */
1106
+ loadSecret(groupId, epoch) {
1107
+ return loadGroupSecret(this._keystore, this._currentAid(), groupId, epoch);
1108
+ }
1109
+ /** 清理过期的旧 epoch */
1110
+ cleanup(groupId, retentionSeconds = OLD_EPOCH_RETENTION_SECONDS) {
1111
+ cleanupOldEpochs(this._keystore, this._currentAid(), groupId, retentionSeconds);
1112
+ }
1113
+ /** 清理过期缓存(replay guard 等),供外部定时调用 */
1114
+ cleanExpiredCaches() {
1115
+ this._replayGuard.trim();
1116
+ }
1117
+ /** 删除群组的所有本地状态(群组解散时使用) */
1118
+ removeGroup(groupId) {
1119
+ try {
1120
+ deleteGroupSecret(this._keystore, this._currentAid(), groupId);
1121
+ }
1122
+ catch {
1123
+ // keystore 不支持 delete 时忽略(降级方案已在 deleteKeyStoreGroupState 中处理)
1124
+ }
1125
+ }
1126
+ _currentAid() {
1127
+ const identity = this._identityFn();
1128
+ const aid = identity.aid;
1129
+ if (!aid)
1130
+ throw new E2EEError('AID unavailable');
1131
+ return String(aid);
1132
+ }
1133
+ }
1134
+ //# sourceMappingURL=e2ee-group.js.map