react-native-quick-crypto 1.0.1 → 1.0.3

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 (72) hide show
  1. package/QuickCrypto.podspec +6 -47
  2. package/README.md +1 -1
  3. package/android/CMakeLists.txt +4 -0
  4. package/cpp/cipher/HybridCipher.cpp +17 -1
  5. package/cpp/ed25519/HybridEdKeyPair.cpp +8 -2
  6. package/cpp/hkdf/HybridHkdf.cpp +96 -0
  7. package/cpp/hkdf/HybridHkdf.hpp +28 -0
  8. package/cpp/scrypt/HybridScrypt.cpp +62 -0
  9. package/cpp/scrypt/HybridScrypt.hpp +28 -0
  10. package/lib/commonjs/ed.js +68 -0
  11. package/lib/commonjs/ed.js.map +1 -1
  12. package/lib/commonjs/hkdf.js +81 -0
  13. package/lib/commonjs/hkdf.js.map +1 -0
  14. package/lib/commonjs/index.js +33 -1
  15. package/lib/commonjs/index.js.map +1 -1
  16. package/lib/commonjs/scrypt.js +98 -0
  17. package/lib/commonjs/scrypt.js.map +1 -0
  18. package/lib/commonjs/specs/hkdf.nitro.js +6 -0
  19. package/lib/commonjs/specs/hkdf.nitro.js.map +1 -0
  20. package/lib/commonjs/specs/scrypt.nitro.js +6 -0
  21. package/lib/commonjs/specs/scrypt.nitro.js.map +1 -0
  22. package/lib/commonjs/subtle.js +400 -7
  23. package/lib/commonjs/subtle.js.map +1 -1
  24. package/lib/commonjs/utils/types.js.map +1 -1
  25. package/lib/module/ed.js +66 -0
  26. package/lib/module/ed.js.map +1 -1
  27. package/lib/module/hkdf.js +75 -0
  28. package/lib/module/hkdf.js.map +1 -0
  29. package/lib/module/index.js +13 -1
  30. package/lib/module/index.js.map +1 -1
  31. package/lib/module/scrypt.js +93 -0
  32. package/lib/module/scrypt.js.map +1 -0
  33. package/lib/module/specs/hkdf.nitro.js +4 -0
  34. package/lib/module/specs/hkdf.nitro.js.map +1 -0
  35. package/lib/module/specs/scrypt.nitro.js +4 -0
  36. package/lib/module/specs/scrypt.nitro.js.map +1 -0
  37. package/lib/module/subtle.js +401 -8
  38. package/lib/module/subtle.js.map +1 -1
  39. package/lib/module/utils/types.js.map +1 -1
  40. package/lib/tsconfig.tsbuildinfo +1 -1
  41. package/lib/typescript/ed.d.ts +4 -1
  42. package/lib/typescript/ed.d.ts.map +1 -1
  43. package/lib/typescript/hkdf.d.ts +26 -0
  44. package/lib/typescript/hkdf.d.ts.map +1 -0
  45. package/lib/typescript/index.d.ts +11 -0
  46. package/lib/typescript/index.d.ts.map +1 -1
  47. package/lib/typescript/scrypt.d.ts +18 -0
  48. package/lib/typescript/scrypt.d.ts.map +1 -0
  49. package/lib/typescript/specs/hkdf.nitro.d.ts +9 -0
  50. package/lib/typescript/specs/hkdf.nitro.d.ts.map +1 -0
  51. package/lib/typescript/specs/scrypt.nitro.d.ts +9 -0
  52. package/lib/typescript/specs/scrypt.nitro.d.ts.map +1 -0
  53. package/lib/typescript/subtle.d.ts +4 -1
  54. package/lib/typescript/subtle.d.ts.map +1 -1
  55. package/lib/typescript/utils/types.d.ts +9 -3
  56. package/lib/typescript/utils/types.d.ts.map +1 -1
  57. package/nitrogen/generated/android/QuickCrypto+autolinking.cmake +2 -0
  58. package/nitrogen/generated/android/QuickCryptoOnLoad.cpp +20 -0
  59. package/nitrogen/generated/ios/QuickCryptoAutolinking.mm +20 -0
  60. package/nitrogen/generated/shared/c++/HybridHkdfSpec.cpp +22 -0
  61. package/nitrogen/generated/shared/c++/HybridHkdfSpec.hpp +66 -0
  62. package/nitrogen/generated/shared/c++/HybridScryptSpec.cpp +22 -0
  63. package/nitrogen/generated/shared/c++/HybridScryptSpec.hpp +65 -0
  64. package/package.json +1 -1
  65. package/src/ed.ts +102 -0
  66. package/src/hkdf.ts +152 -0
  67. package/src/index.ts +13 -1
  68. package/src/scrypt.ts +134 -0
  69. package/src/specs/hkdf.nitro.ts +19 -0
  70. package/src/specs/scrypt.nitro.ts +23 -0
  71. package/src/subtle.ts +564 -9
  72. package/src/utils/types.ts +16 -3
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,8 +42,14 @@ 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';
45
51
  import { mldsa_generateKeyPairWebCrypto, type MlDsaVariant } from './mldsa';
