@agentunion/fastaun 0.5.4 → 0.5.6
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 +126 -90
- package/_packed_docs/AUN/347/246/273/347/272/277/346/216/250/351/200/201/346/216/245/345/205/245/344/270/216/346/274/224/347/244/272/346/214/207/345/215/227.md +290 -0
- package/_packed_docs/AUN/347/246/273/347/272/277/346/216/250/351/200/201/346/234/215/345/212/241/346/236/266/346/236/204/344/270/216/350/257/246/347/273/206/345/256/236/347/216/260/350/256/241/345/210/222-codex.md +994 -0
- package/_packed_docs/AUN/347/246/273/347/272/277/346/216/250/351/200/201/346/234/215/345/212/241/350/277/220/347/273/264/344/270/216/345/217/221/345/270/203/346/214/207/345/215/227.md +144 -0
- package/_packed_docs/CHANGELOG.md +126 -90
- package/_packed_docs/INDEX.md +127 -69
- package/_packed_docs/KITE_DOCS_GUIDE.md +63 -29
- package/_packed_docs/audit/AUN/346/234/215/345/212/241Go/345/214/226/351/207/215/346/236/204/347/262/276/347/273/206/345/214/226/346/272/220/347/240/201/345/256/241/346/237/245-20260718.md +366 -0
- package/_packed_docs/aun/345/205/254/347/275/221/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +227 -0
- package/_packed_docs/aun/345/210/206/345/270/203/345/274/217/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +1004 -0
- package/_packed_docs/aun/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +999 -0
- package/_packed_docs/cli/CLI/346/211/213/345/206/214.md +6 -4
- package/_packed_docs/protocol/06-/346/234/215/345/212/241/345/215/217/350/256/256.md +58 -20
- package/_packed_docs/protocol/10-Group-/345/255/220/345/215/217/350/256/256.md +219 -247
- package/_packed_docs/protocol/12-Stream-/345/255/220/345/215/217/350/256/256.md +14 -14
- package/_packed_docs/protocol/13-Agent/350/241/214/344/270/272/350/247/204/350/214/203.md +3 -3
- package/_packed_docs/protocol/15-/347/246/273/347/272/277/346/216/250/351/200/201/351/200/232/347/237/245/345/215/217/350/256/256.md +165 -421
- package/_packed_docs/protocol/README.md +1 -0
- package/_packed_docs/protocol/aun-docs-guide.md +7 -4
- package/_packed_docs/protocol/index.md +25 -19
- package/_packed_docs/sdk/02-WebSocket/345/215/217/350/256/256.md +62 -17
- package/_packed_docs/sdk/03-/346/240/270/345/277/203/346/246/202/345/277/265.md +22 -0
- package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +67 -78
- package/_packed_docs/sdk/05-E2EE/345/212/240/345/257/206/351/200/232/344/277/241.md +6 -2
- package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +50 -14
- package/_packed_docs/sdk/08-/346/234/200/344/275/263/345/256/236/350/267/265.md +18 -5
- package/_packed_docs/sdk/09-group-rpc-manual.md +312 -282
- package/_packed_docs/sdk/09-message-rpc-manual.md +142 -103
- package/_packed_docs/sdk/09-payload-reference.md +1 -1
- package/_packed_docs/sdk/09-stream-rpc-manual.md +8 -8
- package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +26 -17
- package/_packed_docs/sdk/INDEX.md +42 -34
- package/_packed_docs/sdk/README.md +7 -6
- package/_packed_docs//346/266/210/346/201/257/345/220/214/346/255/245/344/270/216/345/216/206/345/217/262/346/213/211/345/217/226/344/273/243/347/240/201/345/256/241/346/237/245/351/227/256/351/242/230/346/270/205/345/215/225.md +229 -0
- package/_packed_docs//346/266/210/346/201/257/345/220/214/346/255/245/344/270/216/345/216/206/345/217/262/346/213/211/345/217/226/346/224/271/351/200/240/346/226/271/346/241/210.md +750 -0
- package/dist/agent-md.js +8 -2
- package/dist/agent-md.js.map +1 -1
- package/dist/aid-store.d.ts +2 -0
- package/dist/aid-store.js +43 -7
- package/dist/aid-store.js.map +1 -1
- package/dist/auth.d.ts +0 -2
- package/dist/auth.js +97 -50
- package/dist/auth.js.map +1 -1
- package/dist/client/delivery.d.ts +70 -7
- package/dist/client/delivery.js +1332 -236
- package/dist/client/delivery.js.map +1 -1
- package/dist/client/group-state.js +25 -22
- package/dist/client/group-state.js.map +1 -1
- package/dist/client/lifecycle.js +46 -3
- package/dist/client/lifecycle.js.map +1 -1
- package/dist/client/mention-mode.d.ts +6 -0
- package/dist/client/mention-mode.js +184 -0
- package/dist/client/mention-mode.js.map +1 -0
- package/dist/client/peers.d.ts +1 -1
- package/dist/client/peers.js +26 -3
- package/dist/client/peers.js.map +1 -1
- package/dist/client/rpc-pipeline.d.ts +20 -1
- package/dist/client/rpc-pipeline.js +332 -73
- package/dist/client/rpc-pipeline.js.map +1 -1
- package/dist/client/v2-e2ee.d.ts +20 -1
- package/dist/client/v2-e2ee.js +625 -105
- package/dist/client/v2-e2ee.js.map +1 -1
- package/dist/client.d.ts +17 -5
- package/dist/client.js +332 -167
- package/dist/client.js.map +1 -1
- package/dist/errors.js +4 -1
- package/dist/errors.js.map +1 -1
- package/dist/facades.d.ts +3 -0
- package/dist/facades.js +109 -53
- package/dist/facades.js.map +1 -1
- package/dist/group-index.d.ts +5 -0
- package/dist/group-index.js +65 -30
- package/dist/group-index.js.map +1 -1
- package/dist/keystore/aid-db.d.ts +12 -0
- package/dist/keystore/aid-db.js +68 -4
- package/dist/keystore/aid-db.js.map +1 -1
- package/dist/keystore/index.d.ts +12 -0
- package/dist/keystore/local-identity-store.d.ts +10 -0
- package/dist/keystore/local-identity-store.js +8 -2
- package/dist/keystore/local-identity-store.js.map +1 -1
- package/dist/keystore/local-token-store.d.ts +10 -0
- package/dist/keystore/local-token-store.js +8 -2
- package/dist/keystore/local-token-store.js.map +1 -1
- package/dist/register-flow.d.ts +0 -1
- package/dist/register-flow.js +96 -38
- package/dist/register-flow.js.map +1 -1
- package/dist/secret-store/file-store.d.ts +3 -14
- package/dist/secret-store/file-store.js +7 -98
- package/dist/secret-store/file-store.js.map +1 -1
- package/dist/secret-store/index.d.ts +1 -4
- package/dist/secret-store/index.js +2 -2
- package/dist/secret-store/index.js.map +1 -1
- package/dist/seq-tracker.d.ts +29 -9
- package/dist/seq-tracker.js +233 -64
- package/dist/seq-tracker.js.map +1 -1
- package/dist/storage/vfs.d.ts +1 -0
- package/dist/storage/vfs.js +28 -4
- package/dist/storage/vfs.js.map +1 -1
- package/dist/tools/cross-sdk-agent.js +360 -8
- package/dist/tools/cross-sdk-agent.js.map +1 -1
- package/dist/transport.d.ts +19 -0
- package/dist/transport.js +197 -14
- package/dist/transport.js.map +1 -1
- package/dist/v2/e2ee/decrypt.js +26 -23
- package/dist/v2/e2ee/decrypt.js.map +1 -1
- package/dist/v2/session/session.js +3 -4
- package/dist/v2/session/session.js.map +1 -1
- package/dist/v2/state/commitment.js +1 -2
- package/dist/v2/state/commitment.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +5 -4
- package/dist/e2ee-group.d.ts +0 -249
- package/dist/e2ee-group.js +0 -1567
- package/dist/e2ee-group.js.map +0 -1
- package/dist/group-resources.d.ts +0 -31
- package/dist/group-resources.js +0 -84
- package/dist/group-resources.js.map +0 -1
- package/dist/keystore/file.d.ts +0 -109
- package/dist/keystore/file.js +0 -772
- package/dist/keystore/file.js.map +0 -1
- package/dist/keystore/sqlite-backup.d.ts +0 -44
- package/dist/keystore/sqlite-backup.js +0 -382
- package/dist/keystore/sqlite-backup.js.map +0 -1
- package/dist/namespaces/auth.d.ts +0 -118
- package/dist/namespaces/auth.js +0 -968
- package/dist/namespaces/auth.js.map +0 -1
- package/dist/namespaces/custody.d.ts +0 -48
- package/dist/namespaces/custody.js +0 -313
- package/dist/namespaces/custody.js.map +0 -1
- package/dist/namespaces/meta.d.ts +0 -76
- package/dist/namespaces/meta.js +0 -490
- package/dist/namespaces/meta.js.map +0 -1
package/dist/e2ee-group.js
DELETED
|
@@ -1,1567 +0,0 @@
|
|
|
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
|
-
const _noopLogger = { error: () => { }, warn: () => { }, info: () => { }, debug: () => { } };
|
|
11
|
-
// ── 辅助:证书 SHA-256 指纹 ──────────────────────────────────────
|
|
12
|
-
/** PEM 证书 → sha256:{hex} 指纹(与 Python/Go/JS SDK 一致) */
|
|
13
|
-
function certSha256Fingerprint(certPem) {
|
|
14
|
-
const pem = typeof certPem === 'string' ? certPem : certPem.toString('utf-8');
|
|
15
|
-
const b64 = pem.replace(/-----[^-]+-----/g, '').replace(/\s+/g, '');
|
|
16
|
-
const der = Buffer.from(b64, 'base64');
|
|
17
|
-
return `sha256:${crypto.createHash('sha256').update(der).digest('hex')}`;
|
|
18
|
-
}
|
|
19
|
-
// ── 常量 ───────────────────────────────────────────────────────
|
|
20
|
-
const SUITE = 'P256_HKDF_SHA256_AES_256_GCM';
|
|
21
|
-
const MODE_EPOCH_GROUP_KEY = 'epoch_group_key';
|
|
22
|
-
/** 群组消息 AAD 字段 */
|
|
23
|
-
const AAD_FIELDS_GROUP = [
|
|
24
|
-
'group_id', 'from', 'message_id', 'timestamp',
|
|
25
|
-
'epoch', 'encryption_mode', 'suite',
|
|
26
|
-
];
|
|
27
|
-
/** 群组消息 AAD 匹配字段 */
|
|
28
|
-
const AAD_MATCH_FIELDS_GROUP = [
|
|
29
|
-
'group_id', 'from', 'message_id',
|
|
30
|
-
'epoch', 'encryption_mode', 'suite',
|
|
31
|
-
];
|
|
32
|
-
const AAD_OPTIONAL_FIELDS = [
|
|
33
|
-
'payload_type', 'protected_headers', 'context_type', 'context_id',
|
|
34
|
-
];
|
|
35
|
-
const METADATA_AUTH_FIELD = '_auth';
|
|
36
|
-
const METADATA_AUTH_ALG = 'HMAC-SHA256';
|
|
37
|
-
const METADATA_KEY_DOMAIN = Buffer.from('aun-envelope-metadata-key-v1', 'utf-8');
|
|
38
|
-
const PROTECTED_HEADERS_DOMAIN = Buffer.from('aun-protected-headers-v1', 'utf-8');
|
|
39
|
-
const PROTECTED_CONTEXT_DOMAIN = Buffer.from('aun-protected-context-v1', 'utf-8');
|
|
40
|
-
/** 旧 epoch 默认保留 7 天 */
|
|
41
|
-
const OLD_EPOCH_RETENTION_SECONDS = 7 * 24 * 3600;
|
|
42
|
-
// ── ECIES (P-256 ECDH + HKDF-SHA256 + AES-256-GCM) ──────────
|
|
43
|
-
const ECIES_HKDF_INFO = Buffer.from('aun-epoch-key-ecies', 'utf-8');
|
|
44
|
-
/**
|
|
45
|
-
* ECIES 加密:P-256 ECDH + HKDF-SHA256 + AES-256-GCM。
|
|
46
|
-
* @param peerPubkeyBytes 65 字节未压缩 P-256 公钥 (0x04 开头)
|
|
47
|
-
* @param plaintext 待加密明文
|
|
48
|
-
* @returns ephemeral_pubkey(65B) || iv(12B) || ciphertext || tag(16B)
|
|
49
|
-
*/
|
|
50
|
-
export function eciesEncrypt(peerPubkeyBytes, plaintext) {
|
|
51
|
-
// 生成临时密钥对
|
|
52
|
-
const ephemeral = crypto.createECDH('prime256v1');
|
|
53
|
-
ephemeral.generateKeys();
|
|
54
|
-
const ephemeralPubBytes = ephemeral.getPublicKey(); // 65 字节未压缩
|
|
55
|
-
// ECDH 共享密钥
|
|
56
|
-
const shared = ephemeral.computeSecret(peerPubkeyBytes);
|
|
57
|
-
// HKDF 派生 32 字节 AES 密钥
|
|
58
|
-
const derived = crypto.hkdfSync('sha256', shared, Buffer.alloc(0), ECIES_HKDF_INFO, 32);
|
|
59
|
-
const aesKey = Buffer.from(derived);
|
|
60
|
-
// AES-256-GCM 加密
|
|
61
|
-
const iv = crypto.randomBytes(12);
|
|
62
|
-
const cipher = crypto.createCipheriv('aes-256-gcm', aesKey, iv);
|
|
63
|
-
const encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
64
|
-
const tag = cipher.getAuthTag(); // 16 字节
|
|
65
|
-
return Buffer.concat([ephemeralPubBytes, iv, encrypted, tag]);
|
|
66
|
-
}
|
|
67
|
-
/**
|
|
68
|
-
* ECIES 解密:对应 eciesEncrypt。
|
|
69
|
-
* @param privateKeyPem 自己的 PEM 格式 EC 私钥
|
|
70
|
-
* @param ciphertext ephemeral_pubkey(65B) || iv(12B) || encrypted || tag(16B)
|
|
71
|
-
* @returns 解密后的明文
|
|
72
|
-
*/
|
|
73
|
-
export function eciesDecrypt(privateKeyPem, ciphertext) {
|
|
74
|
-
if (ciphertext.length < 65 + 12 + 16) {
|
|
75
|
-
throw new E2EEError('ECIES ciphertext too short');
|
|
76
|
-
}
|
|
77
|
-
const ephemeralPubBytes = ciphertext.subarray(0, 65);
|
|
78
|
-
const iv = ciphertext.subarray(65, 77);
|
|
79
|
-
const encryptedWithTag = ciphertext.subarray(77);
|
|
80
|
-
const tag = encryptedWithTag.subarray(encryptedWithTag.length - 16);
|
|
81
|
-
const encrypted = encryptedWithTag.subarray(0, encryptedWithTag.length - 16);
|
|
82
|
-
// 从 PEM 私钥创建 ECDH 并计算共享密钥
|
|
83
|
-
const privKeyObj = crypto.createPrivateKey(privateKeyPem);
|
|
84
|
-
const ecdh = crypto.createECDH('prime256v1');
|
|
85
|
-
// 从 JWK 提取私钥 d 值设置到 ECDH
|
|
86
|
-
const jwk = privKeyObj.export({ format: 'jwk' });
|
|
87
|
-
ecdh.setPrivateKey(Buffer.from(jwk.d, 'base64url'));
|
|
88
|
-
const shared = ecdh.computeSecret(ephemeralPubBytes);
|
|
89
|
-
// HKDF 派生 AES 密钥
|
|
90
|
-
const derived = crypto.hkdfSync('sha256', shared, Buffer.alloc(0), ECIES_HKDF_INFO, 32);
|
|
91
|
-
const aesKey = Buffer.from(derived);
|
|
92
|
-
// AES-256-GCM 解密
|
|
93
|
-
const decipher = crypto.createDecipheriv('aes-256-gcm', aesKey, iv);
|
|
94
|
-
decipher.setAuthTag(tag);
|
|
95
|
-
return Buffer.concat([decipher.update(encrypted), decipher.final()]);
|
|
96
|
-
}
|
|
97
|
-
// ── Epoch Transcript Chain ────────────────────────────────────
|
|
98
|
-
const EPOCH_CHAIN_GENESIS_PREFIX = Buffer.from('aun-epoch-chain:genesis', 'utf-8');
|
|
99
|
-
/**
|
|
100
|
-
* 计算 Epoch Transcript Chain。
|
|
101
|
-
* genesis(prev_chain=null)使用固定前缀;后续 epoch 使用上一个 chain 的字节。
|
|
102
|
-
*/
|
|
103
|
-
export function computeEpochChain(prevChain, epoch, commitment, rotatorAid) {
|
|
104
|
-
const prefix = prevChain === null
|
|
105
|
-
? EPOCH_CHAIN_GENESIS_PREFIX
|
|
106
|
-
: Buffer.from(prevChain, 'hex');
|
|
107
|
-
const epochBuf = Buffer.alloc(4);
|
|
108
|
-
epochBuf.writeUInt32BE(epoch, 0);
|
|
109
|
-
const data = Buffer.concat([
|
|
110
|
-
prefix,
|
|
111
|
-
epochBuf,
|
|
112
|
-
Buffer.from(commitment, 'utf-8'),
|
|
113
|
-
Buffer.from(rotatorAid, 'utf-8'),
|
|
114
|
-
]);
|
|
115
|
-
return crypto.createHash('sha256').update(data).digest('hex');
|
|
116
|
-
}
|
|
117
|
-
/**
|
|
118
|
-
* 验证 Epoch Transcript Chain(常量时间比较)。
|
|
119
|
-
*/
|
|
120
|
-
export function verifyEpochChain(epochChain, prevChain, epoch, commitment, rotatorAid) {
|
|
121
|
-
const expected = computeEpochChain(prevChain, epoch, commitment, rotatorAid);
|
|
122
|
-
const a = Buffer.from(expected, 'hex');
|
|
123
|
-
const b = Buffer.from(epochChain, 'hex');
|
|
124
|
-
if (a.length !== b.length)
|
|
125
|
-
return false;
|
|
126
|
-
return crypto.timingSafeEqual(a, b);
|
|
127
|
-
}
|
|
128
|
-
function loadKeyStoreGroupEpoch(keystore, aid, groupId, epoch) {
|
|
129
|
-
if (typeof keystore.loadGroupSecretEpoch === 'function') {
|
|
130
|
-
return keystore.loadGroupSecretEpoch(aid, groupId, epoch);
|
|
131
|
-
}
|
|
132
|
-
throw new Error(`keystore ${keystore.constructor?.name ?? 'unknown'} missing loadGroupSecretEpoch method`);
|
|
133
|
-
}
|
|
134
|
-
function loadKeyStoreGroupEpochs(keystore, aid, groupId) {
|
|
135
|
-
if (typeof keystore.loadGroupSecretEpochs === 'function') {
|
|
136
|
-
return keystore.loadGroupSecretEpochs(aid, groupId);
|
|
137
|
-
}
|
|
138
|
-
throw new Error(`keystore ${keystore.constructor?.name ?? 'unknown'} missing loadGroupSecretEpochs method`);
|
|
139
|
-
}
|
|
140
|
-
function storeKeyStoreGroupTransition(keystore, aid, groupId, opts) {
|
|
141
|
-
if (typeof keystore.storeGroupSecretTransition !== 'function')
|
|
142
|
-
return null;
|
|
143
|
-
return keystore.storeGroupSecretTransition(aid, groupId, {
|
|
144
|
-
...opts,
|
|
145
|
-
oldEpochRetentionMs: OLD_EPOCH_RETENTION_SECONDS * 1000,
|
|
146
|
-
});
|
|
147
|
-
}
|
|
148
|
-
function storeKeyStoreGroupEpoch(keystore, aid, groupId, opts) {
|
|
149
|
-
if (typeof keystore.storeGroupSecretEpoch !== 'function')
|
|
150
|
-
return null;
|
|
151
|
-
return keystore.storeGroupSecretEpoch(aid, groupId, {
|
|
152
|
-
...opts,
|
|
153
|
-
oldEpochRetentionMs: OLD_EPOCH_RETENTION_SECONDS * 1000,
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
function cleanupKeyStoreGroupOldEpochs(keystore, aid, groupId, cutoffMs) {
|
|
157
|
-
if (typeof keystore.cleanupGroupOldEpochsState === 'function') {
|
|
158
|
-
return keystore.cleanupGroupOldEpochsState(aid, groupId, cutoffMs);
|
|
159
|
-
}
|
|
160
|
-
throw new Error(`keystore ${keystore.constructor?.name ?? 'unknown'} missing cleanupGroupOldEpochsState method`);
|
|
161
|
-
}
|
|
162
|
-
function deleteKeyStoreGroupState(keystore, aid, groupId) {
|
|
163
|
-
if (typeof keystore.deleteGroupSecretState === 'function') {
|
|
164
|
-
keystore.deleteGroupSecretState(aid, groupId);
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
throw new Error(`keystore ${keystore.constructor?.name ?? 'unknown'} missing deleteGroupSecretState method`);
|
|
168
|
-
}
|
|
169
|
-
// ── 内部工具函数 ───────────────────────────────────────────────
|
|
170
|
-
/** HKDF-SHA256 派生密钥 */
|
|
171
|
-
function hkdfDeriveSync(ikm, info, length) {
|
|
172
|
-
const derived = crypto.hkdfSync('sha256', ikm, Buffer.alloc(0), info, length);
|
|
173
|
-
return Buffer.from(derived);
|
|
174
|
-
}
|
|
175
|
-
/** AES-256-GCM 加密 */
|
|
176
|
-
function aesGcmEncrypt(key, plaintext, aad) {
|
|
177
|
-
const nonce = crypto.randomBytes(12);
|
|
178
|
-
const cipher = crypto.createCipheriv('aes-256-gcm', key, nonce);
|
|
179
|
-
cipher.setAAD(aad);
|
|
180
|
-
const encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
181
|
-
const tag = cipher.getAuthTag();
|
|
182
|
-
return { ciphertext: encrypted, tag, nonce };
|
|
183
|
-
}
|
|
184
|
-
/** AES-256-GCM 解密 */
|
|
185
|
-
function aesGcmDecrypt(key, ciphertext, tag, nonce, aad) {
|
|
186
|
-
const decipher = crypto.createDecipheriv('aes-256-gcm', key, nonce);
|
|
187
|
-
decipher.setAuthTag(tag);
|
|
188
|
-
decipher.setAAD(aad);
|
|
189
|
-
return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
190
|
-
}
|
|
191
|
-
/** ECDSA-SHA256 签名 */
|
|
192
|
-
function ecdsaSign(privateKeyPem, data) {
|
|
193
|
-
const signer = crypto.createSign('SHA256');
|
|
194
|
-
signer.update(data);
|
|
195
|
-
signer.end();
|
|
196
|
-
return signer.sign(privateKeyPem);
|
|
197
|
-
}
|
|
198
|
-
/** ECDSA-SHA256 验签 */
|
|
199
|
-
function ecdsaVerify(publicKey, signature, data) {
|
|
200
|
-
const verifier = crypto.createVerify('SHA256');
|
|
201
|
-
verifier.update(data);
|
|
202
|
-
verifier.end();
|
|
203
|
-
return verifier.verify(publicKey, signature);
|
|
204
|
-
}
|
|
205
|
-
function groupKeyResponseSignData(payload) {
|
|
206
|
-
const fields = [
|
|
207
|
-
String(payload.response_version ?? 1),
|
|
208
|
-
String(payload.group_id ?? ''),
|
|
209
|
-
String(payload.epoch ?? 0),
|
|
210
|
-
String(payload.requester_aid ?? ''),
|
|
211
|
-
String(payload.request_id ?? ''),
|
|
212
|
-
String(payload.responder_aid ?? ''),
|
|
213
|
-
String(payload.commitment ?? ''),
|
|
214
|
-
[...(payload.member_aids ?? [])].sort().join('|'),
|
|
215
|
-
String(payload.issued_at ?? 0),
|
|
216
|
-
];
|
|
217
|
-
return Buffer.from(fields.join('\n'), 'utf-8');
|
|
218
|
-
}
|
|
219
|
-
export function signGroupKeyResponse(payload, privateKeyPem) {
|
|
220
|
-
const signed = {
|
|
221
|
-
...payload,
|
|
222
|
-
response_version: payload.response_version ?? 1,
|
|
223
|
-
issued_at: payload.issued_at ?? Date.now(),
|
|
224
|
-
};
|
|
225
|
-
signed.response_signature = ecdsaSign(privateKeyPem, groupKeyResponseSignData(signed)).toString('base64');
|
|
226
|
-
return signed;
|
|
227
|
-
}
|
|
228
|
-
export function verifyGroupKeyResponseSignature(payload, responderCertPem) {
|
|
229
|
-
const sigB64 = payload.response_signature;
|
|
230
|
-
if (!sigB64)
|
|
231
|
-
return false;
|
|
232
|
-
try {
|
|
233
|
-
return ecdsaVerify(pemToCertPublicKey(responderCertPem), Buffer.from(sigB64, 'base64'), groupKeyResponseSignData(payload));
|
|
234
|
-
}
|
|
235
|
-
catch {
|
|
236
|
-
return false;
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
/** PEM 证书 → 公钥 KeyObject */
|
|
240
|
-
function pemToCertPublicKey(certPem) {
|
|
241
|
-
const pem = typeof certPem === 'string' ? certPem : certPem.toString('utf-8');
|
|
242
|
-
const x509 = new crypto.X509Certificate(pem);
|
|
243
|
-
return x509.publicKey;
|
|
244
|
-
}
|
|
245
|
-
// ── 群组 AAD 工具 ─────────────────────────────────────────────
|
|
246
|
-
function canonicalStringify(value) {
|
|
247
|
-
if (value === null || value === undefined)
|
|
248
|
-
return 'null';
|
|
249
|
-
if (Array.isArray(value)) {
|
|
250
|
-
return `[${value.map(item => canonicalStringify(item)).join(',')}]`;
|
|
251
|
-
}
|
|
252
|
-
if (typeof value === 'object') {
|
|
253
|
-
const record = value;
|
|
254
|
-
const pairs = Object.keys(record)
|
|
255
|
-
.sort()
|
|
256
|
-
.map(key => `${JSON.stringify(key)}:${canonicalStringify(record[key])}`);
|
|
257
|
-
return `{${pairs.join(',')}}`;
|
|
258
|
-
}
|
|
259
|
-
return JSON.stringify(value) ?? 'null';
|
|
260
|
-
}
|
|
261
|
-
function hasOwn(obj, key) {
|
|
262
|
-
return Object.prototype.hasOwnProperty.call(obj, key);
|
|
263
|
-
}
|
|
264
|
-
function normalizeProtectedHeaderKey(key) {
|
|
265
|
-
const value = String(key ?? '').trim().toLowerCase();
|
|
266
|
-
if (!value || !/^[a-z0-9_-]+$/.test(value)) {
|
|
267
|
-
throw new E2EEError('protected header key must match [a-z0-9_-]+');
|
|
268
|
-
}
|
|
269
|
-
if (value === METADATA_AUTH_FIELD) {
|
|
270
|
-
throw new E2EEError('protected header key is reserved');
|
|
271
|
-
}
|
|
272
|
-
return value;
|
|
273
|
-
}
|
|
274
|
-
function normalizeProtectedHeaders(headers) {
|
|
275
|
-
if (headers == null)
|
|
276
|
-
return {};
|
|
277
|
-
const toObject = headers.toObject;
|
|
278
|
-
const raw = typeof toObject === 'function' ? toObject.call(headers) : headers;
|
|
279
|
-
if (typeof raw !== 'object' || Array.isArray(raw)) {
|
|
280
|
-
throw new E2EEError('protected_headers must be an object');
|
|
281
|
-
}
|
|
282
|
-
const result = {};
|
|
283
|
-
for (const [key, value] of Object.entries(raw)) {
|
|
284
|
-
result[normalizeProtectedHeaderKey(key)] = value == null ? '' : String(value);
|
|
285
|
-
}
|
|
286
|
-
return result;
|
|
287
|
-
}
|
|
288
|
-
function copyOptionalEnvelopeMetadata(envelope, messageKey, opts) {
|
|
289
|
-
const payloadType = String(opts?.payloadType ?? '').trim();
|
|
290
|
-
const protectedHeaders = normalizeProtectedHeaders(opts?.protectedHeaders);
|
|
291
|
-
if (payloadType) {
|
|
292
|
-
protectedHeaders.payload_type = payloadType;
|
|
293
|
-
}
|
|
294
|
-
if (Object.keys(protectedHeaders).length > 0) {
|
|
295
|
-
envelope.protected_headers = withMetadataAuth(protectedHeaders, messageKey, PROTECTED_HEADERS_DOMAIN);
|
|
296
|
-
}
|
|
297
|
-
const contextMetadata = normalizeContextMetadata(opts?.context);
|
|
298
|
-
if (Object.keys(contextMetadata).length > 0) {
|
|
299
|
-
envelope.context = withMetadataAuth(contextMetadata, messageKey, PROTECTED_CONTEXT_DOMAIN);
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
function metadataBody(metadata) {
|
|
303
|
-
const body = {};
|
|
304
|
-
for (const [key, value] of Object.entries(metadata)) {
|
|
305
|
-
if (key !== METADATA_AUTH_FIELD) {
|
|
306
|
-
body[key] = value;
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
return body;
|
|
310
|
-
}
|
|
311
|
-
function metadataAuthTag(key, domain, body) {
|
|
312
|
-
const metadataKey = crypto.createHmac('sha256', key).update(METADATA_KEY_DOMAIN).digest();
|
|
313
|
-
return crypto.createHmac('sha256', metadataKey)
|
|
314
|
-
.update(domain)
|
|
315
|
-
.update(Buffer.from([0]))
|
|
316
|
-
.update(Buffer.from(canonicalStringify(body), 'utf-8'))
|
|
317
|
-
.digest();
|
|
318
|
-
}
|
|
319
|
-
function withMetadataAuth(metadata, key, domain) {
|
|
320
|
-
const body = metadataBody(metadata);
|
|
321
|
-
if (Object.keys(body).length === 0)
|
|
322
|
-
return {};
|
|
323
|
-
const tag = metadataAuthTag(key, domain, body);
|
|
324
|
-
return {
|
|
325
|
-
...body,
|
|
326
|
-
[METADATA_AUTH_FIELD]: {
|
|
327
|
-
alg: METADATA_AUTH_ALG,
|
|
328
|
-
tag: tag.toString('base64'),
|
|
329
|
-
},
|
|
330
|
-
};
|
|
331
|
-
}
|
|
332
|
-
function verifyMetadataAuth(metadata, key, domain) {
|
|
333
|
-
if (metadata == null)
|
|
334
|
-
return true;
|
|
335
|
-
if (typeof metadata !== 'object' || Array.isArray(metadata))
|
|
336
|
-
return false;
|
|
337
|
-
const record = metadata;
|
|
338
|
-
const auth = record[METADATA_AUTH_FIELD];
|
|
339
|
-
if (!auth || typeof auth !== 'object' || Array.isArray(auth))
|
|
340
|
-
return false;
|
|
341
|
-
const authObj = auth;
|
|
342
|
-
if (authObj.alg !== METADATA_AUTH_ALG)
|
|
343
|
-
return false;
|
|
344
|
-
if (typeof authObj.tag !== 'string' || !authObj.tag)
|
|
345
|
-
return false;
|
|
346
|
-
const body = metadataBody(record);
|
|
347
|
-
if (Object.keys(body).length === 0)
|
|
348
|
-
return false;
|
|
349
|
-
const actual = Buffer.from(authObj.tag, 'base64');
|
|
350
|
-
const expected = metadataAuthTag(key, domain, body);
|
|
351
|
-
return actual.length === expected.length && crypto.timingSafeEqual(actual, expected);
|
|
352
|
-
}
|
|
353
|
-
function verifyEnvelopeMetadataAuth(payload, messageKey) {
|
|
354
|
-
return verifyMetadataAuth(payload.protected_headers, messageKey, PROTECTED_HEADERS_DOMAIN)
|
|
355
|
-
&& verifyMetadataAuth(payload.context, messageKey, PROTECTED_CONTEXT_DOMAIN);
|
|
356
|
-
}
|
|
357
|
-
function normalizeContextMetadata(context) {
|
|
358
|
-
if (!context || typeof context !== 'object' || Array.isArray(context))
|
|
359
|
-
return {};
|
|
360
|
-
return metadataBody(context);
|
|
361
|
-
}
|
|
362
|
-
function exposedEnvelopeMetadata(metadata) {
|
|
363
|
-
if (!metadata || typeof metadata !== 'object' || Array.isArray(metadata))
|
|
364
|
-
return undefined;
|
|
365
|
-
const body = metadataBody(metadata);
|
|
366
|
-
return Object.keys(body).length > 0 ? body : undefined;
|
|
367
|
-
}
|
|
368
|
-
function validateDecryptedEnvelopeMetadata(decoded, payload, message) {
|
|
369
|
-
if (payload.protected_headers && typeof payload.protected_headers === 'object' && !Array.isArray(payload.protected_headers)) {
|
|
370
|
-
const headers = metadataBody(payload.protected_headers);
|
|
371
|
-
if (hasOwn(headers, 'payload_type')) {
|
|
372
|
-
if (!decoded || typeof decoded !== 'object' || Array.isArray(decoded))
|
|
373
|
-
return false;
|
|
374
|
-
if (String(decoded.type ?? '') !== String(headers.payload_type ?? '')) {
|
|
375
|
-
return false;
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
if (payload.context && typeof payload.context === 'object' && !Array.isArray(payload.context)) {
|
|
380
|
-
const protectedContext = metadataBody(payload.context);
|
|
381
|
-
const outerContext = normalizeContextMetadata(message?.context);
|
|
382
|
-
if (canonicalStringify(outerContext) !== canonicalStringify(protectedContext))
|
|
383
|
-
return false;
|
|
384
|
-
}
|
|
385
|
-
return true;
|
|
386
|
-
}
|
|
387
|
-
/** 群组 AAD 序列化(sorted keys, compact JSON) */
|
|
388
|
-
function aadBytesGroup(aad) {
|
|
389
|
-
const obj = {};
|
|
390
|
-
for (const field of AAD_FIELDS_GROUP) {
|
|
391
|
-
obj[field] = aad[field] ?? null;
|
|
392
|
-
}
|
|
393
|
-
return Buffer.from(canonicalStringify(obj), 'utf-8');
|
|
394
|
-
}
|
|
395
|
-
/** 群组 AAD 字段匹配检查 */
|
|
396
|
-
function aadMatchesGroup(expected, actual) {
|
|
397
|
-
for (const f of AAD_MATCH_FIELDS_GROUP) {
|
|
398
|
-
if (String(expected[f] ?? '') !== String(actual[f] ?? '')) {
|
|
399
|
-
return false;
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
return true;
|
|
403
|
-
}
|
|
404
|
-
// ── 群消息密钥派生 ────────────────────────────────────────────
|
|
405
|
-
/** 从 group_secret 派生单条群消息的加密密钥 */
|
|
406
|
-
function deriveGroupMsgKey(groupSecret, groupId, messageId) {
|
|
407
|
-
return hkdfDeriveSync(groupSecret, Buffer.from(`aun-group:${groupId}:msg:${messageId}`, 'utf-8'), 32);
|
|
408
|
-
}
|
|
409
|
-
// ── 纯函数:群组消息加解密 ────────────────────────────────────
|
|
410
|
-
/**
|
|
411
|
-
* 加密群组消息,返回 e2ee.group_encrypted 信封。
|
|
412
|
-
* senderPrivateKeyPem: 可选,传入时为密文附加发送方 ECDSA 签名。
|
|
413
|
-
*/
|
|
414
|
-
export function encryptGroupMessage(groupId, epoch, groupSecret, payload, opts) {
|
|
415
|
-
const msgKey = deriveGroupMsgKey(groupSecret, groupId, opts.messageId);
|
|
416
|
-
const plaintext = Buffer.from(JSON.stringify(payload), 'utf-8');
|
|
417
|
-
const aad = {
|
|
418
|
-
group_id: groupId,
|
|
419
|
-
from: opts.fromAid,
|
|
420
|
-
message_id: opts.messageId,
|
|
421
|
-
timestamp: opts.timestamp,
|
|
422
|
-
epoch,
|
|
423
|
-
encryption_mode: MODE_EPOCH_GROUP_KEY,
|
|
424
|
-
suite: SUITE,
|
|
425
|
-
};
|
|
426
|
-
const envelope = {
|
|
427
|
-
type: 'e2ee.group_encrypted',
|
|
428
|
-
version: '1',
|
|
429
|
-
encryption_mode: MODE_EPOCH_GROUP_KEY,
|
|
430
|
-
suite: SUITE,
|
|
431
|
-
epoch,
|
|
432
|
-
};
|
|
433
|
-
copyOptionalEnvelopeMetadata(envelope, msgKey, {
|
|
434
|
-
payloadType: payload.type,
|
|
435
|
-
protectedHeaders: opts.protectedHeaders ?? opts.protected_headers ?? opts.headers,
|
|
436
|
-
context: opts.context ?? null,
|
|
437
|
-
});
|
|
438
|
-
const aadBytes = aadBytesGroup(aad);
|
|
439
|
-
const { ciphertext, tag, nonce } = aesGcmEncrypt(msgKey, plaintext, aadBytes);
|
|
440
|
-
envelope.nonce = nonce.toString('base64');
|
|
441
|
-
envelope.ciphertext = ciphertext.toString('base64');
|
|
442
|
-
envelope.tag = tag.toString('base64');
|
|
443
|
-
envelope.aad = aad;
|
|
444
|
-
// 发送方签名
|
|
445
|
-
if (opts.senderPrivateKeyPem) {
|
|
446
|
-
const signPayload = Buffer.concat([ciphertext, tag, aadBytes]);
|
|
447
|
-
const sig = ecdsaSign(opts.senderPrivateKeyPem, signPayload);
|
|
448
|
-
envelope.sender_signature = sig.toString('base64');
|
|
449
|
-
// 证书指纹(与 Python/Go/JS SDK 一致)
|
|
450
|
-
if (opts.senderCertPem) {
|
|
451
|
-
envelope.sender_cert_fingerprint = certSha256Fingerprint(opts.senderCertPem);
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
return envelope;
|
|
455
|
-
}
|
|
456
|
-
/**
|
|
457
|
-
* 解密群组消息。
|
|
458
|
-
* groupSecrets: {epoch: groupSecretBytes} 映射。
|
|
459
|
-
* requireSignature: 为 true 时(默认),若消息含签名但无证书可验证,或缺少签名,则拒绝。
|
|
460
|
-
*/
|
|
461
|
-
export function decryptGroupMessage(message, groupSecrets, senderCertPem, opts) {
|
|
462
|
-
const requireSignature = opts?.requireSignature ?? true;
|
|
463
|
-
const logger = opts?.logger;
|
|
464
|
-
const payload = isJsonObject(message.payload) ? message.payload : null;
|
|
465
|
-
if (payload === null) {
|
|
466
|
-
logger?.warn(`decryptGroupMessage rejected: payload is not an object mid=${String(message.message_id ?? '')}`);
|
|
467
|
-
return null;
|
|
468
|
-
}
|
|
469
|
-
if (payload.type !== 'e2ee.group_encrypted') {
|
|
470
|
-
logger?.warn(`decryptGroupMessage rejected: unexpected payload.type=${String(payload.type)} mid=${String(message.message_id ?? '')}`);
|
|
471
|
-
return null;
|
|
472
|
-
}
|
|
473
|
-
const epoch = payload.epoch;
|
|
474
|
-
if (epoch == null) {
|
|
475
|
-
logger?.warn(`decryptGroupMessage rejected: missing epoch mid=${String(message.message_id ?? '')}`);
|
|
476
|
-
return null;
|
|
477
|
-
}
|
|
478
|
-
const groupSecret = groupSecrets.get(epoch);
|
|
479
|
-
if (!groupSecret) {
|
|
480
|
-
logger?.warn(`decryptGroupMessage rejected: no group secret for epoch=${epoch} mid=${String(message.message_id ?? '')}`);
|
|
481
|
-
return null;
|
|
482
|
-
}
|
|
483
|
-
try {
|
|
484
|
-
// 优先从 AAD 读取 group_id 和 message_id
|
|
485
|
-
const aad = isJsonObject(payload.aad) ? payload.aad : undefined;
|
|
486
|
-
const outerGroupId = message.group_id || '';
|
|
487
|
-
let groupId;
|
|
488
|
-
let messageId;
|
|
489
|
-
let aadFrom = '';
|
|
490
|
-
if (aad) {
|
|
491
|
-
groupId = aad.group_id || outerGroupId;
|
|
492
|
-
messageId = aad.message_id || message.message_id || '';
|
|
493
|
-
aadFrom = aad.from || '';
|
|
494
|
-
// 外层路由字段与 AAD 绑定校验
|
|
495
|
-
if (outerGroupId && groupId !== outerGroupId) {
|
|
496
|
-
logger?.warn(`decryptGroupMessage rejected: outer group_id does not match aad.group_id outer=${outerGroupId} aad=${groupId}`);
|
|
497
|
-
return null;
|
|
498
|
-
}
|
|
499
|
-
if (aadFrom) {
|
|
500
|
-
const outerFrom = message.from || '';
|
|
501
|
-
const outerSender = message.sender_aid || '';
|
|
502
|
-
if (outerFrom && outerFrom !== aadFrom) {
|
|
503
|
-
logger?.warn(`decryptGroupMessage rejected: outer from does not match aad.from outer=${outerFrom} aad=${aadFrom}`);
|
|
504
|
-
return null;
|
|
505
|
-
}
|
|
506
|
-
if (outerSender && outerSender !== aadFrom) {
|
|
507
|
-
logger?.warn(`decryptGroupMessage rejected: outer sender_aid does not match aad.from outer=${outerSender} aad=${aadFrom}`);
|
|
508
|
-
return null;
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
else {
|
|
513
|
-
groupId = outerGroupId;
|
|
514
|
-
messageId = message.message_id || '';
|
|
515
|
-
}
|
|
516
|
-
if (!groupId || !messageId) {
|
|
517
|
-
logger?.warn(`decryptGroupMessage rejected: missing group_id or message_id group=${groupId} mid=${messageId}`);
|
|
518
|
-
return null;
|
|
519
|
-
}
|
|
520
|
-
const msgKey = deriveGroupMsgKey(groupSecret, groupId, messageId);
|
|
521
|
-
const nonce = Buffer.from(payload.nonce, 'base64');
|
|
522
|
-
const ciphertext = Buffer.from(payload.ciphertext, 'base64');
|
|
523
|
-
const tag = Buffer.from(payload.tag, 'base64');
|
|
524
|
-
if (!verifyEnvelopeMetadataAuth(payload, msgKey)) {
|
|
525
|
-
logger?.warn(`decryptGroupMessage rejected: envelope metadata auth failed group=${groupId} mid=${messageId}`);
|
|
526
|
-
return null;
|
|
527
|
-
}
|
|
528
|
-
const aadBytes = aad ? aadBytesGroup(aad) : Buffer.alloc(0);
|
|
529
|
-
const plaintext = aesGcmDecrypt(msgKey, ciphertext, tag, nonce, aadBytes);
|
|
530
|
-
const decoded = JSON.parse(plaintext.toString('utf-8'));
|
|
531
|
-
if (!validateDecryptedEnvelopeMetadata(decoded, payload, message)) {
|
|
532
|
-
logger?.warn(`decryptGroupMessage rejected: decrypted envelope metadata validation failed group=${groupId} mid=${messageId}`);
|
|
533
|
-
return null;
|
|
534
|
-
}
|
|
535
|
-
const e2ee = {
|
|
536
|
-
encryption_mode: MODE_EPOCH_GROUP_KEY,
|
|
537
|
-
suite: SUITE,
|
|
538
|
-
epoch,
|
|
539
|
-
sender_verified: false,
|
|
540
|
-
};
|
|
541
|
-
const protectedHeaders = exposedEnvelopeMetadata(payload.protected_headers);
|
|
542
|
-
if (protectedHeaders)
|
|
543
|
-
e2ee.protected_headers = protectedHeaders;
|
|
544
|
-
const context = exposedEnvelopeMetadata(payload.context);
|
|
545
|
-
if (context)
|
|
546
|
-
e2ee.context = context;
|
|
547
|
-
const result = {
|
|
548
|
-
...message,
|
|
549
|
-
payload: decoded,
|
|
550
|
-
encrypted: true,
|
|
551
|
-
e2ee,
|
|
552
|
-
};
|
|
553
|
-
// 发送方签名验证
|
|
554
|
-
const senderSigB64 = payload.sender_signature;
|
|
555
|
-
if (requireSignature) {
|
|
556
|
-
if (!senderSigB64) {
|
|
557
|
-
logger?.warn(`decryptGroupMessage rejected: sender signature required but missing group=${groupId} mid=${messageId}`);
|
|
558
|
-
return null;
|
|
559
|
-
}
|
|
560
|
-
if (!senderCertPem) {
|
|
561
|
-
logger?.warn(`decryptGroupMessage rejected: sender signature required but senderCertPem missing group=${groupId} mid=${messageId}`);
|
|
562
|
-
return null;
|
|
563
|
-
}
|
|
564
|
-
try {
|
|
565
|
-
const senderPub = pemToCertPublicKey(senderCertPem);
|
|
566
|
-
const sigBytes = Buffer.from(senderSigB64, 'base64');
|
|
567
|
-
const verifyPayload = Buffer.concat([ciphertext, tag, aadBytes]);
|
|
568
|
-
if (!ecdsaVerify(senderPub, sigBytes, verifyPayload)) {
|
|
569
|
-
logger?.warn(`decryptGroupMessage rejected: sender signature verification failed group=${groupId} mid=${messageId}`);
|
|
570
|
-
return null;
|
|
571
|
-
}
|
|
572
|
-
if (isJsonObject(result.e2ee))
|
|
573
|
-
result.e2ee.sender_verified = true;
|
|
574
|
-
}
|
|
575
|
-
catch (exc) {
|
|
576
|
-
logger?.warn(`decryptGroupMessage rejected: sender signature verification threw group=${groupId} mid=${messageId} err=${String(exc)}`);
|
|
577
|
-
return null;
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
else if (senderCertPem) {
|
|
581
|
-
// 非零信任模式但提供了证书:有证书时强制验签
|
|
582
|
-
if (!senderSigB64) {
|
|
583
|
-
logger?.warn(`decryptGroupMessage rejected: senderCertPem present but signature missing group=${groupId} mid=${messageId}`);
|
|
584
|
-
return null;
|
|
585
|
-
}
|
|
586
|
-
try {
|
|
587
|
-
const senderPub = pemToCertPublicKey(senderCertPem);
|
|
588
|
-
const sigBytes = Buffer.from(senderSigB64, 'base64');
|
|
589
|
-
const verifyPayload = Buffer.concat([ciphertext, tag, aadBytes]);
|
|
590
|
-
if (!ecdsaVerify(senderPub, sigBytes, verifyPayload)) {
|
|
591
|
-
logger?.warn(`decryptGroupMessage rejected: optional sender signature verification failed group=${groupId} mid=${messageId}`);
|
|
592
|
-
return null;
|
|
593
|
-
}
|
|
594
|
-
if (isJsonObject(result.e2ee))
|
|
595
|
-
result.e2ee.sender_verified = true;
|
|
596
|
-
}
|
|
597
|
-
catch (exc) {
|
|
598
|
-
logger?.warn(`decryptGroupMessage rejected: optional sender signature verification threw group=${groupId} mid=${messageId} err=${String(exc)}`);
|
|
599
|
-
return null;
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
return result;
|
|
603
|
-
}
|
|
604
|
-
catch (exc) {
|
|
605
|
-
logger?.warn(`decryptGroupMessage rejected: decrypt threw mid=${String(message.message_id ?? '')} err=${String(exc)}`);
|
|
606
|
-
return null;
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
// ── Membership Commitment ─────────────────────────────────────
|
|
610
|
-
/**
|
|
611
|
-
* 计算 Membership Commitment。
|
|
612
|
-
* commitment = SHA-256(sort(aids).join("|") + "|" + epoch + "|" + groupId + "|" + SHA256(groupSecret).hex())
|
|
613
|
-
*/
|
|
614
|
-
export function computeMembershipCommitment(memberAids, epoch, groupId, groupSecret) {
|
|
615
|
-
const sortedAids = [...memberAids].sort();
|
|
616
|
-
const secretHash = crypto.createHash('sha256').update(groupSecret).digest().toString('hex');
|
|
617
|
-
const data = sortedAids.join('|') + '|' + epoch + '|' + groupId + '|' + secretHash;
|
|
618
|
-
return crypto.createHash('sha256').update(data, 'utf-8').digest().toString('hex');
|
|
619
|
-
}
|
|
620
|
-
/**
|
|
621
|
-
* 验证 Membership Commitment。
|
|
622
|
-
* 1. 重算 commitment 是否匹配
|
|
623
|
-
* 2. 检查 myAid 是否在 memberAids 中
|
|
624
|
-
*/
|
|
625
|
-
export function verifyMembershipCommitment(commitment, memberAids, epoch, groupId, myAid, groupSecret) {
|
|
626
|
-
if (!memberAids.includes(myAid))
|
|
627
|
-
return false;
|
|
628
|
-
const expected = computeMembershipCommitment(memberAids, epoch, groupId, groupSecret);
|
|
629
|
-
// 恒定时间比较(长度不同时直接返回 false)
|
|
630
|
-
const a = Buffer.from(expected, 'utf-8');
|
|
631
|
-
const b = Buffer.from(commitment, 'utf-8');
|
|
632
|
-
if (a.length !== b.length)
|
|
633
|
-
return false;
|
|
634
|
-
return crypto.timingSafeEqual(a, b);
|
|
635
|
-
}
|
|
636
|
-
// ── State Hash ────────────────────────────────────────────────
|
|
637
|
-
/**
|
|
638
|
-
* 计算群组状态哈希(链式)。
|
|
639
|
-
* state_hash = SHA-256(group_id | 0x00 | state_version(u64be) | 0x00 |
|
|
640
|
-
* key_epoch(u64be) | 0x00 | membership_block | 0x00 | policy_block | 0x00 | prev_state_hash(32B))
|
|
641
|
-
*/
|
|
642
|
-
export function computeStateHash(params) {
|
|
643
|
-
const { groupId, stateVersion, keyEpoch, members, policy, prevStateHash } = params;
|
|
644
|
-
// 按 AID 排序,构建 membership_block
|
|
645
|
-
const sorted = [...members].sort((a, b) => a.aid.localeCompare(b.aid));
|
|
646
|
-
const membershipBlock = sorted.map(m => `${m.aid}:${m.role}`).join('|');
|
|
647
|
-
// 按 key 排序的 canonical JSON(无空格)
|
|
648
|
-
const sortedPolicy = {};
|
|
649
|
-
for (const key of Object.keys(policy).sort()) {
|
|
650
|
-
sortedPolicy[key] = policy[key];
|
|
651
|
-
}
|
|
652
|
-
const policyBlock = Object.keys(policy).length > 0 ? JSON.stringify(sortedPolicy) : '';
|
|
653
|
-
// prev_state_hash: 32 字节,空则全零
|
|
654
|
-
const prevBytes = prevStateHash
|
|
655
|
-
? Buffer.from(prevStateHash, 'hex')
|
|
656
|
-
: Buffer.alloc(32);
|
|
657
|
-
// state_version / key_epoch → uint64 big-endian
|
|
658
|
-
const svBuf = Buffer.alloc(8);
|
|
659
|
-
svBuf.writeBigUInt64BE(BigInt(stateVersion));
|
|
660
|
-
const keBuf = Buffer.alloc(8);
|
|
661
|
-
keBuf.writeBigUInt64BE(BigInt(keyEpoch));
|
|
662
|
-
const data = Buffer.concat([
|
|
663
|
-
Buffer.from(groupId, 'utf-8'), Buffer.from([0x00]),
|
|
664
|
-
svBuf, Buffer.from([0x00]),
|
|
665
|
-
keBuf, Buffer.from([0x00]),
|
|
666
|
-
Buffer.from(membershipBlock, 'utf-8'), Buffer.from([0x00]),
|
|
667
|
-
Buffer.from(policyBlock, 'utf-8'), Buffer.from([0x00]),
|
|
668
|
-
prevBytes,
|
|
669
|
-
]);
|
|
670
|
-
return crypto.createHash('sha256').update(data).digest('hex');
|
|
671
|
-
}
|
|
672
|
-
// ── Membership Manifest ───────────────────────────────────────
|
|
673
|
-
/** 构建 Membership Manifest(未签名) */
|
|
674
|
-
export function buildMembershipManifest(groupId, epoch, prevEpoch, memberAids, opts) {
|
|
675
|
-
return {
|
|
676
|
-
manifest_version: 1,
|
|
677
|
-
group_id: groupId,
|
|
678
|
-
epoch,
|
|
679
|
-
prev_epoch: prevEpoch,
|
|
680
|
-
member_aids: [...memberAids].sort(),
|
|
681
|
-
added: [...(opts?.added ?? [])].sort(),
|
|
682
|
-
removed: [...(opts?.removed ?? [])].sort(),
|
|
683
|
-
initiator_aid: opts?.initiatorAid ?? '',
|
|
684
|
-
issued_at: Date.now(),
|
|
685
|
-
};
|
|
686
|
-
}
|
|
687
|
-
/** 序列化 manifest 为签名输入 */
|
|
688
|
-
function manifestSignData(manifest) {
|
|
689
|
-
const fields = [
|
|
690
|
-
String(manifest.manifest_version ?? 1),
|
|
691
|
-
manifest.group_id ?? '',
|
|
692
|
-
String(manifest.epoch ?? 0),
|
|
693
|
-
String(manifest.prev_epoch ?? ''),
|
|
694
|
-
(manifest.member_aids ?? []).join('|'),
|
|
695
|
-
(manifest.added ?? []).join('|'),
|
|
696
|
-
(manifest.removed ?? []).join('|'),
|
|
697
|
-
manifest.initiator_aid ?? '',
|
|
698
|
-
String(manifest.issued_at ?? 0),
|
|
699
|
-
];
|
|
700
|
-
return Buffer.from(fields.join('\n'), 'utf-8');
|
|
701
|
-
}
|
|
702
|
-
/** 对 Membership Manifest 签名,返回带 signature 字段的新 manifest */
|
|
703
|
-
export function signMembershipManifest(manifest, privateKeyPem) {
|
|
704
|
-
const signData = manifestSignData(manifest);
|
|
705
|
-
const sig = ecdsaSign(privateKeyPem, signData);
|
|
706
|
-
return { ...manifest, signature: sig.toString('base64') };
|
|
707
|
-
}
|
|
708
|
-
/** 验证 Membership Manifest 签名 */
|
|
709
|
-
export function verifyMembershipManifest(manifest, initiatorCertPem) {
|
|
710
|
-
const sigB64 = manifest.signature;
|
|
711
|
-
if (!sigB64)
|
|
712
|
-
return false;
|
|
713
|
-
try {
|
|
714
|
-
const pubKey = pemToCertPublicKey(initiatorCertPem);
|
|
715
|
-
const sigBytes = Buffer.from(sigB64, 'base64');
|
|
716
|
-
const signData = manifestSignData(manifest);
|
|
717
|
-
return ecdsaVerify(pubKey, sigBytes, signData);
|
|
718
|
-
}
|
|
719
|
-
catch {
|
|
720
|
-
return false;
|
|
721
|
-
}
|
|
722
|
-
}
|
|
723
|
-
// ── Group Secret 生命周期管理 ─────────────────────────────────
|
|
724
|
-
/** 存储 group_secret 到 keystore metadata */
|
|
725
|
-
export function storeGroupSecret(keystore, aid, groupId, epoch, groupSecret, commitment, memberAids, epochChain, pendingRotationId = '', epochChainUnverified, epochChainUnverifiedReason) {
|
|
726
|
-
const transitionResult = storeKeyStoreGroupTransition(keystore, aid, groupId, {
|
|
727
|
-
epoch,
|
|
728
|
-
secret: groupSecret.toString('base64'),
|
|
729
|
-
commitment,
|
|
730
|
-
memberAids: [...memberAids].sort(),
|
|
731
|
-
epochChain,
|
|
732
|
-
pendingRotationId,
|
|
733
|
-
epochChainUnverified,
|
|
734
|
-
epochChainUnverifiedReason,
|
|
735
|
-
});
|
|
736
|
-
if (transitionResult !== null)
|
|
737
|
-
return transitionResult;
|
|
738
|
-
throw new Error(`keystore ${keystore.constructor?.name ?? 'unknown'} missing storeGroupSecretTransition method`);
|
|
739
|
-
}
|
|
740
|
-
/** 保存指定 epoch key;低于 current 时写入 old epoch,不覆盖 current。 */
|
|
741
|
-
export function storeGroupSecretEpoch(keystore, aid, groupId, epoch, groupSecret, commitment, memberAids, epochChain, pendingRotationId = '', epochChainUnverified, epochChainUnverifiedReason) {
|
|
742
|
-
const secret = groupSecret.toString('base64');
|
|
743
|
-
const members = [...memberAids].sort();
|
|
744
|
-
const rowResult = storeKeyStoreGroupEpoch(keystore, aid, groupId, {
|
|
745
|
-
epoch,
|
|
746
|
-
secret,
|
|
747
|
-
commitment,
|
|
748
|
-
memberAids: members,
|
|
749
|
-
epochChain,
|
|
750
|
-
pendingRotationId,
|
|
751
|
-
epochChainUnverified,
|
|
752
|
-
epochChainUnverifiedReason,
|
|
753
|
-
});
|
|
754
|
-
if (rowResult !== null)
|
|
755
|
-
return rowResult;
|
|
756
|
-
throw new Error(`keystore ${keystore.constructor?.name ?? 'unknown'} missing storeGroupSecretEpoch method`);
|
|
757
|
-
}
|
|
758
|
-
/** 读取 group_secret */
|
|
759
|
-
export function loadGroupSecret(keystore, aid, groupId, epoch) {
|
|
760
|
-
const entry = loadKeyStoreGroupEpoch(keystore, aid, groupId, epoch) ?? undefined;
|
|
761
|
-
if (!entry)
|
|
762
|
-
return null;
|
|
763
|
-
const secretStr = entry.secret;
|
|
764
|
-
if (!secretStr)
|
|
765
|
-
return null;
|
|
766
|
-
const loaded = {
|
|
767
|
-
epoch: entry.epoch,
|
|
768
|
-
secret: Buffer.from(secretStr, 'base64'),
|
|
769
|
-
commitment: String(entry.commitment ?? ''),
|
|
770
|
-
member_aids: Array.isArray(entry.member_aids) ? entry.member_aids.map((item) => String(item)) : [],
|
|
771
|
-
};
|
|
772
|
-
if (typeof entry.epoch_chain === 'string')
|
|
773
|
-
loaded.epoch_chain = entry.epoch_chain;
|
|
774
|
-
if (typeof entry.pending_rotation_id === 'string')
|
|
775
|
-
loaded.pending_rotation_id = entry.pending_rotation_id;
|
|
776
|
-
if (typeof entry.epoch_chain_unverified === 'boolean')
|
|
777
|
-
loaded.epoch_chain_unverified = entry.epoch_chain_unverified;
|
|
778
|
-
if (typeof entry.epoch_chain_unverified_reason === 'string') {
|
|
779
|
-
loaded.epoch_chain_unverified_reason = entry.epoch_chain_unverified_reason;
|
|
780
|
-
}
|
|
781
|
-
return loaded;
|
|
782
|
-
}
|
|
783
|
-
function assessIncomingEpochChain(keystore, aid, groupId, epoch, commitment, incomingChain, rotationId, rotatorAid, source, logger = _noopLogger) {
|
|
784
|
-
const chain = (incomingChain ?? '').trim();
|
|
785
|
-
const rid = rotationId.trim();
|
|
786
|
-
const rotator = rotatorAid.trim();
|
|
787
|
-
if (rid && !chain) {
|
|
788
|
-
logger.warn(`[e2ee-group] rejecting rotation key missing epoch_chain source=${source} group=${groupId} epoch=${epoch} rotation=${rid}`);
|
|
789
|
-
return { ok: false };
|
|
790
|
-
}
|
|
791
|
-
const current = loadGroupSecret(keystore, aid, groupId);
|
|
792
|
-
if (current?.epoch === epoch) {
|
|
793
|
-
const currentChain = current.epoch_chain ?? '';
|
|
794
|
-
const currentPendingRotationId = current.pending_rotation_id ?? '';
|
|
795
|
-
if (chain && currentChain === chain)
|
|
796
|
-
return { ok: true };
|
|
797
|
-
if (rid && chain && currentChain && currentChain !== chain) {
|
|
798
|
-
if (!(currentPendingRotationId && currentPendingRotationId !== rid)) {
|
|
799
|
-
logger.warn(`[e2ee-group] rejecting same-epoch chain fork source=${source} group=${groupId} epoch=${epoch} rotation=${rid}`);
|
|
800
|
-
return { ok: false };
|
|
801
|
-
}
|
|
802
|
-
}
|
|
803
|
-
}
|
|
804
|
-
const prev = loadGroupSecret(keystore, aid, groupId, epoch - 1);
|
|
805
|
-
const prevChain = prev?.epoch_chain ?? '';
|
|
806
|
-
if (!chain)
|
|
807
|
-
return { ok: true, unverified: true, reason: 'missing_epoch_chain' };
|
|
808
|
-
if (!prevChain)
|
|
809
|
-
return { ok: true, unverified: true, reason: 'missing_prev_chain' };
|
|
810
|
-
if (!rotator) {
|
|
811
|
-
if (rid) {
|
|
812
|
-
logger.warn(`[e2ee-group] rejecting rotation key missing rotator_aid source=${source} group=${groupId} epoch=${epoch} rotation=${rid}`);
|
|
813
|
-
return { ok: false };
|
|
814
|
-
}
|
|
815
|
-
return { ok: true, unverified: true, reason: 'missing_rotator_aid' };
|
|
816
|
-
}
|
|
817
|
-
if (!verifyEpochChain(chain, prevChain, epoch, commitment, rotator)) {
|
|
818
|
-
if (rid) {
|
|
819
|
-
logger.warn(`[e2ee-group] rejecting rotation key with failed epoch_chain verification source=${source} group=${groupId} epoch=${epoch} rotation=${rid}`);
|
|
820
|
-
return { ok: false };
|
|
821
|
-
}
|
|
822
|
-
logger.warn(`[e2ee-group] epoch_chain verification failed, accepting as unverified for compatibility source=${source} group=${groupId} epoch=${epoch}`);
|
|
823
|
-
return { ok: true, unverified: true, reason: 'chain_mismatch_legacy' };
|
|
824
|
-
}
|
|
825
|
-
if (!rid)
|
|
826
|
-
return { ok: true, unverified: true, reason: 'missing_rotation_id' };
|
|
827
|
-
return { ok: true, unverified: false };
|
|
828
|
-
}
|
|
829
|
-
/** 加载某群组所有 epoch 的 group_secret */
|
|
830
|
-
export function loadAllGroupSecrets(keystore, aid, groupId) {
|
|
831
|
-
const result = new Map();
|
|
832
|
-
for (const entry of loadKeyStoreGroupEpochs(keystore, aid, groupId)) {
|
|
833
|
-
const secretStr = entry.secret;
|
|
834
|
-
if (secretStr && entry.epoch != null) {
|
|
835
|
-
result.set(entry.epoch, Buffer.from(secretStr, 'base64'));
|
|
836
|
-
}
|
|
837
|
-
}
|
|
838
|
-
return result;
|
|
839
|
-
}
|
|
840
|
-
/** 清理过期的旧 epoch 记录。返回清理数量。 */
|
|
841
|
-
export function cleanupOldEpochs(keystore, aid, groupId, retentionSeconds = OLD_EPOCH_RETENTION_SECONDS) {
|
|
842
|
-
const cutoffMs = Date.now() - retentionSeconds * 1000;
|
|
843
|
-
return cleanupKeyStoreGroupOldEpochs(keystore, aid, groupId, cutoffMs);
|
|
844
|
-
}
|
|
845
|
-
/** 仅回滚指定 rotation 写入的本地 pending epoch key。 */
|
|
846
|
-
export function discardPendingGroupSecret(keystore, aid, groupId, epoch, rotationId) {
|
|
847
|
-
const rid = rotationId.trim();
|
|
848
|
-
if (!rid)
|
|
849
|
-
return false;
|
|
850
|
-
if (typeof keystore.discardPendingGroupSecretState === 'function') {
|
|
851
|
-
return keystore.discardPendingGroupSecretState(aid, groupId, epoch, rid);
|
|
852
|
-
}
|
|
853
|
-
throw new Error(`keystore ${keystore.constructor?.name ?? 'unknown'} missing discardPendingGroupSecretState method`);
|
|
854
|
-
}
|
|
855
|
-
/** 删除群组的所有密钥数据(群组解散时使用) */
|
|
856
|
-
export function deleteGroupSecret(keystore, aid, groupId) {
|
|
857
|
-
deleteKeyStoreGroupState(keystore, aid, groupId);
|
|
858
|
-
}
|
|
859
|
-
// ── GroupReplayGuard ──────────────────────────────────────────
|
|
860
|
-
/** 群组消息防重放守卫 */
|
|
861
|
-
export class GroupReplayGuard {
|
|
862
|
-
_seen = new Map();
|
|
863
|
-
_maxSize;
|
|
864
|
-
constructor(maxSize = 50000) {
|
|
865
|
-
this._maxSize = maxSize;
|
|
866
|
-
}
|
|
867
|
-
/** 检查并记录。返回 true 表示首次(通过),false 表示重放(拒绝)。 */
|
|
868
|
-
checkAndRecord(groupId, senderAid, messageId) {
|
|
869
|
-
const key = `${groupId}:${senderAid}:${messageId}`;
|
|
870
|
-
if (this._seen.has(key))
|
|
871
|
-
return false;
|
|
872
|
-
this._seen.set(key, true);
|
|
873
|
-
this.trim();
|
|
874
|
-
return true;
|
|
875
|
-
}
|
|
876
|
-
/** 仅检查是否已记录 */
|
|
877
|
-
isSeen(groupId, senderAid, messageId) {
|
|
878
|
-
return this._seen.has(`${groupId}:${senderAid}:${messageId}`);
|
|
879
|
-
}
|
|
880
|
-
/** 仅记录,不检查 */
|
|
881
|
-
record(groupId, senderAid, messageId) {
|
|
882
|
-
this._seen.set(`${groupId}:${senderAid}:${messageId}`, true);
|
|
883
|
-
this.trim();
|
|
884
|
-
}
|
|
885
|
-
/** LRU 裁剪(供外部调用) */
|
|
886
|
-
trim() {
|
|
887
|
-
if (this._seen.size > this._maxSize) {
|
|
888
|
-
const trimCount = this._seen.size - Math.floor(this._maxSize * 0.8);
|
|
889
|
-
const iter = this._seen.keys();
|
|
890
|
-
for (let i = 0; i < trimCount; i++) {
|
|
891
|
-
const next = iter.next();
|
|
892
|
-
if (next.done)
|
|
893
|
-
break;
|
|
894
|
-
this._seen.delete(next.value);
|
|
895
|
-
}
|
|
896
|
-
}
|
|
897
|
-
}
|
|
898
|
-
get size() {
|
|
899
|
-
return this._seen.size;
|
|
900
|
-
}
|
|
901
|
-
}
|
|
902
|
-
// ── GroupKeyRequestThrottle ──────────────────────────────────
|
|
903
|
-
/** 群组密钥请求/响应频率限制 */
|
|
904
|
-
export class GroupKeyRequestThrottle {
|
|
905
|
-
_last = new Map();
|
|
906
|
-
_cooldown;
|
|
907
|
-
constructor(cooldown = 30.0) {
|
|
908
|
-
this._cooldown = cooldown;
|
|
909
|
-
}
|
|
910
|
-
/** 检查是否允许操作 */
|
|
911
|
-
allow(key) {
|
|
912
|
-
const now = Date.now() / 1000;
|
|
913
|
-
const last = this._last.get(key);
|
|
914
|
-
if (last != null && (now - last) < this._cooldown)
|
|
915
|
-
return false;
|
|
916
|
-
this._last.set(key, now);
|
|
917
|
-
return true;
|
|
918
|
-
}
|
|
919
|
-
reset(key) {
|
|
920
|
-
this._last.delete(key);
|
|
921
|
-
}
|
|
922
|
-
}
|
|
923
|
-
// ── Group Key 分发与恢复协议 ──────────────────────────────────
|
|
924
|
-
/** 生成 32 字节随机 group_secret */
|
|
925
|
-
export function generateGroupSecret() {
|
|
926
|
-
return crypto.randomBytes(32);
|
|
927
|
-
}
|
|
928
|
-
/** 构建 group key 分发消息 payload */
|
|
929
|
-
export function buildKeyDistribution(groupId, epoch, groupSecret, memberAids, distributedBy, manifest, epochChain) {
|
|
930
|
-
const commitment = computeMembershipCommitment(memberAids, epoch, groupId, groupSecret);
|
|
931
|
-
const result = {
|
|
932
|
-
type: 'e2ee.group_key_distribution',
|
|
933
|
-
group_id: groupId,
|
|
934
|
-
epoch,
|
|
935
|
-
group_secret: groupSecret.toString('base64'),
|
|
936
|
-
commitment,
|
|
937
|
-
member_aids: [...memberAids].sort(),
|
|
938
|
-
distributed_by: distributedBy,
|
|
939
|
-
distributed_at: Date.now(),
|
|
940
|
-
};
|
|
941
|
-
if (manifest) {
|
|
942
|
-
result.manifest = manifest;
|
|
943
|
-
}
|
|
944
|
-
if (epochChain !== undefined) {
|
|
945
|
-
result.epoch_chain = epochChain;
|
|
946
|
-
}
|
|
947
|
-
return result;
|
|
948
|
-
}
|
|
949
|
-
/** 处理收到的 group key 分发消息 */
|
|
950
|
-
export function handleKeyDistribution(message, keystore, aid, initiatorCertPem, logger) {
|
|
951
|
-
const payload = 'group_id' in message
|
|
952
|
-
? message
|
|
953
|
-
: (isJsonObject(message.payload) ? message.payload : message);
|
|
954
|
-
const groupId = payload.group_id;
|
|
955
|
-
const epoch = payload.epoch;
|
|
956
|
-
const groupSecretB64 = payload.group_secret;
|
|
957
|
-
const commitment = payload.commitment;
|
|
958
|
-
const memberAids = payload.member_aids ?? [];
|
|
959
|
-
logger?.debug(`handleKeyDistribution enter: group=${String(groupId ?? '')}, epoch=${String(epoch ?? '')}, aid=${aid}, members=${memberAids.length}`);
|
|
960
|
-
if (!groupId || epoch == null || !groupSecretB64 || !commitment) {
|
|
961
|
-
logger?.warn(`handleKeyDistribution rejected: missing required fields group=${String(groupId ?? '')} epoch=${String(epoch ?? '')} has_secret=${!!groupSecretB64} has_commitment=${!!commitment}`);
|
|
962
|
-
return false;
|
|
963
|
-
}
|
|
964
|
-
// 验证 Membership Manifest 签名
|
|
965
|
-
const manifest = isJsonObject(payload.manifest) ? payload.manifest : undefined;
|
|
966
|
-
if (initiatorCertPem) {
|
|
967
|
-
if (!manifest) {
|
|
968
|
-
logger?.warn(`handleKeyDistribution rejected: manifest required when initiatorCertPem present group=${groupId} epoch=${epoch}`);
|
|
969
|
-
return false;
|
|
970
|
-
}
|
|
971
|
-
if (!verifyMembershipManifest(manifest, initiatorCertPem)) {
|
|
972
|
-
logger?.warn(`handleKeyDistribution rejected: manifest signature verification failed group=${groupId} epoch=${epoch}`);
|
|
973
|
-
return false;
|
|
974
|
-
}
|
|
975
|
-
if (manifest.group_id !== groupId || manifest.epoch !== epoch) {
|
|
976
|
-
logger?.warn(`handleKeyDistribution rejected: manifest group_id/epoch mismatch group=${groupId} epoch=${epoch} manifest_group=${String(manifest.group_id)} manifest_epoch=${String(manifest.epoch)}`);
|
|
977
|
-
return false;
|
|
978
|
-
}
|
|
979
|
-
const manifestMembers = [...(manifest.member_aids ?? [])].sort();
|
|
980
|
-
const payloadMembers = [...memberAids].sort();
|
|
981
|
-
if (JSON.stringify(manifestMembers) !== JSON.stringify(payloadMembers)) {
|
|
982
|
-
logger?.warn(`handleKeyDistribution rejected: manifest members mismatch payload members group=${groupId} epoch=${epoch}`);
|
|
983
|
-
return false;
|
|
984
|
-
}
|
|
985
|
-
}
|
|
986
|
-
else if (manifest) {
|
|
987
|
-
if (manifest.group_id !== groupId || manifest.epoch !== epoch) {
|
|
988
|
-
logger?.warn(`handleKeyDistribution rejected: manifest group_id/epoch mismatch (no cert) group=${groupId} epoch=${epoch}`);
|
|
989
|
-
return false;
|
|
990
|
-
}
|
|
991
|
-
const manifestMembers = [...(manifest.member_aids ?? [])].sort();
|
|
992
|
-
const payloadMembers = [...memberAids].sort();
|
|
993
|
-
if (JSON.stringify(manifestMembers) !== JSON.stringify(payloadMembers)) {
|
|
994
|
-
logger?.warn(`handleKeyDistribution rejected: manifest members mismatch payload members (no cert) group=${groupId} epoch=${epoch}`);
|
|
995
|
-
return false;
|
|
996
|
-
}
|
|
997
|
-
}
|
|
998
|
-
const groupSecret = Buffer.from(groupSecretB64, 'base64');
|
|
999
|
-
// 验证 commitment
|
|
1000
|
-
if (!verifyMembershipCommitment(commitment, memberAids, epoch, groupId, aid, groupSecret)) {
|
|
1001
|
-
logger?.warn(`handleKeyDistribution rejected: commitment verification failed group=${groupId} epoch=${epoch}`);
|
|
1002
|
-
return false;
|
|
1003
|
-
}
|
|
1004
|
-
const incomingChain = typeof payload.epoch_chain === 'string' ? payload.epoch_chain : undefined;
|
|
1005
|
-
const rotationId = typeof payload.rotation_id === 'string' ? payload.rotation_id : '';
|
|
1006
|
-
const chainAssessment = assessIncomingEpochChain(keystore, aid, groupId, epoch, commitment, incomingChain, rotationId, String(payload.distributed_by ?? payload.rotator_aid ?? ''), 'key_distribution', logger);
|
|
1007
|
-
if (!chainAssessment.ok) {
|
|
1008
|
-
logger?.warn(`handleKeyDistribution rejected: epoch chain assessment failed group=${groupId} epoch=${epoch} reason=${chainAssessment.reason ?? ''}`);
|
|
1009
|
-
return false;
|
|
1010
|
-
}
|
|
1011
|
-
return storeGroupSecret(keystore, aid, groupId, epoch, groupSecret, commitment, memberAids, incomingChain, rotationId, chainAssessment.unverified, chainAssessment.reason);
|
|
1012
|
-
}
|
|
1013
|
-
/** 构建密钥请求 payload */
|
|
1014
|
-
export function buildKeyRequest(groupId, epoch, requesterAid, requestId) {
|
|
1015
|
-
return {
|
|
1016
|
-
type: 'e2ee.group_key_request',
|
|
1017
|
-
group_id: groupId,
|
|
1018
|
-
epoch,
|
|
1019
|
-
requester_aid: requesterAid,
|
|
1020
|
-
request_id: requestId ?? crypto.randomUUID(),
|
|
1021
|
-
requested_at: Date.now(),
|
|
1022
|
-
};
|
|
1023
|
-
}
|
|
1024
|
-
/** 处理收到的密钥请求 */
|
|
1025
|
-
export function handleKeyRequest(request, keystore, aid, currentMembers, privateKeyPem, logger) {
|
|
1026
|
-
const payload = 'group_id' in request
|
|
1027
|
-
? request
|
|
1028
|
-
: (isJsonObject(request.payload) ? request.payload : request);
|
|
1029
|
-
const requesterAid = payload.requester_aid;
|
|
1030
|
-
const groupId = payload.group_id;
|
|
1031
|
-
const epoch = payload.epoch;
|
|
1032
|
-
logger?.debug(`handleKeyRequest enter: group=${String(groupId ?? '')}, epoch=${String(epoch ?? '')}, requester=${String(requesterAid ?? '')}, aid=${aid}`);
|
|
1033
|
-
if (!requesterAid || !groupId || epoch == null) {
|
|
1034
|
-
logger?.warn(`handleKeyRequest rejected: missing required fields requester=${String(requesterAid ?? '')} group=${String(groupId ?? '')} epoch=${String(epoch ?? '')}`);
|
|
1035
|
-
return null;
|
|
1036
|
-
}
|
|
1037
|
-
// 验证请求者是群成员
|
|
1038
|
-
if (!currentMembers.includes(requesterAid)) {
|
|
1039
|
-
logger?.warn(`handleKeyRequest rejected: requester not in current members group=${groupId} requester=${requesterAid}`);
|
|
1040
|
-
return null;
|
|
1041
|
-
}
|
|
1042
|
-
// 查本地密钥
|
|
1043
|
-
const secretData = loadGroupSecret(keystore, aid, groupId, epoch);
|
|
1044
|
-
if (!secretData) {
|
|
1045
|
-
logger?.warn(`handleKeyRequest rejected: no local secret for epoch group=${groupId} epoch=${epoch}`);
|
|
1046
|
-
return null;
|
|
1047
|
-
}
|
|
1048
|
-
// 历史隔离:密钥存储中记录了该 epoch 的成员列表,
|
|
1049
|
-
// 若请求者不在该列表中,说明其不属于该 epoch,直接拒绝(不响应)。
|
|
1050
|
-
const memberAids = (secretData.member_aids ?? []).map(String).filter(Boolean).sort();
|
|
1051
|
-
if (memberAids.length > 0 && !memberAids.includes(requesterAid)) {
|
|
1052
|
-
logger?.warn(`handleKeyRequest rejected: requester not in epoch member list group=${groupId} epoch=${epoch} requester=${requesterAid}`);
|
|
1053
|
-
return null;
|
|
1054
|
-
}
|
|
1055
|
-
// 确定响应使用的成员列表:优先使用密钥存储中的成员列表,
|
|
1056
|
-
// 若为空(旧数据),则降级使用当前成员列表。
|
|
1057
|
-
const responseMemberAids = memberAids.length > 0
|
|
1058
|
-
? memberAids
|
|
1059
|
-
: currentMembers.map(String).filter(Boolean).sort();
|
|
1060
|
-
// 若存储中无 commitment,按当前成员列表重新计算
|
|
1061
|
-
const commitmentStr = secretData.commitment
|
|
1062
|
-
|| computeMembershipCommitment(responseMemberAids, epoch, groupId, secretData.secret);
|
|
1063
|
-
const response = {
|
|
1064
|
-
type: 'e2ee.group_key_response',
|
|
1065
|
-
group_id: groupId,
|
|
1066
|
-
epoch,
|
|
1067
|
-
group_secret: secretData.secret.toString('base64'),
|
|
1068
|
-
commitment: commitmentStr,
|
|
1069
|
-
member_aids: responseMemberAids,
|
|
1070
|
-
requester_aid: requesterAid,
|
|
1071
|
-
request_id: String(payload.request_id ?? ''),
|
|
1072
|
-
responder_aid: aid,
|
|
1073
|
-
issued_at: Date.now(),
|
|
1074
|
-
};
|
|
1075
|
-
// epoch_chain 始终包含(若有),供接收方验证历史轮转链
|
|
1076
|
-
if (secretData.epoch_chain !== undefined) {
|
|
1077
|
-
response.epoch_chain = secretData.epoch_chain;
|
|
1078
|
-
}
|
|
1079
|
-
return privateKeyPem ? signGroupKeyResponse(response, privateKeyPem) : response;
|
|
1080
|
-
}
|
|
1081
|
-
/** 处理收到的密钥响应 */
|
|
1082
|
-
export function handleKeyResponse(response, keystore, aid, opts) {
|
|
1083
|
-
const payload = 'group_id' in response
|
|
1084
|
-
? response
|
|
1085
|
-
: (isJsonObject(response.payload) ? response.payload : response);
|
|
1086
|
-
const groupId = payload.group_id;
|
|
1087
|
-
const epoch = payload.epoch;
|
|
1088
|
-
const groupSecretB64 = payload.group_secret;
|
|
1089
|
-
const commitment = payload.commitment;
|
|
1090
|
-
const memberAids = payload.member_aids ?? [];
|
|
1091
|
-
const logger = opts?.logger;
|
|
1092
|
-
logger?.debug(`handleKeyResponse enter: group=${String(groupId ?? '')}, epoch=${String(epoch ?? '')}, responder=${String(payload.responder_aid ?? '')}, aid=${aid}, strict=${!!opts?.strict}`);
|
|
1093
|
-
if (!groupId || epoch == null || !groupSecretB64 || !commitment) {
|
|
1094
|
-
logger?.warn(`handleKeyResponse rejected: missing required fields group=${String(groupId ?? '')} epoch=${String(epoch ?? '')} has_secret=${!!groupSecretB64} has_commitment=${!!commitment}`);
|
|
1095
|
-
return false;
|
|
1096
|
-
}
|
|
1097
|
-
const expected = opts?.expectedRequest ?? null;
|
|
1098
|
-
if (expected) {
|
|
1099
|
-
if (payload.requester_aid !== aid) {
|
|
1100
|
-
logger?.warn(`handleKeyResponse rejected: requester_aid mismatch group=${groupId} epoch=${epoch} got=${String(payload.requester_aid)} expected=${aid}`);
|
|
1101
|
-
return false;
|
|
1102
|
-
}
|
|
1103
|
-
const expectedResponder = String(expected._expected_responder_aid ?? '');
|
|
1104
|
-
if (expectedResponder && payload.responder_aid !== expectedResponder) {
|
|
1105
|
-
logger?.warn(`handleKeyResponse rejected: responder_aid mismatch group=${groupId} epoch=${epoch} got=${String(payload.responder_aid)} expected=${expectedResponder}`);
|
|
1106
|
-
return false;
|
|
1107
|
-
}
|
|
1108
|
-
if (payload.request_id !== expected.request_id) {
|
|
1109
|
-
logger?.warn(`handleKeyResponse rejected: request_id mismatch group=${groupId} epoch=${epoch}`);
|
|
1110
|
-
return false;
|
|
1111
|
-
}
|
|
1112
|
-
if (payload.group_id !== expected.group_id) {
|
|
1113
|
-
logger?.warn(`handleKeyResponse rejected: group_id mismatch with expected request`);
|
|
1114
|
-
return false;
|
|
1115
|
-
}
|
|
1116
|
-
if (Number(payload.epoch ?? 0) !== Number(expected.epoch ?? 0)) {
|
|
1117
|
-
logger?.warn(`handleKeyResponse rejected: epoch mismatch with expected request group=${groupId}`);
|
|
1118
|
-
return false;
|
|
1119
|
-
}
|
|
1120
|
-
}
|
|
1121
|
-
const responderAid = String(payload.responder_aid ?? '');
|
|
1122
|
-
if (opts?.strict) {
|
|
1123
|
-
if (!responderAid || !opts.responderCertPem) {
|
|
1124
|
-
logger?.warn(`handleKeyResponse rejected (strict): missing responder aid or cert group=${groupId} epoch=${epoch}`);
|
|
1125
|
-
return false;
|
|
1126
|
-
}
|
|
1127
|
-
if ((opts.currentMembers?.length ?? 0) > 0 && !opts.currentMembers.includes(responderAid)) {
|
|
1128
|
-
logger?.warn(`handleKeyResponse rejected (strict): responder not in current members group=${groupId} epoch=${epoch} responder=${responderAid}`);
|
|
1129
|
-
return false;
|
|
1130
|
-
}
|
|
1131
|
-
if (!verifyGroupKeyResponseSignature(payload, opts.responderCertPem)) {
|
|
1132
|
-
logger?.warn(`handleKeyResponse rejected (strict): response signature verification failed group=${groupId} epoch=${epoch} responder=${responderAid}`);
|
|
1133
|
-
return false;
|
|
1134
|
-
}
|
|
1135
|
-
}
|
|
1136
|
-
else if (opts?.responderCertPem && payload.response_signature) {
|
|
1137
|
-
if (!verifyGroupKeyResponseSignature(payload, opts.responderCertPem)) {
|
|
1138
|
-
logger?.warn(`handleKeyResponse rejected: response signature verification failed group=${groupId} epoch=${epoch} responder=${responderAid}`);
|
|
1139
|
-
return false;
|
|
1140
|
-
}
|
|
1141
|
-
}
|
|
1142
|
-
const groupSecret = Buffer.from(groupSecretB64, 'base64');
|
|
1143
|
-
if (!verifyMembershipCommitment(commitment, memberAids, epoch, groupId, aid, groupSecret)) {
|
|
1144
|
-
logger?.warn(`handleKeyResponse rejected: commitment verification failed group=${groupId} epoch=${epoch}`);
|
|
1145
|
-
return false;
|
|
1146
|
-
}
|
|
1147
|
-
const manifest = isJsonObject(payload.manifest) ? payload.manifest : null;
|
|
1148
|
-
if (manifest) {
|
|
1149
|
-
if (manifest.group_id !== groupId || manifest.epoch !== epoch) {
|
|
1150
|
-
logger?.warn(`handleKeyResponse rejected: manifest group_id/epoch mismatch group=${groupId} epoch=${epoch}`);
|
|
1151
|
-
return false;
|
|
1152
|
-
}
|
|
1153
|
-
const manifestMembers = Array.isArray(manifest.member_aids)
|
|
1154
|
-
? manifest.member_aids.map((item) => String(item ?? '').trim()).filter(Boolean).sort()
|
|
1155
|
-
: [];
|
|
1156
|
-
const payloadMembers = memberAids.map((item) => String(item ?? '').trim()).filter(Boolean).sort();
|
|
1157
|
-
if (manifestMembers.length > 0 && manifestMembers.join('\n') !== payloadMembers.join('\n')) {
|
|
1158
|
-
logger?.warn(`handleKeyResponse rejected: manifest members mismatch payload members group=${groupId} epoch=${epoch}`);
|
|
1159
|
-
return false;
|
|
1160
|
-
}
|
|
1161
|
-
}
|
|
1162
|
-
const incomingChain = typeof payload.epoch_chain === 'string' ? payload.epoch_chain : undefined;
|
|
1163
|
-
const rotationId = typeof payload.rotation_id === 'string' ? payload.rotation_id : '';
|
|
1164
|
-
const chainAssessment = assessIncomingEpochChain(keystore, aid, groupId, epoch, commitment, incomingChain, rotationId, String(payload.distributed_by ?? payload.rotator_aid ?? payload.responder_aid ?? ''), 'key_response', opts?.logger);
|
|
1165
|
-
if (!chainAssessment.ok) {
|
|
1166
|
-
logger?.warn(`handleKeyResponse rejected: epoch chain assessment failed group=${groupId} epoch=${epoch} reason=${chainAssessment.reason ?? ''}`);
|
|
1167
|
-
return false;
|
|
1168
|
-
}
|
|
1169
|
-
return storeGroupSecretEpoch(keystore, aid, groupId, epoch, groupSecret, commitment, memberAids, incomingChain, rotationId, chainAssessment.unverified, chainAssessment.reason);
|
|
1170
|
-
}
|
|
1171
|
-
// ── GroupE2EEManager 类 ───────────────────────────────────────
|
|
1172
|
-
export class GroupE2EEManager {
|
|
1173
|
-
_identityFn;
|
|
1174
|
-
_keystore;
|
|
1175
|
-
_replayGuard;
|
|
1176
|
-
_requestThrottle;
|
|
1177
|
-
_responseThrottle;
|
|
1178
|
-
_senderCertResolver;
|
|
1179
|
-
_initiatorCertResolver;
|
|
1180
|
-
_pendingKeyRequests = new Map();
|
|
1181
|
-
_logger;
|
|
1182
|
-
constructor(opts) {
|
|
1183
|
-
this._identityFn = opts.identityFn;
|
|
1184
|
-
this._keystore = opts.keystore;
|
|
1185
|
-
this._replayGuard = new GroupReplayGuard();
|
|
1186
|
-
this._requestThrottle = new GroupKeyRequestThrottle(opts.requestCooldown ?? 30);
|
|
1187
|
-
this._responseThrottle = new GroupKeyRequestThrottle(opts.responseCooldown ?? 30);
|
|
1188
|
-
this._senderCertResolver = opts.senderCertResolver ?? null;
|
|
1189
|
-
this._initiatorCertResolver = opts.initiatorCertResolver ?? null;
|
|
1190
|
-
this._logger = opts.logger ?? _noopLogger;
|
|
1191
|
-
}
|
|
1192
|
-
// ── 密钥管理 ──────────────────────────────────────────
|
|
1193
|
-
/** 用当前身份私钥签名 manifest */
|
|
1194
|
-
_signManifest(manifest) {
|
|
1195
|
-
const identity = this._identityFn();
|
|
1196
|
-
const pkPem = identity.private_key_pem;
|
|
1197
|
-
if (!pkPem)
|
|
1198
|
-
return manifest;
|
|
1199
|
-
return signMembershipManifest(manifest, pkPem);
|
|
1200
|
-
}
|
|
1201
|
-
/** 创建首个 epoch。返回 {epoch, commitment, distributions: [{to, payload}]} */
|
|
1202
|
-
createEpoch(groupId, memberAids) {
|
|
1203
|
-
const aid = this._currentAid();
|
|
1204
|
-
this._logger.debug(`create first epoch: groupId=${groupId}, members=${memberAids.length}, aid=${aid}`);
|
|
1205
|
-
const gs = generateGroupSecret();
|
|
1206
|
-
const epoch = 1;
|
|
1207
|
-
const commitment = computeMembershipCommitment(memberAids, epoch, groupId, gs);
|
|
1208
|
-
const epochChain = computeEpochChain(null, epoch, commitment, aid);
|
|
1209
|
-
storeGroupSecret(this._keystore, aid, groupId, epoch, gs, commitment, memberAids, epochChain);
|
|
1210
|
-
const manifest = this._signManifest(buildMembershipManifest(groupId, epoch, null, memberAids, { initiatorAid: aid }));
|
|
1211
|
-
const distPayload = buildKeyDistribution(groupId, epoch, gs, memberAids, aid, manifest, epochChain);
|
|
1212
|
-
this._logger.debug(`create first epoch success: groupId=${groupId}, epoch=${epoch}`);
|
|
1213
|
-
return {
|
|
1214
|
-
epoch,
|
|
1215
|
-
commitment,
|
|
1216
|
-
distributions: memberAids
|
|
1217
|
-
.filter(m => m !== aid)
|
|
1218
|
-
.map(m => ({ to: m, payload: distPayload })),
|
|
1219
|
-
};
|
|
1220
|
-
}
|
|
1221
|
-
/** 轮换 epoch(踢人/退出后调用) */
|
|
1222
|
-
rotateEpoch(groupId, memberAids) {
|
|
1223
|
-
const aid = this._currentAid();
|
|
1224
|
-
const current = loadGroupSecret(this._keystore, aid, groupId);
|
|
1225
|
-
const prevEpoch = current ? Number(current.epoch) : null;
|
|
1226
|
-
const newEpoch = (prevEpoch ?? 0) + 1;
|
|
1227
|
-
this._logger.debug(`rotateEpoch start: groupId=${groupId}, prevEpoch=${prevEpoch}, newEpoch=${newEpoch}, members=${memberAids.length}`);
|
|
1228
|
-
const gs = generateGroupSecret();
|
|
1229
|
-
const commitment = computeMembershipCommitment(memberAids, newEpoch, groupId, gs);
|
|
1230
|
-
const prevChain = current?.epoch_chain ?? null;
|
|
1231
|
-
const epochChain = computeEpochChain(prevChain, newEpoch, commitment, aid);
|
|
1232
|
-
const stored = storeGroupSecret(this._keystore, aid, groupId, newEpoch, gs, commitment, memberAids, epochChain);
|
|
1233
|
-
if (!stored) {
|
|
1234
|
-
this._logger.warn(`rotateEpoch failed (epoch already exists or newer): groupId=${groupId}, newEpoch=${newEpoch}`);
|
|
1235
|
-
throw new Error(`group ${groupId} epoch ${newEpoch} secret already exists or is newer; abort distribution`);
|
|
1236
|
-
}
|
|
1237
|
-
const manifest = this._signManifest(buildMembershipManifest(groupId, newEpoch, prevEpoch, memberAids, { initiatorAid: aid }));
|
|
1238
|
-
const distPayload = buildKeyDistribution(groupId, newEpoch, gs, memberAids, aid, manifest, epochChain);
|
|
1239
|
-
this._logger.debug(`rotateEpoch success: groupId=${groupId}, newEpoch=${newEpoch}`);
|
|
1240
|
-
return {
|
|
1241
|
-
epoch: newEpoch,
|
|
1242
|
-
commitment,
|
|
1243
|
-
distributions: memberAids
|
|
1244
|
-
.filter(m => m !== aid)
|
|
1245
|
-
.map(m => ({ to: m, payload: distPayload })),
|
|
1246
|
-
};
|
|
1247
|
-
}
|
|
1248
|
-
/** 指定目标 epoch 号轮换(配合服务端 CAS 使用) */
|
|
1249
|
-
rotateEpochTo(groupId, newEpoch, memberAids, opts) {
|
|
1250
|
-
const aid = this._currentAid();
|
|
1251
|
-
this._logger.debug(`rotateEpochTo start: groupId=${groupId}, newEpoch=${newEpoch}, members=${memberAids.length}`);
|
|
1252
|
-
const current = loadGroupSecret(this._keystore, aid, groupId, newEpoch - 1)
|
|
1253
|
-
?? loadGroupSecret(this._keystore, aid, groupId);
|
|
1254
|
-
const gs = generateGroupSecret();
|
|
1255
|
-
const commitment = computeMembershipCommitment(memberAids, newEpoch, groupId, gs);
|
|
1256
|
-
let prevChain = current?.epoch_chain ?? null;
|
|
1257
|
-
if (!prevChain && opts?.prevChainHint) {
|
|
1258
|
-
prevChain = opts.prevChainHint;
|
|
1259
|
-
}
|
|
1260
|
-
const epochChain = computeEpochChain(prevChain, newEpoch, commitment, aid);
|
|
1261
|
-
const rotationId = opts?.rotationId ?? '';
|
|
1262
|
-
const stored = storeGroupSecret(this._keystore, aid, groupId, newEpoch, gs, commitment, memberAids, epochChain, rotationId);
|
|
1263
|
-
if (!stored) {
|
|
1264
|
-
this._logger.warn(`rotateEpochTo failed (epoch already exists or newer): groupId=${groupId}, newEpoch=${newEpoch}`);
|
|
1265
|
-
throw new Error(`group ${groupId} epoch ${newEpoch} secret already exists or is newer; abort distribution`);
|
|
1266
|
-
}
|
|
1267
|
-
const manifest = this._signManifest(buildMembershipManifest(groupId, newEpoch, newEpoch - 1, memberAids, { initiatorAid: aid }));
|
|
1268
|
-
const distPayload = buildKeyDistribution(groupId, newEpoch, gs, memberAids, aid, manifest, epochChain);
|
|
1269
|
-
if (rotationId) {
|
|
1270
|
-
distPayload.rotation_id = rotationId;
|
|
1271
|
-
}
|
|
1272
|
-
this._logger.debug(`rotateEpochTo success: groupId=${groupId}, newEpoch=${newEpoch}, rotationId=${rotationId}`);
|
|
1273
|
-
return {
|
|
1274
|
-
epoch: newEpoch,
|
|
1275
|
-
commitment,
|
|
1276
|
-
distributions: memberAids
|
|
1277
|
-
.filter(m => m !== aid)
|
|
1278
|
-
.map(m => ({ to: m, payload: distPayload })),
|
|
1279
|
-
};
|
|
1280
|
-
}
|
|
1281
|
-
discardPendingSecret(groupId, epoch, rotationId) {
|
|
1282
|
-
return discardPendingGroupSecret(this._keystore, this._currentAid(), groupId, epoch, rotationId);
|
|
1283
|
-
}
|
|
1284
|
-
/** 加密群消息(含发送方签名)。无密钥时抛 E2EEGroupSecretMissingError。 */
|
|
1285
|
-
encrypt(groupId, payload, opts) {
|
|
1286
|
-
const aid = this._currentAid();
|
|
1287
|
-
this._logger.debug(`group message encrypt start: groupId=${groupId}, aid=${aid}`);
|
|
1288
|
-
const secretData = loadGroupSecret(this._keystore, aid, groupId);
|
|
1289
|
-
if (!secretData) {
|
|
1290
|
-
this._logger.error(`group message encrypt failed: missing group secret, groupId=${groupId}`);
|
|
1291
|
-
throw new E2EEGroupSecretMissingError(`no group secret for ${groupId}`);
|
|
1292
|
-
}
|
|
1293
|
-
const identity = this._identityFn();
|
|
1294
|
-
const senderPkPem = identity ? identity.private_key_pem ?? null : null;
|
|
1295
|
-
// TS-017: 签名失败必须阻止发送,不允许发出无签名的群消息
|
|
1296
|
-
if (!senderPkPem) {
|
|
1297
|
-
throw new E2EEError('sender identity private key unavailable for group message signing');
|
|
1298
|
-
}
|
|
1299
|
-
const senderCertPem = identity ? identity.cert ?? null : null;
|
|
1300
|
-
const result = encryptGroupMessage(groupId, secretData.epoch, secretData.secret, payload, {
|
|
1301
|
-
fromAid: aid,
|
|
1302
|
-
messageId: opts?.messageId ?? `gm-${crypto.randomUUID()}`,
|
|
1303
|
-
timestamp: opts?.timestamp ?? Date.now(),
|
|
1304
|
-
senderPrivateKeyPem: senderPkPem,
|
|
1305
|
-
senderCertPem,
|
|
1306
|
-
protectedHeaders: opts?.protectedHeaders ?? opts?.protected_headers ?? opts?.headers,
|
|
1307
|
-
context: opts?.context ?? null,
|
|
1308
|
-
});
|
|
1309
|
-
this._logger.debug(`group message encrypt success: groupId=${groupId}, epoch=${secretData.epoch}`);
|
|
1310
|
-
return result;
|
|
1311
|
-
}
|
|
1312
|
-
/** 使用指定 epoch 加密群消息。 */
|
|
1313
|
-
encryptWithEpoch(groupId, epoch, payload, opts) {
|
|
1314
|
-
const aid = this._currentAid();
|
|
1315
|
-
const secretData = loadGroupSecret(this._keystore, aid, groupId, epoch);
|
|
1316
|
-
if (!secretData) {
|
|
1317
|
-
throw new E2EEGroupSecretMissingError(`no group secret for ${groupId} epoch ${epoch}`);
|
|
1318
|
-
}
|
|
1319
|
-
const identity = this._identityFn();
|
|
1320
|
-
const senderPkPem = identity ? identity.private_key_pem ?? null : null;
|
|
1321
|
-
if (!senderPkPem) {
|
|
1322
|
-
throw new E2EEError('sender identity private key unavailable for group message signing');
|
|
1323
|
-
}
|
|
1324
|
-
const senderCertPem = identity ? identity.cert ?? null : null;
|
|
1325
|
-
return encryptGroupMessage(groupId, secretData.epoch, secretData.secret, payload, {
|
|
1326
|
-
fromAid: aid,
|
|
1327
|
-
messageId: opts?.messageId ?? `gm-${crypto.randomUUID()}`,
|
|
1328
|
-
timestamp: opts?.timestamp ?? Date.now(),
|
|
1329
|
-
senderPrivateKeyPem: senderPkPem,
|
|
1330
|
-
senderCertPem,
|
|
1331
|
-
protectedHeaders: opts?.protectedHeaders ?? opts?.protected_headers ?? opts?.headers,
|
|
1332
|
-
context: opts?.context ?? null,
|
|
1333
|
-
});
|
|
1334
|
-
}
|
|
1335
|
-
/** 解密单条群消息。内置防重放 + 发送方验签。 */
|
|
1336
|
-
decrypt(message, opts) {
|
|
1337
|
-
const payload = isJsonObject(message.payload) ? message.payload : null;
|
|
1338
|
-
if (payload === null || payload.type !== 'e2ee.group_encrypted') {
|
|
1339
|
-
return message;
|
|
1340
|
-
}
|
|
1341
|
-
const groupId = message.group_id || '';
|
|
1342
|
-
const sender = message.from || message.sender_aid || '';
|
|
1343
|
-
this._logger.debug(`group message decrypt start: groupId=${groupId}, from=${sender}`);
|
|
1344
|
-
// 防重放预检:优先使用 AAD 内 message_id
|
|
1345
|
-
const aad = isJsonObject(payload.aad) ? payload.aad : undefined;
|
|
1346
|
-
const aadMsgId = aad ? (aad.message_id ?? '') : '';
|
|
1347
|
-
const msgId = aadMsgId || message.message_id || '';
|
|
1348
|
-
if (!opts?.skipReplay && groupId && sender && msgId) {
|
|
1349
|
-
if (this._replayGuard.isSeen(groupId, sender, msgId)) {
|
|
1350
|
-
this._logger.debug(`group message replay blocked: groupId=${groupId}, from=${sender}, mid=${msgId}`);
|
|
1351
|
-
// 返回原消息(不含 e2ee 字段),调用方可通过缺失 e2ee 识别 replay
|
|
1352
|
-
return message;
|
|
1353
|
-
}
|
|
1354
|
-
}
|
|
1355
|
-
// 解析发送方证书(零信任:无证书则拒绝)
|
|
1356
|
-
let senderCertPem = null;
|
|
1357
|
-
if (this._senderCertResolver && sender) {
|
|
1358
|
-
senderCertPem = this._senderCertResolver(sender);
|
|
1359
|
-
}
|
|
1360
|
-
if (!senderCertPem) {
|
|
1361
|
-
this._logger.warn(`group message decrypt rejected: sender cert unavailable, groupId=${groupId}, from=${sender}`);
|
|
1362
|
-
return null;
|
|
1363
|
-
}
|
|
1364
|
-
const allSecrets = loadAllGroupSecrets(this._keystore, this._currentAid(), groupId);
|
|
1365
|
-
if (allSecrets.size === 0) {
|
|
1366
|
-
this._logger.warn(`group message decrypt failed: no group secret, groupId=${groupId}`);
|
|
1367
|
-
return null;
|
|
1368
|
-
}
|
|
1369
|
-
const result = decryptGroupMessage(message, allSecrets, senderCertPem, { logger: this._logger });
|
|
1370
|
-
// 解密成功后记录防重放
|
|
1371
|
-
if (result != null) {
|
|
1372
|
-
const finalMsgId = aadMsgId || message.message_id || '';
|
|
1373
|
-
if (!opts?.skipReplay && groupId && sender && finalMsgId) {
|
|
1374
|
-
this._replayGuard.record(groupId, sender, finalMsgId);
|
|
1375
|
-
}
|
|
1376
|
-
this._logger.debug(`group message decrypt success: groupId=${groupId}, from=${sender}, mid=${msgId}`);
|
|
1377
|
-
}
|
|
1378
|
-
else {
|
|
1379
|
-
this._logger.warn(`group message decrypt failed: groupId=${groupId}, from=${sender}, mid=${msgId}`);
|
|
1380
|
-
}
|
|
1381
|
-
return result;
|
|
1382
|
-
}
|
|
1383
|
-
/** 批量解密 */
|
|
1384
|
-
decryptBatch(messages, opts) {
|
|
1385
|
-
return messages.map(m => this.decrypt(m, opts) ?? m);
|
|
1386
|
-
}
|
|
1387
|
-
// ── 密钥协议消息处理 ──────────────────────────────────
|
|
1388
|
-
/**
|
|
1389
|
-
* 处理已解密的 P2P 密钥消息。
|
|
1390
|
-
* 返回 "distribution"/"request"/"response" 表示已成功处理。
|
|
1391
|
-
* 返回 "distribution_rejected"/"response_rejected" 表示被拒绝。
|
|
1392
|
-
* 返回 null 表示不是密钥消息。
|
|
1393
|
-
*/
|
|
1394
|
-
handleIncoming(payload) {
|
|
1395
|
-
const msgType = payload.type || '';
|
|
1396
|
-
const aid = this._currentAid();
|
|
1397
|
-
if (msgType === 'e2ee.group_key_distribution') {
|
|
1398
|
-
const groupId = payload.group_id || '';
|
|
1399
|
-
const epoch = payload.epoch;
|
|
1400
|
-
const distributedBy = payload.distributed_by || '';
|
|
1401
|
-
this._logger.debug(`received key distribution: groupId=${groupId}, epoch=${epoch}, from=${distributedBy}`);
|
|
1402
|
-
let initiatorCert = null;
|
|
1403
|
-
if (this._initiatorCertResolver && distributedBy) {
|
|
1404
|
-
initiatorCert = this._initiatorCertResolver(distributedBy);
|
|
1405
|
-
}
|
|
1406
|
-
const ok = handleKeyDistribution(payload, this._keystore, aid, initiatorCert, this._logger);
|
|
1407
|
-
if (ok) {
|
|
1408
|
-
this._logger.debug(`key distribution handled successfully: groupId=${groupId}, epoch=${epoch}`);
|
|
1409
|
-
}
|
|
1410
|
-
else {
|
|
1411
|
-
this._logger.warn(`key distribution rejected: groupId=${groupId}, epoch=${epoch}, from=${distributedBy}`);
|
|
1412
|
-
}
|
|
1413
|
-
return ok ? 'distribution' : 'distribution_rejected';
|
|
1414
|
-
}
|
|
1415
|
-
if (msgType === 'e2ee.group_key_response') {
|
|
1416
|
-
const groupId = payload.group_id || '';
|
|
1417
|
-
const epoch = payload.epoch;
|
|
1418
|
-
this._logger.debug(`received key response: groupId=${groupId}, epoch=${epoch}`);
|
|
1419
|
-
const pendingKey = `${String(payload.group_id ?? '')}:${String(payload.epoch ?? '')}:${String(payload.request_id ?? '')}`;
|
|
1420
|
-
const expected = this._pendingKeyRequests.get(pendingKey) ?? null;
|
|
1421
|
-
if (expected === null) {
|
|
1422
|
-
this._logger.warn(`key response rejected (no matching request): groupId=${groupId}, epoch=${epoch}`);
|
|
1423
|
-
return 'response_rejected';
|
|
1424
|
-
}
|
|
1425
|
-
const responderAid = String(payload.responder_aid ?? '');
|
|
1426
|
-
const responderCertPem = responderAid && this._initiatorCertResolver
|
|
1427
|
-
? this._initiatorCertResolver(responderAid)
|
|
1428
|
-
: null;
|
|
1429
|
-
const ok = handleKeyResponse(payload, this._keystore, aid, {
|
|
1430
|
-
expectedRequest: expected,
|
|
1431
|
-
responderCertPem,
|
|
1432
|
-
currentMembers: this.getMemberAids(String(payload.group_id ?? '')),
|
|
1433
|
-
strict: true,
|
|
1434
|
-
logger: this._logger,
|
|
1435
|
-
});
|
|
1436
|
-
if (ok && expected)
|
|
1437
|
-
this._pendingKeyRequests.delete(pendingKey);
|
|
1438
|
-
if (ok) {
|
|
1439
|
-
this._logger.debug(`key response handled successfully: groupId=${groupId}, epoch=${epoch}, responder=${responderAid}`);
|
|
1440
|
-
}
|
|
1441
|
-
else {
|
|
1442
|
-
this._logger.warn(`key response verification failed: groupId=${groupId}, epoch=${epoch}, responder=${responderAid}`);
|
|
1443
|
-
}
|
|
1444
|
-
return ok ? 'response' : 'response_rejected';
|
|
1445
|
-
}
|
|
1446
|
-
if (msgType === 'e2ee.group_key_request') {
|
|
1447
|
-
const groupId = payload.group_id || '';
|
|
1448
|
-
const requester = payload.requester_aid || '';
|
|
1449
|
-
this._logger.debug(`received key request: groupId=${groupId}, requester=${requester}, epoch=${payload.epoch}`);
|
|
1450
|
-
return 'request';
|
|
1451
|
-
}
|
|
1452
|
-
return null;
|
|
1453
|
-
}
|
|
1454
|
-
/** 构建恢复请求。返回 {to, payload} 或 null(限流/无目标)。 */
|
|
1455
|
-
buildRecoveryRequest(groupId, epoch, senderAid) {
|
|
1456
|
-
const aid = this._currentAid();
|
|
1457
|
-
if (!this._requestThrottle.allow(`request:${groupId}:${epoch}`)) {
|
|
1458
|
-
return null;
|
|
1459
|
-
}
|
|
1460
|
-
let candidates = [];
|
|
1461
|
-
const secretData = loadGroupSecret(this._keystore, aid, groupId);
|
|
1462
|
-
if (secretData && secretData.member_aids?.length) {
|
|
1463
|
-
candidates = secretData.member_aids.filter(m => m !== aid);
|
|
1464
|
-
}
|
|
1465
|
-
if (candidates.length === 0 && senderAid && senderAid !== aid) {
|
|
1466
|
-
candidates = [senderAid];
|
|
1467
|
-
}
|
|
1468
|
-
if (candidates.length === 0)
|
|
1469
|
-
return null;
|
|
1470
|
-
const payload = buildKeyRequest(groupId, epoch, aid);
|
|
1471
|
-
this.rememberKeyRequest(payload, candidates[0]);
|
|
1472
|
-
return { to: candidates[0], payload };
|
|
1473
|
-
}
|
|
1474
|
-
rememberKeyRequest(payload, expectedResponderAid) {
|
|
1475
|
-
if (payload.type !== 'e2ee.group_key_request')
|
|
1476
|
-
return;
|
|
1477
|
-
const requestId = String(payload.request_id ?? '');
|
|
1478
|
-
if (!requestId)
|
|
1479
|
-
return;
|
|
1480
|
-
this._pendingKeyRequests.set(`${String(payload.group_id ?? '')}:${String(payload.epoch ?? '')}:${requestId}`, expectedResponderAid ? { ...payload, _expected_responder_aid: expectedResponderAid } : { ...payload });
|
|
1481
|
-
}
|
|
1482
|
-
/** 处理密钥请求。返回响应 payload(受频率限制 + 成员资格验证)。 */
|
|
1483
|
-
handleKeyRequestMsg(requestPayload, members) {
|
|
1484
|
-
const requester = requestPayload.requester_aid || '';
|
|
1485
|
-
const groupId = requestPayload.group_id || '';
|
|
1486
|
-
if (!requester || !groupId)
|
|
1487
|
-
return null;
|
|
1488
|
-
// 成员资格验证
|
|
1489
|
-
if (!members.includes(requester)) {
|
|
1490
|
-
this._logger.warn(`key request rejected (not a member): groupId=${groupId}, requester=${requester}`);
|
|
1491
|
-
return null;
|
|
1492
|
-
}
|
|
1493
|
-
if (!this._responseThrottle.allow(`response:${groupId}:${requester}`)) {
|
|
1494
|
-
this._logger.debug(`key request throttled: groupId=${groupId}, requester=${requester}`);
|
|
1495
|
-
return null;
|
|
1496
|
-
}
|
|
1497
|
-
const identity = this._identityFn();
|
|
1498
|
-
const privateKeyPem = identity.private_key_pem;
|
|
1499
|
-
if (!privateKeyPem) {
|
|
1500
|
-
this._logger.error(`key request handling failed: missing private key, groupId=${groupId}`);
|
|
1501
|
-
return null;
|
|
1502
|
-
}
|
|
1503
|
-
const response = handleKeyRequest(requestPayload, this._keystore, this._currentAid(), members, privateKeyPem, this._logger);
|
|
1504
|
-
if (response) {
|
|
1505
|
-
this._logger.debug(`key request responded: groupId=${groupId}, requester=${requester}, epoch=${requestPayload.epoch}`);
|
|
1506
|
-
}
|
|
1507
|
-
else {
|
|
1508
|
-
this._logger.warn(`key request handling failed (no local key or requester not in epoch members): groupId=${groupId}, requester=${requester}`);
|
|
1509
|
-
}
|
|
1510
|
-
return response;
|
|
1511
|
-
}
|
|
1512
|
-
// ── 状态查询 ──────────────────────────────────────────
|
|
1513
|
-
/** 检查是否有群组密钥 */
|
|
1514
|
-
hasSecret(groupId) {
|
|
1515
|
-
return loadGroupSecret(this._keystore, this._currentAid(), groupId) != null;
|
|
1516
|
-
}
|
|
1517
|
-
/** 获取当前 epoch */
|
|
1518
|
-
currentEpoch(groupId) {
|
|
1519
|
-
const s = loadGroupSecret(this._keystore, this._currentAid(), groupId);
|
|
1520
|
-
return s ? s.epoch : null;
|
|
1521
|
-
}
|
|
1522
|
-
/** 获取群组成员 AID 列表 */
|
|
1523
|
-
getMemberAids(groupId) {
|
|
1524
|
-
const s = loadGroupSecret(this._keystore, this._currentAid(), groupId);
|
|
1525
|
-
return s ? (s.member_aids ?? []) : [];
|
|
1526
|
-
}
|
|
1527
|
-
/** 加载群组密钥数据 */
|
|
1528
|
-
loadSecret(groupId, epoch) {
|
|
1529
|
-
const result = loadGroupSecret(this._keystore, this._currentAid(), groupId, epoch);
|
|
1530
|
-
if (result) {
|
|
1531
|
-
this._logger.debug(`load group secret success: groupId=${groupId}, epoch=${result.epoch}`);
|
|
1532
|
-
}
|
|
1533
|
-
else {
|
|
1534
|
-
this._logger.debug(`load group secret not found: groupId=${groupId}, requestedEpoch=${epoch ?? 'latest'}`);
|
|
1535
|
-
}
|
|
1536
|
-
return result;
|
|
1537
|
-
}
|
|
1538
|
-
/** 清理过期的旧 epoch */
|
|
1539
|
-
cleanup(groupId, retentionSeconds = OLD_EPOCH_RETENTION_SECONDS) {
|
|
1540
|
-
cleanupOldEpochs(this._keystore, this._currentAid(), groupId, retentionSeconds);
|
|
1541
|
-
}
|
|
1542
|
-
/** 清理过期缓存(replay guard 等),供外部定时调用 */
|
|
1543
|
-
cleanExpiredCaches() {
|
|
1544
|
-
this._replayGuard.trim();
|
|
1545
|
-
}
|
|
1546
|
-
/** 删除群组的所有本地状态(群组解散时使用) */
|
|
1547
|
-
removeGroup(groupId) {
|
|
1548
|
-
try {
|
|
1549
|
-
deleteGroupSecret(this._keystore, this._currentAid(), groupId);
|
|
1550
|
-
}
|
|
1551
|
-
catch {
|
|
1552
|
-
// keystore 不支持 delete 时忽略(降级方案已在 deleteKeyStoreGroupState 中处理)
|
|
1553
|
-
}
|
|
1554
|
-
}
|
|
1555
|
-
/** 加载指定群组的所有 epoch 密钥。返回 Map<epoch, Buffer>。 */
|
|
1556
|
-
loadAllSecrets(groupId) {
|
|
1557
|
-
return loadAllGroupSecrets(this._keystore, this._currentAid(), groupId);
|
|
1558
|
-
}
|
|
1559
|
-
_currentAid() {
|
|
1560
|
-
const identity = this._identityFn();
|
|
1561
|
-
const aid = identity.aid;
|
|
1562
|
-
if (!aid)
|
|
1563
|
-
throw new E2EEError('AID unavailable');
|
|
1564
|
-
return String(aid);
|
|
1565
|
-
}
|
|
1566
|
-
}
|
|
1567
|
-
//# sourceMappingURL=e2ee-group.js.map
|