@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.
Files changed (82) hide show
  1. package/README.md +604 -0
  2. package/dist/auth.d.ts +150 -0
  3. package/dist/auth.d.ts.map +1 -0
  4. package/dist/auth.js +1388 -0
  5. package/dist/auth.js.map +1 -0
  6. package/dist/certs/root.d.ts +2 -0
  7. package/dist/certs/root.d.ts.map +1 -0
  8. package/dist/certs/root.js +16 -0
  9. package/dist/certs/root.js.map +1 -0
  10. package/dist/client.d.ts +341 -0
  11. package/dist/client.d.ts.map +1 -0
  12. package/dist/client.js +4061 -0
  13. package/dist/client.js.map +1 -0
  14. package/dist/config.d.ts +37 -0
  15. package/dist/config.d.ts.map +1 -0
  16. package/dist/config.js +85 -0
  17. package/dist/config.js.map +1 -0
  18. package/dist/crypto.d.ts +41 -0
  19. package/dist/crypto.d.ts.map +1 -0
  20. package/dist/crypto.js +132 -0
  21. package/dist/crypto.js.map +1 -0
  22. package/dist/discovery.d.ts +20 -0
  23. package/dist/discovery.d.ts.map +1 -0
  24. package/dist/discovery.js +75 -0
  25. package/dist/discovery.js.map +1 -0
  26. package/dist/e2ee-group.d.ts +221 -0
  27. package/dist/e2ee-group.d.ts.map +1 -0
  28. package/dist/e2ee-group.js +1174 -0
  29. package/dist/e2ee-group.js.map +1 -0
  30. package/dist/e2ee.d.ts +187 -0
  31. package/dist/e2ee.d.ts.map +1 -0
  32. package/dist/e2ee.js +1067 -0
  33. package/dist/e2ee.js.map +1 -0
  34. package/dist/errors.d.ts +118 -0
  35. package/dist/errors.d.ts.map +1 -0
  36. package/dist/errors.js +250 -0
  37. package/dist/errors.js.map +1 -0
  38. package/dist/events.d.ts +33 -0
  39. package/dist/events.d.ts.map +1 -0
  40. package/dist/events.js +68 -0
  41. package/dist/events.js.map +1 -0
  42. package/dist/index.d.ts +22 -0
  43. package/dist/index.d.ts.map +1 -0
  44. package/dist/index.js +32 -0
  45. package/dist/index.js.map +1 -0
  46. package/dist/keystore/index.d.ts +88 -0
  47. package/dist/keystore/index.d.ts.map +1 -0
  48. package/dist/keystore/index.js +3 -0
  49. package/dist/keystore/index.js.map +1 -0
  50. package/dist/keystore/indexeddb.d.ts +94 -0
  51. package/dist/keystore/indexeddb.d.ts.map +1 -0
  52. package/dist/keystore/indexeddb.js +1434 -0
  53. package/dist/keystore/indexeddb.js.map +1 -0
  54. package/dist/namespaces/auth.d.ts +52 -0
  55. package/dist/namespaces/auth.d.ts.map +1 -0
  56. package/dist/namespaces/auth.js +237 -0
  57. package/dist/namespaces/auth.js.map +1 -0
  58. package/dist/namespaces/custody.d.ts +48 -0
  59. package/dist/namespaces/custody.d.ts.map +1 -0
  60. package/dist/namespaces/custody.js +230 -0
  61. package/dist/namespaces/custody.js.map +1 -0
  62. package/dist/secret-store/index.d.ts +20 -0
  63. package/dist/secret-store/index.d.ts.map +1 -0
  64. package/dist/secret-store/index.js +12 -0
  65. package/dist/secret-store/index.js.map +1 -0
  66. package/dist/secret-store/indexeddb-store.d.ts +22 -0
  67. package/dist/secret-store/indexeddb-store.d.ts.map +1 -0
  68. package/dist/secret-store/indexeddb-store.js +133 -0
  69. package/dist/secret-store/indexeddb-store.js.map +1 -0
  70. package/dist/seq-tracker.d.ts +30 -0
  71. package/dist/seq-tracker.d.ts.map +1 -0
  72. package/dist/seq-tracker.js +219 -0
  73. package/dist/seq-tracker.js.map +1 -0
  74. package/dist/transport.d.ts +45 -0
  75. package/dist/transport.d.ts.map +1 -0
  76. package/dist/transport.js +251 -0
  77. package/dist/transport.js.map +1 -0
  78. package/dist/types.d.ts +171 -0
  79. package/dist/types.d.ts.map +1 -0
  80. package/dist/types.js +10 -0
  81. package/dist/types.js.map +1 -0
  82. package/package.json +37 -0
