@vex-chat/crypto 2.0.1 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLA.md +38 -0
- package/LICENSE-COMMERCIAL +10 -0
- package/LICENSING.md +15 -0
- package/README.md +25 -9
- package/dist/index.d.ts +65 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +492 -19
- package/dist/index.js.map +1 -1
- package/package.json +8 -2
- package/src/__tests__/XKeyConvert.ts +6 -0
- package/src/__tests__/XUtils/bytesEqual.ts +6 -0
- package/src/__tests__/XUtils/emptyHeader.ts +6 -0
- package/src/__tests__/XUtils/numberToUint8Arr.ts +6 -0
- package/src/__tests__/XUtils/packMessage.ts +6 -0
- package/src/__tests__/XUtils/stringEncoding.ts +6 -0
- package/src/__tests__/XUtils/uint8ArrToNumber.ts +6 -0
- package/src/__tests__/XUtils/unpackMessage.ts +6 -0
- package/src/__tests__/cryptoProfile.ts +64 -0
- package/src/__tests__/xAsyncApi.extended.test.ts +84 -0
- package/src/__tests__/xAsyncProfile.ts +69 -0
- package/src/__tests__/xConcat.ts +6 -0
- package/src/__tests__/xDH.ts +6 -0
- package/src/__tests__/xEncode.ts +6 -0
- package/src/__tests__/xHMAC.ts +6 -0
- package/src/__tests__/xMnemonic.ts +6 -0
- package/src/index.ts +810 -21
package/src/index.ts
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2020-2026 Vex Heavy Industries LLC
|
|
4
|
+
* Licensed under AGPL-3.0. See LICENSE for details.
|
|
5
|
+
* Commercial licenses available at vex.wtf
|
|
6
|
+
*/
|
|
7
|
+
|
|
1
8
|
import type { BaseMsg } from "@vex-chat/types";
|
|
2
9
|
|
|
10
|
+
import { numberToBytesBE } from "@noble/curves/abstract/utils";
|
|
11
|
+
import { p256 } from "@noble/curves/p256";
|
|
3
12
|
import { hkdf } from "@noble/hashes/hkdf.js";
|
|
4
13
|
import { hmac } from "@noble/hashes/hmac.js";
|
|
5
14
|
import { pbkdf2 as noblePbkdf2 } from "@noble/hashes/pbkdf2.js";
|
|
@@ -15,6 +24,347 @@ import { Packr } from "msgpackr";
|
|
|
15
24
|
import nacl from "tweetnacl";
|
|
16
25
|
import { z } from "zod/v4";
|
|
17
26
|
|
|
27
|
+
/** Runtime crypto profile selector. */
|
|
28
|
+
export type CryptoProfile = "fips" | "tweetnacl";
|
|
29
|
+
|
|
30
|
+
interface CryptoProvider {
|
|
31
|
+
boxBefore(myPrivateKey: Uint8Array, theirPublicKey: Uint8Array): Uint8Array;
|
|
32
|
+
boxKeyPair(): KeyPair;
|
|
33
|
+
boxKeyPairFromSecret(secretKey: Uint8Array): KeyPair;
|
|
34
|
+
profile: CryptoProfile;
|
|
35
|
+
randomBytes(length: number): Uint8Array;
|
|
36
|
+
secretbox(
|
|
37
|
+
plaintext: Uint8Array,
|
|
38
|
+
nonce: Uint8Array,
|
|
39
|
+
key: Uint8Array,
|
|
40
|
+
): Uint8Array;
|
|
41
|
+
secretboxOpen(
|
|
42
|
+
ciphertext: Uint8Array,
|
|
43
|
+
nonce: Uint8Array,
|
|
44
|
+
key: Uint8Array,
|
|
45
|
+
): null | Uint8Array;
|
|
46
|
+
sign(message: Uint8Array, secretKey: Uint8Array): Uint8Array;
|
|
47
|
+
signKeyPair(): KeyPair;
|
|
48
|
+
signKeyPairFromSecret(secretKey: Uint8Array): KeyPair;
|
|
49
|
+
signOpen(
|
|
50
|
+
signedMessage: Uint8Array,
|
|
51
|
+
publicKey: Uint8Array,
|
|
52
|
+
): null | Uint8Array;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
type WebCryptoKeyUsage =
|
|
56
|
+
| "decrypt"
|
|
57
|
+
| "deriveBits"
|
|
58
|
+
| "deriveKey"
|
|
59
|
+
| "encrypt"
|
|
60
|
+
| "sign"
|
|
61
|
+
| "unwrapKey"
|
|
62
|
+
| "verify"
|
|
63
|
+
| "wrapKey";
|
|
64
|
+
|
|
65
|
+
interface WebCryptoLike {
|
|
66
|
+
getRandomValues: Crypto["getRandomValues"];
|
|
67
|
+
subtle: SubtleCrypto;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function getWebCrypto(): WebCryptoLike {
|
|
71
|
+
const cryptoCandidate: unknown = globalThis.crypto;
|
|
72
|
+
if (!isWebCryptoLike(cryptoCandidate)) {
|
|
73
|
+
throw new Error(
|
|
74
|
+
"Web Crypto API is not available in this runtime for fips profile operations.",
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
return cryptoCandidate;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function isWebCryptoLike(value: unknown): value is WebCryptoLike {
|
|
81
|
+
if (typeof value !== "object" || value === null) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
return (
|
|
85
|
+
"getRandomValues" in value &&
|
|
86
|
+
typeof value.getRandomValues === "function" &&
|
|
87
|
+
"subtle" in value &&
|
|
88
|
+
typeof value.subtle === "object" &&
|
|
89
|
+
value.subtle !== null
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function randomBytesWebCrypto(length: number): Uint8Array {
|
|
94
|
+
if (!Number.isInteger(length) || length < 0) {
|
|
95
|
+
throw new Error(
|
|
96
|
+
`Expected non-negative integer length, received ${String(length)}.`,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
const cryptoImpl = getWebCrypto();
|
|
100
|
+
const result = new Uint8Array(length);
|
|
101
|
+
let offset = 0;
|
|
102
|
+
while (offset < length) {
|
|
103
|
+
const chunkLength = Math.min(65536, length - offset);
|
|
104
|
+
const chunk = result.subarray(offset, offset + chunkLength);
|
|
105
|
+
cryptoImpl.getRandomValues(chunk);
|
|
106
|
+
offset += chunkLength;
|
|
107
|
+
}
|
|
108
|
+
return result;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function unsupported(profile: CryptoProfile, operation: string): never {
|
|
112
|
+
throw new Error(
|
|
113
|
+
`Crypto profile "${profile}" does not implement ${operation} yet.`,
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const tweetnaclProvider: CryptoProvider = {
|
|
118
|
+
boxBefore: (myPrivateKey, theirPublicKey) =>
|
|
119
|
+
nacl.box.before(theirPublicKey, myPrivateKey),
|
|
120
|
+
boxKeyPair: () => nacl.box.keyPair(),
|
|
121
|
+
boxKeyPairFromSecret: (secretKey) =>
|
|
122
|
+
nacl.box.keyPair.fromSecretKey(secretKey),
|
|
123
|
+
profile: "tweetnacl",
|
|
124
|
+
randomBytes: (length) => nacl.randomBytes(length),
|
|
125
|
+
secretbox: (plaintext, nonce, key) => nacl.secretbox(plaintext, nonce, key),
|
|
126
|
+
secretboxOpen: (ciphertext, nonce, key) =>
|
|
127
|
+
nacl.secretbox.open(ciphertext, nonce, key),
|
|
128
|
+
sign: (message, secretKey) => nacl.sign(message, secretKey),
|
|
129
|
+
signKeyPair: () => nacl.sign.keyPair(),
|
|
130
|
+
signKeyPairFromSecret: (secretKey) =>
|
|
131
|
+
nacl.sign.keyPair.fromSecretKey(secretKey),
|
|
132
|
+
signOpen: (signedMessage, publicKey) =>
|
|
133
|
+
nacl.sign.open(signedMessage, publicKey),
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const fipsProvider: CryptoProvider = {
|
|
137
|
+
boxBefore: (myPrivateKey, theirPublicKey) => {
|
|
138
|
+
void myPrivateKey;
|
|
139
|
+
void theirPublicKey;
|
|
140
|
+
return unsupported("fips", "xDH");
|
|
141
|
+
},
|
|
142
|
+
boxKeyPair: () => unsupported("fips", "xBoxKeyPair"),
|
|
143
|
+
boxKeyPairFromSecret: (secretKey) => {
|
|
144
|
+
void secretKey;
|
|
145
|
+
return unsupported("fips", "xBoxKeyPairFromSecret");
|
|
146
|
+
},
|
|
147
|
+
profile: "fips",
|
|
148
|
+
randomBytes: (length) => randomBytesWebCrypto(length),
|
|
149
|
+
secretbox: (plaintext, nonce, key) => {
|
|
150
|
+
void plaintext;
|
|
151
|
+
void nonce;
|
|
152
|
+
void key;
|
|
153
|
+
return unsupported("fips", "xSecretbox");
|
|
154
|
+
},
|
|
155
|
+
secretboxOpen: (ciphertext, nonce, key) => {
|
|
156
|
+
void ciphertext;
|
|
157
|
+
void nonce;
|
|
158
|
+
void key;
|
|
159
|
+
return unsupported("fips", "xSecretboxOpen");
|
|
160
|
+
},
|
|
161
|
+
sign: (message, secretKey) => {
|
|
162
|
+
void message;
|
|
163
|
+
void secretKey;
|
|
164
|
+
return unsupported("fips", "xSign");
|
|
165
|
+
},
|
|
166
|
+
signKeyPair: () => unsupported("fips", "xSignKeyPair"),
|
|
167
|
+
signKeyPairFromSecret: (secretKey) => {
|
|
168
|
+
void secretKey;
|
|
169
|
+
return unsupported("fips", "xSignKeyPairFromSecret");
|
|
170
|
+
},
|
|
171
|
+
signOpen: (signedMessage, publicKey) => {
|
|
172
|
+
void signedMessage;
|
|
173
|
+
void publicKey;
|
|
174
|
+
return unsupported("fips", "xSignOpen");
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const providers: Record<CryptoProfile, CryptoProvider> = {
|
|
179
|
+
fips: fipsProvider,
|
|
180
|
+
tweetnacl: tweetnaclProvider,
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
let activeCryptoProfile: CryptoProfile = "tweetnacl";
|
|
184
|
+
let activeCryptoProvider: CryptoProvider = providers[activeCryptoProfile];
|
|
185
|
+
|
|
186
|
+
/** `globalThis` may omit `Buffer` in browsers; Node types can still attach `Buffer` to `globalThis` unconditionally. */
|
|
187
|
+
type NodeBufferish = {
|
|
188
|
+
from(data: ArrayBuffer | Uint8Array): {
|
|
189
|
+
toString(encoding: "base64url"): string;
|
|
190
|
+
};
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
/** Returns the currently configured crypto profile. */
|
|
194
|
+
export function getCryptoProfile(): CryptoProfile {
|
|
195
|
+
return activeCryptoProfile;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Sets the runtime crypto profile.
|
|
200
|
+
*
|
|
201
|
+
* `tweetnacl` preserves existing behavior.
|
|
202
|
+
* `fips` currently enables only backend-agnostic helpers; NaCl-coupled
|
|
203
|
+
* primitives throw until a FIPS backend implementation is wired in.
|
|
204
|
+
*/
|
|
205
|
+
export function setCryptoProfile(profile: CryptoProfile): void {
|
|
206
|
+
activeCryptoProfile = profile;
|
|
207
|
+
activeCryptoProvider = providers[profile];
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function bytesToBase64Url(bytes: Uint8Array): string {
|
|
211
|
+
const BufferCtor = getNodeBufferCtor();
|
|
212
|
+
if (BufferCtor !== undefined) {
|
|
213
|
+
return BufferCtor.from(bytes).toString("base64url");
|
|
214
|
+
}
|
|
215
|
+
return globalThis
|
|
216
|
+
.btoa(String.fromCodePoint(...Array.from(bytes)))
|
|
217
|
+
.replace(/\+/g, "-")
|
|
218
|
+
.replace(/\//g, "_")
|
|
219
|
+
.replace(/=+/g, "");
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function concatBytes(a: Uint8Array, b: Uint8Array): Uint8Array {
|
|
223
|
+
const out = new Uint8Array(a.length + b.length);
|
|
224
|
+
out.set(a, 0);
|
|
225
|
+
out.set(b, a.length);
|
|
226
|
+
return out;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function decodeFipsSignedMessage(signedMessage: Uint8Array): {
|
|
230
|
+
message: Uint8Array;
|
|
231
|
+
signature: Uint8Array;
|
|
232
|
+
} {
|
|
233
|
+
if (signedMessage.length < 2) {
|
|
234
|
+
throw new Error(
|
|
235
|
+
"Invalid FIPS signed message: missing signature length prefix.",
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
const signatureLength =
|
|
239
|
+
(signedMessage[0] ?? 0) * 256 + (signedMessage[1] ?? 0);
|
|
240
|
+
const signatureStart = 2;
|
|
241
|
+
const signatureEnd = signatureStart + signatureLength;
|
|
242
|
+
if (signedMessage.length < signatureEnd) {
|
|
243
|
+
throw new Error(
|
|
244
|
+
"Invalid FIPS signed message: signature length exceeds message length.",
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
return {
|
|
248
|
+
message: signedMessage.slice(signatureEnd),
|
|
249
|
+
signature: signedMessage.slice(signatureStart, signatureEnd),
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function encodeFipsSignedMessage(
|
|
254
|
+
signature: Uint8Array,
|
|
255
|
+
message: Uint8Array,
|
|
256
|
+
): Uint8Array {
|
|
257
|
+
if (signature.length > 65535) {
|
|
258
|
+
throw new Error("FIPS signature too long to encode.");
|
|
259
|
+
}
|
|
260
|
+
const prefix = new Uint8Array(2);
|
|
261
|
+
prefix[0] = (signature.length >> 8) & 0xff;
|
|
262
|
+
prefix[1] = signature.length & 0xff;
|
|
263
|
+
return concatBytes(concatBytes(prefix, signature), message);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async function fipsEcdhKeyPairFrom32ByteSeed(
|
|
267
|
+
secretKey: Uint8Array,
|
|
268
|
+
subtle: SubtleCrypto,
|
|
269
|
+
): Promise<KeyPair> {
|
|
270
|
+
if (secretKey.length !== 32) {
|
|
271
|
+
throw new Error(
|
|
272
|
+
"FIPS: expected a 32-byte IKM/seed for ECDH from-secret.",
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
const d = p256.utils.normPrivateKeyToScalar(Uint8Array.from(secretKey));
|
|
276
|
+
const d32 = numberToBytesBE(d, 32);
|
|
277
|
+
const rawPub = p256.getPublicKey(d, false);
|
|
278
|
+
if (rawPub[0] !== 0x04) {
|
|
279
|
+
throw new Error("FIPS: expected uncompressed P-256 public key.");
|
|
280
|
+
}
|
|
281
|
+
const jwk: JsonWebKey = {
|
|
282
|
+
crv: "P-256",
|
|
283
|
+
d: bytesToBase64Url(d32),
|
|
284
|
+
kty: "EC",
|
|
285
|
+
x: bytesToBase64Url(rawPub.subarray(1, 33)),
|
|
286
|
+
y: bytesToBase64Url(rawPub.subarray(33, 65)),
|
|
287
|
+
};
|
|
288
|
+
const ecdhPriv = await subtle.importKey(
|
|
289
|
+
"jwk",
|
|
290
|
+
jwk,
|
|
291
|
+
{ name: "ECDH", namedCurve: "P-256" },
|
|
292
|
+
true,
|
|
293
|
+
["deriveBits"],
|
|
294
|
+
);
|
|
295
|
+
const ecdhPubJwk = await subtle.exportKey("jwk", ecdhPriv);
|
|
296
|
+
const ecdhPubJwkNoD: JsonWebKey = { ...ecdhPubJwk };
|
|
297
|
+
delete ecdhPubJwkNoD.d;
|
|
298
|
+
ecdhPubJwkNoD.key_ops = [];
|
|
299
|
+
const ecdhPub = await subtle.importKey(
|
|
300
|
+
"jwk",
|
|
301
|
+
ecdhPubJwkNoD,
|
|
302
|
+
{ name: "ECDH", namedCurve: "P-256" },
|
|
303
|
+
true,
|
|
304
|
+
[],
|
|
305
|
+
);
|
|
306
|
+
return {
|
|
307
|
+
publicKey: new Uint8Array(await subtle.exportKey("raw", ecdhPub)),
|
|
308
|
+
secretKey: new Uint8Array(await subtle.exportKey("pkcs8", ecdhPriv)),
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
async function fipsEcdhKeyPairFromPkcs8(
|
|
313
|
+
secretKey: Uint8Array,
|
|
314
|
+
subtle: SubtleCrypto,
|
|
315
|
+
): Promise<KeyPair> {
|
|
316
|
+
const ecdhPriv = await subtle.importKey(
|
|
317
|
+
"pkcs8",
|
|
318
|
+
toBufferSource(secretKey),
|
|
319
|
+
{ name: "ECDH", namedCurve: "P-256" },
|
|
320
|
+
true,
|
|
321
|
+
["deriveBits"],
|
|
322
|
+
);
|
|
323
|
+
const jwk = await subtle.exportKey("jwk", ecdhPriv);
|
|
324
|
+
const ecdhPubJwk: JsonWebKey = { ...jwk };
|
|
325
|
+
delete ecdhPubJwk.d;
|
|
326
|
+
ecdhPubJwk.key_ops = [];
|
|
327
|
+
const ecdhPub = await subtle.importKey(
|
|
328
|
+
"jwk",
|
|
329
|
+
ecdhPubJwk,
|
|
330
|
+
{ name: "ECDH", namedCurve: "P-256" },
|
|
331
|
+
true,
|
|
332
|
+
[],
|
|
333
|
+
);
|
|
334
|
+
return {
|
|
335
|
+
publicKey: new Uint8Array(await subtle.exportKey("raw", ecdhPub)),
|
|
336
|
+
secretKey: new Uint8Array(await subtle.exportKey("pkcs8", ecdhPriv)),
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function getNodeBufferCtor(): NodeBufferish | undefined {
|
|
341
|
+
if (!("Buffer" in globalThis)) {
|
|
342
|
+
return undefined;
|
|
343
|
+
}
|
|
344
|
+
return (globalThis as { Buffer: NodeBufferish }).Buffer;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function getSubtleCrypto(): SubtleCrypto {
|
|
348
|
+
return getWebCrypto().subtle;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function provider(): CryptoProvider {
|
|
352
|
+
return activeCryptoProvider;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function requireAesGcmNonce(nonce: Uint8Array): Uint8Array {
|
|
356
|
+
if (nonce.length < 12) {
|
|
357
|
+
throw new Error(
|
|
358
|
+
`AES-GCM requires a nonce of at least 12 bytes, received ${String(nonce.length)}.`,
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
return nonce.slice(0, 12);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function toBufferSource(bytes: Uint8Array): ArrayBuffer {
|
|
365
|
+
return Uint8Array.from(bytes).buffer;
|
|
366
|
+
}
|
|
367
|
+
|
|
18
368
|
// msgpackr with useRecords:false emits standard msgpack (no nonstandard record extension).
|
|
19
369
|
// moreTypes:false keeps the extension set to only what other decoders understand.
|
|
20
370
|
// pack() returns Node Buffer (tight view) so consumers like axios send the correct bytes.
|
|
@@ -106,7 +456,7 @@ export class XUtils {
|
|
|
106
456
|
c: ITERATIONS,
|
|
107
457
|
dkLen: 32,
|
|
108
458
|
});
|
|
109
|
-
const DECRYPTED_SIGNKEY =
|
|
459
|
+
const DECRYPTED_SIGNKEY = provider().secretboxOpen(
|
|
110
460
|
ENCRYPTED_KEY,
|
|
111
461
|
ENCRYPTION_NONCE,
|
|
112
462
|
DERIVED_KEY,
|
|
@@ -118,6 +468,66 @@ export class XUtils {
|
|
|
118
468
|
return XUtils.encodeHex(DECRYPTED_SIGNKEY);
|
|
119
469
|
};
|
|
120
470
|
|
|
471
|
+
/**
|
|
472
|
+
* Async variant of decryptKeyData for cross-runtime/FIPS backends.
|
|
473
|
+
* Supports both profile formats emitted by encryptKeyDataAsync.
|
|
474
|
+
*/
|
|
475
|
+
public static decryptKeyDataAsync = async (
|
|
476
|
+
keyData: Uint8Array,
|
|
477
|
+
password: string,
|
|
478
|
+
): Promise<string> => {
|
|
479
|
+
const ITERATIONS = XUtils.uint8ArrToNumber(keyData.slice(0, 6));
|
|
480
|
+
const PKBDF_SALT = keyData.slice(6, 30);
|
|
481
|
+
const ENCRYPTION_NONCE = keyData.slice(30, 54);
|
|
482
|
+
const ENCRYPTED_KEY = keyData.slice(54);
|
|
483
|
+
const DERIVED_KEY = noblePbkdf2(sha512, password, PKBDF_SALT, {
|
|
484
|
+
c: ITERATIONS,
|
|
485
|
+
dkLen: 32,
|
|
486
|
+
});
|
|
487
|
+
const decrypted =
|
|
488
|
+
activeCryptoProfile === "fips"
|
|
489
|
+
? await xSecretboxOpenAsync(
|
|
490
|
+
ENCRYPTED_KEY,
|
|
491
|
+
ENCRYPTION_NONCE,
|
|
492
|
+
DERIVED_KEY,
|
|
493
|
+
)
|
|
494
|
+
: xSecretboxOpen(ENCRYPTED_KEY, ENCRYPTION_NONCE, DERIVED_KEY);
|
|
495
|
+
|
|
496
|
+
if (decrypted === null) {
|
|
497
|
+
throw new Error("Decryption failed. Wrong password?");
|
|
498
|
+
}
|
|
499
|
+
return XUtils.encodeHex(decrypted);
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* 32-byte AES-256 key for local at-rest encryption (e.g. sqlite) derived from
|
|
504
|
+
* identity `secretKey`. For `tweetnacl` this is the 32-byte X25519 private key.
|
|
505
|
+
* For `fips` the identity secret is PKCS#8; HKDF is applied so AES keys never
|
|
506
|
+
* equal the raw private key material.
|
|
507
|
+
*/
|
|
508
|
+
public static deriveLocalAtRestAesKey(
|
|
509
|
+
identitySk: Uint8Array,
|
|
510
|
+
profile: CryptoProfile,
|
|
511
|
+
): Uint8Array {
|
|
512
|
+
if (profile === "tweetnacl") {
|
|
513
|
+
if (identitySk.length < 32) {
|
|
514
|
+
throw new Error(
|
|
515
|
+
"Expected at least 32 bytes of identity secret in tweetnacl mode.",
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
return identitySk.subarray(0, 32);
|
|
519
|
+
}
|
|
520
|
+
return new Uint8Array(
|
|
521
|
+
hkdf(
|
|
522
|
+
sha256,
|
|
523
|
+
identitySk,
|
|
524
|
+
new Uint8Array(0),
|
|
525
|
+
new TextEncoder().encode("vex:at-rest:2.1.0-fips"),
|
|
526
|
+
32,
|
|
527
|
+
),
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
|
|
121
531
|
/**
|
|
122
532
|
* Returns the empty header (32 0's)
|
|
123
533
|
*
|
|
@@ -158,7 +568,7 @@ export class XUtils {
|
|
|
158
568
|
): Uint8Array => {
|
|
159
569
|
const UNENCRYPTED_SIGNKEY = XUtils.decodeHex(keyToSave);
|
|
160
570
|
const OFFSET = 1000;
|
|
161
|
-
const rand =
|
|
571
|
+
const rand = provider().randomBytes(2);
|
|
162
572
|
const [N1 = 0, N2 = 0] = rand;
|
|
163
573
|
const iterations =
|
|
164
574
|
iterationOverride !== undefined && iterationOverride !== 0
|
|
@@ -171,7 +581,7 @@ export class XUtils {
|
|
|
171
581
|
dkLen: 32,
|
|
172
582
|
});
|
|
173
583
|
const NONCE = xMakeNonce();
|
|
174
|
-
const ENCRYPTED_SIGNKEY =
|
|
584
|
+
const ENCRYPTED_SIGNKEY = provider().secretbox(
|
|
175
585
|
UNENCRYPTED_SIGNKEY,
|
|
176
586
|
NONCE,
|
|
177
587
|
ENCRYPTION_KEY,
|
|
@@ -194,6 +604,56 @@ export class XUtils {
|
|
|
194
604
|
return result;
|
|
195
605
|
};
|
|
196
606
|
|
|
607
|
+
/**
|
|
608
|
+
* Async variant of encryptKeyData for cross-runtime/FIPS backends.
|
|
609
|
+
* Format remains [iterations(6)|salt(24)|nonce(24)|ciphertext(N)].
|
|
610
|
+
*/
|
|
611
|
+
public static encryptKeyDataAsync = async (
|
|
612
|
+
password: string,
|
|
613
|
+
keyToSave: string,
|
|
614
|
+
iterationOverride?: number,
|
|
615
|
+
): Promise<Uint8Array> => {
|
|
616
|
+
const UNENCRYPTED_SIGNKEY = XUtils.decodeHex(keyToSave);
|
|
617
|
+
const OFFSET = 1000;
|
|
618
|
+
const rand = xRandomBytes(2);
|
|
619
|
+
const [N1 = 0, N2 = 0] = rand;
|
|
620
|
+
const iterations =
|
|
621
|
+
iterationOverride !== undefined && iterationOverride !== 0
|
|
622
|
+
? iterationOverride
|
|
623
|
+
: N1 * N2 + OFFSET;
|
|
624
|
+
const ITERATIONS = XUtils.numberToUint8Arr(iterations);
|
|
625
|
+
const PKBDF_SALT = xMakeNonce();
|
|
626
|
+
const ENCRYPTION_KEY = noblePbkdf2(sha512, password, PKBDF_SALT, {
|
|
627
|
+
c: iterations,
|
|
628
|
+
dkLen: 32,
|
|
629
|
+
});
|
|
630
|
+
const NONCE = xMakeNonce();
|
|
631
|
+
const ENCRYPTED_SIGNKEY =
|
|
632
|
+
activeCryptoProfile === "fips"
|
|
633
|
+
? await xSecretboxAsync(
|
|
634
|
+
UNENCRYPTED_SIGNKEY,
|
|
635
|
+
NONCE,
|
|
636
|
+
ENCRYPTION_KEY,
|
|
637
|
+
)
|
|
638
|
+
: xSecretbox(UNENCRYPTED_SIGNKEY, NONCE, ENCRYPTION_KEY);
|
|
639
|
+
|
|
640
|
+
const result = new Uint8Array(
|
|
641
|
+
ITERATIONS.length +
|
|
642
|
+
PKBDF_SALT.length +
|
|
643
|
+
NONCE.length +
|
|
644
|
+
ENCRYPTED_SIGNKEY.length,
|
|
645
|
+
);
|
|
646
|
+
let offset = 0;
|
|
647
|
+
result.set(ITERATIONS, offset);
|
|
648
|
+
offset += ITERATIONS.length;
|
|
649
|
+
result.set(PKBDF_SALT, offset);
|
|
650
|
+
offset += PKBDF_SALT.length;
|
|
651
|
+
result.set(NONCE, offset);
|
|
652
|
+
offset += NONCE.length;
|
|
653
|
+
result.set(ENCRYPTED_SIGNKEY, offset);
|
|
654
|
+
return result;
|
|
655
|
+
};
|
|
656
|
+
|
|
197
657
|
/**
|
|
198
658
|
* Returns a six bit Uint8Array representation of an integer.
|
|
199
659
|
* The integer must be positive, and it must be able to be stored
|
|
@@ -335,16 +795,91 @@ export interface XConstants {
|
|
|
335
795
|
MIN_OTK_SUPPLY: number;
|
|
336
796
|
}
|
|
337
797
|
|
|
798
|
+
/**
|
|
799
|
+
* FIPS: `device.signKey` in the database is the P-256 ECDSA public key (SPKI),
|
|
800
|
+
* used for account/device signature verification. X3DH on the client expects
|
|
801
|
+
* the same curve point as Web Crypto "raw" P-256 ECDH public bytes for
|
|
802
|
+
* `importEcdhPublicKey`. This converts SPKI → raw without a private key.
|
|
803
|
+
*/
|
|
804
|
+
export async function fipsEcdhRawPublicKeyFromEcdsaSpkiAsync(
|
|
805
|
+
ecdsaSpki: Uint8Array,
|
|
806
|
+
): Promise<Uint8Array> {
|
|
807
|
+
if (ecdsaSpki.length === 0) {
|
|
808
|
+
throw new Error("FIPS: empty ECDSA SPKI.");
|
|
809
|
+
}
|
|
810
|
+
const subtle = getSubtleCrypto();
|
|
811
|
+
const ecdsaPub = await importEcdsaPublicKey(ecdsaSpki);
|
|
812
|
+
const jwk = await subtle.exportKey("jwk", ecdsaPub);
|
|
813
|
+
if (
|
|
814
|
+
jwk.x === undefined ||
|
|
815
|
+
jwk.y === undefined ||
|
|
816
|
+
jwk.x.length === 0 ||
|
|
817
|
+
jwk.y.length === 0
|
|
818
|
+
) {
|
|
819
|
+
throw new Error("FIPS: could not export ECDSA public as JWK.");
|
|
820
|
+
}
|
|
821
|
+
const ecdhJwk: JsonWebKey = { ...jwk };
|
|
822
|
+
delete ecdhJwk.d;
|
|
823
|
+
ecdhJwk.key_ops = [];
|
|
824
|
+
ecdhJwk.ext = true;
|
|
825
|
+
const ecdhPub = await subtle.importKey(
|
|
826
|
+
"jwk",
|
|
827
|
+
ecdhJwk,
|
|
828
|
+
{ name: "ECDH", namedCurve: "P-256" },
|
|
829
|
+
true,
|
|
830
|
+
[],
|
|
831
|
+
);
|
|
832
|
+
return new Uint8Array(await subtle.exportKey("raw", ecdhPub));
|
|
833
|
+
}
|
|
834
|
+
|
|
338
835
|
/** Generate a fresh X25519 box key pair. */
|
|
339
836
|
export function xBoxKeyPair(): KeyPair {
|
|
340
|
-
return
|
|
837
|
+
return provider().boxKeyPair();
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
/** Async box keypair generation for the active profile. */
|
|
841
|
+
export async function xBoxKeyPairAsync(): Promise<KeyPair> {
|
|
842
|
+
if (activeCryptoProfile === "tweetnacl") {
|
|
843
|
+
return xBoxKeyPair();
|
|
844
|
+
}
|
|
845
|
+
const subtle = getSubtleCrypto();
|
|
846
|
+
const pair = await subtle.generateKey(
|
|
847
|
+
{ name: "ECDH", namedCurve: "P-256" },
|
|
848
|
+
true,
|
|
849
|
+
["deriveBits"],
|
|
850
|
+
);
|
|
851
|
+
return {
|
|
852
|
+
publicKey: new Uint8Array(
|
|
853
|
+
await subtle.exportKey("raw", pair.publicKey),
|
|
854
|
+
),
|
|
855
|
+
secretKey: new Uint8Array(
|
|
856
|
+
await subtle.exportKey("pkcs8", pair.privateKey),
|
|
857
|
+
),
|
|
858
|
+
};
|
|
341
859
|
}
|
|
342
860
|
|
|
343
861
|
/** Restore an X25519 box key pair from a 32-byte secret key. */
|
|
344
862
|
export function xBoxKeyPairFromSecret(secretKey: Uint8Array): KeyPair {
|
|
345
|
-
return
|
|
863
|
+
return provider().boxKeyPairFromSecret(secretKey);
|
|
346
864
|
}
|
|
347
865
|
|
|
866
|
+
// ── Key pair type ───────────────────────────────────────────────────────────
|
|
867
|
+
|
|
868
|
+
/** Async box key restore from private key material. */
|
|
869
|
+
export async function xBoxKeyPairFromSecretAsync(
|
|
870
|
+
secretKey: Uint8Array,
|
|
871
|
+
): Promise<KeyPair> {
|
|
872
|
+
if (activeCryptoProfile === "tweetnacl") {
|
|
873
|
+
return xBoxKeyPairFromSecret(secretKey);
|
|
874
|
+
}
|
|
875
|
+
if (secretKey.length === 32) {
|
|
876
|
+
return fipsEcdhKeyPairFrom32ByteSeed(secretKey, getSubtleCrypto());
|
|
877
|
+
}
|
|
878
|
+
return fipsEcdhKeyPairFromPkcs8(secretKey, getSubtleCrypto());
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
// ── Key generation ─────────────────────────────────────────────────────────
|
|
882
|
+
|
|
348
883
|
/**
|
|
349
884
|
* Concatanates multiple Uint8Arrays.
|
|
350
885
|
*
|
|
@@ -382,10 +917,76 @@ export function xDH(
|
|
|
382
917
|
myPrivateKey: Uint8Array,
|
|
383
918
|
theirPublicKey: Uint8Array,
|
|
384
919
|
): Uint8Array {
|
|
385
|
-
return
|
|
920
|
+
return provider().boxBefore(myPrivateKey, theirPublicKey);
|
|
386
921
|
}
|
|
387
922
|
|
|
388
|
-
|
|
923
|
+
/** Async DH for cross-runtime/FIPS backends. */
|
|
924
|
+
export async function xDHAsync(
|
|
925
|
+
myPrivateKey: Uint8Array,
|
|
926
|
+
theirPublicKey: Uint8Array,
|
|
927
|
+
): Promise<Uint8Array> {
|
|
928
|
+
if (activeCryptoProfile === "tweetnacl") {
|
|
929
|
+
return xDH(myPrivateKey, theirPublicKey);
|
|
930
|
+
}
|
|
931
|
+
const subtle = getSubtleCrypto();
|
|
932
|
+
const privateKey = await importEcdhPrivateKey(myPrivateKey);
|
|
933
|
+
const publicKey = await importEcdhPublicKey(theirPublicKey);
|
|
934
|
+
const shared = await subtle.deriveBits(
|
|
935
|
+
{ name: "ECDH", public: publicKey },
|
|
936
|
+
privateKey,
|
|
937
|
+
256,
|
|
938
|
+
);
|
|
939
|
+
return new Uint8Array(shared);
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
/**
|
|
943
|
+
* In `fips` mode only: derive a P-256 ECDH `KeyPair` (raw public + pkcs8 secret)
|
|
944
|
+
* from a P-256 ECDSA `KeyPair` (spki + pkcs8) using the same private scalar in Web Crypto.
|
|
945
|
+
* In `tweetnacl` mode, use `XKeyConvert.convertKeyPair` to map Ed25519 → X25519 instead.
|
|
946
|
+
*/
|
|
947
|
+
export async function xEcdhKeyPairFromEcdsaKeyPairAsync(
|
|
948
|
+
sign: KeyPair,
|
|
949
|
+
): Promise<KeyPair> {
|
|
950
|
+
if (activeCryptoProfile === "tweetnacl") {
|
|
951
|
+
return Promise.reject(
|
|
952
|
+
new Error(
|
|
953
|
+
'xEcdhKeyPairFromEcdsaKeyPairAsync is for crypto profile "fips" only. Use XKeyConvert.convertKeyPair in tweetnacl mode.',
|
|
954
|
+
),
|
|
955
|
+
);
|
|
956
|
+
}
|
|
957
|
+
const subtle = getSubtleCrypto();
|
|
958
|
+
const ecdsaPriv = await importEcdsaPrivateKey(sign.secretKey);
|
|
959
|
+
const jwk = await subtle.exportKey("jwk", ecdsaPriv);
|
|
960
|
+
if (typeof jwk.d !== "string" || jwk.d.length === 0) {
|
|
961
|
+
throw new Error("FIPS: could not export ECDSA private as JWK.");
|
|
962
|
+
}
|
|
963
|
+
const ecdhJwk: JsonWebKey = { ...jwk, key_ops: ["deriveBits"] };
|
|
964
|
+
ecdhJwk.ext = true;
|
|
965
|
+
const ecdhPriv = await subtle.importKey(
|
|
966
|
+
"jwk",
|
|
967
|
+
ecdhJwk,
|
|
968
|
+
{ name: "ECDH", namedCurve: "P-256" },
|
|
969
|
+
true,
|
|
970
|
+
["deriveBits"],
|
|
971
|
+
);
|
|
972
|
+
const ecdhPubJwk = await subtle.exportKey("jwk", ecdhPriv);
|
|
973
|
+
const ecdhPubJwkNoD: JsonWebKey = { ...ecdhPubJwk };
|
|
974
|
+
delete ecdhPubJwkNoD.d;
|
|
975
|
+
ecdhPubJwkNoD.key_ops = [];
|
|
976
|
+
const ecdhPub = await subtle.importKey(
|
|
977
|
+
"jwk",
|
|
978
|
+
ecdhPubJwkNoD,
|
|
979
|
+
{ name: "ECDH", namedCurve: "P-256" },
|
|
980
|
+
true,
|
|
981
|
+
[],
|
|
982
|
+
);
|
|
983
|
+
return {
|
|
984
|
+
publicKey: new Uint8Array(await subtle.exportKey("raw", ecdhPub)),
|
|
985
|
+
secretKey: new Uint8Array(await subtle.exportKey("pkcs8", ecdhPriv)),
|
|
986
|
+
};
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
// ── Signing ────────────────────────────────────────────────────────────────
|
|
389
990
|
|
|
390
991
|
/**
|
|
391
992
|
* Encode an X25519 or X448 public key PK into a byte sequence.
|
|
@@ -431,8 +1032,6 @@ export function xEncode(
|
|
|
431
1032
|
return Uint8Array.from(bytes);
|
|
432
1033
|
}
|
|
433
1034
|
|
|
434
|
-
// ── Key generation ─────────────────────────────────────────────────────────
|
|
435
|
-
|
|
436
1035
|
/**
|
|
437
1036
|
* Hashes some data.
|
|
438
1037
|
*
|
|
@@ -443,6 +1042,8 @@ export function xHash(data: Uint8Array) {
|
|
|
443
1042
|
return XUtils.encodeHex(sha512(data));
|
|
444
1043
|
}
|
|
445
1044
|
|
|
1045
|
+
// ── Symmetric encryption (XSalsa20-Poly1305) ──────────────────────────────
|
|
1046
|
+
|
|
446
1047
|
export function xKDF(IKM: Uint8Array): Uint8Array {
|
|
447
1048
|
return hkdf(
|
|
448
1049
|
sha512,
|
|
@@ -457,23 +1058,43 @@ export function xKDF(IKM: Uint8Array): Uint8Array {
|
|
|
457
1058
|
* Returns a 24 byte random nonce of cryptographic quality.
|
|
458
1059
|
*/
|
|
459
1060
|
export function xMakeNonce(): Uint8Array {
|
|
460
|
-
return
|
|
1061
|
+
return provider().randomBytes(24);
|
|
461
1062
|
}
|
|
462
1063
|
|
|
1064
|
+
// ── Random ─────────────────────────────────────────────────────────────────
|
|
1065
|
+
|
|
463
1066
|
/** Cryptographically secure random bytes. */
|
|
464
1067
|
export function xRandomBytes(length: number): Uint8Array {
|
|
465
|
-
return
|
|
1068
|
+
return provider().randomBytes(length);
|
|
466
1069
|
}
|
|
467
1070
|
|
|
468
|
-
// ── Signing ────────────────────────────────────────────────────────────────
|
|
469
|
-
|
|
470
1071
|
/** Encrypt with a shared secret key. */
|
|
471
1072
|
export function xSecretbox(
|
|
472
1073
|
plaintext: Uint8Array,
|
|
473
1074
|
nonce: Uint8Array,
|
|
474
1075
|
key: Uint8Array,
|
|
475
1076
|
): Uint8Array {
|
|
476
|
-
return
|
|
1077
|
+
return provider().secretbox(plaintext, nonce, key);
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
/** Async authenticated encryption for cross-runtime/FIPS backends. */
|
|
1081
|
+
export async function xSecretboxAsync(
|
|
1082
|
+
plaintext: Uint8Array,
|
|
1083
|
+
nonce: Uint8Array,
|
|
1084
|
+
key: Uint8Array,
|
|
1085
|
+
): Promise<Uint8Array> {
|
|
1086
|
+
if (activeCryptoProfile === "tweetnacl") {
|
|
1087
|
+
return xSecretbox(plaintext, nonce, key);
|
|
1088
|
+
}
|
|
1089
|
+
const subtle = getSubtleCrypto();
|
|
1090
|
+
const iv = requireAesGcmNonce(nonce);
|
|
1091
|
+
const aesKey = await importAesKey(key, ["encrypt"]);
|
|
1092
|
+
const ciphertext = await subtle.encrypt(
|
|
1093
|
+
{ iv: toBufferSource(iv), name: "AES-GCM" },
|
|
1094
|
+
aesKey,
|
|
1095
|
+
toBufferSource(plaintext),
|
|
1096
|
+
);
|
|
1097
|
+
return new Uint8Array(ciphertext);
|
|
477
1098
|
}
|
|
478
1099
|
|
|
479
1100
|
/** Decrypt with a shared secret key. Returns null if authentication fails. */
|
|
@@ -482,26 +1103,97 @@ export function xSecretboxOpen(
|
|
|
482
1103
|
nonce: Uint8Array,
|
|
483
1104
|
key: Uint8Array,
|
|
484
1105
|
): null | Uint8Array {
|
|
485
|
-
return
|
|
1106
|
+
return provider().secretboxOpen(ciphertext, nonce, key);
|
|
486
1107
|
}
|
|
487
1108
|
|
|
488
|
-
|
|
1109
|
+
/** Async authenticated decryption for cross-runtime/FIPS backends. */
|
|
1110
|
+
export async function xSecretboxOpenAsync(
|
|
1111
|
+
ciphertext: Uint8Array,
|
|
1112
|
+
nonce: Uint8Array,
|
|
1113
|
+
key: Uint8Array,
|
|
1114
|
+
): Promise<null | Uint8Array> {
|
|
1115
|
+
if (activeCryptoProfile === "tweetnacl") {
|
|
1116
|
+
return xSecretboxOpen(ciphertext, nonce, key);
|
|
1117
|
+
}
|
|
1118
|
+
const subtle = getSubtleCrypto();
|
|
1119
|
+
const iv = requireAesGcmNonce(nonce);
|
|
1120
|
+
const aesKey = await importAesKey(key, ["decrypt"]);
|
|
1121
|
+
try {
|
|
1122
|
+
const plaintext = await subtle.decrypt(
|
|
1123
|
+
{ iv: toBufferSource(iv), name: "AES-GCM" },
|
|
1124
|
+
aesKey,
|
|
1125
|
+
toBufferSource(ciphertext),
|
|
1126
|
+
);
|
|
1127
|
+
return new Uint8Array(plaintext);
|
|
1128
|
+
} catch {
|
|
1129
|
+
return null;
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
489
1132
|
|
|
490
1133
|
/** Sign a message with an Ed25519 secret key. Returns signed message (64-byte signature prefix + message). */
|
|
491
1134
|
export function xSign(message: Uint8Array, secretKey: Uint8Array): Uint8Array {
|
|
492
|
-
return
|
|
1135
|
+
return provider().sign(message, secretKey);
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
/** Async signing for cross-runtime/FIPS backends. */
|
|
1139
|
+
export async function xSignAsync(
|
|
1140
|
+
message: Uint8Array,
|
|
1141
|
+
secretKey: Uint8Array,
|
|
1142
|
+
): Promise<Uint8Array> {
|
|
1143
|
+
if (activeCryptoProfile === "tweetnacl") {
|
|
1144
|
+
return xSign(message, secretKey);
|
|
1145
|
+
}
|
|
1146
|
+
const subtle = getSubtleCrypto();
|
|
1147
|
+
const privateKey = await importEcdsaPrivateKey(secretKey);
|
|
1148
|
+
const signature = new Uint8Array(
|
|
1149
|
+
await subtle.sign(
|
|
1150
|
+
{ hash: "SHA-256", name: "ECDSA" },
|
|
1151
|
+
privateKey,
|
|
1152
|
+
toBufferSource(message),
|
|
1153
|
+
),
|
|
1154
|
+
);
|
|
1155
|
+
return encodeFipsSignedMessage(signature, message);
|
|
493
1156
|
}
|
|
494
1157
|
|
|
495
1158
|
/** Generate a fresh Ed25519 signing key pair. */
|
|
496
1159
|
export function xSignKeyPair(): KeyPair {
|
|
497
|
-
return
|
|
1160
|
+
return provider().signKeyPair();
|
|
498
1161
|
}
|
|
499
1162
|
|
|
500
|
-
|
|
1163
|
+
/** Async keypair generation for the active profile. */
|
|
1164
|
+
export async function xSignKeyPairAsync(): Promise<KeyPair> {
|
|
1165
|
+
if (activeCryptoProfile === "tweetnacl") {
|
|
1166
|
+
return xSignKeyPair();
|
|
1167
|
+
}
|
|
1168
|
+
const subtle = getSubtleCrypto();
|
|
1169
|
+
const pair = await subtle.generateKey(
|
|
1170
|
+
{ name: "ECDSA", namedCurve: "P-256" },
|
|
1171
|
+
true,
|
|
1172
|
+
["sign", "verify"],
|
|
1173
|
+
);
|
|
1174
|
+
return {
|
|
1175
|
+
publicKey: new Uint8Array(
|
|
1176
|
+
await subtle.exportKey("spki", pair.publicKey),
|
|
1177
|
+
),
|
|
1178
|
+
secretKey: new Uint8Array(
|
|
1179
|
+
await subtle.exportKey("pkcs8", pair.privateKey),
|
|
1180
|
+
),
|
|
1181
|
+
};
|
|
1182
|
+
}
|
|
501
1183
|
|
|
502
1184
|
/** Restore an Ed25519 signing key pair from a 64-byte secret key. */
|
|
503
1185
|
export function xSignKeyPairFromSecret(secretKey: Uint8Array): KeyPair {
|
|
504
|
-
return
|
|
1186
|
+
return provider().signKeyPairFromSecret(secretKey);
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
/** Async restore of signing keypair for the active profile. */
|
|
1190
|
+
export async function xSignKeyPairFromSecretAsync(
|
|
1191
|
+
secretKey: Uint8Array,
|
|
1192
|
+
): Promise<KeyPair> {
|
|
1193
|
+
if (activeCryptoProfile === "tweetnacl") {
|
|
1194
|
+
return xSignKeyPairFromSecret(secretKey);
|
|
1195
|
+
}
|
|
1196
|
+
return fipsEcdsaKeyPairFromPkcs8(secretKey, getSubtleCrypto());
|
|
505
1197
|
}
|
|
506
1198
|
|
|
507
1199
|
/** Verify and open a signed message. Returns the original message, or null if verification fails. */
|
|
@@ -509,7 +1201,104 @@ export function xSignOpen(
|
|
|
509
1201
|
signedMessage: Uint8Array,
|
|
510
1202
|
publicKey: Uint8Array,
|
|
511
1203
|
): null | Uint8Array {
|
|
512
|
-
return
|
|
1204
|
+
return provider().signOpen(signedMessage, publicKey);
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
/** Async verify/open for cross-runtime/FIPS backends. */
|
|
1208
|
+
export async function xSignOpenAsync(
|
|
1209
|
+
signedMessage: Uint8Array,
|
|
1210
|
+
publicKey: Uint8Array,
|
|
1211
|
+
): Promise<null | Uint8Array> {
|
|
1212
|
+
if (activeCryptoProfile === "tweetnacl") {
|
|
1213
|
+
return xSignOpen(signedMessage, publicKey);
|
|
1214
|
+
}
|
|
1215
|
+
const subtle = getSubtleCrypto();
|
|
1216
|
+
const parsed = decodeFipsSignedMessage(signedMessage);
|
|
1217
|
+
const verifyKey = await importEcdsaPublicKey(publicKey);
|
|
1218
|
+
const valid = await subtle.verify(
|
|
1219
|
+
{ hash: "SHA-256", name: "ECDSA" },
|
|
1220
|
+
verifyKey,
|
|
1221
|
+
toBufferSource(parsed.signature),
|
|
1222
|
+
toBufferSource(parsed.message),
|
|
1223
|
+
);
|
|
1224
|
+
return valid ? parsed.message : null;
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
async function fipsEcdsaKeyPairFromPkcs8(
|
|
1228
|
+
secretKey: Uint8Array,
|
|
1229
|
+
subtle: SubtleCrypto,
|
|
1230
|
+
): Promise<KeyPair> {
|
|
1231
|
+
const ecdsaPriv = await importEcdsaPrivateKey(secretKey);
|
|
1232
|
+
const jwk = await subtle.exportKey("jwk", ecdsaPriv);
|
|
1233
|
+
const ecdsaPubJwk: JsonWebKey = { ...jwk };
|
|
1234
|
+
delete ecdsaPubJwk.d;
|
|
1235
|
+
ecdsaPubJwk.key_ops = ["verify"];
|
|
1236
|
+
const ecdsaPub = await subtle.importKey(
|
|
1237
|
+
"jwk",
|
|
1238
|
+
ecdsaPubJwk,
|
|
1239
|
+
{ name: "ECDSA", namedCurve: "P-256" },
|
|
1240
|
+
true,
|
|
1241
|
+
["verify"],
|
|
1242
|
+
);
|
|
1243
|
+
return {
|
|
1244
|
+
publicKey: new Uint8Array(await subtle.exportKey("spki", ecdsaPub)),
|
|
1245
|
+
secretKey: Uint8Array.from(secretKey),
|
|
1246
|
+
};
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
async function importAesKey(
|
|
1250
|
+
key: Uint8Array,
|
|
1251
|
+
usages: WebCryptoKeyUsage[],
|
|
1252
|
+
): Promise<CryptoKey> {
|
|
1253
|
+
return getSubtleCrypto().importKey(
|
|
1254
|
+
"raw",
|
|
1255
|
+
toBufferSource(key),
|
|
1256
|
+
{ name: "AES-GCM" },
|
|
1257
|
+
false,
|
|
1258
|
+
usages,
|
|
1259
|
+
);
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
async function importEcdhPrivateKey(secretKey: Uint8Array): Promise<CryptoKey> {
|
|
1263
|
+
return getSubtleCrypto().importKey(
|
|
1264
|
+
"pkcs8",
|
|
1265
|
+
toBufferSource(secretKey),
|
|
1266
|
+
{ name: "ECDH", namedCurve: "P-256" },
|
|
1267
|
+
true,
|
|
1268
|
+
["deriveBits"],
|
|
1269
|
+
);
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
async function importEcdhPublicKey(publicKey: Uint8Array): Promise<CryptoKey> {
|
|
1273
|
+
return getSubtleCrypto().importKey(
|
|
1274
|
+
"raw",
|
|
1275
|
+
toBufferSource(publicKey),
|
|
1276
|
+
{ name: "ECDH", namedCurve: "P-256" },
|
|
1277
|
+
true,
|
|
1278
|
+
[],
|
|
1279
|
+
);
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
async function importEcdsaPrivateKey(
|
|
1283
|
+
secretKey: Uint8Array,
|
|
1284
|
+
): Promise<CryptoKey> {
|
|
1285
|
+
return getSubtleCrypto().importKey(
|
|
1286
|
+
"pkcs8",
|
|
1287
|
+
toBufferSource(secretKey),
|
|
1288
|
+
{ name: "ECDSA", namedCurve: "P-256" },
|
|
1289
|
+
true,
|
|
1290
|
+
["sign"],
|
|
1291
|
+
);
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
async function importEcdsaPublicKey(publicKey: Uint8Array): Promise<CryptoKey> {
|
|
1295
|
+
return getSubtleCrypto().importKey(
|
|
1296
|
+
"spki",
|
|
1297
|
+
toBufferSource(publicKey),
|
|
1298
|
+
{ name: "ECDSA", namedCurve: "P-256" },
|
|
1299
|
+
true,
|
|
1300
|
+
["verify"],
|
|
1301
|
+
);
|
|
513
1302
|
}
|
|
514
1303
|
|
|
515
1304
|
/**
|