@agentunion/fastaun 0.2.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/LICENSE +17 -0
  2. package/README.md +78 -0
  3. package/dist/auth.d.ts +287 -0
  4. package/dist/auth.js +1668 -0
  5. package/dist/auth.js.map +1 -0
  6. package/dist/client.d.ts +359 -0
  7. package/dist/client.js +3918 -0
  8. package/dist/client.js.map +1 -0
  9. package/dist/config.d.ts +43 -0
  10. package/dist/config.js +119 -0
  11. package/dist/config.js.map +1 -0
  12. package/dist/crypto.d.ts +41 -0
  13. package/dist/crypto.js +85 -0
  14. package/dist/crypto.js.map +1 -0
  15. package/dist/discovery.d.ts +22 -0
  16. package/dist/discovery.js +110 -0
  17. package/dist/discovery.js.map +1 -0
  18. package/dist/e2ee-group.d.ts +192 -0
  19. package/dist/e2ee-group.js +1134 -0
  20. package/dist/e2ee-group.js.map +1 -0
  21. package/dist/e2ee.d.ts +120 -0
  22. package/dist/e2ee.js +890 -0
  23. package/dist/e2ee.js.map +1 -0
  24. package/dist/errors.d.ts +115 -0
  25. package/dist/errors.js +253 -0
  26. package/dist/errors.js.map +1 -0
  27. package/dist/events.d.ts +39 -0
  28. package/dist/events.js +82 -0
  29. package/dist/events.js.map +1 -0
  30. package/dist/index.d.ts +23 -0
  31. package/dist/index.js +32 -0
  32. package/dist/index.js.map +1 -0
  33. package/dist/keystore/aid-db.d.ts +79 -0
  34. package/dist/keystore/aid-db.js +621 -0
  35. package/dist/keystore/aid-db.js.map +1 -0
  36. package/dist/keystore/file.d.ts +82 -0
  37. package/dist/keystore/file.js +395 -0
  38. package/dist/keystore/file.js.map +1 -0
  39. package/dist/keystore/index.d.ts +88 -0
  40. package/dist/keystore/index.js +7 -0
  41. package/dist/keystore/index.js.map +1 -0
  42. package/dist/keystore/sqlite-backup.d.ts +40 -0
  43. package/dist/keystore/sqlite-backup.js +379 -0
  44. package/dist/keystore/sqlite-backup.js.map +1 -0
  45. package/dist/logger.d.ts +6 -0
  46. package/dist/logger.js +53 -0
  47. package/dist/logger.js.map +1 -0
  48. package/dist/namespaces/auth.d.ts +49 -0
  49. package/dist/namespaces/auth.js +248 -0
  50. package/dist/namespaces/auth.js.map +1 -0
  51. package/dist/namespaces/custody.d.ts +47 -0
  52. package/dist/namespaces/custody.js +231 -0
  53. package/dist/namespaces/custody.js.map +1 -0
  54. package/dist/secret-store/file-store.d.ts +25 -0
  55. package/dist/secret-store/file-store.js +124 -0
  56. package/dist/secret-store/file-store.js.map +1 -0
  57. package/dist/secret-store/index.d.ts +28 -0
  58. package/dist/secret-store/index.js +19 -0
  59. package/dist/secret-store/index.js.map +1 -0
  60. package/dist/seq-tracker.d.ts +29 -0
  61. package/dist/seq-tracker.js +221 -0
  62. package/dist/seq-tracker.js.map +1 -0
  63. package/dist/transport.d.ts +60 -0
  64. package/dist/transport.js +355 -0
  65. package/dist/transport.js.map +1 -0
  66. package/dist/types.d.ts +170 -0
  67. package/dist/types.js +12 -0
  68. package/dist/types.js.map +1 -0
  69. package/package.json +42 -0