52
+ import { hkdfDeriveBits, type HkdfAlgorithm } from './hkdf';
46
53
  // import { pbkdf2DeriveBits } from './pbkdf2';
47
54
  // import { aesCipher, aesGenerateKey, aesImportKey, getAlgorithmName } from './aes';
48
55
  // import { rsaCipher, rsaExportKey, rsaImportKey, rsaKeyGenerate } from './rsa';
@@ -369,6 +376,156 @@ async function aesGcmCipher(
369
376
  }
370
377
  }
371
378
 
379
+ async function aesKwCipher(
380
+ mode: CipherOrWrapMode,
381
+ key: CryptoKey,
382
+ data: ArrayBuffer,
383
+ ): Promise<ArrayBuffer> {
384
+ const isWrap = mode === CipherOrWrapMode.kWebCryptoCipherEncrypt;
385
+
386
+ // AES-KW requires input to be a multiple of 8 bytes (64 bits)
387
+ if (data.byteLength % 8 !== 0) {
388
+ throw lazyDOMException(
389
+ `AES-KW input length must be a multiple of 8 bytes, got ${data.byteLength}`,
390
+ 'OperationError',
391
+ );
392
+ }
393
+
394
+ // AES-KW requires at least 16 bytes of input (128 bits)
395
+ if (isWrap && data.byteLength < 16) {
396
+ throw lazyDOMException(
397
+ `AES-KW input must be at least 16 bytes, got ${data.byteLength}`,
398
+ 'OperationError',
399
+ );
400
+ }
401
+
402
+ // Get cipher type based on key length
403
+ const keyLength = (key.algorithm as { length: number }).length;
404
+ // Use aes*-wrap for both operations (matching Node.js)
405
+ const cipherType = `aes${keyLength}-wrap`;
406
+
407
+ // Export key material
408
+ const exportedKey = key.keyObject.export();
409
+ const cipherKey = bufferLikeToArrayBuffer(exportedKey);
410
+
411
+ // AES-KW uses a default IV as specified in RFC 3394
412
+ const defaultWrapIV = new Uint8Array([
413
+ 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6,
414
+ ]);
415
+
416
+ const factory =
417
+ NitroModules.createHybridObject<CipherFactory>('CipherFactory');
418
+
419
+ const cipher = factory.createCipher({
420
+ isCipher: isWrap,
421
+ cipherType,
422
+ cipherKey,
423
+ iv: defaultWrapIV.buffer, // RFC 3394 default IV for AES-KW
424
+ });
425
+
426
+ // Process data
427
+ const updated = cipher.update(data);
428
+ const final = cipher.final();
429
+
430
+ // Concatenate results
431
+ const result = new Uint8Array(updated.byteLength + final.byteLength);
432
+ result.set(new Uint8Array(updated), 0);
433
+ result.set(new Uint8Array(final), updated.byteLength);
434
+
435
+ return result.buffer;
436
+ }
437
+
438
+ async function chaCha20Poly1305Cipher(
439
+ mode: CipherOrWrapMode,
440
+ key: CryptoKey,
441
+ data: ArrayBuffer,
442
+ algorithm: ChaCha20Poly1305Params,
443
+ ): Promise<ArrayBuffer> {
444
+ const { iv, additionalData, tagLength = 128 } = algorithm;
445
+
446
+ // Validate IV (must be 12 bytes for ChaCha20-Poly1305)
447
+ const ivBuffer = bufferLikeToArrayBuffer(iv);
448
+ if (!ivBuffer || ivBuffer.byteLength !== 12) {
449
+ throw lazyDOMException(
450
+ 'ChaCha20-Poly1305 IV must be exactly 12 bytes',
451
+ 'OperationError',
452
+ );
453
+ }
454
+
455
+ // Validate tag length (only 128-bit supported)
456
+ if (tagLength !== 128) {
457
+ throw lazyDOMException(
458
+ 'ChaCha20-Poly1305 only supports 128-bit auth tags',
459
+ 'NotSupportedError',
460
+ );
461
+ }
462
+
463
+ const tagByteLength = 16; // 128 bits = 16 bytes
464
+
465
+ // Create cipher using existing ChaCha20-Poly1305 implementation
466
+ const factory =
467
+ NitroModules.createHybridObject<CipherFactory>('CipherFactory');
468
+ const cipher = factory.createCipher({
469
+ isCipher: mode === CipherOrWrapMode.kWebCryptoCipherEncrypt,
470
+ cipherType: 'chacha20-poly1305',
471
+ cipherKey: bufferLikeToArrayBuffer(key.keyObject.export()),
472
+ iv: ivBuffer,
473
+ authTagLen: tagByteLength,
474
+ });
475
+
476
+ let processData: ArrayBuffer;
477
+ let authTag: ArrayBuffer | undefined;
478
+
479
+ if (mode === CipherOrWrapMode.kWebCryptoCipherDecrypt) {
480
+ // For decryption, extract auth tag from end of data
481
+ const dataView = new Uint8Array(data);
482
+
483
+ if (dataView.byteLength < tagByteLength) {
484
+ throw lazyDOMException(
485
+ 'The provided data is too small.',
486
+ 'OperationError',
487
+ );
488
+ }
489
+
490
+ // Split data and tag
491
+ const ciphertextLength = dataView.byteLength - tagByteLength;
492
+ processData = dataView.slice(0, ciphertextLength).buffer;
493
+ authTag = dataView.slice(ciphertextLength).buffer;
494
+
495
+ // Set auth tag for verification
496
+ cipher.setAuthTag(authTag);
497
+ } else {
498
+ processData = data;
499
+ }
500
+
501
+ // Set additional authenticated data if provided
502
+ if (additionalData) {
503
+ cipher.setAAD(bufferLikeToArrayBuffer(additionalData));
504
+ }
505
+
506
+ // Process data
507
+ const updated = cipher.update(processData);
508
+ const final = cipher.final();
509
+
510
+ if (mode === CipherOrWrapMode.kWebCryptoCipherEncrypt) {
511
+ // For encryption, append auth tag to result
512
+ const tag = cipher.getAuthTag();
513
+ const result = new Uint8Array(
514
+ updated.byteLength + final.byteLength + tag.byteLength,
515
+ );
516
+ result.set(new Uint8Array(updated), 0);
517
+ result.set(new Uint8Array(final), updated.byteLength);
518
+ result.set(new Uint8Array(tag), updated.byteLength + final.byteLength);
519
+ return result.buffer;
520
+ } else {
521
+ // For decryption, just concatenate plaintext
522
+ const result = new Uint8Array(updated.byteLength + final.byteLength);
523
+ result.set(new Uint8Array(updated), 0);
524
+ result.set(new Uint8Array(final), updated.byteLength);
525
+ return result.buffer;
526
+ }
527
+ }
528
+
372
529
  async function aesGenerateKey(
373
530
  algorithm: AesKeyGenParams,
374
531
  extractable: boolean,
@@ -737,7 +894,12 @@ function edImportKey(
737
894
  const { name } = algorithm;
738
895
 
739
896
  // Validate usages
740
- if (hasAnyNotIn(keyUsages, ['sign', 'verify'])) {
897
+ const isX = name === 'X25519' || name === 'X448';
898
+ const allowedUsages: KeyUsage[] = isX
899
+ ? ['deriveKey', 'deriveBits']
900
+ : ['sign', 'verify'];
901
+
902
+ if (hasAnyNotIn(keyUsages, allowedUsages)) {
741
903
  throw lazyDOMException(
742
904
  `Unsupported key usage for ${name} key`,
743
905
  'SyntaxError',
@@ -853,8 +1015,12 @@ const exportKeySpki = async (
853
1015
  case 'Ed25519':
854
1016
  // Fall through
855
1017
  case 'Ed448':
1018
+ // Fall through
1019
+ case 'X25519':
1020
+ // Fall through
1021
+ case 'X448':
856
1022
  if (key.type === 'public') {
857
- // Export Ed key in SPKI DER format
1023
+ // Export Ed/X key in SPKI DER format
858
1024
  return bufferLikeToArrayBuffer(
859
1025
  key.keyObject.handle.exportKey(KFormatType.DER, KeyEncoding.SPKI),
860
1026
  );
@@ -902,8 +1068,12 @@ const exportKeyPkcs8 = async (
902
1068
  case 'Ed25519':
903
1069
  // Fall through
904
1070
  case 'Ed448':
1071
+ // Fall through
1072
+ case 'X25519':
1073
+ // Fall through
1074
+ case 'X448':
905
1075
  if (key.type === 'private') {
906
- // Export Ed key in PKCS8 DER format
1076
+ // Export Ed/X key in PKCS8 DER format
907
1077
  return bufferLikeToArrayBuffer(
908
1078
  key.keyObject.handle.exportKey(KFormatType.DER, KeyEncoding.PKCS8),
909
1079
  );
@@ -937,6 +1107,19 @@ const exportKeyRaw = (key: CryptoKey): ArrayBuffer | unknown => {
937
1107
  return ecExportKey(key, KWebCryptoKeyFormat.kWebCryptoKeyFormatRaw);
938
1108
  }
939
1109
  break;
1110
+ case 'Ed25519':
1111
+ // Fall through
1112
+ case 'Ed448':
1113
+ // Fall through
1114
+ case 'X25519':
1115
+ // Fall through
1116
+ case 'X448':
1117
+ if (key.type === 'public') {
1118
+ // Export raw public key
1119
+ const exported = key.keyObject.handle.exportKey();
1120
+ return bufferLikeToArrayBuffer(exported);
1121
+ }
1122
+ break;
940
1123
  case 'AES-CTR':
941
1124
  // Fall through
942
1125
  case 'AES-CBC':
@@ -945,6 +1128,8 @@ const exportKeyRaw = (key: CryptoKey): ArrayBuffer | unknown => {
945
1128
  // Fall through
946
1129
  case 'AES-KW':
947
1130
  // Fall through
1131
+ case 'ChaCha20-Poly1305':
1132
+ // Fall through
948
1133
  case 'HMAC': {
949
1134
  const exported = key.keyObject.export();
950
1135
  // Convert Buffer to ArrayBuffer
@@ -994,6 +1179,8 @@ const exportKeyJWK = (key: CryptoKey): ArrayBuffer | unknown => {
994
1179
  case 'AES-GCM':
995
1180
  // Fall through
996
1181
  case 'AES-KW':
1182
+ // Fall through
1183
+ case 'ChaCha20-Poly1305':
997
1184
  if (key.algorithm.length === undefined) {
998
1185
  throw lazyDOMException(
999
1186
  `Algorithm ${key.algorithm.name} missing required length property`,
@@ -1049,6 +1236,28 @@ const importGenericSecretKey = async (
1049
1236
  throw new Error(`Unable to import ${name} key with format ${format}`);
1050
1237
  };
1051
1238
 
1239
+ const hkdfImportKey = async (
1240
+ format: ImportFormat,
1241
+ keyData: BufferLike | BinaryLike,
1242
+ algorithm: SubtleAlgorithm,
1243
+ extractable: boolean,
1244
+ keyUsages: KeyUsage[],
1245
+ ): Promise<CryptoKey> => {
1246
+ const { name } = algorithm;
1247
+ if (hasAnyNotIn(keyUsages, ['deriveKey', 'deriveBits'])) {
1248
+ throw new Error(`Unsupported key usage for a ${name} key`);
1249
+ }
1250
+
1251
+ switch (format) {
1252
+ case 'raw': {
1253
+ const keyObject = createSecretKey(keyData as BinaryLike);
1254
+ return new CryptoKey(keyObject, { name }, keyUsages, extractable);
1255
+ }
1256
+ default:
1257
+ throw new Error(`Unable to import ${name} key with format ${format}`);
1258
+ }
1259
+ };
1260
+
1052
1261
  const checkCryptoKeyPairUsages = (pair: CryptoKeyPair) => {
1053
1262
  if (
1054
1263
  pair.privateKey &&
@@ -1290,6 +1499,15 @@ const cipherOrWrap = async (
1290
1499
  // Fall through
1291
1500
  case 'AES-GCM':
1292
1501
  return aesCipher(mode, key, data, algorithm);
1502
+ case 'AES-KW':
1503
+ return aesKwCipher(mode, key, data);
1504
+ case 'ChaCha20-Poly1305':
1505
+ return chaCha20Poly1305Cipher(
1506
+ mode,
1507
+ key,
1508
+ data,
1509
+ algorithm as ChaCha20Poly1305Params,
1510
+ );
1293
1511
  }
1294
1512
  };
1295
1513
 
@@ -1325,20 +1543,92 @@ export class Subtle {
1325
1543
  baseKey: CryptoKey,
1326
1544
  length: number,
1327
1545
  ): Promise<ArrayBuffer> {
1328
- if (!baseKey.keyUsages.includes('deriveBits')) {
1329
- throw new Error('baseKey does not have deriveBits usage');
1546
+ // Allow either deriveBits OR deriveKey usage (WebCrypto spec allows both)
1547
+ if (
1548
+ !baseKey.keyUsages.includes('deriveBits') &&
1549
+ !baseKey.keyUsages.includes('deriveKey')
1550
+ ) {
1551
+ throw new Error('baseKey does not have deriveBits or deriveKey usage');
1330
1552
  }
1331
1553
  if (baseKey.algorithm.name !== algorithm.name)
1332
1554
  throw new Error('Key algorithm mismatch');
1333
1555
  switch (algorithm.name) {
1334
1556
  case 'PBKDF2':
1335
1557
  return pbkdf2DeriveBits(algorithm, baseKey, length);
1558
+ case 'X25519':
1559
+ // Fall through
1560
+ case 'X448':
1561
+ return xDeriveBits(algorithm, baseKey, length);
1562
+ case 'HKDF':
1563
+ return hkdfDeriveBits(
1564
+ algorithm as unknown as HkdfAlgorithm,
1565
+ baseKey,
1566
+ length,
1567
+ );
1336
1568
  }
1337
1569
  throw new Error(
1338
1570
  `'subtle.deriveBits()' for ${algorithm.name} is not implemented.`,
1339
1571
  );
1340
1572
  }
1341
1573
 
1574
+ async deriveKey(
1575
+ algorithm: SubtleAlgorithm,
1576
+ baseKey: CryptoKey,
1577
+ derivedKeyAlgorithm: SubtleAlgorithm,
1578
+ extractable: boolean,
1579
+ keyUsages: KeyUsage[],
1580
+ ): Promise<CryptoKey> {
1581
+ // Validate baseKey usage
1582
+ if (
1583
+ !baseKey.usages.includes('deriveKey') &&
1584
+ !baseKey.usages.includes('deriveBits')
1585
+ ) {
1586
+ throw lazyDOMException(
1587
+ 'baseKey does not have deriveKey or deriveBits usage',
1588
+ 'InvalidAccessError',
1589
+ );
1590
+ }
1591
+
1592
+ // Calculate required key length
1593
+ const length = getKeyLength(derivedKeyAlgorithm);
1594
+
1595
+ // Step 1: Derive bits
1596
+ let derivedBits: ArrayBuffer;
1597
+ if (baseKey.algorithm.name !== algorithm.name)
1598
+ throw new Error('Key algorithm mismatch');
1599
+
1600
+ switch (algorithm.name) {
1601
+ case 'PBKDF2':
1602
+ derivedBits = await pbkdf2DeriveBits(algorithm, baseKey, length);
1603
+ break;
1604
+ case 'X25519':
1605
+ // Fall through
1606
+ case 'X448':
1607
+ derivedBits = await xDeriveBits(algorithm, baseKey, length);
1608
+ break;
1609
+ case 'HKDF':
1610
+ derivedBits = hkdfDeriveBits(
1611
+ algorithm as unknown as HkdfAlgorithm,
1612
+ baseKey,
1613
+ length,
1614
+ );
1615
+ break;
1616
+ default:
1617
+ throw new Error(
1618
+ `'subtle.deriveKey()' for ${algorithm.name} is not implemented.`,
1619
+ );
1620
+ }
1621
+
1622
+ // Step 2: Import as key
1623
+ return this.importKey(
1624
+ 'raw',
1625
+ derivedBits,
1626
+ derivedKeyAlgorithm,
1627
+ extractable,
1628
+ keyUsages,
1629
+ );
1630
+ }
1631
+
1342
1632
  async encrypt(
1343
1633
  algorithm: EncryptDecryptParams,
1344
1634
  key: CryptoKey,
@@ -1372,6 +1662,115 @@ export class Subtle {
1372
1662
  }
1373
1663
  }
1374
1664
 
1665
+ async wrapKey(
1666
+ format: ImportFormat,
1667
+ key: CryptoKey,
1668
+ wrappingKey: CryptoKey,
1669
+ wrapAlgorithm: EncryptDecryptParams,
1670
+ ): Promise<ArrayBuffer> {
1671
+ // Validate wrappingKey usage
1672
+ if (!wrappingKey.usages.includes('wrapKey')) {
1673
+ throw lazyDOMException(
1674
+ 'wrappingKey does not have wrapKey usage',
1675
+ 'InvalidAccessError',
1676
+ );
1677
+ }
1678
+
1679
+ // Step 1: Export the key
1680
+ const exported = await this.exportKey(format, key);
1681
+
1682
+ // Step 2: Convert to ArrayBuffer if JWK
1683
+ let keyData: ArrayBuffer;
1684
+ if (format === 'jwk') {
1685
+ const jwkString = JSON.stringify(exported);
1686
+ const buffer = SBuffer.from(jwkString, 'utf8');
1687
+
1688
+ // For AES-KW, pad to multiple of 8 bytes (accounting for null terminator)
1689
+ if (wrapAlgorithm.name === 'AES-KW') {
1690
+ const length = buffer.length;
1691
+ // Add 1 for null terminator, then pad to multiple of 8
1692
+ const paddedLength = Math.ceil((length + 1) / 8) * 8;
1693
+ const paddedBuffer = SBuffer.alloc(paddedLength);
1694
+ buffer.copy(paddedBuffer);
1695
+ // Null terminator for JSON string (remaining bytes are already zeros from alloc)
1696
+ paddedBuffer.writeUInt8(0, length);
1697
+ keyData = bufferLikeToArrayBuffer(paddedBuffer);
1698
+ } else {
1699
+ keyData = bufferLikeToArrayBuffer(buffer);
1700
+ }
1701
+ } else {
1702
+ keyData = exported as ArrayBuffer;
1703
+ }
1704
+
1705
+ // Step 3: Encrypt the exported key
1706
+ return cipherOrWrap(
1707
+ CipherOrWrapMode.kWebCryptoCipherEncrypt,
1708
+ wrapAlgorithm,
1709
+ wrappingKey,
1710
+ keyData,
1711
+ 'wrapKey',
1712
+ );
1713
+ }
1714
+
1715
+ async unwrapKey(
1716
+ format: ImportFormat,
1717
+ wrappedKey: BufferLike,
1718
+ unwrappingKey: CryptoKey,
1719
+ unwrapAlgorithm: EncryptDecryptParams,
1720
+ unwrappedKeyAlgorithm: SubtleAlgorithm | AnyAlgorithm,
1721
+ extractable: boolean,
1722
+ keyUsages: KeyUsage[],
1723
+ ): Promise<CryptoKey> {
1724
+ // Validate unwrappingKey usage
1725
+ if (!unwrappingKey.usages.includes('unwrapKey')) {
1726
+ throw lazyDOMException(
1727
+ 'unwrappingKey does not have unwrapKey usage',
1728
+ 'InvalidAccessError',
1729
+ );
1730
+ }
1731
+
1732
+ // Step 1: Decrypt the wrapped key
1733
+ const decrypted = await cipherOrWrap(
1734
+ CipherOrWrapMode.kWebCryptoCipherDecrypt,
1735
+ unwrapAlgorithm,
1736
+ unwrappingKey,
1737
+ bufferLikeToArrayBuffer(wrappedKey),
1738
+ 'unwrapKey',
1739
+ );
1740
+
1741
+ // Step 2: Convert to appropriate format
1742
+ let keyData: BufferLike | JWK;
1743
+ if (format === 'jwk') {
1744
+ const buffer = SBuffer.from(decrypted);
1745
+ // For AES-KW, the data may be padded - find the null terminator
1746
+ let jwkString: string;
1747
+ if (unwrapAlgorithm.name === 'AES-KW') {
1748
+ // Find the null terminator (if present) to get the original string
1749
+ const nullIndex = buffer.indexOf(0);
1750
+ if (nullIndex !== -1) {
1751
+ jwkString = buffer.toString('utf8', 0, nullIndex);
1752
+ } else {
1753
+ // No null terminator, try to parse the whole buffer
1754
+ jwkString = buffer.toString('utf8').trim();
1755
+ }
1756
+ } else {
1757
+ jwkString = buffer.toString('utf8');
1758
+ }
1759
+ keyData = JSON.parse(jwkString) as JWK;
1760
+ } else {
1761
+ keyData = decrypted;
1762
+ }
1763
+
1764
+ // Step 3: Import the key
1765
+ return this.importKey(
1766
+ format,
1767
+ keyData,
1768
+ unwrappedKeyAlgorithm,
1769
+ extractable,
1770
+ keyUsages,
1771
+ );
1772
+ }
1773
+
1375
1774
  async generateKey(
1376
1775
  algorithm: SubtleAlgorithm,
1377
1776
  extractable: boolean,
@@ -1411,6 +1810,26 @@ export class Subtle {
1411
1810
  keyUsages,
1412
1811
  );
1413
1812
  break;
1813
+ case 'ChaCha20-Poly1305': {
1814
+ const length = (algorithm as AesKeyGenParams).length ?? 256;
1815
+
1816
+ if (length !== 256) {
1817
+ throw lazyDOMException(
1818
+ 'ChaCha20-Poly1305 only supports 256-bit keys',
1819
+ 'NotSupportedError',
1820
+ );
1821
+ }
1822
+
1823
+ result = await aesGenerateKey(
1824
+ {
1825
+ name: 'ChaCha20-Poly1305',
1826
+ length: 256,
1827
+ } as unknown as AesKeyGenParams,
1828
+ extractable,
1829
+ keyUsages,
1830
+ );
1831
+ break;
1832
+ }
1414
1833
  case 'HMAC':
1415
1834
  result = await hmacGenerateKey(algorithm, extractable, keyUsages);
1416
1835
  break;
@@ -1436,6 +1855,16 @@ export class Subtle {
1436
1855
  );
1437
1856
  checkCryptoKeyPairUsages(result as CryptoKeyPair);
1438
1857
  break;
1858
+ case 'X25519':
1859
+ // Fall through
1860
+ case 'X448':
1861
+ result = await x_generateKeyPairWebCrypto(
1862
+ algorithm.name.toLowerCase() as 'x25519' | 'x448',
1863
+ extractable,
1864
+ keyUsages,
1865
+ );
1866
+ checkCryptoKeyPairUsages(result as CryptoKeyPair);
1867
+ break;
1439
1868
  default:
1440
1869
  throw new Error(
1441
1870
  `'subtle.generateKey()' is not implemented for ${algorithm.name}.
@@ -1496,6 +1925,8 @@ export class Subtle {
1496
1925
  case 'AES-GCM':
1497
1926
  // Fall through
1498
1927
  case 'AES-KW':
1928
+ // Fall through
1929
+ case 'ChaCha20-Poly1305':
1499
1930
  result = await aesImportKey(
1500
1931
  normalizedAlgorithm,
1501
1932
  format,
@@ -1513,6 +1944,19 @@ export class Subtle {
1513
1944
  keyUsages,
1514
1945
  );
1515
1946
  break;
1947
+ case 'HKDF':
1948
+ result = await hkdfImportKey(
1949
+ format,
1950
+ data as BufferLike | BinaryLike,
1951
+ normalizedAlgorithm,
1952
+ extractable,
1953
+ keyUsages,
1954
+ );
1955
+ break;
1956
+ case 'X25519':
1957
+ // Fall through
1958
+ case 'X448':
1959
+ // Fall through
1516
1960
  case 'Ed25519':
1517
1961
  // Fall through
1518
1962
  case 'Ed448':
@@ -1560,7 +2004,43 @@ export class Subtle {
1560
2004
  key: CryptoKey,
1561
2005
  data: BufferLike,
1562
2006
  ): Promise<ArrayBuffer> {
1563
- return signVerify(algorithm, key, data) as ArrayBuffer;
2007
+ const normalizedAlgorithm = normalizeAlgorithm(algorithm, 'sign');
2008
+
2009
+ if (normalizedAlgorithm.name === 'HMAC') {
2010
+ // Validate key usage
2011
+ if (!key.usages.includes('sign')) {
2012
+ throw lazyDOMException(
2013
+ 'Key does not have sign usage',
2014
+ 'InvalidAccessError',
2015
+ );
2016
+ }
2017
+
2018
+ // Get hash algorithm from key or algorithm params
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 sign
2037
+ const keyData = key.keyObject.export();
2038
+ const hmac = createHmac(hashAlgorithm, keyData);
2039
+ hmac.update(bufferLikeToArrayBuffer(data));
2040
+ return bufferLikeToArrayBuffer(hmac.digest());
2041
+ }
2042
+
2043
+ return signVerify(normalizedAlgorithm, key, data) as ArrayBuffer;
1564
2044
  }
1565
2045
 
1566
2046
  async verify(
@@ -1568,9 +2048,84 @@ export class Subtle {
1568
2048
  key: CryptoKey,
1569
2049
  signature: BufferLike,
1570
2050
  data: BufferLike,
1571
- ): Promise<ArrayBuffer> {
1572
- return signVerify(algorithm, key, data, signature) as ArrayBuffer;
2051
+ ): Promise<boolean> {
2052
+ const normalizedAlgorithm = normalizeAlgorithm(algorithm, 'verify');
2053
+
2054
+ if (normalizedAlgorithm.name === 'HMAC') {
2055
+ // Validate key usage
2056
+ if (!key.usages.includes('verify')) {
2057
+ throw lazyDOMException(
2058
+ 'Key does not have verify usage',
2059
+ 'InvalidAccessError',
2060
+ );
2061
+ }
2062
+
2063
+ // Get hash algorithm
2064
+ // Hash can be either a string or an object with name property
2065
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2066
+ const alg = normalizedAlgorithm as any;
2067
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2068
+ const keyAlg = key.algorithm as any;
2069
+ let hashAlgorithm = 'SHA-256';
2070
+
2071
+ if (typeof alg.hash === 'string') {
2072
+ hashAlgorithm = alg.hash;
2073
+ } else if (alg.hash?.name) {
2074
+ hashAlgorithm = alg.hash.name;
2075
+ } else if (typeof keyAlg.hash === 'string') {
2076
+ hashAlgorithm = keyAlg.hash;
2077
+ } else if (keyAlg.hash?.name) {
2078
+ hashAlgorithm = keyAlg.hash.name;
2079
+ }
2080
+
2081
+ // Create HMAC and compute expected signature
2082
+ const keyData = key.keyObject.export();
2083
+ const hmac = createHmac(hashAlgorithm, keyData);
2084
+ const dataBuffer = bufferLikeToArrayBuffer(data);
2085
+ hmac.update(dataBuffer);
2086
+ const expectedDigest = hmac.digest();
2087
+ const expected = new Uint8Array(bufferLikeToArrayBuffer(expectedDigest));
2088
+
2089
+ // Constant-time comparison
2090
+ const signatureArray = new Uint8Array(bufferLikeToArrayBuffer(signature));
2091
+ if (expected.length !== signatureArray.length) {
2092
+ return false;
2093
+ }
2094
+
2095
+ // Manual constant-time comparison
2096
+ let result = 0;
2097
+ for (let i = 0; i < expected.length; i++) {
2098
+ result |= expected[i]! ^ signatureArray[i]!;
2099
+ }
2100
+ return result === 0;
2101
+ }
2102
+
2103
+ return signVerify(normalizedAlgorithm, key, data, signature) as boolean;
1573
2104
  }
1574
2105
  }
1575
2106
 
1576
2107
  export const subtle = new Subtle();
2108
+
2109
+ function getKeyLength(algorithm: SubtleAlgorithm): number {
2110
+ const name = algorithm.name;
2111
+
2112
+ switch (name) {
2113
+ case 'AES-CTR':
2114
+ case 'AES-CBC':
2115
+ case 'AES-GCM':
2116
+ case 'AES-KW':
2117
+ case 'ChaCha20-Poly1305':
2118
+ return (algorithm as AesKeyGenParams).length || 256;
2119
+
2120
+ case 'HMAC': {
2121
+ const hmacAlg = algorithm as { length?: number };
2122
+ return hmacAlg.length || 256;
2123
+ }
2124
+
2125
+ default:
2126
+ throw lazyDOMException(
2127
+ `Cannot determine key length for ${name}`,
2128
+ 'NotSupportedError',
2129
+ );
2130
+ }
2131
+ }