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