@vex-chat/crypto 10.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 +43 -80
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +160 -450
- package/dist/index.js.map +1 -1
- package/package.json +3 -4
- package/src/__tests__/XUtils/stringEncoding.ts +6 -0
- package/src/__tests__/xAsyncApi.extended.test.ts +32 -35
- package/src/__tests__/xAsyncProfile.ts +6 -26
- package/src/__tests__/xHMAC.ts +12 -1
- package/src/index.ts +230 -677
- package/src/__tests__/cryptoProfile.ts +0 -64
- 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";
|
|
@@ -29,14 +27,16 @@ import { Packr } from "msgpackr";
|
|
|
29
27
|
import nacl from "tweetnacl";
|
|
30
28
|
import { z } from "zod/v4";
|
|
31
29
|
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
const KEY_DATA_HEADER_BYTES = 54;
|
|
31
|
+
const KEY_DATA_MAC_BYTES = 16;
|
|
32
|
+
const KEY_DATA_PBKDF2_ITERATIONS = 220_000;
|
|
33
|
+
const KEY_DATA_PBKDF2_MIN_ITERATIONS = 1_000;
|
|
34
|
+
const KEY_DATA_PBKDF2_MAX_ITERATIONS = 2_000_000;
|
|
34
35
|
|
|
35
36
|
interface CryptoProvider {
|
|
36
37
|
boxBefore(myPrivateKey: Uint8Array, theirPublicKey: Uint8Array): Uint8Array;
|
|
37
38
|
boxKeyPair(): KeyPair;
|
|
38
39
|
boxKeyPairFromSecret(secretKey: Uint8Array): KeyPair;
|
|
39
|
-
profile: CryptoProfile;
|
|
40
40
|
randomBytes(length: number): Uint8Array;
|
|
41
41
|
secretbox(
|
|
42
42
|
plaintext: Uint8Array,
|
|
@@ -57,31 +57,11 @@ interface CryptoProvider {
|
|
|
57
57
|
): null | Uint8Array;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
type WebCryptoKeyUsage =
|
|
61
|
-
| "decrypt"
|
|
62
|
-
| "deriveBits"
|
|
63
|
-
| "deriveKey"
|
|
64
|
-
| "encrypt"
|
|
65
|
-
| "sign"
|
|
66
|
-
| "unwrapKey"
|
|
67
|
-
| "verify"
|
|
68
|
-
| "wrapKey";
|
|
69
|
-
|
|
70
60
|
interface WebCryptoLike {
|
|
71
61
|
getRandomValues: Crypto["getRandomValues"];
|
|
72
62
|
subtle: SubtleCrypto;
|
|
73
63
|
}
|
|
74
64
|
|
|
75
|
-
function getWebCrypto(): WebCryptoLike {
|
|
76
|
-
const cryptoCandidate: unknown = globalThis.crypto;
|
|
77
|
-
if (!isWebCryptoLike(cryptoCandidate)) {
|
|
78
|
-
throw new Error(
|
|
79
|
-
"Web Crypto API is not available in this runtime for fips profile operations.",
|
|
80
|
-
);
|
|
81
|
-
}
|
|
82
|
-
return cryptoCandidate;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
65
|
function isWebCryptoLike(value: unknown): value is WebCryptoLike {
|
|
86
66
|
if (typeof value !== "object" || value === null) {
|
|
87
67
|
return false;
|
|
@@ -95,28 +75,37 @@ function isWebCryptoLike(value: unknown): value is WebCryptoLike {
|
|
|
95
75
|
);
|
|
96
76
|
}
|
|
97
77
|
|
|
98
|
-
function
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const chunk = result.subarray(offset, offset + chunkLength);
|
|
110
|
-
cryptoImpl.getRandomValues(chunk);
|
|
111
|
-
offset += chunkLength;
|
|
78
|
+
async function pbkdf2Sha512Async(
|
|
79
|
+
password: string,
|
|
80
|
+
salt: Uint8Array,
|
|
81
|
+
iterations: number,
|
|
82
|
+
): Promise<Uint8Array> {
|
|
83
|
+
const cryptoCandidate: unknown = globalThis.crypto;
|
|
84
|
+
if (!isWebCryptoLike(cryptoCandidate)) {
|
|
85
|
+
return noblePbkdf2(sha512, password, salt, {
|
|
86
|
+
c: iterations,
|
|
87
|
+
dkLen: 32,
|
|
88
|
+
});
|
|
112
89
|
}
|
|
113
|
-
return result;
|
|
114
|
-
}
|
|
115
90
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
91
|
+
const passwordKey = await cryptoCandidate.subtle.importKey(
|
|
92
|
+
"raw",
|
|
93
|
+
new TextEncoder().encode(password),
|
|
94
|
+
"PBKDF2",
|
|
95
|
+
false,
|
|
96
|
+
["deriveBits"],
|
|
119
97
|
);
|
|
98
|
+
const bits = await cryptoCandidate.subtle.deriveBits(
|
|
99
|
+
{
|
|
100
|
+
hash: "SHA-512",
|
|
101
|
+
iterations,
|
|
102
|
+
name: "PBKDF2",
|
|
103
|
+
salt: new Uint8Array(salt),
|
|
104
|
+
},
|
|
105
|
+
passwordKey,
|
|
106
|
+
256,
|
|
107
|
+
);
|
|
108
|
+
return new Uint8Array(bits);
|
|
120
109
|
}
|
|
121
110
|
|
|
122
111
|
const tweetnaclProvider: CryptoProvider = {
|
|
@@ -125,7 +114,6 @@ const tweetnaclProvider: CryptoProvider = {
|
|
|
125
114
|
boxKeyPair: () => nacl.box.keyPair(),
|
|
126
115
|
boxKeyPairFromSecret: (secretKey) =>
|
|
127
116
|
nacl.box.keyPair.fromSecretKey(secretKey),
|
|
128
|
-
profile: "tweetnacl",
|
|
129
117
|
randomBytes: (length) => nacl.randomBytes(length),
|
|
130
118
|
secretbox: (plaintext, nonce, key) => nacl.secretbox(plaintext, nonce, key),
|
|
131
119
|
secretboxOpen: (ciphertext, nonce, key) =>
|
|
@@ -138,246 +126,8 @@ const tweetnaclProvider: CryptoProvider = {
|
|
|
138
126
|
nacl.sign.open(signedMessage, publicKey),
|
|
139
127
|
};
|
|
140
128
|
|
|
141
|
-
const fipsProvider: CryptoProvider = {
|
|
142
|
-
boxBefore: (myPrivateKey, theirPublicKey) => {
|
|
143
|
-
void myPrivateKey;
|
|
144
|
-
void theirPublicKey;
|
|
145
|
-
return unsupported("fips", "xDH");
|
|
146
|
-
},
|
|
147
|
-
boxKeyPair: () => unsupported("fips", "xBoxKeyPair"),
|
|
148
|
-
boxKeyPairFromSecret: (secretKey) => {
|
|
149
|
-
void secretKey;
|
|
150
|
-
return unsupported("fips", "xBoxKeyPairFromSecret");
|
|
151
|
-
},
|
|
152
|
-
profile: "fips",
|
|
153
|
-
randomBytes: (length) => randomBytesWebCrypto(length),
|
|
154
|
-
secretbox: (plaintext, nonce, key) => {
|
|
155
|
-
void plaintext;
|
|
156
|
-
void nonce;
|
|
157
|
-
void key;
|
|
158
|
-
return unsupported("fips", "xSecretbox");
|
|
159
|
-
},
|
|
160
|
-
secretboxOpen: (ciphertext, nonce, key) => {
|
|
161
|
-
void ciphertext;
|
|
162
|
-
void nonce;
|
|
163
|
-
void key;
|
|
164
|
-
return unsupported("fips", "xSecretboxOpen");
|
|
165
|
-
},
|
|
166
|
-
sign: (message, secretKey) => {
|
|
167
|
-
void message;
|
|
168
|
-
void secretKey;
|
|
169
|
-
return unsupported("fips", "xSign");
|
|
170
|
-
},
|
|
171
|
-
signKeyPair: () => unsupported("fips", "xSignKeyPair"),
|
|
172
|
-
signKeyPairFromSecret: (secretKey) => {
|
|
173
|
-
void secretKey;
|
|
174
|
-
return unsupported("fips", "xSignKeyPairFromSecret");
|
|
175
|
-
},
|
|
176
|
-
signOpen: (signedMessage, publicKey) => {
|
|
177
|
-
void signedMessage;
|
|
178
|
-
void publicKey;
|
|
179
|
-
return unsupported("fips", "xSignOpen");
|
|
180
|
-
},
|
|
181
|
-
};
|
|
182
|
-
|
|
183
|
-
const providers: Record<CryptoProfile, CryptoProvider> = {
|
|
184
|
-
fips: fipsProvider,
|
|
185
|
-
tweetnacl: tweetnaclProvider,
|
|
186
|
-
};
|
|
187
|
-
|
|
188
|
-
let activeCryptoProfile: CryptoProfile = "tweetnacl";
|
|
189
|
-
let activeCryptoProvider: CryptoProvider = providers[activeCryptoProfile];
|
|
190
|
-
|
|
191
|
-
/** Returns the currently configured crypto profile. */
|
|
192
|
-
export function getCryptoProfile(): CryptoProfile {
|
|
193
|
-
return activeCryptoProfile;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
/**
|
|
197
|
-
* Sets the runtime crypto profile.
|
|
198
|
-
*
|
|
199
|
-
* `tweetnacl` preserves existing behavior.
|
|
200
|
-
* `fips` currently enables only backend-agnostic helpers; NaCl-coupled
|
|
201
|
-
* primitives throw until a FIPS backend implementation is wired in.
|
|
202
|
-
*/
|
|
203
|
-
export function setCryptoProfile(profile: CryptoProfile): void {
|
|
204
|
-
activeCryptoProfile = profile;
|
|
205
|
-
activeCryptoProvider = providers[profile];
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
const cryptoProfileScopeStack: CryptoProfile[] = [];
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* Saves the current profile and switches to `profile`. Pair every call with
|
|
212
|
-
* {@link leaveCryptoProfileScope} in a `finally` block.
|
|
213
|
-
*
|
|
214
|
-
* **Why:** `setCryptoProfile` is process-wide. Several async libvex `Client`s
|
|
215
|
-
* can overlap on `readMail`; a naive save/restore in `finally` can reset the
|
|
216
|
-
* profile while another client still needs FIPS — `xSecretboxOpenAsync` then
|
|
217
|
-
* reads `tweetnacl` and fails to decrypt AES-GCM payloads. Nesting this stack
|
|
218
|
-
* fixes that.
|
|
219
|
-
*/
|
|
220
|
-
export function enterCryptoProfileScope(profile: CryptoProfile): void {
|
|
221
|
-
cryptoProfileScopeStack.push(activeCryptoProfile);
|
|
222
|
-
setCryptoProfile(profile);
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
/** Restores the profile saved by the innermost {@link enterCryptoProfileScope}. */
|
|
226
|
-
export function leaveCryptoProfileScope(): void {
|
|
227
|
-
const prev = cryptoProfileScopeStack.pop();
|
|
228
|
-
if (prev === undefined) {
|
|
229
|
-
throw new Error(
|
|
230
|
-
"leaveCryptoProfileScope called without a matching enterCryptoProfileScope",
|
|
231
|
-
);
|
|
232
|
-
}
|
|
233
|
-
setCryptoProfile(prev);
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
function bytesToBase64Url(bytes: Uint8Array): string {
|
|
237
|
-
return globalThis
|
|
238
|
-
.btoa(String.fromCodePoint(...Array.from(bytes)))
|
|
239
|
-
.replace(/\+/g, "-")
|
|
240
|
-
.replace(/\//g, "_")
|
|
241
|
-
.replace(/=+/g, "");
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
function concatBytes(a: Uint8Array, b: Uint8Array): Uint8Array {
|
|
245
|
-
const out = new Uint8Array(a.length + b.length);
|
|
246
|
-
out.set(a, 0);
|
|
247
|
-
out.set(b, a.length);
|
|
248
|
-
return out;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
function decodeFipsSignedMessage(signedMessage: Uint8Array): {
|
|
252
|
-
message: Uint8Array;
|
|
253
|
-
signature: Uint8Array;
|
|
254
|
-
} {
|
|
255
|
-
if (signedMessage.length < 2) {
|
|
256
|
-
throw new Error(
|
|
257
|
-
"Invalid FIPS signed message: missing signature length prefix.",
|
|
258
|
-
);
|
|
259
|
-
}
|
|
260
|
-
const signatureLength =
|
|
261
|
-
(signedMessage[0] ?? 0) * 256 + (signedMessage[1] ?? 0);
|
|
262
|
-
const signatureStart = 2;
|
|
263
|
-
const signatureEnd = signatureStart + signatureLength;
|
|
264
|
-
if (signedMessage.length < signatureEnd) {
|
|
265
|
-
throw new Error(
|
|
266
|
-
"Invalid FIPS signed message: signature length exceeds message length.",
|
|
267
|
-
);
|
|
268
|
-
}
|
|
269
|
-
return {
|
|
270
|
-
message: signedMessage.slice(signatureEnd),
|
|
271
|
-
signature: signedMessage.slice(signatureStart, signatureEnd),
|
|
272
|
-
};
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
function encodeFipsSignedMessage(
|
|
276
|
-
signature: Uint8Array,
|
|
277
|
-
message: Uint8Array,
|
|
278
|
-
): Uint8Array {
|
|
279
|
-
if (signature.length > 65535) {
|
|
280
|
-
throw new Error("FIPS signature too long to encode.");
|
|
281
|
-
}
|
|
282
|
-
const prefix = new Uint8Array(2);
|
|
283
|
-
prefix[0] = (signature.length >> 8) & 0xff;
|
|
284
|
-
prefix[1] = signature.length & 0xff;
|
|
285
|
-
return concatBytes(concatBytes(prefix, signature), message);
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
async function fipsEcdhKeyPairFrom32ByteSeed(
|
|
289
|
-
secretKey: Uint8Array,
|
|
290
|
-
subtle: SubtleCrypto,
|
|
291
|
-
): Promise<KeyPair> {
|
|
292
|
-
if (secretKey.length !== 32) {
|
|
293
|
-
throw new Error(
|
|
294
|
-
"FIPS: expected a 32-byte IKM/seed for ECDH from-secret.",
|
|
295
|
-
);
|
|
296
|
-
}
|
|
297
|
-
const d = p256.utils.normPrivateKeyToScalar(Uint8Array.from(secretKey));
|
|
298
|
-
const d32 = numberToBytesBE(d, 32);
|
|
299
|
-
const rawPub = p256.getPublicKey(d, false);
|
|
300
|
-
if (rawPub[0] !== 0x04) {
|
|
301
|
-
throw new Error("FIPS: expected uncompressed P-256 public key.");
|
|
302
|
-
}
|
|
303
|
-
const jwk: JsonWebKey = {
|
|
304
|
-
crv: "P-256",
|
|
305
|
-
d: bytesToBase64Url(d32),
|
|
306
|
-
kty: "EC",
|
|
307
|
-
x: bytesToBase64Url(rawPub.subarray(1, 33)),
|
|
308
|
-
y: bytesToBase64Url(rawPub.subarray(33, 65)),
|
|
309
|
-
};
|
|
310
|
-
const ecdhPriv = await subtle.importKey(
|
|
311
|
-
"jwk",
|
|
312
|
-
jwk,
|
|
313
|
-
{ name: "ECDH", namedCurve: "P-256" },
|
|
314
|
-
true,
|
|
315
|
-
["deriveBits"],
|
|
316
|
-
);
|
|
317
|
-
const ecdhPubJwk = await subtle.exportKey("jwk", ecdhPriv);
|
|
318
|
-
const ecdhPubJwkNoD: JsonWebKey = { ...ecdhPubJwk };
|
|
319
|
-
delete ecdhPubJwkNoD.d;
|
|
320
|
-
ecdhPubJwkNoD.key_ops = [];
|
|
321
|
-
const ecdhPub = await subtle.importKey(
|
|
322
|
-
"jwk",
|
|
323
|
-
ecdhPubJwkNoD,
|
|
324
|
-
{ name: "ECDH", namedCurve: "P-256" },
|
|
325
|
-
true,
|
|
326
|
-
[],
|
|
327
|
-
);
|
|
328
|
-
return {
|
|
329
|
-
publicKey: new Uint8Array(await subtle.exportKey("raw", ecdhPub)),
|
|
330
|
-
secretKey: new Uint8Array(await subtle.exportKey("pkcs8", ecdhPriv)),
|
|
331
|
-
};
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
async function fipsEcdhKeyPairFromPkcs8(
|
|
335
|
-
secretKey: Uint8Array,
|
|
336
|
-
subtle: SubtleCrypto,
|
|
337
|
-
): Promise<KeyPair> {
|
|
338
|
-
const ecdhPriv = await subtle.importKey(
|
|
339
|
-
"pkcs8",
|
|
340
|
-
toBufferSource(secretKey),
|
|
341
|
-
{ name: "ECDH", namedCurve: "P-256" },
|
|
342
|
-
true,
|
|
343
|
-
["deriveBits"],
|
|
344
|
-
);
|
|
345
|
-
const jwk = await subtle.exportKey("jwk", ecdhPriv);
|
|
346
|
-
const ecdhPubJwk: JsonWebKey = { ...jwk };
|
|
347
|
-
delete ecdhPubJwk.d;
|
|
348
|
-
ecdhPubJwk.key_ops = [];
|
|
349
|
-
const ecdhPub = await subtle.importKey(
|
|
350
|
-
"jwk",
|
|
351
|
-
ecdhPubJwk,
|
|
352
|
-
{ name: "ECDH", namedCurve: "P-256" },
|
|
353
|
-
true,
|
|
354
|
-
[],
|
|
355
|
-
);
|
|
356
|
-
return {
|
|
357
|
-
publicKey: new Uint8Array(await subtle.exportKey("raw", ecdhPub)),
|
|
358
|
-
secretKey: new Uint8Array(await subtle.exportKey("pkcs8", ecdhPriv)),
|
|
359
|
-
};
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
function getSubtleCrypto(): SubtleCrypto {
|
|
363
|
-
return getWebCrypto().subtle;
|
|
364
|
-
}
|
|
365
|
-
|
|
366
129
|
function provider(): CryptoProvider {
|
|
367
|
-
return
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
function requireAesGcmNonce(nonce: Uint8Array): Uint8Array {
|
|
371
|
-
if (nonce.length < 12) {
|
|
372
|
-
throw new Error(
|
|
373
|
-
`AES-GCM requires a nonce of at least 12 bytes, received ${String(nonce.length)}.`,
|
|
374
|
-
);
|
|
375
|
-
}
|
|
376
|
-
return nonce.slice(0, 12);
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
function toBufferSource(bytes: Uint8Array): ArrayBuffer {
|
|
380
|
-
return Uint8Array.from(bytes).buffer;
|
|
130
|
+
return tweetnaclProvider;
|
|
381
131
|
}
|
|
382
132
|
|
|
383
133
|
// msgpackr with useRecords:false emits standard msgpack (no nonstandard record extension).
|
|
@@ -411,8 +161,8 @@ export class XUtils {
|
|
|
411
161
|
* Checks if two buffer-like objects are equal.
|
|
412
162
|
* When lengths match, comparison is constant-time in the inputs (no early exit on first differing byte).
|
|
413
163
|
*
|
|
414
|
-
* @param buf1
|
|
415
|
-
* @param buf2
|
|
164
|
+
* @param buf1 - First buffer to compare.
|
|
165
|
+
* @param buf2 - Second buffer to compare.
|
|
416
166
|
*
|
|
417
167
|
* @returns True if equal, else false.
|
|
418
168
|
*/
|
|
@@ -446,24 +196,30 @@ export class XUtils {
|
|
|
446
196
|
if (hexString.length === 0) {
|
|
447
197
|
return new Uint8Array();
|
|
448
198
|
}
|
|
199
|
+
if (hexString.length % 2 !== 0 || !/^[0-9a-fA-F]+$/.test(hexString)) {
|
|
200
|
+
throw new Error("Expected an even-length hexadecimal string.");
|
|
201
|
+
}
|
|
449
202
|
|
|
450
|
-
const
|
|
451
|
-
|
|
203
|
+
const bytes = new Uint8Array(hexString.length / 2);
|
|
204
|
+
for (let i = 0; i < bytes.length; i += 1) {
|
|
205
|
+
bytes[i] = Number.parseInt(hexString.slice(i * 2, i * 2 + 2), 16);
|
|
206
|
+
}
|
|
207
|
+
return bytes;
|
|
452
208
|
}
|
|
453
209
|
|
|
454
210
|
/**
|
|
455
211
|
* Decrypts a secret key from the binary format produced by encryptKeyData().
|
|
456
212
|
* No I/O — the caller handles reading the data.
|
|
457
213
|
*
|
|
458
|
-
* @param keyData The encrypted key data as a Uint8Array.
|
|
459
|
-
* @param password The password used to encrypt.
|
|
214
|
+
* @param keyData - The encrypted key data as a Uint8Array.
|
|
215
|
+
* @param password - The password used to encrypt.
|
|
460
216
|
* @returns The hex-encoded secret key.
|
|
461
217
|
*/
|
|
462
218
|
public static decryptKeyData = (
|
|
463
219
|
keyData: Uint8Array,
|
|
464
220
|
password: string,
|
|
465
221
|
): string => {
|
|
466
|
-
const ITERATIONS = XUtils.
|
|
222
|
+
const ITERATIONS = XUtils.readKeyDataIterations(keyData);
|
|
467
223
|
const PKBDF_SALT = keyData.slice(6, 30);
|
|
468
224
|
const ENCRYPTION_NONCE = keyData.slice(30, 54);
|
|
469
225
|
const ENCRYPTED_KEY = keyData.slice(54);
|
|
@@ -471,73 +227,70 @@ export class XUtils {
|
|
|
471
227
|
c: ITERATIONS,
|
|
472
228
|
dkLen: 32,
|
|
473
229
|
});
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
230
|
+
try {
|
|
231
|
+
const DECRYPTED_SIGNKEY = provider().secretboxOpen(
|
|
232
|
+
ENCRYPTED_KEY,
|
|
233
|
+
ENCRYPTION_NONCE,
|
|
234
|
+
DERIVED_KEY,
|
|
235
|
+
);
|
|
479
236
|
|
|
480
|
-
|
|
481
|
-
|
|
237
|
+
if (DECRYPTED_SIGNKEY === null) {
|
|
238
|
+
throw new Error("Decryption failed. Wrong password?");
|
|
239
|
+
}
|
|
240
|
+
const decryptedHex = XUtils.encodeHex(DECRYPTED_SIGNKEY);
|
|
241
|
+
DECRYPTED_SIGNKEY.fill(0);
|
|
242
|
+
return decryptedHex;
|
|
243
|
+
} finally {
|
|
244
|
+
DERIVED_KEY.fill(0);
|
|
482
245
|
}
|
|
483
|
-
return XUtils.encodeHex(DECRYPTED_SIGNKEY);
|
|
484
246
|
};
|
|
485
247
|
|
|
486
|
-
/**
|
|
487
|
-
* Async variant of decryptKeyData for cross-runtime/FIPS backends.
|
|
488
|
-
* Supports both profile formats emitted by encryptKeyDataAsync.
|
|
489
|
-
*/
|
|
248
|
+
/** Async variant of decryptKeyData for cross-runtime callers. */
|
|
490
249
|
public static decryptKeyDataAsync = async (
|
|
491
250
|
keyData: Uint8Array,
|
|
492
251
|
password: string,
|
|
493
252
|
): Promise<string> => {
|
|
494
|
-
const ITERATIONS = XUtils.
|
|
253
|
+
const ITERATIONS = XUtils.readKeyDataIterations(keyData);
|
|
495
254
|
const PKBDF_SALT = keyData.slice(6, 30);
|
|
496
255
|
const ENCRYPTION_NONCE = keyData.slice(30, 54);
|
|
497
256
|
const ENCRYPTED_KEY = keyData.slice(54);
|
|
498
|
-
const DERIVED_KEY =
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
257
|
+
const DERIVED_KEY = await pbkdf2Sha512Async(
|
|
258
|
+
password,
|
|
259
|
+
PKBDF_SALT,
|
|
260
|
+
ITERATIONS,
|
|
261
|
+
);
|
|
262
|
+
try {
|
|
263
|
+
const decrypted = xSecretboxOpen(
|
|
264
|
+
ENCRYPTED_KEY,
|
|
265
|
+
ENCRYPTION_NONCE,
|
|
266
|
+
DERIVED_KEY,
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
if (decrypted === null) {
|
|
270
|
+
throw new Error("Decryption failed. Wrong password?");
|
|
271
|
+
}
|
|
272
|
+
const decryptedHex = XUtils.encodeHex(decrypted);
|
|
273
|
+
decrypted.fill(0);
|
|
274
|
+
return decryptedHex;
|
|
275
|
+
} finally {
|
|
276
|
+
DERIVED_KEY.fill(0);
|
|
513
277
|
}
|
|
514
|
-
return XUtils.encodeHex(decrypted);
|
|
515
278
|
};
|
|
516
279
|
|
|
517
280
|
/**
|
|
518
|
-
* 32-byte
|
|
519
|
-
*
|
|
520
|
-
* For `fips` the identity secret is PKCS#8; HKDF is applied so AES keys never
|
|
521
|
-
* equal the raw private key material.
|
|
281
|
+
* Derive a purpose-separated 32-byte key for local at-rest encryption.
|
|
282
|
+
* The result never aliases raw identity-key bytes.
|
|
522
283
|
*/
|
|
523
|
-
public static deriveLocalAtRestAesKey(
|
|
524
|
-
identitySk
|
|
525
|
-
|
|
526
|
-
): Uint8Array {
|
|
527
|
-
if (profile === "tweetnacl") {
|
|
528
|
-
if (identitySk.length < 32) {
|
|
529
|
-
throw new Error(
|
|
530
|
-
"Expected at least 32 bytes of identity secret in tweetnacl mode.",
|
|
531
|
-
);
|
|
532
|
-
}
|
|
533
|
-
return identitySk.subarray(0, 32);
|
|
284
|
+
public static deriveLocalAtRestAesKey(identitySk: Uint8Array): Uint8Array {
|
|
285
|
+
if (identitySk.length < 32) {
|
|
286
|
+
throw new Error("Expected at least 32 bytes of identity secret.");
|
|
534
287
|
}
|
|
535
288
|
return new Uint8Array(
|
|
536
289
|
hkdf(
|
|
537
290
|
sha256,
|
|
538
291
|
identitySk,
|
|
539
292
|
new Uint8Array(0),
|
|
540
|
-
new TextEncoder().encode("vex:at-rest:
|
|
293
|
+
new TextEncoder().encode("vex:at-rest:3:tweetnacl"),
|
|
541
294
|
32,
|
|
542
295
|
),
|
|
543
296
|
);
|
|
@@ -571,9 +324,9 @@ export class XUtils {
|
|
|
571
324
|
*
|
|
572
325
|
* Format: [iterations(6)|salt(24)|nonce(24)|ciphertext(N)]
|
|
573
326
|
*
|
|
574
|
-
* @param password The password to derive the encryption key from.
|
|
575
|
-
* @param keyToSave The hex-encoded secret key to encrypt.
|
|
576
|
-
* @param iterationOverride Optional PBKDF2 iteration count (
|
|
327
|
+
* @param password - The password to derive the encryption key from.
|
|
328
|
+
* @param keyToSave - The hex-encoded secret key to encrypt.
|
|
329
|
+
* @param iterationOverride - Optional PBKDF2 iteration count (220,000 if omitted).
|
|
577
330
|
* @returns The encrypted key data as a Uint8Array.
|
|
578
331
|
*/
|
|
579
332
|
public static encryptKeyData = (
|
|
@@ -582,13 +335,9 @@ export class XUtils {
|
|
|
582
335
|
iterationOverride?: number,
|
|
583
336
|
): Uint8Array => {
|
|
584
337
|
const UNENCRYPTED_SIGNKEY = XUtils.decodeHex(keyToSave);
|
|
585
|
-
const
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
const iterations =
|
|
589
|
-
iterationOverride !== undefined && iterationOverride !== 0
|
|
590
|
-
? iterationOverride
|
|
591
|
-
: N1 * N2 + OFFSET;
|
|
338
|
+
const iterations = XUtils.validateKeyDataIterations(
|
|
339
|
+
iterationOverride ?? KEY_DATA_PBKDF2_ITERATIONS,
|
|
340
|
+
);
|
|
592
341
|
const ITERATIONS = XUtils.numberToUint8Arr(iterations);
|
|
593
342
|
const PKBDF_SALT = xMakeNonce();
|
|
594
343
|
const ENCRYPTION_KEY = noblePbkdf2(sha512, password, PKBDF_SALT, {
|
|
@@ -596,11 +345,17 @@ export class XUtils {
|
|
|
596
345
|
dkLen: 32,
|
|
597
346
|
});
|
|
598
347
|
const NONCE = xMakeNonce();
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
348
|
+
let ENCRYPTED_SIGNKEY: Uint8Array;
|
|
349
|
+
try {
|
|
350
|
+
ENCRYPTED_SIGNKEY = provider().secretbox(
|
|
351
|
+
UNENCRYPTED_SIGNKEY,
|
|
352
|
+
NONCE,
|
|
353
|
+
ENCRYPTION_KEY,
|
|
354
|
+
);
|
|
355
|
+
} finally {
|
|
356
|
+
ENCRYPTION_KEY.fill(0);
|
|
357
|
+
UNENCRYPTED_SIGNKEY.fill(0);
|
|
358
|
+
}
|
|
604
359
|
|
|
605
360
|
const result = new Uint8Array(
|
|
606
361
|
ITERATIONS.length +
|
|
@@ -619,38 +374,35 @@ export class XUtils {
|
|
|
619
374
|
return result;
|
|
620
375
|
};
|
|
621
376
|
|
|
622
|
-
/**
|
|
623
|
-
* Async variant of encryptKeyData for cross-runtime/FIPS backends.
|
|
624
|
-
* Format remains [iterations(6)|salt(24)|nonce(24)|ciphertext(N)].
|
|
625
|
-
*/
|
|
377
|
+
/** Async variant of encryptKeyData for cross-runtime callers. */
|
|
626
378
|
public static encryptKeyDataAsync = async (
|
|
627
379
|
password: string,
|
|
628
380
|
keyToSave: string,
|
|
629
381
|
iterationOverride?: number,
|
|
630
382
|
): Promise<Uint8Array> => {
|
|
631
383
|
const UNENCRYPTED_SIGNKEY = XUtils.decodeHex(keyToSave);
|
|
632
|
-
const
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
const iterations =
|
|
636
|
-
iterationOverride !== undefined && iterationOverride !== 0
|
|
637
|
-
? iterationOverride
|
|
638
|
-
: N1 * N2 + OFFSET;
|
|
384
|
+
const iterations = XUtils.validateKeyDataIterations(
|
|
385
|
+
iterationOverride ?? KEY_DATA_PBKDF2_ITERATIONS,
|
|
386
|
+
);
|
|
639
387
|
const ITERATIONS = XUtils.numberToUint8Arr(iterations);
|
|
640
388
|
const PKBDF_SALT = xMakeNonce();
|
|
641
|
-
const ENCRYPTION_KEY =
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
389
|
+
const ENCRYPTION_KEY = await pbkdf2Sha512Async(
|
|
390
|
+
password,
|
|
391
|
+
PKBDF_SALT,
|
|
392
|
+
iterations,
|
|
393
|
+
);
|
|
645
394
|
const NONCE = xMakeNonce();
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
395
|
+
let ENCRYPTED_SIGNKEY: Uint8Array;
|
|
396
|
+
try {
|
|
397
|
+
ENCRYPTED_SIGNKEY = xSecretbox(
|
|
398
|
+
UNENCRYPTED_SIGNKEY,
|
|
399
|
+
NONCE,
|
|
400
|
+
ENCRYPTION_KEY,
|
|
401
|
+
);
|
|
402
|
+
} finally {
|
|
403
|
+
ENCRYPTION_KEY.fill(0);
|
|
404
|
+
UNENCRYPTED_SIGNKEY.fill(0);
|
|
405
|
+
}
|
|
654
406
|
|
|
655
407
|
const result = new Uint8Array(
|
|
656
408
|
ITERATIONS.length +
|
|
@@ -674,7 +426,7 @@ export class XUtils {
|
|
|
674
426
|
* The integer must be positive, and it must be able to be stored
|
|
675
427
|
* in six bytes.
|
|
676
428
|
*
|
|
677
|
-
* @param n The number to convert.
|
|
429
|
+
* @param n - The number to convert.
|
|
678
430
|
* @returns The Uint8Array representation of n.
|
|
679
431
|
*/
|
|
680
432
|
public static numberToUint8Arr(n: number): Uint8Array {
|
|
@@ -695,8 +447,8 @@ export class XUtils {
|
|
|
695
447
|
/**
|
|
696
448
|
* Packs a javascript object and a 32 byte header into a vex message.
|
|
697
449
|
*
|
|
698
|
-
* @param msg Message body (msgpack-serialized).
|
|
699
|
-
* @param header Optional 32-byte header; defaults to an empty header.
|
|
450
|
+
* @param msg - Message body (msgpack-serialized).
|
|
451
|
+
* @param header - Optional 32-byte header; defaults to an empty header.
|
|
700
452
|
* @returns the packed message.
|
|
701
453
|
*/
|
|
702
454
|
public static packMessage(msg: unknown, header?: Uint8Array) {
|
|
@@ -708,7 +460,7 @@ export class XUtils {
|
|
|
708
460
|
/**
|
|
709
461
|
* Converts a Uint8Array representation of an integer back into a number.
|
|
710
462
|
*
|
|
711
|
-
* @param arr The array to convert.
|
|
463
|
+
* @param arr - The array to convert.
|
|
712
464
|
* @returns the number representation of arr.
|
|
713
465
|
*/
|
|
714
466
|
public static uint8ArrToNumber(arr: Uint8Array) {
|
|
@@ -723,7 +475,7 @@ export class XUtils {
|
|
|
723
475
|
* Takes a vex message and unpacks it into its header and a javascript object
|
|
724
476
|
* representation of its body.
|
|
725
477
|
*
|
|
726
|
-
* @param msg Full wire message (32-byte header + msgpack body).
|
|
478
|
+
* @param msg - Full wire message (32-byte header + msgpack body).
|
|
727
479
|
* @returns [32 byte header, message body]
|
|
728
480
|
*/
|
|
729
481
|
public static unpackMessage(
|
|
@@ -743,29 +495,93 @@ export class XUtils {
|
|
|
743
495
|
|
|
744
496
|
return [msgh, msgb];
|
|
745
497
|
}
|
|
498
|
+
|
|
499
|
+
private static readKeyDataIterations(keyData: Uint8Array): number {
|
|
500
|
+
if (keyData.length < KEY_DATA_HEADER_BYTES + KEY_DATA_MAC_BYTES) {
|
|
501
|
+
throw new Error("Encrypted key data is truncated.");
|
|
502
|
+
}
|
|
503
|
+
return XUtils.validateKeyDataIterations(
|
|
504
|
+
XUtils.uint8ArrToNumber(keyData.slice(0, 6)),
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
private static validateKeyDataIterations(iterations: number): number {
|
|
509
|
+
if (
|
|
510
|
+
!Number.isSafeInteger(iterations) ||
|
|
511
|
+
iterations < KEY_DATA_PBKDF2_MIN_ITERATIONS ||
|
|
512
|
+
iterations > KEY_DATA_PBKDF2_MAX_ITERATIONS
|
|
513
|
+
) {
|
|
514
|
+
throw new Error(
|
|
515
|
+
`PBKDF2 iterations must be between ${String(KEY_DATA_PBKDF2_MIN_ITERATIONS)} and ${String(KEY_DATA_PBKDF2_MAX_ITERATIONS)}.`,
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
return iterations;
|
|
519
|
+
}
|
|
746
520
|
}
|
|
747
521
|
|
|
748
522
|
/**
|
|
749
523
|
* Returns a 32 byte HMAC of a javscript object.
|
|
750
524
|
*
|
|
751
|
-
* @param msg
|
|
752
|
-
* @param SK
|
|
525
|
+
* @param msg - The message to create the HMAC of.
|
|
526
|
+
* @param SK - The secret key to create the HMAC with.
|
|
753
527
|
*/
|
|
754
528
|
export function xHMAC(msg: unknown, SK: Uint8Array) {
|
|
755
529
|
const packedMsg = msgpackEncode(msg);
|
|
756
530
|
return hmac(sha256, SK, packedMsg);
|
|
757
531
|
}
|
|
758
532
|
|
|
533
|
+
/** Derive independent payload-encryption and envelope-authentication keys. */
|
|
534
|
+
export function xMessageKeySubkeys(messageKey: Uint8Array): {
|
|
535
|
+
authenticationKey: Uint8Array;
|
|
536
|
+
encryptionKey: Uint8Array;
|
|
537
|
+
} {
|
|
538
|
+
if (messageKey.length < 32) {
|
|
539
|
+
throw new Error("Message keys must contain at least 32 bytes.");
|
|
540
|
+
}
|
|
541
|
+
return {
|
|
542
|
+
authenticationKey: hmac(
|
|
543
|
+
sha256,
|
|
544
|
+
messageKey,
|
|
545
|
+
XUtils.decodeUTF8("vex:message:auth:v1"),
|
|
546
|
+
),
|
|
547
|
+
encryptionKey: hmac(
|
|
548
|
+
sha256,
|
|
549
|
+
messageKey,
|
|
550
|
+
XUtils.decodeUTF8("vex:message:encryption:v1"),
|
|
551
|
+
),
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
|
|
759
555
|
/**
|
|
760
556
|
* Gets a word list representation of a byte sequence.
|
|
761
557
|
*
|
|
762
|
-
* @param entropy The bytes to derive the wordlist from.
|
|
763
|
-
* @param wordList Optional
|
|
558
|
+
* @param entropy - The bytes to derive the wordlist from.
|
|
559
|
+
* @param wordList - Optional override for the wordlist. See bip39 docs for details.
|
|
764
560
|
*/
|
|
765
561
|
export function xMnemonic(entropy: Uint8Array, wordList?: string[]) {
|
|
766
562
|
return bip39.entropyToMnemonic(XUtils.encodeHex(entropy), wordList);
|
|
767
563
|
}
|
|
768
564
|
|
|
565
|
+
/** Domain-separated payload signed for X3DH signed and one-time prekeys. */
|
|
566
|
+
export function xPreKeySignaturePayload(
|
|
567
|
+
publicKey: Uint8Array,
|
|
568
|
+
kind: "one-time" | "signed",
|
|
569
|
+
): Uint8Array {
|
|
570
|
+
if (publicKey.length === 0) {
|
|
571
|
+
throw new Error("Prekey public key cannot be empty.");
|
|
572
|
+
}
|
|
573
|
+
const separator = new Uint8Array([0]);
|
|
574
|
+
return xConcat(
|
|
575
|
+
XUtils.decodeUTF8("vex:x3dh:prekey:v2"),
|
|
576
|
+
separator,
|
|
577
|
+
XUtils.decodeUTF8("tweetnacl"),
|
|
578
|
+
separator,
|
|
579
|
+
XUtils.decodeUTF8(kind),
|
|
580
|
+
separator,
|
|
581
|
+
publicKey,
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
|
|
769
585
|
/**
|
|
770
586
|
* Constants for vex.
|
|
771
587
|
*/
|
|
@@ -810,67 +626,14 @@ export interface XConstants {
|
|
|
810
626
|
MIN_OTK_SUPPLY: number;
|
|
811
627
|
}
|
|
812
628
|
|
|
813
|
-
/**
|
|
814
|
-
* FIPS: `device.signKey` in the database is the P-256 ECDSA public key (SPKI),
|
|
815
|
-
* used for account/device signature verification. X3DH on the client expects
|
|
816
|
-
* the same curve point as Web Crypto "raw" P-256 ECDH public bytes for
|
|
817
|
-
* `importEcdhPublicKey`. This converts SPKI → raw without a private key.
|
|
818
|
-
*/
|
|
819
|
-
export async function fipsEcdhRawPublicKeyFromEcdsaSpkiAsync(
|
|
820
|
-
ecdsaSpki: Uint8Array,
|
|
821
|
-
): Promise<Uint8Array> {
|
|
822
|
-
if (ecdsaSpki.length === 0) {
|
|
823
|
-
throw new Error("FIPS: empty ECDSA SPKI.");
|
|
824
|
-
}
|
|
825
|
-
const subtle = getSubtleCrypto();
|
|
826
|
-
const ecdsaPub = await importEcdsaPublicKey(ecdsaSpki);
|
|
827
|
-
const jwk = await subtle.exportKey("jwk", ecdsaPub);
|
|
828
|
-
if (
|
|
829
|
-
jwk.x === undefined ||
|
|
830
|
-
jwk.y === undefined ||
|
|
831
|
-
jwk.x.length === 0 ||
|
|
832
|
-
jwk.y.length === 0
|
|
833
|
-
) {
|
|
834
|
-
throw new Error("FIPS: could not export ECDSA public as JWK.");
|
|
835
|
-
}
|
|
836
|
-
const ecdhJwk: JsonWebKey = { ...jwk };
|
|
837
|
-
delete ecdhJwk.d;
|
|
838
|
-
ecdhJwk.key_ops = [];
|
|
839
|
-
ecdhJwk.ext = true;
|
|
840
|
-
const ecdhPub = await subtle.importKey(
|
|
841
|
-
"jwk",
|
|
842
|
-
ecdhJwk,
|
|
843
|
-
{ name: "ECDH", namedCurve: "P-256" },
|
|
844
|
-
true,
|
|
845
|
-
[],
|
|
846
|
-
);
|
|
847
|
-
return new Uint8Array(await subtle.exportKey("raw", ecdhPub));
|
|
848
|
-
}
|
|
849
|
-
|
|
850
629
|
/** Generate a fresh X25519 box key pair. */
|
|
851
630
|
export function xBoxKeyPair(): KeyPair {
|
|
852
631
|
return provider().boxKeyPair();
|
|
853
632
|
}
|
|
854
633
|
|
|
855
|
-
/** Async box keypair generation
|
|
856
|
-
export
|
|
857
|
-
|
|
858
|
-
return xBoxKeyPair();
|
|
859
|
-
}
|
|
860
|
-
const subtle = getSubtleCrypto();
|
|
861
|
-
const pair = await subtle.generateKey(
|
|
862
|
-
{ name: "ECDH", namedCurve: "P-256" },
|
|
863
|
-
true,
|
|
864
|
-
["deriveBits"],
|
|
865
|
-
);
|
|
866
|
-
return {
|
|
867
|
-
publicKey: new Uint8Array(
|
|
868
|
-
await subtle.exportKey("raw", pair.publicKey),
|
|
869
|
-
),
|
|
870
|
-
secretKey: new Uint8Array(
|
|
871
|
-
await subtle.exportKey("pkcs8", pair.privateKey),
|
|
872
|
-
),
|
|
873
|
-
};
|
|
634
|
+
/** Async X25519 box keypair generation. */
|
|
635
|
+
export function xBoxKeyPairAsync(): Promise<KeyPair> {
|
|
636
|
+
return Promise.resolve(xBoxKeyPair());
|
|
874
637
|
}
|
|
875
638
|
|
|
876
639
|
/** Restore an X25519 box key pair from a 32-byte secret key. */
|
|
@@ -880,17 +643,11 @@ export function xBoxKeyPairFromSecret(secretKey: Uint8Array): KeyPair {
|
|
|
880
643
|
|
|
881
644
|
// ── Key pair type ───────────────────────────────────────────────────────────
|
|
882
645
|
|
|
883
|
-
/** Async box key restore from private key material. */
|
|
884
|
-
export
|
|
646
|
+
/** Async X25519 box key restore from private key material. */
|
|
647
|
+
export function xBoxKeyPairFromSecretAsync(
|
|
885
648
|
secretKey: Uint8Array,
|
|
886
649
|
): Promise<KeyPair> {
|
|
887
|
-
|
|
888
|
-
return xBoxKeyPairFromSecret(secretKey);
|
|
889
|
-
}
|
|
890
|
-
if (secretKey.length === 32) {
|
|
891
|
-
return fipsEcdhKeyPairFrom32ByteSeed(secretKey, getSubtleCrypto());
|
|
892
|
-
}
|
|
893
|
-
return fipsEcdhKeyPairFromPkcs8(secretKey, getSubtleCrypto());
|
|
650
|
+
return Promise.resolve(xBoxKeyPairFromSecret(secretKey));
|
|
894
651
|
}
|
|
895
652
|
|
|
896
653
|
// ── Key generation ─────────────────────────────────────────────────────────
|
|
@@ -898,7 +655,7 @@ export async function xBoxKeyPairFromSecretAsync(
|
|
|
898
655
|
/**
|
|
899
656
|
* Concatanates multiple Uint8Arrays.
|
|
900
657
|
*
|
|
901
|
-
* @param arrays
|
|
658
|
+
* @param arrays - The Uint8Arrays to concatenate.
|
|
902
659
|
*/
|
|
903
660
|
export function xConcat(...arrays: Uint8Array[]): Uint8Array {
|
|
904
661
|
const totalLength = arrays.reduce((acc, value) => acc + value.length, 0);
|
|
@@ -924,8 +681,8 @@ export function xConcat(...arrays: Uint8Array[]): Uint8Array {
|
|
|
924
681
|
* Derives a shared Secret Key from a known private key and
|
|
925
682
|
* a peer's known public key.
|
|
926
683
|
*
|
|
927
|
-
* @param myPrivateKey Your own private key
|
|
928
|
-
* @param theirPublicKey Their public key
|
|
684
|
+
* @param myPrivateKey - Your own private key.
|
|
685
|
+
* @param theirPublicKey - Their public key.
|
|
929
686
|
* @returns The derived shared secret, SK.
|
|
930
687
|
*/
|
|
931
688
|
export function xDH(
|
|
@@ -935,70 +692,12 @@ export function xDH(
|
|
|
935
692
|
return provider().boxBefore(myPrivateKey, theirPublicKey);
|
|
936
693
|
}
|
|
937
694
|
|
|
938
|
-
/** Async DH
|
|
939
|
-
export
|
|
695
|
+
/** Async X25519 DH. */
|
|
696
|
+
export function xDHAsync(
|
|
940
697
|
myPrivateKey: Uint8Array,
|
|
941
698
|
theirPublicKey: Uint8Array,
|
|
942
699
|
): Promise<Uint8Array> {
|
|
943
|
-
|
|
944
|
-
return xDH(myPrivateKey, theirPublicKey);
|
|
945
|
-
}
|
|
946
|
-
const subtle = getSubtleCrypto();
|
|
947
|
-
const privateKey = await importEcdhPrivateKey(myPrivateKey);
|
|
948
|
-
const publicKey = await importEcdhPublicKey(theirPublicKey);
|
|
949
|
-
const shared = await subtle.deriveBits(
|
|
950
|
-
{ name: "ECDH", public: publicKey },
|
|
951
|
-
privateKey,
|
|
952
|
-
256,
|
|
953
|
-
);
|
|
954
|
-
return new Uint8Array(shared);
|
|
955
|
-
}
|
|
956
|
-
|
|
957
|
-
/**
|
|
958
|
-
* In `fips` mode only: derive a P-256 ECDH `KeyPair` (raw public + pkcs8 secret)
|
|
959
|
-
* from a P-256 ECDSA `KeyPair` (spki + pkcs8) using the same private scalar in Web Crypto.
|
|
960
|
-
* In `tweetnacl` mode, use `XKeyConvert.convertKeyPair` to map Ed25519 → X25519 instead.
|
|
961
|
-
*/
|
|
962
|
-
export async function xEcdhKeyPairFromEcdsaKeyPairAsync(
|
|
963
|
-
sign: KeyPair,
|
|
964
|
-
): Promise<KeyPair> {
|
|
965
|
-
if (activeCryptoProfile === "tweetnacl") {
|
|
966
|
-
return Promise.reject(
|
|
967
|
-
new Error(
|
|
968
|
-
'xEcdhKeyPairFromEcdsaKeyPairAsync is for crypto profile "fips" only. Use XKeyConvert.convertKeyPair in tweetnacl mode.',
|
|
969
|
-
),
|
|
970
|
-
);
|
|
971
|
-
}
|
|
972
|
-
const subtle = getSubtleCrypto();
|
|
973
|
-
const ecdsaPriv = await importEcdsaPrivateKey(sign.secretKey);
|
|
974
|
-
const jwk = await subtle.exportKey("jwk", ecdsaPriv);
|
|
975
|
-
if (typeof jwk.d !== "string" || jwk.d.length === 0) {
|
|
976
|
-
throw new Error("FIPS: could not export ECDSA private as JWK.");
|
|
977
|
-
}
|
|
978
|
-
const ecdhJwk: JsonWebKey = { ...jwk, key_ops: ["deriveBits"] };
|
|
979
|
-
ecdhJwk.ext = true;
|
|
980
|
-
const ecdhPriv = await subtle.importKey(
|
|
981
|
-
"jwk",
|
|
982
|
-
ecdhJwk,
|
|
983
|
-
{ name: "ECDH", namedCurve: "P-256" },
|
|
984
|
-
true,
|
|
985
|
-
["deriveBits"],
|
|
986
|
-
);
|
|
987
|
-
const ecdhPubJwk = await subtle.exportKey("jwk", ecdhPriv);
|
|
988
|
-
const ecdhPubJwkNoD: JsonWebKey = { ...ecdhPubJwk };
|
|
989
|
-
delete ecdhPubJwkNoD.d;
|
|
990
|
-
ecdhPubJwkNoD.key_ops = [];
|
|
991
|
-
const ecdhPub = await subtle.importKey(
|
|
992
|
-
"jwk",
|
|
993
|
-
ecdhPubJwkNoD,
|
|
994
|
-
{ name: "ECDH", namedCurve: "P-256" },
|
|
995
|
-
true,
|
|
996
|
-
[],
|
|
997
|
-
);
|
|
998
|
-
return {
|
|
999
|
-
publicKey: new Uint8Array(await subtle.exportKey("raw", ecdhPub)),
|
|
1000
|
-
secretKey: new Uint8Array(await subtle.exportKey("pkcs8", ecdhPriv)),
|
|
1001
|
-
};
|
|
700
|
+
return Promise.resolve(xDH(myPrivateKey, theirPublicKey));
|
|
1002
701
|
}
|
|
1003
702
|
|
|
1004
703
|
// ── Signing ────────────────────────────────────────────────────────────────
|
|
@@ -1050,7 +749,7 @@ export function xEncode(
|
|
|
1050
749
|
/**
|
|
1051
750
|
* Hashes some data.
|
|
1052
751
|
*
|
|
1053
|
-
* @param data
|
|
752
|
+
* @param data - The data to hash.
|
|
1054
753
|
* @returns The hash of the data.
|
|
1055
754
|
*/
|
|
1056
755
|
export function xHash(data: Uint8Array) {
|
|
@@ -1092,24 +791,13 @@ export function xSecretbox(
|
|
|
1092
791
|
return provider().secretbox(plaintext, nonce, key);
|
|
1093
792
|
}
|
|
1094
793
|
|
|
1095
|
-
/** Async
|
|
1096
|
-
export
|
|
794
|
+
/** Async XSalsa20-Poly1305 encryption. */
|
|
795
|
+
export function xSecretboxAsync(
|
|
1097
796
|
plaintext: Uint8Array,
|
|
1098
797
|
nonce: Uint8Array,
|
|
1099
798
|
key: Uint8Array,
|
|
1100
799
|
): Promise<Uint8Array> {
|
|
1101
|
-
|
|
1102
|
-
return xSecretbox(plaintext, nonce, key);
|
|
1103
|
-
}
|
|
1104
|
-
const subtle = getSubtleCrypto();
|
|
1105
|
-
const iv = requireAesGcmNonce(nonce);
|
|
1106
|
-
const aesKey = await importAesKey(key, ["encrypt"]);
|
|
1107
|
-
const ciphertext = await subtle.encrypt(
|
|
1108
|
-
{ iv: toBufferSource(iv), name: "AES-GCM" },
|
|
1109
|
-
aesKey,
|
|
1110
|
-
toBufferSource(plaintext),
|
|
1111
|
-
);
|
|
1112
|
-
return new Uint8Array(ciphertext);
|
|
800
|
+
return Promise.resolve(xSecretbox(plaintext, nonce, key));
|
|
1113
801
|
}
|
|
1114
802
|
|
|
1115
803
|
/** Decrypt with a shared secret key. Returns null if authentication fails. */
|
|
@@ -1121,28 +809,13 @@ export function xSecretboxOpen(
|
|
|
1121
809
|
return provider().secretboxOpen(ciphertext, nonce, key);
|
|
1122
810
|
}
|
|
1123
811
|
|
|
1124
|
-
/** Async
|
|
1125
|
-
export
|
|
812
|
+
/** Async XSalsa20-Poly1305 decryption. */
|
|
813
|
+
export function xSecretboxOpenAsync(
|
|
1126
814
|
ciphertext: Uint8Array,
|
|
1127
815
|
nonce: Uint8Array,
|
|
1128
816
|
key: Uint8Array,
|
|
1129
817
|
): Promise<null | Uint8Array> {
|
|
1130
|
-
|
|
1131
|
-
return xSecretboxOpen(ciphertext, nonce, key);
|
|
1132
|
-
}
|
|
1133
|
-
const subtle = getSubtleCrypto();
|
|
1134
|
-
const iv = requireAesGcmNonce(nonce);
|
|
1135
|
-
const aesKey = await importAesKey(key, ["decrypt"]);
|
|
1136
|
-
try {
|
|
1137
|
-
const plaintext = await subtle.decrypt(
|
|
1138
|
-
{ iv: toBufferSource(iv), name: "AES-GCM" },
|
|
1139
|
-
aesKey,
|
|
1140
|
-
toBufferSource(ciphertext),
|
|
1141
|
-
);
|
|
1142
|
-
return new Uint8Array(plaintext);
|
|
1143
|
-
} catch {
|
|
1144
|
-
return null;
|
|
1145
|
-
}
|
|
818
|
+
return Promise.resolve(xSecretboxOpen(ciphertext, nonce, key));
|
|
1146
819
|
}
|
|
1147
820
|
|
|
1148
821
|
/** Sign a message with an Ed25519 secret key. Returns signed message (64-byte signature prefix + message). */
|
|
@@ -1150,24 +823,12 @@ export function xSign(message: Uint8Array, secretKey: Uint8Array): Uint8Array {
|
|
|
1150
823
|
return provider().sign(message, secretKey);
|
|
1151
824
|
}
|
|
1152
825
|
|
|
1153
|
-
/** Async signing
|
|
1154
|
-
export
|
|
826
|
+
/** Async Ed25519 signing. */
|
|
827
|
+
export function xSignAsync(
|
|
1155
828
|
message: Uint8Array,
|
|
1156
829
|
secretKey: Uint8Array,
|
|
1157
830
|
): Promise<Uint8Array> {
|
|
1158
|
-
|
|
1159
|
-
return xSign(message, secretKey);
|
|
1160
|
-
}
|
|
1161
|
-
const subtle = getSubtleCrypto();
|
|
1162
|
-
const privateKey = await importEcdsaPrivateKey(secretKey);
|
|
1163
|
-
const signature = new Uint8Array(
|
|
1164
|
-
await subtle.sign(
|
|
1165
|
-
{ hash: "SHA-256", name: "ECDSA" },
|
|
1166
|
-
privateKey,
|
|
1167
|
-
toBufferSource(message),
|
|
1168
|
-
),
|
|
1169
|
-
);
|
|
1170
|
-
return encodeFipsSignedMessage(signature, message);
|
|
831
|
+
return Promise.resolve(xSign(message, secretKey));
|
|
1171
832
|
}
|
|
1172
833
|
|
|
1173
834
|
/** Generate a fresh Ed25519 signing key pair. */
|
|
@@ -1175,25 +836,9 @@ export function xSignKeyPair(): KeyPair {
|
|
|
1175
836
|
return provider().signKeyPair();
|
|
1176
837
|
}
|
|
1177
838
|
|
|
1178
|
-
/** Async keypair generation
|
|
1179
|
-
export
|
|
1180
|
-
|
|
1181
|
-
return xSignKeyPair();
|
|
1182
|
-
}
|
|
1183
|
-
const subtle = getSubtleCrypto();
|
|
1184
|
-
const pair = await subtle.generateKey(
|
|
1185
|
-
{ name: "ECDSA", namedCurve: "P-256" },
|
|
1186
|
-
true,
|
|
1187
|
-
["sign", "verify"],
|
|
1188
|
-
);
|
|
1189
|
-
return {
|
|
1190
|
-
publicKey: new Uint8Array(
|
|
1191
|
-
await subtle.exportKey("spki", pair.publicKey),
|
|
1192
|
-
),
|
|
1193
|
-
secretKey: new Uint8Array(
|
|
1194
|
-
await subtle.exportKey("pkcs8", pair.privateKey),
|
|
1195
|
-
),
|
|
1196
|
-
};
|
|
839
|
+
/** Async Ed25519 keypair generation. */
|
|
840
|
+
export function xSignKeyPairAsync(): Promise<KeyPair> {
|
|
841
|
+
return Promise.resolve(xSignKeyPair());
|
|
1197
842
|
}
|
|
1198
843
|
|
|
1199
844
|
/** Restore an Ed25519 signing key pair from a 64-byte secret key. */
|
|
@@ -1201,14 +846,11 @@ export function xSignKeyPairFromSecret(secretKey: Uint8Array): KeyPair {
|
|
|
1201
846
|
return provider().signKeyPairFromSecret(secretKey);
|
|
1202
847
|
}
|
|
1203
848
|
|
|
1204
|
-
/** Async restore of signing keypair
|
|
1205
|
-
export
|
|
849
|
+
/** Async restore of an Ed25519 signing keypair. */
|
|
850
|
+
export function xSignKeyPairFromSecretAsync(
|
|
1206
851
|
secretKey: Uint8Array,
|
|
1207
852
|
): Promise<KeyPair> {
|
|
1208
|
-
|
|
1209
|
-
return xSignKeyPairFromSecret(secretKey);
|
|
1210
|
-
}
|
|
1211
|
-
return fipsEcdsaKeyPairFromPkcs8(secretKey, getSubtleCrypto());
|
|
853
|
+
return Promise.resolve(xSignKeyPairFromSecret(secretKey));
|
|
1212
854
|
}
|
|
1213
855
|
|
|
1214
856
|
/** Verify and open a signed message. Returns the original message, or null if verification fails. */
|
|
@@ -1219,101 +861,12 @@ export function xSignOpen(
|
|
|
1219
861
|
return provider().signOpen(signedMessage, publicKey);
|
|
1220
862
|
}
|
|
1221
863
|
|
|
1222
|
-
/** Async verify/open
|
|
1223
|
-
export
|
|
864
|
+
/** Async Ed25519 verify/open. */
|
|
865
|
+
export function xSignOpenAsync(
|
|
1224
866
|
signedMessage: Uint8Array,
|
|
1225
867
|
publicKey: Uint8Array,
|
|
1226
868
|
): Promise<null | Uint8Array> {
|
|
1227
|
-
|
|
1228
|
-
return xSignOpen(signedMessage, publicKey);
|
|
1229
|
-
}
|
|
1230
|
-
const subtle = getSubtleCrypto();
|
|
1231
|
-
const parsed = decodeFipsSignedMessage(signedMessage);
|
|
1232
|
-
const verifyKey = await importEcdsaPublicKey(publicKey);
|
|
1233
|
-
const valid = await subtle.verify(
|
|
1234
|
-
{ hash: "SHA-256", name: "ECDSA" },
|
|
1235
|
-
verifyKey,
|
|
1236
|
-
toBufferSource(parsed.signature),
|
|
1237
|
-
toBufferSource(parsed.message),
|
|
1238
|
-
);
|
|
1239
|
-
return valid ? parsed.message : null;
|
|
1240
|
-
}
|
|
1241
|
-
|
|
1242
|
-
async function fipsEcdsaKeyPairFromPkcs8(
|
|
1243
|
-
secretKey: Uint8Array,
|
|
1244
|
-
subtle: SubtleCrypto,
|
|
1245
|
-
): Promise<KeyPair> {
|
|
1246
|
-
const ecdsaPriv = await importEcdsaPrivateKey(secretKey);
|
|
1247
|
-
const jwk = await subtle.exportKey("jwk", ecdsaPriv);
|
|
1248
|
-
const ecdsaPubJwk: JsonWebKey = { ...jwk };
|
|
1249
|
-
delete ecdsaPubJwk.d;
|
|
1250
|
-
ecdsaPubJwk.key_ops = ["verify"];
|
|
1251
|
-
const ecdsaPub = await subtle.importKey(
|
|
1252
|
-
"jwk",
|
|
1253
|
-
ecdsaPubJwk,
|
|
1254
|
-
{ name: "ECDSA", namedCurve: "P-256" },
|
|
1255
|
-
true,
|
|
1256
|
-
["verify"],
|
|
1257
|
-
);
|
|
1258
|
-
return {
|
|
1259
|
-
publicKey: new Uint8Array(await subtle.exportKey("spki", ecdsaPub)),
|
|
1260
|
-
secretKey: Uint8Array.from(secretKey),
|
|
1261
|
-
};
|
|
1262
|
-
}
|
|
1263
|
-
|
|
1264
|
-
async function importAesKey(
|
|
1265
|
-
key: Uint8Array,
|
|
1266
|
-
usages: WebCryptoKeyUsage[],
|
|
1267
|
-
): Promise<CryptoKey> {
|
|
1268
|
-
return getSubtleCrypto().importKey(
|
|
1269
|
-
"raw",
|
|
1270
|
-
toBufferSource(key),
|
|
1271
|
-
{ name: "AES-GCM" },
|
|
1272
|
-
false,
|
|
1273
|
-
usages,
|
|
1274
|
-
);
|
|
1275
|
-
}
|
|
1276
|
-
|
|
1277
|
-
async function importEcdhPrivateKey(secretKey: Uint8Array): Promise<CryptoKey> {
|
|
1278
|
-
return getSubtleCrypto().importKey(
|
|
1279
|
-
"pkcs8",
|
|
1280
|
-
toBufferSource(secretKey),
|
|
1281
|
-
{ name: "ECDH", namedCurve: "P-256" },
|
|
1282
|
-
true,
|
|
1283
|
-
["deriveBits"],
|
|
1284
|
-
);
|
|
1285
|
-
}
|
|
1286
|
-
|
|
1287
|
-
async function importEcdhPublicKey(publicKey: Uint8Array): Promise<CryptoKey> {
|
|
1288
|
-
return getSubtleCrypto().importKey(
|
|
1289
|
-
"raw",
|
|
1290
|
-
toBufferSource(publicKey),
|
|
1291
|
-
{ name: "ECDH", namedCurve: "P-256" },
|
|
1292
|
-
true,
|
|
1293
|
-
[],
|
|
1294
|
-
);
|
|
1295
|
-
}
|
|
1296
|
-
|
|
1297
|
-
async function importEcdsaPrivateKey(
|
|
1298
|
-
secretKey: Uint8Array,
|
|
1299
|
-
): Promise<CryptoKey> {
|
|
1300
|
-
return getSubtleCrypto().importKey(
|
|
1301
|
-
"pkcs8",
|
|
1302
|
-
toBufferSource(secretKey),
|
|
1303
|
-
{ name: "ECDSA", namedCurve: "P-256" },
|
|
1304
|
-
true,
|
|
1305
|
-
["sign"],
|
|
1306
|
-
);
|
|
1307
|
-
}
|
|
1308
|
-
|
|
1309
|
-
async function importEcdsaPublicKey(publicKey: Uint8Array): Promise<CryptoKey> {
|
|
1310
|
-
return getSubtleCrypto().importKey(
|
|
1311
|
-
"spki",
|
|
1312
|
-
toBufferSource(publicKey),
|
|
1313
|
-
{ name: "ECDSA", namedCurve: "P-256" },
|
|
1314
|
-
true,
|
|
1315
|
-
["verify"],
|
|
1316
|
-
);
|
|
869
|
+
return Promise.resolve(xSignOpen(signedMessage, publicKey));
|
|
1317
870
|
}
|
|
1318
871
|
|
|
1319
872
|
/**
|