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
package/src/subtle.ts CHANGED
@@ -15,6 +15,7 @@ import type {
15
15
  AesCbcParams,
16
16
  AesGcmParams,
17
17
  RsaOaepParams,
18
+ ChaCha20Poly1305Params,
18
19
  } from './utils';
19
20
  import { KFormatType, KeyEncoding } from './utils';
20
21
  import {
@@ -41,7 +42,13 @@ import { rsa_generateKeyPair } from './rsa';
41
42
  import { getRandomValues } from './random';
42
43
  import { createHmac } from './hmac';
43
44
  import { createSign, createVerify } from './keys/signVerify';
44
- import { ed_generateKeyPairWebCrypto, Ed } from './ed';
45
+ import {
46
+ ed_generateKeyPairWebCrypto,
47
+ x_generateKeyPairWebCrypto,
48
+ xDeriveBits,
49
+ Ed,
50
+ } from './ed';
51
+ import { mldsa_generateKeyPairWebCrypto, type MlDsaVariant } from './mldsa';
45
52
  // import { pbkdf2DeriveBits } from './pbkdf2';
46
53
  // import { aesCipher, aesGenerateKey, aesImportKey, getAlgorithmName } from './aes';
47
54
  // import { rsaCipher, rsaExportKey, rsaImportKey, rsaKeyGenerate } from './rsa';
@@ -368,6 +375,156 @@ async function aesGcmCipher(
368
375
  }
369
376
  }
370
377
 
