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