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