@agentunion/fastaun-browser 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 +66 -26
- package/README.md +0 -1
- package/_packed_docs/CHANGELOG.md +66 -26
- 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.d.ts.map +1 -1
- package/dist/auth.js +25 -5
- package/dist/auth.js.map +1 -1
- package/dist/bundle.js +15042 -0
- package/dist/client.d.ts +179 -187
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +3148 -3993
- package/dist/client.js.map +1 -1
- package/dist/config.d.ts +0 -4
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +0 -4
- package/dist/config.js.map +1 -1
- package/dist/crypto.d.ts +8 -1
- package/dist/crypto.d.ts.map +1 -1
- package/dist/crypto.js +114 -1
- package/dist/crypto.js.map +1 -1
- package/dist/e2ee.d.ts +5 -210
- package/dist/e2ee.d.ts.map +1 -1
- package/dist/e2ee.js +4 -1379
- package/dist/e2ee.js.map +1 -1
- package/dist/index.d.ts +7 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -4
- package/dist/index.js.map +1 -1
- package/dist/namespaces/auth.d.ts +9 -0
- package/dist/namespaces/auth.d.ts.map +1 -1
- package/dist/namespaces/auth.js +248 -4
- package/dist/namespaces/auth.js.map +1 -1
- package/dist/protected-headers.d.ts +14 -0
- package/dist/protected-headers.d.ts.map +1 -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.d.ts.map +1 -1
- package/dist/seq-tracker.js +33 -13
- package/dist/seq-tracker.js.map +1 -1
- package/dist/transport.d.ts +9 -1
- package/dist/transport.d.ts.map +1 -1
- package/dist/transport.js +262 -10
- package/dist/transport.js.map +1 -1
- package/dist/v2/crypto/aead.d.ts +26 -0
- package/dist/v2/crypto/aead.d.ts.map +1 -0
- package/dist/v2/crypto/aead.js +63 -0
- package/dist/v2/crypto/aead.js.map +1 -0
- package/dist/v2/crypto/canonical.d.ts +21 -0
- package/dist/v2/crypto/canonical.d.ts.map +1 -0
- package/dist/v2/crypto/canonical.js +111 -0
- package/dist/v2/crypto/canonical.js.map +1 -0
- package/dist/v2/crypto/dh-path.d.ts +21 -0
- package/dist/v2/crypto/dh-path.d.ts.map +1 -0
- package/dist/v2/crypto/dh-path.js +50 -0
- package/dist/v2/crypto/dh-path.js.map +1 -0
- package/dist/v2/crypto/ecdh.d.ts +19 -0
- package/dist/v2/crypto/ecdh.d.ts.map +1 -0
- package/dist/v2/crypto/ecdh.js +101 -0
- package/dist/v2/crypto/ecdh.js.map +1 -0
- package/dist/v2/crypto/ecdsa.d.ts +16 -0
- package/dist/v2/crypto/ecdsa.d.ts.map +1 -0
- package/dist/v2/crypto/ecdsa.js +52 -0
- package/dist/v2/crypto/ecdsa.js.map +1 -0
- package/dist/v2/crypto/hkdf.d.ts +21 -0
- package/dist/v2/crypto/hkdf.d.ts.map +1 -0
- package/dist/v2/crypto/hkdf.js +32 -0
- package/dist/v2/crypto/hkdf.js.map +1 -0
- package/dist/v2/crypto/index.d.ts +9 -0
- package/dist/v2/crypto/index.d.ts.map +1 -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 +43 -0
- package/dist/v2/crypto/recipients.d.ts.map +1 -0
- package/dist/v2/crypto/recipients.js +188 -0
- package/dist/v2/crypto/recipients.js.map +1 -0
- package/dist/v2/e2ee/decrypt.d.ts +13 -0
- package/dist/v2/e2ee/decrypt.d.ts.map +1 -0
- package/dist/v2/e2ee/decrypt.js +176 -0
- package/dist/v2/e2ee/decrypt.js.map +1 -0
- package/dist/v2/e2ee/encrypt-group.d.ts +14 -0
- package/dist/v2/e2ee/encrypt-group.d.ts.map +1 -0
- package/dist/v2/e2ee/encrypt-group.js +196 -0
- package/dist/v2/e2ee/encrypt-group.js.map +1 -0
- package/dist/v2/e2ee/encrypt-p2p.d.ts +15 -0
- package/dist/v2/e2ee/encrypt-p2p.d.ts.map +1 -0
- package/dist/v2/e2ee/encrypt-p2p.js +240 -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.d.ts.map +1 -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 +9 -0
- package/dist/v2/e2ee/metadata-auth.d.ts.map +1 -0
- package/dist/v2/e2ee/metadata-auth.js +60 -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.d.ts.map +1 -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.d.ts.map +1 -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 +57 -0
- package/dist/v2/session/keystore.d.ts.map +1 -0
- package/dist/v2/session/keystore.js +244 -0
- package/dist/v2/session/keystore.js.map +1 -0
- package/dist/v2/session/session.d.ts +121 -0
- package/dist/v2/session/session.d.ts.map +1 -0
- package/dist/v2/session/session.js +344 -0
- package/dist/v2/session/session.js.map +1 -0
- package/dist/v2/state/commitment.d.ts +10 -0
- package/dist/v2/state/commitment.d.ts.map +1 -0
- package/dist/v2/state/commitment.js +86 -0
- package/dist/v2/state/commitment.js.map +1 -0
- package/dist/v2/state/index.d.ts +2 -0
- package/dist/v2/state/index.d.ts.map +1 -0
- package/dist/v2/state/index.js +2 -0
- package/dist/v2/state/index.js.map +1 -0
- package/package.json +8 -5
package/dist/e2ee.js
CHANGED
|
@@ -1,1383 +1,8 @@
|
|
|
1
|
-
// ── E2EEManager(P2P 端到端加密 — 浏览器 SubtleCrypto 实现)──
|
|
2
|
-
// 所有密码学操作均为异步(SubtleCrypto API 要求)
|
|
3
|
-
import { E2EEDecryptFailedError, E2EEError } from './errors.js';
|
|
4
|
-
import { uint8ToBase64, base64ToUint8, pemToArrayBuffer, p1363ToDer, toArrayBuffer, toBufferSource } from './crypto.js';
|
|
5
|
-
const _noopLog = { error: () => { }, warn: () => { }, info: () => { }, debug: () => { } };
|
|
6
|
-
// 顶层函数共享的模块 logger(client 构造时通过 setModuleLogger 注入)
|
|
7
|
-
let _moduleLog = _noopLog;
|
|
8
|
-
export function setModuleLogger(log) { _moduleLog = log; }
|
|
9
|
-
/** 加密套件标识 */
|
|
10
|
-
export const SUITE = 'P256_HKDF_SHA256_AES_256_GCM';
|
|
11
|
-
/** 加密模式 */
|
|
12
|
-
export const MODE_PREKEY_ECDH_V2 = 'prekey_ecdh_v2';
|
|
13
|
-
export const MODE_LONG_TERM_KEY = 'long_term_key';
|
|
14
|
-
/** AAD 字段定义(P2P) */
|
|
15
|
-
export const AAD_FIELDS_OFFLINE = [
|
|
16
|
-
'from', 'to', 'message_id', 'timestamp',
|
|
17
|
-
'encryption_mode', 'suite', 'ephemeral_public_key',
|
|
18
|
-
'recipient_cert_fingerprint', 'sender_cert_fingerprint',
|
|
19
|
-
'prekey_id',
|
|
20
|
-
];
|
|
21
|
-
/** AAD 匹配字段(解密时校验,不含 timestamp) */
|
|
22
|
-
export const AAD_MATCH_FIELDS_OFFLINE = [
|
|
23
|
-
'from', 'to', 'message_id',
|
|
24
|
-
'encryption_mode', 'suite', 'ephemeral_public_key',
|
|
25
|
-
'recipient_cert_fingerprint', 'sender_cert_fingerprint',
|
|
26
|
-
'prekey_id',
|
|
27
|
-
];
|
|
28
|
-
/** 兼容型可选 AAD 字段:存在时才参与 AAD,不为旧消息补 null。 */
|
|
29
|
-
export const AAD_OPTIONAL_FIELDS = [
|
|
30
|
-
'payload_type', 'protected_headers', 'context_type', 'context_id',
|
|
31
|
-
];
|
|
32
|
-
const METADATA_AUTH_FIELD = '_auth';
|
|
33
|
-
const METADATA_AUTH_ALG = 'HMAC-SHA256';
|
|
34
|
-
const METADATA_KEY_DOMAIN = new TextEncoder().encode('aun-envelope-metadata-key-v1');
|
|
35
|
-
const PROTECTED_HEADERS_DOMAIN = new TextEncoder().encode('aun-protected-headers-v1');
|
|
36
|
-
const PROTECTED_CONTEXT_DOMAIN = new TextEncoder().encode('aun-protected-context-v1');
|
|
37
|
-
/** prekey 私钥本地保留时间(秒) */
|
|
38
|
-
export const PREKEY_RETENTION_SECONDS = 7 * 24 * 3600;
|
|
39
|
-
export const PREKEY_MIN_KEEP_COUNT = 7;
|
|
40
|
-
/** 端到端保护的信封元数据,语义接近 HTTP headers。 */
|
|
41
|
-
export class ProtectedHeaders {
|
|
42
|
-
_items = {};
|
|
43
|
-
constructor(values) {
|
|
44
|
-
if (values) {
|
|
45
|
-
for (const [key, value] of Object.entries(values)) {
|
|
46
|
-
this.set(key, value);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
static normalizeKey(key) {
|
|
51
|
-
const value = String(key ?? '').trim().toLowerCase();
|
|
52
|
-
if (!value || !/^[a-z0-9_-]+$/.test(value)) {
|
|
53
|
-
throw new E2EEError('protected header key must match [a-z0-9_-]+');
|
|
54
|
-
}
|
|
55
|
-
if (value === METADATA_AUTH_FIELD) {
|
|
56
|
-
throw new E2EEError('protected header key is reserved');
|
|
57
|
-
}
|
|
58
|
-
return value;
|
|
59
|
-
}
|
|
60
|
-
set(key, value) {
|
|
61
|
-
this._items[ProtectedHeaders.normalizeKey(key)] = value == null ? '' : String(value);
|
|
62
|
-
return this;
|
|
63
|
-
}
|
|
64
|
-
get(key, defaultValue = null) {
|
|
65
|
-
const normalized = ProtectedHeaders.normalizeKey(key);
|
|
66
|
-
return Object.prototype.hasOwnProperty.call(this._items, normalized)
|
|
67
|
-
? this._items[normalized]
|
|
68
|
-
: defaultValue;
|
|
69
|
-
}
|
|
70
|
-
remove(key) {
|
|
71
|
-
delete this._items[ProtectedHeaders.normalizeKey(key)];
|
|
72
|
-
return this;
|
|
73
|
-
}
|
|
74
|
-
toObject() {
|
|
75
|
-
return { ...this._items };
|
|
76
|
-
}
|
|
77
|
-
toJSON() {
|
|
78
|
-
return this.toObject();
|
|
79
|
-
}
|
|
80
|
-
static from(values) {
|
|
81
|
-
return new ProtectedHeaders(values ?? {});
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
function prekeyCreatedMarker(prekeyData) {
|
|
85
|
-
return Number(prekeyData.created_at ?? prekeyData.updated_at ?? prekeyData.expires_at ?? 0);
|
|
86
|
-
}
|
|
87
|
-
function latestPrekeyIds(prekeys, keepLatest) {
|
|
88
|
-
if (keepLatest <= 0)
|
|
89
|
-
return new Set();
|
|
90
|
-
return new Set(Object.entries(prekeys)
|
|
91
|
-
.filter(([, data]) => typeof data === 'object' && data !== null)
|
|
92
|
-
.sort((left, right) => {
|
|
93
|
-
const markerDiff = prekeyCreatedMarker(right[1]) - prekeyCreatedMarker(left[1]);
|
|
94
|
-
if (markerDiff !== 0)
|
|
95
|
-
return markerDiff;
|
|
96
|
-
return right[0].localeCompare(left[0]);
|
|
97
|
-
})
|
|
98
|
-
.slice(0, keepLatest)
|
|
99
|
-
.map(([prekeyId]) => prekeyId));
|
|
100
|
-
}
|
|
101
|
-
async function loadKeyStorePrekeys(keystore, aid, deviceId = '') {
|
|
102
|
-
const normalizedDeviceId = String(deviceId ?? '').trim();
|
|
103
|
-
if (typeof keystore.loadE2EEPrekeys === 'function') {
|
|
104
|
-
return ((await keystore.loadE2EEPrekeys(aid, normalizedDeviceId)) ?? {});
|
|
105
|
-
}
|
|
106
|
-
throw new Error('keystore 缺少 loadE2EEPrekeys 方法');
|
|
107
|
-
}
|
|
108
|
-
async function saveKeyStorePrekey(keystore, aid, deviceId, prekeyId, prekeyData) {
|
|
109
|
-
const normalizedDeviceId = String(deviceId ?? '').trim();
|
|
110
|
-
if (typeof keystore.saveE2EEPrekey === 'function') {
|
|
111
|
-
await keystore.saveE2EEPrekey(aid, prekeyId, prekeyData, normalizedDeviceId);
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
114
|
-
throw new Error(`keystore ${keystore.constructor?.name ?? 'unknown'} 缺少 saveE2EEPrekey 方法`);
|
|
115
|
-
}
|
|
116
|
-
async function cleanupKeyStorePrekeys(keystore, aid, deviceId, cutoffMs, keepLatest = PREKEY_MIN_KEEP_COUNT) {
|
|
117
|
-
const normalizedDeviceId = String(deviceId ?? '').trim();
|
|
118
|
-
if (typeof keystore.cleanupE2EEPrekeys === 'function') {
|
|
119
|
-
return (await keystore.cleanupE2EEPrekeys(aid, cutoffMs, keepLatest, normalizedDeviceId)) ?? [];
|
|
120
|
-
}
|
|
121
|
-
throw new Error(`keystore ${keystore.constructor?.name ?? 'unknown'} 缺少 cleanupE2EEPrekeys 方法`);
|
|
122
|
-
}
|
|
123
|
-
// ── 工具函数 ────────────────────────────────────────────────
|
|
124
|
-
const _encoder = new TextEncoder();
|
|
125
|
-
const _decoder = new TextDecoder();
|
|
126
|
-
/** 拼接多个 Uint8Array */
|
|
127
|
-
function concatBytes(...arrays) {
|
|
128
|
-
let total = 0;
|
|
129
|
-
for (const a of arrays)
|
|
130
|
-
total += a.byteLength;
|
|
131
|
-
const result = new Uint8Array(total);
|
|
132
|
-
let offset = 0;
|
|
133
|
-
for (const a of arrays) {
|
|
134
|
-
result.set(a, offset);
|
|
135
|
-
offset += a.byteLength;
|
|
136
|
-
}
|
|
137
|
-
return result;
|
|
138
|
-
}
|
|
139
|
-
/** DER 签名转 IEEE P1363 格式(用于 SubtleCrypto 验签) */
|
|
140
|
-
function derToP1363(der, coordLen = 32) {
|
|
141
|
-
// 跳过 SEQUENCE 头
|
|
142
|
-
if (der[0] !== 0x30)
|
|
143
|
-
throw new E2EEError('无效 DER 签名: 缺少 SEQUENCE 标签');
|
|
144
|
-
let pos = 2; // 跳过 30 + length
|
|
145
|
-
// 读取 r
|
|
146
|
-
if (der[pos] !== 0x02)
|
|
147
|
-
throw new E2EEError('无效 DER 签名: 缺少 INTEGER 标签 (r)');
|
|
148
|
-
pos++;
|
|
149
|
-
const rLen = der[pos++];
|
|
150
|
-
let rBytes = der.slice(pos, pos + rLen);
|
|
151
|
-
pos += rLen;
|
|
152
|
-
// 读取 s
|
|
153
|
-
if (der[pos] !== 0x02)
|
|
154
|
-
throw new E2EEError('无效 DER 签名: 缺少 INTEGER 标签 (s)');
|
|
155
|
-
pos++;
|
|
156
|
-
const sLen = der[pos++];
|
|
157
|
-
let sBytes = der.slice(pos, pos + sLen);
|
|
158
|
-
// 去掉前导 0x00(ASN.1 有符号整数的填充)
|
|
159
|
-
if (rBytes.length > coordLen && rBytes[0] === 0)
|
|
160
|
-
rBytes = rBytes.slice(1);
|
|
161
|
-
if (sBytes.length > coordLen && sBytes[0] === 0)
|
|
162
|
-
sBytes = sBytes.slice(1);
|
|
163
|
-
// 左填充到 coordLen
|
|
164
|
-
const result = new Uint8Array(coordLen * 2);
|
|
165
|
-
result.set(rBytes, coordLen - rBytes.length);
|
|
166
|
-
result.set(sBytes, coordLen * 2 - sBytes.length);
|
|
167
|
-
return result;
|
|
168
|
-
}
|
|
169
|
-
function canonicalStringify(value) {
|
|
170
|
-
if (value === null || value === undefined)
|
|
171
|
-
return 'null';
|
|
172
|
-
if (Array.isArray(value)) {
|
|
173
|
-
return `[${value.map(item => canonicalStringify(item)).join(',')}]`;
|
|
174
|
-
}
|
|
175
|
-
if (typeof value === 'object') {
|
|
176
|
-
const record = value;
|
|
177
|
-
const pairs = Object.keys(record)
|
|
178
|
-
.sort()
|
|
179
|
-
.map(key => `${JSON.stringify(key)}:${canonicalStringify(record[key])}`);
|
|
180
|
-
return `{${pairs.join(',')}}`;
|
|
181
|
-
}
|
|
182
|
-
return JSON.stringify(value) ?? 'null';
|
|
183
|
-
}
|
|
184
|
-
function hasOwn(obj, key) {
|
|
185
|
-
return Object.prototype.hasOwnProperty.call(obj, key);
|
|
186
|
-
}
|
|
187
|
-
function normalizeProtectedHeaders(headers) {
|
|
188
|
-
if (headers == null)
|
|
189
|
-
return {};
|
|
190
|
-
if (headers instanceof ProtectedHeaders) {
|
|
191
|
-
return headers.toObject();
|
|
192
|
-
}
|
|
193
|
-
const toObject = headers.toObject;
|
|
194
|
-
if (typeof toObject === 'function') {
|
|
195
|
-
return new ProtectedHeaders(toObject.call(headers)).toObject();
|
|
196
|
-
}
|
|
197
|
-
if (typeof headers !== 'object' || Array.isArray(headers)) {
|
|
198
|
-
throw new E2EEError('protected_headers must be an object');
|
|
199
|
-
}
|
|
200
|
-
return new ProtectedHeaders(headers).toObject();
|
|
201
|
-
}
|
|
202
|
-
function metadataBody(metadata) {
|
|
203
|
-
const body = {};
|
|
204
|
-
for (const [key, value] of Object.entries(metadata)) {
|
|
205
|
-
if (key !== METADATA_AUTH_FIELD) {
|
|
206
|
-
body[key] = value;
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
return body;
|
|
210
|
-
}
|
|
211
|
-
async function hmacSha256(key, data) {
|
|
212
|
-
const hmacKey = await crypto.subtle.importKey('raw', toBufferSource(key), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
|
|
213
|
-
const sig = await crypto.subtle.sign('HMAC', hmacKey, toBufferSource(data));
|
|
214
|
-
return new Uint8Array(sig);
|
|
215
|
-
}
|
|
216
|
-
async function metadataAuthTag(key, domain, body) {
|
|
217
|
-
const metadataKey = await hmacSha256(key, METADATA_KEY_DOMAIN);
|
|
218
|
-
return hmacSha256(metadataKey, concatBytes(domain, new Uint8Array([0]), _encoder.encode(canonicalStringify(body))));
|
|
219
|
-
}
|
|
220
|
-
async function withMetadataAuth(metadata, key, domain) {
|
|
221
|
-
const body = metadataBody(metadata);
|
|
222
|
-
if (Object.keys(body).length === 0)
|
|
223
|
-
return {};
|
|
224
|
-
const tag = await metadataAuthTag(key, domain, body);
|
|
225
|
-
return {
|
|
226
|
-
...body,
|
|
227
|
-
[METADATA_AUTH_FIELD]: {
|
|
228
|
-
alg: METADATA_AUTH_ALG,
|
|
229
|
-
tag: uint8ToBase64(tag),
|
|
230
|
-
},
|
|
231
|
-
};
|
|
232
|
-
}
|
|
233
|
-
function timingSafeEqual(a, b) {
|
|
234
|
-
if (a.byteLength !== b.byteLength)
|
|
235
|
-
return false;
|
|
236
|
-
let diff = 0;
|
|
237
|
-
for (let i = 0; i < a.byteLength; i++) {
|
|
238
|
-
diff |= a[i] ^ b[i];
|
|
239
|
-
}
|
|
240
|
-
return diff === 0;
|
|
241
|
-
}
|
|
242
|
-
async function verifyMetadataAuth(metadata, key, domain) {
|
|
243
|
-
if (metadata == null)
|
|
244
|
-
return true;
|
|
245
|
-
if (typeof metadata !== 'object' || Array.isArray(metadata))
|
|
246
|
-
return false;
|
|
247
|
-
const record = metadata;
|
|
248
|
-
const auth = record[METADATA_AUTH_FIELD];
|
|
249
|
-
if (!auth || typeof auth !== 'object' || Array.isArray(auth))
|
|
250
|
-
return false;
|
|
251
|
-
const authObj = auth;
|
|
252
|
-
if (authObj.alg !== METADATA_AUTH_ALG)
|
|
253
|
-
return false;
|
|
254
|
-
if (typeof authObj.tag !== 'string' || !authObj.tag)
|
|
255
|
-
return false;
|
|
256
|
-
const body = metadataBody(record);
|
|
257
|
-
if (Object.keys(body).length === 0)
|
|
258
|
-
return false;
|
|
259
|
-
let actual;
|
|
260
|
-
try {
|
|
261
|
-
actual = base64ToUint8(authObj.tag);
|
|
262
|
-
}
|
|
263
|
-
catch {
|
|
264
|
-
return false;
|
|
265
|
-
}
|
|
266
|
-
const expected = await metadataAuthTag(key, domain, body);
|
|
267
|
-
return timingSafeEqual(actual, expected);
|
|
268
|
-
}
|
|
269
|
-
async function verifyEnvelopeMetadataAuth(payload, messageKey) {
|
|
270
|
-
return await verifyMetadataAuth(payload.protected_headers, messageKey, PROTECTED_HEADERS_DOMAIN)
|
|
271
|
-
&& await verifyMetadataAuth(payload.context, messageKey, PROTECTED_CONTEXT_DOMAIN);
|
|
272
|
-
}
|
|
273
|
-
function normalizeContextMetadata(context) {
|
|
274
|
-
if (!context || typeof context !== 'object' || Array.isArray(context))
|
|
275
|
-
return {};
|
|
276
|
-
return metadataBody(context);
|
|
277
|
-
}
|
|
278
|
-
function exposedEnvelopeMetadata(metadata) {
|
|
279
|
-
if (!metadata || typeof metadata !== 'object' || Array.isArray(metadata))
|
|
280
|
-
return undefined;
|
|
281
|
-
const body = metadataBody(metadata);
|
|
282
|
-
return Object.keys(body).length > 0 ? body : undefined;
|
|
283
|
-
}
|
|
284
|
-
async function copyOptionalEnvelopeMetadata(envelope, messageKey, opts) {
|
|
285
|
-
const payloadType = String(opts?.payloadType ?? '').trim();
|
|
286
|
-
const protectedHeaders = normalizeProtectedHeaders(opts?.protectedHeaders);
|
|
287
|
-
if (payloadType) {
|
|
288
|
-
protectedHeaders.payload_type = payloadType;
|
|
289
|
-
}
|
|
290
|
-
if (Object.keys(protectedHeaders).length > 0) {
|
|
291
|
-
envelope.protected_headers = await withMetadataAuth(protectedHeaders, messageKey, PROTECTED_HEADERS_DOMAIN);
|
|
292
|
-
}
|
|
293
|
-
const contextMetadata = normalizeContextMetadata(opts?.context);
|
|
294
|
-
if (Object.keys(contextMetadata).length > 0) {
|
|
295
|
-
envelope.context = await withMetadataAuth(contextMetadata, messageKey, PROTECTED_CONTEXT_DOMAIN);
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
function aadBytesWithOptionalFields(aad, baseFields) {
|
|
299
|
-
const obj = {};
|
|
300
|
-
for (const field of baseFields) {
|
|
301
|
-
obj[field] = aad[field] ?? null;
|
|
302
|
-
}
|
|
303
|
-
return _encoder.encode(canonicalStringify(obj));
|
|
304
|
-
}
|
|
305
|
-
function validateDecryptedEnvelopeMetadata(decoded, payload, message) {
|
|
306
|
-
if (payload.protected_headers && typeof payload.protected_headers === 'object' && !Array.isArray(payload.protected_headers)) {
|
|
307
|
-
const headers = metadataBody(payload.protected_headers);
|
|
308
|
-
if (hasOwn(headers, 'payload_type')) {
|
|
309
|
-
if (!decoded || typeof decoded !== 'object' || Array.isArray(decoded))
|
|
310
|
-
return false;
|
|
311
|
-
if (String(decoded.type ?? '') !== String(headers.payload_type ?? '')) {
|
|
312
|
-
return false;
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
if (payload.context && typeof payload.context === 'object' && !Array.isArray(payload.context)) {
|
|
317
|
-
const protectedContext = metadataBody(payload.context);
|
|
318
|
-
const outerContext = normalizeContextMetadata(message?.context);
|
|
319
|
-
if (canonicalStringify(outerContext) !== canonicalStringify(protectedContext))
|
|
320
|
-
return false;
|
|
321
|
-
}
|
|
322
|
-
return true;
|
|
323
|
-
}
|
|
324
|
-
/** AAD 序列化(排序键、紧凑 JSON) */
|
|
325
|
-
function aadBytesOffline(aad) {
|
|
326
|
-
return aadBytesWithOptionalFields(aad, AAD_FIELDS_OFFLINE);
|
|
327
|
-
}
|
|
328
|
-
/** AAD 匹配检查(解密时校验) */
|
|
329
|
-
function aadMatchesOffline(expected, actual) {
|
|
330
|
-
for (const field of AAD_MATCH_FIELDS_OFFLINE) {
|
|
331
|
-
// 宽松比较:JSON 序列化后对比
|
|
332
|
-
if (JSON.stringify(expected[field] ?? null) !== JSON.stringify(actual[field] ?? null)) {
|
|
333
|
-
return false;
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
return true;
|
|
337
|
-
}
|
|
338
|
-
/** 从 PEM 证书中提取 SPKI 公钥字节(解析 X.509 DER 结构) */
|
|
339
|
-
function extractSpkiFromCertPem(certPem) {
|
|
340
|
-
const der = pemToArrayBuffer(certPem);
|
|
341
|
-
return extractSpkiFromCertDer(new Uint8Array(der));
|
|
342
|
-
}
|
|
343
1
|
/**
|
|
344
|
-
*
|
|
345
|
-
* 简化解析:仅处理 P-256 ECDSA 证书(AUN 协议限定)。
|
|
346
|
-
*/
|
|
347
|
-
function extractSpkiFromCertDer(certDer) {
|
|
348
|
-
// X.509 结构: SEQUENCE { tbsCertificate, signatureAlgorithm, signatureValue }
|
|
349
|
-
// tbsCertificate: SEQUENCE { version, serialNumber, signature, issuer, validity, subject, subjectPublicKeyInfo, ... }
|
|
350
|
-
// 我们需要提取 subjectPublicKeyInfo 段
|
|
351
|
-
// 递归查找 SPKI:P-256 SPKI 的 OID 前缀为 30 59 30 13 06 07 2a 86 48 ce 3d 02 01
|
|
352
|
-
// 搜索模式:找到 P-256 的 AlgorithmIdentifier OID
|
|
353
|
-
const ecOid = new Uint8Array([0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01]);
|
|
354
|
-
const p256Oid = new Uint8Array([0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07]);
|
|
355
|
-
// 在 DER 中搜索 EC public key OID
|
|
356
|
-
for (let i = 0; i < certDer.length - ecOid.length; i++) {
|
|
357
|
-
let match = true;
|
|
358
|
-
for (let j = 0; j < ecOid.length; j++) {
|
|
359
|
-
if (certDer[i + j] !== ecOid[j]) {
|
|
360
|
-
match = false;
|
|
361
|
-
break;
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
if (!match)
|
|
365
|
-
continue;
|
|
366
|
-
// 找到 EC OID,往前回溯找 SEQUENCE 起始
|
|
367
|
-
// SPKI 格式: 30 <len> 30 <algLen> <ecOid> <p256Oid> 03 <bitStringLen> 00 <pubKeyBytes>
|
|
368
|
-
// 向前查找最近的 0x30(SEQUENCE)
|
|
369
|
-
for (let back = 1; back <= 4; back++) {
|
|
370
|
-
const seqStart = i - back;
|
|
371
|
-
if (seqStart < 0)
|
|
372
|
-
continue;
|
|
373
|
-
if (certDer[seqStart] !== 0x30)
|
|
374
|
-
continue;
|
|
375
|
-
// 解析 SEQUENCE 长度
|
|
376
|
-
const seqLen = parseDerLength(certDer, seqStart + 1);
|
|
377
|
-
if (seqLen === null)
|
|
378
|
-
continue;
|
|
379
|
-
const totalLen = 1 + seqLen.lenBytes + seqLen.value;
|
|
380
|
-
// SPKI 应包含完整的 AlgorithmIdentifier + BIT STRING
|
|
381
|
-
if (totalLen < 50 || totalLen > 120)
|
|
382
|
-
continue;
|
|
383
|
-
// 验证这确实是 SPKI(内部应包含 BIT STRING tag 0x03)
|
|
384
|
-
const spkiCandidate = certDer.slice(seqStart, seqStart + totalLen);
|
|
385
|
-
// 检查末尾附近有 BIT STRING
|
|
386
|
-
let hasBitString = false;
|
|
387
|
-
for (let k = 20; k < spkiCandidate.length - 10; k++) {
|
|
388
|
-
if (spkiCandidate[k] === 0x03 && spkiCandidate[k + 2] === 0x00) {
|
|
389
|
-
hasBitString = true;
|
|
390
|
-
break;
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
if (hasBitString) {
|
|
394
|
-
return spkiCandidate.buffer.slice(spkiCandidate.byteOffset, spkiCandidate.byteOffset + spkiCandidate.byteLength);
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
throw new E2EEError('无法从证书中提取 SPKI 公钥');
|
|
399
|
-
}
|
|
400
|
-
/** 解析 DER 长度字段 */
|
|
401
|
-
function parseDerLength(data, offset) {
|
|
402
|
-
if (offset >= data.length)
|
|
403
|
-
return null;
|
|
404
|
-
const first = data[offset];
|
|
405
|
-
if (first < 0x80) {
|
|
406
|
-
return { value: first, lenBytes: 1 };
|
|
407
|
-
}
|
|
408
|
-
const numBytes = first & 0x7f;
|
|
409
|
-
if (numBytes === 0 || numBytes > 4)
|
|
410
|
-
return null;
|
|
411
|
-
let value = 0;
|
|
412
|
-
for (let i = 0; i < numBytes; i++) {
|
|
413
|
-
if (offset + 1 + i >= data.length)
|
|
414
|
-
return null;
|
|
415
|
-
value = (value << 8) | data[offset + 1 + i];
|
|
416
|
-
}
|
|
417
|
-
return { value, lenBytes: 1 + numBytes };
|
|
418
|
-
}
|
|
419
|
-
/** 计算 SPKI 公钥的 SHA-256 指纹 */
|
|
420
|
-
async function fingerprintSpki(spkiBytes) {
|
|
421
|
-
const hash = await crypto.subtle.digest('SHA-256', spkiBytes);
|
|
422
|
-
const hex = Array.from(new Uint8Array(hash)).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
423
|
-
return `sha256:${hex}`;
|
|
424
|
-
}
|
|
425
|
-
/** 从 PEM 证书计算证书 SHA-256 指纹 */
|
|
426
|
-
async function fingerprintCertPem(certPem) {
|
|
427
|
-
return certificateSha256Fingerprint(certPem);
|
|
428
|
-
}
|
|
429
|
-
/** 从 PEM 证书计算证书 SHA-256 指纹 */
|
|
430
|
-
async function certificateSha256Fingerprint(certPem) {
|
|
431
|
-
const der = pemToArrayBuffer(certPem);
|
|
432
|
-
const hash = await crypto.subtle.digest('SHA-256', der);
|
|
433
|
-
const hex = Array.from(new Uint8Array(hash)).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
434
|
-
return `sha256:${hex}`;
|
|
435
|
-
}
|
|
436
|
-
/** 从 SPKI DER base64 计算公钥指纹 */
|
|
437
|
-
async function fingerprintDerB64(derB64) {
|
|
438
|
-
const der = base64ToUint8(derB64);
|
|
439
|
-
return fingerprintSpki(toArrayBuffer(der));
|
|
440
|
-
}
|
|
441
|
-
/** 导入 PEM 证书公钥为 ECDSA CryptoKey */
|
|
442
|
-
async function importCertPublicKeyEcdsa(certPem) {
|
|
443
|
-
const spki = extractSpkiFromCertPem(certPem);
|
|
444
|
-
return crypto.subtle.importKey('spki', spki, { name: 'ECDSA', namedCurve: 'P-256' }, true, ['verify']);
|
|
445
|
-
}
|
|
446
|
-
/** 导入 PEM 证书公钥为 ECDH CryptoKey */
|
|
447
|
-
async function importCertPublicKeyEcdh(certPem) {
|
|
448
|
-
const spki = extractSpkiFromCertPem(certPem);
|
|
449
|
-
return crypto.subtle.importKey('spki', spki, { name: 'ECDH', namedCurve: 'P-256' }, true, []);
|
|
450
|
-
}
|
|
451
|
-
/** 导入 SPKI DER base64 为 ECDH CryptoKey */
|
|
452
|
-
async function importSpkiDerB64Ecdh(derB64) {
|
|
453
|
-
const der = base64ToUint8(derB64);
|
|
454
|
-
return crypto.subtle.importKey('spki', toArrayBuffer(der), { name: 'ECDH', namedCurve: 'P-256' }, true, []);
|
|
455
|
-
}
|
|
456
|
-
/** 导入 SPKI DER base64 为 ECDSA CryptoKey */
|
|
457
|
-
async function importSpkiDerB64Ecdsa(derB64) {
|
|
458
|
-
const der = base64ToUint8(derB64);
|
|
459
|
-
return crypto.subtle.importKey('spki', toArrayBuffer(der), { name: 'ECDSA', namedCurve: 'P-256' }, true, ['verify']);
|
|
460
|
-
}
|
|
461
|
-
/** 导入 PEM 私钥为 ECDSA CryptoKey */
|
|
462
|
-
/**
|
|
463
|
-
* ECDSA 私钥导入缓存:避免每次签名都重复调用 crypto.subtle.importKey。
|
|
464
|
-
* 缓存键为 PEM 字符串本身,identity 变更时新 PEM 自然不命中旧缓存。
|
|
465
|
-
*/
|
|
466
|
-
const _ecdsaKeyCache = new Map();
|
|
467
|
-
async function importPrivateKeyEcdsa(pem) {
|
|
468
|
-
const cached = _ecdsaKeyCache.get(pem);
|
|
469
|
-
if (cached)
|
|
470
|
-
return cached;
|
|
471
|
-
const pkcs8 = pemToArrayBuffer(pem);
|
|
472
|
-
const key = await crypto.subtle.importKey('pkcs8', pkcs8, { name: 'ECDSA', namedCurve: 'P-256' }, true, ['sign']);
|
|
473
|
-
_ecdsaKeyCache.set(pem, key);
|
|
474
|
-
return key;
|
|
475
|
-
}
|
|
476
|
-
/** 导入 PEM 私钥为 ECDH CryptoKey */
|
|
477
|
-
async function importPrivateKeyEcdh(pem) {
|
|
478
|
-
const pkcs8 = pemToArrayBuffer(pem);
|
|
479
|
-
return crypto.subtle.importKey('pkcs8', pkcs8, { name: 'ECDH', namedCurve: 'P-256' }, true, ['deriveBits']);
|
|
480
|
-
}
|
|
481
|
-
/** ECDH 派生共享密钥(256 位) */
|
|
482
|
-
async function ecdhDeriveBits(privateKey, publicKey) {
|
|
483
|
-
const bits = await crypto.subtle.deriveBits({ name: 'ECDH', public: publicKey }, privateKey, 256);
|
|
484
|
-
return new Uint8Array(bits);
|
|
485
|
-
}
|
|
486
|
-
/** HKDF 派生密钥(256 位) */
|
|
487
|
-
async function hkdfDerive(ikm, info) {
|
|
488
|
-
const ikmKey = await crypto.subtle.importKey('raw', toBufferSource(ikm), 'HKDF', false, ['deriveBits']);
|
|
489
|
-
const bits = await crypto.subtle.deriveBits({
|
|
490
|
-
name: 'HKDF',
|
|
491
|
-
hash: 'SHA-256',
|
|
492
|
-
salt: toBufferSource(new Uint8Array(0)),
|
|
493
|
-
info: toBufferSource(_encoder.encode(info)),
|
|
494
|
-
}, ikmKey, 256);
|
|
495
|
-
return new Uint8Array(bits);
|
|
496
|
-
}
|
|
497
|
-
/** AES-GCM 加密,返回 [ciphertext, tag](SubtleCrypto 将 tag 附加到末尾) */
|
|
498
|
-
async function aesGcmEncrypt(key, nonce, plaintext, aad) {
|
|
499
|
-
const aesKey = await crypto.subtle.importKey('raw', toBufferSource(key), 'AES-GCM', false, ['encrypt']);
|
|
500
|
-
const ct = await crypto.subtle.encrypt({ name: 'AES-GCM', iv: toBufferSource(nonce), additionalData: toBufferSource(aad), tagLength: 128 }, aesKey, toBufferSource(plaintext));
|
|
501
|
-
const arr = new Uint8Array(ct);
|
|
502
|
-
// SubtleCrypto 将 16 字节 tag 附加到 ciphertext 末尾
|
|
503
|
-
return [arr.slice(0, -16), arr.slice(-16)];
|
|
504
|
-
}
|
|
505
|
-
/** AES-GCM 解密 */
|
|
506
|
-
async function aesGcmDecrypt(key, nonce, ciphertext, tag, aad) {
|
|
507
|
-
const aesKey = await crypto.subtle.importKey('raw', toBufferSource(key), 'AES-GCM', false, ['decrypt']);
|
|
508
|
-
// SubtleCrypto 要求 ciphertext + tag 拼接传入
|
|
509
|
-
const combined = concatBytes(ciphertext, tag);
|
|
510
|
-
const pt = await crypto.subtle.decrypt({ name: 'AES-GCM', iv: toBufferSource(nonce), additionalData: toBufferSource(aad), tagLength: 128 }, aesKey, toBufferSource(combined));
|
|
511
|
-
return new Uint8Array(pt);
|
|
512
|
-
}
|
|
513
|
-
/** ECDSA 签名(输出 DER 格式,兼容 Python/Go) */
|
|
514
|
-
async function ecdsaSignDer(privateKey, data) {
|
|
515
|
-
const sig = await crypto.subtle.sign({ name: 'ECDSA', hash: 'SHA-256' }, privateKey, toBufferSource(data));
|
|
516
|
-
// SubtleCrypto 输出 P1363 格式,转换为 DER
|
|
517
|
-
return p1363ToDer(new Uint8Array(sig));
|
|
518
|
-
}
|
|
519
|
-
/** ECDSA 验签(输入 DER 格式签名) */
|
|
520
|
-
async function ecdsaVerifyDer(publicKey, signature, data) {
|
|
521
|
-
// DER → P1363 用于 SubtleCrypto 验签
|
|
522
|
-
const p1363 = derToP1363(signature);
|
|
523
|
-
return crypto.subtle.verify({ name: 'ECDSA', hash: 'SHA-256' }, publicKey, toBufferSource(p1363), toBufferSource(data));
|
|
524
|
-
}
|
|
525
|
-
/** 生成 12 字节随机 nonce */
|
|
526
|
-
function randomNonce() {
|
|
527
|
-
const nonce = new Uint8Array(12);
|
|
528
|
-
crypto.getRandomValues(nonce);
|
|
529
|
-
return nonce;
|
|
530
|
-
}
|
|
531
|
-
/** 生成 UUID v4 */
|
|
532
|
-
function uuidV4() {
|
|
533
|
-
const bytes = new Uint8Array(16);
|
|
534
|
-
crypto.getRandomValues(bytes);
|
|
535
|
-
bytes[6] = (bytes[6] & 0x0f) | 0x40;
|
|
536
|
-
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
|
537
|
-
const hex = Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
538
|
-
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
539
|
-
}
|
|
540
|
-
/** 导入 X9.62 未压缩点为 ECDH CryptoKey(用于解密时导入临时公钥) */
|
|
541
|
-
async function importUncompressedPointEcdh(pointBytes) {
|
|
542
|
-
// 将未压缩点(0x04 || x || y)包装为 SPKI 格式
|
|
543
|
-
// P-256 SPKI = 固定头 + 未压缩点(65 字节)
|
|
544
|
-
const spkiHeader = new Uint8Array([
|
|
545
|
-
0x30, 0x59, // SEQUENCE (89 bytes)
|
|
546
|
-
0x30, 0x13, // SEQUENCE (19 bytes) - AlgorithmIdentifier
|
|
547
|
-
0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, // OID: 1.2.840.10045.2.1 (EC)
|
|
548
|
-
0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07, // OID: 1.2.840.10045.3.1.7 (P-256)
|
|
549
|
-
0x03, 0x42, 0x00, // BIT STRING (66 bytes, 0 unused bits)
|
|
550
|
-
]);
|
|
551
|
-
const spki = concatBytes(spkiHeader, pointBytes);
|
|
552
|
-
return crypto.subtle.importKey('spki', toArrayBuffer(spki), { name: 'ECDH', namedCurve: 'P-256' }, true, []);
|
|
553
|
-
}
|
|
554
|
-
// ── E2EEManager 主类 ────────────────────────────────────────
|
|
555
|
-
/**
|
|
556
|
-
* P2P 端到端加密管理器 — 浏览器 SubtleCrypto 实现。
|
|
557
|
-
*
|
|
558
|
-
* 加密策略: prekey_ecdh_v2(四路 ECDH)→ long_term_key(二路 ECDH)两层降级。
|
|
559
|
-
* I/O(获取 prekey、证书)由调用方(AUNClient)负责。
|
|
560
|
-
* 内置本地防重放(seen set),裸 WebSocket 开发者无需额外实现。
|
|
2
|
+
* E2EE V2-only 兼容入口。
|
|
561
3
|
*
|
|
562
|
-
*
|
|
4
|
+
* 旧版 P2P E2EEManager 已移除;这里仅保留应用层可能直接使用的
|
|
5
|
+
* protected headers helper,和 TS SDK 的 V2-only 入口保持一致。
|
|
563
6
|
*/
|
|
564
|
-
export
|
|
565
|
-
_log = _noopLog;
|
|
566
|
-
setLogger(log) { this._log = log; }
|
|
567
|
-
_identityFn;
|
|
568
|
-
_deviceIdFn;
|
|
569
|
-
_keystoreRef;
|
|
570
|
-
/** 本地防重放 seen set */
|
|
571
|
-
_seenMessages = new Map();
|
|
572
|
-
_seenMaxSize = 50000;
|
|
573
|
-
/** 对方 prekey 内存缓存 {peerAid: {prekey, expireAt}} */
|
|
574
|
-
_prekeyCache = new Map();
|
|
575
|
-
_prekeyCacheTtl;
|
|
576
|
-
/** 本地 prekey 私钥 PEM 内存缓存 {prekeyId: privateKeyPem} */
|
|
577
|
-
_localPrekeyCache = new Map();
|
|
578
|
-
/** 防重放时间窗口(秒) */
|
|
579
|
-
_replayWindowSeconds;
|
|
580
|
-
constructor(opts) {
|
|
581
|
-
this._identityFn = opts.identityFn;
|
|
582
|
-
this._deviceIdFn = opts.deviceIdFn ?? (() => '');
|
|
583
|
-
this._keystoreRef = opts.keystore;
|
|
584
|
-
this._prekeyCacheTtl = opts.prekeyCacheTtl ?? 3600;
|
|
585
|
-
this._replayWindowSeconds = opts.replayWindowSeconds ?? 300;
|
|
586
|
-
}
|
|
587
|
-
// ── Prekey 缓存 ──────────────────────────────────
|
|
588
|
-
/** 缓存对方的 prekey */
|
|
589
|
-
cachePrekey(peerAid, prekey) {
|
|
590
|
-
this._prekeyCache.set(peerAid, {
|
|
591
|
-
prekey,
|
|
592
|
-
expireAt: Date.now() / 1000 + this._prekeyCacheTtl,
|
|
593
|
-
});
|
|
594
|
-
}
|
|
595
|
-
/** 获取缓存的 prekey(过期返回 null) */
|
|
596
|
-
getCachedPrekey(peerAid) {
|
|
597
|
-
const cached = this._prekeyCache.get(peerAid);
|
|
598
|
-
if (!cached)
|
|
599
|
-
return null;
|
|
600
|
-
if (Date.now() / 1000 >= cached.expireAt) {
|
|
601
|
-
this._prekeyCache.delete(peerAid);
|
|
602
|
-
return null;
|
|
603
|
-
}
|
|
604
|
-
return cached.prekey;
|
|
605
|
-
}
|
|
606
|
-
/** 使 prekey 缓存失效 */
|
|
607
|
-
invalidatePrekeyCache(peerAid) {
|
|
608
|
-
this._prekeyCache.delete(peerAid);
|
|
609
|
-
}
|
|
610
|
-
// ── 便利方法 ──────────────────────────────────────
|
|
611
|
-
/**
|
|
612
|
-
* 加密消息(便利方法)。
|
|
613
|
-
* 调用方负责提前获取 peerCertPem 和 prekey(可选)。
|
|
614
|
-
*/
|
|
615
|
-
async encryptMessage(toAid, payload, opts) {
|
|
616
|
-
const tStart = Date.now();
|
|
617
|
-
this._log.debug(`encryptMessage enter: to_aid=${toAid} has_prekey=${!!opts.prekey}`);
|
|
618
|
-
try {
|
|
619
|
-
const messageId = opts.messageId ?? uuidV4();
|
|
620
|
-
const timestamp = opts.timestamp ?? Date.now();
|
|
621
|
-
const result = await this.encryptOutbound(toAid, payload, {
|
|
622
|
-
peerCertPem: opts.peerCertPem,
|
|
623
|
-
prekey: opts.prekey ?? null,
|
|
624
|
-
messageId,
|
|
625
|
-
timestamp,
|
|
626
|
-
protectedHeaders: opts.protectedHeaders ?? opts.protected_headers ?? opts.headers,
|
|
627
|
-
context: opts.context ?? null,
|
|
628
|
-
});
|
|
629
|
-
this._log.debug(`encryptMessage exit: elapsed=${Date.now() - tStart}ms to_aid=${toAid} mode=${result[1].mode}`);
|
|
630
|
-
return result;
|
|
631
|
-
}
|
|
632
|
-
catch (err) {
|
|
633
|
-
this._log.debug(`encryptMessage exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
|
|
634
|
-
throw err;
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
// ── 加密 ──────────────────────────────────────────
|
|
638
|
-
/**
|
|
639
|
-
* 加密出站消息:有 prekey → prekey_ecdh_v2(四路 ECDH),无 prekey → long_term_key。
|
|
640
|
-
*
|
|
641
|
-
* 返回 [envelope, resultInfo],resultInfo 包含加密状态详情。
|
|
642
|
-
* prekey 传入时自动缓存;传入 null 时自动查缓存。
|
|
643
|
-
*/
|
|
644
|
-
async encryptOutbound(peerAid, payload, opts) {
|
|
645
|
-
const tStart = Date.now();
|
|
646
|
-
this._log.debug(`encryptOutbound enter: peer_aid=${peerAid} mid=${opts.messageId} has_prekey=${!!opts.prekey}`);
|
|
647
|
-
try {
|
|
648
|
-
let prekey = opts.prekey ?? null;
|
|
649
|
-
// 传入 prekey → 缓存;传入 null → 查缓存
|
|
650
|
-
if (prekey !== null) {
|
|
651
|
-
this.cachePrekey(peerAid, prekey);
|
|
652
|
-
}
|
|
653
|
-
else {
|
|
654
|
-
prekey = this.getCachedPrekey(peerAid);
|
|
655
|
-
}
|
|
656
|
-
if (prekey) {
|
|
657
|
-
try {
|
|
658
|
-
const envelope = await this._encryptWithPrekey(peerAid, payload, prekey, opts.peerCertPem, opts.messageId, opts.timestamp, opts.protectedHeaders ?? opts.protected_headers ?? opts.headers, opts.context ?? null);
|
|
659
|
-
this._log.debug(`encryptOutbound exit: elapsed=${Date.now() - tStart}ms peer_aid=${peerAid} mode=prekey_ecdh_v2`);
|
|
660
|
-
return [envelope, {
|
|
661
|
-
encrypted: true,
|
|
662
|
-
forward_secrecy: true,
|
|
663
|
-
mode: MODE_PREKEY_ECDH_V2,
|
|
664
|
-
degraded: false,
|
|
665
|
-
}];
|
|
666
|
-
}
|
|
667
|
-
catch (exc) {
|
|
668
|
-
this._log.warn('prekey encrypt failed, degrade to long_term_key (no forward secrecy):', exc);
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
const envelope = await this._encryptWithLongTermKey(peerAid, payload, opts.peerCertPem, opts.messageId, opts.timestamp, opts.protectedHeaders ?? opts.protected_headers ?? opts.headers, opts.context ?? null);
|
|
672
|
-
const degraded = prekey !== null; // 有 prekey 但失败了才算降级
|
|
673
|
-
this._log.debug(`encryptOutbound exit: elapsed=${Date.now() - tStart}ms peer_aid=${peerAid} mode=long_term_key degraded=${degraded}`);
|
|
674
|
-
return [envelope, {
|
|
675
|
-
encrypted: true,
|
|
676
|
-
forward_secrecy: false,
|
|
677
|
-
mode: MODE_LONG_TERM_KEY,
|
|
678
|
-
degraded,
|
|
679
|
-
degradation_reason: degraded ? 'prekey_encrypt_failed' : 'no_prekey_available',
|
|
680
|
-
}];
|
|
681
|
-
}
|
|
682
|
-
catch (err) {
|
|
683
|
-
this._log.debug(`encryptOutbound exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
|
|
684
|
-
throw err;
|
|
685
|
-
}
|
|
686
|
-
}
|
|
687
|
-
/**
|
|
688
|
-
* 使用对方 prekey 加密(prekey_ecdh_v2 模式,四路 ECDH + 发送方签名)
|
|
689
|
-
*
|
|
690
|
-
* 四路 ECDH:
|
|
691
|
-
* DH1 = ECDH(ephemeral, peer_prekey)
|
|
692
|
-
* DH2 = ECDH(ephemeral, peer_identity)
|
|
693
|
-
* DH3 = ECDH(sender_identity, peer_prekey) ← 绑定发送方身份
|
|
694
|
-
* DH4 = ECDH(sender_identity, peer_identity) ← 双方身份互绑
|
|
695
|
-
*/
|
|
696
|
-
async _encryptWithPrekey(peerAid, payload, prekey, peerCertPem, messageId, timestamp, protectedHeaders, context) {
|
|
697
|
-
// 导入对方 identity 公钥(ECDSA 用于验签,ECDH 用于密钥交换)
|
|
698
|
-
const peerIdentityEcdsa = await importCertPublicKeyEcdsa(peerCertPem);
|
|
699
|
-
const peerIdentityEcdh = await importCertPublicKeyEcdh(peerCertPem);
|
|
700
|
-
const expectedCertFingerprint = String(prekey.cert_fingerprint ?? '').trim().toLowerCase();
|
|
701
|
-
if (expectedCertFingerprint) {
|
|
702
|
-
const actualCertFingerprint = await certificateSha256Fingerprint(peerCertPem);
|
|
703
|
-
if (actualCertFingerprint !== expectedCertFingerprint) {
|
|
704
|
-
throw new E2EEError('prekey cert fingerprint mismatch');
|
|
705
|
-
}
|
|
706
|
-
}
|
|
707
|
-
// 验证 prekey 签名
|
|
708
|
-
const prekeyId = prekey.prekey_id;
|
|
709
|
-
const prekeyPubB64 = prekey.public_key;
|
|
710
|
-
const createdAt = prekey.created_at;
|
|
711
|
-
let signData;
|
|
712
|
-
if (createdAt !== undefined) {
|
|
713
|
-
signData = _encoder.encode(`${prekeyId}|${prekeyPubB64}|${createdAt}`);
|
|
714
|
-
}
|
|
715
|
-
else {
|
|
716
|
-
signData = _encoder.encode(`${prekeyId}|${prekeyPubB64}`);
|
|
717
|
-
}
|
|
718
|
-
const sigBytes = base64ToUint8(prekey.signature);
|
|
719
|
-
const sigValid = await ecdsaVerifyDer(peerIdentityEcdsa, sigBytes, signData);
|
|
720
|
-
if (!sigValid) {
|
|
721
|
-
throw new E2EEError('prekey 签名验证失败');
|
|
722
|
-
}
|
|
723
|
-
// 导入对方 prekey 公钥(ECDH)
|
|
724
|
-
const peerPrekeyEcdh = await importSpkiDerB64Ecdh(prekeyPubB64);
|
|
725
|
-
// 加载发送方 identity 私钥
|
|
726
|
-
const senderIdentityEcdhKey = await this._loadSenderIdentityPrivateEcdh();
|
|
727
|
-
const senderSignKey = await this._loadSenderIdentityPrivateEcdsa();
|
|
728
|
-
// 生成临时 ECDH 密钥对
|
|
729
|
-
const ephemeral = await crypto.subtle.generateKey({ name: 'ECDH', namedCurve: 'P-256' }, true, ['deriveBits']);
|
|
730
|
-
// 导出临时公钥为 X9.62 未压缩点
|
|
731
|
-
const ephRaw = await crypto.subtle.exportKey('raw', ephemeral.publicKey);
|
|
732
|
-
const ephPublicBytes = new Uint8Array(ephRaw);
|
|
733
|
-
// 四路 ECDH
|
|
734
|
-
const dh1 = await ecdhDeriveBits(ephemeral.privateKey, peerPrekeyEcdh);
|
|
735
|
-
const dh2 = await ecdhDeriveBits(ephemeral.privateKey, peerIdentityEcdh);
|
|
736
|
-
const dh3 = await ecdhDeriveBits(senderIdentityEcdhKey, peerPrekeyEcdh);
|
|
737
|
-
const dh4 = await ecdhDeriveBits(senderIdentityEcdhKey, peerIdentityEcdh);
|
|
738
|
-
const combined = concatBytes(dh1, dh2, dh3, dh4);
|
|
739
|
-
// HKDF
|
|
740
|
-
const messageKey = await hkdfDerive(combined, `aun-prekey-v2:${prekeyId}`);
|
|
741
|
-
// AES-GCM 加密
|
|
742
|
-
const plaintext = _encoder.encode(JSON.stringify(payload));
|
|
743
|
-
const nonce = randomNonce();
|
|
744
|
-
const senderFingerprint = await this._localCertSha256Fingerprint() || await this._localIdentityFingerprint();
|
|
745
|
-
const recipientFingerprint = await fingerprintCertPem(peerCertPem);
|
|
746
|
-
const ephPkB64 = uint8ToBase64(ephPublicBytes);
|
|
747
|
-
const aad = {
|
|
748
|
-
from: this._currentAid(),
|
|
749
|
-
to: peerAid,
|
|
750
|
-
message_id: messageId,
|
|
751
|
-
timestamp,
|
|
752
|
-
encryption_mode: MODE_PREKEY_ECDH_V2,
|
|
753
|
-
suite: SUITE,
|
|
754
|
-
ephemeral_public_key: ephPkB64,
|
|
755
|
-
recipient_cert_fingerprint: recipientFingerprint,
|
|
756
|
-
sender_cert_fingerprint: senderFingerprint,
|
|
757
|
-
prekey_id: prekeyId,
|
|
758
|
-
};
|
|
759
|
-
const envelope = {
|
|
760
|
-
type: 'e2ee.encrypted',
|
|
761
|
-
version: '1',
|
|
762
|
-
encryption_mode: MODE_PREKEY_ECDH_V2,
|
|
763
|
-
suite: SUITE,
|
|
764
|
-
prekey_id: prekeyId,
|
|
765
|
-
ephemeral_public_key: ephPkB64,
|
|
766
|
-
};
|
|
767
|
-
await copyOptionalEnvelopeMetadata(envelope, messageKey, {
|
|
768
|
-
payloadType: payload.type,
|
|
769
|
-
protectedHeaders,
|
|
770
|
-
context,
|
|
771
|
-
});
|
|
772
|
-
const aadBytes = aadBytesOffline(aad);
|
|
773
|
-
const [ciphertext, tag] = await aesGcmEncrypt(messageKey, nonce, plaintext, aadBytes);
|
|
774
|
-
envelope.nonce = uint8ToBase64(nonce);
|
|
775
|
-
envelope.ciphertext = uint8ToBase64(ciphertext);
|
|
776
|
-
envelope.tag = uint8ToBase64(tag);
|
|
777
|
-
envelope.aad = aad;
|
|
778
|
-
// 发送方签名:对 ciphertext + tag + aad_bytes 签名(不可否认性)
|
|
779
|
-
const signPayload = concatBytes(ciphertext, tag, aadBytes);
|
|
780
|
-
const sig = await ecdsaSignDer(senderSignKey, signPayload);
|
|
781
|
-
envelope.sender_signature = uint8ToBase64(sig);
|
|
782
|
-
envelope.sender_cert_fingerprint = senderFingerprint;
|
|
783
|
-
return envelope;
|
|
784
|
-
}
|
|
785
|
-
/**
|
|
786
|
-
* 使用 2DH 加密(long_term_key 模式 + 发送方签名)
|
|
787
|
-
*
|
|
788
|
-
* 2DH:
|
|
789
|
-
* DH1 = ECDH(ephemeral, peer_identity) ← 前向保密(每消息)
|
|
790
|
-
* DH2 = ECDH(sender_identity, peer_identity) ← 绑定双方身份
|
|
791
|
-
*/
|
|
792
|
-
async _encryptWithLongTermKey(peerAid, payload, peerCertPem, messageId, timestamp, protectedHeaders, context) {
|
|
793
|
-
const peerIdentityEcdh = await importCertPublicKeyEcdh(peerCertPem);
|
|
794
|
-
const senderIdentityEcdhKey = await this._loadSenderIdentityPrivateEcdh();
|
|
795
|
-
const senderSignKey = await this._loadSenderIdentityPrivateEcdsa();
|
|
796
|
-
// 生成临时 ECDH 密钥对
|
|
797
|
-
const ephemeral = await crypto.subtle.generateKey({ name: 'ECDH', namedCurve: 'P-256' }, true, ['deriveBits']);
|
|
798
|
-
const ephRaw = await crypto.subtle.exportKey('raw', ephemeral.publicKey);
|
|
799
|
-
const ephPublicBytes = new Uint8Array(ephRaw);
|
|
800
|
-
// 2DH + HKDF
|
|
801
|
-
const dh1 = await ecdhDeriveBits(ephemeral.privateKey, peerIdentityEcdh);
|
|
802
|
-
const dh2 = await ecdhDeriveBits(senderIdentityEcdhKey, peerIdentityEcdh);
|
|
803
|
-
const combined = concatBytes(dh1, dh2);
|
|
804
|
-
const messageKey = await hkdfDerive(combined, 'aun-longterm-v2');
|
|
805
|
-
// AES-GCM 加密
|
|
806
|
-
const plaintext = _encoder.encode(JSON.stringify(payload));
|
|
807
|
-
const nonce = randomNonce();
|
|
808
|
-
const senderFingerprint = await this._localCertSha256Fingerprint() || await this._localIdentityFingerprint();
|
|
809
|
-
const recipientFingerprint = await fingerprintCertPem(peerCertPem);
|
|
810
|
-
const ephPkB64 = uint8ToBase64(ephPublicBytes);
|
|
811
|
-
const aad = {
|
|
812
|
-
from: this._currentAid(),
|
|
813
|
-
to: peerAid,
|
|
814
|
-
message_id: messageId,
|
|
815
|
-
timestamp,
|
|
816
|
-
encryption_mode: MODE_LONG_TERM_KEY,
|
|
817
|
-
suite: SUITE,
|
|
818
|
-
ephemeral_public_key: ephPkB64,
|
|
819
|
-
recipient_cert_fingerprint: recipientFingerprint,
|
|
820
|
-
sender_cert_fingerprint: senderFingerprint,
|
|
821
|
-
};
|
|
822
|
-
const envelope = {
|
|
823
|
-
type: 'e2ee.encrypted',
|
|
824
|
-
version: '1',
|
|
825
|
-
encryption_mode: MODE_LONG_TERM_KEY,
|
|
826
|
-
suite: SUITE,
|
|
827
|
-
ephemeral_public_key: ephPkB64,
|
|
828
|
-
};
|
|
829
|
-
await copyOptionalEnvelopeMetadata(envelope, messageKey, {
|
|
830
|
-
payloadType: payload.type,
|
|
831
|
-
protectedHeaders,
|
|
832
|
-
context,
|
|
833
|
-
});
|
|
834
|
-
const aadBytes = aadBytesOffline(aad);
|
|
835
|
-
const [ciphertext, tag] = await aesGcmEncrypt(messageKey, nonce, plaintext, aadBytes);
|
|
836
|
-
envelope.nonce = uint8ToBase64(nonce);
|
|
837
|
-
envelope.ciphertext = uint8ToBase64(ciphertext);
|
|
838
|
-
envelope.tag = uint8ToBase64(tag);
|
|
839
|
-
envelope.aad = aad;
|
|
840
|
-
// 发送方签名(不可否认性)
|
|
841
|
-
const signPayload = concatBytes(ciphertext, tag, aadBytes);
|
|
842
|
-
const sig = await ecdsaSignDer(senderSignKey, signPayload);
|
|
843
|
-
envelope.sender_signature = uint8ToBase64(sig);
|
|
844
|
-
envelope.sender_cert_fingerprint = senderFingerprint;
|
|
845
|
-
return envelope;
|
|
846
|
-
}
|
|
847
|
-
// ── 解密 ──────────────────────────────────────────
|
|
848
|
-
/**
|
|
849
|
-
* 解密单条消息(内置本地防重放 + timestamp 窗口 + 发送方签名验证)。
|
|
850
|
-
*
|
|
851
|
-
* 返回解密后的 message 对象,或 null 表示失败/拒绝。
|
|
852
|
-
* 非加密消息原样返回。
|
|
853
|
-
*
|
|
854
|
-
* opts.skipReplay: 跳过防重放和 timestamp 窗口检查(用于 message.pull 场景)。
|
|
855
|
-
*/
|
|
856
|
-
async decryptMessage(message, opts) {
|
|
857
|
-
const tStart = Date.now();
|
|
858
|
-
const mid = String(message.message_id ?? '');
|
|
859
|
-
const fromAid = String(message.from ?? '');
|
|
860
|
-
this._log.debug(`decryptMessage enter: from=${fromAid} mid=${mid} skip_replay=${!!opts?.skipReplay}`);
|
|
861
|
-
try {
|
|
862
|
-
const payload = message.payload;
|
|
863
|
-
if (!payload || typeof payload !== 'object') {
|
|
864
|
-
this._log.debug(`decryptMessage exit: elapsed=${Date.now() - tStart}ms result=passthrough_no_payload`);
|
|
865
|
-
return message;
|
|
866
|
-
}
|
|
867
|
-
if (payload.type !== 'e2ee.encrypted') {
|
|
868
|
-
this._log.debug(`decryptMessage exit: elapsed=${Date.now() - tStart}ms result=passthrough_not_encrypted`);
|
|
869
|
-
return message;
|
|
870
|
-
}
|
|
871
|
-
if (message.encrypted === false) {
|
|
872
|
-
this._log.debug(`decryptMessage exit: elapsed=${Date.now() - tStart}ms result=passthrough_flag_false`);
|
|
873
|
-
return message;
|
|
874
|
-
}
|
|
875
|
-
if (!this._shouldDecryptForCurrentAid(message, payload)) {
|
|
876
|
-
this._log.debug(`decryptMessage exit: elapsed=${Date.now() - tStart}ms result=passthrough_not_for_current_aid`);
|
|
877
|
-
return message;
|
|
878
|
-
}
|
|
879
|
-
const skipReplay = opts?.skipReplay ?? false;
|
|
880
|
-
if (!skipReplay) {
|
|
881
|
-
// timestamp 窗口检查
|
|
882
|
-
const ts = (message.timestamp ?? payload.aad?.timestamp);
|
|
883
|
-
if (typeof ts === 'number' && this._replayWindowSeconds > 0) {
|
|
884
|
-
const nowMs = Date.now();
|
|
885
|
-
const diffS = Math.abs(nowMs - ts) / 1000;
|
|
886
|
-
if (diffS > this._replayWindowSeconds) {
|
|
887
|
-
this._log.warn(`消息 timestamp 超出窗口 (${Math.round(diffS)}s > ${this._replayWindowSeconds}s),拒绝: from=${message.from} mid=${message.message_id}`);
|
|
888
|
-
this._log.debug(`decryptMessage exit: elapsed=${Date.now() - tStart}ms result=rejected_timestamp_window`);
|
|
889
|
-
return null;
|
|
890
|
-
}
|
|
891
|
-
}
|
|
892
|
-
// 本地防重放:先检查,解密成功后再记录。
|
|
893
|
-
const messageIdVal = message.message_id;
|
|
894
|
-
const fromAidLocal = message.from;
|
|
895
|
-
let seenKey = '';
|
|
896
|
-
if (messageIdVal && fromAidLocal) {
|
|
897
|
-
seenKey = `${fromAidLocal}:${messageIdVal}`;
|
|
898
|
-
if (this._seenMessages.has(seenKey)) {
|
|
899
|
-
this._log.debug(`decryptMessage exit: elapsed=${Date.now() - tStart}ms result=rejected_replay`);
|
|
900
|
-
return null;
|
|
901
|
-
}
|
|
902
|
-
}
|
|
903
|
-
const result = await this._decryptMessageInternal(message);
|
|
904
|
-
if (result !== null && seenKey) {
|
|
905
|
-
this._seenMessages.set(seenKey, true);
|
|
906
|
-
this._trimSeenSet();
|
|
907
|
-
}
|
|
908
|
-
this._log.debug(`decryptMessage exit: elapsed=${Date.now() - tStart}ms result=${result !== null ? 'ok' : 'failed'}`);
|
|
909
|
-
return result;
|
|
910
|
-
}
|
|
911
|
-
const result = await this._decryptMessageInternal(message);
|
|
912
|
-
this._log.debug(`decryptMessage exit: elapsed=${Date.now() - tStart}ms result=${result !== null ? 'ok' : 'failed'} skip_replay=true`);
|
|
913
|
-
return result;
|
|
914
|
-
}
|
|
915
|
-
catch (err) {
|
|
916
|
-
this._log.debug(`decryptMessage exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
|
|
917
|
-
throw err;
|
|
918
|
-
}
|
|
919
|
-
}
|
|
920
|
-
/** 判断是否应该为当前 AID 解密(避免发送端回显消息误走解密) */
|
|
921
|
-
_shouldDecryptForCurrentAid(message, payload) {
|
|
922
|
-
if (String(message.direction ?? '').trim().toLowerCase() === 'outbound_sync') {
|
|
923
|
-
return true;
|
|
924
|
-
}
|
|
925
|
-
const currentAid = this._currentAid();
|
|
926
|
-
if (!currentAid)
|
|
927
|
-
return true;
|
|
928
|
-
const targetAid = (message.to
|
|
929
|
-
?? payload.aad?.to
|
|
930
|
-
?? payload.to);
|
|
931
|
-
if (!targetAid)
|
|
932
|
-
return true;
|
|
933
|
-
return String(targetAid) === String(currentAid);
|
|
934
|
-
}
|
|
935
|
-
/** 内部解密分发 */
|
|
936
|
-
async _decryptMessageInternal(message) {
|
|
937
|
-
const payload = message.payload;
|
|
938
|
-
// 验证发送方签名(适用于所有模式)
|
|
939
|
-
try {
|
|
940
|
-
await this._verifySenderSignature(payload, message);
|
|
941
|
-
}
|
|
942
|
-
catch (exc) {
|
|
943
|
-
this._log.warn('sendersignature verifyfailed:', exc);
|
|
944
|
-
return null;
|
|
945
|
-
}
|
|
946
|
-
const encryptionMode = payload.encryption_mode;
|
|
947
|
-
if (encryptionMode === MODE_PREKEY_ECDH_V2) {
|
|
948
|
-
return this._decryptMessagePrekeyV2(message);
|
|
949
|
-
}
|
|
950
|
-
else if (encryptionMode === MODE_LONG_TERM_KEY) {
|
|
951
|
-
return this._decryptMessageLongTerm(message);
|
|
952
|
-
}
|
|
953
|
-
else {
|
|
954
|
-
this._log.warn('unsupported encryption mode:', encryptionMode);
|
|
955
|
-
return null;
|
|
956
|
-
}
|
|
957
|
-
}
|
|
958
|
-
/** 验证发送方签名 */
|
|
959
|
-
async _verifySenderSignature(payload, message) {
|
|
960
|
-
const senderSigB64 = payload.sender_signature;
|
|
961
|
-
if (!senderSigB64) {
|
|
962
|
-
throw new E2EEDecryptFailedError('sender_signature missing: 拒绝无发送方签名的消息');
|
|
963
|
-
}
|
|
964
|
-
// 获取发送方公钥
|
|
965
|
-
const fromAid = (message.from ?? payload.aad?.from);
|
|
966
|
-
if (!fromAid)
|
|
967
|
-
throw new E2EEDecryptFailedError('from_aid missing in message');
|
|
968
|
-
const senderCertFingerprint = String(payload.sender_cert_fingerprint ?? payload.aad?.sender_cert_fingerprint ?? '').trim().toLowerCase();
|
|
969
|
-
const senderCertPem = await this._getSenderCert(fromAid, senderCertFingerprint || undefined);
|
|
970
|
-
if (!senderCertPem)
|
|
971
|
-
throw new E2EEDecryptFailedError(`sender cert not found for ${fromAid}`);
|
|
972
|
-
const senderPubKey = await importCertPublicKeyEcdsa(senderCertPem);
|
|
973
|
-
// 重建签名载荷
|
|
974
|
-
const ciphertext = base64ToUint8(payload.ciphertext);
|
|
975
|
-
const tag = base64ToUint8(payload.tag);
|
|
976
|
-
const aad = payload.aad;
|
|
977
|
-
const aadBytes = aad ? aadBytesOffline(aad) : new Uint8Array(0);
|
|
978
|
-
const signPayload = concatBytes(ciphertext, tag, aadBytes);
|
|
979
|
-
const sigBytes = base64ToUint8(senderSigB64);
|
|
980
|
-
const valid = await ecdsaVerifyDer(senderPubKey, sigBytes, signPayload);
|
|
981
|
-
if (!valid) {
|
|
982
|
-
throw new E2EEDecryptFailedError('sender signature verification failed');
|
|
983
|
-
}
|
|
984
|
-
}
|
|
985
|
-
/** 从 keystore 获取发送方证书 PEM */
|
|
986
|
-
async _getSenderCert(aid, certFingerprint) {
|
|
987
|
-
const certPem = await this._keystoreRef.loadCert(aid, certFingerprint);
|
|
988
|
-
const normalized = String(certFingerprint ?? '').trim().toLowerCase();
|
|
989
|
-
if (!certPem)
|
|
990
|
-
return null;
|
|
991
|
-
if (!normalized)
|
|
992
|
-
return certPem;
|
|
993
|
-
const actualFingerprint = await certificateSha256Fingerprint(certPem);
|
|
994
|
-
return actualFingerprint === normalized ? certPem : null;
|
|
995
|
-
}
|
|
996
|
-
/** 解密 prekey_ecdh_v2 模式的消息(四路 ECDH) */
|
|
997
|
-
async _decryptMessagePrekeyV2(message) {
|
|
998
|
-
const payload = message.payload;
|
|
999
|
-
try {
|
|
1000
|
-
const ephPublicBytes = base64ToUint8(payload.ephemeral_public_key);
|
|
1001
|
-
const prekeyId = (payload.prekey_id ?? '');
|
|
1002
|
-
const nonce = base64ToUint8(payload.nonce);
|
|
1003
|
-
const ciphertext = base64ToUint8(payload.ciphertext);
|
|
1004
|
-
const tag = base64ToUint8(payload.tag);
|
|
1005
|
-
// 加载 prekey 私钥
|
|
1006
|
-
const prekeyPrivatePem = await this._loadPrekeyPrivateKey(prekeyId);
|
|
1007
|
-
if (!prekeyPrivatePem)
|
|
1008
|
-
throw new E2EEError(`prekey not found: ${prekeyId}`);
|
|
1009
|
-
const prekeyPrivateEcdh = await importPrivateKeyEcdh(prekeyPrivatePem);
|
|
1010
|
-
// 加载接收方 identity 私钥
|
|
1011
|
-
const myAid = this._currentAid();
|
|
1012
|
-
if (!myAid)
|
|
1013
|
-
throw new E2EEError('AID unavailable');
|
|
1014
|
-
const keyPair = await this._keystoreRef.loadKeyPair(myAid);
|
|
1015
|
-
if (!keyPair || !keyPair.private_key_pem)
|
|
1016
|
-
throw new E2EEError('Identity private key not found');
|
|
1017
|
-
const myIdentityEcdh = await importPrivateKeyEcdh(keyPair.private_key_pem);
|
|
1018
|
-
// 获取发送方公钥(四路 ECDH 需要)
|
|
1019
|
-
const fromAid = (message.from ?? payload.aad?.from);
|
|
1020
|
-
const senderCertFingerprint = String(payload.sender_cert_fingerprint ?? payload.aad?.sender_cert_fingerprint ?? '').trim().toLowerCase();
|
|
1021
|
-
const senderCertPem = await this._getSenderCert(fromAid, senderCertFingerprint || undefined);
|
|
1022
|
-
if (!senderCertPem)
|
|
1023
|
-
throw new E2EEError(`sender public key not found for ${fromAid}`);
|
|
1024
|
-
const senderPubEcdh = await importCertPublicKeyEcdh(senderCertPem);
|
|
1025
|
-
// 导入临时公钥
|
|
1026
|
-
const ephPubKey = await importUncompressedPointEcdh(ephPublicBytes);
|
|
1027
|
-
// 四路 ECDH + HKDF(接收方视角:prekey 和 identity 角色互换)
|
|
1028
|
-
const dh1 = await ecdhDeriveBits(prekeyPrivateEcdh, ephPubKey);
|
|
1029
|
-
const dh2 = await ecdhDeriveBits(myIdentityEcdh, ephPubKey);
|
|
1030
|
-
const dh3 = await ecdhDeriveBits(prekeyPrivateEcdh, senderPubEcdh);
|
|
1031
|
-
const dh4 = await ecdhDeriveBits(myIdentityEcdh, senderPubEcdh);
|
|
1032
|
-
const combined = concatBytes(dh1, dh2, dh3, dh4);
|
|
1033
|
-
const messageKey = await hkdfDerive(combined, `aun-prekey-v2:${prekeyId}`);
|
|
1034
|
-
// 验证 AAD 并解密
|
|
1035
|
-
const aad = payload.aad;
|
|
1036
|
-
let aadBytes;
|
|
1037
|
-
if (aad) {
|
|
1038
|
-
const expectedAad = this._buildInboundAadOffline(message, payload);
|
|
1039
|
-
if (!aadMatchesOffline(expectedAad, aad)) {
|
|
1040
|
-
throw new E2EEDecryptFailedError('aad mismatch');
|
|
1041
|
-
}
|
|
1042
|
-
aadBytes = aadBytesOffline(aad);
|
|
1043
|
-
}
|
|
1044
|
-
else {
|
|
1045
|
-
aadBytes = new Uint8Array(0);
|
|
1046
|
-
}
|
|
1047
|
-
if (!await verifyEnvelopeMetadataAuth(payload, messageKey)) {
|
|
1048
|
-
throw new E2EEDecryptFailedError('envelope metadata auth failed');
|
|
1049
|
-
}
|
|
1050
|
-
const plaintext = await aesGcmDecrypt(messageKey, nonce, ciphertext, tag, aadBytes);
|
|
1051
|
-
const decoded = JSON.parse(_decoder.decode(plaintext));
|
|
1052
|
-
if (!validateDecryptedEnvelopeMetadata(decoded, payload, message)) {
|
|
1053
|
-
throw new E2EEDecryptFailedError('envelope metadata mismatch');
|
|
1054
|
-
}
|
|
1055
|
-
const e2ee = {
|
|
1056
|
-
encryption_mode: MODE_PREKEY_ECDH_V2,
|
|
1057
|
-
suite: payload.suite ?? SUITE,
|
|
1058
|
-
prekey_id: prekeyId,
|
|
1059
|
-
};
|
|
1060
|
-
const protectedHeaders = exposedEnvelopeMetadata(payload.protected_headers);
|
|
1061
|
-
if (protectedHeaders)
|
|
1062
|
-
e2ee.protected_headers = protectedHeaders;
|
|
1063
|
-
const context = exposedEnvelopeMetadata(payload.context);
|
|
1064
|
-
if (context)
|
|
1065
|
-
e2ee.context = context;
|
|
1066
|
-
return {
|
|
1067
|
-
...message,
|
|
1068
|
-
payload: decoded,
|
|
1069
|
-
encrypted: true,
|
|
1070
|
-
e2ee,
|
|
1071
|
-
};
|
|
1072
|
-
}
|
|
1073
|
-
catch (exc) {
|
|
1074
|
-
if (exc instanceof E2EEError) {
|
|
1075
|
-
this._log.warn('prekey_ecdh_v2 decryptfailed (E2EE):', exc);
|
|
1076
|
-
}
|
|
1077
|
-
else {
|
|
1078
|
-
this._log.warn('prekey_ecdh_v2 decryptfailed:', exc);
|
|
1079
|
-
}
|
|
1080
|
-
return null;
|
|
1081
|
-
}
|
|
1082
|
-
}
|
|
1083
|
-
/** 解密 long_term_key 模式的消息(2DH) */
|
|
1084
|
-
async _decryptMessageLongTerm(message) {
|
|
1085
|
-
const payload = message.payload;
|
|
1086
|
-
try {
|
|
1087
|
-
const ephPublicBytes = base64ToUint8(payload.ephemeral_public_key);
|
|
1088
|
-
const nonce = base64ToUint8(payload.nonce);
|
|
1089
|
-
const ciphertext = base64ToUint8(payload.ciphertext);
|
|
1090
|
-
const tag = base64ToUint8(payload.tag);
|
|
1091
|
-
// 加载接收方 identity 私钥
|
|
1092
|
-
const myAid = this._currentAid();
|
|
1093
|
-
if (!myAid)
|
|
1094
|
-
throw new E2EEError('AID unavailable');
|
|
1095
|
-
const keyPair = await this._keystoreRef.loadKeyPair(myAid);
|
|
1096
|
-
if (!keyPair || !keyPair.private_key_pem)
|
|
1097
|
-
throw new E2EEError('Private key not found');
|
|
1098
|
-
const myIdentityEcdh = await importPrivateKeyEcdh(keyPair.private_key_pem);
|
|
1099
|
-
// 获取发送方公钥(2DH 需要)
|
|
1100
|
-
const fromAid = (message.from ?? payload.aad?.from);
|
|
1101
|
-
const senderCertFingerprint = String(payload.sender_cert_fingerprint ?? payload.aad?.sender_cert_fingerprint ?? '').trim().toLowerCase();
|
|
1102
|
-
const senderCertPem = await this._getSenderCert(fromAid, senderCertFingerprint || undefined);
|
|
1103
|
-
if (!senderCertPem)
|
|
1104
|
-
throw new E2EEError(`sender public key not found for ${fromAid}`);
|
|
1105
|
-
const senderPubEcdh = await importCertPublicKeyEcdh(senderCertPem);
|
|
1106
|
-
// 导入临时公钥
|
|
1107
|
-
const ephPubKey = await importUncompressedPointEcdh(ephPublicBytes);
|
|
1108
|
-
// 2DH + HKDF
|
|
1109
|
-
const dh1 = await ecdhDeriveBits(myIdentityEcdh, ephPubKey);
|
|
1110
|
-
const dh2 = await ecdhDeriveBits(myIdentityEcdh, senderPubEcdh);
|
|
1111
|
-
const combined = concatBytes(dh1, dh2);
|
|
1112
|
-
const messageKey = await hkdfDerive(combined, 'aun-longterm-v2');
|
|
1113
|
-
// 验证 AAD 并解密
|
|
1114
|
-
const aad = payload.aad;
|
|
1115
|
-
let aadBytes;
|
|
1116
|
-
if (aad) {
|
|
1117
|
-
const expectedAad = this._buildInboundAadOffline(message, payload);
|
|
1118
|
-
if (!aadMatchesOffline(expectedAad, aad)) {
|
|
1119
|
-
throw new E2EEDecryptFailedError('aad mismatch');
|
|
1120
|
-
}
|
|
1121
|
-
aadBytes = aadBytesOffline(aad);
|
|
1122
|
-
}
|
|
1123
|
-
else {
|
|
1124
|
-
aadBytes = new Uint8Array(0);
|
|
1125
|
-
}
|
|
1126
|
-
if (!await verifyEnvelopeMetadataAuth(payload, messageKey)) {
|
|
1127
|
-
throw new E2EEDecryptFailedError('envelope metadata auth failed');
|
|
1128
|
-
}
|
|
1129
|
-
const plaintext = await aesGcmDecrypt(messageKey, nonce, ciphertext, tag, aadBytes);
|
|
1130
|
-
const decoded = JSON.parse(_decoder.decode(plaintext));
|
|
1131
|
-
if (!validateDecryptedEnvelopeMetadata(decoded, payload, message)) {
|
|
1132
|
-
throw new E2EEDecryptFailedError('envelope metadata mismatch');
|
|
1133
|
-
}
|
|
1134
|
-
const e2ee = {
|
|
1135
|
-
encryption_mode: MODE_LONG_TERM_KEY,
|
|
1136
|
-
suite: payload.suite,
|
|
1137
|
-
};
|
|
1138
|
-
const protectedHeaders = exposedEnvelopeMetadata(payload.protected_headers);
|
|
1139
|
-
if (protectedHeaders)
|
|
1140
|
-
e2ee.protected_headers = protectedHeaders;
|
|
1141
|
-
const context = exposedEnvelopeMetadata(payload.context);
|
|
1142
|
-
if (context)
|
|
1143
|
-
e2ee.context = context;
|
|
1144
|
-
return {
|
|
1145
|
-
...message,
|
|
1146
|
-
payload: decoded,
|
|
1147
|
-
encrypted: true,
|
|
1148
|
-
e2ee,
|
|
1149
|
-
};
|
|
1150
|
-
}
|
|
1151
|
-
catch (exc) {
|
|
1152
|
-
if (exc instanceof E2EEError) {
|
|
1153
|
-
this._log.warn('long_term_key decryptfailed (E2EE):', exc);
|
|
1154
|
-
}
|
|
1155
|
-
else {
|
|
1156
|
-
this._log.warn('long_term_key decryptfailed:', exc);
|
|
1157
|
-
}
|
|
1158
|
-
return null;
|
|
1159
|
-
}
|
|
1160
|
-
}
|
|
1161
|
-
// ── AAD 工具 ─────────────────────────────────────
|
|
1162
|
-
/** 构建解密时的期望 AAD(接收方视角) */
|
|
1163
|
-
_buildInboundAadOffline(message, payload) {
|
|
1164
|
-
const aad = payload.aad;
|
|
1165
|
-
return {
|
|
1166
|
-
from: message.from,
|
|
1167
|
-
to: message.to,
|
|
1168
|
-
message_id: message.message_id,
|
|
1169
|
-
timestamp: message.timestamp,
|
|
1170
|
-
encryption_mode: payload.encryption_mode,
|
|
1171
|
-
suite: payload.suite ?? SUITE,
|
|
1172
|
-
ephemeral_public_key: payload.ephemeral_public_key,
|
|
1173
|
-
recipient_cert_fingerprint: aad?.recipient_cert_fingerprint,
|
|
1174
|
-
sender_cert_fingerprint: payload.sender_cert_fingerprint ?? aad?.sender_cert_fingerprint,
|
|
1175
|
-
prekey_id: payload.prekey_id ?? aad?.prekey_id,
|
|
1176
|
-
};
|
|
1177
|
-
}
|
|
1178
|
-
// ── Prekey 生成 ──────────────────────────────────
|
|
1179
|
-
/**
|
|
1180
|
-
* 生成 prekey 材料并保存私钥到本地 keystore。
|
|
1181
|
-
*
|
|
1182
|
-
* 返回 { prekey_id, public_key, signature, created_at },可直接用于 RPC 上传。
|
|
1183
|
-
*/
|
|
1184
|
-
async generatePrekey() {
|
|
1185
|
-
const tStart = Date.now();
|
|
1186
|
-
this._log.debug('generatePrekey enter');
|
|
1187
|
-
try {
|
|
1188
|
-
const aid = this._currentAid();
|
|
1189
|
-
if (!aid)
|
|
1190
|
-
throw new E2EEError('AID unavailable for prekey generation');
|
|
1191
|
-
const deviceId = this._currentDeviceId();
|
|
1192
|
-
// 生成新 ECDH 密钥对(标记为 ECDH 用途)
|
|
1193
|
-
const keyPair = await crypto.subtle.generateKey({ name: 'ECDH', namedCurve: 'P-256' }, true, ['deriveBits']);
|
|
1194
|
-
// 导出 SPKI 公钥(DER 格式)
|
|
1195
|
-
const spki = await crypto.subtle.exportKey('spki', keyPair.publicKey);
|
|
1196
|
-
const publicKeyB64 = uint8ToBase64(new Uint8Array(spki));
|
|
1197
|
-
// 导出 PKCS8 私钥(PEM 格式存储)
|
|
1198
|
-
const pkcs8 = await crypto.subtle.exportKey('pkcs8', keyPair.privateKey);
|
|
1199
|
-
const privateKeyPem = arrayBufferToPemLocal(pkcs8, 'PRIVATE KEY');
|
|
1200
|
-
const prekeyId = uuidV4();
|
|
1201
|
-
const nowMs = Date.now();
|
|
1202
|
-
// 签名:prekey_id|public_key|created_at(绑定时间戳,防止旧 prekey 重放)
|
|
1203
|
-
const signData = _encoder.encode(`${prekeyId}|${publicKeyB64}|${nowMs}`);
|
|
1204
|
-
const senderSignKey = await this._loadSenderIdentityPrivateEcdsa();
|
|
1205
|
-
const sig = await ecdsaSignDer(senderSignKey, signData);
|
|
1206
|
-
const signatureB64 = uint8ToBase64(sig);
|
|
1207
|
-
await saveKeyStorePrekey(this._keystoreRef, aid, deviceId, prekeyId, {
|
|
1208
|
-
private_key_pem: privateKeyPem,
|
|
1209
|
-
created_at: nowMs,
|
|
1210
|
-
updated_at: nowMs,
|
|
1211
|
-
});
|
|
1212
|
-
// 内存缓存私钥 PEM
|
|
1213
|
-
this._localPrekeyCache.set(prekeyId, privateKeyPem);
|
|
1214
|
-
// 清理过期的旧 prekey
|
|
1215
|
-
await this._cleanupExpiredPrekeys(aid, deviceId);
|
|
1216
|
-
const result = {
|
|
1217
|
-
prekey_id: prekeyId,
|
|
1218
|
-
public_key: publicKeyB64,
|
|
1219
|
-
signature: signatureB64,
|
|
1220
|
-
created_at: nowMs,
|
|
1221
|
-
};
|
|
1222
|
-
const certFingerprint = await this._localCertSha256Fingerprint();
|
|
1223
|
-
if (certFingerprint) {
|
|
1224
|
-
result.cert_fingerprint = certFingerprint;
|
|
1225
|
-
}
|
|
1226
|
-
if (deviceId) {
|
|
1227
|
-
result.device_id = deviceId;
|
|
1228
|
-
}
|
|
1229
|
-
this._log.debug(`generatePrekey exit: elapsed=${Date.now() - tStart}ms aid=${aid} prekey_id=${prekeyId}`);
|
|
1230
|
-
return result;
|
|
1231
|
-
}
|
|
1232
|
-
catch (err) {
|
|
1233
|
-
this._log.debug(`generatePrekey exit (error): elapsed=${Date.now() - tStart}ms err=${err instanceof Error ? err.message : String(err)}`);
|
|
1234
|
-
throw err;
|
|
1235
|
-
}
|
|
1236
|
-
}
|
|
1237
|
-
/** 清理过期的本地 prekey 私钥 */
|
|
1238
|
-
async _cleanupExpiredPrekeys(aid, deviceId) {
|
|
1239
|
-
const nowMs = Date.now();
|
|
1240
|
-
const cutoffMs = nowMs - PREKEY_RETENTION_SECONDS * 1000;
|
|
1241
|
-
const expired = await cleanupKeyStorePrekeys(this._keystoreRef, aid, deviceId, cutoffMs, PREKEY_MIN_KEEP_COUNT);
|
|
1242
|
-
if (expired.length > 0) {
|
|
1243
|
-
for (const pid of expired) {
|
|
1244
|
-
this._localPrekeyCache.delete(pid);
|
|
1245
|
-
}
|
|
1246
|
-
}
|
|
1247
|
-
}
|
|
1248
|
-
/** 从内存缓存或 keystore 加载 prekey 私钥 PEM */
|
|
1249
|
-
async _loadPrekeyPrivateKey(prekeyId) {
|
|
1250
|
-
// 优先从内存缓存获取
|
|
1251
|
-
const cached = this._localPrekeyCache.get(prekeyId);
|
|
1252
|
-
if (cached)
|
|
1253
|
-
return cached;
|
|
1254
|
-
const aid = this._currentAid();
|
|
1255
|
-
if (!aid)
|
|
1256
|
-
return null;
|
|
1257
|
-
// 优先按 prekey_id 单点查询(IndexedDB 主键直查 O(log N))。
|
|
1258
|
-
// 旧路径需要全量加载 prekey 池(prekey 数量大时是性能瓶颈),单查能直接命中信封里的目标 prekey。
|
|
1259
|
-
const byIdLoader = this._keystoreRef.loadE2EEPrekeyById;
|
|
1260
|
-
if (typeof byIdLoader === 'function') {
|
|
1261
|
-
try {
|
|
1262
|
-
const byIdData = await byIdLoader.call(this._keystoreRef, aid, prekeyId);
|
|
1263
|
-
if (byIdData) {
|
|
1264
|
-
const pem = byIdData.private_key_pem;
|
|
1265
|
-
if (typeof pem === 'string' && pem) {
|
|
1266
|
-
this._log.debug(`prekey ${prekeyId} by_id lookup hit`);
|
|
1267
|
-
this._localPrekeyCache.set(prekeyId, pem);
|
|
1268
|
-
return pem;
|
|
1269
|
-
}
|
|
1270
|
-
}
|
|
1271
|
-
}
|
|
1272
|
-
catch (err) {
|
|
1273
|
-
this._log.warn(`prekey ${prekeyId} by_id loader failed, falling back to full load: ${err instanceof Error ? err.message : String(err)}`);
|
|
1274
|
-
}
|
|
1275
|
-
}
|
|
1276
|
-
// 回退:旧版 keystore 没有 by_id 方法,或单查未命中 → 全量扫描(保持向后兼容)。
|
|
1277
|
-
const prekeys = await loadKeyStorePrekeys(this._keystoreRef, aid, this._currentDeviceId());
|
|
1278
|
-
const prekeyData = prekeys[prekeyId];
|
|
1279
|
-
if (!prekeyData)
|
|
1280
|
-
return null;
|
|
1281
|
-
const pem = prekeyData.private_key_pem;
|
|
1282
|
-
if (!pem)
|
|
1283
|
-
return null;
|
|
1284
|
-
// 回填内存缓存
|
|
1285
|
-
this._localPrekeyCache.set(prekeyId, pem);
|
|
1286
|
-
return pem;
|
|
1287
|
-
}
|
|
1288
|
-
// ── 内部工具 ──────────────────────────────────────
|
|
1289
|
-
_currentAid() {
|
|
1290
|
-
const identity = this._identityFn();
|
|
1291
|
-
const aid = identity.aid;
|
|
1292
|
-
return typeof aid === 'string' ? aid : null;
|
|
1293
|
-
}
|
|
1294
|
-
_currentDeviceId() {
|
|
1295
|
-
try {
|
|
1296
|
-
return String(this._deviceIdFn() ?? '').trim();
|
|
1297
|
-
}
|
|
1298
|
-
catch {
|
|
1299
|
-
return '';
|
|
1300
|
-
}
|
|
1301
|
-
}
|
|
1302
|
-
/** 加载发送方 identity 私钥(ECDH 用途) */
|
|
1303
|
-
async _loadSenderIdentityPrivateEcdh() {
|
|
1304
|
-
const identity = this._identityFn();
|
|
1305
|
-
const pem = identity.private_key_pem;
|
|
1306
|
-
if (!pem)
|
|
1307
|
-
throw new E2EEError('sender identity private key unavailable');
|
|
1308
|
-
return importPrivateKeyEcdh(pem);
|
|
1309
|
-
}
|
|
1310
|
-
/** 加载发送方 identity 私钥(ECDSA 签名用途) */
|
|
1311
|
-
async _loadSenderIdentityPrivateEcdsa() {
|
|
1312
|
-
const identity = this._identityFn();
|
|
1313
|
-
const pem = identity.private_key_pem;
|
|
1314
|
-
if (!pem)
|
|
1315
|
-
throw new E2EEError('sender identity private key unavailable');
|
|
1316
|
-
return importPrivateKeyEcdsa(pem);
|
|
1317
|
-
}
|
|
1318
|
-
/** 获取本地 identity 指纹(优先证书 DER SHA-256,缺失时回退到公钥指纹) */
|
|
1319
|
-
async _localIdentityFingerprint() {
|
|
1320
|
-
const identity = this._identityFn();
|
|
1321
|
-
// 优先用证书指纹(与 PKI 一致)
|
|
1322
|
-
const certPem = identity.cert;
|
|
1323
|
-
if (certPem)
|
|
1324
|
-
return fingerprintCertPem(certPem);
|
|
1325
|
-
// 无证书时回退到公钥 SPKI 指纹
|
|
1326
|
-
const pubDerB64 = identity.public_key_der_b64;
|
|
1327
|
-
if (pubDerB64)
|
|
1328
|
-
return fingerprintDerB64(pubDerB64);
|
|
1329
|
-
// 从私钥导出公钥的 SPKI 指纹
|
|
1330
|
-
const pem = identity.private_key_pem;
|
|
1331
|
-
if (pem) {
|
|
1332
|
-
const pk = await crypto.subtle.importKey('pkcs8', pemToArrayBuffer(pem), { name: 'ECDSA', namedCurve: 'P-256' }, true, ['sign']);
|
|
1333
|
-
const jwk = await crypto.subtle.exportKey('jwk', pk);
|
|
1334
|
-
const pubJwk = { kty: jwk.kty, crv: jwk.crv, x: jwk.x, y: jwk.y };
|
|
1335
|
-
const pubKey = await crypto.subtle.importKey('jwk', pubJwk, { name: 'ECDSA', namedCurve: 'P-256' }, true, ['verify']);
|
|
1336
|
-
const spki = await crypto.subtle.exportKey('spki', pubKey);
|
|
1337
|
-
return fingerprintSpki(spki);
|
|
1338
|
-
}
|
|
1339
|
-
throw new E2EEError('identity fingerprint unavailable');
|
|
1340
|
-
}
|
|
1341
|
-
/** 本地证书的 SHA-256 指纹(用于锁定证书版本) */
|
|
1342
|
-
async _localCertSha256Fingerprint() {
|
|
1343
|
-
const identity = this._identityFn();
|
|
1344
|
-
const certPem = identity.cert;
|
|
1345
|
-
if (!certPem)
|
|
1346
|
-
return '';
|
|
1347
|
-
return certificateSha256Fingerprint(certPem);
|
|
1348
|
-
}
|
|
1349
|
-
/** 裁剪 seen set */
|
|
1350
|
-
_trimSeenSet() {
|
|
1351
|
-
if (this._seenMessages.size > this._seenMaxSize) {
|
|
1352
|
-
const trimCount = this._seenMessages.size - Math.floor(this._seenMaxSize * 0.8);
|
|
1353
|
-
const keys = [...this._seenMessages.keys()].slice(0, trimCount);
|
|
1354
|
-
for (const k of keys) {
|
|
1355
|
-
this._seenMessages.delete(k);
|
|
1356
|
-
}
|
|
1357
|
-
}
|
|
1358
|
-
}
|
|
1359
|
-
/** 清理过期的 prekey 缓存和 seen set 条目(供外部定时调用) */
|
|
1360
|
-
cleanExpiredCaches() {
|
|
1361
|
-
const now = Date.now() / 1000;
|
|
1362
|
-
// 清理过期的 prekey 缓存
|
|
1363
|
-
for (const [k, v] of this._prekeyCache) {
|
|
1364
|
-
if (now >= v.expireAt)
|
|
1365
|
-
this._prekeyCache.delete(k);
|
|
1366
|
-
}
|
|
1367
|
-
// 清理 seen set(LRU 裁剪)
|
|
1368
|
-
this._trimSeenSet();
|
|
1369
|
-
}
|
|
1370
|
-
}
|
|
1371
|
-
// ── 内部工具函数(PEM 生成) ────────────────────────────────
|
|
1372
|
-
/** 将 ArrayBuffer 转为 PEM 格式(本地版本,避免循环依赖) */
|
|
1373
|
-
function arrayBufferToPemLocal(buffer, label) {
|
|
1374
|
-
const b64 = uint8ToBase64(new Uint8Array(buffer));
|
|
1375
|
-
const lines = [];
|
|
1376
|
-
for (let i = 0; i < b64.length; i += 64) {
|
|
1377
|
-
lines.push(b64.slice(i, i + 64));
|
|
1378
|
-
}
|
|
1379
|
-
return `-----BEGIN ${label}-----\n${lines.join('\n')}\n-----END ${label}-----`;
|
|
1380
|
-
}
|
|
1381
|
-
// ── 导出额外工具(供 e2ee-group.ts 使用)──────────────────
|
|
1382
|
-
export { aadBytesOffline as _aadBytesOffline, concatBytes as _concatBytes, ecdsaSignDer as _ecdsaSignDer, ecdsaVerifyDer as _ecdsaVerifyDer, hkdfDerive as _hkdfDerive, aesGcmEncrypt as _aesGcmEncrypt, aesGcmDecrypt as _aesGcmDecrypt, randomNonce as _randomNonce, uuidV4 as _uuidV4, fingerprintCertPem as _fingerprintCertPem, certificateSha256Fingerprint as _certificateSha256Fingerprint, fingerprintSpki as _fingerprintSpki, importCertPublicKeyEcdsa as _importCertPublicKeyEcdsa, importPrivateKeyEcdsa as _importPrivateKeyEcdsa, derToP1363 as _derToP1363, };
|
|
7
|
+
export { ProtectedHeaders } from './protected-headers.js';
|
|
1383
8
|
//# sourceMappingURL=e2ee.js.map
|