@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/dist/index.js
CHANGED
|
@@ -3,8 +3,6 @@
|
|
|
3
3
|
* Licensed under AGPL-3.0. See LICENSE for details.
|
|
4
4
|
* Commercial licenses available at vex.wtf
|
|
5
5
|
*/
|
|
6
|
-
import { numberToBytesBE } from "@noble/curves/abstract/utils";
|
|
7
|
-
import { p256 } from "@noble/curves/p256";
|
|
8
6
|
import { hkdf } from "@noble/hashes/hkdf.js";
|
|
9
7
|
import { hmac } from "@noble/hashes/hmac.js";
|
|
10
8
|
import { pbkdf2 as noblePbkdf2 } from "@noble/hashes/pbkdf2.js";
|
|
@@ -16,13 +14,11 @@ import ed2curve from "ed2curve";
|
|
|
16
14
|
import { Packr } from "msgpackr";
|
|
17
15
|
import nacl from "tweetnacl";
|
|
18
16
|
import { z } from "zod/v4";
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
return cryptoCandidate;
|
|
25
|
-
}
|
|
17
|
+
const KEY_DATA_HEADER_BYTES = 54;
|
|
18
|
+
const KEY_DATA_MAC_BYTES = 16;
|
|
19
|
+
const KEY_DATA_PBKDF2_ITERATIONS = 220_000;
|
|
20
|
+
const KEY_DATA_PBKDF2_MIN_ITERATIONS = 1_000;
|
|
21
|
+
const KEY_DATA_PBKDF2_MAX_ITERATIONS = 2_000_000;
|
|
26
22
|
function isWebCryptoLike(value) {
|
|
27
23
|
if (typeof value !== "object" || value === null) {
|
|
28
24
|
return false;
|
|
@@ -33,29 +29,27 @@ function isWebCryptoLike(value) {
|
|
|
33
29
|
typeof value.subtle === "object" &&
|
|
34
30
|
value.subtle !== null);
|
|
35
31
|
}
|
|
36
|
-
function
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
while (offset < length) {
|
|
44
|
-
const chunkLength = Math.min(65536, length - offset);
|
|
45
|
-
const chunk = result.subarray(offset, offset + chunkLength);
|
|
46
|
-
cryptoImpl.getRandomValues(chunk);
|
|
47
|
-
offset += chunkLength;
|
|
32
|
+
async function pbkdf2Sha512Async(password, salt, iterations) {
|
|
33
|
+
const cryptoCandidate = globalThis.crypto;
|
|
34
|
+
if (!isWebCryptoLike(cryptoCandidate)) {
|
|
35
|
+
return noblePbkdf2(sha512, password, salt, {
|
|
36
|
+
c: iterations,
|
|
37
|
+
dkLen: 32,
|
|
38
|
+
});
|
|
48
39
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
40
|
+
const passwordKey = await cryptoCandidate.subtle.importKey("raw", new TextEncoder().encode(password), "PBKDF2", false, ["deriveBits"]);
|
|
41
|
+
const bits = await cryptoCandidate.subtle.deriveBits({
|
|
42
|
+
hash: "SHA-512",
|
|
43
|
+
iterations,
|
|
44
|
+
name: "PBKDF2",
|
|
45
|
+
salt: new Uint8Array(salt),
|
|
46
|
+
}, passwordKey, 256);
|
|
47
|
+
return new Uint8Array(bits);
|
|
53
48
|
}
|
|
54
49
|
const tweetnaclProvider = {
|
|
55
50
|
boxBefore: (myPrivateKey, theirPublicKey) => nacl.box.before(theirPublicKey, myPrivateKey),
|
|
56
51
|
boxKeyPair: () => nacl.box.keyPair(),
|
|
57
52
|
boxKeyPairFromSecret: (secretKey) => nacl.box.keyPair.fromSecretKey(secretKey),
|
|
58
|
-
profile: "tweetnacl",
|
|
59
53
|
randomBytes: (length) => nacl.randomBytes(length),
|
|
60
54
|
secretbox: (plaintext, nonce, key) => nacl.secretbox(plaintext, nonce, key),
|
|
61
55
|
secretboxOpen: (ciphertext, nonce, key) => nacl.secretbox.open(ciphertext, nonce, key),
|
|
@@ -64,182 +58,8 @@ const tweetnaclProvider = {
|
|
|
64
58
|
signKeyPairFromSecret: (secretKey) => nacl.sign.keyPair.fromSecretKey(secretKey),
|
|
65
59
|
signOpen: (signedMessage, publicKey) => nacl.sign.open(signedMessage, publicKey),
|
|
66
60
|
};
|
|
67
|
-
const fipsProvider = {
|
|
68
|
-
boxBefore: (myPrivateKey, theirPublicKey) => {
|
|
69
|
-
void myPrivateKey;
|
|
70
|
-
void theirPublicKey;
|
|
71
|
-
return unsupported("fips", "xDH");
|
|
72
|
-
},
|
|
73
|
-
boxKeyPair: () => unsupported("fips", "xBoxKeyPair"),
|
|
74
|
-
boxKeyPairFromSecret: (secretKey) => {
|
|
75
|
-
void secretKey;
|
|
76
|
-
return unsupported("fips", "xBoxKeyPairFromSecret");
|
|
77
|
-
},
|
|
78
|
-
profile: "fips",
|
|
79
|
-
randomBytes: (length) => randomBytesWebCrypto(length),
|
|
80
|
-
secretbox: (plaintext, nonce, key) => {
|
|
81
|
-
void plaintext;
|
|
82
|
-
void nonce;
|
|
83
|
-
void key;
|
|
84
|
-
return unsupported("fips", "xSecretbox");
|
|
85
|
-
},
|
|
86
|
-
secretboxOpen: (ciphertext, nonce, key) => {
|
|
87
|
-
void ciphertext;
|
|
88
|
-
void nonce;
|
|
89
|
-
void key;
|
|
90
|
-
return unsupported("fips", "xSecretboxOpen");
|
|
91
|
-
},
|
|
92
|
-
sign: (message, secretKey) => {
|
|
93
|
-
void message;
|
|
94
|
-
void secretKey;
|
|
95
|
-
return unsupported("fips", "xSign");
|
|
96
|
-
},
|
|
97
|
-
signKeyPair: () => unsupported("fips", "xSignKeyPair"),
|
|
98
|
-
signKeyPairFromSecret: (secretKey) => {
|
|
99
|
-
void secretKey;
|
|
100
|
-
return unsupported("fips", "xSignKeyPairFromSecret");
|
|
101
|
-
},
|
|
102
|
-
signOpen: (signedMessage, publicKey) => {
|
|
103
|
-
void signedMessage;
|
|
104
|
-
void publicKey;
|
|
105
|
-
return unsupported("fips", "xSignOpen");
|
|
106
|
-
},
|
|
107
|
-
};
|
|
108
|
-
const providers = {
|
|
109
|
-
fips: fipsProvider,
|
|
110
|
-
tweetnacl: tweetnaclProvider,
|
|
111
|
-
};
|
|
112
|
-
let activeCryptoProfile = "tweetnacl";
|
|
113
|
-
let activeCryptoProvider = providers[activeCryptoProfile];
|
|
114
|
-
/** Returns the currently configured crypto profile. */
|
|
115
|
-
export function getCryptoProfile() {
|
|
116
|
-
return activeCryptoProfile;
|
|
117
|
-
}
|
|
118
|
-
/**
|
|
119
|
-
* Sets the runtime crypto profile.
|
|
120
|
-
*
|
|
121
|
-
* `tweetnacl` preserves existing behavior.
|
|
122
|
-
* `fips` currently enables only backend-agnostic helpers; NaCl-coupled
|
|
123
|
-
* primitives throw until a FIPS backend implementation is wired in.
|
|
124
|
-
*/
|
|
125
|
-
export function setCryptoProfile(profile) {
|
|
126
|
-
activeCryptoProfile = profile;
|
|
127
|
-
activeCryptoProvider = providers[profile];
|
|
128
|
-
}
|
|
129
|
-
const cryptoProfileScopeStack = [];
|
|
130
|
-
/**
|
|
131
|
-
* Saves the current profile and switches to `profile`. Pair every call with
|
|
132
|
-
* {@link leaveCryptoProfileScope} in a `finally` block.
|
|
133
|
-
*
|
|
134
|
-
* **Why:** `setCryptoProfile` is process-wide. Several async libvex `Client`s
|
|
135
|
-
* can overlap on `readMail`; a naive save/restore in `finally` can reset the
|
|
136
|
-
* profile while another client still needs FIPS — `xSecretboxOpenAsync` then
|
|
137
|
-
* reads `tweetnacl` and fails to decrypt AES-GCM payloads. Nesting this stack
|
|
138
|
-
* fixes that.
|
|
139
|
-
*/
|
|
140
|
-
export function enterCryptoProfileScope(profile) {
|
|
141
|
-
cryptoProfileScopeStack.push(activeCryptoProfile);
|
|
142
|
-
setCryptoProfile(profile);
|
|
143
|
-
}
|
|
144
|
-
/** Restores the profile saved by the innermost {@link enterCryptoProfileScope}. */
|
|
145
|
-
export function leaveCryptoProfileScope() {
|
|
146
|
-
const prev = cryptoProfileScopeStack.pop();
|
|
147
|
-
if (prev === undefined) {
|
|
148
|
-
throw new Error("leaveCryptoProfileScope called without a matching enterCryptoProfileScope");
|
|
149
|
-
}
|
|
150
|
-
setCryptoProfile(prev);
|
|
151
|
-
}
|
|
152
|
-
function bytesToBase64Url(bytes) {
|
|
153
|
-
return globalThis
|
|
154
|
-
.btoa(String.fromCodePoint(...Array.from(bytes)))
|
|
155
|
-
.replace(/\+/g, "-")
|
|
156
|
-
.replace(/\//g, "_")
|
|
157
|
-
.replace(/=+/g, "");
|
|
158
|
-
}
|
|
159
|
-
function concatBytes(a, b) {
|
|
160
|
-
const out = new Uint8Array(a.length + b.length);
|
|
161
|
-
out.set(a, 0);
|
|
162
|
-
out.set(b, a.length);
|
|
163
|
-
return out;
|
|
164
|
-
}
|
|
165
|
-
function decodeFipsSignedMessage(signedMessage) {
|
|
166
|
-
if (signedMessage.length < 2) {
|
|
167
|
-
throw new Error("Invalid FIPS signed message: missing signature length prefix.");
|
|
168
|
-
}
|
|
169
|
-
const signatureLength = (signedMessage[0] ?? 0) * 256 + (signedMessage[1] ?? 0);
|
|
170
|
-
const signatureStart = 2;
|
|
171
|
-
const signatureEnd = signatureStart + signatureLength;
|
|
172
|
-
if (signedMessage.length < signatureEnd) {
|
|
173
|
-
throw new Error("Invalid FIPS signed message: signature length exceeds message length.");
|
|
174
|
-
}
|
|
175
|
-
return {
|
|
176
|
-
message: signedMessage.slice(signatureEnd),
|
|
177
|
-
signature: signedMessage.slice(signatureStart, signatureEnd),
|
|
178
|
-
};
|
|
179
|
-
}
|
|
180
|
-
function encodeFipsSignedMessage(signature, message) {
|
|
181
|
-
if (signature.length > 65535) {
|
|
182
|
-
throw new Error("FIPS signature too long to encode.");
|
|
183
|
-
}
|
|
184
|
-
const prefix = new Uint8Array(2);
|
|
185
|
-
prefix[0] = (signature.length >> 8) & 0xff;
|
|
186
|
-
prefix[1] = signature.length & 0xff;
|
|
187
|
-
return concatBytes(concatBytes(prefix, signature), message);
|
|
188
|
-
}
|
|
189
|
-
async function fipsEcdhKeyPairFrom32ByteSeed(secretKey, subtle) {
|
|
190
|
-
if (secretKey.length !== 32) {
|
|
191
|
-
throw new Error("FIPS: expected a 32-byte IKM/seed for ECDH from-secret.");
|
|
192
|
-
}
|
|
193
|
-
const d = p256.utils.normPrivateKeyToScalar(Uint8Array.from(secretKey));
|
|
194
|
-
const d32 = numberToBytesBE(d, 32);
|
|
195
|
-
const rawPub = p256.getPublicKey(d, false);
|
|
196
|
-
if (rawPub[0] !== 0x04) {
|
|
197
|
-
throw new Error("FIPS: expected uncompressed P-256 public key.");
|
|
198
|
-
}
|
|
199
|
-
const jwk = {
|
|
200
|
-
crv: "P-256",
|
|
201
|
-
d: bytesToBase64Url(d32),
|
|
202
|
-
kty: "EC",
|
|
203
|
-
x: bytesToBase64Url(rawPub.subarray(1, 33)),
|
|
204
|
-
y: bytesToBase64Url(rawPub.subarray(33, 65)),
|
|
205
|
-
};
|
|
206
|
-
const ecdhPriv = await subtle.importKey("jwk", jwk, { name: "ECDH", namedCurve: "P-256" }, true, ["deriveBits"]);
|
|
207
|
-
const ecdhPubJwk = await subtle.exportKey("jwk", ecdhPriv);
|
|
208
|
-
const ecdhPubJwkNoD = { ...ecdhPubJwk };
|
|
209
|
-
delete ecdhPubJwkNoD.d;
|
|
210
|
-
ecdhPubJwkNoD.key_ops = [];
|
|
211
|
-
const ecdhPub = await subtle.importKey("jwk", ecdhPubJwkNoD, { name: "ECDH", namedCurve: "P-256" }, true, []);
|
|
212
|
-
return {
|
|
213
|
-
publicKey: new Uint8Array(await subtle.exportKey("raw", ecdhPub)),
|
|
214
|
-
secretKey: new Uint8Array(await subtle.exportKey("pkcs8", ecdhPriv)),
|
|
215
|
-
};
|
|
216
|
-
}
|
|
217
|
-
async function fipsEcdhKeyPairFromPkcs8(secretKey, subtle) {
|
|
218
|
-
const ecdhPriv = await subtle.importKey("pkcs8", toBufferSource(secretKey), { name: "ECDH", namedCurve: "P-256" }, true, ["deriveBits"]);
|
|
219
|
-
const jwk = await subtle.exportKey("jwk", ecdhPriv);
|
|
220
|
-
const ecdhPubJwk = { ...jwk };
|
|
221
|
-
delete ecdhPubJwk.d;
|
|
222
|
-
ecdhPubJwk.key_ops = [];
|
|
223
|
-
const ecdhPub = await subtle.importKey("jwk", ecdhPubJwk, { name: "ECDH", namedCurve: "P-256" }, true, []);
|
|
224
|
-
return {
|
|
225
|
-
publicKey: new Uint8Array(await subtle.exportKey("raw", ecdhPub)),
|
|
226
|
-
secretKey: new Uint8Array(await subtle.exportKey("pkcs8", ecdhPriv)),
|
|
227
|
-
};
|
|
228
|
-
}
|
|
229
|
-
function getSubtleCrypto() {
|
|
230
|
-
return getWebCrypto().subtle;
|
|
231
|
-
}
|
|
232
61
|
function provider() {
|
|
233
|
-
return
|
|
234
|
-
}
|
|
235
|
-
function requireAesGcmNonce(nonce) {
|
|
236
|
-
if (nonce.length < 12) {
|
|
237
|
-
throw new Error(`AES-GCM requires a nonce of at least 12 bytes, received ${String(nonce.length)}.`);
|
|
238
|
-
}
|
|
239
|
-
return nonce.slice(0, 12);
|
|
240
|
-
}
|
|
241
|
-
function toBufferSource(bytes) {
|
|
242
|
-
return Uint8Array.from(bytes).buffer;
|
|
62
|
+
return tweetnaclProvider;
|
|
243
63
|
}
|
|
244
64
|
// msgpackr with useRecords:false emits standard msgpack (no nonstandard record extension).
|
|
245
65
|
// moreTypes:false keeps the extension set to only what other decoders understand.
|
|
@@ -266,8 +86,8 @@ export class XUtils {
|
|
|
266
86
|
* Checks if two buffer-like objects are equal.
|
|
267
87
|
* When lengths match, comparison is constant-time in the inputs (no early exit on first differing byte).
|
|
268
88
|
*
|
|
269
|
-
* @param buf1
|
|
270
|
-
* @param buf2
|
|
89
|
+
* @param buf1 - First buffer to compare.
|
|
90
|
+
* @param buf2 - Second buffer to compare.
|
|
271
91
|
*
|
|
272
92
|
* @returns True if equal, else false.
|
|
273
93
|
*/
|
|
@@ -297,19 +117,25 @@ export class XUtils {
|
|
|
297
117
|
if (hexString.length === 0) {
|
|
298
118
|
return new Uint8Array();
|
|
299
119
|
}
|
|
300
|
-
|
|
301
|
-
|
|
120
|
+
if (hexString.length % 2 !== 0 || !/^[0-9a-fA-F]+$/.test(hexString)) {
|
|
121
|
+
throw new Error("Expected an even-length hexadecimal string.");
|
|
122
|
+
}
|
|
123
|
+
const bytes = new Uint8Array(hexString.length / 2);
|
|
124
|
+
for (let i = 0; i < bytes.length; i += 1) {
|
|
125
|
+
bytes[i] = Number.parseInt(hexString.slice(i * 2, i * 2 + 2), 16);
|
|
126
|
+
}
|
|
127
|
+
return bytes;
|
|
302
128
|
}
|
|
303
129
|
/**
|
|
304
130
|
* Decrypts a secret key from the binary format produced by encryptKeyData().
|
|
305
131
|
* No I/O — the caller handles reading the data.
|
|
306
132
|
*
|
|
307
|
-
* @param keyData The encrypted key data as a Uint8Array.
|
|
308
|
-
* @param password The password used to encrypt.
|
|
133
|
+
* @param keyData - The encrypted key data as a Uint8Array.
|
|
134
|
+
* @param password - The password used to encrypt.
|
|
309
135
|
* @returns The hex-encoded secret key.
|
|
310
136
|
*/
|
|
311
137
|
static decryptKeyData = (keyData, password) => {
|
|
312
|
-
const ITERATIONS = XUtils.
|
|
138
|
+
const ITERATIONS = XUtils.readKeyDataIterations(keyData);
|
|
313
139
|
const PKBDF_SALT = keyData.slice(6, 30);
|
|
314
140
|
const ENCRYPTION_NONCE = keyData.slice(30, 54);
|
|
315
141
|
const ENCRYPTED_KEY = keyData.slice(54);
|
|
@@ -317,47 +143,48 @@ export class XUtils {
|
|
|
317
143
|
c: ITERATIONS,
|
|
318
144
|
dkLen: 32,
|
|
319
145
|
});
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
146
|
+
try {
|
|
147
|
+
const DECRYPTED_SIGNKEY = provider().secretboxOpen(ENCRYPTED_KEY, ENCRYPTION_NONCE, DERIVED_KEY);
|
|
148
|
+
if (DECRYPTED_SIGNKEY === null) {
|
|
149
|
+
throw new Error("Decryption failed. Wrong password?");
|
|
150
|
+
}
|
|
151
|
+
const decryptedHex = XUtils.encodeHex(DECRYPTED_SIGNKEY);
|
|
152
|
+
DECRYPTED_SIGNKEY.fill(0);
|
|
153
|
+
return decryptedHex;
|
|
154
|
+
}
|
|
155
|
+
finally {
|
|
156
|
+
DERIVED_KEY.fill(0);
|
|
323
157
|
}
|
|
324
|
-
return XUtils.encodeHex(DECRYPTED_SIGNKEY);
|
|
325
158
|
};
|
|
326
|
-
/**
|
|
327
|
-
* Async variant of decryptKeyData for cross-runtime/FIPS backends.
|
|
328
|
-
* Supports both profile formats emitted by encryptKeyDataAsync.
|
|
329
|
-
*/
|
|
159
|
+
/** Async variant of decryptKeyData for cross-runtime callers. */
|
|
330
160
|
static decryptKeyDataAsync = async (keyData, password) => {
|
|
331
|
-
const ITERATIONS = XUtils.
|
|
161
|
+
const ITERATIONS = XUtils.readKeyDataIterations(keyData);
|
|
332
162
|
const PKBDF_SALT = keyData.slice(6, 30);
|
|
333
163
|
const ENCRYPTION_NONCE = keyData.slice(30, 54);
|
|
334
164
|
const ENCRYPTED_KEY = keyData.slice(54);
|
|
335
|
-
const DERIVED_KEY =
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
165
|
+
const DERIVED_KEY = await pbkdf2Sha512Async(password, PKBDF_SALT, ITERATIONS);
|
|
166
|
+
try {
|
|
167
|
+
const decrypted = xSecretboxOpen(ENCRYPTED_KEY, ENCRYPTION_NONCE, DERIVED_KEY);
|
|
168
|
+
if (decrypted === null) {
|
|
169
|
+
throw new Error("Decryption failed. Wrong password?");
|
|
170
|
+
}
|
|
171
|
+
const decryptedHex = XUtils.encodeHex(decrypted);
|
|
172
|
+
decrypted.fill(0);
|
|
173
|
+
return decryptedHex;
|
|
174
|
+
}
|
|
175
|
+
finally {
|
|
176
|
+
DERIVED_KEY.fill(0);
|
|
344
177
|
}
|
|
345
|
-
return XUtils.encodeHex(decrypted);
|
|
346
178
|
};
|
|
347
179
|
/**
|
|
348
|
-
* 32-byte
|
|
349
|
-
*
|
|
350
|
-
* For `fips` the identity secret is PKCS#8; HKDF is applied so AES keys never
|
|
351
|
-
* equal the raw private key material.
|
|
180
|
+
* Derive a purpose-separated 32-byte key for local at-rest encryption.
|
|
181
|
+
* The result never aliases raw identity-key bytes.
|
|
352
182
|
*/
|
|
353
|
-
static deriveLocalAtRestAesKey(identitySk
|
|
354
|
-
if (
|
|
355
|
-
|
|
356
|
-
throw new Error("Expected at least 32 bytes of identity secret in tweetnacl mode.");
|
|
357
|
-
}
|
|
358
|
-
return identitySk.subarray(0, 32);
|
|
183
|
+
static deriveLocalAtRestAesKey(identitySk) {
|
|
184
|
+
if (identitySk.length < 32) {
|
|
185
|
+
throw new Error("Expected at least 32 bytes of identity secret.");
|
|
359
186
|
}
|
|
360
|
-
return new Uint8Array(hkdf(sha256, identitySk, new Uint8Array(0), new TextEncoder().encode("vex:at-rest:
|
|
187
|
+
return new Uint8Array(hkdf(sha256, identitySk, new Uint8Array(0), new TextEncoder().encode("vex:at-rest:3:tweetnacl"), 32));
|
|
361
188
|
}
|
|
362
189
|
/**
|
|
363
190
|
* Returns the empty header (32 0's)
|
|
@@ -382,19 +209,14 @@ export class XUtils {
|
|
|
382
209
|
*
|
|
383
210
|
* Format: [iterations(6)|salt(24)|nonce(24)|ciphertext(N)]
|
|
384
211
|
*
|
|
385
|
-
* @param password The password to derive the encryption key from.
|
|
386
|
-
* @param keyToSave The hex-encoded secret key to encrypt.
|
|
387
|
-
* @param iterationOverride Optional PBKDF2 iteration count (
|
|
212
|
+
* @param password - The password to derive the encryption key from.
|
|
213
|
+
* @param keyToSave - The hex-encoded secret key to encrypt.
|
|
214
|
+
* @param iterationOverride - Optional PBKDF2 iteration count (220,000 if omitted).
|
|
388
215
|
* @returns The encrypted key data as a Uint8Array.
|
|
389
216
|
*/
|
|
390
217
|
static encryptKeyData = (password, keyToSave, iterationOverride) => {
|
|
391
218
|
const UNENCRYPTED_SIGNKEY = XUtils.decodeHex(keyToSave);
|
|
392
|
-
const
|
|
393
|
-
const rand = provider().randomBytes(2);
|
|
394
|
-
const [N1 = 0, N2 = 0] = rand;
|
|
395
|
-
const iterations = iterationOverride !== undefined && iterationOverride !== 0
|
|
396
|
-
? iterationOverride
|
|
397
|
-
: N1 * N2 + OFFSET;
|
|
219
|
+
const iterations = XUtils.validateKeyDataIterations(iterationOverride ?? KEY_DATA_PBKDF2_ITERATIONS);
|
|
398
220
|
const ITERATIONS = XUtils.numberToUint8Arr(iterations);
|
|
399
221
|
const PKBDF_SALT = xMakeNonce();
|
|
400
222
|
const ENCRYPTION_KEY = noblePbkdf2(sha512, password, PKBDF_SALT, {
|
|
@@ -402,7 +224,14 @@ export class XUtils {
|
|
|
402
224
|
dkLen: 32,
|
|
403
225
|
});
|
|
404
226
|
const NONCE = xMakeNonce();
|
|
405
|
-
|
|
227
|
+
let ENCRYPTED_SIGNKEY;
|
|
228
|
+
try {
|
|
229
|
+
ENCRYPTED_SIGNKEY = provider().secretbox(UNENCRYPTED_SIGNKEY, NONCE, ENCRYPTION_KEY);
|
|
230
|
+
}
|
|
231
|
+
finally {
|
|
232
|
+
ENCRYPTION_KEY.fill(0);
|
|
233
|
+
UNENCRYPTED_SIGNKEY.fill(0);
|
|
234
|
+
}
|
|
406
235
|
const result = new Uint8Array(ITERATIONS.length +
|
|
407
236
|
PKBDF_SALT.length +
|
|
408
237
|
NONCE.length +
|
|
@@ -417,28 +246,22 @@ export class XUtils {
|
|
|
417
246
|
result.set(ENCRYPTED_SIGNKEY, offset);
|
|
418
247
|
return result;
|
|
419
248
|
};
|
|
420
|
-
/**
|
|
421
|
-
* Async variant of encryptKeyData for cross-runtime/FIPS backends.
|
|
422
|
-
* Format remains [iterations(6)|salt(24)|nonce(24)|ciphertext(N)].
|
|
423
|
-
*/
|
|
249
|
+
/** Async variant of encryptKeyData for cross-runtime callers. */
|
|
424
250
|
static encryptKeyDataAsync = async (password, keyToSave, iterationOverride) => {
|
|
425
251
|
const UNENCRYPTED_SIGNKEY = XUtils.decodeHex(keyToSave);
|
|
426
|
-
const
|
|
427
|
-
const rand = xRandomBytes(2);
|
|
428
|
-
const [N1 = 0, N2 = 0] = rand;
|
|
429
|
-
const iterations = iterationOverride !== undefined && iterationOverride !== 0
|
|
430
|
-
? iterationOverride
|
|
431
|
-
: N1 * N2 + OFFSET;
|
|
252
|
+
const iterations = XUtils.validateKeyDataIterations(iterationOverride ?? KEY_DATA_PBKDF2_ITERATIONS);
|
|
432
253
|
const ITERATIONS = XUtils.numberToUint8Arr(iterations);
|
|
433
254
|
const PKBDF_SALT = xMakeNonce();
|
|
434
|
-
const ENCRYPTION_KEY =
|
|
435
|
-
c: iterations,
|
|
436
|
-
dkLen: 32,
|
|
437
|
-
});
|
|
255
|
+
const ENCRYPTION_KEY = await pbkdf2Sha512Async(password, PKBDF_SALT, iterations);
|
|
438
256
|
const NONCE = xMakeNonce();
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
257
|
+
let ENCRYPTED_SIGNKEY;
|
|
258
|
+
try {
|
|
259
|
+
ENCRYPTED_SIGNKEY = xSecretbox(UNENCRYPTED_SIGNKEY, NONCE, ENCRYPTION_KEY);
|
|
260
|
+
}
|
|
261
|
+
finally {
|
|
262
|
+
ENCRYPTION_KEY.fill(0);
|
|
263
|
+
UNENCRYPTED_SIGNKEY.fill(0);
|
|
264
|
+
}
|
|
442
265
|
const result = new Uint8Array(ITERATIONS.length +
|
|
443
266
|
PKBDF_SALT.length +
|
|
444
267
|
NONCE.length +
|
|
@@ -458,7 +281,7 @@ export class XUtils {
|
|
|
458
281
|
* The integer must be positive, and it must be able to be stored
|
|
459
282
|
* in six bytes.
|
|
460
283
|
*
|
|
461
|
-
* @param n The number to convert.
|
|
284
|
+
* @param n - The number to convert.
|
|
462
285
|
* @returns The Uint8Array representation of n.
|
|
463
286
|
*/
|
|
464
287
|
static numberToUint8Arr(n) {
|
|
@@ -475,8 +298,8 @@ export class XUtils {
|
|
|
475
298
|
/**
|
|
476
299
|
* Packs a javascript object and a 32 byte header into a vex message.
|
|
477
300
|
*
|
|
478
|
-
* @param msg Message body (msgpack-serialized).
|
|
479
|
-
* @param header Optional 32-byte header; defaults to an empty header.
|
|
301
|
+
* @param msg - Message body (msgpack-serialized).
|
|
302
|
+
* @param header - Optional 32-byte header; defaults to an empty header.
|
|
480
303
|
* @returns the packed message.
|
|
481
304
|
*/
|
|
482
305
|
static packMessage(msg, header) {
|
|
@@ -487,7 +310,7 @@ export class XUtils {
|
|
|
487
310
|
/**
|
|
488
311
|
* Converts a Uint8Array representation of an integer back into a number.
|
|
489
312
|
*
|
|
490
|
-
* @param arr The array to convert.
|
|
313
|
+
* @param arr - The array to convert.
|
|
491
314
|
* @returns the number representation of arr.
|
|
492
315
|
*/
|
|
493
316
|
static uint8ArrToNumber(arr) {
|
|
@@ -501,7 +324,7 @@ export class XUtils {
|
|
|
501
324
|
* Takes a vex message and unpacks it into its header and a javascript object
|
|
502
325
|
* representation of its body.
|
|
503
326
|
*
|
|
504
|
-
* @param msg Full wire message (32-byte header + msgpack body).
|
|
327
|
+
* @param msg - Full wire message (32-byte header + msgpack body).
|
|
505
328
|
* @returns [32 byte header, message body]
|
|
506
329
|
*/
|
|
507
330
|
static unpackMessage(msg) {
|
|
@@ -518,26 +341,58 @@ export class XUtils {
|
|
|
518
341
|
.parse(raw);
|
|
519
342
|
return [msgh, msgb];
|
|
520
343
|
}
|
|
344
|
+
static readKeyDataIterations(keyData) {
|
|
345
|
+
if (keyData.length < KEY_DATA_HEADER_BYTES + KEY_DATA_MAC_BYTES) {
|
|
346
|
+
throw new Error("Encrypted key data is truncated.");
|
|
347
|
+
}
|
|
348
|
+
return XUtils.validateKeyDataIterations(XUtils.uint8ArrToNumber(keyData.slice(0, 6)));
|
|
349
|
+
}
|
|
350
|
+
static validateKeyDataIterations(iterations) {
|
|
351
|
+
if (!Number.isSafeInteger(iterations) ||
|
|
352
|
+
iterations < KEY_DATA_PBKDF2_MIN_ITERATIONS ||
|
|
353
|
+
iterations > KEY_DATA_PBKDF2_MAX_ITERATIONS) {
|
|
354
|
+
throw new Error(`PBKDF2 iterations must be between ${String(KEY_DATA_PBKDF2_MIN_ITERATIONS)} and ${String(KEY_DATA_PBKDF2_MAX_ITERATIONS)}.`);
|
|
355
|
+
}
|
|
356
|
+
return iterations;
|
|
357
|
+
}
|
|
521
358
|
}
|
|
522
359
|
/**
|
|
523
360
|
* Returns a 32 byte HMAC of a javscript object.
|
|
524
361
|
*
|
|
525
|
-
* @param msg
|
|
526
|
-
* @param SK
|
|
362
|
+
* @param msg - The message to create the HMAC of.
|
|
363
|
+
* @param SK - The secret key to create the HMAC with.
|
|
527
364
|
*/
|
|
528
365
|
export function xHMAC(msg, SK) {
|
|
529
366
|
const packedMsg = msgpackEncode(msg);
|
|
530
367
|
return hmac(sha256, SK, packedMsg);
|
|
531
368
|
}
|
|
369
|
+
/** Derive independent payload-encryption and envelope-authentication keys. */
|
|
370
|
+
export function xMessageKeySubkeys(messageKey) {
|
|
371
|
+
if (messageKey.length < 32) {
|
|
372
|
+
throw new Error("Message keys must contain at least 32 bytes.");
|
|
373
|
+
}
|
|
374
|
+
return {
|
|
375
|
+
authenticationKey: hmac(sha256, messageKey, XUtils.decodeUTF8("vex:message:auth:v1")),
|
|
376
|
+
encryptionKey: hmac(sha256, messageKey, XUtils.decodeUTF8("vex:message:encryption:v1")),
|
|
377
|
+
};
|
|
378
|
+
}
|
|
532
379
|
/**
|
|
533
380
|
* Gets a word list representation of a byte sequence.
|
|
534
381
|
*
|
|
535
|
-
* @param entropy The bytes to derive the wordlist from.
|
|
536
|
-
* @param wordList Optional
|
|
382
|
+
* @param entropy - The bytes to derive the wordlist from.
|
|
383
|
+
* @param wordList - Optional override for the wordlist. See bip39 docs for details.
|
|
537
384
|
*/
|
|
538
385
|
export function xMnemonic(entropy, wordList) {
|
|
539
386
|
return bip39.entropyToMnemonic(XUtils.encodeHex(entropy), wordList);
|
|
540
387
|
}
|
|
388
|
+
/** Domain-separated payload signed for X3DH signed and one-time prekeys. */
|
|
389
|
+
export function xPreKeySignaturePayload(publicKey, kind) {
|
|
390
|
+
if (publicKey.length === 0) {
|
|
391
|
+
throw new Error("Prekey public key cannot be empty.");
|
|
392
|
+
}
|
|
393
|
+
const separator = new Uint8Array([0]);
|
|
394
|
+
return xConcat(XUtils.decodeUTF8("vex:x3dh:prekey:v2"), separator, XUtils.decodeUTF8("tweetnacl"), separator, XUtils.decodeUTF8(kind), separator, publicKey);
|
|
395
|
+
}
|
|
541
396
|
/**
|
|
542
397
|
* Constants for vex.
|
|
543
398
|
*/
|
|
@@ -549,68 +404,28 @@ export const xConstants = {
|
|
|
549
404
|
KEY_LENGTH: 32,
|
|
550
405
|
MIN_OTK_SUPPLY: 100,
|
|
551
406
|
};
|
|
552
|
-
/**
|
|
553
|
-
* FIPS: `device.signKey` in the database is the P-256 ECDSA public key (SPKI),
|
|
554
|
-
* used for account/device signature verification. X3DH on the client expects
|
|
555
|
-
* the same curve point as Web Crypto "raw" P-256 ECDH public bytes for
|
|
556
|
-
* `importEcdhPublicKey`. This converts SPKI → raw without a private key.
|
|
557
|
-
*/
|
|
558
|
-
export async function fipsEcdhRawPublicKeyFromEcdsaSpkiAsync(ecdsaSpki) {
|
|
559
|
-
if (ecdsaSpki.length === 0) {
|
|
560
|
-
throw new Error("FIPS: empty ECDSA SPKI.");
|
|
561
|
-
}
|
|
562
|
-
const subtle = getSubtleCrypto();
|
|
563
|
-
const ecdsaPub = await importEcdsaPublicKey(ecdsaSpki);
|
|
564
|
-
const jwk = await subtle.exportKey("jwk", ecdsaPub);
|
|
565
|
-
if (jwk.x === undefined ||
|
|
566
|
-
jwk.y === undefined ||
|
|
567
|
-
jwk.x.length === 0 ||
|
|
568
|
-
jwk.y.length === 0) {
|
|
569
|
-
throw new Error("FIPS: could not export ECDSA public as JWK.");
|
|
570
|
-
}
|
|
571
|
-
const ecdhJwk = { ...jwk };
|
|
572
|
-
delete ecdhJwk.d;
|
|
573
|
-
ecdhJwk.key_ops = [];
|
|
574
|
-
ecdhJwk.ext = true;
|
|
575
|
-
const ecdhPub = await subtle.importKey("jwk", ecdhJwk, { name: "ECDH", namedCurve: "P-256" }, true, []);
|
|
576
|
-
return new Uint8Array(await subtle.exportKey("raw", ecdhPub));
|
|
577
|
-
}
|
|
578
407
|
/** Generate a fresh X25519 box key pair. */
|
|
579
408
|
export function xBoxKeyPair() {
|
|
580
409
|
return provider().boxKeyPair();
|
|
581
410
|
}
|
|
582
|
-
/** Async box keypair generation
|
|
583
|
-
export
|
|
584
|
-
|
|
585
|
-
return xBoxKeyPair();
|
|
586
|
-
}
|
|
587
|
-
const subtle = getSubtleCrypto();
|
|
588
|
-
const pair = await subtle.generateKey({ name: "ECDH", namedCurve: "P-256" }, true, ["deriveBits"]);
|
|
589
|
-
return {
|
|
590
|
-
publicKey: new Uint8Array(await subtle.exportKey("raw", pair.publicKey)),
|
|
591
|
-
secretKey: new Uint8Array(await subtle.exportKey("pkcs8", pair.privateKey)),
|
|
592
|
-
};
|
|
411
|
+
/** Async X25519 box keypair generation. */
|
|
412
|
+
export function xBoxKeyPairAsync() {
|
|
413
|
+
return Promise.resolve(xBoxKeyPair());
|
|
593
414
|
}
|
|
594
415
|
/** Restore an X25519 box key pair from a 32-byte secret key. */
|
|
595
416
|
export function xBoxKeyPairFromSecret(secretKey) {
|
|
596
417
|
return provider().boxKeyPairFromSecret(secretKey);
|
|
597
418
|
}
|
|
598
419
|
// ── Key pair type ───────────────────────────────────────────────────────────
|
|
599
|
-
/** Async box key restore from private key material. */
|
|
600
|
-
export
|
|
601
|
-
|
|
602
|
-
return xBoxKeyPairFromSecret(secretKey);
|
|
603
|
-
}
|
|
604
|
-
if (secretKey.length === 32) {
|
|
605
|
-
return fipsEcdhKeyPairFrom32ByteSeed(secretKey, getSubtleCrypto());
|
|
606
|
-
}
|
|
607
|
-
return fipsEcdhKeyPairFromPkcs8(secretKey, getSubtleCrypto());
|
|
420
|
+
/** Async X25519 box key restore from private key material. */
|
|
421
|
+
export function xBoxKeyPairFromSecretAsync(secretKey) {
|
|
422
|
+
return Promise.resolve(xBoxKeyPairFromSecret(secretKey));
|
|
608
423
|
}
|
|
609
424
|
// ── Key generation ─────────────────────────────────────────────────────────
|
|
610
425
|
/**
|
|
611
426
|
* Concatanates multiple Uint8Arrays.
|
|
612
427
|
*
|
|
613
|
-
* @param arrays
|
|
428
|
+
* @param arrays - The Uint8Arrays to concatenate.
|
|
614
429
|
*/
|
|
615
430
|
export function xConcat(...arrays) {
|
|
616
431
|
const totalLength = arrays.reduce((acc, value) => acc + value.length, 0);
|
|
@@ -631,51 +446,16 @@ export function xConcat(...arrays) {
|
|
|
631
446
|
* Derives a shared Secret Key from a known private key and
|
|
632
447
|
* a peer's known public key.
|
|
633
448
|
*
|
|
634
|
-
* @param myPrivateKey Your own private key
|
|
635
|
-
* @param theirPublicKey Their public key
|
|
449
|
+
* @param myPrivateKey - Your own private key.
|
|
450
|
+
* @param theirPublicKey - Their public key.
|
|
636
451
|
* @returns The derived shared secret, SK.
|
|
637
452
|
*/
|
|
638
453
|
export function xDH(myPrivateKey, theirPublicKey) {
|
|
639
454
|
return provider().boxBefore(myPrivateKey, theirPublicKey);
|
|
640
455
|
}
|
|
641
|
-
/** Async DH
|
|
642
|
-
export
|
|
643
|
-
|
|
644
|
-
return xDH(myPrivateKey, theirPublicKey);
|
|
645
|
-
}
|
|
646
|
-
const subtle = getSubtleCrypto();
|
|
647
|
-
const privateKey = await importEcdhPrivateKey(myPrivateKey);
|
|
648
|
-
const publicKey = await importEcdhPublicKey(theirPublicKey);
|
|
649
|
-
const shared = await subtle.deriveBits({ name: "ECDH", public: publicKey }, privateKey, 256);
|
|
650
|
-
return new Uint8Array(shared);
|
|
651
|
-
}
|
|
652
|
-
/**
|
|
653
|
-
* In `fips` mode only: derive a P-256 ECDH `KeyPair` (raw public + pkcs8 secret)
|
|
654
|
-
* from a P-256 ECDSA `KeyPair` (spki + pkcs8) using the same private scalar in Web Crypto.
|
|
655
|
-
* In `tweetnacl` mode, use `XKeyConvert.convertKeyPair` to map Ed25519 → X25519 instead.
|
|
656
|
-
*/
|
|
657
|
-
export async function xEcdhKeyPairFromEcdsaKeyPairAsync(sign) {
|
|
658
|
-
if (activeCryptoProfile === "tweetnacl") {
|
|
659
|
-
return Promise.reject(new Error('xEcdhKeyPairFromEcdsaKeyPairAsync is for crypto profile "fips" only. Use XKeyConvert.convertKeyPair in tweetnacl mode.'));
|
|
660
|
-
}
|
|
661
|
-
const subtle = getSubtleCrypto();
|
|
662
|
-
const ecdsaPriv = await importEcdsaPrivateKey(sign.secretKey);
|
|
663
|
-
const jwk = await subtle.exportKey("jwk", ecdsaPriv);
|
|
664
|
-
if (typeof jwk.d !== "string" || jwk.d.length === 0) {
|
|
665
|
-
throw new Error("FIPS: could not export ECDSA private as JWK.");
|
|
666
|
-
}
|
|
667
|
-
const ecdhJwk = { ...jwk, key_ops: ["deriveBits"] };
|
|
668
|
-
ecdhJwk.ext = true;
|
|
669
|
-
const ecdhPriv = await subtle.importKey("jwk", ecdhJwk, { name: "ECDH", namedCurve: "P-256" }, true, ["deriveBits"]);
|
|
670
|
-
const ecdhPubJwk = await subtle.exportKey("jwk", ecdhPriv);
|
|
671
|
-
const ecdhPubJwkNoD = { ...ecdhPubJwk };
|
|
672
|
-
delete ecdhPubJwkNoD.d;
|
|
673
|
-
ecdhPubJwkNoD.key_ops = [];
|
|
674
|
-
const ecdhPub = await subtle.importKey("jwk", ecdhPubJwkNoD, { name: "ECDH", namedCurve: "P-256" }, true, []);
|
|
675
|
-
return {
|
|
676
|
-
publicKey: new Uint8Array(await subtle.exportKey("raw", ecdhPub)),
|
|
677
|
-
secretKey: new Uint8Array(await subtle.exportKey("pkcs8", ecdhPriv)),
|
|
678
|
-
};
|
|
456
|
+
/** Async X25519 DH. */
|
|
457
|
+
export function xDHAsync(myPrivateKey, theirPublicKey) {
|
|
458
|
+
return Promise.resolve(xDH(myPrivateKey, theirPublicKey));
|
|
679
459
|
}
|
|
680
460
|
// ── Signing ────────────────────────────────────────────────────────────────
|
|
681
461
|
/**
|
|
@@ -714,7 +494,7 @@ export function xEncode(curveType, publicKey) {
|
|
|
714
494
|
/**
|
|
715
495
|
* Hashes some data.
|
|
716
496
|
*
|
|
717
|
-
* @param data
|
|
497
|
+
* @param data - The data to hash.
|
|
718
498
|
* @returns The hash of the data.
|
|
719
499
|
*/
|
|
720
500
|
export function xHash(data) {
|
|
@@ -739,119 +519,49 @@ export function xRandomBytes(length) {
|
|
|
739
519
|
export function xSecretbox(plaintext, nonce, key) {
|
|
740
520
|
return provider().secretbox(plaintext, nonce, key);
|
|
741
521
|
}
|
|
742
|
-
/** Async
|
|
743
|
-
export
|
|
744
|
-
|
|
745
|
-
return xSecretbox(plaintext, nonce, key);
|
|
746
|
-
}
|
|
747
|
-
const subtle = getSubtleCrypto();
|
|
748
|
-
const iv = requireAesGcmNonce(nonce);
|
|
749
|
-
const aesKey = await importAesKey(key, ["encrypt"]);
|
|
750
|
-
const ciphertext = await subtle.encrypt({ iv: toBufferSource(iv), name: "AES-GCM" }, aesKey, toBufferSource(plaintext));
|
|
751
|
-
return new Uint8Array(ciphertext);
|
|
522
|
+
/** Async XSalsa20-Poly1305 encryption. */
|
|
523
|
+
export function xSecretboxAsync(plaintext, nonce, key) {
|
|
524
|
+
return Promise.resolve(xSecretbox(plaintext, nonce, key));
|
|
752
525
|
}
|
|
753
526
|
/** Decrypt with a shared secret key. Returns null if authentication fails. */
|
|
754
527
|
export function xSecretboxOpen(ciphertext, nonce, key) {
|
|
755
528
|
return provider().secretboxOpen(ciphertext, nonce, key);
|
|
756
529
|
}
|
|
757
|
-
/** Async
|
|
758
|
-
export
|
|
759
|
-
|
|
760
|
-
return xSecretboxOpen(ciphertext, nonce, key);
|
|
761
|
-
}
|
|
762
|
-
const subtle = getSubtleCrypto();
|
|
763
|
-
const iv = requireAesGcmNonce(nonce);
|
|
764
|
-
const aesKey = await importAesKey(key, ["decrypt"]);
|
|
765
|
-
try {
|
|
766
|
-
const plaintext = await subtle.decrypt({ iv: toBufferSource(iv), name: "AES-GCM" }, aesKey, toBufferSource(ciphertext));
|
|
767
|
-
return new Uint8Array(plaintext);
|
|
768
|
-
}
|
|
769
|
-
catch {
|
|
770
|
-
return null;
|
|
771
|
-
}
|
|
530
|
+
/** Async XSalsa20-Poly1305 decryption. */
|
|
531
|
+
export function xSecretboxOpenAsync(ciphertext, nonce, key) {
|
|
532
|
+
return Promise.resolve(xSecretboxOpen(ciphertext, nonce, key));
|
|
772
533
|
}
|
|
773
534
|
/** Sign a message with an Ed25519 secret key. Returns signed message (64-byte signature prefix + message). */
|
|
774
535
|
export function xSign(message, secretKey) {
|
|
775
536
|
return provider().sign(message, secretKey);
|
|
776
537
|
}
|
|
777
|
-
/** Async signing
|
|
778
|
-
export
|
|
779
|
-
|
|
780
|
-
return xSign(message, secretKey);
|
|
781
|
-
}
|
|
782
|
-
const subtle = getSubtleCrypto();
|
|
783
|
-
const privateKey = await importEcdsaPrivateKey(secretKey);
|
|
784
|
-
const signature = new Uint8Array(await subtle.sign({ hash: "SHA-256", name: "ECDSA" }, privateKey, toBufferSource(message)));
|
|
785
|
-
return encodeFipsSignedMessage(signature, message);
|
|
538
|
+
/** Async Ed25519 signing. */
|
|
539
|
+
export function xSignAsync(message, secretKey) {
|
|
540
|
+
return Promise.resolve(xSign(message, secretKey));
|
|
786
541
|
}
|
|
787
542
|
/** Generate a fresh Ed25519 signing key pair. */
|
|
788
543
|
export function xSignKeyPair() {
|
|
789
544
|
return provider().signKeyPair();
|
|
790
545
|
}
|
|
791
|
-
/** Async keypair generation
|
|
792
|
-
export
|
|
793
|
-
|
|
794
|
-
return xSignKeyPair();
|
|
795
|
-
}
|
|
796
|
-
const subtle = getSubtleCrypto();
|
|
797
|
-
const pair = await subtle.generateKey({ name: "ECDSA", namedCurve: "P-256" }, true, ["sign", "verify"]);
|
|
798
|
-
return {
|
|
799
|
-
publicKey: new Uint8Array(await subtle.exportKey("spki", pair.publicKey)),
|
|
800
|
-
secretKey: new Uint8Array(await subtle.exportKey("pkcs8", pair.privateKey)),
|
|
801
|
-
};
|
|
546
|
+
/** Async Ed25519 keypair generation. */
|
|
547
|
+
export function xSignKeyPairAsync() {
|
|
548
|
+
return Promise.resolve(xSignKeyPair());
|
|
802
549
|
}
|
|
803
550
|
/** Restore an Ed25519 signing key pair from a 64-byte secret key. */
|
|
804
551
|
export function xSignKeyPairFromSecret(secretKey) {
|
|
805
552
|
return provider().signKeyPairFromSecret(secretKey);
|
|
806
553
|
}
|
|
807
|
-
/** Async restore of signing keypair
|
|
808
|
-
export
|
|
809
|
-
|
|
810
|
-
return xSignKeyPairFromSecret(secretKey);
|
|
811
|
-
}
|
|
812
|
-
return fipsEcdsaKeyPairFromPkcs8(secretKey, getSubtleCrypto());
|
|
554
|
+
/** Async restore of an Ed25519 signing keypair. */
|
|
555
|
+
export function xSignKeyPairFromSecretAsync(secretKey) {
|
|
556
|
+
return Promise.resolve(xSignKeyPairFromSecret(secretKey));
|
|
813
557
|
}
|
|
814
558
|
/** Verify and open a signed message. Returns the original message, or null if verification fails. */
|
|
815
559
|
export function xSignOpen(signedMessage, publicKey) {
|
|
816
560
|
return provider().signOpen(signedMessage, publicKey);
|
|
817
561
|
}
|
|
818
|
-
/** Async verify/open
|
|
819
|
-
export
|
|
820
|
-
|
|
821
|
-
return xSignOpen(signedMessage, publicKey);
|
|
822
|
-
}
|
|
823
|
-
const subtle = getSubtleCrypto();
|
|
824
|
-
const parsed = decodeFipsSignedMessage(signedMessage);
|
|
825
|
-
const verifyKey = await importEcdsaPublicKey(publicKey);
|
|
826
|
-
const valid = await subtle.verify({ hash: "SHA-256", name: "ECDSA" }, verifyKey, toBufferSource(parsed.signature), toBufferSource(parsed.message));
|
|
827
|
-
return valid ? parsed.message : null;
|
|
828
|
-
}
|
|
829
|
-
async function fipsEcdsaKeyPairFromPkcs8(secretKey, subtle) {
|
|
830
|
-
const ecdsaPriv = await importEcdsaPrivateKey(secretKey);
|
|
831
|
-
const jwk = await subtle.exportKey("jwk", ecdsaPriv);
|
|
832
|
-
const ecdsaPubJwk = { ...jwk };
|
|
833
|
-
delete ecdsaPubJwk.d;
|
|
834
|
-
ecdsaPubJwk.key_ops = ["verify"];
|
|
835
|
-
const ecdsaPub = await subtle.importKey("jwk", ecdsaPubJwk, { name: "ECDSA", namedCurve: "P-256" }, true, ["verify"]);
|
|
836
|
-
return {
|
|
837
|
-
publicKey: new Uint8Array(await subtle.exportKey("spki", ecdsaPub)),
|
|
838
|
-
secretKey: Uint8Array.from(secretKey),
|
|
839
|
-
};
|
|
840
|
-
}
|
|
841
|
-
async function importAesKey(key, usages) {
|
|
842
|
-
return getSubtleCrypto().importKey("raw", toBufferSource(key), { name: "AES-GCM" }, false, usages);
|
|
843
|
-
}
|
|
844
|
-
async function importEcdhPrivateKey(secretKey) {
|
|
845
|
-
return getSubtleCrypto().importKey("pkcs8", toBufferSource(secretKey), { name: "ECDH", namedCurve: "P-256" }, true, ["deriveBits"]);
|
|
846
|
-
}
|
|
847
|
-
async function importEcdhPublicKey(publicKey) {
|
|
848
|
-
return getSubtleCrypto().importKey("raw", toBufferSource(publicKey), { name: "ECDH", namedCurve: "P-256" }, true, []);
|
|
849
|
-
}
|
|
850
|
-
async function importEcdsaPrivateKey(secretKey) {
|
|
851
|
-
return getSubtleCrypto().importKey("pkcs8", toBufferSource(secretKey), { name: "ECDSA", namedCurve: "P-256" }, true, ["sign"]);
|
|
852
|
-
}
|
|
853
|
-
async function importEcdsaPublicKey(publicKey) {
|
|
854
|
-
return getSubtleCrypto().importKey("spki", toBufferSource(publicKey), { name: "ECDSA", namedCurve: "P-256" }, true, ["verify"]);
|
|
562
|
+
/** Async Ed25519 verify/open. */
|
|
563
|
+
export function xSignOpenAsync(signedMessage, publicKey) {
|
|
564
|
+
return Promise.resolve(xSignOpen(signedMessage, publicKey));
|
|
855
565
|
}
|
|
856
566
|
/**
|
|
857
567
|
* @internal
|