@vex-chat/crypto 11.0.0 → 12.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -20
- package/dist/index.d.ts +14 -58
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +37 -395
- package/dist/index.js.map +1 -1
- package/package.json +1 -2
- package/src/__tests__/xAsyncApi.extended.test.ts +3 -44
- package/src/__tests__/xAsyncProfile.ts +6 -26
- package/src/index.ts +44 -616
- package/src/__tests__/cryptoProfile.ts +0 -74
- package/src/__tests__/cryptoProfileScope.test.ts +0 -77
package/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";
|
|
@@ -21,13 +19,6 @@ const KEY_DATA_MAC_BYTES = 16;
|
|
|
21
19
|
const KEY_DATA_PBKDF2_ITERATIONS = 220_000;
|
|
22
20
|
const KEY_DATA_PBKDF2_MIN_ITERATIONS = 1_000;
|
|
23
21
|
const KEY_DATA_PBKDF2_MAX_ITERATIONS = 2_000_000;
|
|
24
|
-
function getWebCrypto() {
|
|
25
|
-
const cryptoCandidate = globalThis.crypto;
|
|
26
|
-
if (!isWebCryptoLike(cryptoCandidate)) {
|
|
27
|
-
throw new Error("Web Crypto API is not available in this runtime for fips profile operations.");
|
|
28
|
-
}
|
|
29
|
-
return cryptoCandidate;
|
|
30
|
-
}
|
|
31
22
|
function isWebCryptoLike(value) {
|
|
32
23
|
if (typeof value !== "object" || value === null) {
|
|
33
24
|
return false;
|
|
@@ -55,29 +46,10 @@ async function pbkdf2Sha512Async(password, salt, iterations) {
|
|
|
55
46
|
}, passwordKey, 256);
|
|
56
47
|
return new Uint8Array(bits);
|
|
57
48
|
}
|
|
58
|
-
function randomBytesWebCrypto(length) {
|
|
59
|
-
if (!Number.isInteger(length) || length < 0) {
|
|
60
|
-
throw new Error(`Expected non-negative integer length, received ${String(length)}.`);
|
|
61
|
-
}
|
|
62
|
-
const cryptoImpl = getWebCrypto();
|
|
63
|
-
const result = new Uint8Array(length);
|
|
64
|
-
let offset = 0;
|
|
65
|
-
while (offset < length) {
|
|
66
|
-
const chunkLength = Math.min(65536, length - offset);
|
|
67
|
-
const chunk = result.subarray(offset, offset + chunkLength);
|
|
68
|
-
cryptoImpl.getRandomValues(chunk);
|
|
69
|
-
offset += chunkLength;
|
|
70
|
-
}
|
|
71
|
-
return result;
|
|
72
|
-
}
|
|
73
|
-
function unsupported(profile, operation) {
|
|
74
|
-
throw new Error(`Crypto profile "${profile}" does not implement ${operation} yet.`);
|
|
75
|
-
}
|
|
76
49
|
const tweetnaclProvider = {
|
|
77
50
|
boxBefore: (myPrivateKey, theirPublicKey) => nacl.box.before(theirPublicKey, myPrivateKey),
|
|
78
51
|
boxKeyPair: () => nacl.box.keyPair(),
|
|
79
52
|
boxKeyPairFromSecret: (secretKey) => nacl.box.keyPair.fromSecretKey(secretKey),
|
|
80
|
-
profile: "tweetnacl",
|
|
81
53
|
randomBytes: (length) => nacl.randomBytes(length),
|
|
82
54
|
secretbox: (plaintext, nonce, key) => nacl.secretbox(plaintext, nonce, key),
|
|
83
55
|
secretboxOpen: (ciphertext, nonce, key) => nacl.secretbox.open(ciphertext, nonce, key),
|
|
@@ -86,182 +58,8 @@ const tweetnaclProvider = {
|
|
|
86
58
|
signKeyPairFromSecret: (secretKey) => nacl.sign.keyPair.fromSecretKey(secretKey),
|
|
87
59
|
signOpen: (signedMessage, publicKey) => nacl.sign.open(signedMessage, publicKey),
|
|
88
60
|
};
|
|
89
|
-
const fipsProvider = {
|
|
90
|
-
boxBefore: (myPrivateKey, theirPublicKey) => {
|
|
91
|
-
void myPrivateKey;
|
|
92
|
-
void theirPublicKey;
|
|
93
|
-
return unsupported("fips", "xDH");
|
|
94
|
-
},
|
|
95
|
-
boxKeyPair: () => unsupported("fips", "xBoxKeyPair"),
|
|
96
|
-
boxKeyPairFromSecret: (secretKey) => {
|
|
97
|
-
void secretKey;
|
|
98
|
-
return unsupported("fips", "xBoxKeyPairFromSecret");
|
|
99
|
-
},
|
|
100
|
-
profile: "fips",
|
|
101
|
-
randomBytes: (length) => randomBytesWebCrypto(length),
|
|
102
|
-
secretbox: (plaintext, nonce, key) => {
|
|
103
|
-
void plaintext;
|
|
104
|
-
void nonce;
|
|
105
|
-
void key;
|
|
106
|
-
return unsupported("fips", "xSecretbox");
|
|
107
|
-
},
|
|
108
|
-
secretboxOpen: (ciphertext, nonce, key) => {
|
|
109
|
-
void ciphertext;
|
|
110
|
-
void nonce;
|
|
111
|
-
void key;
|
|
112
|
-
return unsupported("fips", "xSecretboxOpen");
|
|
113
|
-
},
|
|
114
|
-
sign: (message, secretKey) => {
|
|
115
|
-
void message;
|
|
116
|
-
void secretKey;
|
|
117
|
-
return unsupported("fips", "xSign");
|
|
118
|
-
},
|
|
119
|
-
signKeyPair: () => unsupported("fips", "xSignKeyPair"),
|
|
120
|
-
signKeyPairFromSecret: (secretKey) => {
|
|
121
|
-
void secretKey;
|
|
122
|
-
return unsupported("fips", "xSignKeyPairFromSecret");
|
|
123
|
-
},
|
|
124
|
-
signOpen: (signedMessage, publicKey) => {
|
|
125
|
-
void signedMessage;
|
|
126
|
-
void publicKey;
|
|
127
|
-
return unsupported("fips", "xSignOpen");
|
|
128
|
-
},
|
|
129
|
-
};
|
|
130
|
-
const providers = {
|
|
131
|
-
fips: fipsProvider,
|
|
132
|
-
tweetnacl: tweetnaclProvider,
|
|
133
|
-
};
|
|
134
|
-
let activeCryptoProfile = "tweetnacl";
|
|
135
|
-
let activeCryptoProvider = providers[activeCryptoProfile];
|
|
136
|
-
/** Returns the currently configured crypto profile. */
|
|
137
|
-
export function getCryptoProfile() {
|
|
138
|
-
return activeCryptoProfile;
|
|
139
|
-
}
|
|
140
|
-
/**
|
|
141
|
-
* Sets the runtime crypto profile.
|
|
142
|
-
*
|
|
143
|
-
* `tweetnacl` preserves existing behavior.
|
|
144
|
-
* `fips` currently enables only backend-agnostic helpers; NaCl-coupled
|
|
145
|
-
* primitives throw until a FIPS backend implementation is wired in.
|
|
146
|
-
*/
|
|
147
|
-
export function setCryptoProfile(profile) {
|
|
148
|
-
activeCryptoProfile = profile;
|
|
149
|
-
activeCryptoProvider = providers[profile];
|
|
150
|
-
}
|
|
151
|
-
const cryptoProfileScopeStack = [];
|
|
152
|
-
/**
|
|
153
|
-
* Saves the current profile and switches to `profile`. Pair every call with
|
|
154
|
-
* {@link leaveCryptoProfileScope} in a `finally` block.
|
|
155
|
-
*
|
|
156
|
-
* **Why:** `setCryptoProfile` is process-wide. Several async libvex `Client`s
|
|
157
|
-
* can overlap on `readMail`; a naive save/restore in `finally` can reset the
|
|
158
|
-
* profile while another client still needs FIPS — `xSecretboxOpenAsync` then
|
|
159
|
-
* reads `tweetnacl` and fails to decrypt AES-GCM payloads. Nesting this stack
|
|
160
|
-
* fixes that.
|
|
161
|
-
*/
|
|
162
|
-
export function enterCryptoProfileScope(profile) {
|
|
163
|
-
cryptoProfileScopeStack.push(activeCryptoProfile);
|
|
164
|
-
setCryptoProfile(profile);
|
|
165
|
-
}
|
|
166
|
-
/** Restores the profile saved by the innermost {@link enterCryptoProfileScope}. */
|
|
167
|
-
export function leaveCryptoProfileScope() {
|
|
168
|
-
const prev = cryptoProfileScopeStack.pop();
|
|
169
|
-
if (prev === undefined) {
|
|
170
|
-
throw new Error("leaveCryptoProfileScope called without a matching enterCryptoProfileScope");
|
|
171
|
-
}
|
|
172
|
-
setCryptoProfile(prev);
|
|
173
|
-
}
|
|
174
|
-
function bytesToBase64Url(bytes) {
|
|
175
|
-
return globalThis
|
|
176
|
-
.btoa(String.fromCodePoint(...Array.from(bytes)))
|
|
177
|
-
.replace(/\+/g, "-")
|
|
178
|
-
.replace(/\//g, "_")
|
|
179
|
-
.replace(/=+/g, "");
|
|
180
|
-
}
|
|
181
|
-
function concatBytes(a, b) {
|
|
182
|
-
const out = new Uint8Array(a.length + b.length);
|
|
183
|
-
out.set(a, 0);
|
|
184
|
-
out.set(b, a.length);
|
|
185
|
-
return out;
|
|
186
|
-
}
|
|
187
|
-
function decodeFipsSignedMessage(signedMessage) {
|
|
188
|
-
if (signedMessage.length < 2) {
|
|
189
|
-
throw new Error("Invalid FIPS signed message: missing signature length prefix.");
|
|
190
|
-
}
|
|
191
|
-
const signatureLength = (signedMessage[0] ?? 0) * 256 + (signedMessage[1] ?? 0);
|
|
192
|
-
const signatureStart = 2;
|
|
193
|
-
const signatureEnd = signatureStart + signatureLength;
|
|
194
|
-
if (signedMessage.length < signatureEnd) {
|
|
195
|
-
throw new Error("Invalid FIPS signed message: signature length exceeds message length.");
|
|
196
|
-
}
|
|
197
|
-
return {
|
|
198
|
-
message: signedMessage.slice(signatureEnd),
|
|
199
|
-
signature: signedMessage.slice(signatureStart, signatureEnd),
|
|
200
|
-
};
|
|
201
|
-
}
|
|
202
|
-
function encodeFipsSignedMessage(signature, message) {
|
|
203
|
-
if (signature.length > 65535) {
|
|
204
|
-
throw new Error("FIPS signature too long to encode.");
|
|
205
|
-
}
|
|
206
|
-
const prefix = new Uint8Array(2);
|
|
207
|
-
prefix[0] = (signature.length >> 8) & 0xff;
|
|
208
|
-
prefix[1] = signature.length & 0xff;
|
|
209
|
-
return concatBytes(concatBytes(prefix, signature), message);
|
|
210
|
-
}
|
|
211
|
-
async function fipsEcdhKeyPairFrom32ByteSeed(secretKey, subtle) {
|
|
212
|
-
if (secretKey.length !== 32) {
|
|
213
|
-
throw new Error("FIPS: expected a 32-byte IKM/seed for ECDH from-secret.");
|
|
214
|
-
}
|
|
215
|
-
const d = p256.utils.normPrivateKeyToScalar(Uint8Array.from(secretKey));
|
|
216
|
-
const d32 = numberToBytesBE(d, 32);
|
|
217
|
-
const rawPub = p256.getPublicKey(d, false);
|
|
218
|
-
if (rawPub[0] !== 0x04) {
|
|
219
|
-
throw new Error("FIPS: expected uncompressed P-256 public key.");
|
|
220
|
-
}
|
|
221
|
-
const jwk = {
|
|
222
|
-
crv: "P-256",
|
|
223
|
-
d: bytesToBase64Url(d32),
|
|
224
|
-
kty: "EC",
|
|
225
|
-
x: bytesToBase64Url(rawPub.subarray(1, 33)),
|
|
226
|
-
y: bytesToBase64Url(rawPub.subarray(33, 65)),
|
|
227
|
-
};
|
|
228
|
-
const ecdhPriv = await subtle.importKey("jwk", jwk, { name: "ECDH", namedCurve: "P-256" }, true, ["deriveBits"]);
|
|
229
|
-
const ecdhPubJwk = await subtle.exportKey("jwk", ecdhPriv);
|
|
230
|
-
const ecdhPubJwkNoD = { ...ecdhPubJwk };
|
|
231
|
-
delete ecdhPubJwkNoD.d;
|
|
232
|
-
ecdhPubJwkNoD.key_ops = [];
|
|
233
|
-
const ecdhPub = await subtle.importKey("jwk", ecdhPubJwkNoD, { name: "ECDH", namedCurve: "P-256" }, true, []);
|
|
234
|
-
return {
|
|
235
|
-
publicKey: new Uint8Array(await subtle.exportKey("raw", ecdhPub)),
|
|
236
|
-
secretKey: new Uint8Array(await subtle.exportKey("pkcs8", ecdhPriv)),
|
|
237
|
-
};
|
|
238
|
-
}
|
|
239
|
-
async function fipsEcdhKeyPairFromPkcs8(secretKey, subtle) {
|
|
240
|
-
const ecdhPriv = await subtle.importKey("pkcs8", toBufferSource(secretKey), { name: "ECDH", namedCurve: "P-256" }, true, ["deriveBits"]);
|
|
241
|
-
const jwk = await subtle.exportKey("jwk", ecdhPriv);
|
|
242
|
-
const ecdhPubJwk = { ...jwk };
|
|
243
|
-
delete ecdhPubJwk.d;
|
|
244
|
-
ecdhPubJwk.key_ops = [];
|
|
245
|
-
const ecdhPub = await subtle.importKey("jwk", ecdhPubJwk, { name: "ECDH", namedCurve: "P-256" }, true, []);
|
|
246
|
-
return {
|
|
247
|
-
publicKey: new Uint8Array(await subtle.exportKey("raw", ecdhPub)),
|
|
248
|
-
secretKey: new Uint8Array(await subtle.exportKey("pkcs8", ecdhPriv)),
|
|
249
|
-
};
|
|
250
|
-
}
|
|
251
|
-
function getSubtleCrypto() {
|
|
252
|
-
return getWebCrypto().subtle;
|
|
253
|
-
}
|
|
254
61
|
function provider() {
|
|
255
|
-
return
|
|
256
|
-
}
|
|
257
|
-
function requireAesGcmNonce(nonce) {
|
|
258
|
-
if (nonce.length < 12) {
|
|
259
|
-
throw new Error(`AES-GCM requires a nonce of at least 12 bytes, received ${String(nonce.length)}.`);
|
|
260
|
-
}
|
|
261
|
-
return nonce.slice(0, 12);
|
|
262
|
-
}
|
|
263
|
-
function toBufferSource(bytes) {
|
|
264
|
-
return Uint8Array.from(bytes).buffer;
|
|
62
|
+
return tweetnaclProvider;
|
|
265
63
|
}
|
|
266
64
|
// msgpackr with useRecords:false emits standard msgpack (no nonstandard record extension).
|
|
267
65
|
// moreTypes:false keeps the extension set to only what other decoders understand.
|
|
@@ -358,10 +156,7 @@ export class XUtils {
|
|
|
358
156
|
DERIVED_KEY.fill(0);
|
|
359
157
|
}
|
|
360
158
|
};
|
|
361
|
-
/**
|
|
362
|
-
* Async variant of decryptKeyData for cross-runtime/FIPS backends.
|
|
363
|
-
* Supports both profile formats emitted by encryptKeyDataAsync.
|
|
364
|
-
*/
|
|
159
|
+
/** Async variant of decryptKeyData for cross-runtime callers. */
|
|
365
160
|
static decryptKeyDataAsync = async (keyData, password) => {
|
|
366
161
|
const ITERATIONS = XUtils.readKeyDataIterations(keyData);
|
|
367
162
|
const PKBDF_SALT = keyData.slice(6, 30);
|
|
@@ -369,9 +164,7 @@ export class XUtils {
|
|
|
369
164
|
const ENCRYPTED_KEY = keyData.slice(54);
|
|
370
165
|
const DERIVED_KEY = await pbkdf2Sha512Async(password, PKBDF_SALT, ITERATIONS);
|
|
371
166
|
try {
|
|
372
|
-
const decrypted =
|
|
373
|
-
? await xSecretboxOpenAsync(ENCRYPTED_KEY, ENCRYPTION_NONCE, DERIVED_KEY)
|
|
374
|
-
: xSecretboxOpen(ENCRYPTED_KEY, ENCRYPTION_NONCE, DERIVED_KEY);
|
|
167
|
+
const decrypted = xSecretboxOpen(ENCRYPTED_KEY, ENCRYPTION_NONCE, DERIVED_KEY);
|
|
375
168
|
if (decrypted === null) {
|
|
376
169
|
throw new Error("Decryption failed. Wrong password?");
|
|
377
170
|
}
|
|
@@ -385,13 +178,13 @@ export class XUtils {
|
|
|
385
178
|
};
|
|
386
179
|
/**
|
|
387
180
|
* Derive a purpose-separated 32-byte key for local at-rest encryption.
|
|
388
|
-
* The result never aliases raw identity-key bytes
|
|
181
|
+
* The result never aliases raw identity-key bytes.
|
|
389
182
|
*/
|
|
390
|
-
static deriveLocalAtRestAesKey(identitySk
|
|
183
|
+
static deriveLocalAtRestAesKey(identitySk) {
|
|
391
184
|
if (identitySk.length < 32) {
|
|
392
185
|
throw new Error("Expected at least 32 bytes of identity secret.");
|
|
393
186
|
}
|
|
394
|
-
return new Uint8Array(hkdf(sha256, identitySk, new Uint8Array(0), new TextEncoder().encode(
|
|
187
|
+
return new Uint8Array(hkdf(sha256, identitySk, new Uint8Array(0), new TextEncoder().encode("vex:at-rest:3:tweetnacl"), 32));
|
|
395
188
|
}
|
|
396
189
|
/**
|
|
397
190
|
* Returns the empty header (32 0's)
|
|
@@ -453,10 +246,7 @@ export class XUtils {
|
|
|
453
246
|
result.set(ENCRYPTED_SIGNKEY, offset);
|
|
454
247
|
return result;
|
|
455
248
|
};
|
|
456
|
-
/**
|
|
457
|
-
* Async variant of encryptKeyData for cross-runtime/FIPS backends.
|
|
458
|
-
* Format remains [iterations(6)|salt(24)|nonce(24)|ciphertext(N)].
|
|
459
|
-
*/
|
|
249
|
+
/** Async variant of encryptKeyData for cross-runtime callers. */
|
|
460
250
|
static encryptKeyDataAsync = async (password, keyToSave, iterationOverride) => {
|
|
461
251
|
const UNENCRYPTED_SIGNKEY = XUtils.decodeHex(keyToSave);
|
|
462
252
|
const iterations = XUtils.validateKeyDataIterations(iterationOverride ?? KEY_DATA_PBKDF2_ITERATIONS);
|
|
@@ -466,10 +256,7 @@ export class XUtils {
|
|
|
466
256
|
const NONCE = xMakeNonce();
|
|
467
257
|
let ENCRYPTED_SIGNKEY;
|
|
468
258
|
try {
|
|
469
|
-
ENCRYPTED_SIGNKEY =
|
|
470
|
-
activeCryptoProfile === "fips"
|
|
471
|
-
? await xSecretboxAsync(UNENCRYPTED_SIGNKEY, NONCE, ENCRYPTION_KEY)
|
|
472
|
-
: xSecretbox(UNENCRYPTED_SIGNKEY, NONCE, ENCRYPTION_KEY);
|
|
259
|
+
ENCRYPTED_SIGNKEY = xSecretbox(UNENCRYPTED_SIGNKEY, NONCE, ENCRYPTION_KEY);
|
|
473
260
|
}
|
|
474
261
|
finally {
|
|
475
262
|
ENCRYPTION_KEY.fill(0);
|
|
@@ -599,12 +386,12 @@ export function xMnemonic(entropy, wordList) {
|
|
|
599
386
|
return bip39.entropyToMnemonic(XUtils.encodeHex(entropy), wordList);
|
|
600
387
|
}
|
|
601
388
|
/** Domain-separated payload signed for X3DH signed and one-time prekeys. */
|
|
602
|
-
export function xPreKeySignaturePayload(publicKey, kind
|
|
389
|
+
export function xPreKeySignaturePayload(publicKey, kind) {
|
|
603
390
|
if (publicKey.length === 0) {
|
|
604
391
|
throw new Error("Prekey public key cannot be empty.");
|
|
605
392
|
}
|
|
606
393
|
const separator = new Uint8Array([0]);
|
|
607
|
-
return xConcat(XUtils.decodeUTF8("vex:x3dh:prekey:v2"), separator, XUtils.decodeUTF8(
|
|
394
|
+
return xConcat(XUtils.decodeUTF8("vex:x3dh:prekey:v2"), separator, XUtils.decodeUTF8("tweetnacl"), separator, XUtils.decodeUTF8(kind), separator, publicKey);
|
|
608
395
|
}
|
|
609
396
|
/**
|
|
610
397
|
* Constants for vex.
|
|
@@ -617,62 +404,22 @@ export const xConstants = {
|
|
|
617
404
|
KEY_LENGTH: 32,
|
|
618
405
|
MIN_OTK_SUPPLY: 100,
|
|
619
406
|
};
|
|
620
|
-
/**
|
|
621
|
-
* FIPS: `device.signKey` in the database is the P-256 ECDSA public key (SPKI),
|
|
622
|
-
* used for account/device signature verification. X3DH on the client expects
|
|
623
|
-
* the same curve point as Web Crypto "raw" P-256 ECDH public bytes for
|
|
624
|
-
* `importEcdhPublicKey`. This converts SPKI → raw without a private key.
|
|
625
|
-
*/
|
|
626
|
-
export async function fipsEcdhRawPublicKeyFromEcdsaSpkiAsync(ecdsaSpki) {
|
|
627
|
-
if (ecdsaSpki.length === 0) {
|
|
628
|
-
throw new Error("FIPS: empty ECDSA SPKI.");
|
|
629
|
-
}
|
|
630
|
-
const subtle = getSubtleCrypto();
|
|
631
|
-
const ecdsaPub = await importEcdsaPublicKey(ecdsaSpki);
|
|
632
|
-
const jwk = await subtle.exportKey("jwk", ecdsaPub);
|
|
633
|
-
if (jwk.x === undefined ||
|
|
634
|
-
jwk.y === undefined ||
|
|
635
|
-
jwk.x.length === 0 ||
|
|
636
|
-
jwk.y.length === 0) {
|
|
637
|
-
throw new Error("FIPS: could not export ECDSA public as JWK.");
|
|
638
|
-
}
|
|
639
|
-
const ecdhJwk = { ...jwk };
|
|
640
|
-
delete ecdhJwk.d;
|
|
641
|
-
ecdhJwk.key_ops = [];
|
|
642
|
-
ecdhJwk.ext = true;
|
|
643
|
-
const ecdhPub = await subtle.importKey("jwk", ecdhJwk, { name: "ECDH", namedCurve: "P-256" }, true, []);
|
|
644
|
-
return new Uint8Array(await subtle.exportKey("raw", ecdhPub));
|
|
645
|
-
}
|
|
646
407
|
/** Generate a fresh X25519 box key pair. */
|
|
647
408
|
export function xBoxKeyPair() {
|
|
648
409
|
return provider().boxKeyPair();
|
|
649
410
|
}
|
|
650
|
-
/** Async box keypair generation
|
|
651
|
-
export
|
|
652
|
-
|
|
653
|
-
return xBoxKeyPair();
|
|
654
|
-
}
|
|
655
|
-
const subtle = getSubtleCrypto();
|
|
656
|
-
const pair = await subtle.generateKey({ name: "ECDH", namedCurve: "P-256" }, true, ["deriveBits"]);
|
|
657
|
-
return {
|
|
658
|
-
publicKey: new Uint8Array(await subtle.exportKey("raw", pair.publicKey)),
|
|
659
|
-
secretKey: new Uint8Array(await subtle.exportKey("pkcs8", pair.privateKey)),
|
|
660
|
-
};
|
|
411
|
+
/** Async X25519 box keypair generation. */
|
|
412
|
+
export function xBoxKeyPairAsync() {
|
|
413
|
+
return Promise.resolve(xBoxKeyPair());
|
|
661
414
|
}
|
|
662
415
|
/** Restore an X25519 box key pair from a 32-byte secret key. */
|
|
663
416
|
export function xBoxKeyPairFromSecret(secretKey) {
|
|
664
417
|
return provider().boxKeyPairFromSecret(secretKey);
|
|
665
418
|
}
|
|
666
419
|
// ── Key pair type ───────────────────────────────────────────────────────────
|
|
667
|
-
/** Async box key restore from private key material. */
|
|
668
|
-
export
|
|
669
|
-
|
|
670
|
-
return xBoxKeyPairFromSecret(secretKey);
|
|
671
|
-
}
|
|
672
|
-
if (secretKey.length === 32) {
|
|
673
|
-
return fipsEcdhKeyPairFrom32ByteSeed(secretKey, getSubtleCrypto());
|
|
674
|
-
}
|
|
675
|
-
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));
|
|
676
423
|
}
|
|
677
424
|
// ── Key generation ─────────────────────────────────────────────────────────
|
|
678
425
|
/**
|
|
@@ -706,44 +453,9 @@ export function xConcat(...arrays) {
|
|
|
706
453
|
export function xDH(myPrivateKey, theirPublicKey) {
|
|
707
454
|
return provider().boxBefore(myPrivateKey, theirPublicKey);
|
|
708
455
|
}
|
|
709
|
-
/** Async DH
|
|
710
|
-
export
|
|
711
|
-
|
|
712
|
-
return xDH(myPrivateKey, theirPublicKey);
|
|
713
|
-
}
|
|
714
|
-
const subtle = getSubtleCrypto();
|
|
715
|
-
const privateKey = await importEcdhPrivateKey(myPrivateKey);
|
|
716
|
-
const publicKey = await importEcdhPublicKey(theirPublicKey);
|
|
717
|
-
const shared = await subtle.deriveBits({ name: "ECDH", public: publicKey }, privateKey, 256);
|
|
718
|
-
return new Uint8Array(shared);
|
|
719
|
-
}
|
|
720
|
-
/**
|
|
721
|
-
* In `fips` mode only: derive a P-256 ECDH `KeyPair` (raw public + pkcs8 secret)
|
|
722
|
-
* from a P-256 ECDSA `KeyPair` (spki + pkcs8) using the same private scalar in Web Crypto.
|
|
723
|
-
* In `tweetnacl` mode, use `XKeyConvert.convertKeyPair` to map Ed25519 → X25519 instead.
|
|
724
|
-
*/
|
|
725
|
-
export async function xEcdhKeyPairFromEcdsaKeyPairAsync(sign) {
|
|
726
|
-
if (activeCryptoProfile === "tweetnacl") {
|
|
727
|
-
return Promise.reject(new Error('xEcdhKeyPairFromEcdsaKeyPairAsync is for crypto profile "fips" only. Use XKeyConvert.convertKeyPair in tweetnacl mode.'));
|
|
728
|
-
}
|
|
729
|
-
const subtle = getSubtleCrypto();
|
|
730
|
-
const ecdsaPriv = await importEcdsaPrivateKey(sign.secretKey);
|
|
731
|
-
const jwk = await subtle.exportKey("jwk", ecdsaPriv);
|
|
732
|
-
if (typeof jwk.d !== "string" || jwk.d.length === 0) {
|
|
733
|
-
throw new Error("FIPS: could not export ECDSA private as JWK.");
|
|
734
|
-
}
|
|
735
|
-
const ecdhJwk = { ...jwk, key_ops: ["deriveBits"] };
|
|
736
|
-
ecdhJwk.ext = true;
|
|
737
|
-
const ecdhPriv = await subtle.importKey("jwk", ecdhJwk, { name: "ECDH", namedCurve: "P-256" }, true, ["deriveBits"]);
|
|
738
|
-
const ecdhPubJwk = await subtle.exportKey("jwk", ecdhPriv);
|
|
739
|
-
const ecdhPubJwkNoD = { ...ecdhPubJwk };
|
|
740
|
-
delete ecdhPubJwkNoD.d;
|
|
741
|
-
ecdhPubJwkNoD.key_ops = [];
|
|
742
|
-
const ecdhPub = await subtle.importKey("jwk", ecdhPubJwkNoD, { name: "ECDH", namedCurve: "P-256" }, true, []);
|
|
743
|
-
return {
|
|
744
|
-
publicKey: new Uint8Array(await subtle.exportKey("raw", ecdhPub)),
|
|
745
|
-
secretKey: new Uint8Array(await subtle.exportKey("pkcs8", ecdhPriv)),
|
|
746
|
-
};
|
|
456
|
+
/** Async X25519 DH. */
|
|
457
|
+
export function xDHAsync(myPrivateKey, theirPublicKey) {
|
|
458
|
+
return Promise.resolve(xDH(myPrivateKey, theirPublicKey));
|
|
747
459
|
}
|
|
748
460
|
// ── Signing ────────────────────────────────────────────────────────────────
|
|
749
461
|
/**
|
|
@@ -807,119 +519,49 @@ export function xRandomBytes(length) {
|
|
|
807
519
|
export function xSecretbox(plaintext, nonce, key) {
|
|
808
520
|
return provider().secretbox(plaintext, nonce, key);
|
|
809
521
|
}
|
|
810
|
-
/** Async
|
|
811
|
-
export
|
|
812
|
-
|
|
813
|
-
return xSecretbox(plaintext, nonce, key);
|
|
814
|
-
}
|
|
815
|
-
const subtle = getSubtleCrypto();
|
|
816
|
-
const iv = requireAesGcmNonce(nonce);
|
|
817
|
-
const aesKey = await importAesKey(key, ["encrypt"]);
|
|
818
|
-
const ciphertext = await subtle.encrypt({ iv: toBufferSource(iv), name: "AES-GCM" }, aesKey, toBufferSource(plaintext));
|
|
819
|
-
return new Uint8Array(ciphertext);
|
|
522
|
+
/** Async XSalsa20-Poly1305 encryption. */
|
|
523
|
+
export function xSecretboxAsync(plaintext, nonce, key) {
|
|
524
|
+
return Promise.resolve(xSecretbox(plaintext, nonce, key));
|
|
820
525
|
}
|
|
821
526
|
/** Decrypt with a shared secret key. Returns null if authentication fails. */
|
|
822
527
|
export function xSecretboxOpen(ciphertext, nonce, key) {
|
|
823
528
|
return provider().secretboxOpen(ciphertext, nonce, key);
|
|
824
529
|
}
|
|
825
|
-
/** Async
|
|
826
|
-
export
|
|
827
|
-
|
|
828
|
-
return xSecretboxOpen(ciphertext, nonce, key);
|
|
829
|
-
}
|
|
830
|
-
const subtle = getSubtleCrypto();
|
|
831
|
-
const iv = requireAesGcmNonce(nonce);
|
|
832
|
-
const aesKey = await importAesKey(key, ["decrypt"]);
|
|
833
|
-
try {
|
|
834
|
-
const plaintext = await subtle.decrypt({ iv: toBufferSource(iv), name: "AES-GCM" }, aesKey, toBufferSource(ciphertext));
|
|
835
|
-
return new Uint8Array(plaintext);
|
|
836
|
-
}
|
|
837
|
-
catch {
|
|
838
|
-
return null;
|
|
839
|
-
}
|
|
530
|
+
/** Async XSalsa20-Poly1305 decryption. */
|
|
531
|
+
export function xSecretboxOpenAsync(ciphertext, nonce, key) {
|
|
532
|
+
return Promise.resolve(xSecretboxOpen(ciphertext, nonce, key));
|
|
840
533
|
}
|
|
841
534
|
/** Sign a message with an Ed25519 secret key. Returns signed message (64-byte signature prefix + message). */
|
|
842
535
|
export function xSign(message, secretKey) {
|
|
843
536
|
return provider().sign(message, secretKey);
|
|
844
537
|
}
|
|
845
|
-
/** Async signing
|
|
846
|
-
export
|
|
847
|
-
|
|
848
|
-
return xSign(message, secretKey);
|
|
849
|
-
}
|
|
850
|
-
const subtle = getSubtleCrypto();
|
|
851
|
-
const privateKey = await importEcdsaPrivateKey(secretKey);
|
|
852
|
-
const signature = new Uint8Array(await subtle.sign({ hash: "SHA-256", name: "ECDSA" }, privateKey, toBufferSource(message)));
|
|
853
|
-
return encodeFipsSignedMessage(signature, message);
|
|
538
|
+
/** Async Ed25519 signing. */
|
|
539
|
+
export function xSignAsync(message, secretKey) {
|
|
540
|
+
return Promise.resolve(xSign(message, secretKey));
|
|
854
541
|
}
|
|
855
542
|
/** Generate a fresh Ed25519 signing key pair. */
|
|
856
543
|
export function xSignKeyPair() {
|
|
857
544
|
return provider().signKeyPair();
|
|
858
545
|
}
|
|
859
|
-
/** Async keypair generation
|
|
860
|
-
export
|
|
861
|
-
|
|
862
|
-
return xSignKeyPair();
|
|
863
|
-
}
|
|
864
|
-
const subtle = getSubtleCrypto();
|
|
865
|
-
const pair = await subtle.generateKey({ name: "ECDSA", namedCurve: "P-256" }, true, ["sign", "verify"]);
|
|
866
|
-
return {
|
|
867
|
-
publicKey: new Uint8Array(await subtle.exportKey("spki", pair.publicKey)),
|
|
868
|
-
secretKey: new Uint8Array(await subtle.exportKey("pkcs8", pair.privateKey)),
|
|
869
|
-
};
|
|
546
|
+
/** Async Ed25519 keypair generation. */
|
|
547
|
+
export function xSignKeyPairAsync() {
|
|
548
|
+
return Promise.resolve(xSignKeyPair());
|
|
870
549
|
}
|
|
871
550
|
/** Restore an Ed25519 signing key pair from a 64-byte secret key. */
|
|
872
551
|
export function xSignKeyPairFromSecret(secretKey) {
|
|
873
552
|
return provider().signKeyPairFromSecret(secretKey);
|
|
874
553
|
}
|
|
875
|
-
/** Async restore of signing keypair
|
|
876
|
-
export
|
|
877
|
-
|
|
878
|
-
return xSignKeyPairFromSecret(secretKey);
|
|
879
|
-
}
|
|
880
|
-
return fipsEcdsaKeyPairFromPkcs8(secretKey, getSubtleCrypto());
|
|
554
|
+
/** Async restore of an Ed25519 signing keypair. */
|
|
555
|
+
export function xSignKeyPairFromSecretAsync(secretKey) {
|
|
556
|
+
return Promise.resolve(xSignKeyPairFromSecret(secretKey));
|
|
881
557
|
}
|
|
882
558
|
/** Verify and open a signed message. Returns the original message, or null if verification fails. */
|
|
883
559
|
export function xSignOpen(signedMessage, publicKey) {
|
|
884
560
|
return provider().signOpen(signedMessage, publicKey);
|
|
885
561
|
}
|
|
886
|
-
/** Async verify/open
|
|
887
|
-
export
|
|
888
|
-
|
|
889
|
-
return xSignOpen(signedMessage, publicKey);
|
|
890
|
-
}
|
|
891
|
-
const subtle = getSubtleCrypto();
|
|
892
|
-
const parsed = decodeFipsSignedMessage(signedMessage);
|
|
893
|
-
const verifyKey = await importEcdsaPublicKey(publicKey);
|
|
894
|
-
const valid = await subtle.verify({ hash: "SHA-256", name: "ECDSA" }, verifyKey, toBufferSource(parsed.signature), toBufferSource(parsed.message));
|
|
895
|
-
return valid ? parsed.message : null;
|
|
896
|
-
}
|
|
897
|
-
async function fipsEcdsaKeyPairFromPkcs8(secretKey, subtle) {
|
|
898
|
-
const ecdsaPriv = await importEcdsaPrivateKey(secretKey);
|
|
899
|
-
const jwk = await subtle.exportKey("jwk", ecdsaPriv);
|
|
900
|
-
const ecdsaPubJwk = { ...jwk };
|
|
901
|
-
delete ecdsaPubJwk.d;
|
|
902
|
-
ecdsaPubJwk.key_ops = ["verify"];
|
|
903
|
-
const ecdsaPub = await subtle.importKey("jwk", ecdsaPubJwk, { name: "ECDSA", namedCurve: "P-256" }, true, ["verify"]);
|
|
904
|
-
return {
|
|
905
|
-
publicKey: new Uint8Array(await subtle.exportKey("spki", ecdsaPub)),
|
|
906
|
-
secretKey: Uint8Array.from(secretKey),
|
|
907
|
-
};
|
|
908
|
-
}
|
|
909
|
-
async function importAesKey(key, usages) {
|
|
910
|
-
return getSubtleCrypto().importKey("raw", toBufferSource(key), { name: "AES-GCM" }, false, usages);
|
|
911
|
-
}
|
|
912
|
-
async function importEcdhPrivateKey(secretKey) {
|
|
913
|
-
return getSubtleCrypto().importKey("pkcs8", toBufferSource(secretKey), { name: "ECDH", namedCurve: "P-256" }, true, ["deriveBits"]);
|
|
914
|
-
}
|
|
915
|
-
async function importEcdhPublicKey(publicKey) {
|
|
916
|
-
return getSubtleCrypto().importKey("raw", toBufferSource(publicKey), { name: "ECDH", namedCurve: "P-256" }, true, []);
|
|
917
|
-
}
|
|
918
|
-
async function importEcdsaPrivateKey(secretKey) {
|
|
919
|
-
return getSubtleCrypto().importKey("pkcs8", toBufferSource(secretKey), { name: "ECDSA", namedCurve: "P-256" }, true, ["sign"]);
|
|
920
|
-
}
|
|
921
|
-
async function importEcdsaPublicKey(publicKey) {
|
|
922
|
-
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));
|
|
923
565
|
}
|
|
924
566
|
/**
|
|
925
567
|
* @internal
|