react-native-quick-crypto 1.0.0 → 1.0.2

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.
Files changed (92) hide show
  1. package/QuickCrypto.podspec +19 -52
  2. package/android/CMakeLists.txt +4 -2
  3. package/android/build.gradle +1 -1
  4. package/cpp/cipher/HybridCipher.cpp +20 -3
  5. package/cpp/cipher/HybridRsaCipher.cpp +20 -1
  6. package/cpp/ed25519/HybridEdKeyPair.cpp +8 -2
  7. package/cpp/keys/HybridKeyObjectHandle.cpp +8 -0
  8. package/cpp/keys/KeyObjectData.hpp +1 -1
  9. package/cpp/mldsa/HybridMlDsaKeyPair.cpp +264 -0
  10. package/cpp/mldsa/HybridMlDsaKeyPair.hpp +47 -0
  11. package/cpp/sign/HybridSignHandle.cpp +97 -22
  12. package/cpp/sign/HybridVerifyHandle.cpp +90 -21
  13. package/deps/ncrypto/.bazelignore +4 -0
  14. package/deps/ncrypto/.bazelrc +2 -0
  15. package/deps/ncrypto/.bazelversion +1 -0
  16. package/deps/ncrypto/.clang-format +111 -0
  17. package/deps/ncrypto/.github/workflows/bazel.yml +58 -0
  18. package/deps/ncrypto/.github/workflows/linter.yml +38 -0
  19. package/deps/ncrypto/.github/workflows/macos.yml +43 -0
  20. package/deps/ncrypto/.github/workflows/ubuntu.yml +46 -0
  21. package/deps/ncrypto/.github/workflows/visual-studio.yml +49 -0
  22. package/deps/ncrypto/.python-version +1 -0
  23. package/deps/ncrypto/BUILD.bazel +36 -0
  24. package/deps/ncrypto/CMakeLists.txt +55 -0
  25. package/deps/ncrypto/LICENSE +21 -0
  26. package/deps/ncrypto/MODULE.bazel +1 -0
  27. package/deps/ncrypto/MODULE.bazel.lock +280 -0
  28. package/deps/ncrypto/README.md +18 -0
  29. package/deps/ncrypto/WORKSPACE +15 -0
  30. package/deps/ncrypto/cmake/CPM.cmake +1225 -0
  31. package/deps/ncrypto/cmake/ncrypto-flags.cmake +16 -0
  32. package/deps/ncrypto/include/dh-primes.h +67 -0
  33. package/deps/ncrypto/{ncrypto.h → include/ncrypto.h} +361 -89
  34. package/deps/ncrypto/patches/0001-Expose-libdecrepit-so-NodeJS-can-use-it-for-ncrypto.patch +28 -0
  35. package/deps/ncrypto/pyproject.toml +38 -0
  36. package/deps/ncrypto/src/CMakeLists.txt +15 -0
  37. package/deps/ncrypto/src/engine.cpp +93 -0
  38. package/deps/ncrypto/{ncrypto.cc → src/ncrypto.cpp} +1168 -234
  39. package/deps/ncrypto/tests/BUILD.bazel +9 -0
  40. package/deps/ncrypto/tests/CMakeLists.txt +7 -0
  41. package/deps/ncrypto/tests/basic.cpp +86 -0
  42. package/deps/ncrypto/tools/run-clang-format.sh +42 -0
  43. package/lib/commonjs/ed.js +68 -0
  44. package/lib/commonjs/ed.js.map +1 -1
  45. package/lib/commonjs/keys/classes.js +6 -0
  46. package/lib/commonjs/keys/classes.js.map +1 -1
  47. package/lib/commonjs/mldsa.js +69 -0
  48. package/lib/commonjs/mldsa.js.map +1 -0
  49. package/lib/commonjs/specs/mlDsaKeyPair.nitro.js +6 -0
  50. package/lib/commonjs/specs/mlDsaKeyPair.nitro.js.map +1 -0
  51. package/lib/commonjs/subtle.js +483 -13
  52. package/lib/commonjs/subtle.js.map +1 -1
  53. package/lib/commonjs/utils/types.js.map +1 -1
  54. package/lib/module/ed.js +66 -0
  55. package/lib/module/ed.js.map +1 -1
  56. package/lib/module/keys/classes.js +6 -0
  57. package/lib/module/keys/classes.js.map +1 -1
  58. package/lib/module/mldsa.js +63 -0
  59. package/lib/module/mldsa.js.map +1 -0
  60. package/lib/module/specs/mlDsaKeyPair.nitro.js +4 -0
  61. package/lib/module/specs/mlDsaKeyPair.nitro.js.map +1 -0
  62. package/lib/module/subtle.js +484 -14
  63. package/lib/module/subtle.js.map +1 -1
  64. package/lib/module/utils/types.js.map +1 -1
  65. package/lib/tsconfig.tsbuildinfo +1 -1
  66. package/lib/typescript/ed.d.ts +4 -1
  67. package/lib/typescript/ed.d.ts.map +1 -1
  68. package/lib/typescript/index.d.ts +2 -0
  69. package/lib/typescript/index.d.ts.map +1 -1
  70. package/lib/typescript/keys/classes.d.ts +2 -0
  71. package/lib/typescript/keys/classes.d.ts.map +1 -1
  72. package/lib/typescript/mldsa.d.ts +18 -0
  73. package/lib/typescript/mldsa.d.ts.map +1 -0
  74. package/lib/typescript/specs/mlDsaKeyPair.nitro.d.ts +16 -0
  75. package/lib/typescript/specs/mlDsaKeyPair.nitro.d.ts.map +1 -0
  76. package/lib/typescript/subtle.d.ts +4 -1
  77. package/lib/typescript/subtle.d.ts.map +1 -1
  78. package/lib/typescript/utils/types.d.ts +14 -6
  79. package/lib/typescript/utils/types.d.ts.map +1 -1
  80. package/nitrogen/generated/android/QuickCrypto+autolinking.cmake +1 -0
  81. package/nitrogen/generated/android/QuickCryptoOnLoad.cpp +10 -0
  82. package/nitrogen/generated/ios/QuickCryptoAutolinking.mm +10 -0
  83. package/nitrogen/generated/shared/c++/AsymmetricKeyType.hpp +12 -0
  84. package/nitrogen/generated/shared/c++/HybridMlDsaKeyPairSpec.cpp +29 -0
  85. package/nitrogen/generated/shared/c++/HybridMlDsaKeyPairSpec.hpp +73 -0
  86. package/package.json +7 -3
  87. package/src/ed.ts +102 -0
  88. package/src/keys/classes.ts +9 -0
  89. package/src/mldsa.ts +125 -0
  90. package/src/specs/mlDsaKeyPair.nitro.ts +29 -0
  91. package/src/subtle.ts +667 -17
  92. package/src/utils/types.ts +27 -6
