react-native-quick-crypto 1.0.1 → 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.
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,12 @@ 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';
46
52
  // import { pbkdf2DeriveBits } from './pbkdf2';
47
53
  // import { aesCipher, aesGenerateKey, aesImportKey, getAlgorithmName } from './aes';
@@ -369,6 +375,156 @@ async function aesGcmCipher(
369
375
  }
370
376
  }
371
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
+
372
528
  async function aesGenerateKey(
373
529
  algorithm: AesKeyGenParams,
374
530
  extractable: boolean,
@@ -737,7 +893,12 @@ function edImportKey(
737
893
  const { name } = algorithm;
738
894
 
739
895
  // Validate usages
740
- 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)) {
741
902
  throw lazyDOMException(
742
903
  `Unsupported key usage for ${name} key`,
743
904
  'SyntaxError',
@@ -853,8 +1014,12 @@ const exportKeySpki = async (
853
1014
  case 'Ed25519':
854
1015
  // Fall through
855
1016
  case 'Ed448':
1017
+ // Fall through
1018
+ case 'X25519':
1019
+ // Fall through
1020
+ case 'X448':
856
1021
  if (key.type === 'public') {
857
- // Export Ed key in SPKI DER format
1022
+ // Export Ed/X key in SPKI DER format
858
1023
  return bufferLikeToArrayBuffer(
859
1024
  key.keyObject.handle.exportKey(KFormatType.DER, KeyEncoding.SPKI),
860
1025
  );
@@ -902,8 +1067,12 @@ const exportKeyPkcs8 = async (
902
1067
  case 'Ed25519':
903
1068
  // Fall through
904
1069
  case 'Ed448':
1070
+ // Fall through
1071
+ case 'X25519':
1072
+ // Fall through
1073
+ case 'X448':
905
1074
  if (key.type === 'private') {
906
- // Export Ed key in PKCS8 DER format
1075
+ // Export Ed/X key in PKCS8 DER format
907
1076
  return bufferLikeToArrayBuffer(
908
1077
  key.keyObject.handle.exportKey(KFormatType.DER, KeyEncoding.PKCS8),
909
1078
  );
@@ -937,6 +1106,19 @@ const exportKeyRaw = (key: CryptoKey): ArrayBuffer | unknown => {
937
1106
  return ecExportKey(key, KWebCryptoKeyFormat.kWebCryptoKeyFormatRaw);
938
1107
  }
939
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;
940
1122
  case 'AES-CTR':
941
1123
  // Fall through
942
1124
  case 'AES-CBC':
@@ -945,6 +1127,8 @@ const exportKeyRaw = (key: CryptoKey): ArrayBuffer | unknown => {
945
1127
  // Fall through
946
1128
  case 'AES-KW':
947
1129
  // Fall through
1130
+ case 'ChaCha20-Poly1305':
1131
+ // Fall through
948
1132
  case 'HMAC': {
949
1133
  const exported = key.keyObject.export();
950
1134
  // Convert Buffer to ArrayBuffer
@@ -994,6 +1178,8 @@ const exportKeyJWK = (key: CryptoKey): ArrayBuffer | unknown => {
994
1178
  case 'AES-GCM':
995
1179
  // Fall through
996
1180
  case 'AES-KW':
1181
+ // Fall through
1182
+ case 'ChaCha20-Poly1305':
997
1183
  if (key.algorithm.length === undefined) {
998
1184
  throw lazyDOMException(
999
1185
  `Algorithm ${key.algorithm.name} missing required length property`,
@@ -1290,6 +1476,15 @@ const cipherOrWrap = async (
1290
1476
  // Fall through
1291
1477
  case 'AES-GCM':
1292
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
+ );
1293
1488
  }
1294
1489
  };
1295
1490
 
@@ -1325,20 +1520,79 @@ export class Subtle {
1325
1520
  baseKey: CryptoKey,
1326
1521
  length: number,
1327
1522
  ): Promise<ArrayBuffer> {
1328
- if (!baseKey.keyUsages.includes('deriveBits')) {
1329
- 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');
1330
1529
  }
1331
1530
  if (baseKey.algorithm.name !== algorithm.name)
1332
1531
  throw new Error('Key algorithm mismatch');
1333
1532
  switch (algorithm.name) {
1334
1533
  case 'PBKDF2':
1335
1534
  return pbkdf2DeriveBits(algorithm, baseKey, length);
1535
+ case 'X25519':
1536
+ // Fall through
1537
+ case 'X448':
1538
+ return xDeriveBits(algorithm, baseKey, length);
1336
1539
  }
1337
1540
  throw new Error(
1338
1541
  `'subtle.deriveBits()' for ${algorithm.name} is not implemented.`,
1339
1542
  );
1340
1543
  }
1341
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
+
1342
1596
  async encrypt(
1343
1597
  algorithm: EncryptDecryptParams,
1344
1598
  key: CryptoKey,
@@ -1372,6 +1626,115 @@ export class Subtle {
1372
1626
  }
1373
1627
  }
1374
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
+
1375
1738
  async generateKey(
1376
1739
  algorithm: SubtleAlgorithm,
1377
1740
  extractable: boolean,
@@ -1411,6 +1774,26 @@ export class Subtle {
1411
1774
  keyUsages,
1412
1775
  );
1413
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
+ }
1414
1797
  case 'HMAC':
1415
1798
  result = await hmacGenerateKey(algorithm, extractable, keyUsages);
1416
1799
  break;
@@ -1436,6 +1819,16 @@ export class Subtle {
1436
1819
  );
1437
1820
  checkCryptoKeyPairUsages(result as CryptoKeyPair);
1438
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;
1439
1832
  default:
1440
1833
  throw new Error(
1441
1834
  `'subtle.generateKey()' is not implemented for ${algorithm.name}.
@@ -1496,6 +1889,8 @@ export class Subtle {
1496
1889
  case 'AES-GCM':
1497
1890
  // Fall through
1498
1891
  case 'AES-KW':
1892
+ // Fall through
1893
+ case 'ChaCha20-Poly1305':
1499
1894
  result = await aesImportKey(
1500
1895
  normalizedAlgorithm,
1501
1896
  format,
@@ -1513,6 +1908,10 @@ export class Subtle {
1513
1908
  keyUsages,
1514
1909
  );
1515
1910
  break;
1911
+ case 'X25519':
1912
+ // Fall through
1913
+ case 'X448':
1914
+ // Fall through
1516
1915
  case 'Ed25519':
1517
1916
  // Fall through
1518
1917
  case 'Ed448':
@@ -1560,7 +1959,43 @@ export class Subtle {
1560
1959
  key: CryptoKey,
1561
1960
  data: BufferLike,
1562
1961
  ): Promise<ArrayBuffer> {
1563
- 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;
1564
1999
  }
1565
2000
 
1566
2001
  async verify(
@@ -1568,9 +2003,84 @@ export class Subtle {
1568
2003
  key: CryptoKey,
1569
2004
  signature: BufferLike,
1570
2005
  data: BufferLike,
1571
- ): Promise<ArrayBuffer> {
1572
- 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;
1573
2059
  }
1574
2060
  }
1575
2061
 
1576
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
+ }
@@ -106,7 +106,9 @@ export type EncryptDecryptAlgorithm =
106
106
  | 'RSA-OAEP'
107
107
  | 'AES-CTR'
108
108
  | 'AES-CBC'
109
- | 'AES-GCM';
109
+ | 'AES-GCM'
110
+ | 'AES-KW'
111
+ | 'ChaCha20-Poly1305';
110
112
 
111
113
  export type RsaOaepParams = {
112
114
  name: 'RSA-OAEP';
@@ -131,6 +133,13 @@ export type AesGcmParams = {
131
133
  additionalData?: BufferLike;
132
134
  };
133
135
 
136
+ export type ChaCha20Poly1305Params = {
137
+ name: 'ChaCha20-Poly1305';
138
+ iv: BufferLike;
139
+ tagLength?: 128;
140
+ additionalData?: BufferLike;
141
+ };
142
+
134
143
  export type AesKwParams = {
135
144
  name: 'AES-KW';
136
145
  wrappingKey?: BufferLike;
@@ -149,7 +158,9 @@ export type EncryptDecryptParams =
149
158
  | AesCbcParams
150
159
  | AesCtrParams
151
160
  | AesGcmParams
152
- | RsaOaepParams;
161
+ | AesKwParams
162
+ | RsaOaepParams
163
+ | ChaCha20Poly1305Params;
153
164
 
154
165
  export type AnyAlgorithm =
155
166
  | DigestAlgorithm
@@ -446,7 +457,9 @@ export type Operation =
446
457
  | 'generateKey'
447
458
  | 'importKey'
448
459
  | 'exportKey'
449
- | 'deriveBits';
460
+ | 'deriveBits'
461
+ | 'wrapKey'
462
+ | 'unwrapKey';
450
463
 
451
464
  export interface KeyPairOptions {
452
465
  namedCurve: string;