package/dist/e2ee.js ADDED
@@ -0,0 +1,1067 @@
1
+ // ── E2EEManager(P2P 端到端加密 — 浏览器 SubtleCrypto 实现)──
2
+ // 所有密码学操作均为异步(SubtleCrypto API 要求)
3
+ import { E2EEDecryptFailedError, E2EEError } from './errors.js';
4
+ import { uint8ToBase64, base64ToUint8, pemToArrayBuffer, p1363ToDer, toArrayBuffer, toBufferSource } from './crypto.js';
5
+ /** 加密套件标识 */
6
+ export const SUITE = 'P256_HKDF_SHA256_AES_256_GCM';
7
+ /** 加密模式 */
8
+ export const MODE_PREKEY_ECDH_V2 = 'prekey_ecdh_v2';
9
+ export const MODE_LONG_TERM_KEY = 'long_term_key';
10
+ /** AAD 字段定义(P2P) */
11
+ export const AAD_FIELDS_OFFLINE = [
12
+ 'from', 'to', 'message_id', 'timestamp',
13
+ 'encryption_mode', 'suite', 'ephemeral_public_key',
14
+ 'recipient_cert_fingerprint', 'sender_cert_fingerprint',
15
+ 'prekey_id',
16
+ ];
17
+ /** AAD 匹配字段(解密时校验,不含 timestamp) */
18
+ export const AAD_MATCH_FIELDS_OFFLINE = [
19
+ 'from', 'to', 'message_id',
20
+ 'encryption_mode', 'suite', 'ephemeral_public_key',
21
+ 'recipient_cert_fingerprint', 'sender_cert_fingerprint',
22
+ 'prekey_id',
23
+ ];
24
+ /** prekey 私钥本地保留时间(秒) */
25
+ export const PREKEY_RETENTION_SECONDS = 7 * 24 * 3600;
26
+ export const PREKEY_MIN_KEEP_COUNT = 7;
27
+ function prekeyCreatedMarker(prekeyData) {
28
+ return Number(prekeyData.created_at ?? prekeyData.updated_at ?? prekeyData.expires_at ?? 0);
29
+ }
30
+ function latestPrekeyIds(prekeys, keepLatest) {
31
+ if (keepLatest <= 0)
32
+ return new Set();
33
+ return new Set(Object.entries(prekeys)
34
+ .filter(([, data]) => typeof data === 'object' && data !== null)
35
+ .sort((left, right) => {
36
+ const markerDiff = prekeyCreatedMarker(right[1]) - prekeyCreatedMarker(left[1]);
37
+ if (markerDiff !== 0)
38
+ return markerDiff;
39
+ return right[0].localeCompare(left[0]);
40
+ })
41
+ .slice(0, keepLatest)
42
+ .map(([prekeyId]) => prekeyId));
43
+ }
44
+ async function loadKeyStorePrekeys(keystore, aid, deviceId = '') {
45
+ const normalizedDeviceId = String(deviceId ?? '').trim();
46
+ if (typeof keystore.loadE2EEPrekeys === 'function') {
47
+ return ((await keystore.loadE2EEPrekeys(aid, normalizedDeviceId)) ?? {});
48
+ }
49
+ throw new Error('keystore 缺少 loadE2EEPrekeys 方法');
50
+ }
51
+ async function saveKeyStorePrekey(keystore, aid, deviceId, prekeyId, prekeyData) {
52
+ const normalizedDeviceId = String(deviceId ?? '').trim();
53
+ if (typeof keystore.saveE2EEPrekey === 'function') {
54
+ await keystore.saveE2EEPrekey(aid, prekeyId, prekeyData, normalizedDeviceId);
55
+ return;
56
+ }
57
+ throw new Error(`keystore ${keystore.constructor?.name ?? 'unknown'} 缺少 saveE2EEPrekey 方法`);
58
+ }
59
+ async function cleanupKeyStorePrekeys(keystore, aid, deviceId, cutoffMs, keepLatest = PREKEY_MIN_KEEP_COUNT) {
60
+ const normalizedDeviceId = String(deviceId ?? '').trim();
61
+ if (typeof keystore.cleanupE2EEPrekeys === 'function') {
62
+ return (await keystore.cleanupE2EEPrekeys(aid, cutoffMs, keepLatest, normalizedDeviceId)) ?? [];
63
+ }
64
+ throw new Error(`keystore ${keystore.constructor?.name ?? 'unknown'} 缺少 cleanupE2EEPrekeys 方法`);
65
+ }
66
+ // ── 工具函数 ────────────────────────────────────────────────
67
+ const _encoder = new TextEncoder();
68
+ const _decoder = new TextDecoder();
69
+ /** 拼接多个 Uint8Array */
70
+ function concatBytes(...arrays) {
71
+ let total = 0;
72
+ for (const a of arrays)
73
+ total += a.byteLength;
74
+ const result = new Uint8Array(total);
75
+ let offset = 0;
76
+ for (const a of arrays) {
77
+ result.set(a, offset);
78
+ offset += a.byteLength;
79
+ }
80
+ return result;
81
+ }
82
+ /** DER 签名转 IEEE P1363 格式(用于 SubtleCrypto 验签) */
83
+ function derToP1363(der, coordLen = 32) {
84
+ // 跳过 SEQUENCE 头
85
+ if (der[0] !== 0x30)
86
+ throw new E2EEError('无效 DER 签名: 缺少 SEQUENCE 标签');
87
+ let pos = 2; // 跳过 30 + length
88
+ // 读取 r
89
+ if (der[pos] !== 0x02)
90
+ throw new E2EEError('无效 DER 签名: 缺少 INTEGER 标签 (r)');
91
+ pos++;
92
+ const rLen = der[pos++];
93
+ let rBytes = der.slice(pos, pos + rLen);
94
+ pos += rLen;
95
+ // 读取 s
96
+ if (der[pos] !== 0x02)
97
+ throw new E2EEError('无效 DER 签名: 缺少 INTEGER 标签 (s)');
98
+ pos++;
99
+ const sLen = der[pos++];
100
+ let sBytes = der.slice(pos, pos + sLen);
101
+ // 去掉前导 0x00(ASN.1 有符号整数的填充)
102
+ if (rBytes.length > coordLen && rBytes[0] === 0)
103
+ rBytes = rBytes.slice(1);
104
+ if (sBytes.length > coordLen && sBytes[0] === 0)
105
+ sBytes = sBytes.slice(1);
106
+ // 左填充到 coordLen
107
+ const result = new Uint8Array(coordLen * 2);
108
+ result.set(rBytes, coordLen - rBytes.length);
109
+ result.set(sBytes, coordLen * 2 - sBytes.length);
110
+ return result;
111
+ }
112
+ /** AAD 序列化(排序键、紧凑 JSON) */
113
+ function aadBytesOffline(aad) {
114
+ const obj = {};
115
+ for (const field of AAD_FIELDS_OFFLINE) {
116
+ obj[field] = aad[field] ?? null;
117
+ }
118
+ // 按键排序
119
+ const sorted = {};
120
+ for (const key of Object.keys(obj).sort()) {
121
+ sorted[key] = obj[key];
122
+ }
123
+ return _encoder.encode(JSON.stringify(sorted));
124
+ }
125
+ /** AAD 匹配检查(解密时校验) */
126
+ function aadMatchesOffline(expected, actual) {
127
+ for (const field of AAD_MATCH_FIELDS_OFFLINE) {
128
+ // 宽松比较:JSON 序列化后对比
129
+ if (JSON.stringify(expected[field] ?? null) !== JSON.stringify(actual[field] ?? null)) {
130
+ return false;
131
+ }
132
+ }
133
+ return true;
134
+ }
135
+ /** 从 PEM 证书中提取 SPKI 公钥字节(解析 X.509 DER 结构) */
136
+ function extractSpkiFromCertPem(certPem) {
137
+ const der = pemToArrayBuffer(certPem);
138
+ return extractSpkiFromCertDer(new Uint8Array(der));
139
+ }
140
+ /**
141
+ * 从 X.509 DER 证书中提取 SubjectPublicKeyInfo 字段。
142
+ * 简化解析:仅处理 P-256 ECDSA 证书(AUN 协议限定)。
143
+ */
144
+ function extractSpkiFromCertDer(certDer) {
145
+ // X.509 结构: SEQUENCE { tbsCertificate, signatureAlgorithm, signatureValue }
146
+ // tbsCertificate: SEQUENCE { version, serialNumber, signature, issuer, validity, subject, subjectPublicKeyInfo, ... }
147
+ // 我们需要提取 subjectPublicKeyInfo 段
148
+ // 递归查找 SPKI:P-256 SPKI 的 OID 前缀为 30 59 30 13 06 07 2a 86 48 ce 3d 02 01
149
+ // 搜索模式:找到 P-256 的 AlgorithmIdentifier OID
150
+ const ecOid = new Uint8Array([0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01]);
151
+ const p256Oid = new Uint8Array([0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07]);
152
+ // 在 DER 中搜索 EC public key OID
153
+ for (let i = 0; i < certDer.length - ecOid.length; i++) {
154
+ let match = true;
155
+ for (let j = 0; j < ecOid.length; j++) {
156
+ if (certDer[i + j] !== ecOid[j]) {
157
+ match = false;
158
+ break;
159
+ }
160
+ }
161
+ if (!match)
162
+ continue;
163
+ // 找到 EC OID,往前回溯找 SEQUENCE 起始
164
+ // SPKI 格式: 30 <len> 30 <algLen> <ecOid> <p256Oid> 03 <bitStringLen> 00 <pubKeyBytes>
165
+ // 向前查找最近的 0x30(SEQUENCE)
166
+ for (let back = 1; back <= 4; back++) {
167
+ const seqStart = i - back;
168
+ if (seqStart < 0)
169
+ continue;
170
+ if (certDer[seqStart] !== 0x30)
171
+ continue;
172
+ // 解析 SEQUENCE 长度
173
+ const seqLen = parseDerLength(certDer, seqStart + 1);
174
+ if (seqLen === null)
175
+ continue;
176
+ const totalLen = 1 + seqLen.lenBytes + seqLen.value;
177
+ // SPKI 应包含完整的 AlgorithmIdentifier + BIT STRING
178
+ if (totalLen < 50 || totalLen > 120)
179
+ continue;
180
+ // 验证这确实是 SPKI(内部应包含 BIT STRING tag 0x03)
181
+ const spkiCandidate = certDer.slice(seqStart, seqStart + totalLen);
182
+ // 检查末尾附近有 BIT STRING
183
+ let hasBitString = false;
184
+ for (let k = 20; k < spkiCandidate.length - 10; k++) {
185
+ if (spkiCandidate[k] === 0x03 && spkiCandidate[k + 2] === 0x00) {
186
+ hasBitString = true;
187
+ break;
188
+ }
189
+ }
190
+ if (hasBitString) {
191
+ return spkiCandidate.buffer.slice(spkiCandidate.byteOffset, spkiCandidate.byteOffset + spkiCandidate.byteLength);
192
+ }
193
+ }
194
+ }
195
+ throw new E2EEError('无法从证书中提取 SPKI 公钥');
196
+ }
197
+ /** 解析 DER 长度字段 */
198
+ function parseDerLength(data, offset) {
199
+ if (offset >= data.length)
200
+ return null;
201
+ const first = data[offset];
202
+ if (first < 0x80) {
203
+ return { value: first, lenBytes: 1 };
204
+ }
205
+ const numBytes = first & 0x7f;
206
+ if (numBytes === 0 || numBytes > 4)
207
+ return null;
208
+ let value = 0;
209
+ for (let i = 0; i < numBytes; i++) {
210
+ if (offset + 1 + i >= data.length)
211
+ return null;
212
+ value = (value << 8) | data[offset + 1 + i];
213
+ }
214
+ return { value, lenBytes: 1 + numBytes };
215
+ }
216
+ /** 计算 SPKI 公钥的 SHA-256 指纹 */
217
+ async function fingerprintSpki(spkiBytes) {
218
+ const hash = await crypto.subtle.digest('SHA-256', spkiBytes);
219
+ const hex = Array.from(new Uint8Array(hash)).map(b => b.toString(16).padStart(2, '0')).join('');
220
+ return `sha256:${hex}`;
221
+ }
222
+ /** 从 PEM 证书计算证书 SHA-256 指纹 */
223
+ async function fingerprintCertPem(certPem) {
224
+ return certificateSha256Fingerprint(certPem);
225
+ }
226
+ /** 从 PEM 证书计算证书 SHA-256 指纹 */
227
+ async function certificateSha256Fingerprint(certPem) {
228
+ const der = pemToArrayBuffer(certPem);
229
+ const hash = await crypto.subtle.digest('SHA-256', der);
230
+ const hex = Array.from(new Uint8Array(hash)).map(b => b.toString(16).padStart(2, '0')).join('');
231
+ return `sha256:${hex}`;
232
+ }
233
+ /** 从 SPKI DER base64 计算公钥指纹 */
234
+ async function fingerprintDerB64(derB64) {
235
+ const der = base64ToUint8(derB64);
236
+ return fingerprintSpki(toArrayBuffer(der));
237
+ }
238
+ /** 导入 PEM 证书公钥为 ECDSA CryptoKey */
239
+ async function importCertPublicKeyEcdsa(certPem) {
240
+ const spki = extractSpkiFromCertPem(certPem);
241
+ return crypto.subtle.importKey('spki', spki, { name: 'ECDSA', namedCurve: 'P-256' }, true, ['verify']);
242
+ }
243
+ /** 导入 PEM 证书公钥为 ECDH CryptoKey */
244
+ async function importCertPublicKeyEcdh(certPem) {
245
+ const spki = extractSpkiFromCertPem(certPem);
246
+ return crypto.subtle.importKey('spki', spki, { name: 'ECDH', namedCurve: 'P-256' }, true, []);
247
+ }
248
+ /** 导入 SPKI DER base64 为 ECDH CryptoKey */
249
+ async function importSpkiDerB64Ecdh(derB64) {
250
+ const der = base64ToUint8(derB64);
251
+ return crypto.subtle.importKey('spki', toArrayBuffer(der), { name: 'ECDH', namedCurve: 'P-256' }, true, []);
252
+ }
253
+ /** 导入 SPKI DER base64 为 ECDSA CryptoKey */
254
+ async function importSpkiDerB64Ecdsa(derB64) {
255
+ const der = base64ToUint8(derB64);
256
+ return crypto.subtle.importKey('spki', toArrayBuffer(der), { name: 'ECDSA', namedCurve: 'P-256' }, true, ['verify']);
257
+ }
258
+ /** 导入 PEM 私钥为 ECDSA CryptoKey */
259
+ /**
260
+ * ECDSA 私钥导入缓存:避免每次签名都重复调用 crypto.subtle.importKey。
261
+ * 缓存键为 PEM 字符串本身,identity 变更时新 PEM 自然不命中旧缓存。
262
+ */
263
+ const _ecdsaKeyCache = new Map();
264
+ async function importPrivateKeyEcdsa(pem) {
265
+ const cached = _ecdsaKeyCache.get(pem);
266
+ if (cached)
267
+ return cached;
268
+ const pkcs8 = pemToArrayBuffer(pem);
269
+ const key = await crypto.subtle.importKey('pkcs8', pkcs8, { name: 'ECDSA', namedCurve: 'P-256' }, true, ['sign']);
270
+ _ecdsaKeyCache.set(pem, key);
271
+ return key;
272
+ }
273
+ /** 导入 PEM 私钥为 ECDH CryptoKey */
274
+ async function importPrivateKeyEcdh(pem) {
275
+ const pkcs8 = pemToArrayBuffer(pem);
276
+ return crypto.subtle.importKey('pkcs8', pkcs8, { name: 'ECDH', namedCurve: 'P-256' }, true, ['deriveBits']);
277
+ }
278
+ /** ECDH 派生共享密钥(256 位) */
279
+ async function ecdhDeriveBits(privateKey, publicKey) {
280
+ const bits = await crypto.subtle.deriveBits({ name: 'ECDH', public: publicKey }, privateKey, 256);
281
+ return new Uint8Array(bits);
282
+ }
283
+ /** HKDF 派生密钥(256 位) */
284
+ async function hkdfDerive(ikm, info) {
285
+ const ikmKey = await crypto.subtle.importKey('raw', toBufferSource(ikm), 'HKDF', false, ['deriveBits']);
286
+ const bits = await crypto.subtle.deriveBits({
287
+ name: 'HKDF',
288
+ hash: 'SHA-256',
289
+ salt: toBufferSource(new Uint8Array(0)),
290
+ info: toBufferSource(_encoder.encode(info)),
291
+ }, ikmKey, 256);
292
+ return new Uint8Array(bits);
293
+ }
294
+ /** AES-GCM 加密,返回 [ciphertext, tag](SubtleCrypto 将 tag 附加到末尾) */
295
+ async function aesGcmEncrypt(key, nonce, plaintext, aad) {
296
+ const aesKey = await crypto.subtle.importKey('raw', toBufferSource(key), 'AES-GCM', false, ['encrypt']);
297
+ const ct = await crypto.subtle.encrypt({ name: 'AES-GCM', iv: toBufferSource(nonce), additionalData: toBufferSource(aad), tagLength: 128 }, aesKey, toBufferSource(plaintext));
298
+ const arr = new Uint8Array(ct);
299
+ // SubtleCrypto 将 16 字节 tag 附加到 ciphertext 末尾
300
+ return [arr.slice(0, -16), arr.slice(-16)];
301
+ }
302
+ /** AES-GCM 解密 */
303
+ async function aesGcmDecrypt(key, nonce, ciphertext, tag, aad) {
304
+ const aesKey = await crypto.subtle.importKey('raw', toBufferSource(key), 'AES-GCM', false, ['decrypt']);
305
+ // SubtleCrypto 要求 ciphertext + tag 拼接传入
306
+ const combined = concatBytes(ciphertext, tag);
307
+ const pt = await crypto.subtle.decrypt({ name: 'AES-GCM', iv: toBufferSource(nonce), additionalData: toBufferSource(aad), tagLength: 128 }, aesKey, toBufferSource(combined));
308
+ return new Uint8Array(pt);
309
+ }
310
+ /** ECDSA 签名(输出 DER 格式,兼容 Python/Go) */
311
+ async function ecdsaSignDer(privateKey, data) {
312
+ const sig = await crypto.subtle.sign({ name: 'ECDSA', hash: 'SHA-256' }, privateKey, toBufferSource(data));
313
+ // SubtleCrypto 输出 P1363 格式,转换为 DER
314
+ return p1363ToDer(new Uint8Array(sig));
315
+ }
316
+ /** ECDSA 验签(输入 DER 格式签名) */
317
+ async function ecdsaVerifyDer(publicKey, signature, data) {
318
+ // DER → P1363 用于 SubtleCrypto 验签
319
+ const p1363 = derToP1363(signature);
320
+ return crypto.subtle.verify({ name: 'ECDSA', hash: 'SHA-256' }, publicKey, toBufferSource(p1363), toBufferSource(data));
321
+ }
322
+ /** 生成 12 字节随机 nonce */
323
+ function randomNonce() {
324
+ const nonce = new Uint8Array(12);
325
+ crypto.getRandomValues(nonce);
326
+ return nonce;
327
+ }
328
+ /** 生成 UUID v4 */
329
+ function uuidV4() {
330
+ const bytes = new Uint8Array(16);
331
+ crypto.getRandomValues(bytes);
332
+ bytes[6] = (bytes[6] & 0x0f) | 0x40;
333
+ bytes[8] = (bytes[8] & 0x3f) | 0x80;
334
+ const hex = Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
335
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
336
+ }
337
+ /** 导入 X9.62 未压缩点为 ECDH CryptoKey(用于解密时导入临时公钥) */
338
+ async function importUncompressedPointEcdh(pointBytes) {
339
+ // 将未压缩点(0x04 || x || y)包装为 SPKI 格式
340
+ // P-256 SPKI = 固定头 + 未压缩点(65 字节)
341
+ const spkiHeader = new Uint8Array([
342
+ 0x30, 0x59, // SEQUENCE (89 bytes)
343
+ 0x30, 0x13, // SEQUENCE (19 bytes) - AlgorithmIdentifier
344
+ 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, // OID: 1.2.840.10045.2.1 (EC)
345
+ 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07, // OID: 1.2.840.10045.3.1.7 (P-256)
346
+ 0x03, 0x42, 0x00, // BIT STRING (66 bytes, 0 unused bits)
347
+ ]);
348
+ const spki = concatBytes(spkiHeader, pointBytes);
349
+ return crypto.subtle.importKey('spki', toArrayBuffer(spki), { name: 'ECDH', namedCurve: 'P-256' }, true, []);
350
+ }
351
+ // ── E2EEManager 主类 ────────────────────────────────────────
352
+ /**
353
+ * P2P 端到端加密管理器 — 浏览器 SubtleCrypto 实现。
354
+ *
355
+ * 加密策略: prekey_ecdh_v2(四路 ECDH)→ long_term_key(二路 ECDH)两层降级。
356
+ * I/O(获取 prekey、证书)由调用方(AUNClient)负责。
357
+ * 内置本地防重放(seen set),裸 WebSocket 开发者无需额外实现。
358
+ *
359
+ * 所有密码学操作均为异步(SubtleCrypto 要求)。
360
+ */
361
+ export class E2EEManager {
362
+ _identityFn;
363
+ _deviceIdFn;
364
+ _keystoreRef;
365
+ /** 本地防重放 seen set */
366
+ _seenMessages = new Map();
367
+ _seenMaxSize = 50000;
368
+ /** 对方 prekey 内存缓存 {peerAid: {prekey, expireAt}} */
369
+ _prekeyCache = new Map();
370
+ _prekeyCacheTtl;
371
+ /** 本地 prekey 私钥 PEM 内存缓存 {prekeyId: privateKeyPem} */
372
+ _localPrekeyCache = new Map();
373
+ /** 防重放时间窗口(秒) */
374
+ _replayWindowSeconds;
375
+ constructor(opts) {
376
+ this._identityFn = opts.identityFn;
377
+ this._deviceIdFn = opts.deviceIdFn ?? (() => '');
378
+ this._keystoreRef = opts.keystore;
379
+ this._prekeyCacheTtl = opts.prekeyCacheTtl ?? 3600;
380
+ this._replayWindowSeconds = opts.replayWindowSeconds ?? 300;
381
+ }
382
+ // ── Prekey 缓存 ──────────────────────────────────
383
+ /** 缓存对方的 prekey */
384
+ cachePrekey(peerAid, prekey) {
385
+ this._prekeyCache.set(peerAid, {
386
+ prekey,
387
+ expireAt: Date.now() / 1000 + this._prekeyCacheTtl,
388
+ });
389
+ }
390
+ /** 获取缓存的 prekey(过期返回 null) */
391
+ getCachedPrekey(peerAid) {
392
+ const cached = this._prekeyCache.get(peerAid);
393
+ if (!cached)
394
+ return null;
395
+ if (Date.now() / 1000 >= cached.expireAt) {
396
+ this._prekeyCache.delete(peerAid);
397
+ return null;
398
+ }
399
+ return cached.prekey;
400
+ }
401
+ /** 使 prekey 缓存失效 */
402
+ invalidatePrekeyCache(peerAid) {
403
+ this._prekeyCache.delete(peerAid);
404
+ }
405
+ // ── 便利方法 ──────────────────────────────────────
406
+ /**
407
+ * 加密消息(便利方法)。
408
+ * 调用方负责提前获取 peerCertPem 和 prekey(可选)。
409
+ */
410
+ async encryptMessage(toAid, payload, opts) {
411
+ const messageId = opts.messageId ?? uuidV4();
412
+ const timestamp = opts.timestamp ?? Date.now();
413
+ return this.encryptOutbound(toAid, payload, {
414
+ peerCertPem: opts.peerCertPem,
415
+ prekey: opts.prekey ?? null,
416
+ messageId,
417
+ timestamp,
418
+ });
419
+ }
420
+ // ── 加密 ──────────────────────────────────────────
421
+ /**
422
+ * 加密出站消息:有 prekey → prekey_ecdh_v2(四路 ECDH),无 prekey → long_term_key。
423
+ *
424
+ * 返回 [envelope, resultInfo],resultInfo 包含加密状态详情。
425
+ * prekey 传入时自动缓存;传入 null 时自动查缓存。
426
+ */
427
+ async encryptOutbound(peerAid, payload, opts) {
428
+ let prekey = opts.prekey ?? null;
429
+ // 传入 prekey → 缓存;传入 null → 查缓存
430
+ if (prekey !== null) {
431
+ this.cachePrekey(peerAid, prekey);
432
+ }
433
+ else {
434
+ prekey = this.getCachedPrekey(peerAid);
435
+ }
436
+ if (prekey) {
437
+ try {
438
+ const envelope = await this._encryptWithPrekey(peerAid, payload, prekey, opts.peerCertPem, opts.messageId, opts.timestamp);
439
+ return [envelope, {
440
+ encrypted: true,
441
+ forward_secrecy: true,
442
+ mode: MODE_PREKEY_ECDH_V2,
443
+ degraded: false,
444
+ }];
445
+ }
446
+ catch (exc) {
447
+ console.warn('prekey 加密失败,降级到 long_term_key(无前向保密):', exc);
448
+ }
449
+ }
450
+ const envelope = await this._encryptWithLongTermKey(peerAid, payload, opts.peerCertPem, opts.messageId, opts.timestamp);
451
+ const degraded = prekey !== null; // 有 prekey 但失败了才算降级
452
+ return [envelope, {
453
+ encrypted: true,
454
+ forward_secrecy: false,
455
+ mode: MODE_LONG_TERM_KEY,
456
+ degraded,
457
+ degradation_reason: degraded ? 'prekey_encrypt_failed' : 'no_prekey_available',
458
+ }];
459
+ }
460
+ /**
461
+ * 使用对方 prekey 加密(prekey_ecdh_v2 模式,四路 ECDH + 发送方签名)
462
+ *
463
+ * 四路 ECDH:
464
+ * DH1 = ECDH(ephemeral, peer_prekey)
465
+ * DH2 = ECDH(ephemeral, peer_identity)
466
+ * DH3 = ECDH(sender_identity, peer_prekey) ← 绑定发送方身份
467
+ * DH4 = ECDH(sender_identity, peer_identity) ← 双方身份互绑
468
+ */
469
+ async _encryptWithPrekey(peerAid, payload, prekey, peerCertPem, messageId, timestamp) {
470
+ // 导入对方 identity 公钥(ECDSA 用于验签,ECDH 用于密钥交换)
471
+ const peerIdentityEcdsa = await importCertPublicKeyEcdsa(peerCertPem);
472
+ const peerIdentityEcdh = await importCertPublicKeyEcdh(peerCertPem);
473
+ const expectedCertFingerprint = String(prekey.cert_fingerprint ?? '').trim().toLowerCase();
474
+ if (expectedCertFingerprint) {
475
+ const actualCertFingerprint = await certificateSha256Fingerprint(peerCertPem);
476
+ if (actualCertFingerprint !== expectedCertFingerprint) {
477
+ throw new E2EEError('prekey cert fingerprint mismatch');
478
+ }
479
+ }
480
+ // 验证 prekey 签名
481
+ const prekeyId = prekey.prekey_id;
482
+ const prekeyPubB64 = prekey.public_key;
483
+ const createdAt = prekey.created_at;
484
+ let signData;
485
+ if (createdAt !== undefined) {
486
+ signData = _encoder.encode(`${prekeyId}|${prekeyPubB64}|${createdAt}`);
487
+ }
488
+ else {
489
+ signData = _encoder.encode(`${prekeyId}|${prekeyPubB64}`);
490
+ }
491
+ const sigBytes = base64ToUint8(prekey.signature);
492
+ const sigValid = await ecdsaVerifyDer(peerIdentityEcdsa, sigBytes, signData);
493
+ if (!sigValid) {
494
+ throw new E2EEError('prekey 签名验证失败');
495
+ }
496
+ // 导入对方 prekey 公钥(ECDH)
497
+ const peerPrekeyEcdh = await importSpkiDerB64Ecdh(prekeyPubB64);
498
+ // 加载发送方 identity 私钥
499
+ const senderIdentityEcdhKey = await this._loadSenderIdentityPrivateEcdh();
500
+ const senderSignKey = await this._loadSenderIdentityPrivateEcdsa();
501
+ // 生成临时 ECDH 密钥对
502
+ const ephemeral = await crypto.subtle.generateKey({ name: 'ECDH', namedCurve: 'P-256' }, true, ['deriveBits']);
503
+ // 导出临时公钥为 X9.62 未压缩点
504
+ const ephRaw = await crypto.subtle.exportKey('raw', ephemeral.publicKey);
505
+ const ephPublicBytes = new Uint8Array(ephRaw);
506
+ // 四路 ECDH
507
+ const dh1 = await ecdhDeriveBits(ephemeral.privateKey, peerPrekeyEcdh);
508
+ const dh2 = await ecdhDeriveBits(ephemeral.privateKey, peerIdentityEcdh);
509
+ const dh3 = await ecdhDeriveBits(senderIdentityEcdhKey, peerPrekeyEcdh);
510
+ const dh4 = await ecdhDeriveBits(senderIdentityEcdhKey, peerIdentityEcdh);
511
+ const combined = concatBytes(dh1, dh2, dh3, dh4);
512
+ // HKDF
513
+ const messageKey = await hkdfDerive(combined, `aun-prekey-v2:${prekeyId}`);
514
+ // AES-GCM 加密
515
+ const plaintext = _encoder.encode(JSON.stringify(payload));
516
+ const nonce = randomNonce();
517
+ const senderFingerprint = await this._localCertSha256Fingerprint() || await this._localIdentityFingerprint();
518
+ const recipientFingerprint = await fingerprintCertPem(peerCertPem);
519
+ const ephPkB64 = uint8ToBase64(ephPublicBytes);
520
+ const aad = {
521
+ from: this._currentAid(),
522
+ to: peerAid,
523
+ message_id: messageId,
524
+ timestamp,
525
+ encryption_mode: MODE_PREKEY_ECDH_V2,
526
+ suite: SUITE,
527
+ ephemeral_public_key: ephPkB64,
528
+ recipient_cert_fingerprint: recipientFingerprint,
529
+ sender_cert_fingerprint: senderFingerprint,
530
+ prekey_id: prekeyId,
531
+ };
532
+ const aadBytes = aadBytesOffline(aad);
533
+ const [ciphertext, tag] = await aesGcmEncrypt(messageKey, nonce, plaintext, aadBytes);
534
+ const envelope = {
535
+ type: 'e2ee.encrypted',
536
+ version: '1',
537
+ encryption_mode: MODE_PREKEY_ECDH_V2,
538
+ suite: SUITE,
539
+ prekey_id: prekeyId,
540
+ ephemeral_public_key: ephPkB64,
541
+ nonce: uint8ToBase64(nonce),
542
+ ciphertext: uint8ToBase64(ciphertext),
543
+ tag: uint8ToBase64(tag),
544
+ aad,
545
+ };
546
+ // 发送方签名:对 ciphertext + tag + aad_bytes 签名(不可否认性)
547
+ const signPayload = concatBytes(ciphertext, tag, aadBytes);
548
+ const sig = await ecdsaSignDer(senderSignKey, signPayload);
549
+ envelope.sender_signature = uint8ToBase64(sig);
550
+ envelope.sender_cert_fingerprint = senderFingerprint;
551
+ return envelope;
552
+ }
553
+ /**
554
+ * 使用 2DH 加密(long_term_key 模式 + 发送方签名)
555
+ *
556
+ * 2DH:
557
+ * DH1 = ECDH(ephemeral, peer_identity) ← 前向保密(每消息)
558
+ * DH2 = ECDH(sender_identity, peer_identity) ← 绑定双方身份
559
+ */
560
+ async _encryptWithLongTermKey(peerAid, payload, peerCertPem, messageId, timestamp) {
561
+ const peerIdentityEcdh = await importCertPublicKeyEcdh(peerCertPem);
562
+ const senderIdentityEcdhKey = await this._loadSenderIdentityPrivateEcdh();
563
+ const senderSignKey = await this._loadSenderIdentityPrivateEcdsa();
564
+ // 生成临时 ECDH 密钥对
565
+ const ephemeral = await crypto.subtle.generateKey({ name: 'ECDH', namedCurve: 'P-256' }, true, ['deriveBits']);
566
+ const ephRaw = await crypto.subtle.exportKey('raw', ephemeral.publicKey);
567
+ const ephPublicBytes = new Uint8Array(ephRaw);
568
+ // 2DH + HKDF
569
+ const dh1 = await ecdhDeriveBits(ephemeral.privateKey, peerIdentityEcdh);
570
+ const dh2 = await ecdhDeriveBits(senderIdentityEcdhKey, peerIdentityEcdh);
571
+ const combined = concatBytes(dh1, dh2);
572
+ const messageKey = await hkdfDerive(combined, 'aun-longterm-v2');
573
+ // AES-GCM 加密
574
+ const plaintext = _encoder.encode(JSON.stringify(payload));
575
+ const nonce = randomNonce();
576
+ const senderFingerprint = await this._localCertSha256Fingerprint() || await this._localIdentityFingerprint();
577
+ const recipientFingerprint = await fingerprintCertPem(peerCertPem);
578
+ const ephPkB64 = uint8ToBase64(ephPublicBytes);
579
+ const aad = {
580
+ from: this._currentAid(),
581
+ to: peerAid,
582
+ message_id: messageId,
583
+ timestamp,
584
+ encryption_mode: MODE_LONG_TERM_KEY,
585
+ suite: SUITE,
586
+ ephemeral_public_key: ephPkB64,
587
+ recipient_cert_fingerprint: recipientFingerprint,
588
+ sender_cert_fingerprint: senderFingerprint,
589
+ };
590
+ const aadBytes = aadBytesOffline(aad);
591
+ const [ciphertext, tag] = await aesGcmEncrypt(messageKey, nonce, plaintext, aadBytes);
592
+ const envelope = {
593
+ type: 'e2ee.encrypted',
594
+ version: '1',
595
+ encryption_mode: MODE_LONG_TERM_KEY,
596
+ suite: SUITE,
597
+ ephemeral_public_key: ephPkB64,
598
+ nonce: uint8ToBase64(nonce),
599
+ ciphertext: uint8ToBase64(ciphertext),
600
+ tag: uint8ToBase64(tag),
601
+ aad,
602
+ };
603
+ // 发送方签名(不可否认性)
604
+ const signPayload = concatBytes(ciphertext, tag, aadBytes);
605
+ const sig = await ecdsaSignDer(senderSignKey, signPayload);
606
+ envelope.sender_signature = uint8ToBase64(sig);
607
+ envelope.sender_cert_fingerprint = senderFingerprint;
608
+ return envelope;
609
+ }
610
+ // ── 解密 ──────────────────────────────────────────
611
+ /**
612
+ * 解密单条消息(内置本地防重放 + timestamp 窗口 + 发送方签名验证)。
613
+ *
614
+ * 返回解密后的 message 对象,或 null 表示失败/拒绝。
615
+ * 非加密消息原样返回。
616
+ *
617
+ * opts.skipReplay: 跳过防重放和 timestamp 窗口检查(用于 message.pull 场景)。
618
+ */
619
+ async decryptMessage(message, opts) {
620
+ const payload = message.payload;
621
+ if (!payload || typeof payload !== 'object')
622
+ return message;
623
+ if (payload.type !== 'e2ee.encrypted')
624
+ return message;
625
+ if (message.encrypted === false)
626
+ return message;
627
+ if (!this._shouldDecryptForCurrentAid(message, payload))
628
+ return message;
629
+ const skipReplay = opts?.skipReplay ?? false;
630
+ if (!skipReplay) {
631
+ // timestamp 窗口检查
632
+ const ts = (message.timestamp ?? payload.aad?.timestamp);
633
+ if (typeof ts === 'number' && this._replayWindowSeconds > 0) {
634
+ const nowMs = Date.now();
635
+ const diffS = Math.abs(nowMs - ts) / 1000;
636
+ if (diffS > this._replayWindowSeconds) {
637
+ console.warn(`消息 timestamp 超出窗口 (${Math.round(diffS)}s > ${this._replayWindowSeconds}s),拒绝: from=${message.from} mid=${message.message_id}`);
638
+ return null;
639
+ }
640
+ }
641
+ // 本地防重放:先检查,解密成功后再记录。
642
+ const messageIdVal = message.message_id;
643
+ const fromAid = message.from;
644
+ let seenKey = '';
645
+ if (messageIdVal && fromAid) {
646
+ seenKey = `${fromAid}:${messageIdVal}`;
647
+ if (this._seenMessages.has(seenKey))
648
+ return null;
649
+ }
650
+ const result = await this._decryptMessageInternal(message);
651
+ if (result !== null && seenKey) {
652
+ this._seenMessages.set(seenKey, true);
653
+ this._trimSeenSet();
654
+ }
655
+ return result;
656
+ }
657
+ return this._decryptMessageInternal(message);
658
+ }
659
+ /** 判断是否应该为当前 AID 解密(避免发送端回显消息误走解密) */
660
+ _shouldDecryptForCurrentAid(message, payload) {
661
+ if (String(message.direction ?? '').trim().toLowerCase() === 'outbound_sync') {
662
+ return true;
663
+ }
664
+ const currentAid = this._currentAid();
665
+ if (!currentAid)
666
+ return true;
667
+ const targetAid = (message.to
668
+ ?? payload.aad?.to
669
+ ?? payload.to);
670
+ if (!targetAid)
671
+ return true;
672
+ return String(targetAid) === String(currentAid);
673
+ }
674
+ /** 内部解密分发 */
675
+ async _decryptMessageInternal(message) {
676
+ const payload = message.payload;
677
+ // 验证发送方签名(适用于所有模式)
678
+ try {
679
+ await this._verifySenderSignature(payload, message);
680
+ }
681
+ catch (exc) {
682
+ console.warn('发送方签名验证失败:', exc);
683
+ return null;
684
+ }
685
+ const encryptionMode = payload.encryption_mode;
686
+ if (encryptionMode === MODE_PREKEY_ECDH_V2) {
687
+ return this._decryptMessagePrekeyV2(message);
688
+ }
689
+ else if (encryptionMode === MODE_LONG_TERM_KEY) {
690
+ return this._decryptMessageLongTerm(message);
691
+ }
692
+ else {
693
+ console.warn('不支持的加密模式:', encryptionMode);
694
+ return null;
695
+ }
696
+ }
697
+ /** 验证发送方签名 */
698
+ async _verifySenderSignature(payload, message) {
699
+ const senderSigB64 = payload.sender_signature;
700
+ if (!senderSigB64) {
701
+ throw new E2EEDecryptFailedError('sender_signature missing: 拒绝无发送方签名的消息');
702
+ }
703
+ // 获取发送方公钥
704
+ const fromAid = (message.from ?? payload.aad?.from);
705
+ if (!fromAid)
706
+ throw new E2EEDecryptFailedError('from_aid missing in message');
707
+ const senderCertFingerprint = String(payload.sender_cert_fingerprint ?? payload.aad?.sender_cert_fingerprint ?? '').trim().toLowerCase();
708
+ const senderCertPem = await this._getSenderCert(fromAid, senderCertFingerprint || undefined);
709
+ if (!senderCertPem)
710
+ throw new E2EEDecryptFailedError(`sender cert not found for ${fromAid}`);
711
+ const senderPubKey = await importCertPublicKeyEcdsa(senderCertPem);
712
+ // 重建签名载荷
713
+ const ciphertext = base64ToUint8(payload.ciphertext);
714
+ const tag = base64ToUint8(payload.tag);
715
+ const aad = payload.aad;
716
+ const aadBytes = aad ? aadBytesOffline(aad) : new Uint8Array(0);
717
+ const signPayload = concatBytes(ciphertext, tag, aadBytes);
718
+ const sigBytes = base64ToUint8(senderSigB64);
719
+ const valid = await ecdsaVerifyDer(senderPubKey, sigBytes, signPayload);
720
+ if (!valid) {
721
+ throw new E2EEDecryptFailedError('sender signature verification failed');
722
+ }
723
+ }
724
+ /** 从 keystore 获取发送方证书 PEM */
725
+ async _getSenderCert(aid, certFingerprint) {
726
+ const certPem = await this._keystoreRef.loadCert(aid, certFingerprint);
727
+ const normalized = String(certFingerprint ?? '').trim().toLowerCase();
728
+ if (!certPem)
729
+ return null;
730
+ if (!normalized)
731
+ return certPem;
732
+ const actualFingerprint = await certificateSha256Fingerprint(certPem);
733
+ return actualFingerprint === normalized ? certPem : null;
734
+ }
735
+ /** 解密 prekey_ecdh_v2 模式的消息(四路 ECDH) */
736
+ async _decryptMessagePrekeyV2(message) {
737
+ const payload = message.payload;
738
+ try {
739
+ const ephPublicBytes = base64ToUint8(payload.ephemeral_public_key);
740
+ const prekeyId = (payload.prekey_id ?? '');
741
+ const nonce = base64ToUint8(payload.nonce);
742
+ const ciphertext = base64ToUint8(payload.ciphertext);
743
+ const tag = base64ToUint8(payload.tag);
744
+ // 加载 prekey 私钥
745
+ const prekeyPrivatePem = await this._loadPrekeyPrivateKey(prekeyId);
746
+ if (!prekeyPrivatePem)
747
+ throw new E2EEError(`prekey not found: ${prekeyId}`);
748
+ const prekeyPrivateEcdh = await importPrivateKeyEcdh(prekeyPrivatePem);
749
+ // 加载接收方 identity 私钥
750
+ const myAid = this._currentAid();
751
+ if (!myAid)
752
+ throw new E2EEError('AID unavailable');
753
+ const keyPair = await this._keystoreRef.loadKeyPair(myAid);
754
+ if (!keyPair || !keyPair.private_key_pem)
755
+ throw new E2EEError('Identity private key not found');
756
+ const myIdentityEcdh = await importPrivateKeyEcdh(keyPair.private_key_pem);
757
+ // 获取发送方公钥(四路 ECDH 需要)
758
+ const fromAid = (message.from ?? payload.aad?.from);
759
+ const senderCertFingerprint = String(payload.sender_cert_fingerprint ?? payload.aad?.sender_cert_fingerprint ?? '').trim().toLowerCase();
760
+ const senderCertPem = await this._getSenderCert(fromAid, senderCertFingerprint || undefined);
761
+ if (!senderCertPem)
762
+ throw new E2EEError(`sender public key not found for ${fromAid}`);
763
+ const senderPubEcdh = await importCertPublicKeyEcdh(senderCertPem);
764
+ // 导入临时公钥
765
+ const ephPubKey = await importUncompressedPointEcdh(ephPublicBytes);
766
+ // 四路 ECDH + HKDF(接收方视角:prekey 和 identity 角色互换)
767
+ const dh1 = await ecdhDeriveBits(prekeyPrivateEcdh, ephPubKey);
768
+ const dh2 = await ecdhDeriveBits(myIdentityEcdh, ephPubKey);
769
+ const dh3 = await ecdhDeriveBits(prekeyPrivateEcdh, senderPubEcdh);
770
+ const dh4 = await ecdhDeriveBits(myIdentityEcdh, senderPubEcdh);
771
+ const combined = concatBytes(dh1, dh2, dh3, dh4);
772
+ const messageKey = await hkdfDerive(combined, `aun-prekey-v2:${prekeyId}`);
773
+ // 验证 AAD 并解密
774
+ const aad = payload.aad;
775
+ let aadBytes;
776
+ if (aad) {
777
+ const expectedAad = this._buildInboundAadOffline(message, payload);
778
+ if (!aadMatchesOffline(expectedAad, aad)) {
779
+ throw new E2EEDecryptFailedError('aad mismatch');
780
+ }
781
+ aadBytes = aadBytesOffline(aad);
782
+ }
783
+ else {
784
+ aadBytes = new Uint8Array(0);
785
+ }
786
+ const plaintext = await aesGcmDecrypt(messageKey, nonce, ciphertext, tag, aadBytes);
787
+ const decoded = JSON.parse(_decoder.decode(plaintext));
788
+ return {
789
+ ...message,
790
+ payload: decoded,
791
+ encrypted: true,
792
+ e2ee: {
793
+ encryption_mode: MODE_PREKEY_ECDH_V2,
794
+ suite: payload.suite ?? SUITE,
795
+ prekey_id: prekeyId,
796
+ },
797
+ };
798
+ }
799
+ catch (exc) {
800
+ if (exc instanceof E2EEError) {
801
+ console.warn('prekey_ecdh_v2 解密失败 (E2EE):', exc);
802
+ }
803
+ else {
804
+ console.warn('prekey_ecdh_v2 解密失败:', exc);
805
+ }
806
+ return null;
807
+ }
808
+ }
809
+ /** 解密 long_term_key 模式的消息(2DH) */
810
+ async _decryptMessageLongTerm(message) {
811
+ const payload = message.payload;
812
+ try {
813
+ const ephPublicBytes = base64ToUint8(payload.ephemeral_public_key);
814
+ const nonce = base64ToUint8(payload.nonce);
815
+ const ciphertext = base64ToUint8(payload.ciphertext);
816
+ const tag = base64ToUint8(payload.tag);
817
+ // 加载接收方 identity 私钥
818
+ const myAid = this._currentAid();
819
+ if (!myAid)
820
+ throw new E2EEError('AID unavailable');
821
+ const keyPair = await this._keystoreRef.loadKeyPair(myAid);
822
+ if (!keyPair || !keyPair.private_key_pem)
823
+ throw new E2EEError('Private key not found');
824
+ const myIdentityEcdh = await importPrivateKeyEcdh(keyPair.private_key_pem);
825
+ // 获取发送方公钥(2DH 需要)
826
+ const fromAid = (message.from ?? payload.aad?.from);
827
+ const senderCertFingerprint = String(payload.sender_cert_fingerprint ?? payload.aad?.sender_cert_fingerprint ?? '').trim().toLowerCase();
828
+ const senderCertPem = await this._getSenderCert(fromAid, senderCertFingerprint || undefined);
829
+ if (!senderCertPem)
830
+ throw new E2EEError(`sender public key not found for ${fromAid}`);
831
+ const senderPubEcdh = await importCertPublicKeyEcdh(senderCertPem);
832
+ // 导入临时公钥
833
+ const ephPubKey = await importUncompressedPointEcdh(ephPublicBytes);
834
+ // 2DH + HKDF
835
+ const dh1 = await ecdhDeriveBits(myIdentityEcdh, ephPubKey);
836
+ const dh2 = await ecdhDeriveBits(myIdentityEcdh, senderPubEcdh);
837
+ const combined = concatBytes(dh1, dh2);
838
+ const messageKey = await hkdfDerive(combined, 'aun-longterm-v2');
839
+ // 验证 AAD 并解密
840
+ const aad = payload.aad;
841
+ let aadBytes;
842
+ if (aad) {
843
+ const expectedAad = this._buildInboundAadOffline(message, payload);
844
+ if (!aadMatchesOffline(expectedAad, aad)) {
845
+ throw new E2EEDecryptFailedError('aad mismatch');
846
+ }
847
+ aadBytes = aadBytesOffline(aad);
848
+ }
849
+ else {
850
+ aadBytes = new Uint8Array(0);
851
+ }
852
+ const plaintext = await aesGcmDecrypt(messageKey, nonce, ciphertext, tag, aadBytes);
853
+ const decoded = JSON.parse(_decoder.decode(plaintext));
854
+ return {
855
+ ...message,
856
+ payload: decoded,
857
+ encrypted: true,
858
+ e2ee: {
859
+ encryption_mode: MODE_LONG_TERM_KEY,
860
+ suite: payload.suite,
861
+ },
862
+ };
863
+ }
864
+ catch (exc) {
865
+ if (exc instanceof E2EEError) {
866
+ console.warn('long_term_key 解密失败 (E2EE):', exc);
867
+ }
868
+ else {
869
+ console.warn('long_term_key 解密失败:', exc);
870
+ }
871
+ return null;
872
+ }
873
+ }
874
+ // ── AAD 工具 ─────────────────────────────────────
875
+ /** 构建解密时的期望 AAD(接收方视角) */
876
+ _buildInboundAadOffline(message, payload) {
877
+ const aad = payload.aad;
878
+ return {
879
+ from: message.from,
880
+ to: message.to,
881
+ message_id: message.message_id,
882
+ timestamp: message.timestamp,
883
+ encryption_mode: payload.encryption_mode,
884
+ suite: payload.suite ?? SUITE,
885
+ ephemeral_public_key: payload.ephemeral_public_key,
886
+ recipient_cert_fingerprint: aad?.recipient_cert_fingerprint,
887
+ sender_cert_fingerprint: payload.sender_cert_fingerprint ?? aad?.sender_cert_fingerprint,
888
+ prekey_id: payload.prekey_id ?? aad?.prekey_id,
889
+ };
890
+ }
891
+ // ── Prekey 生成 ──────────────────────────────────
892
+ /**
893
+ * 生成 prekey 材料并保存私钥到本地 keystore。
894
+ *
895
+ * 返回 { prekey_id, public_key, signature, created_at },可直接用于 RPC 上传。
896
+ */
897
+ async generatePrekey() {
898
+ const aid = this._currentAid();
899
+ if (!aid)
900
+ throw new E2EEError('AID unavailable for prekey generation');
901
+ const deviceId = this._currentDeviceId();
902
+ // 生成新 ECDH 密钥对(标记为 ECDH 用途)
903
+ const keyPair = await crypto.subtle.generateKey({ name: 'ECDH', namedCurve: 'P-256' }, true, ['deriveBits']);
904
+ // 导出 SPKI 公钥(DER 格式)
905
+ const spki = await crypto.subtle.exportKey('spki', keyPair.publicKey);
906
+ const publicKeyB64 = uint8ToBase64(new Uint8Array(spki));
907
+ // 导出 PKCS8 私钥(PEM 格式存储)
908
+ const pkcs8 = await crypto.subtle.exportKey('pkcs8', keyPair.privateKey);
909
+ const privateKeyPem = arrayBufferToPemLocal(pkcs8, 'PRIVATE KEY');
910
+ const prekeyId = uuidV4();
911
+ const nowMs = Date.now();
912
+ // 签名:prekey_id|public_key|created_at(绑定时间戳,防止旧 prekey 重放)
913
+ const signData = _encoder.encode(`${prekeyId}|${publicKeyB64}|${nowMs}`);
914
+ const senderSignKey = await this._loadSenderIdentityPrivateEcdsa();
915
+ const sig = await ecdsaSignDer(senderSignKey, signData);
916
+ const signatureB64 = uint8ToBase64(sig);
917
+ await saveKeyStorePrekey(this._keystoreRef, aid, deviceId, prekeyId, {
918
+ private_key_pem: privateKeyPem,
919
+ created_at: nowMs,
920
+ updated_at: nowMs,
921
+ });
922
+ // 内存缓存私钥 PEM
923
+ this._localPrekeyCache.set(prekeyId, privateKeyPem);
924
+ // 清理过期的旧 prekey
925
+ await this._cleanupExpiredPrekeys(aid, deviceId);
926
+ const result = {
927
+ prekey_id: prekeyId,
928
+ public_key: publicKeyB64,
929
+ signature: signatureB64,
930
+ created_at: nowMs,
931
+ };
932
+ const certFingerprint = await this._localCertSha256Fingerprint();
933
+ if (certFingerprint) {
934
+ result.cert_fingerprint = certFingerprint;
935
+ }
936
+ if (deviceId) {
937
+ result.device_id = deviceId;
938
+ }
939
+ return result;
940
+ }
941
+ /** 清理过期的本地 prekey 私钥 */
942
+ async _cleanupExpiredPrekeys(aid, deviceId) {
943
+ const nowMs = Date.now();
944
+ const cutoffMs = nowMs - PREKEY_RETENTION_SECONDS * 1000;
945
+ const expired = await cleanupKeyStorePrekeys(this._keystoreRef, aid, deviceId, cutoffMs, PREKEY_MIN_KEEP_COUNT);
946
+ if (expired.length > 0) {
947
+ for (const pid of expired) {
948
+ this._localPrekeyCache.delete(pid);
949
+ }
950
+ }
951
+ }
952
+ /** 从内存缓存或 keystore 加载 prekey 私钥 PEM */
953
+ async _loadPrekeyPrivateKey(prekeyId) {
954
+ // 优先从内存缓存获取
955
+ const cached = this._localPrekeyCache.get(prekeyId);
956
+ if (cached)
957
+ return cached;
958
+ const aid = this._currentAid();
959
+ if (!aid)
960
+ return null;
961
+ const prekeys = await loadKeyStorePrekeys(this._keystoreRef, aid, this._currentDeviceId());
962
+ const prekeyData = prekeys[prekeyId];
963
+ if (!prekeyData)
964
+ return null;
965
+ const pem = prekeyData.private_key_pem;
966
+ if (!pem)
967
+ return null;
968
+ // 回填内存缓存
969
+ this._localPrekeyCache.set(prekeyId, pem);
970
+ return pem;
971
+ }
972
+ // ── 内部工具 ──────────────────────────────────────
973
+ _currentAid() {
974
+ const identity = this._identityFn();
975
+ const aid = identity.aid;
976
+ return typeof aid === 'string' ? aid : null;
977
+ }
978
+ _currentDeviceId() {
979
+ try {
980
+ return String(this._deviceIdFn() ?? '').trim();
981
+ }
982
+ catch {
983
+ return '';
984
+ }
985
+ }
986
+ /** 加载发送方 identity 私钥(ECDH 用途) */
987
+ async _loadSenderIdentityPrivateEcdh() {
988
+ const identity = this._identityFn();
989
+ const pem = identity.private_key_pem;
990
+ if (!pem)
991
+ throw new E2EEError('sender identity private key unavailable');
992
+ return importPrivateKeyEcdh(pem);
993
+ }
994
+ /** 加载发送方 identity 私钥(ECDSA 签名用途) */
995
+ async _loadSenderIdentityPrivateEcdsa() {
996
+ const identity = this._identityFn();
997
+ const pem = identity.private_key_pem;
998
+ if (!pem)
999
+ throw new E2EEError('sender identity private key unavailable');
1000
+ return importPrivateKeyEcdsa(pem);
1001
+ }
1002
+ /** 获取本地 identity 指纹(优先证书 DER SHA-256,缺失时回退到公钥指纹) */
1003
+ async _localIdentityFingerprint() {
1004
+ const identity = this._identityFn();
1005
+ // 优先用证书指纹(与 PKI 一致)
1006
+ const certPem = identity.cert;
1007
+ if (certPem)
1008
+ return fingerprintCertPem(certPem);
1009
+ // 无证书时回退到公钥 SPKI 指纹
1010
+ const pubDerB64 = identity.public_key_der_b64;
1011
+ if (pubDerB64)
1012
+ return fingerprintDerB64(pubDerB64);
1013
+ // 从私钥导出公钥的 SPKI 指纹
1014
+ const pem = identity.private_key_pem;
1015
+ if (pem) {
1016
+ const pk = await crypto.subtle.importKey('pkcs8', pemToArrayBuffer(pem), { name: 'ECDSA', namedCurve: 'P-256' }, true, ['sign']);
1017
+ const jwk = await crypto.subtle.exportKey('jwk', pk);
1018
+ const pubJwk = { kty: jwk.kty, crv: jwk.crv, x: jwk.x, y: jwk.y };
1019
+ const pubKey = await crypto.subtle.importKey('jwk', pubJwk, { name: 'ECDSA', namedCurve: 'P-256' }, true, ['verify']);
1020
+ const spki = await crypto.subtle.exportKey('spki', pubKey);
1021
+ return fingerprintSpki(spki);
1022
+ }
1023
+ throw new E2EEError('identity fingerprint unavailable');
1024
+ }
1025
+ /** 本地证书的 SHA-256 指纹(用于锁定证书版本) */
1026
+ async _localCertSha256Fingerprint() {
1027
+ const identity = this._identityFn();
1028
+ const certPem = identity.cert;
1029
+ if (!certPem)
1030
+ return '';
1031
+ return certificateSha256Fingerprint(certPem);
1032
+ }
1033
+ /** 裁剪 seen set */
1034
+ _trimSeenSet() {
1035
+ if (this._seenMessages.size > this._seenMaxSize) {
1036
+ const trimCount = this._seenMessages.size - Math.floor(this._seenMaxSize * 0.8);
1037
+ const keys = [...this._seenMessages.keys()].slice(0, trimCount);
1038
+ for (const k of keys) {
1039
+ this._seenMessages.delete(k);
1040
+ }
1041
+ }
1042
+ }
1043
+ /** 清理过期的 prekey 缓存和 seen set 条目(供外部定时调用) */
1044
+ cleanExpiredCaches() {
1045
+ const now = Date.now() / 1000;
1046
+ // 清理过期的 prekey 缓存
1047
+ for (const [k, v] of this._prekeyCache) {
1048
+ if (now >= v.expireAt)
1049
+ this._prekeyCache.delete(k);
1050
+ }
1051
+ // 清理 seen set(LRU 裁剪)
1052
+ this._trimSeenSet();
1053
+ }
1054
+ }
1055
+ // ── 内部工具函数(PEM 生成) ────────────────────────────────
1056
+ /** 将 ArrayBuffer 转为 PEM 格式(本地版本,避免循环依赖) */
1057
+ function arrayBufferToPemLocal(buffer, label) {
1058
+ const b64 = uint8ToBase64(new Uint8Array(buffer));
1059
+ const lines = [];
1060
+ for (let i = 0; i < b64.length; i += 64) {
1061
+ lines.push(b64.slice(i, i + 64));
1062
+ }
1063
+ return `-----BEGIN ${label}-----\n${lines.join('\n')}\n-----END ${label}-----`;
1064
+ }
1065
+ // ── 导出额外工具(供 e2ee-group.ts 使用)──────────────────
1066
+ export { aadBytesOffline as _aadBytesOffline, concatBytes as _concatBytes, ecdsaSignDer as _ecdsaSignDer, ecdsaVerifyDer as _ecdsaVerifyDer, hkdfDerive as _hkdfDerive, aesGcmEncrypt as _aesGcmEncrypt, aesGcmDecrypt as _aesGcmDecrypt, randomNonce as _randomNonce, uuidV4 as _uuidV4, fingerprintCertPem as _fingerprintCertPem, certificateSha256Fingerprint as _certificateSha256Fingerprint, fingerprintSpki as _fingerprintSpki, importCertPublicKeyEcdsa as _importCertPublicKeyEcdsa, importPrivateKeyEcdsa as _importPrivateKeyEcdsa, derToP1363 as _derToP1363, };
1067
+ //# sourceMappingURL=e2ee.js.map