@@ -17,7 +17,8 @@ import { rsa_generateKeyPair } from './rsa';
17
17
  import { getRandomValues } from './random';
18
18
  import { createHmac } from './hmac';
19
19
  import { createSign, createVerify } from './keys/signVerify';
20
- import { ed_generateKeyPairWebCrypto, Ed } from './ed';
20
+ import { ed_generateKeyPairWebCrypto, x_generateKeyPairWebCrypto, xDeriveBits, Ed } from './ed';
21
+ import { mldsa_generateKeyPairWebCrypto } from './mldsa';
21
22
  // import { pbkdf2DeriveBits } from './pbkdf2';
22
23
  // import { aesCipher, aesGenerateKey, aesImportKey, getAlgorithmName } from './aes';
23
24
  // import { rsaCipher, rsaExportKey, rsaImportKey, rsaKeyGenerate } from './rsa';
@@ -266,6 +267,120 @@ async function aesGcmCipher(mode, key, data, algorithm) {
266
267
  return result.buffer;
267
268
  }
268
269
  }
270
+ async function aesKwCipher(mode, key, data) {
271
+ const isWrap = mode === CipherOrWrapMode.kWebCryptoCipherEncrypt;
272
+
273
+ // AES-KW requires input to be a multiple of 8 bytes (64 bits)
274
+ if (data.byteLength % 8 !== 0) {
275
+ throw lazyDOMException(`AES-KW input length must be a multiple of 8 bytes, got ${data.byteLength}`, 'OperationError');
276
+ }
277
+
278
+ // AES-KW requires at least 16 bytes of input (128 bits)
279
+ if (isWrap && data.byteLength < 16) {
280
+ throw lazyDOMException(`AES-KW input must be at least 16 bytes, got ${data.byteLength}`, 'OperationError');
281
+ }
282
+
283
+ // Get cipher type based on key length
284
+ const keyLength = key.algorithm.length;
285
+ // Use aes*-wrap for both operations (matching Node.js)
286
+ const cipherType = `aes${keyLength}-wrap`;
287
+
288
+ // Export key material
289
+ const exportedKey = key.keyObject.export();
290
+ const cipherKey = bufferLikeToArrayBuffer(exportedKey);
291
+
292
+ // AES-KW uses a default IV as specified in RFC 3394
293
+ const defaultWrapIV = new Uint8Array([0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6]);
294
+ const factory = NitroModules.createHybridObject('CipherFactory');
295
+ const cipher = factory.createCipher({
296
+ isCipher: isWrap,
297
+ cipherType,
298
+ cipherKey,
299
+ iv: defaultWrapIV.buffer // RFC 3394 default IV for AES-KW
300
+ });
301
+
302
+ // Process data
303
+ const updated = cipher.update(data);
304
+ const final = cipher.final();
305
+
306
+ // Concatenate results
307
+ const result = new Uint8Array(updated.byteLength + final.byteLength);
308
+ result.set(new Uint8Array(updated), 0);
309
+ result.set(new Uint8Array(final), updated.byteLength);
310
+ return result.buffer;
311
+ }
312
+ async function chaCha20Poly1305Cipher(mode, key, data, algorithm) {
313
+ const {
314
+ iv,
315
+ additionalData,
316
+ tagLength = 128
317
+ } = algorithm;
318
+
319
+ // Validate IV (must be 12 bytes for ChaCha20-Poly1305)
320
+ const ivBuffer = bufferLikeToArrayBuffer(iv);
321
+ if (!ivBuffer || ivBuffer.byteLength !== 12) {
322
+ throw lazyDOMException('ChaCha20-Poly1305 IV must be exactly 12 bytes', 'OperationError');
323
+ }
324
+
325
+ // Validate tag length (only 128-bit supported)
326
+ if (tagLength !== 128) {
327
+ throw lazyDOMException('ChaCha20-Poly1305 only supports 128-bit auth tags', 'NotSupportedError');
328
+ }
329
+ const tagByteLength = 16; // 128 bits = 16 bytes
330
+
331
+ // Create cipher using existing ChaCha20-Poly1305 implementation
332
+ const factory = NitroModules.createHybridObject('CipherFactory');
333
+ const cipher = factory.createCipher({
334
+ isCipher: mode === CipherOrWrapMode.kWebCryptoCipherEncrypt,
335
+ cipherType: 'chacha20-poly1305',
336
+ cipherKey: bufferLikeToArrayBuffer(key.keyObject.export()),
337
+ iv: ivBuffer,
338
+ authTagLen: tagByteLength
339
+ });
340
+ let processData;
341
+ let authTag;
342
+ if (mode === CipherOrWrapMode.kWebCryptoCipherDecrypt) {
343
+ // For decryption, extract auth tag from end of data
344
+ const dataView = new Uint8Array(data);
345
+ if (dataView.byteLength < tagByteLength) {
346
+ throw lazyDOMException('The provided data is too small.', 'OperationError');
347
+ }
348
+
349
+ // Split data and tag
350
+ const ciphertextLength = dataView.byteLength - tagByteLength;
351
+ processData = dataView.slice(0, ciphertextLength).buffer;
352
+ authTag = dataView.slice(ciphertextLength).buffer;
353
+
354
+ // Set auth tag for verification
355
+ cipher.setAuthTag(authTag);
356
+ } else {
357
+ processData = data;
358
+ }
359
+
360
+ // Set additional authenticated data if provided
361
+ if (additionalData) {
362
+ cipher.setAAD(bufferLikeToArrayBuffer(additionalData));
363
+ }
364
+
365
+ // Process data
366
+ const updated = cipher.update(processData);
367
+ const final = cipher.final();
368
+ if (mode === CipherOrWrapMode.kWebCryptoCipherEncrypt) {
369
+ // For encryption, append auth tag to result
370
+ const tag = cipher.getAuthTag();
371
+ const result = new Uint8Array(updated.byteLength + final.byteLength + tag.byteLength);
372
+ result.set(new Uint8Array(updated), 0);
373
+ result.set(new Uint8Array(final), updated.byteLength);
374
+ result.set(new Uint8Array(tag), updated.byteLength + final.byteLength);
375
+ return result.buffer;
376
+ } else {
377
+ // For decryption, just concatenate plaintext
378
+ const result = new Uint8Array(updated.byteLength + final.byteLength);
379
+ result.set(new Uint8Array(updated), 0);
380
+ result.set(new Uint8Array(final), updated.byteLength);
381
+ return result.buffer;
382
+ }
383
+ }
269
384
  async function aesGenerateKey(algorithm, extractable, keyUsages) {
270
385
  const {
271
386
  length
@@ -346,10 +461,13 @@ async function hmacGenerateKey(algorithm, extractable, keyUsages) {
346
461
  // Create secret key
347
462
  const keyObject = createSecretKey(keyBytes);
348
463
 
349
- // Construct algorithm object
464
+ // Construct algorithm object with hash normalized to { name: string } format per WebCrypto spec
465
+ const webCryptoHashName = normalizeHashName(hash, HashContext.WebCrypto);
350
466
  const keyAlgorithm = {
351
467
  name: 'HMAC',
352
- hash: hashName,
468
+ hash: {
469
+ name: webCryptoHashName
470
+ },
353
471
  length
354
472
  };
355
473
  return new CryptoKey(keyObject, keyAlgorithm, keyUsages, extractable);
@@ -423,10 +541,17 @@ function rsaImportKey(format, data, algorithm, extractable, keyUsages) {
423
541
  }
424
542
  publicExponentBytes = new Uint8Array(bytes.length > 0 ? bytes : [0]);
425
543
  }
544
+
545
+ // Normalize hash to { name: string } format per WebCrypto spec
546
+ const hashName = normalizeHashName(algorithm.hash, HashContext.WebCrypto);
547
+ const normalizedHash = {
548
+ name: hashName
549
+ };
426
550
  const algorithmWithDetails = {
427
551
  ...algorithm,
428
552
  modulusLength: keyDetails?.modulusLength,
429
- publicExponent: publicExponentBytes
553
+ publicExponent: publicExponentBytes,
554
+ hash: normalizedHash
430
555
  };
431
556
  return new CryptoKey(keyObject, algorithmWithDetails, keyUsages, extractable);
432
557
  }
@@ -473,10 +598,17 @@ async function hmacImportKey(algorithm, format, data, extractable, keyUsages) {
473
598
  } else {
474
599
  throw new Error(`Unable to import HMAC key with format ${format}`);
475
600
  }
476
- return new CryptoKey(keyObject, {
601
+
602
+ // Normalize hash to { name: string } format per WebCrypto spec
603
+ const hashName = normalizeHashName(algorithm.hash, HashContext.WebCrypto);
604
+ const normalizedAlgorithm = {
477
605
  ...algorithm,
478
- name: 'HMAC'
479
- }, keyUsages, extractable);
606
+ name: 'HMAC',
607
+ hash: {
608
+ name: hashName
609
+ }
610
+ };
611
+ return new CryptoKey(keyObject, normalizedAlgorithm, keyUsages, extractable);
480
612
  }
