@agentunion/fastaun 0.2.20 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +63 -23
- package/_packed_docs/CHANGELOG.md +63 -23
- package/_packed_docs/design/2026-05-22-aun-rpc-trace-enhancement.md +542 -0
- package/_packed_docs/protocol/06-/346/234/215/345/212/241/345/215/217/350/256/256.md +1 -24
- package/_packed_docs/protocol/15-/347/246/273/347/272/277/346/216/250/351/200/201/351/200/232/347/237/245/345/215/217/350/256/256.md +419 -0
- package/_packed_docs/protocol/index.md +13 -3
- package/_packed_docs/python-sdk-v2-only-changelog.md +189 -0
- package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +39 -16
- package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +131 -39
- package/_packed_docs/sdk/09-message-rpc-manual.md +30 -67
- package/dist/auth.js +26 -7
- package/dist/auth.js.map +1 -1
- package/dist/client.d.ts +117 -166
- package/dist/client.js +2130 -3419
- package/dist/client.js.map +1 -1
- package/dist/config.d.ts +0 -4
- package/dist/config.js +0 -4
- package/dist/config.js.map +1 -1
- package/dist/e2ee.d.ts +5 -139
- package/dist/e2ee.js +4 -1151
- package/dist/e2ee.js.map +1 -1
- package/dist/errors.d.ts +0 -8
- package/dist/errors.js +0 -14
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +9 -5
- package/dist/index.js +6 -3
- package/dist/index.js.map +1 -1
- package/dist/keystore/aid-db.d.ts +12 -61
- package/dist/keystore/aid-db.js +41 -539
- package/dist/keystore/aid-db.js.map +1 -1
- package/dist/keystore/file.d.ts +5 -41
- package/dist/keystore/file.js +8 -64
- package/dist/keystore/file.js.map +1 -1
- package/dist/keystore/index.d.ts +1 -49
- package/dist/namespaces/auth.d.ts +8 -0
- package/dist/namespaces/auth.js +169 -2
- package/dist/namespaces/auth.js.map +1 -1
- package/dist/protected-headers.d.ts +13 -0
- package/dist/protected-headers.js +47 -0
- package/dist/protected-headers.js.map +1 -0
- package/dist/seq-tracker.d.ts +7 -2
- package/dist/seq-tracker.js +33 -13
- package/dist/seq-tracker.js.map +1 -1
- package/dist/transport.d.ts +11 -1
- package/dist/transport.js +255 -6
- package/dist/transport.js.map +1 -1
- package/dist/types.d.ts +0 -56
- package/dist/v2/crypto/aead.d.ts +20 -0
- package/dist/v2/crypto/aead.js +59 -0
- package/dist/v2/crypto/aead.js.map +1 -0
- package/dist/v2/crypto/canonical.d.ts +20 -0
- package/dist/v2/crypto/canonical.js +119 -0
- package/dist/v2/crypto/canonical.js.map +1 -0
- package/dist/v2/crypto/dh-path.d.ts +39 -0
- package/dist/v2/crypto/dh-path.js +55 -0
- package/dist/v2/crypto/dh-path.js.map +1 -0
- package/dist/v2/crypto/ecdh.d.ts +29 -0
- package/dist/v2/crypto/ecdh.js +122 -0
- package/dist/v2/crypto/ecdh.js.map +1 -0
- package/dist/v2/crypto/ecdsa.d.ts +29 -0
- package/dist/v2/crypto/ecdsa.js +120 -0
- package/dist/v2/crypto/ecdsa.js.map +1 -0
- package/dist/v2/crypto/hkdf.d.ts +19 -0
- package/dist/v2/crypto/hkdf.js +47 -0
- package/dist/v2/crypto/hkdf.js.map +1 -0
- package/dist/v2/crypto/index.d.ts +8 -0
- package/dist/v2/crypto/index.js +8 -0
- package/dist/v2/crypto/index.js.map +1 -0
- package/dist/v2/crypto/recipients.d.ts +32 -0
- package/dist/v2/crypto/recipients.js +183 -0
- package/dist/v2/crypto/recipients.js.map +1 -0
- package/dist/v2/e2ee/decrypt.d.ts +29 -0
- package/dist/v2/e2ee/decrypt.js +159 -0
- package/dist/v2/e2ee/decrypt.js.map +1 -0
- package/dist/v2/e2ee/encrypt-group.d.ts +17 -0
- package/dist/v2/e2ee/encrypt-group.js +143 -0
- package/dist/v2/e2ee/encrypt-group.js.map +1 -0
- package/dist/v2/e2ee/encrypt-p2p.d.ts +31 -0
- package/dist/v2/e2ee/encrypt-p2p.js +190 -0
- package/dist/v2/e2ee/encrypt-p2p.js.map +1 -0
- package/dist/v2/e2ee/index.d.ts +9 -0
- package/dist/v2/e2ee/index.js +9 -0
- package/dist/v2/e2ee/index.js.map +1 -0
- package/dist/v2/e2ee/metadata-auth.d.ts +15 -0
- package/dist/v2/e2ee/metadata-auth.js +50 -0
- package/dist/v2/e2ee/metadata-auth.js.map +1 -0
- package/dist/v2/e2ee/types.d.ts +57 -0
- package/dist/v2/e2ee/types.js +7 -0
- package/dist/v2/e2ee/types.js.map +1 -0
- package/dist/v2/session/index.d.ts +4 -0
- package/dist/v2/session/index.js +3 -0
- package/dist/v2/session/index.js.map +1 -0
- package/dist/v2/session/keystore.d.ts +50 -0
- package/dist/v2/session/keystore.js +138 -0
- package/dist/v2/session/keystore.js.map +1 -0
- package/dist/v2/session/session.d.ts +124 -0
- package/dist/v2/session/session.js +318 -0
- package/dist/v2/session/session.js.map +1 -0
- package/dist/v2/state/commitment.d.ts +58 -0
- package/dist/v2/state/commitment.js +85 -0
- package/dist/v2/state/commitment.js.map +1 -0
- package/dist/v2/state/index.d.ts +2 -0
- package/dist/v2/state/index.js +2 -0
- package/dist/v2/state/index.js.map +1 -0
- package/package.json +4 -3
package/dist/e2ee.js
CHANGED
|
@@ -1,1155 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* E2EE V2-only 兼容入口。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* 内置本地防重放(seen set),裸 WebSocket 开发者无需额外实现。
|
|
4
|
+
* 旧版 manager 已移除;这里仅保留应用层可能直接使用的
|
|
5
|
+
* protected headers helper。
|
|
7
6
|
*/
|
|
8
|
-
|
|
9
|
-
import { E2EEError, E2EEDecryptFailedError } from './errors.js';
|
|
10
|
-
const _noopLogger = {
|
|
11
|
-
error: () => { },
|
|
12
|
-
warn: () => { },
|
|
13
|
-
info: () => { },
|
|
14
|
-
debug: () => { },
|
|
15
|
-
};
|
|
16
|
-
// ── 常量 ───────────────────────────────────────────────────────
|
|
17
|
-
export const SUITE = 'P256_HKDF_SHA256_AES_256_GCM';
|
|
18
|
-
/** 四路 ECDH:prekey + identity */
|
|
19
|
-
export const MODE_PREKEY_ECDH_V2 = 'prekey_ecdh_v2';
|
|
20
|
-
/** 降级:长期公钥加密 */
|
|
21
|
-
export const MODE_LONG_TERM_KEY = 'long_term_key';
|
|
22
|
-
/** 离线消息 AAD 字段 */
|
|
23
|
-
export const AAD_FIELDS_OFFLINE = [
|
|
24
|
-
'from', 'to', 'message_id', 'timestamp',
|
|
25
|
-
'encryption_mode', 'suite', 'ephemeral_public_key',
|
|
26
|
-
'recipient_cert_fingerprint', 'sender_cert_fingerprint',
|
|
27
|
-
'prekey_id',
|
|
28
|
-
];
|
|
29
|
-
/** 离线消息 AAD 匹配字段(不含 timestamp) */
|
|
30
|
-
export const AAD_MATCH_FIELDS_OFFLINE = [
|
|
31
|
-
'from', 'to', 'message_id',
|
|
32
|
-
'encryption_mode', 'suite', 'ephemeral_public_key',
|
|
33
|
-
'recipient_cert_fingerprint', 'sender_cert_fingerprint',
|
|
34
|
-
'prekey_id',
|
|
35
|
-
];
|
|
36
|
-
/** 兼容型可选 AAD 字段:存在时才参与 AAD,不为旧消息补 null。 */
|
|
37
|
-
export const AAD_OPTIONAL_FIELDS = [
|
|
38
|
-
'payload_type', 'protected_headers', 'context_type', 'context_id',
|
|
39
|
-
];
|
|
40
|
-
const METADATA_AUTH_FIELD = '_auth';
|
|
41
|
-
const METADATA_AUTH_ALG = 'HMAC-SHA256';
|
|
42
|
-
const METADATA_KEY_DOMAIN = Buffer.from('aun-envelope-metadata-key-v1', 'utf-8');
|
|
43
|
-
const PROTECTED_HEADERS_DOMAIN = Buffer.from('aun-protected-headers-v1', 'utf-8');
|
|
44
|
-
const PROTECTED_CONTEXT_DOMAIN = Buffer.from('aun-protected-context-v1', 'utf-8');
|
|
45
|
-
/** prekey 私钥本地保留时间(秒)— 7 天 */
|
|
46
|
-
const PREKEY_RETENTION_SECONDS = 7 * 24 * 3600;
|
|
47
|
-
const PREKEY_MIN_KEEP_COUNT = 7;
|
|
48
|
-
/** 端到端保护的信封元数据,语义接近 HTTP headers。 */
|
|
49
|
-
export class ProtectedHeaders {
|
|
50
|
-
_items = {};
|
|
51
|
-
constructor(values) {
|
|
52
|
-
if (values) {
|
|
53
|
-
for (const [key, value] of Object.entries(values)) {
|
|
54
|
-
this.set(key, value);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
static normalizeKey(key) {
|
|
59
|
-
const value = String(key ?? '').trim().toLowerCase();
|
|
60
|
-
if (!value || !/^[a-z0-9_-]+$/.test(value)) {
|
|
61
|
-
throw new E2EEError('protected header key must match [a-z0-9_-]+');
|
|
62
|
-
}
|
|
63
|
-
if (value === METADATA_AUTH_FIELD) {
|
|
64
|
-
throw new E2EEError('protected header key is reserved');
|
|
65
|
-
}
|
|
66
|
-
return value;
|
|
67
|
-
}
|
|
68
|
-
set(key, value) {
|
|
69
|
-
this._items[ProtectedHeaders.normalizeKey(key)] = value == null ? '' : String(value);
|
|
70
|
-
return this;
|
|
71
|
-
}
|
|
72
|
-
get(key, defaultValue = null) {
|
|
73
|
-
const normalized = ProtectedHeaders.normalizeKey(key);
|
|
74
|
-
return Object.prototype.hasOwnProperty.call(this._items, normalized)
|
|
75
|
-
? this._items[normalized]
|
|
76
|
-
: defaultValue;
|
|
77
|
-
}
|
|
78
|
-
remove(key) {
|
|
79
|
-
delete this._items[ProtectedHeaders.normalizeKey(key)];
|
|
80
|
-
return this;
|
|
81
|
-
}
|
|
82
|
-
toObject() {
|
|
83
|
-
return { ...this._items };
|
|
84
|
-
}
|
|
85
|
-
toJSON() {
|
|
86
|
-
return this.toObject();
|
|
87
|
-
}
|
|
88
|
-
static from(values) {
|
|
89
|
-
return new ProtectedHeaders(values ?? {});
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
function prekeyCreatedMarker(prekeyData) {
|
|
93
|
-
return Number(prekeyData.created_at ?? prekeyData.updated_at ?? prekeyData.expires_at ?? 0);
|
|
94
|
-
}
|
|
95
|
-
function latestPrekeyIds(prekeys, keepLatest) {
|
|
96
|
-
if (keepLatest <= 0)
|
|
97
|
-
return new Set();
|
|
98
|
-
return new Set(Object.entries(prekeys)
|
|
99
|
-
.filter(([, data]) => typeof data === 'object' && data !== null)
|
|
100
|
-
.sort((left, right) => {
|
|
101
|
-
const markerDiff = prekeyCreatedMarker(right[1]) - prekeyCreatedMarker(left[1]);
|
|
102
|
-
if (markerDiff !== 0)
|
|
103
|
-
return markerDiff;
|
|
104
|
-
return right[0].localeCompare(left[0]);
|
|
105
|
-
})
|
|
106
|
-
.slice(0, keepLatest)
|
|
107
|
-
.map(([prekeyId]) => prekeyId));
|
|
108
|
-
}
|
|
109
|
-
function loadKeyStorePrekeys(keystore, aid, deviceId = '') {
|
|
110
|
-
const normalizedDeviceId = String(deviceId ?? '').trim();
|
|
111
|
-
if (typeof keystore.loadE2EEPrekeys === 'function') {
|
|
112
|
-
return (keystore.loadE2EEPrekeys(aid, normalizedDeviceId) ?? {});
|
|
113
|
-
}
|
|
114
|
-
throw new Error('keystore missing loadE2EEPrekeys method');
|
|
115
|
-
}
|
|
116
|
-
function saveKeyStorePrekey(keystore, aid, deviceId, prekeyId, prekeyData) {
|
|
117
|
-
const normalizedDeviceId = String(deviceId ?? '').trim();
|
|
118
|
-
if (typeof keystore.saveE2EEPrekey === 'function') {
|
|
119
|
-
keystore.saveE2EEPrekey(aid, prekeyId, prekeyData, normalizedDeviceId);
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
|
-
throw new Error(`keystore ${keystore.constructor?.name ?? 'unknown'} missing saveE2EEPrekey method`);
|
|
123
|
-
}
|
|
124
|
-
function cleanupKeyStorePrekeys(keystore, aid, deviceId, cutoffMs, keepLatest = PREKEY_MIN_KEEP_COUNT) {
|
|
125
|
-
const normalizedDeviceId = String(deviceId ?? '').trim();
|
|
126
|
-
if (typeof keystore.cleanupE2EEPrekeys === 'function') {
|
|
127
|
-
return keystore.cleanupE2EEPrekeys(aid, cutoffMs, keepLatest, normalizedDeviceId) ?? [];
|
|
128
|
-
}
|
|
129
|
-
throw new Error(`keystore ${keystore.constructor?.name ?? 'unknown'} missing cleanupE2EEPrekeys method`);
|
|
130
|
-
}
|
|
131
|
-
// ── 工具函数 ───────────────────────────────────────────────────
|
|
132
|
-
/** 将 PEM 证书/公钥转为 KeyObject */
|
|
133
|
-
function pemToCertPublicKey(certPem) {
|
|
134
|
-
const pem = typeof certPem === 'string' ? certPem : certPem.toString('utf-8');
|
|
135
|
-
try {
|
|
136
|
-
const x509 = new crypto.X509Certificate(pem);
|
|
137
|
-
return x509.publicKey;
|
|
138
|
-
}
|
|
139
|
-
catch {
|
|
140
|
-
return crypto.createPublicKey(pem);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
/** ECDH 共享密钥计算 */
|
|
144
|
-
function ecdhShared(privateKey, publicKey) {
|
|
145
|
-
return crypto.diffieHellman({ privateKey, publicKey });
|
|
146
|
-
}
|
|
147
|
-
/** HKDF-SHA256 派生密钥 */
|
|
148
|
-
function hkdfDeriveSync(ikm, info, length) {
|
|
149
|
-
// Node.js crypto.hkdfSync 在 v16+ 可用
|
|
150
|
-
const derived = crypto.hkdfSync('sha256', ikm, Buffer.alloc(0), info, length);
|
|
151
|
-
return Buffer.from(derived);
|
|
152
|
-
}
|
|
153
|
-
/** AES-256-GCM 加密,返回 {ciphertext, tag, nonce} */
|
|
154
|
-
function aesGcmEncrypt(key, plaintext, aad) {
|
|
155
|
-
const nonce = crypto.randomBytes(12);
|
|
156
|
-
const cipher = crypto.createCipheriv('aes-256-gcm', key, nonce);
|
|
157
|
-
cipher.setAAD(aad);
|
|
158
|
-
const encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
159
|
-
const tag = cipher.getAuthTag();
|
|
160
|
-
return { ciphertext: encrypted, tag, nonce };
|
|
161
|
-
}
|
|
162
|
-
/** AES-256-GCM 解密 */
|
|
163
|
-
function aesGcmDecrypt(key, ciphertext, tag, nonce, aad) {
|
|
164
|
-
const decipher = crypto.createDecipheriv('aes-256-gcm', key, nonce);
|
|
165
|
-
decipher.setAuthTag(tag);
|
|
166
|
-
decipher.setAAD(aad);
|
|
167
|
-
return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
168
|
-
}
|
|
169
|
-
/** ECDSA-SHA256 签名 */
|
|
170
|
-
function ecdsaSign(privateKeyPem, data) {
|
|
171
|
-
const signer = crypto.createSign('SHA256');
|
|
172
|
-
signer.update(data);
|
|
173
|
-
signer.end();
|
|
174
|
-
return signer.sign(privateKeyPem);
|
|
175
|
-
}
|
|
176
|
-
/** ECDSA-SHA256 验签 */
|
|
177
|
-
function ecdsaVerify(publicKey, signature, data) {
|
|
178
|
-
const verifier = crypto.createVerify('SHA256');
|
|
179
|
-
verifier.update(data);
|
|
180
|
-
verifier.end();
|
|
181
|
-
return verifier.verify(publicKey, signature);
|
|
182
|
-
}
|
|
183
|
-
/** 生成 ECDSA P-256 密钥对 */
|
|
184
|
-
function generateECKeyPair() {
|
|
185
|
-
return crypto.generateKeyPairSync('ec', { namedCurve: 'prime256v1' });
|
|
186
|
-
}
|
|
187
|
-
/** 公钥 → DER SPKI 指纹 */
|
|
188
|
-
function fingerprintPublicKeyDer(derBytes) {
|
|
189
|
-
const hash = crypto.createHash('sha256').update(derBytes).digest();
|
|
190
|
-
return `sha256:${hash.toString('hex')}`;
|
|
191
|
-
}
|
|
192
|
-
/** KeyObject 公钥 → 指纹 */
|
|
193
|
-
function fingerprintKeyObject(pubKey) {
|
|
194
|
-
const der = pubKey.export({ type: 'spki', format: 'der' });
|
|
195
|
-
return fingerprintPublicKeyDer(der);
|
|
196
|
-
}
|
|
197
|
-
/** PEM 证书 → 证书 SHA-256 指纹 */
|
|
198
|
-
function fingerprintCertPem(certPem) {
|
|
199
|
-
return certificateSha256Fingerprint(certPem);
|
|
200
|
-
}
|
|
201
|
-
/** PEM/DER 证书 → DER 字节 */
|
|
202
|
-
function certToDerBytes(certPem) {
|
|
203
|
-
const raw = Buffer.isBuffer(certPem) ? Buffer.from(certPem) : Buffer.from(certPem, 'utf-8');
|
|
204
|
-
const text = raw.toString('utf-8');
|
|
205
|
-
if (text.includes('-----BEGIN CERTIFICATE-----')) {
|
|
206
|
-
const body = text
|
|
207
|
-
.replace(/-----BEGIN CERTIFICATE-----/g, '')
|
|
208
|
-
.replace(/-----END CERTIFICATE-----/g, '')
|
|
209
|
-
.replace(/\s+/g, '');
|
|
210
|
-
if (!body) {
|
|
211
|
-
throw new E2EEError('invalid certificate PEM: missing base64 content');
|
|
212
|
-
}
|
|
213
|
-
return Buffer.from(body, 'base64');
|
|
214
|
-
}
|
|
215
|
-
try {
|
|
216
|
-
return new crypto.X509Certificate(raw).raw;
|
|
217
|
-
}
|
|
218
|
-
catch {
|
|
219
|
-
return raw;
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
/** PEM 证书 → 证书 SHA-256 指纹 */
|
|
223
|
-
function certificateSha256Fingerprint(certPem) {
|
|
224
|
-
const der = certToDerBytes(certPem);
|
|
225
|
-
const hash = crypto.createHash('sha256').update(der).digest('hex');
|
|
226
|
-
return `sha256:${hash}`;
|
|
227
|
-
}
|
|
228
|
-
/** 公钥 KeyObject → 未压缩点(0x04 || x || y,65 字节) */
|
|
229
|
-
function publicKeyToUncompressedPoint(pubKey) {
|
|
230
|
-
// 用 JWK 提取 x, y,并显式校验曲线必须是 P-256。
|
|
231
|
-
// 这样 P-384/P-521 错误会在导出阶段立即抛出,而不是延后到解密时才失败。
|
|
232
|
-
const jwk = pubKey.export({ format: 'jwk' });
|
|
233
|
-
if (jwk.kty !== 'EC') {
|
|
234
|
-
throw new E2EEError(`unsupported public key type: ${jwk.kty ?? 'unknown'}`);
|
|
235
|
-
}
|
|
236
|
-
if (jwk.crv !== 'P-256') {
|
|
237
|
-
throw new E2EEError(`unsupported EC curve: ${jwk.crv ?? 'unknown'} (only P-256 is supported)`);
|
|
238
|
-
}
|
|
239
|
-
const x = Buffer.from(jwk.x, 'base64url');
|
|
240
|
-
const y = Buffer.from(jwk.y, 'base64url');
|
|
241
|
-
if (x.length !== 32 || y.length !== 32) {
|
|
242
|
-
throw new E2EEError(`invalid P-256 coordinate length: x=${x.length}, y=${y.length}`);
|
|
243
|
-
}
|
|
244
|
-
return Buffer.concat([Buffer.from([0x04]), x, y]);
|
|
245
|
-
}
|
|
246
|
-
/** 未压缩点 → KeyObject */
|
|
247
|
-
function uncompressedPointToPublicKey(point) {
|
|
248
|
-
// 构造 JWK
|
|
249
|
-
if (point[0] !== 0x04 || point.length !== 65) {
|
|
250
|
-
throw new E2EEError('invalid uncompressed public key point format');
|
|
251
|
-
}
|
|
252
|
-
const x = point.subarray(1, 33).toString('base64url');
|
|
253
|
-
const y = point.subarray(33, 65).toString('base64url');
|
|
254
|
-
return crypto.createPublicKey({
|
|
255
|
-
key: { kty: 'EC', crv: 'P-256', x, y },
|
|
256
|
-
format: 'jwk',
|
|
257
|
-
});
|
|
258
|
-
}
|
|
259
|
-
function canonicalStringify(value) {
|
|
260
|
-
if (value === null || value === undefined)
|
|
261
|
-
return 'null';
|
|
262
|
-
if (Array.isArray(value)) {
|
|
263
|
-
return `[${value.map(item => canonicalStringify(item)).join(',')}]`;
|
|
264
|
-
}
|
|
265
|
-
if (typeof value === 'object') {
|
|
266
|
-
const record = value;
|
|
267
|
-
const pairs = Object.keys(record)
|
|
268
|
-
.sort()
|
|
269
|
-
.map(key => `${JSON.stringify(key)}:${canonicalStringify(record[key])}`);
|
|
270
|
-
return `{${pairs.join(',')}}`;
|
|
271
|
-
}
|
|
272
|
-
return JSON.stringify(value) ?? 'null';
|
|
273
|
-
}
|
|
274
|
-
function normalizeProtectedHeaders(headers) {
|
|
275
|
-
if (headers == null)
|
|
276
|
-
return {};
|
|
277
|
-
if (headers instanceof ProtectedHeaders) {
|
|
278
|
-
return headers.toObject();
|
|
279
|
-
}
|
|
280
|
-
const toObject = headers.toObject;
|
|
281
|
-
if (typeof toObject === 'function') {
|
|
282
|
-
return new ProtectedHeaders(toObject.call(headers)).toObject();
|
|
283
|
-
}
|
|
284
|
-
if (typeof headers !== 'object' || Array.isArray(headers)) {
|
|
285
|
-
throw new E2EEError('protected_headers must be an object');
|
|
286
|
-
}
|
|
287
|
-
return new ProtectedHeaders(headers).toObject();
|
|
288
|
-
}
|
|
289
|
-
function hasOwn(obj, key) {
|
|
290
|
-
return Object.prototype.hasOwnProperty.call(obj, key);
|
|
291
|
-
}
|
|
292
|
-
function metadataBody(metadata) {
|
|
293
|
-
const body = {};
|
|
294
|
-
for (const [key, value] of Object.entries(metadata)) {
|
|
295
|
-
if (key !== METADATA_AUTH_FIELD) {
|
|
296
|
-
body[key] = value;
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
return body;
|
|
300
|
-
}
|
|
301
|
-
function metadataAuthTag(key, domain, body) {
|
|
302
|
-
const metadataKey = crypto.createHmac('sha256', key).update(METADATA_KEY_DOMAIN).digest();
|
|
303
|
-
return crypto.createHmac('sha256', metadataKey)
|
|
304
|
-
.update(domain)
|
|
305
|
-
.update(Buffer.from([0]))
|
|
306
|
-
.update(Buffer.from(canonicalStringify(body), 'utf-8'))
|
|
307
|
-
.digest();
|
|
308
|
-
}
|
|
309
|
-
function withMetadataAuth(metadata, key, domain) {
|
|
310
|
-
const body = metadataBody(metadata);
|
|
311
|
-
if (Object.keys(body).length === 0)
|
|
312
|
-
return {};
|
|
313
|
-
const tag = metadataAuthTag(key, domain, body);
|
|
314
|
-
return {
|
|
315
|
-
...body,
|
|
316
|
-
[METADATA_AUTH_FIELD]: {
|
|
317
|
-
alg: METADATA_AUTH_ALG,
|
|
318
|
-
tag: tag.toString('base64'),
|
|
319
|
-
},
|
|
320
|
-
};
|
|
321
|
-
}
|
|
322
|
-
function verifyMetadataAuth(metadata, key, domain) {
|
|
323
|
-
if (metadata == null)
|
|
324
|
-
return true;
|
|
325
|
-
if (typeof metadata !== 'object' || Array.isArray(metadata))
|
|
326
|
-
return false;
|
|
327
|
-
const record = metadata;
|
|
328
|
-
const auth = record[METADATA_AUTH_FIELD];
|
|
329
|
-
if (!auth || typeof auth !== 'object' || Array.isArray(auth))
|
|
330
|
-
return false;
|
|
331
|
-
const authObj = auth;
|
|
332
|
-
if (authObj.alg !== METADATA_AUTH_ALG)
|
|
333
|
-
return false;
|
|
334
|
-
if (typeof authObj.tag !== 'string' || !authObj.tag)
|
|
335
|
-
return false;
|
|
336
|
-
const body = metadataBody(record);
|
|
337
|
-
if (Object.keys(body).length === 0)
|
|
338
|
-
return false;
|
|
339
|
-
const actual = Buffer.from(authObj.tag, 'base64');
|
|
340
|
-
const expected = metadataAuthTag(key, domain, body);
|
|
341
|
-
return actual.length === expected.length && crypto.timingSafeEqual(actual, expected);
|
|
342
|
-
}
|
|
343
|
-
function normalizeContextMetadata(context) {
|
|
344
|
-
if (!context || typeof context !== 'object' || Array.isArray(context))
|
|
345
|
-
return {};
|
|
346
|
-
return metadataBody(context);
|
|
347
|
-
}
|
|
348
|
-
function exposedEnvelopeMetadata(metadata) {
|
|
349
|
-
if (!metadata || typeof metadata !== 'object' || Array.isArray(metadata))
|
|
350
|
-
return undefined;
|
|
351
|
-
const body = metadataBody(metadata);
|
|
352
|
-
return Object.keys(body).length > 0 ? body : undefined;
|
|
353
|
-
}
|
|
354
|
-
function copyOptionalEnvelopeMetadata(envelope, opts) {
|
|
355
|
-
if (!opts)
|
|
356
|
-
return;
|
|
357
|
-
const payloadType = String(opts?.payloadType ?? '').trim();
|
|
358
|
-
const protectedHeaders = normalizeProtectedHeaders(opts?.protectedHeaders);
|
|
359
|
-
if (payloadType) {
|
|
360
|
-
protectedHeaders.payload_type = payloadType;
|
|
361
|
-
}
|
|
362
|
-
if (Object.keys(protectedHeaders).length > 0) {
|
|
363
|
-
envelope.protected_headers = withMetadataAuth(protectedHeaders, opts.messageKey, PROTECTED_HEADERS_DOMAIN);
|
|
364
|
-
}
|
|
365
|
-
const contextMetadata = normalizeContextMetadata(opts.context);
|
|
366
|
-
if (Object.keys(contextMetadata).length > 0) {
|
|
367
|
-
envelope.context = withMetadataAuth(contextMetadata, opts.messageKey, PROTECTED_CONTEXT_DOMAIN);
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
function aadBytesWithOptionalFields(aad, baseFields) {
|
|
371
|
-
const obj = {};
|
|
372
|
-
for (const field of baseFields) {
|
|
373
|
-
obj[field] = aad[field] ?? null;
|
|
374
|
-
}
|
|
375
|
-
return Buffer.from(canonicalStringify(obj), 'utf-8');
|
|
376
|
-
}
|
|
377
|
-
function verifyEnvelopeMetadataAuth(payload, messageKey) {
|
|
378
|
-
return verifyMetadataAuth(payload.protected_headers, messageKey, PROTECTED_HEADERS_DOMAIN)
|
|
379
|
-
&& verifyMetadataAuth(payload.context, messageKey, PROTECTED_CONTEXT_DOMAIN);
|
|
380
|
-
}
|
|
381
|
-
function validateDecryptedEnvelopeMetadata(decoded, payload, message) {
|
|
382
|
-
if (payload.protected_headers && typeof payload.protected_headers === 'object' && !Array.isArray(payload.protected_headers)) {
|
|
383
|
-
const headers = metadataBody(payload.protected_headers);
|
|
384
|
-
if (hasOwn(headers, 'payload_type')) {
|
|
385
|
-
if (!decoded || typeof decoded !== 'object' || Array.isArray(decoded))
|
|
386
|
-
return false;
|
|
387
|
-
if (String(decoded.type ?? '') !== String(headers.payload_type ?? '')) {
|
|
388
|
-
return false;
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
if (payload.context && typeof payload.context === 'object' && !Array.isArray(payload.context)) {
|
|
393
|
-
const protectedContext = metadataBody(payload.context);
|
|
394
|
-
const outerContext = normalizeContextMetadata(message?.context);
|
|
395
|
-
if (canonicalStringify(outerContext) !== canonicalStringify(protectedContext))
|
|
396
|
-
return false;
|
|
397
|
-
}
|
|
398
|
-
return true;
|
|
399
|
-
}
|
|
400
|
-
/** 离线消息 AAD → 排序紧凑 JSON bytes */
|
|
401
|
-
function aadBytesOffline(aad) {
|
|
402
|
-
return aadBytesWithOptionalFields(aad, AAD_FIELDS_OFFLINE);
|
|
403
|
-
}
|
|
404
|
-
/** AAD 匹配检查 */
|
|
405
|
-
function aadMatchesOffline(expected, actual) {
|
|
406
|
-
for (const field of AAD_MATCH_FIELDS_OFFLINE) {
|
|
407
|
-
if (String(expected[field] ?? '') !== String(actual[field] ?? '')) {
|
|
408
|
-
return false;
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
return true;
|
|
412
|
-
}
|
|
413
|
-
// ── E2EEManager 类 ────────────────────────────────────────────
|
|
414
|
-
export class E2EEManager {
|
|
415
|
-
_identityFn;
|
|
416
|
-
_deviceIdFn;
|
|
417
|
-
_keystore;
|
|
418
|
-
/** 本地防重放 seen set */
|
|
419
|
-
_seenMessages = new Map();
|
|
420
|
-
_seenMaxSize = 50000;
|
|
421
|
-
/** 对方 prekey 内存缓存(TTL) */
|
|
422
|
-
_prekeyCache = new Map();
|
|
423
|
-
_prekeyCacheTtl;
|
|
424
|
-
/** 本地 prekey 私钥内存缓存 {prekeyId: privateKeyPem} */
|
|
425
|
-
_localPrekeyCache = new Map();
|
|
426
|
-
/** 防重放时间窗口(秒) */
|
|
427
|
-
_replayWindowSeconds;
|
|
428
|
-
_logger;
|
|
429
|
-
constructor(opts) {
|
|
430
|
-
this._identityFn = opts.identityFn;
|
|
431
|
-
this._deviceIdFn = opts.deviceIdFn ?? (() => '');
|
|
432
|
-
this._keystore = opts.keystore;
|
|
433
|
-
this._prekeyCacheTtl = opts.prekeyCacheTtl ?? 3600;
|
|
434
|
-
this._replayWindowSeconds = opts.replayWindowSeconds ?? 300;
|
|
435
|
-
this._logger = opts.logger ?? _noopLogger;
|
|
436
|
-
}
|
|
437
|
-
// ── 便利方法 ──────────────────────────────────────────────
|
|
438
|
-
/**
|
|
439
|
-
* 加密消息(便利方法)。
|
|
440
|
-
* 有 prekey 时用 prekey_ecdh_v2(四路 ECDH),无 prekey 时降级为 long_term_key。
|
|
441
|
-
*/
|
|
442
|
-
encryptMessage(toAid, payload, opts) {
|
|
443
|
-
const messageId = opts.messageId ?? crypto.randomUUID();
|
|
444
|
-
const timestamp = opts.timestamp ?? Math.floor(Date.now());
|
|
445
|
-
return this.encryptOutbound(toAid, payload, opts.peerCertPem, opts.prekey ?? null, messageId, timestamp, opts.protectedHeaders ?? opts.protected_headers ?? opts.headers, opts.context ?? null);
|
|
446
|
-
}
|
|
447
|
-
// ── 加密 ─────────────────────────────────────────────────
|
|
448
|
-
/**
|
|
449
|
-
* 加密出站消息:有 prekey → prekey_ecdh_v2(四路 ECDH),无 prekey → long_term_key。
|
|
450
|
-
* 返回 [envelope, resultInfo]。
|
|
451
|
-
*/
|
|
452
|
-
encryptOutbound(peerAid, payload, peerCertPem, prekey, messageId, timestamp, protectedHeaders, context) {
|
|
453
|
-
this._logger.debug(`encrypt start: to=${peerAid}, mid=${messageId}, hasPrekey=${prekey != null}`);
|
|
454
|
-
// 传入 prekey → 缓存;传入 null → 查缓存
|
|
455
|
-
if (prekey != null) {
|
|
456
|
-
this.cachePrekey(peerAid, prekey);
|
|
457
|
-
}
|
|
458
|
-
else {
|
|
459
|
-
prekey = this.getCachedPrekey(peerAid);
|
|
460
|
-
}
|
|
461
|
-
if (prekey) {
|
|
462
|
-
try {
|
|
463
|
-
const envelope = this._encryptWithPrekey(peerAid, payload, prekey, peerCertPem, messageId, timestamp, protectedHeaders, context);
|
|
464
|
-
this._logger.debug(`encrypt success: mode=prekey_ecdh_v2, to=${peerAid}, mid=${messageId}`);
|
|
465
|
-
return [envelope, {
|
|
466
|
-
encrypted: true,
|
|
467
|
-
forward_secrecy: true,
|
|
468
|
-
mode: MODE_PREKEY_ECDH_V2,
|
|
469
|
-
degraded: false,
|
|
470
|
-
}];
|
|
471
|
-
}
|
|
472
|
-
catch (exc) {
|
|
473
|
-
// prekey 加密失败,降级到 long_term_key — 记录异常以便排查安全降级原因
|
|
474
|
-
this._logger.warn(`prekey encrypt failed, degrading to long_term_key: to=${peerAid}, mid=${messageId}, error=${exc instanceof Error ? exc.message : String(exc)}`);
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
const envelope = this._encryptWithLongTermKey(peerAid, payload, peerCertPem, messageId, timestamp, protectedHeaders, context);
|
|
478
|
-
const degraded = prekey != null;
|
|
479
|
-
this._logger.debug(`encrypt success: mode=long_term_key, to=${peerAid}, mid=${messageId}, degraded=${degraded}`);
|
|
480
|
-
return [envelope, {
|
|
481
|
-
encrypted: true,
|
|
482
|
-
forward_secrecy: false,
|
|
483
|
-
mode: MODE_LONG_TERM_KEY,
|
|
484
|
-
degraded,
|
|
485
|
-
degradation_reason: degraded ? 'prekey_encrypt_failed' : 'no_prekey_available',
|
|
486
|
-
}];
|
|
487
|
-
}
|
|
488
|
-
/** 使用对方 prekey 加密(prekey_ecdh_v2 模式,四路 ECDH + 发送方签名) */
|
|
489
|
-
_encryptWithPrekey(peerAid, payload, prekey, peerCertPem, messageId, timestamp, protectedHeaders, context) {
|
|
490
|
-
this._logger.debug(`_encryptWithPrekey enter: to=${peerAid}, mid=${messageId}, prekey_id=${String(prekey.prekey_id ?? '')}`);
|
|
491
|
-
const peerIdentityPublic = pemToCertPublicKey(peerCertPem);
|
|
492
|
-
const expectedCertFingerprint = String(prekey.cert_fingerprint ?? '').trim().toLowerCase();
|
|
493
|
-
if (expectedCertFingerprint) {
|
|
494
|
-
const actualCertFingerprint = certificateSha256Fingerprint(peerCertPem);
|
|
495
|
-
if (actualCertFingerprint !== expectedCertFingerprint) {
|
|
496
|
-
throw new E2EEError('prekey cert fingerprint mismatch');
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
// 验证 prekey 签名
|
|
500
|
-
const createdAt = prekey.created_at;
|
|
501
|
-
let signData;
|
|
502
|
-
if (createdAt != null) {
|
|
503
|
-
signData = Buffer.from(`${prekey.prekey_id}|${prekey.public_key}|${createdAt}`, 'utf-8');
|
|
504
|
-
}
|
|
505
|
-
else {
|
|
506
|
-
signData = Buffer.from(`${prekey.prekey_id}|${prekey.public_key}`, 'utf-8');
|
|
507
|
-
}
|
|
508
|
-
const sigBytes = Buffer.from(prekey.signature, 'base64');
|
|
509
|
-
if (!ecdsaVerify(peerIdentityPublic, sigBytes, signData)) {
|
|
510
|
-
throw new E2EEError('prekey signature verification failed');
|
|
511
|
-
}
|
|
512
|
-
// 导入对方 prekey 公钥(DER SPKI)
|
|
513
|
-
const peerPrekeyDer = Buffer.from(prekey.public_key, 'base64');
|
|
514
|
-
const peerPrekeyPublic = crypto.createPublicKey({ key: peerPrekeyDer, format: 'der', type: 'spki' });
|
|
515
|
-
// 加载发送方自己的 identity 私钥
|
|
516
|
-
const senderIdentityPrivate = this._loadSenderIdentityPrivate();
|
|
517
|
-
// 生成临时 ECDH 密钥对
|
|
518
|
-
const { privateKey: ephemeralPrivate, publicKey: ephemeralPublic } = generateECKeyPair();
|
|
519
|
-
const ephemeralPublicBytes = publicKeyToUncompressedPoint(ephemeralPublic);
|
|
520
|
-
// 四路 ECDH + HKDF
|
|
521
|
-
const dh1 = ecdhShared(ephemeralPrivate, peerPrekeyPublic);
|
|
522
|
-
const dh2 = ecdhShared(ephemeralPrivate, peerIdentityPublic);
|
|
523
|
-
const dh3 = ecdhShared(senderIdentityPrivate, peerPrekeyPublic);
|
|
524
|
-
const dh4 = ecdhShared(senderIdentityPrivate, peerIdentityPublic);
|
|
525
|
-
const combined = Buffer.concat([dh1, dh2, dh3, dh4]);
|
|
526
|
-
const messageKey = hkdfDeriveSync(combined, Buffer.from(`aun-prekey-v2:${prekey.prekey_id}`, 'utf-8'), 32);
|
|
527
|
-
// AES-GCM 加密
|
|
528
|
-
const plaintext = Buffer.from(JSON.stringify(payload), 'utf-8');
|
|
529
|
-
const senderFingerprint = this._localCertSha256Fingerprint() || this._localIdentityFingerprint();
|
|
530
|
-
const recipientFingerprint = fingerprintCertPem(peerCertPem);
|
|
531
|
-
const ephemeralPkB64 = ephemeralPublicBytes.toString('base64');
|
|
532
|
-
const aad = {
|
|
533
|
-
from: this._currentAid(),
|
|
534
|
-
to: peerAid,
|
|
535
|
-
message_id: messageId,
|
|
536
|
-
timestamp,
|
|
537
|
-
encryption_mode: MODE_PREKEY_ECDH_V2,
|
|
538
|
-
suite: SUITE,
|
|
539
|
-
ephemeral_public_key: ephemeralPkB64,
|
|
540
|
-
recipient_cert_fingerprint: recipientFingerprint,
|
|
541
|
-
sender_cert_fingerprint: senderFingerprint,
|
|
542
|
-
prekey_id: prekey.prekey_id,
|
|
543
|
-
};
|
|
544
|
-
const envelope = {
|
|
545
|
-
type: 'e2ee.encrypted',
|
|
546
|
-
version: '1',
|
|
547
|
-
encryption_mode: MODE_PREKEY_ECDH_V2,
|
|
548
|
-
suite: SUITE,
|
|
549
|
-
prekey_id: prekey.prekey_id,
|
|
550
|
-
ephemeral_public_key: ephemeralPkB64,
|
|
551
|
-
};
|
|
552
|
-
copyOptionalEnvelopeMetadata(envelope, {
|
|
553
|
-
messageKey,
|
|
554
|
-
payloadType: payload.type,
|
|
555
|
-
protectedHeaders,
|
|
556
|
-
context,
|
|
557
|
-
});
|
|
558
|
-
const aadBytes = aadBytesOffline(aad);
|
|
559
|
-
const { ciphertext, tag, nonce } = aesGcmEncrypt(messageKey, plaintext, aadBytes);
|
|
560
|
-
envelope.nonce = nonce.toString('base64');
|
|
561
|
-
envelope.ciphertext = ciphertext.toString('base64');
|
|
562
|
-
envelope.tag = tag.toString('base64');
|
|
563
|
-
envelope.aad = aad;
|
|
564
|
-
// 发送方签名:对 ciphertext + tag + aad_bytes 签名(不可否认性)
|
|
565
|
-
const signPayload = Buffer.concat([ciphertext, tag, aadBytes]);
|
|
566
|
-
envelope.sender_signature = this._signBytes(signPayload);
|
|
567
|
-
envelope.sender_cert_fingerprint = senderFingerprint;
|
|
568
|
-
return envelope;
|
|
569
|
-
}
|
|
570
|
-
/** 使用 2DH 加密(long_term_key 模式 + 发送方签名) */
|
|
571
|
-
_encryptWithLongTermKey(peerAid, payload, peerCertPem, messageId, timestamp, protectedHeaders, context) {
|
|
572
|
-
this._logger.debug(`_encryptWithLongTermKey enter: to=${peerAid}, mid=${messageId}`);
|
|
573
|
-
const peerPublicKey = pemToCertPublicKey(peerCertPem);
|
|
574
|
-
const senderIdentityPrivate = this._loadSenderIdentityPrivate();
|
|
575
|
-
// 生成临时密钥对
|
|
576
|
-
const { privateKey: ephemeralPrivate, publicKey: ephemeralPublic } = generateECKeyPair();
|
|
577
|
-
const ephemeralPublicBytes = publicKeyToUncompressedPoint(ephemeralPublic);
|
|
578
|
-
// 2DH + HKDF
|
|
579
|
-
const dh1 = ecdhShared(ephemeralPrivate, peerPublicKey);
|
|
580
|
-
const dh2 = ecdhShared(senderIdentityPrivate, peerPublicKey);
|
|
581
|
-
const combined = Buffer.concat([dh1, dh2]);
|
|
582
|
-
const messageKey = hkdfDeriveSync(combined, Buffer.from('aun-longterm-v2', 'utf-8'), 32);
|
|
583
|
-
const plaintext = Buffer.from(JSON.stringify(payload), 'utf-8');
|
|
584
|
-
const senderFingerprint = this._localCertSha256Fingerprint() || this._localIdentityFingerprint();
|
|
585
|
-
const recipientFingerprint = fingerprintCertPem(peerCertPem);
|
|
586
|
-
const ephemeralPkB64 = ephemeralPublicBytes.toString('base64');
|
|
587
|
-
const aad = {
|
|
588
|
-
from: this._currentAid(),
|
|
589
|
-
to: peerAid,
|
|
590
|
-
message_id: messageId,
|
|
591
|
-
timestamp,
|
|
592
|
-
encryption_mode: MODE_LONG_TERM_KEY,
|
|
593
|
-
suite: SUITE,
|
|
594
|
-
ephemeral_public_key: ephemeralPkB64,
|
|
595
|
-
recipient_cert_fingerprint: recipientFingerprint,
|
|
596
|
-
sender_cert_fingerprint: senderFingerprint,
|
|
597
|
-
};
|
|
598
|
-
const envelope = {
|
|
599
|
-
type: 'e2ee.encrypted',
|
|
600
|
-
version: '1',
|
|
601
|
-
encryption_mode: MODE_LONG_TERM_KEY,
|
|
602
|
-
suite: SUITE,
|
|
603
|
-
ephemeral_public_key: ephemeralPkB64,
|
|
604
|
-
};
|
|
605
|
-
copyOptionalEnvelopeMetadata(envelope, {
|
|
606
|
-
messageKey,
|
|
607
|
-
payloadType: payload.type,
|
|
608
|
-
protectedHeaders,
|
|
609
|
-
context,
|
|
610
|
-
});
|
|
611
|
-
const aadBytes = aadBytesOffline(aad);
|
|
612
|
-
const { ciphertext, tag, nonce } = aesGcmEncrypt(messageKey, plaintext, aadBytes);
|
|
613
|
-
envelope.nonce = nonce.toString('base64');
|
|
614
|
-
envelope.ciphertext = ciphertext.toString('base64');
|
|
615
|
-
envelope.tag = tag.toString('base64');
|
|
616
|
-
envelope.aad = aad;
|
|
617
|
-
// 发送方签名(不可否认性)
|
|
618
|
-
const signPayload = Buffer.concat([ciphertext, tag, aadBytes]);
|
|
619
|
-
envelope.sender_signature = this._signBytes(signPayload);
|
|
620
|
-
envelope.sender_cert_fingerprint = senderFingerprint;
|
|
621
|
-
return envelope;
|
|
622
|
-
}
|
|
623
|
-
// ── 解密 ─────────────────────────────────────────────────
|
|
624
|
-
/** 解密单条消息(便利方法,内置本地防重放 + timestamp 窗口) */
|
|
625
|
-
decryptMessage(message) {
|
|
626
|
-
const payload = message.payload;
|
|
627
|
-
if (!payload || typeof payload !== 'object')
|
|
628
|
-
return message;
|
|
629
|
-
if (payload.type !== 'e2ee.encrypted')
|
|
630
|
-
return message;
|
|
631
|
-
if (!this._shouldDecryptForCurrentAid(message, payload))
|
|
632
|
-
return message;
|
|
633
|
-
const fromAid = (message.from ?? '');
|
|
634
|
-
const msgId = (message.message_id ?? '');
|
|
635
|
-
this._logger.debug(`decrypt message start: from=${fromAid}, mid=${msgId}`);
|
|
636
|
-
// timestamp 窗口检查
|
|
637
|
-
const ts = (message.timestamp ?? payload.aad?.timestamp);
|
|
638
|
-
if (typeof ts === 'number' && this._replayWindowSeconds > 0) {
|
|
639
|
-
const nowMs = Date.now();
|
|
640
|
-
const diffS = Math.abs(nowMs - ts) / 1000;
|
|
641
|
-
if (diffS > this._replayWindowSeconds) {
|
|
642
|
-
this._logger.warn(`message dropped: outside time window: from=${fromAid}, mid=${msgId}, diffS=${diffS.toFixed(1)}`);
|
|
643
|
-
return null;
|
|
644
|
-
}
|
|
645
|
-
}
|
|
646
|
-
// H27: 本地防重放——必须在 _decryptMessage 成功后再 set seenKey,
|
|
647
|
-
// 否则解密/验签失败时合法重传会被当作重复丢弃。
|
|
648
|
-
const messageId = message.message_id || '';
|
|
649
|
-
const fromAidKey = message.from || '';
|
|
650
|
-
let seenKey = '';
|
|
651
|
-
if (messageId && fromAidKey) {
|
|
652
|
-
seenKey = `${fromAidKey}:${messageId}`;
|
|
653
|
-
if (this._seenMessages.has(seenKey)) {
|
|
654
|
-
this._logger.debug(`message replay blocked: from=${fromAidKey}, mid=${messageId}`);
|
|
655
|
-
return null;
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
const result = this._decryptMessage(message);
|
|
659
|
-
if (result !== null && seenKey) {
|
|
660
|
-
this._seenMessages.set(seenKey, true);
|
|
661
|
-
this._trimSeenSet();
|
|
662
|
-
}
|
|
663
|
-
return result;
|
|
664
|
-
}
|
|
665
|
-
/** 解密入站消息(不消耗 seen set,用于 pull 场景) */
|
|
666
|
-
_decryptMessage(message) {
|
|
667
|
-
const payload = message.payload;
|
|
668
|
-
if (typeof payload === 'object' && !this._shouldDecryptForCurrentAid(message, payload)) {
|
|
669
|
-
return message;
|
|
670
|
-
}
|
|
671
|
-
const encryptionMode = payload.encryption_mode || '';
|
|
672
|
-
// 验证发送方签名(适用于所有模式)
|
|
673
|
-
try {
|
|
674
|
-
this._verifySenderSignature(payload, message);
|
|
675
|
-
}
|
|
676
|
-
catch (exc) {
|
|
677
|
-
this._logger.warn('sender signature verification failed');
|
|
678
|
-
return null;
|
|
679
|
-
}
|
|
680
|
-
if (encryptionMode === MODE_PREKEY_ECDH_V2) {
|
|
681
|
-
return this._decryptMessagePrekeyV2(message);
|
|
682
|
-
}
|
|
683
|
-
else if (encryptionMode === MODE_LONG_TERM_KEY) {
|
|
684
|
-
return this._decryptMessageLongTerm(message);
|
|
685
|
-
}
|
|
686
|
-
return null;
|
|
687
|
-
}
|
|
688
|
-
/** 解密 prekey_ecdh_v2 模式的消息(四路 ECDH) */
|
|
689
|
-
_decryptMessagePrekeyV2(message) {
|
|
690
|
-
const payload = message.payload;
|
|
691
|
-
try {
|
|
692
|
-
const ephemeralPublicBytes = Buffer.from(payload.ephemeral_public_key, 'base64');
|
|
693
|
-
const prekeyId = payload.prekey_id || '';
|
|
694
|
-
const nonce = Buffer.from(payload.nonce, 'base64');
|
|
695
|
-
const ciphertext = Buffer.from(payload.ciphertext, 'base64');
|
|
696
|
-
const tag = Buffer.from(payload.tag, 'base64');
|
|
697
|
-
// 加载 prekey 私钥
|
|
698
|
-
const prekeyPrivateKey = this._loadPrekeyPrivateKey(prekeyId);
|
|
699
|
-
if (!prekeyPrivateKey) {
|
|
700
|
-
throw new E2EEError(`prekey not found: ${prekeyId}`);
|
|
701
|
-
}
|
|
702
|
-
// 加载接收方自己的 identity 私钥
|
|
703
|
-
const myAid = this._currentAid();
|
|
704
|
-
if (!myAid)
|
|
705
|
-
throw new E2EEError('AID unavailable');
|
|
706
|
-
const keyPair = this._keystore.loadKeyPair(myAid);
|
|
707
|
-
if (!keyPair || !keyPair.private_key_pem) {
|
|
708
|
-
throw new E2EEError('Identity private key not found');
|
|
709
|
-
}
|
|
710
|
-
const myIdentityPrivate = crypto.createPrivateKey(keyPair.private_key_pem);
|
|
711
|
-
// 获取发送方公钥
|
|
712
|
-
const fromAid = (message.from ?? payload.aad?.from);
|
|
713
|
-
const senderCertFingerprint = String(payload.sender_cert_fingerprint ?? payload.aad?.sender_cert_fingerprint ?? '').trim().toLowerCase();
|
|
714
|
-
const senderPublicKey = this._loadSenderPublicKey(fromAid, senderCertFingerprint || undefined);
|
|
715
|
-
if (!senderPublicKey) {
|
|
716
|
-
throw new E2EEError(`sender public key not found for ${fromAid}`);
|
|
717
|
-
}
|
|
718
|
-
// 四路 ECDH + HKDF
|
|
719
|
-
const ephemeralPublic = uncompressedPointToPublicKey(ephemeralPublicBytes);
|
|
720
|
-
const dh1 = ecdhShared(prekeyPrivateKey, ephemeralPublic);
|
|
721
|
-
const dh2 = ecdhShared(myIdentityPrivate, ephemeralPublic);
|
|
722
|
-
const dh3 = ecdhShared(prekeyPrivateKey, senderPublicKey);
|
|
723
|
-
const dh4 = ecdhShared(myIdentityPrivate, senderPublicKey);
|
|
724
|
-
const combined = Buffer.concat([dh1, dh2, dh3, dh4]);
|
|
725
|
-
const messageKey = hkdfDeriveSync(combined, Buffer.from(`aun-prekey-v2:${prekeyId}`, 'utf-8'), 32);
|
|
726
|
-
// 验证 AAD 并解密
|
|
727
|
-
const aad = payload.aad;
|
|
728
|
-
let aadBytes;
|
|
729
|
-
if (aad && typeof aad === 'object') {
|
|
730
|
-
const expectedAad = this._buildInboundAadOffline(message, payload);
|
|
731
|
-
if (!aadMatchesOffline(expectedAad, aad)) {
|
|
732
|
-
throw new E2EEDecryptFailedError('aad mismatch');
|
|
733
|
-
}
|
|
734
|
-
aadBytes = aadBytesOffline(aad);
|
|
735
|
-
}
|
|
736
|
-
else {
|
|
737
|
-
aadBytes = Buffer.alloc(0);
|
|
738
|
-
}
|
|
739
|
-
if (!verifyEnvelopeMetadataAuth(payload, messageKey)) {
|
|
740
|
-
throw new E2EEDecryptFailedError('envelope metadata auth failed');
|
|
741
|
-
}
|
|
742
|
-
const plaintext = aesGcmDecrypt(messageKey, ciphertext, tag, nonce, aadBytes);
|
|
743
|
-
const decoded = JSON.parse(plaintext.toString('utf-8'));
|
|
744
|
-
if (!validateDecryptedEnvelopeMetadata(decoded, payload, message)) {
|
|
745
|
-
throw new E2EEDecryptFailedError('envelope metadata mismatch');
|
|
746
|
-
}
|
|
747
|
-
const e2ee = {
|
|
748
|
-
encryption_mode: MODE_PREKEY_ECDH_V2,
|
|
749
|
-
suite: payload.suite || SUITE,
|
|
750
|
-
prekey_id: prekeyId,
|
|
751
|
-
};
|
|
752
|
-
const protectedHeaders = exposedEnvelopeMetadata(payload.protected_headers);
|
|
753
|
-
if (protectedHeaders)
|
|
754
|
-
e2ee.protected_headers = protectedHeaders;
|
|
755
|
-
const context = exposedEnvelopeMetadata(payload.context);
|
|
756
|
-
if (context)
|
|
757
|
-
e2ee.context = context;
|
|
758
|
-
this._logger.debug(`_decryptMessagePrekeyV2 success: from=${String(message.from ?? '')}, mid=${String(message.message_id ?? '')}, prekey_id=${prekeyId}`);
|
|
759
|
-
return {
|
|
760
|
-
...message,
|
|
761
|
-
payload: decoded,
|
|
762
|
-
encrypted: true,
|
|
763
|
-
e2ee,
|
|
764
|
-
};
|
|
765
|
-
}
|
|
766
|
-
catch (exc) {
|
|
767
|
-
const fromAid = (message.from ?? '');
|
|
768
|
-
const msgId = (message.message_id ?? '');
|
|
769
|
-
this._logger.error(`解密失败: mode=prekey_ecdh_v2, from=${fromAid}, mid=${msgId}`, exc instanceof Error ? exc : new Error(String(exc)));
|
|
770
|
-
return null;
|
|
771
|
-
}
|
|
772
|
-
}
|
|
773
|
-
/** 解密 long_term_key 模式的消息(2DH) */
|
|
774
|
-
_decryptMessageLongTerm(message) {
|
|
775
|
-
const payload = message.payload;
|
|
776
|
-
try {
|
|
777
|
-
const ephemeralPublicBytes = Buffer.from(payload.ephemeral_public_key, 'base64');
|
|
778
|
-
const nonce = Buffer.from(payload.nonce, 'base64');
|
|
779
|
-
const ciphertext = Buffer.from(payload.ciphertext, 'base64');
|
|
780
|
-
const tag = Buffer.from(payload.tag, 'base64');
|
|
781
|
-
const myAid = this._currentAid();
|
|
782
|
-
if (!myAid)
|
|
783
|
-
throw new E2EEError('AID unavailable');
|
|
784
|
-
const keyPair = this._keystore.loadKeyPair(myAid);
|
|
785
|
-
if (!keyPair || !keyPair.private_key_pem) {
|
|
786
|
-
throw new E2EEError('Private key not found');
|
|
787
|
-
}
|
|
788
|
-
const privateKey = crypto.createPrivateKey(keyPair.private_key_pem);
|
|
789
|
-
// 获取发送方公钥
|
|
790
|
-
const fromAid = (message.from ?? payload.aad?.from);
|
|
791
|
-
const senderCertFingerprint = String(payload.sender_cert_fingerprint ?? payload.aad?.sender_cert_fingerprint ?? '').trim().toLowerCase();
|
|
792
|
-
const senderPublicKey = this._loadSenderPublicKey(fromAid, senderCertFingerprint || undefined);
|
|
793
|
-
if (!senderPublicKey) {
|
|
794
|
-
throw new E2EEError(`sender public key not found for ${fromAid}`);
|
|
795
|
-
}
|
|
796
|
-
// 2DH + HKDF
|
|
797
|
-
const ephemeralPublic = uncompressedPointToPublicKey(ephemeralPublicBytes);
|
|
798
|
-
const dh1 = ecdhShared(privateKey, ephemeralPublic);
|
|
799
|
-
const dh2 = ecdhShared(privateKey, senderPublicKey);
|
|
800
|
-
const combined = Buffer.concat([dh1, dh2]);
|
|
801
|
-
const messageKey = hkdfDeriveSync(combined, Buffer.from('aun-longterm-v2', 'utf-8'), 32);
|
|
802
|
-
const aad = payload.aad;
|
|
803
|
-
let aadBytes;
|
|
804
|
-
if (aad && typeof aad === 'object') {
|
|
805
|
-
const expectedAad = this._buildInboundAadOffline(message, payload);
|
|
806
|
-
if (!aadMatchesOffline(expectedAad, aad)) {
|
|
807
|
-
throw new E2EEDecryptFailedError('aad mismatch');
|
|
808
|
-
}
|
|
809
|
-
aadBytes = aadBytesOffline(aad);
|
|
810
|
-
}
|
|
811
|
-
else {
|
|
812
|
-
aadBytes = Buffer.alloc(0);
|
|
813
|
-
}
|
|
814
|
-
if (!verifyEnvelopeMetadataAuth(payload, messageKey)) {
|
|
815
|
-
throw new E2EEDecryptFailedError('envelope metadata auth failed');
|
|
816
|
-
}
|
|
817
|
-
const plaintext = aesGcmDecrypt(messageKey, ciphertext, tag, nonce, aadBytes);
|
|
818
|
-
const decoded = JSON.parse(plaintext.toString('utf-8'));
|
|
819
|
-
if (!validateDecryptedEnvelopeMetadata(decoded, payload, message)) {
|
|
820
|
-
throw new E2EEDecryptFailedError('envelope metadata mismatch');
|
|
821
|
-
}
|
|
822
|
-
const e2ee = {
|
|
823
|
-
encryption_mode: MODE_LONG_TERM_KEY,
|
|
824
|
-
suite: payload.suite,
|
|
825
|
-
};
|
|
826
|
-
const protectedHeaders = exposedEnvelopeMetadata(payload.protected_headers);
|
|
827
|
-
if (protectedHeaders)
|
|
828
|
-
e2ee.protected_headers = protectedHeaders;
|
|
829
|
-
const context = exposedEnvelopeMetadata(payload.context);
|
|
830
|
-
if (context)
|
|
831
|
-
e2ee.context = context;
|
|
832
|
-
this._logger.debug(`_decryptMessageLongTerm success: from=${String(message.from ?? '')}, mid=${String(message.message_id ?? '')}`);
|
|
833
|
-
return {
|
|
834
|
-
...message,
|
|
835
|
-
payload: decoded,
|
|
836
|
-
encrypted: true,
|
|
837
|
-
e2ee,
|
|
838
|
-
};
|
|
839
|
-
}
|
|
840
|
-
catch (exc) {
|
|
841
|
-
const fromAid = (message.from ?? '');
|
|
842
|
-
const msgId = (message.message_id ?? '');
|
|
843
|
-
this._logger.error(`解密失败: mode=long_term_key, from=${fromAid}, mid=${msgId}`, exc instanceof Error ? exc : new Error(String(exc)));
|
|
844
|
-
return null;
|
|
845
|
-
}
|
|
846
|
-
}
|
|
847
|
-
// ── 发送方签名验证 ─────────────────────────────────────────
|
|
848
|
-
_verifySenderSignature(payload, message) {
|
|
849
|
-
const senderSigB64 = payload.sender_signature;
|
|
850
|
-
if (!senderSigB64) {
|
|
851
|
-
throw new E2EEDecryptFailedError('sender_signature missing: 拒绝无发送方签名的消息');
|
|
852
|
-
}
|
|
853
|
-
const fromAid = (message.from ?? payload.aad?.from);
|
|
854
|
-
if (!fromAid) {
|
|
855
|
-
throw new E2EEDecryptFailedError('from_aid missing in message');
|
|
856
|
-
}
|
|
857
|
-
const senderCertFingerprint = String(payload.sender_cert_fingerprint ?? payload.aad?.sender_cert_fingerprint ?? '').trim().toLowerCase();
|
|
858
|
-
const senderCertPem = this._getSenderCert(fromAid, senderCertFingerprint || undefined);
|
|
859
|
-
if (!senderCertPem) {
|
|
860
|
-
throw new E2EEDecryptFailedError(`sender cert not found for ${fromAid}`);
|
|
861
|
-
}
|
|
862
|
-
const senderPublicKey = pemToCertPublicKey(senderCertPem);
|
|
863
|
-
// 重建签名载荷
|
|
864
|
-
const ciphertextBuf = Buffer.from(payload.ciphertext, 'base64');
|
|
865
|
-
const tagBuf = Buffer.from(payload.tag, 'base64');
|
|
866
|
-
const aad = payload.aad;
|
|
867
|
-
const aadBytes = (aad && typeof aad === 'object') ? aadBytesOffline(aad) : Buffer.alloc(0);
|
|
868
|
-
const signPayload = Buffer.concat([ciphertextBuf, tagBuf, aadBytes]);
|
|
869
|
-
const sigBytes = Buffer.from(senderSigB64, 'base64');
|
|
870
|
-
if (!ecdsaVerify(senderPublicKey, sigBytes, signPayload)) {
|
|
871
|
-
throw new E2EEDecryptFailedError('sender signature verification failed');
|
|
872
|
-
}
|
|
873
|
-
}
|
|
874
|
-
// ── Prekey 缓存 ────────────────────────────────────────────
|
|
875
|
-
/** 缓存对方的 prekey */
|
|
876
|
-
cachePrekey(peerAid, prekey) {
|
|
877
|
-
this._prekeyCache.set(peerAid, {
|
|
878
|
-
prekey,
|
|
879
|
-
expireAt: Date.now() / 1000 + this._prekeyCacheTtl,
|
|
880
|
-
});
|
|
881
|
-
}
|
|
882
|
-
/** 获取缓存的 prekey(过期返回 null) */
|
|
883
|
-
getCachedPrekey(peerAid) {
|
|
884
|
-
const cached = this._prekeyCache.get(peerAid);
|
|
885
|
-
if (!cached)
|
|
886
|
-
return null;
|
|
887
|
-
if (Date.now() / 1000 >= cached.expireAt) {
|
|
888
|
-
this._prekeyCache.delete(peerAid);
|
|
889
|
-
return null;
|
|
890
|
-
}
|
|
891
|
-
return cached.prekey;
|
|
892
|
-
}
|
|
893
|
-
/** 使指定 peer 的 prekey 缓存失效 */
|
|
894
|
-
invalidatePrekeyCache(peerAid) {
|
|
895
|
-
this._prekeyCache.delete(peerAid);
|
|
896
|
-
}
|
|
897
|
-
// ── Prekey 生成 ──────────────────────────────────────────
|
|
898
|
-
/**
|
|
899
|
-
* 生成 prekey 材料并保存私钥到本地 keystore。
|
|
900
|
-
* 返回 {prekey_id, public_key, signature, created_at},可直接用于 RPC 上传。
|
|
901
|
-
*/
|
|
902
|
-
generatePrekey() {
|
|
903
|
-
const aid = this._currentAid();
|
|
904
|
-
if (!aid)
|
|
905
|
-
throw new E2EEError('AID unavailable for prekey generation');
|
|
906
|
-
const deviceId = this._currentDeviceId();
|
|
907
|
-
this._logger.debug(`generate prekey start: aid=${aid}, deviceId=${deviceId}`);
|
|
908
|
-
// 生成新 prekey
|
|
909
|
-
const { privateKey, publicKey } = generateECKeyPair();
|
|
910
|
-
const publicDer = publicKey.export({ type: 'spki', format: 'der' });
|
|
911
|
-
const prekeyId = crypto.randomUUID();
|
|
912
|
-
const publicKeyB64 = publicDer.toString('base64');
|
|
913
|
-
const nowMs = Date.now();
|
|
914
|
-
// 签名:prekey_id|public_key|created_at
|
|
915
|
-
const signDataBuf = Buffer.from(`${prekeyId}|${publicKeyB64}|${nowMs}`, 'utf-8');
|
|
916
|
-
const signature = this._signBytes(signDataBuf);
|
|
917
|
-
// 保存私钥到本地 keystore
|
|
918
|
-
const privateKeyPem = privateKey.export({ type: 'pkcs8', format: 'pem' });
|
|
919
|
-
saveKeyStorePrekey(this._keystore, aid, deviceId, prekeyId, {
|
|
920
|
-
private_key_pem: privateKeyPem,
|
|
921
|
-
created_at: nowMs,
|
|
922
|
-
updated_at: nowMs,
|
|
923
|
-
});
|
|
924
|
-
// 内存缓存私钥
|
|
925
|
-
this._localPrekeyCache.set(prekeyId, privateKeyPem);
|
|
926
|
-
// 清理过期的旧 prekey 私钥
|
|
927
|
-
this._cleanupExpiredPrekeys(aid, deviceId);
|
|
928
|
-
const result = {
|
|
929
|
-
prekey_id: prekeyId,
|
|
930
|
-
public_key: publicKeyB64,
|
|
931
|
-
signature,
|
|
932
|
-
created_at: nowMs,
|
|
933
|
-
};
|
|
934
|
-
const certFingerprint = this._localCertSha256Fingerprint();
|
|
935
|
-
if (certFingerprint) {
|
|
936
|
-
result.cert_fingerprint = certFingerprint;
|
|
937
|
-
}
|
|
938
|
-
if (deviceId) {
|
|
939
|
-
result.device_id = deviceId;
|
|
940
|
-
}
|
|
941
|
-
this._logger.debug(`generate prekey success: aid=${aid}, prekeyId=${prekeyId}`);
|
|
942
|
-
return result;
|
|
943
|
-
}
|
|
944
|
-
/** 清理本地过期的 prekey 私钥 */
|
|
945
|
-
_cleanupExpiredPrekeys(aid, deviceId) {
|
|
946
|
-
const nowMs = Date.now();
|
|
947
|
-
const cutoffMs = nowMs - PREKEY_RETENTION_SECONDS * 1000;
|
|
948
|
-
const expired = cleanupKeyStorePrekeys(this._keystore, aid, deviceId, cutoffMs, PREKEY_MIN_KEEP_COUNT);
|
|
949
|
-
if (expired.length > 0) {
|
|
950
|
-
for (const pid of expired) {
|
|
951
|
-
this._localPrekeyCache.delete(pid);
|
|
952
|
-
}
|
|
953
|
-
}
|
|
954
|
-
}
|
|
955
|
-
/** 从内存缓存或 keystore 加载 prekey 私钥 */
|
|
956
|
-
_loadPrekeyPrivateKey(prekeyId) {
|
|
957
|
-
// 优先从内存缓存获取
|
|
958
|
-
const cachedPem = this._localPrekeyCache.get(prekeyId);
|
|
959
|
-
if (cachedPem) {
|
|
960
|
-
return crypto.createPrivateKey(cachedPem);
|
|
961
|
-
}
|
|
962
|
-
const aid = this._currentAid();
|
|
963
|
-
if (!aid)
|
|
964
|
-
return null;
|
|
965
|
-
// 优先按 prekey_id 单点查询(O(1) 数据库行级查询)。
|
|
966
|
-
// 信封里都带 prekey_id,没必要全量加载所有 prekey(旧路径在 9000+ prekey 下要 ~430ms/次)。
|
|
967
|
-
let prekeyData;
|
|
968
|
-
const byIdLoader = this._keystore.loadE2EEPrekeyById;
|
|
969
|
-
if (typeof byIdLoader === 'function') {
|
|
970
|
-
try {
|
|
971
|
-
const hit = byIdLoader.call(this._keystore, aid, prekeyId);
|
|
972
|
-
if (hit) {
|
|
973
|
-
prekeyData = hit;
|
|
974
|
-
this._logger.debug(`prekey ${prekeyId} by_id lookup hit`);
|
|
975
|
-
}
|
|
976
|
-
}
|
|
977
|
-
catch (exc) {
|
|
978
|
-
this._logger.warn(`prekey ${prekeyId} by_id loader failed, falling back to full load: ${exc instanceof Error ? exc.message : String(exc)}`);
|
|
979
|
-
}
|
|
980
|
-
}
|
|
981
|
-
// 回退:旧版 keystore 没有 by_id 方法,或单点查询未命中 → 全量扫描
|
|
982
|
-
if (!prekeyData) {
|
|
983
|
-
const prekeys = loadKeyStorePrekeys(this._keystore, aid, this._currentDeviceId());
|
|
984
|
-
prekeyData = prekeys[prekeyId];
|
|
985
|
-
}
|
|
986
|
-
if (!prekeyData)
|
|
987
|
-
return null;
|
|
988
|
-
const privateKeyPem = prekeyData.private_key_pem;
|
|
989
|
-
if (!privateKeyPem)
|
|
990
|
-
return null;
|
|
991
|
-
try {
|
|
992
|
-
const pk = crypto.createPrivateKey(privateKeyPem);
|
|
993
|
-
this._localPrekeyCache.set(prekeyId, privateKeyPem);
|
|
994
|
-
return pk;
|
|
995
|
-
}
|
|
996
|
-
catch {
|
|
997
|
-
return null;
|
|
998
|
-
}
|
|
999
|
-
}
|
|
1000
|
-
// ── 证书指纹工具 ────────────────────────────────────────
|
|
1001
|
-
/** 从 PEM 证书计算公钥指纹 */
|
|
1002
|
-
static fingerprintCertPem(certPem) {
|
|
1003
|
-
return fingerprintCertPem(certPem);
|
|
1004
|
-
}
|
|
1005
|
-
/** 公钥 DER bytes → 指纹 */
|
|
1006
|
-
static fingerprintDerPublicKey(derBytes) {
|
|
1007
|
-
return fingerprintPublicKeyDer(derBytes);
|
|
1008
|
-
}
|
|
1009
|
-
// ── 内部工具 ─────────────────────────────────────────────
|
|
1010
|
-
/** 仅解密发给当前 AID 的消息 */
|
|
1011
|
-
_shouldDecryptForCurrentAid(message, payload) {
|
|
1012
|
-
if (String(message.direction ?? '').trim().toLowerCase() === 'outbound_sync') {
|
|
1013
|
-
return true;
|
|
1014
|
-
}
|
|
1015
|
-
const currentAid = this._currentAid();
|
|
1016
|
-
if (!currentAid)
|
|
1017
|
-
return true;
|
|
1018
|
-
const targetAid = message.to ||
|
|
1019
|
-
payload.aad?.to ||
|
|
1020
|
-
payload.to;
|
|
1021
|
-
if (!targetAid)
|
|
1022
|
-
return true;
|
|
1023
|
-
return String(targetAid) === String(currentAid);
|
|
1024
|
-
}
|
|
1025
|
-
/** LRU 裁剪 seen set */
|
|
1026
|
-
_trimSeenSet() {
|
|
1027
|
-
if (this._seenMessages.size > this._seenMaxSize) {
|
|
1028
|
-
const trimCount = this._seenMessages.size - Math.floor(this._seenMaxSize * 0.8);
|
|
1029
|
-
const iter = this._seenMessages.keys();
|
|
1030
|
-
for (let i = 0; i < trimCount; i++) {
|
|
1031
|
-
const next = iter.next();
|
|
1032
|
-
if (next.done)
|
|
1033
|
-
break;
|
|
1034
|
-
this._seenMessages.delete(next.value);
|
|
1035
|
-
}
|
|
1036
|
-
}
|
|
1037
|
-
}
|
|
1038
|
-
/** 获取当前 AID */
|
|
1039
|
-
_currentAid() {
|
|
1040
|
-
const identity = this._identityFn();
|
|
1041
|
-
const aid = identity.aid;
|
|
1042
|
-
return aid ? String(aid) : null;
|
|
1043
|
-
}
|
|
1044
|
-
_currentDeviceId() {
|
|
1045
|
-
try {
|
|
1046
|
-
return String(this._deviceIdFn() ?? '').trim();
|
|
1047
|
-
}
|
|
1048
|
-
catch {
|
|
1049
|
-
return '';
|
|
1050
|
-
}
|
|
1051
|
-
}
|
|
1052
|
-
/** 获取发送方证书 */
|
|
1053
|
-
_getSenderCert(aid, certFingerprint) {
|
|
1054
|
-
const certPem = this._keystore.loadCert(aid, certFingerprint);
|
|
1055
|
-
const normalized = String(certFingerprint ?? '').trim().toLowerCase();
|
|
1056
|
-
if (!certPem)
|
|
1057
|
-
return null;
|
|
1058
|
-
if (!normalized)
|
|
1059
|
-
return certPem;
|
|
1060
|
-
return certificateSha256Fingerprint(certPem) === normalized ? certPem : null;
|
|
1061
|
-
}
|
|
1062
|
-
/** 获取发送方的 identity 公钥(从本地证书缓存) */
|
|
1063
|
-
_loadSenderPublicKey(aid, certFingerprint) {
|
|
1064
|
-
if (!aid)
|
|
1065
|
-
return null;
|
|
1066
|
-
const certPem = this._getSenderCert(aid, certFingerprint);
|
|
1067
|
-
if (!certPem)
|
|
1068
|
-
return null;
|
|
1069
|
-
try {
|
|
1070
|
-
return pemToCertPublicKey(certPem);
|
|
1071
|
-
}
|
|
1072
|
-
catch {
|
|
1073
|
-
return null;
|
|
1074
|
-
}
|
|
1075
|
-
}
|
|
1076
|
-
/** 用当前身份私钥签名 */
|
|
1077
|
-
_signBytes(data) {
|
|
1078
|
-
const identity = this._identityFn();
|
|
1079
|
-
const privateKeyPem = identity.private_key_pem;
|
|
1080
|
-
if (!privateKeyPem) {
|
|
1081
|
-
throw new E2EEError('identity private key unavailable');
|
|
1082
|
-
}
|
|
1083
|
-
return ecdsaSign(privateKeyPem, data).toString('base64');
|
|
1084
|
-
}
|
|
1085
|
-
/** 加载发送方自己的 identity 私钥 */
|
|
1086
|
-
_loadSenderIdentityPrivate() {
|
|
1087
|
-
const identity = this._identityFn();
|
|
1088
|
-
const privateKeyPem = identity.private_key_pem;
|
|
1089
|
-
if (!privateKeyPem) {
|
|
1090
|
-
throw new E2EEError('sender identity private key unavailable');
|
|
1091
|
-
}
|
|
1092
|
-
return crypto.createPrivateKey(privateKeyPem);
|
|
1093
|
-
}
|
|
1094
|
-
/** 本地 identity 指纹(优先证书 DER SHA-256,缺失时回退到公钥指纹) */
|
|
1095
|
-
_localIdentityFingerprint() {
|
|
1096
|
-
// 优先用证书指纹(与 PKI 一致)
|
|
1097
|
-
const identity = this._identityFn();
|
|
1098
|
-
const cert = identity.cert;
|
|
1099
|
-
if (cert) {
|
|
1100
|
-
return certificateSha256Fingerprint(cert);
|
|
1101
|
-
}
|
|
1102
|
-
// 无证书时回退到公钥 SPKI 指纹
|
|
1103
|
-
const publicKeyDerB64 = identity.public_key_der_b64;
|
|
1104
|
-
if (publicKeyDerB64) {
|
|
1105
|
-
return fingerprintPublicKeyDer(Buffer.from(publicKeyDerB64, 'base64'));
|
|
1106
|
-
}
|
|
1107
|
-
const privateKeyPem = identity.private_key_pem;
|
|
1108
|
-
if (privateKeyPem) {
|
|
1109
|
-
const pk = crypto.createPrivateKey(privateKeyPem);
|
|
1110
|
-
const pubKey = crypto.createPublicKey(pk);
|
|
1111
|
-
return fingerprintKeyObject(pubKey);
|
|
1112
|
-
}
|
|
1113
|
-
throw new E2EEError('identity fingerprint unavailable');
|
|
1114
|
-
}
|
|
1115
|
-
/** 本地证书指纹(优先证书 SHA-256,缺失时回退到 identity 公钥指纹) */
|
|
1116
|
-
_localCertFingerprint() {
|
|
1117
|
-
return this._localCertSha256Fingerprint() || this._localIdentityFingerprint();
|
|
1118
|
-
}
|
|
1119
|
-
/** 本地证书的 SHA-256 指纹(用于锁定证书版本) */
|
|
1120
|
-
_localCertSha256Fingerprint() {
|
|
1121
|
-
const identity = this._identityFn();
|
|
1122
|
-
const cert = identity.cert;
|
|
1123
|
-
if (!cert)
|
|
1124
|
-
return '';
|
|
1125
|
-
return certificateSha256Fingerprint(cert);
|
|
1126
|
-
}
|
|
1127
|
-
/** 构建接收端 AAD */
|
|
1128
|
-
_buildInboundAadOffline(message, payload) {
|
|
1129
|
-
const aad = payload.aad;
|
|
1130
|
-
return {
|
|
1131
|
-
from: message.from,
|
|
1132
|
-
to: message.to,
|
|
1133
|
-
message_id: message.message_id,
|
|
1134
|
-
timestamp: message.timestamp,
|
|
1135
|
-
encryption_mode: payload.encryption_mode,
|
|
1136
|
-
suite: payload.suite || SUITE,
|
|
1137
|
-
ephemeral_public_key: payload.ephemeral_public_key,
|
|
1138
|
-
recipient_cert_fingerprint: this._localCertFingerprint(),
|
|
1139
|
-
sender_cert_fingerprint: (payload.sender_cert_fingerprint ?? aad?.sender_cert_fingerprint),
|
|
1140
|
-
prekey_id: (payload.prekey_id ?? aad?.prekey_id),
|
|
1141
|
-
};
|
|
1142
|
-
}
|
|
1143
|
-
/** 清理过期的 prekey 缓存和 seen set 条目(供外部定时调用) */
|
|
1144
|
-
cleanExpiredCaches() {
|
|
1145
|
-
const now = Date.now() / 1000;
|
|
1146
|
-
// 清理过期的 prekey 缓存
|
|
1147
|
-
for (const [k, v] of this._prekeyCache) {
|
|
1148
|
-
if (now >= v.expireAt)
|
|
1149
|
-
this._prekeyCache.delete(k);
|
|
1150
|
-
}
|
|
1151
|
-
// 清理 seen set(LRU 裁剪)
|
|
1152
|
-
this._trimSeenSet();
|
|
1153
|
-
}
|
|
1154
|
-
}
|
|
7
|
+
export { ProtectedHeaders } from './protected-headers.js';
|
|
1155
8
|
//# sourceMappingURL=e2ee.js.map
|