@agentunion/fastaun-browser 0.2.14 → 0.2.15
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/dist/client.d.ts +4 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +138 -31
- package/dist/client.js.map +1 -1
- package/dist/e2ee-group.d.ts +13 -0
- package/dist/e2ee-group.d.ts.map +1 -1
- package/dist/e2ee-group.js +200 -17
- package/dist/e2ee-group.js.map +1 -1
- package/dist/e2ee.d.ts +23 -0
- package/dist/e2ee.d.ts.map +1 -1
- package/dist/e2ee.js +270 -33
- package/dist/e2ee.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/keystore/index.d.ts +10 -0
- package/dist/keystore/index.d.ts.map +1 -1
- package/dist/keystore/indexeddb.d.ts +19 -0
- package/dist/keystore/indexeddb.d.ts.map +1 -1
- package/dist/keystore/indexeddb.js +83 -0
- package/dist/keystore/indexeddb.js.map +1 -1
- package/dist/namespaces/auth.d.ts +19 -0
- package/dist/namespaces/auth.d.ts.map +1 -1
- package/dist/namespaces/auth.js +237 -0
- package/dist/namespaces/auth.js.map +1 -1
- package/dist/namespaces/meta.d.ts +106 -0
- package/dist/namespaces/meta.d.ts.map +1 -0
- package/dist/namespaces/meta.js +498 -0
- package/dist/namespaces/meta.js.map +1 -0
- package/package.json +1 -1
package/dist/e2ee.d.ts
CHANGED
|
@@ -9,6 +9,8 @@ export declare const MODE_LONG_TERM_KEY = "long_term_key";
|
|
|
9
9
|
export declare const AAD_FIELDS_OFFLINE: readonly ["from", "to", "message_id", "timestamp", "encryption_mode", "suite", "ephemeral_public_key", "recipient_cert_fingerprint", "sender_cert_fingerprint", "prekey_id"];
|
|
10
10
|
/** AAD 匹配字段(解密时校验,不含 timestamp) */
|
|
11
11
|
export declare const AAD_MATCH_FIELDS_OFFLINE: readonly ["from", "to", "message_id", "encryption_mode", "suite", "ephemeral_public_key", "recipient_cert_fingerprint", "sender_cert_fingerprint", "prekey_id"];
|
|
12
|
+
/** 兼容型可选 AAD 字段:存在时才参与 AAD,不为旧消息补 null。 */
|
|
13
|
+
export declare const AAD_OPTIONAL_FIELDS: readonly ["payload_type", "protected_headers", "context_type", "context_id"];
|
|
12
14
|
/** prekey 私钥本地保留时间(秒) */
|
|
13
15
|
export declare const PREKEY_RETENTION_SECONDS: number;
|
|
14
16
|
export declare const PREKEY_MIN_KEEP_COUNT = 7;
|
|
@@ -20,6 +22,19 @@ export interface PrekeyMaterial extends JsonObject {
|
|
|
20
22
|
device_id?: string;
|
|
21
23
|
cert_fingerprint?: string;
|
|
22
24
|
}
|
|
25
|
+
export type ProtectedHeadersInput = ProtectedHeaders | Record<string, unknown> | null | undefined;
|
|
26
|
+
/** 端到端保护的信封元数据,语义接近 HTTP headers。 */
|
|
27
|
+
export declare class ProtectedHeaders {
|
|
28
|
+
private _items;
|
|
29
|
+
constructor(values?: Record<string, unknown> | null);
|
|
30
|
+
private static normalizeKey;
|
|
31
|
+
set(key: string, value: unknown): this;
|
|
32
|
+
get(key: string, defaultValue?: string | null): string | null;
|
|
33
|
+
remove(key: string): this;
|
|
34
|
+
toObject(): Record<string, string>;
|
|
35
|
+
toJSON(): Record<string, string>;
|
|
36
|
+
static from(values?: Record<string, unknown> | null): ProtectedHeaders;
|
|
37
|
+
}
|
|
23
38
|
/** 加密结果信息 */
|
|
24
39
|
export interface EncryptResult {
|
|
25
40
|
encrypted: boolean;
|
|
@@ -102,6 +117,10 @@ export declare class E2EEManager {
|
|
|
102
117
|
prekey?: PrekeyMaterial | null;
|
|
103
118
|
messageId?: string;
|
|
104
119
|
timestamp?: number;
|
|
120
|
+
protectedHeaders?: ProtectedHeadersInput;
|
|
121
|
+
protected_headers?: ProtectedHeadersInput;
|
|
122
|
+
headers?: ProtectedHeadersInput;
|
|
123
|
+
context?: JsonObject | null;
|
|
105
124
|
}): Promise<[JsonObject, EncryptResult]>;
|
|
106
125
|
/**
|
|
107
126
|
* 加密出站消息:有 prekey → prekey_ecdh_v2(四路 ECDH),无 prekey → long_term_key。
|
|
@@ -114,6 +133,10 @@ export declare class E2EEManager {
|
|
|
114
133
|
prekey?: PrekeyMaterial | null;
|
|
115
134
|
messageId: string;
|
|
116
135
|
timestamp: number;
|
|
136
|
+
protectedHeaders?: ProtectedHeadersInput;
|
|
137
|
+
protected_headers?: ProtectedHeadersInput;
|
|
138
|
+
headers?: ProtectedHeadersInput;
|
|
139
|
+
context?: JsonObject | null;
|
|
117
140
|
}): Promise<[JsonObject, EncryptResult]>;
|
|
118
141
|
/**
|
|
119
142
|
* 使用对方 prekey 加密(prekey_ecdh_v2 模式,四路 ECDH + 发送方签名)
|
package/dist/e2ee.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"e2ee.d.ts","sourceRoot":"","sources":["../src/e2ee.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,OAAO,EAAgB,MAAM,YAAY,CAAC;AAEpF,aAAa;AACb,eAAO,MAAM,KAAK,iCAAiC,CAAC;AAEpD,WAAW;AACX,eAAO,MAAM,mBAAmB,mBAAmB,CAAC;AACpD,eAAO,MAAM,kBAAkB,kBAAkB,CAAC;AAElD,oBAAoB;AACpB,eAAO,MAAM,kBAAkB,8KAKrB,CAAC;AAEX,mCAAmC;AACnC,eAAO,MAAM,wBAAwB,iKAK3B,CAAC;AAEX,yBAAyB;AACzB,eAAO,MAAM,wBAAwB,QAAgB,CAAC;AACtD,eAAO,MAAM,qBAAqB,IAAI,CAAC;AAEvC,MAAM,WAAW,cAAe,SAAQ,UAAU;IAChD,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;
|
|
1
|
+
{"version":3,"file":"e2ee.d.ts","sourceRoot":"","sources":["../src/e2ee.ts"],"names":[],"mappings":"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"}
|
package/dist/e2ee.js
CHANGED
|
@@ -21,9 +21,62 @@ export const AAD_MATCH_FIELDS_OFFLINE = [
|
|
|
21
21
|
'recipient_cert_fingerprint', 'sender_cert_fingerprint',
|
|
22
22
|
'prekey_id',
|
|
23
23
|
];
|
|
24
|
+
/** 兼容型可选 AAD 字段:存在时才参与 AAD,不为旧消息补 null。 */
|
|
25
|
+
export const AAD_OPTIONAL_FIELDS = [
|
|
26
|
+
'payload_type', 'protected_headers', 'context_type', 'context_id',
|
|
27
|
+
];
|
|
28
|
+
const METADATA_AUTH_FIELD = '_auth';
|
|
29
|
+
const METADATA_AUTH_ALG = 'HMAC-SHA256';
|
|
30
|
+
const METADATA_KEY_DOMAIN = new TextEncoder().encode('aun-envelope-metadata-key-v1');
|
|
31
|
+
const PROTECTED_HEADERS_DOMAIN = new TextEncoder().encode('aun-protected-headers-v1');
|
|
32
|
+
const PROTECTED_CONTEXT_DOMAIN = new TextEncoder().encode('aun-protected-context-v1');
|
|
24
33
|
/** prekey 私钥本地保留时间(秒) */
|
|
25
34
|
export const PREKEY_RETENTION_SECONDS = 7 * 24 * 3600;
|
|
26
35
|
export const PREKEY_MIN_KEEP_COUNT = 7;
|
|
36
|
+
/** 端到端保护的信封元数据,语义接近 HTTP headers。 */
|
|
37
|
+
export class ProtectedHeaders {
|
|
38
|
+
_items = {};
|
|
39
|
+
constructor(values) {
|
|
40
|
+
if (values) {
|
|
41
|
+
for (const [key, value] of Object.entries(values)) {
|
|
42
|
+
this.set(key, value);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
static normalizeKey(key) {
|
|
47
|
+
const value = String(key ?? '').trim().toLowerCase();
|
|
48
|
+
if (!value || !/^[a-z0-9_-]+$/.test(value)) {
|
|
49
|
+
throw new E2EEError('protected header key must match [a-z0-9_-]+');
|
|
50
|
+
}
|
|
51
|
+
if (value === METADATA_AUTH_FIELD) {
|
|
52
|
+
throw new E2EEError('protected header key is reserved');
|
|
53
|
+
}
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
set(key, value) {
|
|
57
|
+
this._items[ProtectedHeaders.normalizeKey(key)] = value == null ? '' : String(value);
|
|
58
|
+
return this;
|
|
59
|
+
}
|
|
60
|
+
get(key, defaultValue = null) {
|
|
61
|
+
const normalized = ProtectedHeaders.normalizeKey(key);
|
|
62
|
+
return Object.prototype.hasOwnProperty.call(this._items, normalized)
|
|
63
|
+
? this._items[normalized]
|
|
64
|
+
: defaultValue;
|
|
65
|
+
}
|
|
66
|
+
remove(key) {
|
|
67
|
+
delete this._items[ProtectedHeaders.normalizeKey(key)];
|
|
68
|
+
return this;
|
|
69
|
+
}
|
|
70
|
+
toObject() {
|
|
71
|
+
return { ...this._items };
|
|
72
|
+
}
|
|
73
|
+
toJSON() {
|
|
74
|
+
return this.toObject();
|
|
75
|
+
}
|
|
76
|
+
static from(values) {
|
|
77
|
+
return new ProtectedHeaders(values ?? {});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
27
80
|
function prekeyCreatedMarker(prekeyData) {
|
|
28
81
|
return Number(prekeyData.created_at ?? prekeyData.updated_at ?? prekeyData.expires_at ?? 0);
|
|
29
82
|
}
|
|
@@ -109,18 +162,164 @@ function derToP1363(der, coordLen = 32) {
|
|
|
109
162
|
result.set(sBytes, coordLen * 2 - sBytes.length);
|
|
110
163
|
return result;
|
|
111
164
|
}
|
|
112
|
-
|
|
113
|
-
|
|
165
|
+
function canonicalStringify(value) {
|
|
166
|
+
if (value === null || value === undefined)
|
|
167
|
+
return 'null';
|
|
168
|
+
if (Array.isArray(value)) {
|
|
169
|
+
return `[${value.map(item => canonicalStringify(item)).join(',')}]`;
|
|
170
|
+
}
|
|
171
|
+
if (typeof value === 'object') {
|
|
172
|
+
const record = value;
|
|
173
|
+
const pairs = Object.keys(record)
|
|
174
|
+
.sort()
|
|
175
|
+
.map(key => `${JSON.stringify(key)}:${canonicalStringify(record[key])}`);
|
|
176
|
+
return `{${pairs.join(',')}}`;
|
|
177
|
+
}
|
|
178
|
+
return JSON.stringify(value) ?? 'null';
|
|
179
|
+
}
|
|
180
|
+
function hasOwn(obj, key) {
|
|
181
|
+
return Object.prototype.hasOwnProperty.call(obj, key);
|
|
182
|
+
}
|
|
183
|
+
function normalizeProtectedHeaders(headers) {
|
|
184
|
+
if (headers == null)
|
|
185
|
+
return {};
|
|
186
|
+
if (headers instanceof ProtectedHeaders) {
|
|
187
|
+
return headers.toObject();
|
|
188
|
+
}
|
|
189
|
+
const toObject = headers.toObject;
|
|
190
|
+
if (typeof toObject === 'function') {
|
|
191
|
+
return new ProtectedHeaders(toObject.call(headers)).toObject();
|
|
192
|
+
}
|
|
193
|
+
if (typeof headers !== 'object' || Array.isArray(headers)) {
|
|
194
|
+
throw new E2EEError('protected_headers must be an object');
|
|
195
|
+
}
|
|
196
|
+
return new ProtectedHeaders(headers).toObject();
|
|
197
|
+
}
|
|
198
|
+
function metadataBody(metadata) {
|
|
199
|
+
const body = {};
|
|
200
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
201
|
+
if (key !== METADATA_AUTH_FIELD) {
|
|
202
|
+
body[key] = value;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return body;
|
|
206
|
+
}
|
|
207
|
+
async function hmacSha256(key, data) {
|
|
208
|
+
const hmacKey = await crypto.subtle.importKey('raw', toBufferSource(key), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
|
|
209
|
+
const sig = await crypto.subtle.sign('HMAC', hmacKey, toBufferSource(data));
|
|
210
|
+
return new Uint8Array(sig);
|
|
211
|
+
}
|
|
212
|
+
async function metadataAuthTag(key, domain, body) {
|
|
213
|
+
const metadataKey = await hmacSha256(key, METADATA_KEY_DOMAIN);
|
|
214
|
+
return hmacSha256(metadataKey, concatBytes(domain, new Uint8Array([0]), _encoder.encode(canonicalStringify(body))));
|
|
215
|
+
}
|
|
216
|
+
async function withMetadataAuth(metadata, key, domain) {
|
|
217
|
+
const body = metadataBody(metadata);
|
|
218
|
+
if (Object.keys(body).length === 0)
|
|
219
|
+
return {};
|
|
220
|
+
const tag = await metadataAuthTag(key, domain, body);
|
|
221
|
+
return {
|
|
222
|
+
...body,
|
|
223
|
+
[METADATA_AUTH_FIELD]: {
|
|
224
|
+
alg: METADATA_AUTH_ALG,
|
|
225
|
+
tag: uint8ToBase64(tag),
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
function timingSafeEqual(a, b) {
|
|
230
|
+
if (a.byteLength !== b.byteLength)
|
|
231
|
+
return false;
|
|
232
|
+
let diff = 0;
|
|
233
|
+
for (let i = 0; i < a.byteLength; i++) {
|
|
234
|
+
diff |= a[i] ^ b[i];
|
|
235
|
+
}
|
|
236
|
+
return diff === 0;
|
|
237
|
+
}
|
|
238
|
+
async function verifyMetadataAuth(metadata, key, domain) {
|
|
239
|
+
if (metadata == null)
|
|
240
|
+
return true;
|
|
241
|
+
if (typeof metadata !== 'object' || Array.isArray(metadata))
|
|
242
|
+
return false;
|
|
243
|
+
const record = metadata;
|
|
244
|
+
const auth = record[METADATA_AUTH_FIELD];
|
|
245
|
+
if (!auth || typeof auth !== 'object' || Array.isArray(auth))
|
|
246
|
+
return false;
|
|
247
|
+
const authObj = auth;
|
|
248
|
+
if (authObj.alg !== METADATA_AUTH_ALG)
|
|
249
|
+
return false;
|
|
250
|
+
if (typeof authObj.tag !== 'string' || !authObj.tag)
|
|
251
|
+
return false;
|
|
252
|
+
const body = metadataBody(record);
|
|
253
|
+
if (Object.keys(body).length === 0)
|
|
254
|
+
return false;
|
|
255
|
+
let actual;
|
|
256
|
+
try {
|
|
257
|
+
actual = base64ToUint8(authObj.tag);
|
|
258
|
+
}
|
|
259
|
+
catch {
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
const expected = await metadataAuthTag(key, domain, body);
|
|
263
|
+
return timingSafeEqual(actual, expected);
|
|
264
|
+
}
|
|
265
|
+
async function verifyEnvelopeMetadataAuth(payload, messageKey) {
|
|
266
|
+
return await verifyMetadataAuth(payload.protected_headers, messageKey, PROTECTED_HEADERS_DOMAIN)
|
|
267
|
+
&& await verifyMetadataAuth(payload.context, messageKey, PROTECTED_CONTEXT_DOMAIN);
|
|
268
|
+
}
|
|
269
|
+
function normalizeContextMetadata(context) {
|
|
270
|
+
if (!context || typeof context !== 'object' || Array.isArray(context))
|
|
271
|
+
return {};
|
|
272
|
+
return metadataBody(context);
|
|
273
|
+
}
|
|
274
|
+
function exposedEnvelopeMetadata(metadata) {
|
|
275
|
+
if (!metadata || typeof metadata !== 'object' || Array.isArray(metadata))
|
|
276
|
+
return undefined;
|
|
277
|
+
const body = metadataBody(metadata);
|
|
278
|
+
return Object.keys(body).length > 0 ? body : undefined;
|
|
279
|
+
}
|
|
280
|
+
async function copyOptionalEnvelopeMetadata(envelope, messageKey, opts) {
|
|
281
|
+
const payloadType = String(opts?.payloadType ?? '').trim();
|
|
282
|
+
const protectedHeaders = normalizeProtectedHeaders(opts?.protectedHeaders);
|
|
283
|
+
if (payloadType) {
|
|
284
|
+
protectedHeaders.payload_type = payloadType;
|
|
285
|
+
}
|
|
286
|
+
if (Object.keys(protectedHeaders).length > 0) {
|
|
287
|
+
envelope.protected_headers = await withMetadataAuth(protectedHeaders, messageKey, PROTECTED_HEADERS_DOMAIN);
|
|
288
|
+
}
|
|
289
|
+
const contextMetadata = normalizeContextMetadata(opts?.context);
|
|
290
|
+
if (Object.keys(contextMetadata).length > 0) {
|
|
291
|
+
envelope.context = await withMetadataAuth(contextMetadata, messageKey, PROTECTED_CONTEXT_DOMAIN);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
function aadBytesWithOptionalFields(aad, baseFields) {
|
|
114
295
|
const obj = {};
|
|
115
|
-
for (const field of
|
|
296
|
+
for (const field of baseFields) {
|
|
116
297
|
obj[field] = aad[field] ?? null;
|
|
117
298
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
299
|
+
return _encoder.encode(canonicalStringify(obj));
|
|
300
|
+
}
|
|
301
|
+
function validateDecryptedEnvelopeMetadata(decoded, payload, message) {
|
|
302
|
+
if (payload.protected_headers && typeof payload.protected_headers === 'object' && !Array.isArray(payload.protected_headers)) {
|
|
303
|
+
const headers = metadataBody(payload.protected_headers);
|
|
304
|
+
if (hasOwn(headers, 'payload_type')) {
|
|
305
|
+
if (!decoded || typeof decoded !== 'object' || Array.isArray(decoded))
|
|
306
|
+
return false;
|
|
307
|
+
if (String(decoded.type ?? '') !== String(headers.payload_type ?? '')) {
|
|
308
|
+
return false;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
if (payload.context && typeof payload.context === 'object' && !Array.isArray(payload.context)) {
|
|
313
|
+
const protectedContext = metadataBody(payload.context);
|
|
314
|
+
const outerContext = normalizeContextMetadata(message?.context);
|
|
315
|
+
if (canonicalStringify(outerContext) !== canonicalStringify(protectedContext))
|
|
316
|
+
return false;
|
|
122
317
|
}
|
|
123
|
-
return
|
|
318
|
+
return true;
|
|
319
|
+
}
|
|
320
|
+
/** AAD 序列化(排序键、紧凑 JSON) */
|
|
321
|
+
function aadBytesOffline(aad) {
|
|
322
|
+
return aadBytesWithOptionalFields(aad, AAD_FIELDS_OFFLINE);
|
|
124
323
|
}
|
|
125
324
|
/** AAD 匹配检查(解密时校验) */
|
|
126
325
|
function aadMatchesOffline(expected, actual) {
|
|
@@ -415,6 +614,8 @@ export class E2EEManager {
|
|
|
415
614
|
prekey: opts.prekey ?? null,
|
|
416
615
|
messageId,
|
|
417
616
|
timestamp,
|
|
617
|
+
protectedHeaders: opts.protectedHeaders ?? opts.protected_headers ?? opts.headers,
|
|
618
|
+
context: opts.context ?? null,
|
|
418
619
|
});
|
|
419
620
|
}
|
|
420
621
|
// ── 加密 ──────────────────────────────────────────
|
|
@@ -435,7 +636,7 @@ export class E2EEManager {
|
|
|
435
636
|
}
|
|
436
637
|
if (prekey) {
|
|
437
638
|
try {
|
|
438
|
-
const envelope = await this._encryptWithPrekey(peerAid, payload, prekey, opts.peerCertPem, opts.messageId, opts.timestamp);
|
|
639
|
+
const envelope = await this._encryptWithPrekey(peerAid, payload, prekey, opts.peerCertPem, opts.messageId, opts.timestamp, opts.protectedHeaders ?? opts.protected_headers ?? opts.headers, opts.context ?? null);
|
|
439
640
|
return [envelope, {
|
|
440
641
|
encrypted: true,
|
|
441
642
|
forward_secrecy: true,
|
|
@@ -447,7 +648,7 @@ export class E2EEManager {
|
|
|
447
648
|
console.warn('prekey 加密失败,降级到 long_term_key(无前向保密):', exc);
|
|
448
649
|
}
|
|
449
650
|
}
|
|
450
|
-
const envelope = await this._encryptWithLongTermKey(peerAid, payload, opts.peerCertPem, opts.messageId, opts.timestamp);
|
|
651
|
+
const envelope = await this._encryptWithLongTermKey(peerAid, payload, opts.peerCertPem, opts.messageId, opts.timestamp, opts.protectedHeaders ?? opts.protected_headers ?? opts.headers, opts.context ?? null);
|
|
451
652
|
const degraded = prekey !== null; // 有 prekey 但失败了才算降级
|
|
452
653
|
return [envelope, {
|
|
453
654
|
encrypted: true,
|
|
@@ -466,7 +667,7 @@ export class E2EEManager {
|
|
|
466
667
|
* DH3 = ECDH(sender_identity, peer_prekey) ← 绑定发送方身份
|
|
467
668
|
* DH4 = ECDH(sender_identity, peer_identity) ← 双方身份互绑
|
|
468
669
|
*/
|
|
469
|
-
async _encryptWithPrekey(peerAid, payload, prekey, peerCertPem, messageId, timestamp) {
|
|
670
|
+
async _encryptWithPrekey(peerAid, payload, prekey, peerCertPem, messageId, timestamp, protectedHeaders, context) {
|
|
470
671
|
// 导入对方 identity 公钥(ECDSA 用于验签,ECDH 用于密钥交换)
|
|
471
672
|
const peerIdentityEcdsa = await importCertPublicKeyEcdsa(peerCertPem);
|
|
472
673
|
const peerIdentityEcdh = await importCertPublicKeyEcdh(peerCertPem);
|
|
@@ -529,8 +730,6 @@ export class E2EEManager {
|
|
|
529
730
|
sender_cert_fingerprint: senderFingerprint,
|
|
530
731
|
prekey_id: prekeyId,
|
|
531
732
|
};
|
|
532
|
-
const aadBytes = aadBytesOffline(aad);
|
|
533
|
-
const [ciphertext, tag] = await aesGcmEncrypt(messageKey, nonce, plaintext, aadBytes);
|
|
534
733
|
const envelope = {
|
|
535
734
|
type: 'e2ee.encrypted',
|
|
536
735
|
version: '1',
|
|
@@ -538,11 +737,18 @@ export class E2EEManager {
|
|
|
538
737
|
suite: SUITE,
|
|
539
738
|
prekey_id: prekeyId,
|
|
540
739
|
ephemeral_public_key: ephPkB64,
|
|
541
|
-
nonce: uint8ToBase64(nonce),
|
|
542
|
-
ciphertext: uint8ToBase64(ciphertext),
|
|
543
|
-
tag: uint8ToBase64(tag),
|
|
544
|
-
aad,
|
|
545
740
|
};
|
|
741
|
+
await copyOptionalEnvelopeMetadata(envelope, messageKey, {
|
|
742
|
+
payloadType: payload.type,
|
|
743
|
+
protectedHeaders,
|
|
744
|
+
context,
|
|
745
|
+
});
|
|
746
|
+
const aadBytes = aadBytesOffline(aad);
|
|
747
|
+
const [ciphertext, tag] = await aesGcmEncrypt(messageKey, nonce, plaintext, aadBytes);
|
|
748
|
+
envelope.nonce = uint8ToBase64(nonce);
|
|
749
|
+
envelope.ciphertext = uint8ToBase64(ciphertext);
|
|
750
|
+
envelope.tag = uint8ToBase64(tag);
|
|
751
|
+
envelope.aad = aad;
|
|
546
752
|
// 发送方签名:对 ciphertext + tag + aad_bytes 签名(不可否认性)
|
|
547
753
|
const signPayload = concatBytes(ciphertext, tag, aadBytes);
|
|
548
754
|
const sig = await ecdsaSignDer(senderSignKey, signPayload);
|
|
@@ -557,7 +763,7 @@ export class E2EEManager {
|
|
|
557
763
|
* DH1 = ECDH(ephemeral, peer_identity) ← 前向保密(每消息)
|
|
558
764
|
* DH2 = ECDH(sender_identity, peer_identity) ← 绑定双方身份
|
|
559
765
|
*/
|
|
560
|
-
async _encryptWithLongTermKey(peerAid, payload, peerCertPem, messageId, timestamp) {
|
|
766
|
+
async _encryptWithLongTermKey(peerAid, payload, peerCertPem, messageId, timestamp, protectedHeaders, context) {
|
|
561
767
|
const peerIdentityEcdh = await importCertPublicKeyEcdh(peerCertPem);
|
|
562
768
|
const senderIdentityEcdhKey = await this._loadSenderIdentityPrivateEcdh();
|
|
563
769
|
const senderSignKey = await this._loadSenderIdentityPrivateEcdsa();
|
|
@@ -587,19 +793,24 @@ export class E2EEManager {
|
|
|
587
793
|
recipient_cert_fingerprint: recipientFingerprint,
|
|
588
794
|
sender_cert_fingerprint: senderFingerprint,
|
|
589
795
|
};
|
|
590
|
-
const aadBytes = aadBytesOffline(aad);
|
|
591
|
-
const [ciphertext, tag] = await aesGcmEncrypt(messageKey, nonce, plaintext, aadBytes);
|
|
592
796
|
const envelope = {
|
|
593
797
|
type: 'e2ee.encrypted',
|
|
594
798
|
version: '1',
|
|
595
799
|
encryption_mode: MODE_LONG_TERM_KEY,
|
|
596
800
|
suite: SUITE,
|
|
597
801
|
ephemeral_public_key: ephPkB64,
|
|
598
|
-
nonce: uint8ToBase64(nonce),
|
|
599
|
-
ciphertext: uint8ToBase64(ciphertext),
|
|
600
|
-
tag: uint8ToBase64(tag),
|
|
601
|
-
aad,
|
|
602
802
|
};
|
|
803
|
+
await copyOptionalEnvelopeMetadata(envelope, messageKey, {
|
|
804
|
+
payloadType: payload.type,
|
|
805
|
+
protectedHeaders,
|
|
806
|
+
context,
|
|
807
|
+
});
|
|
808
|
+
const aadBytes = aadBytesOffline(aad);
|
|
809
|
+
const [ciphertext, tag] = await aesGcmEncrypt(messageKey, nonce, plaintext, aadBytes);
|
|
810
|
+
envelope.nonce = uint8ToBase64(nonce);
|
|
811
|
+
envelope.ciphertext = uint8ToBase64(ciphertext);
|
|
812
|
+
envelope.tag = uint8ToBase64(tag);
|
|
813
|
+
envelope.aad = aad;
|
|
603
814
|
// 发送方签名(不可否认性)
|
|
604
815
|
const signPayload = concatBytes(ciphertext, tag, aadBytes);
|
|
605
816
|
const sig = await ecdsaSignDer(senderSignKey, signPayload);
|
|
@@ -783,17 +994,30 @@ export class E2EEManager {
|
|
|
783
994
|
else {
|
|
784
995
|
aadBytes = new Uint8Array(0);
|
|
785
996
|
}
|
|
997
|
+
if (!await verifyEnvelopeMetadataAuth(payload, messageKey)) {
|
|
998
|
+
throw new E2EEDecryptFailedError('envelope metadata auth failed');
|
|
999
|
+
}
|
|
786
1000
|
const plaintext = await aesGcmDecrypt(messageKey, nonce, ciphertext, tag, aadBytes);
|
|
787
1001
|
const decoded = JSON.parse(_decoder.decode(plaintext));
|
|
1002
|
+
if (!validateDecryptedEnvelopeMetadata(decoded, payload, message)) {
|
|
1003
|
+
throw new E2EEDecryptFailedError('envelope metadata mismatch');
|
|
1004
|
+
}
|
|
1005
|
+
const e2ee = {
|
|
1006
|
+
encryption_mode: MODE_PREKEY_ECDH_V2,
|
|
1007
|
+
suite: payload.suite ?? SUITE,
|
|
1008
|
+
prekey_id: prekeyId,
|
|
1009
|
+
};
|
|
1010
|
+
const protectedHeaders = exposedEnvelopeMetadata(payload.protected_headers);
|
|
1011
|
+
if (protectedHeaders)
|
|
1012
|
+
e2ee.protected_headers = protectedHeaders;
|
|
1013
|
+
const context = exposedEnvelopeMetadata(payload.context);
|
|
1014
|
+
if (context)
|
|
1015
|
+
e2ee.context = context;
|
|
788
1016
|
return {
|
|
789
1017
|
...message,
|
|
790
1018
|
payload: decoded,
|
|
791
1019
|
encrypted: true,
|
|
792
|
-
e2ee
|
|
793
|
-
encryption_mode: MODE_PREKEY_ECDH_V2,
|
|
794
|
-
suite: payload.suite ?? SUITE,
|
|
795
|
-
prekey_id: prekeyId,
|
|
796
|
-
},
|
|
1020
|
+
e2ee,
|
|
797
1021
|
};
|
|
798
1022
|
}
|
|
799
1023
|
catch (exc) {
|
|
@@ -849,16 +1073,29 @@ export class E2EEManager {
|
|
|
849
1073
|
else {
|
|
850
1074
|
aadBytes = new Uint8Array(0);
|
|
851
1075
|
}
|
|
1076
|
+
if (!await verifyEnvelopeMetadataAuth(payload, messageKey)) {
|
|
1077
|
+
throw new E2EEDecryptFailedError('envelope metadata auth failed');
|
|
1078
|
+
}
|
|
852
1079
|
const plaintext = await aesGcmDecrypt(messageKey, nonce, ciphertext, tag, aadBytes);
|
|
853
1080
|
const decoded = JSON.parse(_decoder.decode(plaintext));
|
|
1081
|
+
if (!validateDecryptedEnvelopeMetadata(decoded, payload, message)) {
|
|
1082
|
+
throw new E2EEDecryptFailedError('envelope metadata mismatch');
|
|
1083
|
+
}
|
|
1084
|
+
const e2ee = {
|
|
1085
|
+
encryption_mode: MODE_LONG_TERM_KEY,
|
|
1086
|
+
suite: payload.suite,
|
|
1087
|
+
};
|
|
1088
|
+
const protectedHeaders = exposedEnvelopeMetadata(payload.protected_headers);
|
|
1089
|
+
if (protectedHeaders)
|
|
1090
|
+
e2ee.protected_headers = protectedHeaders;
|
|
1091
|
+
const context = exposedEnvelopeMetadata(payload.context);
|
|
1092
|
+
if (context)
|
|
1093
|
+
e2ee.context = context;
|
|
854
1094
|
return {
|
|
855
1095
|
...message,
|
|
856
1096
|
payload: decoded,
|
|
857
1097
|
encrypted: true,
|
|
858
|
-
e2ee
|
|
859
|
-
encryption_mode: MODE_LONG_TERM_KEY,
|
|
860
|
-
suite: payload.suite,
|
|
861
|
-
},
|
|
1098
|
+
e2ee,
|
|
862
1099
|
};
|
|
863
1100
|
}
|
|
864
1101
|
catch (exc) {
|