481
613
  async function aesImportKey(algorithm, format, data, extractable, keyUsages) {
482
614
  const {
@@ -536,7 +668,9 @@ function edImportKey(format, data, algorithm, extractable, keyUsages) {
536
668
  } = algorithm;
537
669
 
538
670
  // Validate usages
539
- if (hasAnyNotIn(keyUsages, ['sign', 'verify'])) {
671
+ const isX = name === 'X25519' || name === 'X448';
672
+ const allowedUsages = isX ? ['deriveKey', 'deriveBits'] : ['sign', 'verify'];
673
+ if (hasAnyNotIn(keyUsages, allowedUsages)) {
540
674
  throw lazyDOMException(`Unsupported key usage for ${name} key`, 'SyntaxError');
541
675
  }
542
676
  let keyObject;
@@ -563,6 +697,31 @@ function edImportKey(format, data, algorithm, extractable, keyUsages) {
563
697
  name
564
698
  }, keyUsages, extractable);
565
699
  }
700
+ function mldsaImportKey(format, data, algorithm, extractable, keyUsages) {
701
+ const {
702
+ name
703
+ } = algorithm;
704
+
705
+ // Validate usages
706
+ if (hasAnyNotIn(keyUsages, ['sign', 'verify'])) {
707
+ throw lazyDOMException(`Unsupported key usage for ${name} key`, 'SyntaxError');
708
+ }
709
+ let keyObject;
710
+ if (format === 'spki') {
711
+ // Import public key
712
+ const keyData = bufferLikeToArrayBuffer(data);
713
+ keyObject = KeyObject.createKeyObject('public', keyData, KFormatType.DER, KeyEncoding.SPKI);
714
+ } else if (format === 'pkcs8') {
715
+ // Import private key
716
+ const keyData = bufferLikeToArrayBuffer(data);
717
+ keyObject = KeyObject.createKeyObject('private', keyData, KFormatType.DER, KeyEncoding.PKCS8);
718
+ } else {
719
+ throw lazyDOMException(`Unsupported format for ${name} import: ${format}`, 'NotSupportedError');
720
+ }
721
+ return new CryptoKey(keyObject, {
722
+ name
723
+ }, keyUsages, extractable);
724
+ }
566
725
  const exportKeySpki = async key => {
567
726
  switch (key.algorithm.name) {
568
727
  case 'RSASSA-PKCS1-v1_5':
@@ -584,8 +743,22 @@ const exportKeySpki = async key => {
584
743
  case 'Ed25519':
585
744
  // Fall through
586
745
  case 'Ed448':
746
+ // Fall through
747
+ case 'X25519':
748
+ // Fall through
749
+ case 'X448':
750
+ if (key.type === 'public') {
751
+ // Export Ed/X key in SPKI DER format
752
+ return bufferLikeToArrayBuffer(key.keyObject.handle.exportKey(KFormatType.DER, KeyEncoding.SPKI));
753
+ }
754
+ break;
755
+ case 'ML-DSA-44':
756
+ // Fall through
757
+ case 'ML-DSA-65':
758
+ // Fall through
759
+ case 'ML-DSA-87':
587
760
  if (key.type === 'public') {
588
- // Export Ed key in SPKI DER format
761
+ // Export ML-DSA key in SPKI DER format
589
762
  return bufferLikeToArrayBuffer(key.keyObject.handle.exportKey(KFormatType.DER, KeyEncoding.SPKI));
590
763
  }
591
764
  break;
@@ -613,8 +786,22 @@ const exportKeyPkcs8 = async key => {
613
786
  case 'Ed25519':
614
787
  // Fall through
615
788
  case 'Ed448':
789
+ // Fall through
790
+ case 'X25519':
791
+ // Fall through
792
+ case 'X448':
616
793
  if (key.type === 'private') {
617
- // Export Ed key in PKCS8 DER format
794
+ // Export Ed/X key in PKCS8 DER format
795
+ return bufferLikeToArrayBuffer(key.keyObject.handle.exportKey(KFormatType.DER, KeyEncoding.PKCS8));
796
+ }
797
+ break;
798
+ case 'ML-DSA-44':
799
+ // Fall through
800
+ case 'ML-DSA-65':
801
+ // Fall through
802
+ case 'ML-DSA-87':
803
+ if (key.type === 'private') {
804
+ // Export ML-DSA key in PKCS8 DER format
618
805
  return bufferLikeToArrayBuffer(key.keyObject.handle.exportKey(KFormatType.DER, KeyEncoding.PKCS8));
619
806
  }
620
807
  break;
@@ -630,6 +817,19 @@ const exportKeyRaw = key => {
630
817
  return ecExportKey(key, KWebCryptoKeyFormat.kWebCryptoKeyFormatRaw);
631
818
  }
632
819
  break;
820
+ case 'Ed25519':
821
+ // Fall through
822
+ case 'Ed448':
823
+ // Fall through
824
+ case 'X25519':
825
+ // Fall through
826
+ case 'X448':
827
+ if (key.type === 'public') {
828
+ // Export raw public key
829
+ const exported = key.keyObject.handle.exportKey();
830
+ return bufferLikeToArrayBuffer(exported);
831
+ }
832
+ break;
633
833
  case 'AES-CTR':
634
834
  // Fall through
635
835
  case 'AES-CBC':
@@ -638,6 +838,8 @@ const exportKeyRaw = key => {
638
838
  // Fall through
639
839
  case 'AES-KW':
640
840
  // Fall through
841
+ case 'ChaCha20-Poly1305':
842
+ // Fall through
641
843
  case 'HMAC':
642
844
  {
643
845
  const exported = key.keyObject.export();
@@ -677,6 +879,8 @@ const exportKeyJWK = key => {
677
879
  case 'AES-GCM':
678
880
  // Fall through
679
881
  case 'AES-KW':
882
+ // Fall through
883
+ case 'ChaCha20-Poly1305':
680
884
  if (key.algorithm.length === undefined) {
681
885
  throw lazyDOMException(`Algorithm ${key.algorithm.name} missing required length property`, 'InvalidAccessError');
682
886
  }
@@ -812,6 +1016,29 @@ function edSignVerify(key, data, signature) {
812
1016
  return ed.verifySync(signatureBuffer, dataBuffer, rawKey);
813
1017
  }
814
1018
  }
1019
+ function mldsaSignVerify(key, data, signature) {
1020
+ const isSign = signature === undefined;
1021
+ const expectedKeyType = isSign ? 'private' : 'public';
1022
+ if (key.type !== expectedKeyType) {
1023
+ throw lazyDOMException(`Key must be a ${expectedKeyType} key`, 'InvalidAccessError');
1024
+ }
1025
+ const dataBuffer = bufferLikeToArrayBuffer(data);
1026
+ if (isSign) {
1027
+ const signer = createSign('');
1028
+ signer.update(dataBuffer);
1029
+ const sig = signer.sign({
1030
+ key: key
1031
+ });
1032
+ return sig.buffer.slice(sig.byteOffset, sig.byteOffset + sig.byteLength);
1033
+ } else {
1034
+ const signatureBuffer = bufferLikeToArrayBuffer(signature);
1035
+ const verifier = createVerify('');
1036
+ verifier.update(dataBuffer);
1037
+ return verifier.verify({
1038
+ key: key
1039
+ }, signatureBuffer);
1040
+ }
1041
+ }
815
1042
  const signVerify = (algorithm, key, data, signature) => {
816
1043
  const usage = signature === undefined ? 'sign' : 'verify';
817
1044
  algorithm = normalizeAlgorithm(algorithm, usage);
@@ -830,6 +1057,10 @@ const signVerify = (algorithm, key, data, signature) => {
830
1057
  case 'Ed25519':
831
1058
  case 'Ed448':
832
1059
  return edSignVerify(key, data, signature);
1060
+ case 'ML-DSA-44':
1061
+ case 'ML-DSA-65':
1062
+ case 'ML-DSA-87':
1063
+ return mldsaSignVerify(key, data, signature);
833
1064
  }
834
1065
  throw lazyDOMException(`Unrecognized algorithm name '${algorithm.name}' for '${usage}'`, 'NotSupportedError');
835
1066
  };
@@ -847,6 +1078,10 @@ const cipherOrWrap = async (mode, algorithm, key, data, op) => {
847
1078
  // Fall through
848
1079
  case 'AES-GCM':
849
1080
  return aesCipher(mode, key, data, algorithm);
1081
+ case 'AES-KW':
1082
+ return aesKwCipher(mode, key, data);
1083
+ case 'ChaCha20-Poly1305':
1084
+ return chaCha20Poly1305Cipher(mode, key, data, algorithm);
850
1085
  }
851
1086
  };
852
1087
  export class Subtle {
@@ -859,16 +1094,49 @@ export class Subtle {
859
1094
  return asyncDigest(normalizedAlgorithm, data);
860
1095
  }
861
1096
  async deriveBits(algorithm, baseKey, length) {
862
- if (!baseKey.keyUsages.includes('deriveBits')) {
863
- throw new Error('baseKey does not have deriveBits usage');
1097
+ // Allow either deriveBits OR deriveKey usage (WebCrypto spec allows both)
1098
+ if (!baseKey.keyUsages.includes('deriveBits') && !baseKey.keyUsages.includes('deriveKey')) {
1099
+ throw new Error('baseKey does not have deriveBits or deriveKey usage');
864
1100
  }
865
1101
  if (baseKey.algorithm.name !== algorithm.name) throw new Error('Key algorithm mismatch');
866
1102
  switch (algorithm.name) {
867
1103
  case 'PBKDF2':
868
1104
  return pbkdf2DeriveBits(algorithm, baseKey, length);
1105
+ case 'X25519':
1106
+ // Fall through
1107
+ case 'X448':
1108
+ return xDeriveBits(algorithm, baseKey, length);
869
1109
  }
870
1110
  throw new Error(`'subtle.deriveBits()' for ${algorithm.name} is not implemented.`);
871
1111
  }
1112
+ async deriveKey(algorithm, baseKey, derivedKeyAlgorithm, extractable, keyUsages) {
1113
+ // Validate baseKey usage
1114
+ if (!baseKey.usages.includes('deriveKey') && !baseKey.usages.includes('deriveBits')) {
1115
+ throw lazyDOMException('baseKey does not have deriveKey or deriveBits usage', 'InvalidAccessError');
1116
+ }
1117
+
1118
+ // Calculate required key length
1119
+ const length = getKeyLength(derivedKeyAlgorithm);
1120
+
1121
+ // Step 1: Derive bits
1122
+ let derivedBits;
1123
+ if (baseKey.algorithm.name !== algorithm.name) throw new Error('Key algorithm mismatch');
1124
+ switch (algorithm.name) {
1125
+ case 'PBKDF2':
1126
+ derivedBits = await pbkdf2DeriveBits(algorithm, baseKey, length);
1127
+ break;
1128
+ case 'X25519':
1129
+ // Fall through
1130
+ case 'X448':
1131
+ derivedBits = await xDeriveBits(algorithm, baseKey, length);
1132
+ break;
1133
+ default:
1134
+ throw new Error(`'subtle.deriveKey()' for ${algorithm.name} is not implemented.`);
1135
+ }
1136
+
1137
+ // Step 2: Import as key
1138
+ return this.importKey('raw', derivedBits, derivedKeyAlgorithm, extractable, keyUsages);
1139
+ }
872
1140
  async encrypt(algorithm, key, data) {
873
1141
  const normalizedAlgorithm = normalizeAlgorithm(algorithm, 'encrypt');
874
1142
  return cipherOrWrap(CipherOrWrapMode.kWebCryptoCipherEncrypt, normalizedAlgorithm, key, bufferLikeToArrayBuffer(data), 'encrypt');
@@ -886,6 +1154,76 @@ export class Subtle {
886
1154
  return exportKeyRaw(key);
887
1155
  }
888
1156
  }
1157
+ async wrapKey(format, key, wrappingKey, wrapAlgorithm) {
1158
+ // Validate wrappingKey usage
1159
+ if (!wrappingKey.usages.includes('wrapKey')) {
1160
+ throw lazyDOMException('wrappingKey does not have wrapKey usage', 'InvalidAccessError');
1161
+ }
1162
+
1163
+ // Step 1: Export the key
1164
+ const exported = await this.exportKey(format, key);
1165
+
1166
+ // Step 2: Convert to ArrayBuffer if JWK
1167
+ let keyData;
1168
+ if (format === 'jwk') {
1169
+ const jwkString = JSON.stringify(exported);
1170
+ const buffer = SBuffer.from(jwkString, 'utf8');
1171
+
1172
+ // For AES-KW, pad to multiple of 8 bytes (accounting for null terminator)
1173
+ if (wrapAlgorithm.name === 'AES-KW') {
1174
+ const length = buffer.length;
1175
+ // Add 1 for null terminator, then pad to multiple of 8
1176
+ const paddedLength = Math.ceil((length + 1) / 8) * 8;
1177
+ const paddedBuffer = SBuffer.alloc(paddedLength);
1178
+ buffer.copy(paddedBuffer);
1179
+ // Null terminator for JSON string (remaining bytes are already zeros from alloc)
1180
+ paddedBuffer.writeUInt8(0, length);
1181
+ keyData = bufferLikeToArrayBuffer(paddedBuffer);
1182
+ } else {
1183
+ keyData = bufferLikeToArrayBuffer(buffer);
1184
+ }
1185
+ } else {
1186
+ keyData = exported;
1187
+ }
1188
+
1189
+ // Step 3: Encrypt the exported key
1190
+ return cipherOrWrap(CipherOrWrapMode.kWebCryptoCipherEncrypt, wrapAlgorithm, wrappingKey, keyData, 'wrapKey');
1191
+ }
1192
+ async unwrapKey(format, wrappedKey, unwrappingKey, unwrapAlgorithm, unwrappedKeyAlgorithm, extractable, keyUsages) {
1193
+ // Validate unwrappingKey usage
1194
+ if (!unwrappingKey.usages.includes('unwrapKey')) {
1195
+ throw lazyDOMException('unwrappingKey does not have unwrapKey usage', 'InvalidAccessError');
1196
+ }
1197
+
1198
+ // Step 1: Decrypt the wrapped key
1199
+ const decrypted = await cipherOrWrap(CipherOrWrapMode.kWebCryptoCipherDecrypt, unwrapAlgorithm, unwrappingKey, bufferLikeToArrayBuffer(wrappedKey), 'unwrapKey');
1200
+
1201
+ // Step 2: Convert to appropriate format
1202
+ let keyData;
1203
+ if (format === 'jwk') {
1204
+ const buffer = SBuffer.from(decrypted);
1205
+ // For AES-KW, the data may be padded - find the null terminator
1206
+ let jwkString;
1207
+ if (unwrapAlgorithm.name === 'AES-KW') {
1208
+ // Find the null terminator (if present) to get the original string
1209
+ const nullIndex = buffer.indexOf(0);
1210
+ if (nullIndex !== -1) {
1211
+ jwkString = buffer.toString('utf8', 0, nullIndex);
1212
+ } else {
1213
+ // No null terminator, try to parse the whole buffer
1214
+ jwkString = buffer.toString('utf8').trim();
1215
+ }
1216
+ } else {
1217
+ jwkString = buffer.toString('utf8');
1218
+ }
1219
+ keyData = JSON.parse(jwkString);
1220
+ } else {
1221
+ keyData = decrypted;
1222
+ }
1223
+
1224
+ // Step 3: Import the key
1225
+ return this.importKey(format, keyData, unwrappedKeyAlgorithm, extractable, keyUsages);
1226
+ }
889
1227
  async generateKey(algorithm, extractable, keyUsages) {
890
1228
  algorithm = normalizeAlgorithm(algorithm, 'generateKey');
891
1229
  let result;
@@ -912,6 +1250,18 @@ export class Subtle {
912
1250
  case 'AES-KW':
913
1251
  result = await aesGenerateKey(algorithm, extractable, keyUsages);
914
1252
  break;
1253
+ case 'ChaCha20-Poly1305':
1254
+ {
1255
+ const length = algorithm.length ?? 256;
1256
+ if (length !== 256) {
1257
+ throw lazyDOMException('ChaCha20-Poly1305 only supports 256-bit keys', 'NotSupportedError');
1258
+ }
1259
+ result = await aesGenerateKey({
1260
+ name: 'ChaCha20-Poly1305',
1261
+ length: 256
1262
+ }, extractable, keyUsages);
1263
+ break;
1264
+ }
915
1265
  case 'HMAC':
916
1266
  result = await hmacGenerateKey(algorithm, extractable, keyUsages);
917
1267
  break;
@@ -921,6 +1271,20 @@ export class Subtle {
921
1271
  result = await ed_generateKeyPairWebCrypto(algorithm.name.toLowerCase(), extractable, keyUsages);
922
1272
  checkCryptoKeyPairUsages(result);
923
1273
  break;
1274
+ case 'ML-DSA-44':
1275
+ // Fall through
1276
+ case 'ML-DSA-65':
1277
+ // Fall through
1278
+ case 'ML-DSA-87':
1279
+ result = await mldsa_generateKeyPairWebCrypto(algorithm.name, extractable, keyUsages);
1280
+ checkCryptoKeyPairUsages(result);
1281
+ break;
1282
+ case 'X25519':
1283
+ // Fall through
1284
+ case 'X448':
1285
+ result = await x_generateKeyPairWebCrypto(algorithm.name.toLowerCase(), extractable, keyUsages);
1286
+ checkCryptoKeyPairUsages(result);
1287
+ break;
924
1288
  default:
925
1289
  throw new Error(`'subtle.generateKey()' is not implemented for ${algorithm.name}.
926
1290
  Unrecognized algorithm name`);
@@ -953,16 +1317,29 @@ export class Subtle {
953
1317
  case 'AES-GCM':
954
1318
  // Fall through
955
1319
  case 'AES-KW':
1320
+ // Fall through
1321
+ case 'ChaCha20-Poly1305':
956
1322
  result = await aesImportKey(normalizedAlgorithm, format, data, extractable, keyUsages);
957
1323
  break;
958
1324
  case 'PBKDF2':
959
1325
  result = await importGenericSecretKey(normalizedAlgorithm, format, data, extractable, keyUsages);
960
1326
  break;
1327
+ case 'X25519':
1328
+ // Fall through
1329
+ case 'X448':
1330
+ // Fall through
961
1331
  case 'Ed25519':
962
1332
  // Fall through
963
1333
  case 'Ed448':
964
1334
  result = edImportKey(format, data, normalizedAlgorithm, extractable, keyUsages);
965
1335
  break;
1336
+ case 'ML-DSA-44':
1337
+ // Fall through
1338
+ case 'ML-DSA-65':
1339
+ // Fall through
1340
+ case 'ML-DSA-87':
1341
+ result = mldsaImportKey(format, data, normalizedAlgorithm, extractable, keyUsages);
1342
+ break;
966
1343
  default:
967
1344
  throw new Error(`"subtle.importKey()" is not implemented for ${normalizedAlgorithm.name}`);
968
1345
  }
@@ -972,11 +1349,104 @@ export class Subtle {
972
1349
  return result;
973
1350
  }
974
1351
  async sign(algorithm, key, data) {
975
- return signVerify(algorithm, key, data);
1352
+ const normalizedAlgorithm = normalizeAlgorithm(algorithm, 'sign');
1353
+ if (normalizedAlgorithm.name === 'HMAC') {
1354
+ // Validate key usage
1355
+ if (!key.usages.includes('sign')) {
1356
+ throw lazyDOMException('Key does not have sign usage', 'InvalidAccessError');
1357
+ }
1358
+
1359
+ // Get hash algorithm from key or algorithm params
1360
+ // Hash can be either a string or an object with name property
1361
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1362
+ const alg = normalizedAlgorithm;
1363
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1364
+ const keyAlg = key.algorithm;
1365
+ let hashAlgorithm = 'SHA-256';
1366
+ if (typeof alg.hash === 'string') {
1367
+ hashAlgorithm = alg.hash;
1368
+ } else if (alg.hash?.name) {
1369
+ hashAlgorithm = alg.hash.name;
1370
+ } else if (typeof keyAlg.hash === 'string') {
1371
+ hashAlgorithm = keyAlg.hash;
1372
+ } else if (keyAlg.hash?.name) {
1373
+ hashAlgorithm = keyAlg.hash.name;
1374
+ }
1375
+
1376
+ // Create HMAC and sign
1377
+ const keyData = key.keyObject.export();
1378
+ const hmac = createHmac(hashAlgorithm, keyData);
1379
+ hmac.update(bufferLikeToArrayBuffer(data));
1380
+ return bufferLikeToArrayBuffer(hmac.digest());
1381
+ }
1382
+ return signVerify(normalizedAlgorithm, key, data);
976
1383
  }
977
1384
  async verify(algorithm, key, signature, data) {
978
- return signVerify(algorithm, key, data, signature);
1385
+ const normalizedAlgorithm = normalizeAlgorithm(algorithm, 'verify');
1386
+ if (normalizedAlgorithm.name === 'HMAC') {
1387
+ // Validate key usage
1388
+ if (!key.usages.includes('verify')) {
1389
+ throw lazyDOMException('Key does not have verify usage', 'InvalidAccessError');
1390
+ }
1391
+
1392
+ // Get hash algorithm
1393
+ // Hash can be either a string or an object with name property
1394
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1395
+ const alg = normalizedAlgorithm;
1396
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1397
+ const keyAlg = key.algorithm;
1398
+ let hashAlgorithm = 'SHA-256';
1399
+ if (typeof alg.hash === 'string') {
1400
+ hashAlgorithm = alg.hash;
1401
+ } else if (alg.hash?.name) {
1402
+ hashAlgorithm = alg.hash.name;
1403
+ } else if (typeof keyAlg.hash === 'string') {
1404
+ hashAlgorithm = keyAlg.hash;
1405
+ } else if (keyAlg.hash?.name) {
1406
+ hashAlgorithm = keyAlg.hash.name;
1407
+ }
1408
+
1409
+ // Create HMAC and compute expected signature
1410
+ const keyData = key.keyObject.export();
1411
+ const hmac = createHmac(hashAlgorithm, keyData);
1412
+ const dataBuffer = bufferLikeToArrayBuffer(data);
1413
+ hmac.update(dataBuffer);
1414
+ const expectedDigest = hmac.digest();
1415
+ const expected = new Uint8Array(bufferLikeToArrayBuffer(expectedDigest));
1416
+
1417
+ // Constant-time comparison
1418
+ const signatureArray = new Uint8Array(bufferLikeToArrayBuffer(signature));
1419
+ if (expected.length !== signatureArray.length) {
1420
+ return false;
1421
+ }
1422
+
1423
+ // Manual constant-time comparison
1424
+ let result = 0;
1425
+ for (let i = 0; i < expected.length; i++) {
1426
+ result |= expected[i] ^ signatureArray[i];
1427
+ }
1428
+ return result === 0;
1429
+ }
1430
+ return signVerify(normalizedAlgorithm, key, data, signature);
979
1431
  }
980
1432
  }
981
1433
  export const subtle = new Subtle();
1434
+ function getKeyLength(algorithm) {
1435
+ const name = algorithm.name;
1436
+ switch (name) {
1437
+ case 'AES-CTR':
1438
+ case 'AES-CBC':
1439
+ case 'AES-GCM':
1440
+ case 'AES-KW':
1441
+ case 'ChaCha20-Poly1305':
1442
+ return algorithm.length || 256;
1443
+ case 'HMAC':
1444
+ {
1445
+ const hmacAlg = algorithm;
1446
+ return hmacAlg.length || 256;
1447
+ }
1448
+ default:
1449
+ throw lazyDOMException(`Cannot determine key length for ${name}`, 'NotSupportedError');
1450
+ }
1451
+ }
982
1452
  //# sourceMappingURL=subtle.js.map