package/dist/e2ee.js ADDED
@@ -0,0 +1,890 @@
1
+ /**
2
+ * E2EEManager — 端到端加密管理器
3
+ *
4
+ * 加密策略:prekey_ecdh_v2(四路 ECDH)→ long_term_key(二路 ECDH)两层降级。
5
+ * I/O(获取 prekey、证书)由调用方(AUNClient)负责。
6
+ * 内置本地防重放(seen set),裸 WebSocket 开发者无需额外实现。
7
+ */
8
+ import * as crypto from 'node:crypto';
9
+ import { E2EEError, E2EEDecryptFailedError } from './errors.js';
10
+ // ── 常量 ───────────────────────────────────────────────────────
11
+ export const SUITE = 'P256_HKDF_SHA256_AES_256_GCM';
12
+ /** 四路 ECDH:prekey + identity */
13
+ export const MODE_PREKEY_ECDH_V2 = 'prekey_ecdh_v2';
14
+ /** 降级:长期公钥加密 */
15
+ export const MODE_LONG_TERM_KEY = 'long_term_key';
16
+ /** 离线消息 AAD 字段 */
17
+ export const AAD_FIELDS_OFFLINE = [
18
+ 'from', 'to', 'message_id', 'timestamp',
19
+ 'encryption_mode', 'suite', 'ephemeral_public_key',
20
+ 'recipient_cert_fingerprint', 'sender_cert_fingerprint',
21
+ 'prekey_id',
22
+ ];
23
+ /** 离线消息 AAD 匹配字段(不含 timestamp) */
24
+ export const AAD_MATCH_FIELDS_OFFLINE = [
25
+ 'from', 'to', 'message_id',
26
+ 'encryption_mode', 'suite', 'ephemeral_public_key',
27
+ 'recipient_cert_fingerprint', 'sender_cert_fingerprint',
28
+ 'prekey_id',
29
+ ];
30
+ /** prekey 私钥本地保留时间(秒)— 7 天 */
31
+ const PREKEY_RETENTION_SECONDS = 7 * 24 * 3600;
32
+ const PREKEY_MIN_KEEP_COUNT = 7;
33
+ function prekeyCreatedMarker(prekeyData) {
34
+ return Number(prekeyData.created_at ?? prekeyData.updated_at ?? prekeyData.expires_at ?? 0);
35
+ }
36
+ function latestPrekeyIds(prekeys, keepLatest) {
37
+ if (keepLatest <= 0)
38
+ return new Set();
39
+ return new Set(Object.entries(prekeys)
40
+ .filter(([, data]) => typeof data === 'object' && data !== null)
41
+ .sort((left, right) => {
42
+ const markerDiff = prekeyCreatedMarker(right[1]) - prekeyCreatedMarker(left[1]);
43
+ if (markerDiff !== 0)
44
+ return markerDiff;
45
+ return right[0].localeCompare(left[0]);
46
+ })
47
+ .slice(0, keepLatest)
48
+ .map(([prekeyId]) => prekeyId));
49
+ }
50
+ function loadKeyStorePrekeys(keystore, aid, deviceId = '') {
51
+ const normalizedDeviceId = String(deviceId ?? '').trim();
52
+ if (typeof keystore.loadE2EEPrekeys === 'function') {
53
+ return (keystore.loadE2EEPrekeys(aid, normalizedDeviceId) ?? {});
54
+ }
55
+ throw new Error('keystore 缺少 loadE2EEPrekeys 方法');
56
+ }
57
+ function saveKeyStorePrekey(keystore, aid, deviceId, prekeyId, prekeyData) {
58
+ const normalizedDeviceId = String(deviceId ?? '').trim();
59
+ if (typeof keystore.saveE2EEPrekey === 'function') {
60
+ keystore.saveE2EEPrekey(aid, prekeyId, prekeyData, normalizedDeviceId);
61
+ return;
62
+ }
63
+ throw new Error(`keystore ${keystore.constructor?.name ?? 'unknown'} missing saveE2EEPrekey method`);
64
+ }
65
+ function cleanupKeyStorePrekeys(keystore, aid, deviceId, cutoffMs, keepLatest = PREKEY_MIN_KEEP_COUNT) {
66
+ const normalizedDeviceId = String(deviceId ?? '').trim();
67
+ if (typeof keystore.cleanupE2EEPrekeys === 'function') {
68
+ return keystore.cleanupE2EEPrekeys(aid, cutoffMs, keepLatest, normalizedDeviceId) ?? [];
69
+ }
70
+ throw new Error(`keystore ${keystore.constructor?.name ?? 'unknown'} missing cleanupE2EEPrekeys method`);
71
+ }
72
+ // ── 工具函数 ───────────────────────────────────────────────────
73
+ /** 将 PEM 证书/公钥转为 KeyObject */
74
+ function pemToCertPublicKey(certPem) {
75
+ const pem = typeof certPem === 'string' ? certPem : certPem.toString('utf-8');
76
+ try {
77
+ const x509 = new crypto.X509Certificate(pem);
78
+ return x509.publicKey;
79
+ }
80
+ catch {
81
+ return crypto.createPublicKey(pem);
82
+ }
83
+ }
84
+ /** ECDH 共享密钥计算 */
85
+ function ecdhShared(privateKey, publicKey) {
86
+ return crypto.diffieHellman({ privateKey, publicKey });
87
+ }
88
+ /** HKDF-SHA256 派生密钥 */
89
+ function hkdfDeriveSync(ikm, info, length) {
90
+ // Node.js crypto.hkdfSync 在 v16+ 可用
91
+ const derived = crypto.hkdfSync('sha256', ikm, Buffer.alloc(0), info, length);
92
+ return Buffer.from(derived);
93
+ }
94
+ /** AES-256-GCM 加密,返回 {ciphertext, tag, nonce} */
95
+ function aesGcmEncrypt(key, plaintext, aad) {
96
+ const nonce = crypto.randomBytes(12);
97
+ const cipher = crypto.createCipheriv('aes-256-gcm', key, nonce);
98
+ cipher.setAAD(aad);
99
+ const encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()]);
100
+ const tag = cipher.getAuthTag();
101
+ return { ciphertext: encrypted, tag, nonce };
102
+ }
103
+ /** AES-256-GCM 解密 */
104
+ function aesGcmDecrypt(key, ciphertext, tag, nonce, aad) {
105
+ const decipher = crypto.createDecipheriv('aes-256-gcm', key, nonce);
106
+ decipher.setAuthTag(tag);
107
+ decipher.setAAD(aad);
108
+ return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
109
+ }
110
+ /** ECDSA-SHA256 签名 */
111
+ function ecdsaSign(privateKeyPem, data) {
112
+ const signer = crypto.createSign('SHA256');
113
+ signer.update(data);
114
+ signer.end();
115
+ return signer.sign(privateKeyPem);
116
+ }
117
+ /** ECDSA-SHA256 验签 */
118
+ function ecdsaVerify(publicKey, signature, data) {
119
+ const verifier = crypto.createVerify('SHA256');
120
+ verifier.update(data);
121
+ verifier.end();
122
+ return verifier.verify(publicKey, signature);
123
+ }
124
+ /** 生成 ECDSA P-256 密钥对 */
125
+ function generateECKeyPair() {
126
+ return crypto.generateKeyPairSync('ec', { namedCurve: 'prime256v1' });
127
+ }
128
+ /** 公钥 → DER SPKI 指纹 */
129
+ function fingerprintPublicKeyDer(derBytes) {
130
+ const hash = crypto.createHash('sha256').update(derBytes).digest();
131
+ return `sha256:${hash.toString('hex')}`;
132
+ }
133
+ /** KeyObject 公钥 → 指纹 */
134
+ function fingerprintKeyObject(pubKey) {
135
+ const der = pubKey.export({ type: 'spki', format: 'der' });
136
+ return fingerprintPublicKeyDer(der);
137
+ }
138
+ /** PEM 证书 → 证书 SHA-256 指纹 */
139
+ function fingerprintCertPem(certPem) {
140
+ return certificateSha256Fingerprint(certPem);
141
+ }
142
+ /** PEM/DER 证书 → DER 字节 */
143
+ function certToDerBytes(certPem) {
144
+ const raw = Buffer.isBuffer(certPem) ? Buffer.from(certPem) : Buffer.from(certPem, 'utf-8');
145
+ const text = raw.toString('utf-8');
146
+ if (text.includes('-----BEGIN CERTIFICATE-----')) {
147
+ const body = text
148
+ .replace(/-----BEGIN CERTIFICATE-----/g, '')
149
+ .replace(/-----END CERTIFICATE-----/g, '')
150
+ .replace(/\s+/g, '');
151
+ if (!body) {
152
+ throw new E2EEError('无效证书 PEM:缺少 base64 内容');
153
+ }
154
+ return Buffer.from(body, 'base64');
155
+ }
156
+ try {
157
+ return new crypto.X509Certificate(raw).raw;
158
+ }
159
+ catch {
160
+ return raw;
161
+ }
162
+ }
163
+ /** PEM 证书 → 证书 SHA-256 指纹 */
164
+ function certificateSha256Fingerprint(certPem) {
165
+ const der = certToDerBytes(certPem);
166
+ const hash = crypto.createHash('sha256').update(der).digest('hex');
167
+ return `sha256:${hash}`;
168
+ }
169
+ /** 公钥 KeyObject → 未压缩点(0x04 || x || y,65 字节) */
170
+ function publicKeyToUncompressedPoint(pubKey) {
171
+ // 用 JWK 提取 x, y,并显式校验曲线必须是 P-256。
172
+ // 这样 P-384/P-521 错误会在导出阶段立即抛出,而不是延后到解密时才失败。
173
+ const jwk = pubKey.export({ format: 'jwk' });
174
+ if (jwk.kty !== 'EC') {
175
+ throw new E2EEError(`unsupported public key type: ${jwk.kty ?? 'unknown'}`);
176
+ }
177
+ if (jwk.crv !== 'P-256') {
178
+ throw new E2EEError(`unsupported EC curve: ${jwk.crv ?? 'unknown'} (only P-256 is supported)`);
179
+ }
180
+ const x = Buffer.from(jwk.x, 'base64url');
181
+ const y = Buffer.from(jwk.y, 'base64url');
182
+ if (x.length !== 32 || y.length !== 32) {
183
+ throw new E2EEError(`invalid P-256 coordinate length: x=${x.length}, y=${y.length}`);
184
+ }
185
+ return Buffer.concat([Buffer.from([0x04]), x, y]);
186
+ }
187
+ /** 未压缩点 → KeyObject */
188
+ function uncompressedPointToPublicKey(point) {
189
+ // 构造 JWK
190
+ if (point[0] !== 0x04 || point.length !== 65) {
191
+ throw new E2EEError('无效的未压缩公钥点格式');
192
+ }
193
+ const x = point.subarray(1, 33).toString('base64url');
194
+ const y = point.subarray(33, 65).toString('base64url');
195
+ return crypto.createPublicKey({
196
+ key: { kty: 'EC', crv: 'P-256', x, y },
197
+ format: 'jwk',
198
+ });
199
+ }
200
+ /** 离线消息 AAD → 排序紧凑 JSON bytes */
201
+ function aadBytesOffline(aad) {
202
+ const obj = {};
203
+ for (const field of AAD_FIELDS_OFFLINE) {
204
+ obj[field] = aad[field] ?? null;
205
+ }
206
+ // sorted keys, compact JSON — 与 Python json.dumps(sort_keys=True, separators=(",",":")) 一致
207
+ const sorted = {};
208
+ for (const k of Object.keys(obj).sort()) {
209
+ sorted[k] = obj[k];
210
+ }
211
+ return Buffer.from(JSON.stringify(sorted), 'utf-8');
212
+ }
213
+ /** AAD 匹配检查 */
214
+ function aadMatchesOffline(expected, actual) {
215
+ for (const field of AAD_MATCH_FIELDS_OFFLINE) {
216
+ if (String(expected[field] ?? '') !== String(actual[field] ?? '')) {
217
+ return false;
218
+ }
219
+ }
220
+ return true;
221
+ }
222
+ // ── E2EEManager 类 ────────────────────────────────────────────
223
+ export class E2EEManager {
224
+ _identityFn;
225
+ _deviceIdFn;
226
+ _keystore;
227
+ /** 本地防重放 seen set */
228
+ _seenMessages = new Map();
229
+ _seenMaxSize = 50000;
230
+ /** 对方 prekey 内存缓存(TTL) */
231
+ _prekeyCache = new Map();
232
+ _prekeyCacheTtl;
233
+ /** 本地 prekey 私钥内存缓存 {prekeyId: privateKeyPem} */
234
+ _localPrekeyCache = new Map();
235
+ /** 防重放时间窗口(秒) */
236
+ _replayWindowSeconds;
237
+ constructor(opts) {
238
+ this._identityFn = opts.identityFn;
239
+ this._deviceIdFn = opts.deviceIdFn ?? (() => '');
240
+ this._keystore = opts.keystore;
241
+ this._prekeyCacheTtl = opts.prekeyCacheTtl ?? 3600;
242
+ this._replayWindowSeconds = opts.replayWindowSeconds ?? 300;
243
+ }
244
+ // ── 便利方法 ──────────────────────────────────────────────
245
+ /**
246
+ * 加密消息(便利方法)。
247
+ * 有 prekey 时用 prekey_ecdh_v2(四路 ECDH),无 prekey 时降级为 long_term_key。
248
+ */
249
+ encryptMessage(toAid, payload, opts) {
250
+ const messageId = opts.messageId ?? crypto.randomUUID();
251
+ const timestamp = opts.timestamp ?? Math.floor(Date.now());
252
+ return this.encryptOutbound(toAid, payload, opts.peerCertPem, opts.prekey ?? null, messageId, timestamp);
253
+ }
254
+ // ── 加密 ─────────────────────────────────────────────────
255
+ /**
256
+ * 加密出站消息:有 prekey → prekey_ecdh_v2(四路 ECDH),无 prekey → long_term_key。
257
+ * 返回 [envelope, resultInfo]。
258
+ */
259
+ encryptOutbound(peerAid, payload, peerCertPem, prekey, messageId, timestamp) {
260
+ // 传入 prekey → 缓存;传入 null → 查缓存
261
+ if (prekey != null) {
262
+ this.cachePrekey(peerAid, prekey);
263
+ }
264
+ else {
265
+ prekey = this.getCachedPrekey(peerAid);
266
+ }
267
+ if (prekey) {
268
+ try {
269
+ const envelope = this._encryptWithPrekey(peerAid, payload, prekey, peerCertPem, messageId, timestamp);
270
+ return [envelope, {
271
+ encrypted: true,
272
+ forward_secrecy: true,
273
+ mode: MODE_PREKEY_ECDH_V2,
274
+ degraded: false,
275
+ }];
276
+ }
277
+ catch (exc) {
278
+ // prekey 加密失败,降级到 long_term_key — 记录异常以便排查安全降级原因
279
+ console.warn('[aun_core.e2ee] prekey 加密失败,降级到 long_term_key:', exc);
280
+ }
281
+ }
282
+ const envelope = this._encryptWithLongTermKey(peerAid, payload, peerCertPem, messageId, timestamp);
283
+ const degraded = prekey != null;
284
+ return [envelope, {
285
+ encrypted: true,
286
+ forward_secrecy: false,
287
+ mode: MODE_LONG_TERM_KEY,
288
+ degraded,
289
+ degradation_reason: degraded ? 'prekey_encrypt_failed' : 'no_prekey_available',
290
+ }];
291
+ }
292
+ /** 使用对方 prekey 加密(prekey_ecdh_v2 模式,四路 ECDH + 发送方签名) */
293
+ _encryptWithPrekey(peerAid, payload, prekey, peerCertPem, messageId, timestamp) {
294
+ const peerIdentityPublic = pemToCertPublicKey(peerCertPem);
295
+ const expectedCertFingerprint = String(prekey.cert_fingerprint ?? '').trim().toLowerCase();
296
+ if (expectedCertFingerprint) {
297
+ const actualCertFingerprint = certificateSha256Fingerprint(peerCertPem);
298
+ if (actualCertFingerprint !== expectedCertFingerprint) {
299
+ throw new E2EEError('prekey cert fingerprint mismatch');
300
+ }
301
+ }
302
+ // 验证 prekey 签名
303
+ const createdAt = prekey.created_at;
304
+ let signData;
305
+ if (createdAt != null) {
306
+ signData = Buffer.from(`${prekey.prekey_id}|${prekey.public_key}|${createdAt}`, 'utf-8');
307
+ }
308
+ else {
309
+ signData = Buffer.from(`${prekey.prekey_id}|${prekey.public_key}`, 'utf-8');
310
+ }
311
+ const sigBytes = Buffer.from(prekey.signature, 'base64');
312
+ if (!ecdsaVerify(peerIdentityPublic, sigBytes, signData)) {
313
+ throw new E2EEError('prekey signature verification failed');
314
+ }
315
+ // 导入对方 prekey 公钥(DER SPKI)
316
+ const peerPrekeyDer = Buffer.from(prekey.public_key, 'base64');
317
+ const peerPrekeyPublic = crypto.createPublicKey({ key: peerPrekeyDer, format: 'der', type: 'spki' });
318
+ // 加载发送方自己的 identity 私钥
319
+ const senderIdentityPrivate = this._loadSenderIdentityPrivate();
320
+ // 生成临时 ECDH 密钥对
321
+ const { privateKey: ephemeralPrivate, publicKey: ephemeralPublic } = generateECKeyPair();
322
+ const ephemeralPublicBytes = publicKeyToUncompressedPoint(ephemeralPublic);
323
+ // 四路 ECDH + HKDF
324
+ const dh1 = ecdhShared(ephemeralPrivate, peerPrekeyPublic);
325
+ const dh2 = ecdhShared(ephemeralPrivate, peerIdentityPublic);
326
+ const dh3 = ecdhShared(senderIdentityPrivate, peerPrekeyPublic);
327
+ const dh4 = ecdhShared(senderIdentityPrivate, peerIdentityPublic);
328
+ const combined = Buffer.concat([dh1, dh2, dh3, dh4]);
329
+ const messageKey = hkdfDeriveSync(combined, Buffer.from(`aun-prekey-v2:${prekey.prekey_id}`, 'utf-8'), 32);
330
+ // AES-GCM 加密
331
+ const plaintext = Buffer.from(JSON.stringify(payload), 'utf-8');
332
+ const senderFingerprint = this._localCertSha256Fingerprint() || this._localIdentityFingerprint();
333
+ const recipientFingerprint = fingerprintCertPem(peerCertPem);
334
+ const ephemeralPkB64 = ephemeralPublicBytes.toString('base64');
335
+ const aad = {
336
+ from: this._currentAid(),
337
+ to: peerAid,
338
+ message_id: messageId,
339
+ timestamp,
340
+ encryption_mode: MODE_PREKEY_ECDH_V2,
341
+ suite: SUITE,
342
+ ephemeral_public_key: ephemeralPkB64,
343
+ recipient_cert_fingerprint: recipientFingerprint,
344
+ sender_cert_fingerprint: senderFingerprint,
345
+ prekey_id: prekey.prekey_id,
346
+ };
347
+ const aadBytes = aadBytesOffline(aad);
348
+ const { ciphertext, tag, nonce } = aesGcmEncrypt(messageKey, plaintext, aadBytes);
349
+ const envelope = {
350
+ type: 'e2ee.encrypted',
351
+ version: '1',
352
+ encryption_mode: MODE_PREKEY_ECDH_V2,
353
+ suite: SUITE,
354
+ prekey_id: prekey.prekey_id,
355
+ ephemeral_public_key: ephemeralPkB64,
356
+ nonce: nonce.toString('base64'),
357
+ ciphertext: ciphertext.toString('base64'),
358
+ tag: tag.toString('base64'),
359
+ aad,
360
+ };
361
+ // 发送方签名:对 ciphertext + tag + aad_bytes 签名(不可否认性)
362
+ const signPayload = Buffer.concat([ciphertext, tag, aadBytes]);
363
+ envelope.sender_signature = this._signBytes(signPayload);
364
+ envelope.sender_cert_fingerprint = senderFingerprint;
365
+ return envelope;
366
+ }
367
+ /** 使用 2DH 加密(long_term_key 模式 + 发送方签名) */
368
+ _encryptWithLongTermKey(peerAid, payload, peerCertPem, messageId, timestamp) {
369
+ const peerPublicKey = pemToCertPublicKey(peerCertPem);
370
+ const senderIdentityPrivate = this._loadSenderIdentityPrivate();
371
+ // 生成临时密钥对
372
+ const { privateKey: ephemeralPrivate, publicKey: ephemeralPublic } = generateECKeyPair();
373
+ const ephemeralPublicBytes = publicKeyToUncompressedPoint(ephemeralPublic);
374
+ // 2DH + HKDF
375
+ const dh1 = ecdhShared(ephemeralPrivate, peerPublicKey);
376
+ const dh2 = ecdhShared(senderIdentityPrivate, peerPublicKey);
377
+ const combined = Buffer.concat([dh1, dh2]);
378
+ const messageKey = hkdfDeriveSync(combined, Buffer.from('aun-longterm-v2', 'utf-8'), 32);
379
+ const plaintext = Buffer.from(JSON.stringify(payload), 'utf-8');
380
+ const senderFingerprint = this._localCertSha256Fingerprint() || this._localIdentityFingerprint();
381
+ const recipientFingerprint = fingerprintCertPem(peerCertPem);
382
+ const ephemeralPkB64 = ephemeralPublicBytes.toString('base64');
383
+ const aad = {
384
+ from: this._currentAid(),
385
+ to: peerAid,
386
+ message_id: messageId,
387
+ timestamp,
388
+ encryption_mode: MODE_LONG_TERM_KEY,
389
+ suite: SUITE,
390
+ ephemeral_public_key: ephemeralPkB64,
391
+ recipient_cert_fingerprint: recipientFingerprint,
392
+ sender_cert_fingerprint: senderFingerprint,
393
+ };
394
+ const aadBytes = aadBytesOffline(aad);
395
+ const { ciphertext, tag, nonce } = aesGcmEncrypt(messageKey, plaintext, aadBytes);
396
+ const envelope = {
397
+ type: 'e2ee.encrypted',
398
+ version: '1',
399
+ encryption_mode: MODE_LONG_TERM_KEY,
400
+ suite: SUITE,
401
+ ephemeral_public_key: ephemeralPkB64,
402
+ nonce: nonce.toString('base64'),
403
+ ciphertext: ciphertext.toString('base64'),
404
+ tag: tag.toString('base64'),
405
+ aad,
406
+ };
407
+ // 发送方签名(不可否认性)
408
+ const signPayload = Buffer.concat([ciphertext, tag, aadBytes]);
409
+ envelope.sender_signature = this._signBytes(signPayload);
410
+ envelope.sender_cert_fingerprint = senderFingerprint;
411
+ return envelope;
412
+ }
413
+ // ── 解密 ─────────────────────────────────────────────────
414
+ /** 解密单条消息(便利方法,内置本地防重放 + timestamp 窗口) */
415
+ decryptMessage(message) {
416
+ const payload = message.payload;
417
+ if (!payload || typeof payload !== 'object')
418
+ return message;
419
+ if (payload.type !== 'e2ee.encrypted')
420
+ return message;
421
+ if (!this._shouldDecryptForCurrentAid(message, payload))
422
+ return message;
423
+ // timestamp 窗口检查
424
+ const ts = (message.timestamp ?? payload.aad?.timestamp);
425
+ if (typeof ts === 'number' && this._replayWindowSeconds > 0) {
426
+ const nowMs = Date.now();
427
+ const diffS = Math.abs(nowMs - ts) / 1000;
428
+ if (diffS > this._replayWindowSeconds) {
429
+ return null;
430
+ }
431
+ }
432
+ // H27: 本地防重放——必须在 _decryptMessage 成功后再 set seenKey,
433
+ // 否则解密/验签失败时合法重传会被当作重复丢弃。
434
+ const messageId = message.message_id || '';
435
+ const fromAid = message.from || '';
436
+ let seenKey = '';
437
+ if (messageId && fromAid) {
438
+ seenKey = `${fromAid}:${messageId}`;
439
+ if (this._seenMessages.has(seenKey))
440
+ return null;
441
+ }
442
+ const result = this._decryptMessage(message);
443
+ if (result !== null && seenKey) {
444
+ this._seenMessages.set(seenKey, true);
445
+ this._trimSeenSet();
446
+ }
447
+ return result;
448
+ }
449
+ /** 解密入站消息(不消耗 seen set,用于 pull 场景) */
450
+ _decryptMessage(message) {
451
+ const payload = message.payload;
452
+ if (typeof payload === 'object' && !this._shouldDecryptForCurrentAid(message, payload)) {
453
+ return message;
454
+ }
455
+ const encryptionMode = payload.encryption_mode || '';
456
+ // 验证发送方签名(适用于所有模式)
457
+ try {
458
+ this._verifySenderSignature(payload, message);
459
+ }
460
+ catch (exc) {
461
+ console.warn('[aun_core.e2ee] 发送方签名验证失败');
462
+ return null;
463
+ }
464
+ if (encryptionMode === MODE_PREKEY_ECDH_V2) {
465
+ return this._decryptMessagePrekeyV2(message);
466
+ }
467
+ else if (encryptionMode === MODE_LONG_TERM_KEY) {
468
+ return this._decryptMessageLongTerm(message);
469
+ }
470
+ return null;
471
+ }
472
+ /** 解密 prekey_ecdh_v2 模式的消息(四路 ECDH) */
473
+ _decryptMessagePrekeyV2(message) {
474
+ const payload = message.payload;
475
+ try {
476
+ const ephemeralPublicBytes = Buffer.from(payload.ephemeral_public_key, 'base64');
477
+ const prekeyId = payload.prekey_id || '';
478
+ const nonce = Buffer.from(payload.nonce, 'base64');
479
+ const ciphertext = Buffer.from(payload.ciphertext, 'base64');
480
+ const tag = Buffer.from(payload.tag, 'base64');
481
+ // 加载 prekey 私钥
482
+ const prekeyPrivateKey = this._loadPrekeyPrivateKey(prekeyId);
483
+ if (!prekeyPrivateKey) {
484
+ throw new E2EEError(`prekey not found: ${prekeyId}`);
485
+ }
486
+ // 加载接收方自己的 identity 私钥
487
+ const myAid = this._currentAid();
488
+ if (!myAid)
489
+ throw new E2EEError('AID unavailable');
490
+ const keyPair = this._keystore.loadKeyPair(myAid);
491
+ if (!keyPair || !keyPair.private_key_pem) {
492
+ throw new E2EEError('Identity private key not found');
493
+ }
494
+ const myIdentityPrivate = crypto.createPrivateKey(keyPair.private_key_pem);
495
+ // 获取发送方公钥
496
+ const fromAid = (message.from ?? payload.aad?.from);
497
+ const senderCertFingerprint = String(payload.sender_cert_fingerprint ?? payload.aad?.sender_cert_fingerprint ?? '').trim().toLowerCase();
498
+ const senderPublicKey = this._loadSenderPublicKey(fromAid, senderCertFingerprint || undefined);
499
+ if (!senderPublicKey) {
500
+ throw new E2EEError(`sender public key not found for ${fromAid}`);
501
+ }
502
+ // 四路 ECDH + HKDF
503
+ const ephemeralPublic = uncompressedPointToPublicKey(ephemeralPublicBytes);
504
+ const dh1 = ecdhShared(prekeyPrivateKey, ephemeralPublic);
505
+ const dh2 = ecdhShared(myIdentityPrivate, ephemeralPublic);
506
+ const dh3 = ecdhShared(prekeyPrivateKey, senderPublicKey);
507
+ const dh4 = ecdhShared(myIdentityPrivate, senderPublicKey);
508
+ const combined = Buffer.concat([dh1, dh2, dh3, dh4]);
509
+ const messageKey = hkdfDeriveSync(combined, Buffer.from(`aun-prekey-v2:${prekeyId}`, 'utf-8'), 32);
510
+ // 验证 AAD 并解密
511
+ const aad = payload.aad;
512
+ let aadBytes;
513
+ if (aad && typeof aad === 'object') {
514
+ const expectedAad = this._buildInboundAadOffline(message, payload);
515
+ if (!aadMatchesOffline(expectedAad, aad)) {
516
+ throw new E2EEDecryptFailedError('aad mismatch');
517
+ }
518
+ aadBytes = aadBytesOffline(aad);
519
+ }
520
+ else {
521
+ aadBytes = Buffer.alloc(0);
522
+ }
523
+ const plaintext = aesGcmDecrypt(messageKey, ciphertext, tag, nonce, aadBytes);
524
+ const decoded = JSON.parse(plaintext.toString('utf-8'));
525
+ return {
526
+ ...message,
527
+ payload: decoded,
528
+ encrypted: true,
529
+ e2ee: {
530
+ encryption_mode: MODE_PREKEY_ECDH_V2,
531
+ suite: payload.suite || SUITE,
532
+ prekey_id: prekeyId,
533
+ },
534
+ };
535
+ }
536
+ catch (exc) {
537
+ const fromAid = (message.from ?? '');
538
+ const msgId = (message.message_id ?? '');
539
+ console.warn(`[aun_core.e2ee] 解密失败: mode=prekey_ecdh_v2, from=${fromAid}, mid=${msgId}`, exc);
540
+ return null;
541
+ }
542
+ }
543
+ /** 解密 long_term_key 模式的消息(2DH) */
544
+ _decryptMessageLongTerm(message) {
545
+ const payload = message.payload;
546
+ try {
547
+ const ephemeralPublicBytes = Buffer.from(payload.ephemeral_public_key, 'base64');
548
+ const nonce = Buffer.from(payload.nonce, 'base64');
549
+ const ciphertext = Buffer.from(payload.ciphertext, 'base64');
550
+ const tag = Buffer.from(payload.tag, 'base64');
551
+ const myAid = this._currentAid();
552
+ if (!myAid)
553
+ throw new E2EEError('AID unavailable');
554
+ const keyPair = this._keystore.loadKeyPair(myAid);
555
+ if (!keyPair || !keyPair.private_key_pem) {
556
+ throw new E2EEError('Private key not found');
557
+ }
558
+ const privateKey = crypto.createPrivateKey(keyPair.private_key_pem);
559
+ // 获取发送方公钥
560
+ const fromAid = (message.from ?? payload.aad?.from);
561
+ const senderCertFingerprint = String(payload.sender_cert_fingerprint ?? payload.aad?.sender_cert_fingerprint ?? '').trim().toLowerCase();
562
+ const senderPublicKey = this._loadSenderPublicKey(fromAid, senderCertFingerprint || undefined);
563
+ if (!senderPublicKey) {
564
+ throw new E2EEError(`sender public key not found for ${fromAid}`);
565
+ }
566
+ // 2DH + HKDF
567
+ const ephemeralPublic = uncompressedPointToPublicKey(ephemeralPublicBytes);
568
+ const dh1 = ecdhShared(privateKey, ephemeralPublic);
569
+ const dh2 = ecdhShared(privateKey, senderPublicKey);
570
+ const combined = Buffer.concat([dh1, dh2]);
571
+ const messageKey = hkdfDeriveSync(combined, Buffer.from('aun-longterm-v2', 'utf-8'), 32);
572
+ const aad = payload.aad;
573
+ let aadBytes;
574
+ if (aad && typeof aad === 'object') {
575
+ const expectedAad = this._buildInboundAadOffline(message, payload);
576
+ if (!aadMatchesOffline(expectedAad, aad)) {
577
+ throw new E2EEDecryptFailedError('aad mismatch');
578
+ }
579
+ aadBytes = aadBytesOffline(aad);
580
+ }
581
+ else {
582
+ aadBytes = Buffer.alloc(0);
583
+ }
584
+ const plaintext = aesGcmDecrypt(messageKey, ciphertext, tag, nonce, aadBytes);
585
+ const decoded = JSON.parse(plaintext.toString('utf-8'));
586
+ return {
587
+ ...message,
588
+ payload: decoded,
589
+ encrypted: true,
590
+ e2ee: {
591
+ encryption_mode: MODE_LONG_TERM_KEY,
592
+ suite: payload.suite,
593
+ },
594
+ };
595
+ }
596
+ catch (exc) {
597
+ const fromAid = (message.from ?? '');
598
+ const msgId = (message.message_id ?? '');
599
+ console.warn(`[aun_core.e2ee] 解密失败: mode=long_term_key, from=${fromAid}, mid=${msgId}`, exc);
600
+ return null;
601
+ }
602
+ }
603
+ // ── 发送方签名验证 ─────────────────────────────────────────
604
+ _verifySenderSignature(payload, message) {
605
+ const senderSigB64 = payload.sender_signature;
606
+ if (!senderSigB64) {
607
+ throw new E2EEDecryptFailedError('sender_signature missing: 拒绝无发送方签名的消息');
608
+ }
609
+ const fromAid = (message.from ?? payload.aad?.from);
610
+ if (!fromAid) {
611
+ throw new E2EEDecryptFailedError('from_aid missing in message');
612
+ }
613
+ const senderCertFingerprint = String(payload.sender_cert_fingerprint ?? payload.aad?.sender_cert_fingerprint ?? '').trim().toLowerCase();
614
+ const senderCertPem = this._getSenderCert(fromAid, senderCertFingerprint || undefined);
615
+ if (!senderCertPem) {
616
+ throw new E2EEDecryptFailedError(`sender cert not found for ${fromAid}`);
617
+ }
618
+ const senderPublicKey = pemToCertPublicKey(senderCertPem);
619
+ // 重建签名载荷
620
+ const ciphertextBuf = Buffer.from(payload.ciphertext, 'base64');
621
+ const tagBuf = Buffer.from(payload.tag, 'base64');
622
+ const aad = payload.aad;
623
+ const aadBytes = (aad && typeof aad === 'object') ? aadBytesOffline(aad) : Buffer.alloc(0);
624
+ const signPayload = Buffer.concat([ciphertextBuf, tagBuf, aadBytes]);
625
+ const sigBytes = Buffer.from(senderSigB64, 'base64');
626
+ if (!ecdsaVerify(senderPublicKey, sigBytes, signPayload)) {
627
+ throw new E2EEDecryptFailedError('sender signature verification failed');
628
+ }
629
+ }
630
+ // ── Prekey 缓存 ────────────────────────────────────────────
631
+ /** 缓存对方的 prekey */
632
+ cachePrekey(peerAid, prekey) {
633
+ this._prekeyCache.set(peerAid, {
634
+ prekey,
635
+ expireAt: Date.now() / 1000 + this._prekeyCacheTtl,
636
+ });
637
+ }
638
+ /** 获取缓存的 prekey(过期返回 null) */
639
+ getCachedPrekey(peerAid) {
640
+ const cached = this._prekeyCache.get(peerAid);
641
+ if (!cached)
642
+ return null;
643
+ if (Date.now() / 1000 >= cached.expireAt) {
644
+ this._prekeyCache.delete(peerAid);
645
+ return null;
646
+ }
647
+ return cached.prekey;
648
+ }
649
+ /** 使指定 peer 的 prekey 缓存失效 */
650
+ invalidatePrekeyCache(peerAid) {
651
+ this._prekeyCache.delete(peerAid);
652
+ }
653
+ // ── Prekey 生成 ──────────────────────────────────────────
654
+ /**
655
+ * 生成 prekey 材料并保存私钥到本地 keystore。
656
+ * 返回 {prekey_id, public_key, signature, created_at},可直接用于 RPC 上传。
657
+ */
658
+ generatePrekey() {
659
+ const aid = this._currentAid();
660
+ if (!aid)
661
+ throw new E2EEError('AID unavailable for prekey generation');
662
+ const deviceId = this._currentDeviceId();
663
+ // 生成新 prekey
664
+ const { privateKey, publicKey } = generateECKeyPair();
665
+ const publicDer = publicKey.export({ type: 'spki', format: 'der' });
666
+ const prekeyId = crypto.randomUUID();
667
+ const publicKeyB64 = publicDer.toString('base64');
668
+ const nowMs = Date.now();
669
+ // 签名:prekey_id|public_key|created_at
670
+ const signDataBuf = Buffer.from(`${prekeyId}|${publicKeyB64}|${nowMs}`, 'utf-8');
671
+ const signature = this._signBytes(signDataBuf);
672
+ // 保存私钥到本地 keystore
673
+ const privateKeyPem = privateKey.export({ type: 'pkcs8', format: 'pem' });
674
+ saveKeyStorePrekey(this._keystore, aid, deviceId, prekeyId, {
675
+ private_key_pem: privateKeyPem,
676
+ created_at: nowMs,
677
+ updated_at: nowMs,
678
+ });
679
+ // 内存缓存私钥
680
+ this._localPrekeyCache.set(prekeyId, privateKeyPem);
681
+ // 清理过期的旧 prekey 私钥
682
+ this._cleanupExpiredPrekeys(aid, deviceId);
683
+ const result = {
684
+ prekey_id: prekeyId,
685
+ public_key: publicKeyB64,
686
+ signature,
687
+ created_at: nowMs,
688
+ };
689
+ const certFingerprint = this._localCertSha256Fingerprint();
690
+ if (certFingerprint) {
691
+ result.cert_fingerprint = certFingerprint;
692
+ }
693
+ if (deviceId) {
694
+ result.device_id = deviceId;
695
+ }
696
+ return result;
697
+ }
698
+ /** 清理本地过期的 prekey 私钥 */
699
+ _cleanupExpiredPrekeys(aid, deviceId) {
700
+ const nowMs = Date.now();
701
+ const cutoffMs = nowMs - PREKEY_RETENTION_SECONDS * 1000;
702
+ const expired = cleanupKeyStorePrekeys(this._keystore, aid, deviceId, cutoffMs, PREKEY_MIN_KEEP_COUNT);
703
+ if (expired.length > 0) {
704
+ for (const pid of expired) {
705
+ this._localPrekeyCache.delete(pid);
706
+ }
707
+ }
708
+ }
709
+ /** 从内存缓存或 keystore 加载 prekey 私钥 */
710
+ _loadPrekeyPrivateKey(prekeyId) {
711
+ // 优先从内存缓存获取
712
+ const cachedPem = this._localPrekeyCache.get(prekeyId);
713
+ if (cachedPem) {
714
+ return crypto.createPrivateKey(cachedPem);
715
+ }
716
+ const aid = this._currentAid();
717
+ if (!aid)
718
+ return null;
719
+ const prekeys = loadKeyStorePrekeys(this._keystore, aid, this._currentDeviceId());
720
+ const prekeyData = prekeys[prekeyId];
721
+ if (!prekeyData)
722
+ return null;
723
+ const privateKeyPem = prekeyData.private_key_pem;
724
+ if (!privateKeyPem)
725
+ return null;
726
+ try {
727
+ const pk = crypto.createPrivateKey(privateKeyPem);
728
+ this._localPrekeyCache.set(prekeyId, privateKeyPem);
729
+ return pk;
730
+ }
731
+ catch {
732
+ return null;
733
+ }
734
+ }
735
+ // ── 证书指纹工具 ────────────────────────────────────────
736
+ /** 从 PEM 证书计算公钥指纹 */
737
+ static fingerprintCertPem(certPem) {
738
+ return fingerprintCertPem(certPem);
739
+ }
740
+ /** 公钥 DER bytes → 指纹 */
741
+ static fingerprintDerPublicKey(derBytes) {
742
+ return fingerprintPublicKeyDer(derBytes);
743
+ }
744
+ // ── 内部工具 ─────────────────────────────────────────────
745
+ /** 仅解密发给当前 AID 的消息 */
746
+ _shouldDecryptForCurrentAid(message, payload) {
747
+ if (String(message.direction ?? '').trim().toLowerCase() === 'outbound_sync') {
748
+ return true;
749
+ }
750
+ const currentAid = this._currentAid();
751
+ if (!currentAid)
752
+ return true;
753
+ const targetAid = message.to ||
754
+ payload.aad?.to ||
755
+ payload.to;
756
+ if (!targetAid)
757
+ return true;
758
+ return String(targetAid) === String(currentAid);
759
+ }
760
+ /** LRU 裁剪 seen set */
761
+ _trimSeenSet() {
762
+ if (this._seenMessages.size > this._seenMaxSize) {
763
+ const trimCount = this._seenMessages.size - Math.floor(this._seenMaxSize * 0.8);
764
+ const iter = this._seenMessages.keys();
765
+ for (let i = 0; i < trimCount; i++) {
766
+ const next = iter.next();
767
+ if (next.done)
768
+ break;
769
+ this._seenMessages.delete(next.value);
770
+ }
771
+ }
772
+ }
773
+ /** 获取当前 AID */
774
+ _currentAid() {
775
+ const identity = this._identityFn();
776
+ const aid = identity.aid;
777
+ return aid ? String(aid) : null;
778
+ }
779
+ _currentDeviceId() {
780
+ try {
781
+ return String(this._deviceIdFn() ?? '').trim();
782
+ }
783
+ catch {
784
+ return '';
785
+ }
786
+ }
787
+ /** 获取发送方证书 */
788
+ _getSenderCert(aid, certFingerprint) {
789
+ const certPem = this._keystore.loadCert(aid, certFingerprint);
790
+ const normalized = String(certFingerprint ?? '').trim().toLowerCase();
791
+ if (!certPem)
792
+ return null;
793
+ if (!normalized)
794
+ return certPem;
795
+ return certificateSha256Fingerprint(certPem) === normalized ? certPem : null;
796
+ }
797
+ /** 获取发送方的 identity 公钥(从本地证书缓存) */
798
+ _loadSenderPublicKey(aid, certFingerprint) {
799
+ if (!aid)
800
+ return null;
801
+ const certPem = this._getSenderCert(aid, certFingerprint);
802
+ if (!certPem)
803
+ return null;
804
+ try {
805
+ return pemToCertPublicKey(certPem);
806
+ }
807
+ catch {
808
+ return null;
809
+ }
810
+ }
811
+ /** 用当前身份私钥签名 */
812
+ _signBytes(data) {
813
+ const identity = this._identityFn();
814
+ const privateKeyPem = identity.private_key_pem;
815
+ if (!privateKeyPem) {
816
+ throw new E2EEError('identity private key unavailable');
817
+ }
818
+ return ecdsaSign(privateKeyPem, data).toString('base64');
819
+ }
820
+ /** 加载发送方自己的 identity 私钥 */
821
+ _loadSenderIdentityPrivate() {
822
+ const identity = this._identityFn();
823
+ const privateKeyPem = identity.private_key_pem;
824
+ if (!privateKeyPem) {
825
+ throw new E2EEError('sender identity private key unavailable');
826
+ }
827
+ return crypto.createPrivateKey(privateKeyPem);
828
+ }
829
+ /** 本地 identity 指纹(优先证书 DER SHA-256,缺失时回退到公钥指纹) */
830
+ _localIdentityFingerprint() {
831
+ // 优先用证书指纹(与 PKI 一致)
832
+ const identity = this._identityFn();
833
+ const cert = identity.cert;
834
+ if (cert) {
835
+ return certificateSha256Fingerprint(cert);
836
+ }
837
+ // 无证书时回退到公钥 SPKI 指纹
838
+ const publicKeyDerB64 = identity.public_key_der_b64;
839
+ if (publicKeyDerB64) {
840
+ return fingerprintPublicKeyDer(Buffer.from(publicKeyDerB64, 'base64'));
841
+ }
842
+ const privateKeyPem = identity.private_key_pem;
843
+ if (privateKeyPem) {
844
+ const pk = crypto.createPrivateKey(privateKeyPem);
845
+ const pubKey = crypto.createPublicKey(pk);
846
+ return fingerprintKeyObject(pubKey);
847
+ }
848
+ throw new E2EEError('identity fingerprint unavailable');
849
+ }
850
+ /** 本地证书指纹(优先证书 SHA-256,缺失时回退到 identity 公钥指纹) */
851
+ _localCertFingerprint() {
852
+ return this._localCertSha256Fingerprint() || this._localIdentityFingerprint();
853
+ }
854
+ /** 本地证书的 SHA-256 指纹(用于锁定证书版本) */
855
+ _localCertSha256Fingerprint() {
856
+ const identity = this._identityFn();
857
+ const cert = identity.cert;
858
+ if (!cert)
859
+ return '';
860
+ return certificateSha256Fingerprint(cert);
861
+ }
862
+ /** 构建接收端 AAD */
863
+ _buildInboundAadOffline(message, payload) {
864
+ const aad = payload.aad;
865
+ return {
866
+ from: message.from,
867
+ to: message.to,
868
+ message_id: message.message_id,
869
+ timestamp: message.timestamp,
870
+ encryption_mode: payload.encryption_mode,
871
+ suite: payload.suite || SUITE,
872
+ ephemeral_public_key: payload.ephemeral_public_key,
873
+ recipient_cert_fingerprint: this._localCertFingerprint(),
874
+ sender_cert_fingerprint: (payload.sender_cert_fingerprint ?? aad?.sender_cert_fingerprint),
875
+ prekey_id: (payload.prekey_id ?? aad?.prekey_id),
876
+ };
877
+ }
878
+ /** 清理过期的 prekey 缓存和 seen set 条目(供外部定时调用) */
879
+ cleanExpiredCaches() {
880
+ const now = Date.now() / 1000;
881
+ // 清理过期的 prekey 缓存
882
+ for (const [k, v] of this._prekeyCache) {
883
+ if (now >= v.expireAt)
884
+ this._prekeyCache.delete(k);
885
+ }
886
+ // 清理 seen set(LRU 裁剪)
887
+ this._trimSeenSet();
888
+ }
889
+ }
890
+ //# sourceMappingURL=e2ee.js.map