378
+ async function aesKwCipher(
379
+ mode: CipherOrWrapMode,
380
+ key: CryptoKey,
381
+ data: ArrayBuffer,
382
+ ): Promise<ArrayBuffer> {
383
+ const isWrap = mode === CipherOrWrapMode.kWebCryptoCipherEncrypt;
384
+
385
+ // AES-KW requires input to be a multiple of 8 bytes (64 bits)
386
+ if (data.byteLength % 8 !== 0) {
387
+ throw lazyDOMException(
388
+ `AES-KW input length must be a multiple of 8 bytes, got ${data.byteLength}`,
389
+ 'OperationError',
390
+ );
391
+ }
392
+
393
+ // AES-KW requires at least 16 bytes of input (128 bits)
394
+ if (isWrap && data.byteLength < 16) {
395
+ throw lazyDOMException(
396
+ `AES-KW input must be at least 16 bytes, got ${data.byteLength}`,
397
+ 'OperationError',
398
+ );
399
+ }
400
+
401
+ // Get cipher type based on key length
402
+ const keyLength = (key.algorithm as { length: number }).length;
403
+ // Use aes*-wrap for both operations (matching Node.js)
404
+ const cipherType = `aes${keyLength}-wrap`;
405
+
406
+ // Export key material
407
+ const exportedKey = key.keyObject.export();
408
+ const cipherKey = bufferLikeToArrayBuffer(exportedKey);
409
+
410
+ // AES-KW uses a default IV as specified in RFC 3394
411
+ const defaultWrapIV = new Uint8Array([
412
+ 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6,
413
+ ]);
414
+
415
+ const factory =
416
+ NitroModules.createHybridObject<CipherFactory>('CipherFactory');
417
+
418
+ const cipher = factory.createCipher({
419
+ isCipher: isWrap,
420
+ cipherType,
421
+ cipherKey,
422
+ iv: defaultWrapIV.buffer, // RFC 3394 default IV for AES-KW
423
+ });
424
+
425
+ // Process data
426
+ const updated = cipher.update(data);
427
+ const final = cipher.final();
428
+
429
+ // Concatenate results
430
+ const result = new Uint8Array(updated.byteLength + final.byteLength);
431
+ result.set(new Uint8Array(updated), 0);
432
+ result.set(new Uint8Array(final), updated.byteLength);
433
+
434
+ return result.buffer;
435
+ }
436
+
437
+ async function chaCha20Poly1305Cipher(
438
+ mode: CipherOrWrapMode,
439
+ key: CryptoKey,
440
+ data: ArrayBuffer,
441
+ algorithm: ChaCha20Poly1305Params,
442
+ ): Promise<ArrayBuffer> {
443
+ const { iv, additionalData, tagLength = 128 } = algorithm;
444
+
445
+ // Validate IV (must be 12 bytes for ChaCha20-Poly1305)
446
+ const ivBuffer = bufferLikeToArrayBuffer(iv);
447
+ if (!ivBuffer || ivBuffer.byteLength !== 12) {
448
+ throw lazyDOMException(
449
+ 'ChaCha20-Poly1305 IV must be exactly 12 bytes',
450
+ 'OperationError',
451
+ );
452
+ }
453
+
454
+ // Validate tag length (only 128-bit supported)
455
+ if (tagLength !== 128) {
456
+ throw lazyDOMException(
457
+ 'ChaCha20-Poly1305 only supports 128-bit auth tags',
458
+ 'NotSupportedError',
459
+ );
460
+ }
461
+
462
+ const tagByteLength = 16; // 128 bits = 16 bytes
463
+
464
+ // Create cipher using existing ChaCha20-Poly1305 implementation
465
+ const factory =
466
+ NitroModules.createHybridObject<CipherFactory>('CipherFactory');
467
+ const cipher = factory.createCipher({
468
+ isCipher: mode === CipherOrWrapMode.kWebCryptoCipherEncrypt,
469
+ cipherType: 'chacha20-poly1305',
470
+ cipherKey: bufferLikeToArrayBuffer(key.keyObject.export()),
471
+ iv: ivBuffer,
472
+ authTagLen: tagByteLength,
473
+ });
474
+
475
+ let processData: ArrayBuffer;
476
+ let authTag: ArrayBuffer | undefined;
477
+
478
+ if (mode === CipherOrWrapMode.kWebCryptoCipherDecrypt) {
479
+ // For decryption, extract auth tag from end of data
480
+ const dataView = new Uint8Array(data);
481
+
482
+ if (dataView.byteLength < tagByteLength) {
483
+ throw lazyDOMException(
484
+ 'The provided data is too small.',
485
+ 'OperationError',
486
+ );
487
+ }
488
+
489
+ // Split data and tag
490
+ const ciphertextLength = dataView.byteLength - tagByteLength;
491
+ processData = dataView.slice(0, ciphertextLength).buffer;
492
+ authTag = dataView.slice(ciphertextLength).buffer;
493
+
494
+ // Set auth tag for verification
495
+ cipher.setAuthTag(authTag);
496
+ } else {
497
+ processData = data;
498
+ }
499
+
500
+ // Set additional authenticated data if provided
501
+ if (additionalData) {
502
+ cipher.setAAD(bufferLikeToArrayBuffer(additionalData));
503
+ }
504
+
505
+ // Process data
506
+ const updated = cipher.update(processData);
507
+ const final = cipher.final();
508
+
509
+ if (mode === CipherOrWrapMode.kWebCryptoCipherEncrypt) {
510
+ // For encryption, append auth tag to result
511
+ const tag = cipher.getAuthTag();
512
+ const result = new Uint8Array(
513
+ updated.byteLength + final.byteLength + tag.byteLength,
514
+ );
515
+ result.set(new Uint8Array(updated), 0);
516
+ result.set(new Uint8Array(final), updated.byteLength);
517
+ result.set(new Uint8Array(tag), updated.byteLength + final.byteLength);
518
+ return result.buffer;
519
+ } else {
520
+ // For decryption, just concatenate plaintext
521
+ const result = new Uint8Array(updated.byteLength + final.byteLength);
522
+ result.set(new Uint8Array(updated), 0);
523
+ result.set(new Uint8Array(final), updated.byteLength);
524
+ return result.buffer;
525
+ }
526
+ }
527
+
371
528
  async function aesGenerateKey(
372
529
  algorithm: AesKeyGenParams,
373
530
  extractable: boolean,
@@ -469,10 +626,11 @@ async function hmacGenerateKey(
469
626
  // Create secret key
470
627
  const keyObject = createSecretKey(keyBytes);
471
628
 
472
- // Construct algorithm object
629
+ // Construct algorithm object with hash normalized to { name: string } format per WebCrypto spec
630
+ const webCryptoHashName = normalizeHashName(hash, HashContext.WebCrypto);
473
631
  const keyAlgorithm: SubtleAlgorithm = {
474
632
  name: 'HMAC',
475
- hash: hashName,
633
+ hash: { name: webCryptoHashName },
476
634
  length,
477
635
  };
478
636
 
@@ -570,10 +728,15 @@ function rsaImportKey(
570
728
  publicExponentBytes = new Uint8Array(bytes.length > 0 ? bytes : [0]);
571
729
  }
572
730
 
731
+ // Normalize hash to { name: string } format per WebCrypto spec
732
+ const hashName = normalizeHashName(algorithm.hash, HashContext.WebCrypto);
733
+ const normalizedHash = { name: hashName };
734
+
573
735
  const algorithmWithDetails = {
574
736
  ...algorithm,
575
737
  modulusLength: keyDetails?.modulusLength,
576
738
  publicExponent: publicExponentBytes,
739
+ hash: normalizedHash,
577
740
  };
578
741
 
579
742
  return new CryptoKey(keyObject, algorithmWithDetails, keyUsages, extractable);
@@ -636,12 +799,15 @@ async function hmacImportKey(
636
799
  throw new Error(`Unable to import HMAC key with format ${format}`);
637
800
  }
638
801
 
639
- return new CryptoKey(
640
- keyObject,
641
- { ...algorithm, name: 'HMAC' },
642
- keyUsages,
643
- extractable,
644
- );
802
+ // Normalize hash to { name: string } format per WebCrypto spec
803
+ const hashName = normalizeHashName(algorithm.hash, HashContext.WebCrypto);
804
+ const normalizedAlgorithm: SubtleAlgorithm = {
805
+ ...algorithm,
806
+ name: 'HMAC',
807
+ hash: { name: hashName },
808
+ };
809
+
810
+ return new CryptoKey(keyObject, normalizedAlgorithm, keyUsages, extractable);
645
811
  }
646
812
 
647
813
  async function aesImportKey(
@@ -727,7 +893,12 @@ function edImportKey(
727
893
  const { name } = algorithm;
728
894
 
729
895
  // Validate usages
730
- if (hasAnyNotIn(keyUsages, ['sign', 'verify'])) {
896
+ const isX = name === 'X25519' || name === 'X448';
897
+ const allowedUsages: KeyUsage[] = isX
898
+ ? ['deriveKey', 'deriveBits']
899
+ : ['sign', 'verify'];
900
+
901
+ if (hasAnyNotIn(keyUsages, allowedUsages)) {
731
902
  throw lazyDOMException(
732
903
  `Unsupported key usage for ${name} key`,
733
904
  'SyntaxError',
@@ -773,6 +944,53 @@ function edImportKey(
773
944
  return new CryptoKey(keyObject, { name }, keyUsages, extractable);
774
945
  }
775
946
 
947
+ function mldsaImportKey(
948
+ format: ImportFormat,
949
+ data: BufferLike,
950
+ algorithm: SubtleAlgorithm,
951
+ extractable: boolean,
952
+ keyUsages: KeyUsage[],
953
+ ): CryptoKey {
954
+ const { name } = algorithm;
955
+
956
+ // Validate usages
957
+ if (hasAnyNotIn(keyUsages, ['sign', 'verify'])) {
958
+ throw lazyDOMException(
959
+ `Unsupported key usage for ${name} key`,
960
+ 'SyntaxError',
961
+ );
962
+ }
963
+
964
+ let keyObject: KeyObject;
965
+
966
+ if (format === 'spki') {
967
+ // Import public key
968
+ const keyData = bufferLikeToArrayBuffer(data);
969
+ keyObject = KeyObject.createKeyObject(
970
+ 'public',
971
+ keyData,
972
+ KFormatType.DER,
973
+ KeyEncoding.SPKI,
974
+ );
975
+ } else if (format === 'pkcs8') {
976
+ // Import private key
977
+ const keyData = bufferLikeToArrayBuffer(data);
978
+ keyObject = KeyObject.createKeyObject(
979
+ 'private',
980
+ keyData,
981
+ KFormatType.DER,
982
+ KeyEncoding.PKCS8,
983
+ );
984
+ } else {
985
+ throw lazyDOMException(
986
+ `Unsupported format for ${name} import: ${format}`,
987
+ 'NotSupportedError',
988
+ );
989
+ }
990
+
991
+ return new CryptoKey(keyObject, { name }, keyUsages, extractable);
992
+ }
993
+
776
994
  const exportKeySpki = async (
777
995
  key: CryptoKey,
778
996
  ): Promise<ArrayBuffer | unknown> => {
@@ -796,8 +1014,24 @@ const exportKeySpki = async (
796
1014
  case 'Ed25519':
797
1015
  // Fall through
798
1016
  case 'Ed448':
1017
+ // Fall through
1018
+ case 'X25519':
1019
+ // Fall through
1020
+ case 'X448':
799
1021
  if (key.type === 'public') {
800
- // Export Ed key in SPKI DER format
1022
+ // Export Ed/X key in SPKI DER format
1023
+ return bufferLikeToArrayBuffer(
1024
+ key.keyObject.handle.exportKey(KFormatType.DER, KeyEncoding.SPKI),
1025
+ );
1026
+ }
1027
+ break;
1028
+ case 'ML-DSA-44':
1029
+ // Fall through
1030
+ case 'ML-DSA-65':
1031
+ // Fall through
1032
+ case 'ML-DSA-87':
1033
+ if (key.type === 'public') {
1034
+ // Export ML-DSA key in SPKI DER format
801
1035
  return bufferLikeToArrayBuffer(
802
1036
  key.keyObject.handle.exportKey(KFormatType.DER, KeyEncoding.SPKI),
803
1037
  );
@@ -833,8 +1067,24 @@ const exportKeyPkcs8 = async (
833
1067
  case 'Ed25519':
834
1068
  // Fall through
835
1069
  case 'Ed448':
1070
+ // Fall through
1071
+ case 'X25519':
1072
+ // Fall through
1073
+ case 'X448':
836
1074
  if (key.type === 'private') {
837
- // Export Ed key in PKCS8 DER format
1075
+ // Export Ed/X key in PKCS8 DER format
1076
+ return bufferLikeToArrayBuffer(
1077
+ key.keyObject.handle.exportKey(KFormatType.DER, KeyEncoding.PKCS8),
1078
+ );
1079
+ }
1080
+ break;
1081
+ case 'ML-DSA-44':
1082
+ // Fall through
1083
+ case 'ML-DSA-65':
1084
+ // Fall through
1085
+ case 'ML-DSA-87':
1086
+ if (key.type === 'private') {
1087
+ // Export ML-DSA key in PKCS8 DER format
838
1088
  return bufferLikeToArrayBuffer(
839
1089
  key.keyObject.handle.exportKey(KFormatType.DER, KeyEncoding.PKCS8),
840
1090
  );
@@ -856,6 +1106,19 @@ const exportKeyRaw = (key: CryptoKey): ArrayBuffer | unknown => {
856
1106
  return ecExportKey(key, KWebCryptoKeyFormat.kWebCryptoKeyFormatRaw);
857
1107
  }
858
1108
  break;
1109
+ case 'Ed25519':
1110
+ // Fall through
1111
+ case 'Ed448':
1112
+ // Fall through
1113
+ case 'X25519':
1114
+ // Fall through
1115
+ case 'X448':
1116
+ if (key.type === 'public') {
1117
+ // Export raw public key
1118
+ const exported = key.keyObject.handle.exportKey();
1119
+ return bufferLikeToArrayBuffer(exported);
1120
+ }
1121
+ break;
859
1122
  case 'AES-CTR':
860
1123
  // Fall through
861
1124
  case 'AES-CBC':
@@ -864,6 +1127,8 @@ const exportKeyRaw = (key: CryptoKey): ArrayBuffer | unknown => {
864
1127
  // Fall through
865
1128
  case 'AES-KW':
866
1129
  // Fall through
1130
+ case 'ChaCha20-Poly1305':
1131
+ // Fall through
867
1132
  case 'HMAC': {
868
1133
  const exported = key.keyObject.export();
869
1134
  // Convert Buffer to ArrayBuffer
@@ -913,6 +1178,8 @@ const exportKeyJWK = (key: CryptoKey): ArrayBuffer | unknown => {
913
1178
  case 'AES-GCM':
914
1179
  // Fall through
915
1180
  case 'AES-KW':
1181
+ // Fall through
1182
+ case 'ChaCha20-Poly1305':
916
1183
  if (key.algorithm.length === undefined) {
917
1184
  throw lazyDOMException(
918
1185
  `Algorithm ${key.algorithm.name} missing required length property`,
@@ -1112,6 +1379,36 @@ function edSignVerify(
1112
1379
  }
1113
1380
  }
1114
1381
 
1382
+ function mldsaSignVerify(
1383
+ key: CryptoKey,
1384
+ data: BufferLike,
1385
+ signature?: BufferLike,
1386
+ ): ArrayBuffer | boolean {
1387
+ const isSign = signature === undefined;
1388
+ const expectedKeyType = isSign ? 'private' : 'public';
1389
+
1390
+ if (key.type !== expectedKeyType) {
1391
+ throw lazyDOMException(
1392
+ `Key must be a ${expectedKeyType} key`,
1393
+ 'InvalidAccessError',
1394
+ );
1395
+ }
1396
+
1397
+ const dataBuffer = bufferLikeToArrayBuffer(data);
1398
+
1399
+ if (isSign) {
1400
+ const signer = createSign('');
1401
+ signer.update(dataBuffer);
1402
+ const sig = signer.sign({ key: key });
1403
+ return sig.buffer.slice(sig.byteOffset, sig.byteOffset + sig.byteLength);
1404
+ } else {
1405
+ const signatureBuffer = bufferLikeToArrayBuffer(signature!);
1406
+ const verifier = createVerify('');
1407
+ verifier.update(dataBuffer);
1408
+ return verifier.verify({ key: key }, signatureBuffer);
1409
+ }
1410
+ }
1411
+
1115
1412
  const signVerify = (
1116
1413
  algorithm: SubtleAlgorithm,
1117
1414
  key: CryptoKey,
@@ -1140,6 +1437,10 @@ const signVerify = (
1140
1437
  case 'Ed25519':
1141
1438
  case 'Ed448':
1142
1439
  return edSignVerify(key, data, signature);
1440
+ case 'ML-DSA-44':
1441
+ case 'ML-DSA-65':
1442
+ case 'ML-DSA-87':
1443
+ return mldsaSignVerify(key, data, signature);
1143
1444
  }
1144
1445
  throw lazyDOMException(
1145
1446
  `Unrecognized algorithm name '${algorithm.name}' for '${usage}'`,
@@ -1175,6 +1476,15 @@ const cipherOrWrap = async (
1175
1476
  // Fall through
1176
1477
  case 'AES-GCM':
1177
1478
  return aesCipher(mode, key, data, algorithm);
1479
+ case 'AES-KW':
1480
+ return aesKwCipher(mode, key, data);
1481
+ case 'ChaCha20-Poly1305':
1482
+ return chaCha20Poly1305Cipher(
1483
+ mode,
1484
+ key,
1485
+ data,
1486
+ algorithm as ChaCha20Poly1305Params,
1487
+ );
1178
1488
  }
1179
1489
  };
1180
1490
 
@@ -1210,20 +1520,79 @@ export class Subtle {
1210
1520
  baseKey: CryptoKey,
1211
1521
  length: number,
1212
1522
  ): Promise<ArrayBuffer> {
1213
- if (!baseKey.keyUsages.includes('deriveBits')) {
1214
- throw new Error('baseKey does not have deriveBits usage');
1523
+ // Allow either deriveBits OR deriveKey usage (WebCrypto spec allows both)
1524
+ if (
1525
+ !baseKey.keyUsages.includes('deriveBits') &&
1526
+ !baseKey.keyUsages.includes('deriveKey')
1527
+ ) {
1528
+ throw new Error('baseKey does not have deriveBits or deriveKey usage');
1215
1529
  }
1216
1530
  if (baseKey.algorithm.name !== algorithm.name)
1217
1531
  throw new Error('Key algorithm mismatch');
1218
1532
  switch (algorithm.name) {
1219
1533
  case 'PBKDF2':
1220
1534
  return pbkdf2DeriveBits(algorithm, baseKey, length);
1535
+ case 'X25519':
1536
+ // Fall through
1537
+ case 'X448':
1538
+ return xDeriveBits(algorithm, baseKey, length);
1221
1539
  }
1222
1540
  throw new Error(
1223
1541
  `'subtle.deriveBits()' for ${algorithm.name} is not implemented.`,
1224
1542
  );
1225
1543
  }
1226
1544
 
1545
+ async deriveKey(
1546
+ algorithm: SubtleAlgorithm,
1547
+ baseKey: CryptoKey,
1548
+ derivedKeyAlgorithm: SubtleAlgorithm,
1549
+ extractable: boolean,
1550
+ keyUsages: KeyUsage[],
1551
+ ): Promise<CryptoKey> {
1552
+ // Validate baseKey usage
1553
+ if (
1554
+ !baseKey.usages.includes('deriveKey') &&
1555
+ !baseKey.usages.includes('deriveBits')
1556
+ ) {
1557
+ throw lazyDOMException(
1558
+ 'baseKey does not have deriveKey or deriveBits usage',
1559
+ 'InvalidAccessError',
1560
+ );
1561
+ }
1562
+
1563
+ // Calculate required key length
1564
+ const length = getKeyLength(derivedKeyAlgorithm);
1565
+
1566
+ // Step 1: Derive bits
1567
+ let derivedBits: ArrayBuffer;
1568
+ if (baseKey.algorithm.name !== algorithm.name)
1569
+ throw new Error('Key algorithm mismatch');
1570
+
1571
+ switch (algorithm.name) {
1572
+ case 'PBKDF2':
1573
+ derivedBits = await pbkdf2DeriveBits(algorithm, baseKey, length);
1574
+ break;
1575
+ case 'X25519':
1576
+ // Fall through
1577
+ case 'X448':
1578
+ derivedBits = await xDeriveBits(algorithm, baseKey, length);
1579
+ break;
1580
+ default:
1581
+ throw new Error(
1582
+ `'subtle.deriveKey()' for ${algorithm.name} is not implemented.`,
1583
+ );
1584
+ }
1585
+
1586
+ // Step 2: Import as key
1587
+ return this.importKey(
1588
+ 'raw',
1589
+ derivedBits,
1590
+ derivedKeyAlgorithm,
1591
+ extractable,
1592
+ keyUsages,
1593
+ );
1594
+ }
1595
+
1227
1596
  async encrypt(
1228
1597
  algorithm: EncryptDecryptParams,
1229
1598
  key: CryptoKey,
@@ -1257,6 +1626,115 @@ export class Subtle {
1257
1626
  }
1258
1627
  }
1259
1628
 
1629
+ async wrapKey(
1630
+ format: ImportFormat,
1631
+ key: CryptoKey,
1632
+ wrappingKey: CryptoKey,
1633
+ wrapAlgorithm: EncryptDecryptParams,
1634
+ ): Promise<ArrayBuffer> {
1635
+ // Validate wrappingKey usage
1636
+ if (!wrappingKey.usages.includes('wrapKey')) {
1637
+ throw lazyDOMException(
1638
+ 'wrappingKey does not have wrapKey usage',
1639
+ 'InvalidAccessError',
1640
+ );
1641
+ }
1642
+
1643
+ // Step 1: Export the key
1644
+ const exported = await this.exportKey(format, key);
1645
+
1646
+ // Step 2: Convert to ArrayBuffer if JWK
1647
+ let keyData: ArrayBuffer;
1648
+ if (format === 'jwk') {
1649
+ const jwkString = JSON.stringify(exported);
1650
+ const buffer = SBuffer.from(jwkString, 'utf8');
1651
+
1652
+ // For AES-KW, pad to multiple of 8 bytes (accounting for null terminator)
1653
+ if (wrapAlgorithm.name === 'AES-KW') {
1654
+ const length = buffer.length;
1655
+ // Add 1 for null terminator, then pad to multiple of 8
1656
+ const paddedLength = Math.ceil((length + 1) / 8) * 8;
1657
+ const paddedBuffer = SBuffer.alloc(paddedLength);
1658
+ buffer.copy(paddedBuffer);
1659
+ // Null terminator for JSON string (remaining bytes are already zeros from alloc)
1660
+ paddedBuffer.writeUInt8(0, length);
1661
+ keyData = bufferLikeToArrayBuffer(paddedBuffer);
1662
+ } else {
1663
+ keyData = bufferLikeToArrayBuffer(buffer);
1664
+ }
1665
+ } else {
1666
+ keyData = exported as ArrayBuffer;
1667
+ }
1668
+
1669
+ // Step 3: Encrypt the exported key
1670
+ return cipherOrWrap(
1671
+ CipherOrWrapMode.kWebCryptoCipherEncrypt,
1672
+ wrapAlgorithm,
1673
+ wrappingKey,
1674
+ keyData,
1675
+ 'wrapKey',
1676
+ );
1677
+ }
1678
+
1679
+ async unwrapKey(
1680
+ format: ImportFormat,
1681
+ wrappedKey: BufferLike,
1682
+ unwrappingKey: CryptoKey,
1683
+ unwrapAlgorithm: EncryptDecryptParams,
1684
+ unwrappedKeyAlgorithm: SubtleAlgorithm | AnyAlgorithm,
1685
+ extractable: boolean,
1686
+ keyUsages: KeyUsage[],
1687
+ ): Promise<CryptoKey> {
1688
+ // Validate unwrappingKey usage
1689
+ if (!unwrappingKey.usages.includes('unwrapKey')) {
1690
+ throw lazyDOMException(
1691
+ 'unwrappingKey does not have unwrapKey usage',
1692
+ 'InvalidAccessError',
1693
+ );
1694
+ }
1695
+
1696
+ // Step 1: Decrypt the wrapped key
1697
+ const decrypted = await cipherOrWrap(
1698
+ CipherOrWrapMode.kWebCryptoCipherDecrypt,
1699
+ unwrapAlgorithm,
1700
+ unwrappingKey,
1701
+ bufferLikeToArrayBuffer(wrappedKey),
1702
+ 'unwrapKey',
1703
+ );
1704
+
1705
+ // Step 2: Convert to appropriate format
1706
+ let keyData: BufferLike | JWK;
1707
+ if (format === 'jwk') {
1708
+ const buffer = SBuffer.from(decrypted);
1709
+ // For AES-KW, the data may be padded - find the null terminator
1710
+ let jwkString: string;
1711
+ if (unwrapAlgorithm.name === 'AES-KW') {
1712
+ // Find the null terminator (if present) to get the original string
1713
+ const nullIndex = buffer.indexOf(0);
1714
+ if (nullIndex !== -1) {
1715
+ jwkString = buffer.toString('utf8', 0, nullIndex);
1716
+ } else {
1717
+ // No null terminator, try to parse the whole buffer
1718
+ jwkString = buffer.toString('utf8').trim();
1719
+ }
1720
+ } else {
1721
+ jwkString = buffer.toString('utf8');
1722
+ }
1723
+ keyData = JSON.parse(jwkString) as JWK;
1724
+ } else {
1725
+ keyData = decrypted;
1726
+ }
1727
+
1728
+ // Step 3: Import the key
1729
+ return this.importKey(
1730
+ format,
1731
+ keyData,
1732
+ unwrappedKeyAlgorithm,
1733
+ extractable,
1734
+ keyUsages,
1735
+ );
1736
+ }
1737
+
1260
1738
  async generateKey(
1261
1739
  algorithm: SubtleAlgorithm,
1262
1740
  extractable: boolean,
@@ -1296,6 +1774,26 @@ export class Subtle {
1296
1774
  keyUsages,
1297
1775
  );
1298
1776
  break;
1777
+ case 'ChaCha20-Poly1305': {
1778
+ const length = (algorithm as AesKeyGenParams).length ?? 256;
1779
+
1780
+ if (length !== 256) {
1781
+ throw lazyDOMException(
1782
+ 'ChaCha20-Poly1305 only supports 256-bit keys',
1783
+ 'NotSupportedError',
1784
+ );
1785
+ }
1786
+
1787
+ result = await aesGenerateKey(
1788
+ {
1789
+ name: 'ChaCha20-Poly1305',
1790
+ length: 256,
1791
+ } as unknown as AesKeyGenParams,
1792
+ extractable,
1793
+ keyUsages,
1794
+ );
1795
+ break;
1796
+ }
1299
1797
  case 'HMAC':
1300
1798
  result = await hmacGenerateKey(algorithm, extractable, keyUsages);
1301
1799
  break;
@@ -1309,6 +1807,28 @@ export class Subtle {
1309
1807
  );
1310
1808
  checkCryptoKeyPairUsages(result as CryptoKeyPair);
1311
1809
  break;
1810
+ case 'ML-DSA-44':
1811
+ // Fall through
1812
+ case 'ML-DSA-65':
1813
+ // Fall through
1814
+ case 'ML-DSA-87':
1815
+ result = await mldsa_generateKeyPairWebCrypto(
1816
+ algorithm.name as MlDsaVariant,
1817
+ extractable,
1818
+ keyUsages,
1819
+ );
1820
+ checkCryptoKeyPairUsages(result as CryptoKeyPair);
1821
+ break;
1822
+ case 'X25519':
1823
+ // Fall through
1824
+ case 'X448':
1825
+ result = await x_generateKeyPairWebCrypto(
1826
+ algorithm.name.toLowerCase() as 'x25519' | 'x448',
1827
+ extractable,
1828
+ keyUsages,
1829
+ );
1830
+ checkCryptoKeyPairUsages(result as CryptoKeyPair);
1831
+ break;
1312
1832
  default:
1313
1833
  throw new Error(
1314
1834
  `'subtle.generateKey()' is not implemented for ${algorithm.name}.
@@ -1369,6 +1889,8 @@ export class Subtle {
1369
1889
  case 'AES-GCM':
1370
1890
  // Fall through
1371
1891
  case 'AES-KW':
1892
+ // Fall through
1893
+ case 'ChaCha20-Poly1305':
1372
1894
  result = await aesImportKey(
1373
1895
  normalizedAlgorithm,
1374
1896
  format,
@@ -1386,6 +1908,10 @@ export class Subtle {
1386
1908
  keyUsages,
1387
1909
  );
1388
1910
  break;
1911
+ case 'X25519':
1912
+ // Fall through
1913
+ case 'X448':
1914
+ // Fall through
1389
1915
  case 'Ed25519':
1390
1916
  // Fall through
1391
1917
  case 'Ed448':
@@ -1397,6 +1923,19 @@ export class Subtle {
1397
1923
  keyUsages,
1398
1924
  );
1399
1925
  break;
1926
+ case 'ML-DSA-44':
1927
+ // Fall through
1928
+ case 'ML-DSA-65':
1929
+ // Fall through
1930
+ case 'ML-DSA-87':
1931
+ result = mldsaImportKey(
1932
+ format,
1933
+ data as BufferLike,
1934
+ normalizedAlgorithm,
1935
+ extractable,
1936
+ keyUsages,
1937
+ );
1938
+ break;
1400
1939
  default:
1401
1940
  throw new Error(
1402
1941
  `"subtle.importKey()" is not implemented for ${normalizedAlgorithm.name}`,
@@ -1420,7 +1959,43 @@ export class Subtle {
1420
1959
  key: CryptoKey,
1421
1960
  data: BufferLike,
1422
1961
  ): Promise<ArrayBuffer> {
1423
- return signVerify(algorithm, key, data) as ArrayBuffer;
1962
+ const normalizedAlgorithm = normalizeAlgorithm(algorithm, 'sign');
1963
+
1964
+ if (normalizedAlgorithm.name === 'HMAC') {
1965
+ // Validate key usage
1966
+ if (!key.usages.includes('sign')) {
1967
+ throw lazyDOMException(
1968
+ 'Key does not have sign usage',
1969
+ 'InvalidAccessError',
1970
+ );
1971
+ }
1972
+
1973
+ // Get hash algorithm from key or algorithm params
1974
+ // Hash can be either a string or an object with name property
1975
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1976
+ const alg = normalizedAlgorithm as any;
1977
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1978
+ const keyAlg = key.algorithm as any;
1979
+ let hashAlgorithm = 'SHA-256';
1980
+
1981
+ if (typeof alg.hash === 'string') {
1982
+ hashAlgorithm = alg.hash;
1983
+ } else if (alg.hash?.name) {
1984
+ hashAlgorithm = alg.hash.name;
1985
+ } else if (typeof keyAlg.hash === 'string') {
1986
+ hashAlgorithm = keyAlg.hash;
1987
+ } else if (keyAlg.hash?.name) {
1988
+ hashAlgorithm = keyAlg.hash.name;
1989
+ }
1990
+
1991
+ // Create HMAC and sign
1992
+ const keyData = key.keyObject.export();
1993
+ const hmac = createHmac(hashAlgorithm, keyData);
1994
+ hmac.update(bufferLikeToArrayBuffer(data));
1995
+ return bufferLikeToArrayBuffer(hmac.digest());
1996
+ }
1997
+
1998
+ return signVerify(normalizedAlgorithm, key, data) as ArrayBuffer;
1424
1999
  }
1425
2000
 
1426
2001
  async verify(
@@ -1428,9 +2003,84 @@ export class Subtle {
1428
2003
  key: CryptoKey,
1429
2004
  signature: BufferLike,
1430
2005
  data: BufferLike,
1431
- ): Promise<ArrayBuffer> {
1432
- return signVerify(algorithm, key, data, signature) as ArrayBuffer;
2006
+ ): Promise<boolean> {
2007
+ const normalizedAlgorithm = normalizeAlgorithm(algorithm, 'verify');
2008
+
2009
+ if (normalizedAlgorithm.name === 'HMAC') {
2010
+ // Validate key usage
2011
+ if (!key.usages.includes('verify')) {
2012
+ throw lazyDOMException(
2013
+ 'Key does not have verify usage',
2014
+ 'InvalidAccessError',
2015
+ );
2016
+ }
2017
+
2018
+ // Get hash algorithm
2019
+ // Hash can be either a string or an object with name property
2020
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2021
+ const alg = normalizedAlgorithm as any;
2022
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2023
+ const keyAlg = key.algorithm as any;
2024
+ let hashAlgorithm = 'SHA-256';
2025
+
2026
+ if (typeof alg.hash === 'string') {
2027
+ hashAlgorithm = alg.hash;
2028
+ } else if (alg.hash?.name) {
2029
+ hashAlgorithm = alg.hash.name;
2030
+ } else if (typeof keyAlg.hash === 'string') {
2031
+ hashAlgorithm = keyAlg.hash;
2032
+ } else if (keyAlg.hash?.name) {
2033
+ hashAlgorithm = keyAlg.hash.name;
2034
+ }
2035
+
2036
+ // Create HMAC and compute expected signature
2037
+ const keyData = key.keyObject.export();
2038
+ const hmac = createHmac(hashAlgorithm, keyData);
2039
+ const dataBuffer = bufferLikeToArrayBuffer(data);
2040
+ hmac.update(dataBuffer);
2041
+ const expectedDigest = hmac.digest();
2042
+ const expected = new Uint8Array(bufferLikeToArrayBuffer(expectedDigest));
2043
+
2044
+ // Constant-time comparison
2045
+ const signatureArray = new Uint8Array(bufferLikeToArrayBuffer(signature));
2046
+ if (expected.length !== signatureArray.length) {
2047
+ return false;
2048
+ }
2049
+
2050
+ // Manual constant-time comparison
2051
+ let result = 0;
2052
+ for (let i = 0; i < expected.length; i++) {
2053
+ result |= expected[i]! ^ signatureArray[i]!;
2054
+ }
2055
+ return result === 0;
2056
+ }
2057
+
2058
+ return signVerify(normalizedAlgorithm, key, data, signature) as boolean;
1433
2059
  }
1434
2060
  }
1435
2061
 
1436
2062
  export const subtle = new Subtle();
2063
+
2064
+ function getKeyLength(algorithm: SubtleAlgorithm): number {
2065
+ const name = algorithm.name;
2066
+
2067
+ switch (name) {
2068
+ case 'AES-CTR':
2069
+ case 'AES-CBC':
2070
+ case 'AES-GCM':
2071
+ case 'AES-KW':
2072
+ case 'ChaCha20-Poly1305':
2073
+ return (algorithm as AesKeyGenParams).length || 256;
2074
+
2075
+ case 'HMAC': {
2076
+ const hmacAlg = algorithm as { length?: number };
2077
+ return hmacAlg.length || 256;
2078
+ }
2079
+
2080
+ default:
2081
+ throw lazyDOMException(
2082
+ `Cannot determine key length for ${name}`,
2083
+ 'NotSupportedError',
2084
+ );
2085
+ }
2086
+ }