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/QuickCrypto.podspec +5 -47
- package/cpp/cipher/HybridCipher.cpp +17 -1
- package/cpp/ed25519/HybridEdKeyPair.cpp +8 -2
- package/lib/commonjs/ed.js +68 -0
- package/lib/commonjs/ed.js.map +1 -1
- package/lib/commonjs/subtle.js +372 -7
- 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/subtle.js +373 -8
- 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/subtle.d.ts +4 -1
- package/lib/typescript/subtle.d.ts.map +1 -1
- package/lib/typescript/utils/types.d.ts +9 -3
- package/lib/typescript/utils/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/ed.ts +102 -0
- package/src/subtle.ts +519 -9
- package/src/utils/types.ts +16 -3
package/src/subtle.ts
CHANGED
|
@@ -15,6 +15,7 @@ import type {
|
|
|
15
15
|
AesCbcParams,
|
|
16
16
|
AesGcmParams,
|
|
17
17
|
RsaOaepParams,
|
|
18
|
+
ChaCha20Poly1305Params,
|
|
18
19
|
} from './utils';
|
|
19
20
|
import { KFormatType, KeyEncoding } from './utils';
|
|
20
21
|
import {
|
|
@@ -41,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 {
|
|
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
|
-
|
|
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
|
-
|
|
1329
|
-
|
|
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
|
-
|
|
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<
|
|
1572
|
-
|
|
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
|
+
}
|
package/src/utils/types.ts
CHANGED
|
@@ -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
|
-
|
|
|
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;
|