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.
- package/QuickCrypto.podspec +19 -52
- package/android/CMakeLists.txt +4 -2
- package/android/build.gradle +1 -1
- package/cpp/cipher/HybridCipher.cpp +20 -3
- package/cpp/cipher/HybridRsaCipher.cpp +20 -1
- package/cpp/ed25519/HybridEdKeyPair.cpp +8 -2
- package/cpp/keys/HybridKeyObjectHandle.cpp +8 -0
- package/cpp/keys/KeyObjectData.hpp +1 -1
- package/cpp/mldsa/HybridMlDsaKeyPair.cpp +264 -0
- package/cpp/mldsa/HybridMlDsaKeyPair.hpp +47 -0
- package/cpp/sign/HybridSignHandle.cpp +97 -22
- package/cpp/sign/HybridVerifyHandle.cpp +90 -21
- package/deps/ncrypto/.bazelignore +4 -0
- package/deps/ncrypto/.bazelrc +2 -0
- package/deps/ncrypto/.bazelversion +1 -0
- package/deps/ncrypto/.clang-format +111 -0
- package/deps/ncrypto/.github/workflows/bazel.yml +58 -0
- package/deps/ncrypto/.github/workflows/linter.yml +38 -0
- package/deps/ncrypto/.github/workflows/macos.yml +43 -0
- package/deps/ncrypto/.github/workflows/ubuntu.yml +46 -0
- package/deps/ncrypto/.github/workflows/visual-studio.yml +49 -0
- package/deps/ncrypto/.python-version +1 -0
- package/deps/ncrypto/BUILD.bazel +36 -0
- package/deps/ncrypto/CMakeLists.txt +55 -0
- package/deps/ncrypto/LICENSE +21 -0
- package/deps/ncrypto/MODULE.bazel +1 -0
- package/deps/ncrypto/MODULE.bazel.lock +280 -0
- package/deps/ncrypto/README.md +18 -0
- package/deps/ncrypto/WORKSPACE +15 -0
- package/deps/ncrypto/cmake/CPM.cmake +1225 -0
- package/deps/ncrypto/cmake/ncrypto-flags.cmake +16 -0
- package/deps/ncrypto/include/dh-primes.h +67 -0
- package/deps/ncrypto/{ncrypto.h → include/ncrypto.h} +361 -89
- package/deps/ncrypto/patches/0001-Expose-libdecrepit-so-NodeJS-can-use-it-for-ncrypto.patch +28 -0
- package/deps/ncrypto/pyproject.toml +38 -0
- package/deps/ncrypto/src/CMakeLists.txt +15 -0
- package/deps/ncrypto/src/engine.cpp +93 -0
- package/deps/ncrypto/{ncrypto.cc → src/ncrypto.cpp} +1168 -234
- package/deps/ncrypto/tests/BUILD.bazel +9 -0
- package/deps/ncrypto/tests/CMakeLists.txt +7 -0
- package/deps/ncrypto/tests/basic.cpp +86 -0
- package/deps/ncrypto/tools/run-clang-format.sh +42 -0
- package/lib/commonjs/ed.js +68 -0
- package/lib/commonjs/ed.js.map +1 -1
- package/lib/commonjs/keys/classes.js +6 -0
- package/lib/commonjs/keys/classes.js.map +1 -1
- package/lib/commonjs/mldsa.js +69 -0
- package/lib/commonjs/mldsa.js.map +1 -0
- package/lib/commonjs/specs/mlDsaKeyPair.nitro.js +6 -0
- package/lib/commonjs/specs/mlDsaKeyPair.nitro.js.map +1 -0
- package/lib/commonjs/subtle.js +483 -13
- package/lib/commonjs/subtle.js.map +1 -1
- package/lib/commonjs/utils/types.js.map +1 -1
- package/lib/module/ed.js +66 -0
- package/lib/module/ed.js.map +1 -1
- package/lib/module/keys/classes.js +6 -0
- package/lib/module/keys/classes.js.map +1 -1
- package/lib/module/mldsa.js +63 -0
- package/lib/module/mldsa.js.map +1 -0
- package/lib/module/specs/mlDsaKeyPair.nitro.js +4 -0
- package/lib/module/specs/mlDsaKeyPair.nitro.js.map +1 -0
- package/lib/module/subtle.js +484 -14
- package/lib/module/subtle.js.map +1 -1
- package/lib/module/utils/types.js.map +1 -1
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/lib/typescript/ed.d.ts +4 -1
- package/lib/typescript/ed.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +2 -0
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/keys/classes.d.ts +2 -0
- package/lib/typescript/keys/classes.d.ts.map +1 -1
- package/lib/typescript/mldsa.d.ts +18 -0
- package/lib/typescript/mldsa.d.ts.map +1 -0
- package/lib/typescript/specs/mlDsaKeyPair.nitro.d.ts +16 -0
- package/lib/typescript/specs/mlDsaKeyPair.nitro.d.ts.map +1 -0
- package/lib/typescript/subtle.d.ts +4 -1
- package/lib/typescript/subtle.d.ts.map +1 -1
- package/lib/typescript/utils/types.d.ts +14 -6
- package/lib/typescript/utils/types.d.ts.map +1 -1
- package/nitrogen/generated/android/QuickCrypto+autolinking.cmake +1 -0
- package/nitrogen/generated/android/QuickCryptoOnLoad.cpp +10 -0
- package/nitrogen/generated/ios/QuickCryptoAutolinking.mm +10 -0
- package/nitrogen/generated/shared/c++/AsymmetricKeyType.hpp +12 -0
- package/nitrogen/generated/shared/c++/HybridMlDsaKeyPairSpec.cpp +29 -0
- package/nitrogen/generated/shared/c++/HybridMlDsaKeyPairSpec.hpp +73 -0
- package/package.json +7 -3
- package/src/ed.ts +102 -0
- package/src/keys/classes.ts +9 -0
- package/src/mldsa.ts +125 -0
- package/src/specs/mlDsaKeyPair.nitro.ts +29 -0
- package/src/subtle.ts +667 -17
- 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 {
|
|
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:
|
|
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
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1214
|
-
|
|
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
|
-
|
|
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<
|
|
1432
|
-
|
|
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
|
+
}
|