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