@vex-chat/crypto 11.0.0 → 12.0.0
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/README.md +5 -20
- package/dist/index.d.ts +14 -58
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +37 -395
- package/dist/index.js.map +1 -1
- package/package.json +1 -2
- package/src/__tests__/xAsyncApi.extended.test.ts +3 -44
- package/src/__tests__/xAsyncProfile.ts +6 -26
- package/src/index.ts +44 -616
- package/src/__tests__/cryptoProfile.ts +0 -74
- package/src/__tests__/cryptoProfileScope.test.ts +0 -77
package/src/index.ts
CHANGED
|
@@ -12,8 +12,6 @@
|
|
|
12
12
|
|
|
13
13
|
import type { BaseMsg } from "@vex-chat/types";
|
|
14
14
|
|
|
15
|
-
import { numberToBytesBE } from "@noble/curves/abstract/utils";
|
|
16
|
-
import { p256 } from "@noble/curves/p256";
|
|
17
15
|
import { hkdf } from "@noble/hashes/hkdf.js";
|
|
18
16
|
import { hmac } from "@noble/hashes/hmac.js";
|
|
19
17
|
import { pbkdf2 as noblePbkdf2 } from "@noble/hashes/pbkdf2.js";
|
|
@@ -35,14 +33,10 @@ const KEY_DATA_PBKDF2_ITERATIONS = 220_000;
|
|
|
35
33
|
const KEY_DATA_PBKDF2_MIN_ITERATIONS = 1_000;
|
|
36
34
|
const KEY_DATA_PBKDF2_MAX_ITERATIONS = 2_000_000;
|
|
37
35
|
|
|
38
|
-
/** Runtime crypto profile selector. */
|
|
39
|
-
export type CryptoProfile = "fips" | "tweetnacl";
|
|
40
|
-
|
|
41
36
|
interface CryptoProvider {
|
|
42
37
|
boxBefore(myPrivateKey: Uint8Array, theirPublicKey: Uint8Array): Uint8Array;
|
|
43
38
|
boxKeyPair(): KeyPair;
|
|
44
39
|
boxKeyPairFromSecret(secretKey: Uint8Array): KeyPair;
|
|
45
|
-
profile: CryptoProfile;
|
|
46
40
|
randomBytes(length: number): Uint8Array;
|
|
47
41
|
secretbox(
|
|
48
42
|
plaintext: Uint8Array,
|
|
@@ -63,31 +57,11 @@ interface CryptoProvider {
|
|
|
63
57
|
): null | Uint8Array;
|
|
64
58
|
}
|
|
65
59
|
|
|
66
|
-
type WebCryptoKeyUsage =
|
|
67
|
-
| "decrypt"
|
|
68
|
-
| "deriveBits"
|
|
69
|
-
| "deriveKey"
|
|
70
|
-
| "encrypt"
|
|
71
|
-
| "sign"
|
|
72
|
-
| "unwrapKey"
|
|
73
|
-
| "verify"
|
|
74
|
-
| "wrapKey";
|
|
75
|
-
|
|
76
60
|
interface WebCryptoLike {
|
|
77
61
|
getRandomValues: Crypto["getRandomValues"];
|
|
78
62
|
subtle: SubtleCrypto;
|
|
79
63
|
}
|
|
80
64
|
|
|
81
|
-
function getWebCrypto(): WebCryptoLike {
|
|
82
|
-
const cryptoCandidate: unknown = globalThis.crypto;
|
|
83
|
-
if (!isWebCryptoLike(cryptoCandidate)) {
|
|
84
|
-
throw new Error(
|
|
85
|
-
"Web Crypto API is not available in this runtime for fips profile operations.",
|
|
86
|
-
);
|
|
87
|
-
}
|
|
88
|
-
return cryptoCandidate;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
65
|
function isWebCryptoLike(value: unknown): value is WebCryptoLike {
|
|
92
66
|
if (typeof value !== "object" || value === null) {
|
|
93
67
|
return false;
|
|
@@ -134,37 +108,12 @@ async function pbkdf2Sha512Async(
|
|
|
134
108
|
return new Uint8Array(bits);
|
|
135
109
|
}
|
|
136
110
|
|
|
137
|
-
function randomBytesWebCrypto(length: number): Uint8Array {
|
|
138
|
-
if (!Number.isInteger(length) || length < 0) {
|
|
139
|
-
throw new Error(
|
|
140
|
-
`Expected non-negative integer length, received ${String(length)}.`,
|
|
141
|
-
);
|
|
142
|
-
}
|
|
143
|
-
const cryptoImpl = getWebCrypto();
|
|
144
|
-
const result = new Uint8Array(length);
|
|
145
|
-
let offset = 0;
|
|
146
|
-
while (offset < length) {
|
|
147
|
-
const chunkLength = Math.min(65536, length - offset);
|
|
148
|
-
const chunk = result.subarray(offset, offset + chunkLength);
|
|
149
|
-
cryptoImpl.getRandomValues(chunk);
|
|
150
|
-
offset += chunkLength;
|
|
151
|
-
}
|
|
152
|
-
return result;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function unsupported(profile: CryptoProfile, operation: string): never {
|
|
156
|
-
throw new Error(
|
|
157
|
-
`Crypto profile "${profile}" does not implement ${operation} yet.`,
|
|
158
|
-
);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
111
|
const tweetnaclProvider: CryptoProvider = {
|
|
162
112
|
boxBefore: (myPrivateKey, theirPublicKey) =>
|
|
163
113
|
nacl.box.before(theirPublicKey, myPrivateKey),
|
|
164
114
|
boxKeyPair: () => nacl.box.keyPair(),
|
|
165
115
|
boxKeyPairFromSecret: (secretKey) =>
|
|
166
116
|
nacl.box.keyPair.fromSecretKey(secretKey),
|
|
167
|
-
profile: "tweetnacl",
|
|
168
117
|
randomBytes: (length) => nacl.randomBytes(length),
|
|
169
118
|
secretbox: (plaintext, nonce, key) => nacl.secretbox(plaintext, nonce, key),
|
|
170
119
|
secretboxOpen: (ciphertext, nonce, key) =>
|
|
@@ -177,246 +126,8 @@ const tweetnaclProvider: CryptoProvider = {
|
|
|
177
126
|
nacl.sign.open(signedMessage, publicKey),
|
|
178
127
|
};
|
|
179
128
|
|
|
180
|
-
const fipsProvider: CryptoProvider = {
|
|
181
|
-
boxBefore: (myPrivateKey, theirPublicKey) => {
|
|
182
|
-
void myPrivateKey;
|
|
183
|
-
void theirPublicKey;
|
|
184
|
-
return unsupported("fips", "xDH");
|
|
185
|
-
},
|
|
186
|
-
boxKeyPair: () => unsupported("fips", "xBoxKeyPair"),
|
|
187
|
-
boxKeyPairFromSecret: (secretKey) => {
|
|
188
|
-
void secretKey;
|
|
189
|
-
return unsupported("fips", "xBoxKeyPairFromSecret");
|
|
190
|
-
},
|
|
191
|
-
profile: "fips",
|
|
192
|
-
randomBytes: (length) => randomBytesWebCrypto(length),
|
|
193
|
-
secretbox: (plaintext, nonce, key) => {
|
|
194
|
-
void plaintext;
|
|
195
|
-
void nonce;
|
|
196
|
-
void key;
|
|
197
|
-
return unsupported("fips", "xSecretbox");
|
|
198
|
-
},
|
|
199
|
-
secretboxOpen: (ciphertext, nonce, key) => {
|
|
200
|
-
void ciphertext;
|
|
201
|
-
void nonce;
|
|
202
|
-
void key;
|
|
203
|
-
return unsupported("fips", "xSecretboxOpen");
|
|
204
|
-
},
|
|
205
|
-
sign: (message, secretKey) => {
|
|
206
|
-
void message;
|
|
207
|
-
void secretKey;
|
|
208
|
-
return unsupported("fips", "xSign");
|
|
209
|
-
},
|
|
210
|
-
signKeyPair: () => unsupported("fips", "xSignKeyPair"),
|
|
211
|
-
signKeyPairFromSecret: (secretKey) => {
|
|
212
|
-
void secretKey;
|
|
213
|
-
return unsupported("fips", "xSignKeyPairFromSecret");
|
|
214
|
-
},
|
|
215
|
-
signOpen: (signedMessage, publicKey) => {
|
|
216
|
-
void signedMessage;
|
|
217
|
-
void publicKey;
|
|
218
|
-
return unsupported("fips", "xSignOpen");
|
|
219
|
-
},
|
|
220
|
-
};
|
|
221
|
-
|
|
222
|
-
const providers: Record<CryptoProfile, CryptoProvider> = {
|
|
223
|
-
fips: fipsProvider,
|
|
224
|
-
tweetnacl: tweetnaclProvider,
|
|
225
|
-
};
|
|
226
|
-
|
|
227
|
-
let activeCryptoProfile: CryptoProfile = "tweetnacl";
|
|
228
|
-
let activeCryptoProvider: CryptoProvider = providers[activeCryptoProfile];
|
|
229
|
-
|
|
230
|
-
/** Returns the currently configured crypto profile. */
|
|
231
|
-
export function getCryptoProfile(): CryptoProfile {
|
|
232
|
-
return activeCryptoProfile;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
/**
|
|
236
|
-
* Sets the runtime crypto profile.
|
|
237
|
-
*
|
|
238
|
-
* `tweetnacl` preserves existing behavior.
|
|
239
|
-
* `fips` currently enables only backend-agnostic helpers; NaCl-coupled
|
|
240
|
-
* primitives throw until a FIPS backend implementation is wired in.
|
|
241
|
-
*/
|
|
242
|
-
export function setCryptoProfile(profile: CryptoProfile): void {
|
|
243
|
-
activeCryptoProfile = profile;
|
|
244
|
-
activeCryptoProvider = providers[profile];
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
const cryptoProfileScopeStack: CryptoProfile[] = [];
|
|
248
|
-
|
|
249
|
-
/**
|
|
250
|
-
* Saves the current profile and switches to `profile`. Pair every call with
|
|
251
|
-
* {@link leaveCryptoProfileScope} in a `finally` block.
|
|
252
|
-
*
|
|
253
|
-
* **Why:** `setCryptoProfile` is process-wide. Several async libvex `Client`s
|
|
254
|
-
* can overlap on `readMail`; a naive save/restore in `finally` can reset the
|
|
255
|
-
* profile while another client still needs FIPS — `xSecretboxOpenAsync` then
|
|
256
|
-
* reads `tweetnacl` and fails to decrypt AES-GCM payloads. Nesting this stack
|
|
257
|
-
* fixes that.
|
|
258
|
-
*/
|
|
259
|
-
export function enterCryptoProfileScope(profile: CryptoProfile): void {
|
|
260
|
-
cryptoProfileScopeStack.push(activeCryptoProfile);
|
|
261
|
-
setCryptoProfile(profile);
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
/** Restores the profile saved by the innermost {@link enterCryptoProfileScope}. */
|
|
265
|
-
export function leaveCryptoProfileScope(): void {
|
|
266
|
-
const prev = cryptoProfileScopeStack.pop();
|
|
267
|
-
if (prev === undefined) {
|
|
268
|
-
throw new Error(
|
|
269
|
-
"leaveCryptoProfileScope called without a matching enterCryptoProfileScope",
|
|
270
|
-
);
|
|
271
|
-
}
|
|
272
|
-
setCryptoProfile(prev);
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
function bytesToBase64Url(bytes: Uint8Array): string {
|
|
276
|
-
return globalThis
|
|
277
|
-
.btoa(String.fromCodePoint(...Array.from(bytes)))
|
|
278
|
-
.replace(/\+/g, "-")
|
|
279
|
-
.replace(/\//g, "_")
|
|
280
|
-
.replace(/=+/g, "");
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
function concatBytes(a: Uint8Array, b: Uint8Array): Uint8Array {
|
|
284
|
-
const out = new Uint8Array(a.length + b.length);
|
|
285
|
-
out.set(a, 0);
|
|
286
|
-
out.set(b, a.length);
|
|
287
|
-
return out;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
function decodeFipsSignedMessage(signedMessage: Uint8Array): {
|
|
291
|
-
message: Uint8Array;
|
|
292
|
-
signature: Uint8Array;
|
|
293
|
-
} {
|
|
294
|
-
if (signedMessage.length < 2) {
|
|
295
|
-
throw new Error(
|
|
296
|
-
"Invalid FIPS signed message: missing signature length prefix.",
|
|
297
|
-
);
|
|
298
|
-
}
|
|
299
|
-
const signatureLength =
|
|
300
|
-
(signedMessage[0] ?? 0) * 256 + (signedMessage[1] ?? 0);
|
|
301
|
-
const signatureStart = 2;
|
|
302
|
-
const signatureEnd = signatureStart + signatureLength;
|
|
303
|
-
if (signedMessage.length < signatureEnd) {
|
|
304
|
-
throw new Error(
|
|
305
|
-
"Invalid FIPS signed message: signature length exceeds message length.",
|
|
306
|
-
);
|
|
307
|
-
}
|
|
308
|
-
return {
|
|
309
|
-
message: signedMessage.slice(signatureEnd),
|
|
310
|
-
signature: signedMessage.slice(signatureStart, signatureEnd),
|
|
311
|
-
};
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
function encodeFipsSignedMessage(
|
|
315
|
-
signature: Uint8Array,
|
|
316
|
-
message: Uint8Array,
|
|
317
|
-
): Uint8Array {
|
|
318
|
-
if (signature.length > 65535) {
|
|
319
|
-
throw new Error("FIPS signature too long to encode.");
|
|
320
|
-
}
|
|
321
|
-
const prefix = new Uint8Array(2);
|
|
322
|
-
prefix[0] = (signature.length >> 8) & 0xff;
|
|
323
|
-
prefix[1] = signature.length & 0xff;
|
|
324
|
-
return concatBytes(concatBytes(prefix, signature), message);
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
async function fipsEcdhKeyPairFrom32ByteSeed(
|
|
328
|
-
secretKey: Uint8Array,
|
|
329
|
-
subtle: SubtleCrypto,
|
|
330
|
-
): Promise<KeyPair> {
|
|
331
|
-
if (secretKey.length !== 32) {
|
|
332
|
-
throw new Error(
|
|
333
|
-
"FIPS: expected a 32-byte IKM/seed for ECDH from-secret.",
|
|
334
|
-
);
|
|
335
|
-
}
|
|
336
|
-
const d = p256.utils.normPrivateKeyToScalar(Uint8Array.from(secretKey));
|
|
337
|
-
const d32 = numberToBytesBE(d, 32);
|
|
338
|
-
const rawPub = p256.getPublicKey(d, false);
|
|
339
|
-
if (rawPub[0] !== 0x04) {
|
|
340
|
-
throw new Error("FIPS: expected uncompressed P-256 public key.");
|
|
341
|
-
}
|
|
342
|
-
const jwk: JsonWebKey = {
|
|
343
|
-
crv: "P-256",
|
|
344
|
-
d: bytesToBase64Url(d32),
|
|
345
|
-
kty: "EC",
|
|
346
|
-
x: bytesToBase64Url(rawPub.subarray(1, 33)),
|
|
347
|
-
y: bytesToBase64Url(rawPub.subarray(33, 65)),
|
|
348
|
-
};
|
|
349
|
-
const ecdhPriv = await subtle.importKey(
|
|
350
|
-
"jwk",
|
|
351
|
-
jwk,
|
|
352
|
-
{ name: "ECDH", namedCurve: "P-256" },
|
|
353
|
-
true,
|
|
354
|
-
["deriveBits"],
|
|
355
|
-
);
|
|
356
|
-
const ecdhPubJwk = await subtle.exportKey("jwk", ecdhPriv);
|
|
357
|
-
const ecdhPubJwkNoD: JsonWebKey = { ...ecdhPubJwk };
|
|
358
|
-
delete ecdhPubJwkNoD.d;
|
|
359
|
-
ecdhPubJwkNoD.key_ops = [];
|
|
360
|
-
const ecdhPub = await subtle.importKey(
|
|
361
|
-
"jwk",
|
|
362
|
-
ecdhPubJwkNoD,
|
|
363
|
-
{ name: "ECDH", namedCurve: "P-256" },
|
|
364
|
-
true,
|
|
365
|
-
[],
|
|
366
|
-
);
|
|
367
|
-
return {
|
|
368
|
-
publicKey: new Uint8Array(await subtle.exportKey("raw", ecdhPub)),
|
|
369
|
-
secretKey: new Uint8Array(await subtle.exportKey("pkcs8", ecdhPriv)),
|
|
370
|
-
};
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
async function fipsEcdhKeyPairFromPkcs8(
|
|
374
|
-
secretKey: Uint8Array,
|
|
375
|
-
subtle: SubtleCrypto,
|
|
376
|
-
): Promise<KeyPair> {
|
|
377
|
-
const ecdhPriv = await subtle.importKey(
|
|
378
|
-
"pkcs8",
|
|
379
|
-
toBufferSource(secretKey),
|
|
380
|
-
{ name: "ECDH", namedCurve: "P-256" },
|
|
381
|
-
true,
|
|
382
|
-
["deriveBits"],
|
|
383
|
-
);
|
|
384
|
-
const jwk = await subtle.exportKey("jwk", ecdhPriv);
|
|
385
|
-
const ecdhPubJwk: JsonWebKey = { ...jwk };
|
|
386
|
-
delete ecdhPubJwk.d;
|
|
387
|
-
ecdhPubJwk.key_ops = [];
|
|
388
|
-
const ecdhPub = await subtle.importKey(
|
|
389
|
-
"jwk",
|
|
390
|
-
ecdhPubJwk,
|
|
391
|
-
{ name: "ECDH", namedCurve: "P-256" },
|
|
392
|
-
true,
|
|
393
|
-
[],
|
|
394
|
-
);
|
|
395
|
-
return {
|
|
396
|
-
publicKey: new Uint8Array(await subtle.exportKey("raw", ecdhPub)),
|
|
397
|
-
secretKey: new Uint8Array(await subtle.exportKey("pkcs8", ecdhPriv)),
|
|
398
|
-
};
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
function getSubtleCrypto(): SubtleCrypto {
|
|
402
|
-
return getWebCrypto().subtle;
|
|
403
|
-
}
|
|
404
|
-
|
|
405
129
|
function provider(): CryptoProvider {
|
|
406
|
-
return
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
function requireAesGcmNonce(nonce: Uint8Array): Uint8Array {
|
|
410
|
-
if (nonce.length < 12) {
|
|
411
|
-
throw new Error(
|
|
412
|
-
`AES-GCM requires a nonce of at least 12 bytes, received ${String(nonce.length)}.`,
|
|
413
|
-
);
|
|
414
|
-
}
|
|
415
|
-
return nonce.slice(0, 12);
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
function toBufferSource(bytes: Uint8Array): ArrayBuffer {
|
|
419
|
-
return Uint8Array.from(bytes).buffer;
|
|
130
|
+
return tweetnaclProvider;
|
|
420
131
|
}
|
|
421
132
|
|
|
422
133
|
// msgpackr with useRecords:false emits standard msgpack (no nonstandard record extension).
|
|
@@ -534,10 +245,7 @@ export class XUtils {
|
|
|
534
245
|
}
|
|
535
246
|
};
|
|
536
247
|
|
|
537
|
-
/**
|
|
538
|
-
* Async variant of decryptKeyData for cross-runtime/FIPS backends.
|
|
539
|
-
* Supports both profile formats emitted by encryptKeyDataAsync.
|
|
540
|
-
*/
|
|
248
|
+
/** Async variant of decryptKeyData for cross-runtime callers. */
|
|
541
249
|
public static decryptKeyDataAsync = async (
|
|
542
250
|
keyData: Uint8Array,
|
|
543
251
|
password: string,
|
|
@@ -552,18 +260,11 @@ export class XUtils {
|
|
|
552
260
|
ITERATIONS,
|
|
553
261
|
);
|
|
554
262
|
try {
|
|
555
|
-
const decrypted =
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
DERIVED_KEY,
|
|
561
|
-
)
|
|
562
|
-
: xSecretboxOpen(
|
|
563
|
-
ENCRYPTED_KEY,
|
|
564
|
-
ENCRYPTION_NONCE,
|
|
565
|
-
DERIVED_KEY,
|
|
566
|
-
);
|
|
263
|
+
const decrypted = xSecretboxOpen(
|
|
264
|
+
ENCRYPTED_KEY,
|
|
265
|
+
ENCRYPTION_NONCE,
|
|
266
|
+
DERIVED_KEY,
|
|
267
|
+
);
|
|
567
268
|
|
|
568
269
|
if (decrypted === null) {
|
|
569
270
|
throw new Error("Decryption failed. Wrong password?");
|
|
@@ -578,12 +279,9 @@ export class XUtils {
|
|
|
578
279
|
|
|
579
280
|
/**
|
|
580
281
|
* Derive a purpose-separated 32-byte key for local at-rest encryption.
|
|
581
|
-
* The result never aliases raw identity-key bytes
|
|
282
|
+
* The result never aliases raw identity-key bytes.
|
|
582
283
|
*/
|
|
583
|
-
public static deriveLocalAtRestAesKey(
|
|
584
|
-
identitySk: Uint8Array,
|
|
585
|
-
profile: CryptoProfile,
|
|
586
|
-
): Uint8Array {
|
|
284
|
+
public static deriveLocalAtRestAesKey(identitySk: Uint8Array): Uint8Array {
|
|
587
285
|
if (identitySk.length < 32) {
|
|
588
286
|
throw new Error("Expected at least 32 bytes of identity secret.");
|
|
589
287
|
}
|
|
@@ -592,7 +290,7 @@ export class XUtils {
|
|
|
592
290
|
sha256,
|
|
593
291
|
identitySk,
|
|
594
292
|
new Uint8Array(0),
|
|
595
|
-
new TextEncoder().encode(
|
|
293
|
+
new TextEncoder().encode("vex:at-rest:3:tweetnacl"),
|
|
596
294
|
32,
|
|
597
295
|
),
|
|
598
296
|
);
|
|
@@ -676,10 +374,7 @@ export class XUtils {
|
|
|
676
374
|
return result;
|
|
677
375
|
};
|
|
678
376
|
|
|
679
|
-
/**
|
|
680
|
-
* Async variant of encryptKeyData for cross-runtime/FIPS backends.
|
|
681
|
-
* Format remains [iterations(6)|salt(24)|nonce(24)|ciphertext(N)].
|
|
682
|
-
*/
|
|
377
|
+
/** Async variant of encryptKeyData for cross-runtime callers. */
|
|
683
378
|
public static encryptKeyDataAsync = async (
|
|
684
379
|
password: string,
|
|
685
380
|
keyToSave: string,
|
|
@@ -699,14 +394,11 @@ export class XUtils {
|
|
|
699
394
|
const NONCE = xMakeNonce();
|
|
700
395
|
let ENCRYPTED_SIGNKEY: Uint8Array;
|
|
701
396
|
try {
|
|
702
|
-
ENCRYPTED_SIGNKEY =
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
ENCRYPTION_KEY,
|
|
708
|
-
)
|
|
709
|
-
: xSecretbox(UNENCRYPTED_SIGNKEY, NONCE, ENCRYPTION_KEY);
|
|
397
|
+
ENCRYPTED_SIGNKEY = xSecretbox(
|
|
398
|
+
UNENCRYPTED_SIGNKEY,
|
|
399
|
+
NONCE,
|
|
400
|
+
ENCRYPTION_KEY,
|
|
401
|
+
);
|
|
710
402
|
} finally {
|
|
711
403
|
ENCRYPTION_KEY.fill(0);
|
|
712
404
|
UNENCRYPTED_SIGNKEY.fill(0);
|
|
@@ -874,7 +566,6 @@ export function xMnemonic(entropy: Uint8Array, wordList?: string[]) {
|
|
|
874
566
|
export function xPreKeySignaturePayload(
|
|
875
567
|
publicKey: Uint8Array,
|
|
876
568
|
kind: "one-time" | "signed",
|
|
877
|
-
profile: CryptoProfile = getCryptoProfile(),
|
|
878
569
|
): Uint8Array {
|
|
879
570
|
if (publicKey.length === 0) {
|
|
880
571
|
throw new Error("Prekey public key cannot be empty.");
|
|
@@ -883,7 +574,7 @@ export function xPreKeySignaturePayload(
|
|
|
883
574
|
return xConcat(
|
|
884
575
|
XUtils.decodeUTF8("vex:x3dh:prekey:v2"),
|
|
885
576
|
separator,
|
|
886
|
-
XUtils.decodeUTF8(
|
|
577
|
+
XUtils.decodeUTF8("tweetnacl"),
|
|
887
578
|
separator,
|
|
888
579
|
XUtils.decodeUTF8(kind),
|
|
889
580
|
separator,
|
|
@@ -935,67 +626,14 @@ export interface XConstants {
|
|
|
935
626
|
MIN_OTK_SUPPLY: number;
|
|
936
627
|
}
|
|
937
628
|
|
|
938
|
-
/**
|
|
939
|
-
* FIPS: `device.signKey` in the database is the P-256 ECDSA public key (SPKI),
|
|
940
|
-
* used for account/device signature verification. X3DH on the client expects
|
|
941
|
-
* the same curve point as Web Crypto "raw" P-256 ECDH public bytes for
|
|
942
|
-
* `importEcdhPublicKey`. This converts SPKI → raw without a private key.
|
|
943
|
-
*/
|
|
944
|
-
export async function fipsEcdhRawPublicKeyFromEcdsaSpkiAsync(
|
|
945
|
-
ecdsaSpki: Uint8Array,
|
|
946
|
-
): Promise<Uint8Array> {
|
|
947
|
-
if (ecdsaSpki.length === 0) {
|
|
948
|
-
throw new Error("FIPS: empty ECDSA SPKI.");
|
|
949
|
-
}
|
|
950
|
-
const subtle = getSubtleCrypto();
|
|
951
|
-
const ecdsaPub = await importEcdsaPublicKey(ecdsaSpki);
|
|
952
|
-
const jwk = await subtle.exportKey("jwk", ecdsaPub);
|
|
953
|
-
if (
|
|
954
|
-
jwk.x === undefined ||
|
|
955
|
-
jwk.y === undefined ||
|
|
956
|
-
jwk.x.length === 0 ||
|
|
957
|
-
jwk.y.length === 0
|
|
958
|
-
) {
|
|
959
|
-
throw new Error("FIPS: could not export ECDSA public as JWK.");
|
|
960
|
-
}
|
|
961
|
-
const ecdhJwk: JsonWebKey = { ...jwk };
|
|
962
|
-
delete ecdhJwk.d;
|
|
963
|
-
ecdhJwk.key_ops = [];
|
|
964
|
-
ecdhJwk.ext = true;
|
|
965
|
-
const ecdhPub = await subtle.importKey(
|
|
966
|
-
"jwk",
|
|
967
|
-
ecdhJwk,
|
|
968
|
-
{ name: "ECDH", namedCurve: "P-256" },
|
|
969
|
-
true,
|
|
970
|
-
[],
|
|
971
|
-
);
|
|
972
|
-
return new Uint8Array(await subtle.exportKey("raw", ecdhPub));
|
|
973
|
-
}
|
|
974
|
-
|
|
975
629
|
/** Generate a fresh X25519 box key pair. */
|
|
976
630
|
export function xBoxKeyPair(): KeyPair {
|
|
977
631
|
return provider().boxKeyPair();
|
|
978
632
|
}
|
|
979
633
|
|
|
980
|
-
/** Async box keypair generation
|
|
981
|
-
export
|
|
982
|
-
|
|
983
|
-
return xBoxKeyPair();
|
|
984
|
-
}
|
|
985
|
-
const subtle = getSubtleCrypto();
|
|
986
|
-
const pair = await subtle.generateKey(
|
|
987
|
-
{ name: "ECDH", namedCurve: "P-256" },
|
|
988
|
-
true,
|
|
989
|
-
["deriveBits"],
|
|
990
|
-
);
|
|
991
|
-
return {
|
|
992
|
-
publicKey: new Uint8Array(
|
|
993
|
-
await subtle.exportKey("raw", pair.publicKey),
|
|
994
|
-
),
|
|
995
|
-
secretKey: new Uint8Array(
|
|
996
|
-
await subtle.exportKey("pkcs8", pair.privateKey),
|
|
997
|
-
),
|
|
998
|
-
};
|
|
634
|
+
/** Async X25519 box keypair generation. */
|
|
635
|
+
export function xBoxKeyPairAsync(): Promise<KeyPair> {
|
|
636
|
+
return Promise.resolve(xBoxKeyPair());
|
|
999
637
|
}
|
|
1000
638
|
|
|
1001
639
|
/** Restore an X25519 box key pair from a 32-byte secret key. */
|
|
@@ -1005,17 +643,11 @@ export function xBoxKeyPairFromSecret(secretKey: Uint8Array): KeyPair {
|
|
|
1005
643
|
|
|
1006
644
|
// ── Key pair type ───────────────────────────────────────────────────────────
|
|
1007
645
|
|
|
1008
|
-
/** Async box key restore from private key material. */
|
|
1009
|
-
export
|
|
646
|
+
/** Async X25519 box key restore from private key material. */
|
|
647
|
+
export function xBoxKeyPairFromSecretAsync(
|
|
1010
648
|
secretKey: Uint8Array,
|
|
1011
649
|
): Promise<KeyPair> {
|
|
1012
|
-
|
|
1013
|
-
return xBoxKeyPairFromSecret(secretKey);
|
|
1014
|
-
}
|
|
1015
|
-
if (secretKey.length === 32) {
|
|
1016
|
-
return fipsEcdhKeyPairFrom32ByteSeed(secretKey, getSubtleCrypto());
|
|
1017
|
-
}
|
|
1018
|
-
return fipsEcdhKeyPairFromPkcs8(secretKey, getSubtleCrypto());
|
|
650
|
+
return Promise.resolve(xBoxKeyPairFromSecret(secretKey));
|
|
1019
651
|
}
|
|
1020
652
|
|
|
1021
653
|
// ── Key generation ─────────────────────────────────────────────────────────
|
|
@@ -1060,70 +692,12 @@ export function xDH(
|
|
|
1060
692
|
return provider().boxBefore(myPrivateKey, theirPublicKey);
|
|
1061
693
|
}
|
|
1062
694
|
|
|
1063
|
-
/** Async DH
|
|
1064
|
-
export
|
|
695
|
+
/** Async X25519 DH. */
|
|
696
|
+
export function xDHAsync(
|
|
1065
697
|
myPrivateKey: Uint8Array,
|
|
1066
698
|
theirPublicKey: Uint8Array,
|
|
1067
699
|
): Promise<Uint8Array> {
|
|
1068
|
-
|
|
1069
|
-
return xDH(myPrivateKey, theirPublicKey);
|
|
1070
|
-
}
|
|
1071
|
-
const subtle = getSubtleCrypto();
|
|
1072
|
-
const privateKey = await importEcdhPrivateKey(myPrivateKey);
|
|
1073
|
-
const publicKey = await importEcdhPublicKey(theirPublicKey);
|
|
1074
|
-
const shared = await subtle.deriveBits(
|
|
1075
|
-
{ name: "ECDH", public: publicKey },
|
|
1076
|
-
privateKey,
|
|
1077
|
-
256,
|
|
1078
|
-
);
|
|
1079
|
-
return new Uint8Array(shared);
|
|
1080
|
-
}
|
|
1081
|
-
|
|
1082
|
-
/**
|
|
1083
|
-
* In `fips` mode only: derive a P-256 ECDH `KeyPair` (raw public + pkcs8 secret)
|
|
1084
|
-
* from a P-256 ECDSA `KeyPair` (spki + pkcs8) using the same private scalar in Web Crypto.
|
|
1085
|
-
* In `tweetnacl` mode, use `XKeyConvert.convertKeyPair` to map Ed25519 → X25519 instead.
|
|
1086
|
-
*/
|
|
1087
|
-
export async function xEcdhKeyPairFromEcdsaKeyPairAsync(
|
|
1088
|
-
sign: KeyPair,
|
|
1089
|
-
): Promise<KeyPair> {
|
|
1090
|
-
if (activeCryptoProfile === "tweetnacl") {
|
|
1091
|
-
return Promise.reject(
|
|
1092
|
-
new Error(
|
|
1093
|
-
'xEcdhKeyPairFromEcdsaKeyPairAsync is for crypto profile "fips" only. Use XKeyConvert.convertKeyPair in tweetnacl mode.',
|
|
1094
|
-
),
|
|
1095
|
-
);
|
|
1096
|
-
}
|
|
1097
|
-
const subtle = getSubtleCrypto();
|
|
1098
|
-
const ecdsaPriv = await importEcdsaPrivateKey(sign.secretKey);
|
|
1099
|
-
const jwk = await subtle.exportKey("jwk", ecdsaPriv);
|
|
1100
|
-
if (typeof jwk.d !== "string" || jwk.d.length === 0) {
|
|
1101
|
-
throw new Error("FIPS: could not export ECDSA private as JWK.");
|
|
1102
|
-
}
|
|
1103
|
-
const ecdhJwk: JsonWebKey = { ...jwk, key_ops: ["deriveBits"] };
|
|
1104
|
-
ecdhJwk.ext = true;
|
|
1105
|
-
const ecdhPriv = await subtle.importKey(
|
|
1106
|
-
"jwk",
|
|
1107
|
-
ecdhJwk,
|
|
1108
|
-
{ name: "ECDH", namedCurve: "P-256" },
|
|
1109
|
-
true,
|
|
1110
|
-
["deriveBits"],
|
|
1111
|
-
);
|
|
1112
|
-
const ecdhPubJwk = await subtle.exportKey("jwk", ecdhPriv);
|
|
1113
|
-
const ecdhPubJwkNoD: JsonWebKey = { ...ecdhPubJwk };
|
|
1114
|
-
delete ecdhPubJwkNoD.d;
|
|
1115
|
-
ecdhPubJwkNoD.key_ops = [];
|
|
1116
|
-
const ecdhPub = await subtle.importKey(
|
|
1117
|
-
"jwk",
|
|
1118
|
-
ecdhPubJwkNoD,
|
|
1119
|
-
{ name: "ECDH", namedCurve: "P-256" },
|
|
1120
|
-
true,
|
|
1121
|
-
[],
|
|
1122
|
-
);
|
|
1123
|
-
return {
|
|
1124
|
-
publicKey: new Uint8Array(await subtle.exportKey("raw", ecdhPub)),
|
|
1125
|
-
secretKey: new Uint8Array(await subtle.exportKey("pkcs8", ecdhPriv)),
|
|
1126
|
-
};
|
|
700
|
+
return Promise.resolve(xDH(myPrivateKey, theirPublicKey));
|
|
1127
701
|
}
|
|
1128
702
|
|
|
1129
703
|
// ── Signing ────────────────────────────────────────────────────────────────
|
|
@@ -1217,24 +791,13 @@ export function xSecretbox(
|
|
|
1217
791
|
return provider().secretbox(plaintext, nonce, key);
|
|
1218
792
|
}
|
|
1219
793
|
|
|
1220
|
-
/** Async
|
|
1221
|
-
export
|
|
794
|
+
/** Async XSalsa20-Poly1305 encryption. */
|
|
795
|
+
export function xSecretboxAsync(
|
|
1222
796
|
plaintext: Uint8Array,
|
|
1223
797
|
nonce: Uint8Array,
|
|
1224
798
|
key: Uint8Array,
|
|
1225
799
|
): Promise<Uint8Array> {
|
|
1226
|
-
|
|
1227
|
-
return xSecretbox(plaintext, nonce, key);
|
|
1228
|
-
}
|
|
1229
|
-
const subtle = getSubtleCrypto();
|
|
1230
|
-
const iv = requireAesGcmNonce(nonce);
|
|
1231
|
-
const aesKey = await importAesKey(key, ["encrypt"]);
|
|
1232
|
-
const ciphertext = await subtle.encrypt(
|
|
1233
|
-
{ iv: toBufferSource(iv), name: "AES-GCM" },
|
|
1234
|
-
aesKey,
|
|
1235
|
-
toBufferSource(plaintext),
|
|
1236
|
-
);
|
|
1237
|
-
return new Uint8Array(ciphertext);
|
|
800
|
+
return Promise.resolve(xSecretbox(plaintext, nonce, key));
|
|
1238
801
|
}
|
|
1239
802
|
|
|
1240
803
|
/** Decrypt with a shared secret key. Returns null if authentication fails. */
|
|
@@ -1246,28 +809,13 @@ export function xSecretboxOpen(
|
|
|
1246
809
|
return provider().secretboxOpen(ciphertext, nonce, key);
|
|
1247
810
|
}
|
|
1248
811
|
|
|
1249
|
-
/** Async
|
|
1250
|
-
export
|
|
812
|
+
/** Async XSalsa20-Poly1305 decryption. */
|
|
813
|
+
export function xSecretboxOpenAsync(
|
|
1251
814
|
ciphertext: Uint8Array,
|
|
1252
815
|
nonce: Uint8Array,
|
|
1253
816
|
key: Uint8Array,
|
|
1254
817
|
): Promise<null | Uint8Array> {
|
|
1255
|
-
|
|
1256
|
-
return xSecretboxOpen(ciphertext, nonce, key);
|
|
1257
|
-
}
|
|
1258
|
-
const subtle = getSubtleCrypto();
|
|
1259
|
-
const iv = requireAesGcmNonce(nonce);
|
|
1260
|
-
const aesKey = await importAesKey(key, ["decrypt"]);
|
|
1261
|
-
try {
|
|
1262
|
-
const plaintext = await subtle.decrypt(
|
|
1263
|
-
{ iv: toBufferSource(iv), name: "AES-GCM" },
|
|
1264
|
-
aesKey,
|
|
1265
|
-
toBufferSource(ciphertext),
|
|
1266
|
-
);
|
|
1267
|
-
return new Uint8Array(plaintext);
|
|
1268
|
-
} catch {
|
|
1269
|
-
return null;
|
|
1270
|
-
}
|
|
818
|
+
return Promise.resolve(xSecretboxOpen(ciphertext, nonce, key));
|
|
1271
819
|
}
|
|
1272
820
|
|
|
1273
821
|
/** Sign a message with an Ed25519 secret key. Returns signed message (64-byte signature prefix + message). */
|
|
@@ -1275,24 +823,12 @@ export function xSign(message: Uint8Array, secretKey: Uint8Array): Uint8Array {
|
|
|
1275
823
|
return provider().sign(message, secretKey);
|
|
1276
824
|
}
|
|
1277
825
|
|
|
1278
|
-
/** Async signing
|
|
1279
|
-
export
|
|
826
|
+
/** Async Ed25519 signing. */
|
|
827
|
+
export function xSignAsync(
|
|
1280
828
|
message: Uint8Array,
|
|
1281
829
|
secretKey: Uint8Array,
|
|
1282
830
|
): Promise<Uint8Array> {
|
|
1283
|
-
|
|
1284
|
-
return xSign(message, secretKey);
|
|
1285
|
-
}
|
|
1286
|
-
const subtle = getSubtleCrypto();
|
|
1287
|
-
const privateKey = await importEcdsaPrivateKey(secretKey);
|
|
1288
|
-
const signature = new Uint8Array(
|
|
1289
|
-
await subtle.sign(
|
|
1290
|
-
{ hash: "SHA-256", name: "ECDSA" },
|
|
1291
|
-
privateKey,
|
|
1292
|
-
toBufferSource(message),
|
|
1293
|
-
),
|
|
1294
|
-
);
|
|
1295
|
-
return encodeFipsSignedMessage(signature, message);
|
|
831
|
+
return Promise.resolve(xSign(message, secretKey));
|
|
1296
832
|
}
|
|
1297
833
|
|
|
1298
834
|
/** Generate a fresh Ed25519 signing key pair. */
|
|
@@ -1300,25 +836,9 @@ export function xSignKeyPair(): KeyPair {
|
|
|
1300
836
|
return provider().signKeyPair();
|
|
1301
837
|
}
|
|
1302
838
|
|
|
1303
|
-
/** Async keypair generation
|
|
1304
|
-
export
|
|
1305
|
-
|
|
1306
|
-
return xSignKeyPair();
|
|
1307
|
-
}
|
|
1308
|
-
const subtle = getSubtleCrypto();
|
|
1309
|
-
const pair = await subtle.generateKey(
|
|
1310
|
-
{ name: "ECDSA", namedCurve: "P-256" },
|
|
1311
|
-
true,
|
|
1312
|
-
["sign", "verify"],
|
|
1313
|
-
);
|
|
1314
|
-
return {
|
|
1315
|
-
publicKey: new Uint8Array(
|
|
1316
|
-
await subtle.exportKey("spki", pair.publicKey),
|
|
1317
|
-
),
|
|
1318
|
-
secretKey: new Uint8Array(
|
|
1319
|
-
await subtle.exportKey("pkcs8", pair.privateKey),
|
|
1320
|
-
),
|
|
1321
|
-
};
|
|
839
|
+
/** Async Ed25519 keypair generation. */
|
|
840
|
+
export function xSignKeyPairAsync(): Promise<KeyPair> {
|
|
841
|
+
return Promise.resolve(xSignKeyPair());
|
|
1322
842
|
}
|
|
1323
843
|
|
|
1324
844
|
/** Restore an Ed25519 signing key pair from a 64-byte secret key. */
|
|
@@ -1326,14 +846,11 @@ export function xSignKeyPairFromSecret(secretKey: Uint8Array): KeyPair {
|
|
|
1326
846
|
return provider().signKeyPairFromSecret(secretKey);
|
|
1327
847
|
}
|
|
1328
848
|
|
|
1329
|
-
/** Async restore of signing keypair
|
|
1330
|
-
export
|
|
849
|
+
/** Async restore of an Ed25519 signing keypair. */
|
|
850
|
+
export function xSignKeyPairFromSecretAsync(
|
|
1331
851
|
secretKey: Uint8Array,
|
|
1332
852
|
): Promise<KeyPair> {
|
|
1333
|
-
|
|
1334
|
-
return xSignKeyPairFromSecret(secretKey);
|
|
1335
|
-
}
|
|
1336
|
-
return fipsEcdsaKeyPairFromPkcs8(secretKey, getSubtleCrypto());
|
|
853
|
+
return Promise.resolve(xSignKeyPairFromSecret(secretKey));
|
|
1337
854
|
}
|
|
1338
855
|
|
|
1339
856
|
/** Verify and open a signed message. Returns the original message, or null if verification fails. */
|
|
@@ -1344,101 +861,12 @@ export function xSignOpen(
|
|
|
1344
861
|
return provider().signOpen(signedMessage, publicKey);
|
|
1345
862
|
}
|
|
1346
863
|
|
|
1347
|
-
/** Async verify/open
|
|
1348
|
-
export
|
|
864
|
+
/** Async Ed25519 verify/open. */
|
|
865
|
+
export function xSignOpenAsync(
|
|
1349
866
|
signedMessage: Uint8Array,
|
|
1350
867
|
publicKey: Uint8Array,
|
|
1351
868
|
): Promise<null | Uint8Array> {
|
|
1352
|
-
|
|
1353
|
-
return xSignOpen(signedMessage, publicKey);
|
|
1354
|
-
}
|
|
1355
|
-
const subtle = getSubtleCrypto();
|
|
1356
|
-
const parsed = decodeFipsSignedMessage(signedMessage);
|
|
1357
|
-
const verifyKey = await importEcdsaPublicKey(publicKey);
|
|
1358
|
-
const valid = await subtle.verify(
|
|
1359
|
-
{ hash: "SHA-256", name: "ECDSA" },
|
|
1360
|
-
verifyKey,
|
|
1361
|
-
toBufferSource(parsed.signature),
|
|
1362
|
-
toBufferSource(parsed.message),
|
|
1363
|
-
);
|
|
1364
|
-
return valid ? parsed.message : null;
|
|
1365
|
-
}
|
|
1366
|
-
|
|
1367
|
-
async function fipsEcdsaKeyPairFromPkcs8(
|
|
1368
|
-
secretKey: Uint8Array,
|
|
1369
|
-
subtle: SubtleCrypto,
|
|
1370
|
-
): Promise<KeyPair> {
|
|
1371
|
-
const ecdsaPriv = await importEcdsaPrivateKey(secretKey);
|
|
1372
|
-
const jwk = await subtle.exportKey("jwk", ecdsaPriv);
|
|
1373
|
-
const ecdsaPubJwk: JsonWebKey = { ...jwk };
|
|
1374
|
-
delete ecdsaPubJwk.d;
|
|
1375
|
-
ecdsaPubJwk.key_ops = ["verify"];
|
|
1376
|
-
const ecdsaPub = await subtle.importKey(
|
|
1377
|
-
"jwk",
|
|
1378
|
-
ecdsaPubJwk,
|
|
1379
|
-
{ name: "ECDSA", namedCurve: "P-256" },
|
|
1380
|
-
true,
|
|
1381
|
-
["verify"],
|
|
1382
|
-
);
|
|
1383
|
-
return {
|
|
1384
|
-
publicKey: new Uint8Array(await subtle.exportKey("spki", ecdsaPub)),
|
|
1385
|
-
secretKey: Uint8Array.from(secretKey),
|
|
1386
|
-
};
|
|
1387
|
-
}
|
|
1388
|
-
|
|
1389
|
-
async function importAesKey(
|
|
1390
|
-
key: Uint8Array,
|
|
1391
|
-
usages: WebCryptoKeyUsage[],
|
|
1392
|
-
): Promise<CryptoKey> {
|
|
1393
|
-
return getSubtleCrypto().importKey(
|
|
1394
|
-
"raw",
|
|
1395
|
-
toBufferSource(key),
|
|
1396
|
-
{ name: "AES-GCM" },
|
|
1397
|
-
false,
|
|
1398
|
-
usages,
|
|
1399
|
-
);
|
|
1400
|
-
}
|
|
1401
|
-
|
|
1402
|
-
async function importEcdhPrivateKey(secretKey: Uint8Array): Promise<CryptoKey> {
|
|
1403
|
-
return getSubtleCrypto().importKey(
|
|
1404
|
-
"pkcs8",
|
|
1405
|
-
toBufferSource(secretKey),
|
|
1406
|
-
{ name: "ECDH", namedCurve: "P-256" },
|
|
1407
|
-
true,
|
|
1408
|
-
["deriveBits"],
|
|
1409
|
-
);
|
|
1410
|
-
}
|
|
1411
|
-
|
|
1412
|
-
async function importEcdhPublicKey(publicKey: Uint8Array): Promise<CryptoKey> {
|
|
1413
|
-
return getSubtleCrypto().importKey(
|
|
1414
|
-
"raw",
|
|
1415
|
-
toBufferSource(publicKey),
|
|
1416
|
-
{ name: "ECDH", namedCurve: "P-256" },
|
|
1417
|
-
true,
|
|
1418
|
-
[],
|
|
1419
|
-
);
|
|
1420
|
-
}
|
|
1421
|
-
|
|
1422
|
-
async function importEcdsaPrivateKey(
|
|
1423
|
-
secretKey: Uint8Array,
|
|
1424
|
-
): Promise<CryptoKey> {
|
|
1425
|
-
return getSubtleCrypto().importKey(
|
|
1426
|
-
"pkcs8",
|
|
1427
|
-
toBufferSource(secretKey),
|
|
1428
|
-
{ name: "ECDSA", namedCurve: "P-256" },
|
|
1429
|
-
true,
|
|
1430
|
-
["sign"],
|
|
1431
|
-
);
|
|
1432
|
-
}
|
|
1433
|
-
|
|
1434
|
-
async function importEcdsaPublicKey(publicKey: Uint8Array): Promise<CryptoKey> {
|
|
1435
|
-
return getSubtleCrypto().importKey(
|
|
1436
|
-
"spki",
|
|
1437
|
-
toBufferSource(publicKey),
|
|
1438
|
-
{ name: "ECDSA", namedCurve: "P-256" },
|
|
1439
|
-
true,
|
|
1440
|
-
["verify"],
|
|
1441
|
-
);
|
|
869
|
+
return Promise.resolve(xSignOpen(signedMessage, publicKey));
|
|
1442
870
|
}
|
|
1443
871
|
|
|
1444
872
|
/**
|