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