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/lib/module/subtle.js
CHANGED
|
@@ -17,8 +17,9 @@ 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
21
|
import { mldsa_generateKeyPairWebCrypto } from './mldsa';
|
|
22
|
+
import { hkdfDeriveBits } from './hkdf';
|
|
22
23
|
// import { pbkdf2DeriveBits } from './pbkdf2';
|
|
23
24
|
// import { aesCipher, aesGenerateKey, aesImportKey, getAlgorithmName } from './aes';
|
|
24
25
|
// import { rsaCipher, rsaExportKey, rsaImportKey, rsaKeyGenerate } from './rsa';
|
|
@@ -267,6 +268,120 @@ async function aesGcmCipher(mode, key, data, algorithm) {
|
|
|
267
268
|
return result.buffer;
|
|
268
269
|
}
|
|
269
270
|
}
|
|
271
|
+
async function aesKwCipher(mode, key, data) {
|
|
272
|
+
const isWrap = mode === CipherOrWrapMode.kWebCryptoCipherEncrypt;
|
|
273
|
+
|
|
274
|
+
// AES-KW requires input to be a multiple of 8 bytes (64 bits)
|
|
275
|
+
if (data.byteLength % 8 !== 0) {
|
|
276
|
+
throw lazyDOMException(`AES-KW input length must be a multiple of 8 bytes, got ${data.byteLength}`, 'OperationError');
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// AES-KW requires at least 16 bytes of input (128 bits)
|
|
280
|
+
if (isWrap && data.byteLength < 16) {
|
|
281
|
+
throw lazyDOMException(`AES-KW input must be at least 16 bytes, got ${data.byteLength}`, 'OperationError');
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Get cipher type based on key length
|
|
285
|
+
const keyLength = key.algorithm.length;
|
|
286
|
+
// Use aes*-wrap for both operations (matching Node.js)
|
|
287
|
+
const cipherType = `aes${keyLength}-wrap`;
|
|
288
|
+
|
|
289
|
+
// Export key material
|
|
290
|
+
const exportedKey = key.keyObject.export();
|
|
291
|
+
const cipherKey = bufferLikeToArrayBuffer(exportedKey);
|
|
292
|
+
|
|
293
|
+
// AES-KW uses a default IV as specified in RFC 3394
|
|
294
|
+
const defaultWrapIV = new Uint8Array([0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6]);
|
|
295
|
+
const factory = NitroModules.createHybridObject('CipherFactory');
|
|
296
|
+
const cipher = factory.createCipher({
|
|
297
|
+
isCipher: isWrap,
|
|
298
|
+
cipherType,
|
|
299
|
+
cipherKey,
|
|
300
|
+
iv: defaultWrapIV.buffer // RFC 3394 default IV for AES-KW
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
// Process data
|
|
304
|
+
const updated = cipher.update(data);
|
|
305
|
+
const final = cipher.final();
|
|
306
|
+
|
|
307
|
+
// Concatenate results
|
|
308
|
+
const result = new Uint8Array(updated.byteLength + final.byteLength);
|
|
309
|
+
result.set(new Uint8Array(updated), 0);
|
|
310
|
+
result.set(new Uint8Array(final), updated.byteLength);
|
|
311
|
+
return result.buffer;
|
|
312
|
+
}
|
|
313
|
+
async function chaCha20Poly1305Cipher(mode, key, data, algorithm) {
|
|
314
|
+
const {
|
|
315
|
+
iv,
|
|
316
|
+
additionalData,
|
|
317
|
+
tagLength = 128
|
|
318
|
+
} = algorithm;
|
|
319
|
+
|
|
320
|
+
// Validate IV (must be 12 bytes for ChaCha20-Poly1305)
|
|
321
|
+
const ivBuffer = bufferLikeToArrayBuffer(iv);
|
|
322
|
+
if (!ivBuffer || ivBuffer.byteLength !== 12) {
|
|
323
|
+
throw lazyDOMException('ChaCha20-Poly1305 IV must be exactly 12 bytes', 'OperationError');
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Validate tag length (only 128-bit supported)
|
|
327
|
+
if (tagLength !== 128) {
|
|
328
|
+
throw lazyDOMException('ChaCha20-Poly1305 only supports 128-bit auth tags', 'NotSupportedError');
|
|
329
|
+
}
|
|
330
|
+
const tagByteLength = 16; // 128 bits = 16 bytes
|
|
331
|
+
|
|
332
|
+
// Create cipher using existing ChaCha20-Poly1305 implementation
|
|
333
|
+
const factory = NitroModules.createHybridObject('CipherFactory');
|
|
334
|
+
const cipher = factory.createCipher({
|
|
335
|
+
isCipher: mode === CipherOrWrapMode.kWebCryptoCipherEncrypt,
|
|
336
|
+
cipherType: 'chacha20-poly1305',
|
|
337
|
+
cipherKey: bufferLikeToArrayBuffer(key.keyObject.export()),
|
|
338
|
+
iv: ivBuffer,
|
|
339
|
+
authTagLen: tagByteLength
|
|
340
|
+
});
|
|
341
|
+
let processData;
|
|
342
|
+
let authTag;
|
|
343
|
+
if (mode === CipherOrWrapMode.kWebCryptoCipherDecrypt) {
|
|
344
|
+
// For decryption, extract auth tag from end of data
|
|
345
|
+
const dataView = new Uint8Array(data);
|
|
346
|
+
if (dataView.byteLength < tagByteLength) {
|
|
347
|
+
throw lazyDOMException('The provided data is too small.', 'OperationError');
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Split data and tag
|
|
351
|
+
const ciphertextLength = dataView.byteLength - tagByteLength;
|
|
352
|
+
processData = dataView.slice(0, ciphertextLength).buffer;
|
|
353
|
+
authTag = dataView.slice(ciphertextLength).buffer;
|
|
354
|
+
|
|
355
|
+
// Set auth tag for verification
|
|
356
|
+
cipher.setAuthTag(authTag);
|
|
357
|
+
} else {
|
|
358
|
+
processData = data;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Set additional authenticated data if provided
|
|
362
|
+
if (additionalData) {
|
|
363
|
+
cipher.setAAD(bufferLikeToArrayBuffer(additionalData));
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Process data
|
|
367
|
+
const updated = cipher.update(processData);
|
|
368
|
+
const final = cipher.final();
|
|
369
|
+
if (mode === CipherOrWrapMode.kWebCryptoCipherEncrypt) {
|
|
370
|
+
// For encryption, append auth tag to result
|
|
371
|
+
const tag = cipher.getAuthTag();
|
|
372
|
+
const result = new Uint8Array(updated.byteLength + final.byteLength + tag.byteLength);
|
|
373
|
+
result.set(new Uint8Array(updated), 0);
|
|
374
|
+
result.set(new Uint8Array(final), updated.byteLength);
|
|
375
|
+
result.set(new Uint8Array(tag), updated.byteLength + final.byteLength);
|
|
376
|
+
return result.buffer;
|
|
377
|
+
} else {
|
|
378
|
+
// For decryption, just concatenate plaintext
|
|
379
|
+
const result = new Uint8Array(updated.byteLength + final.byteLength);
|
|
380
|
+
result.set(new Uint8Array(updated), 0);
|
|
381
|
+
result.set(new Uint8Array(final), updated.byteLength);
|
|
382
|
+
return result.buffer;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
270
385
|
async function aesGenerateKey(algorithm, extractable, keyUsages) {
|
|
271
386
|
const {
|
|
272
387
|
length
|
|
@@ -554,7 +669,9 @@ function edImportKey(format, data, algorithm, extractable, keyUsages) {
|
|
|
554
669
|
} = algorithm;
|
|
555
670
|
|
|
556
671
|
// Validate usages
|
|
557
|
-
|
|
672
|
+
const isX = name === 'X25519' || name === 'X448';
|
|
673
|
+
const allowedUsages = isX ? ['deriveKey', 'deriveBits'] : ['sign', 'verify'];
|
|
674
|
+
if (hasAnyNotIn(keyUsages, allowedUsages)) {
|
|
558
675
|
throw lazyDOMException(`Unsupported key usage for ${name} key`, 'SyntaxError');
|
|
559
676
|
}
|
|
560
677
|
let keyObject;
|
|
@@ -627,8 +744,12 @@ const exportKeySpki = async key => {
|
|
|
627
744
|
case 'Ed25519':
|
|
628
745
|
// Fall through
|
|
629
746
|
case 'Ed448':
|
|
747
|
+
// Fall through
|
|
748
|
+
case 'X25519':
|
|
749
|
+
// Fall through
|
|
750
|
+
case 'X448':
|
|
630
751
|
if (key.type === 'public') {
|
|
631
|
-
// Export Ed key in SPKI DER format
|
|
752
|
+
// Export Ed/X key in SPKI DER format
|
|
632
753
|
return bufferLikeToArrayBuffer(key.keyObject.handle.exportKey(KFormatType.DER, KeyEncoding.SPKI));
|
|
633
754
|
}
|
|
634
755
|
break;
|
|
@@ -666,8 +787,12 @@ const exportKeyPkcs8 = async key => {
|
|
|
666
787
|
case 'Ed25519':
|
|
667
788
|
// Fall through
|
|
668
789
|
case 'Ed448':
|
|
790
|
+
// Fall through
|
|
791
|
+
case 'X25519':
|
|
792
|
+
// Fall through
|
|
793
|
+
case 'X448':
|
|
669
794
|
if (key.type === 'private') {
|
|
670
|
-
// Export Ed key in PKCS8 DER format
|
|
795
|
+
// Export Ed/X key in PKCS8 DER format
|
|
671
796
|
return bufferLikeToArrayBuffer(key.keyObject.handle.exportKey(KFormatType.DER, KeyEncoding.PKCS8));
|
|
672
797
|
}
|
|
673
798
|
break;
|
|
@@ -693,6 +818,19 @@ const exportKeyRaw = key => {
|
|
|
693
818
|
return ecExportKey(key, KWebCryptoKeyFormat.kWebCryptoKeyFormatRaw);
|
|
694
819
|
}
|
|
695
820
|
break;
|
|
821
|
+
case 'Ed25519':
|
|
822
|
+
// Fall through
|
|
823
|
+
case 'Ed448':
|
|
824
|
+
// Fall through
|
|
825
|
+
case 'X25519':
|
|
826
|
+
// Fall through
|
|
827
|
+
case 'X448':
|
|
828
|
+
if (key.type === 'public') {
|
|
829
|
+
// Export raw public key
|
|
830
|
+
const exported = key.keyObject.handle.exportKey();
|
|
831
|
+
return bufferLikeToArrayBuffer(exported);
|
|
832
|
+
}
|
|
833
|
+
break;
|
|
696
834
|
case 'AES-CTR':
|
|
697
835
|
// Fall through
|
|
698
836
|
case 'AES-CBC':
|
|
@@ -701,6 +839,8 @@ const exportKeyRaw = key => {
|
|
|
701
839
|
// Fall through
|
|
702
840
|
case 'AES-KW':
|
|
703
841
|
// Fall through
|
|
842
|
+
case 'ChaCha20-Poly1305':
|
|
843
|
+
// Fall through
|
|
704
844
|
case 'HMAC':
|
|
705
845
|
{
|
|
706
846
|
const exported = key.keyObject.export();
|
|
@@ -740,6 +880,8 @@ const exportKeyJWK = key => {
|
|
|
740
880
|
case 'AES-GCM':
|
|
741
881
|
// Fall through
|
|
742
882
|
case 'AES-KW':
|
|
883
|
+
// Fall through
|
|
884
|
+
case 'ChaCha20-Poly1305':
|
|
743
885
|
if (key.algorithm.length === undefined) {
|
|
744
886
|
throw lazyDOMException(`Algorithm ${key.algorithm.name} missing required length property`, 'InvalidAccessError');
|
|
745
887
|
}
|
|
@@ -778,6 +920,25 @@ const importGenericSecretKey = async ({
|
|
|
778
920
|
}
|
|
779
921
|
throw new Error(`Unable to import ${name} key with format ${format}`);
|
|
780
922
|
};
|
|
923
|
+
const hkdfImportKey = async (format, keyData, algorithm, extractable, keyUsages) => {
|
|
924
|
+
const {
|
|
925
|
+
name
|
|
926
|
+
} = algorithm;
|
|
927
|
+
if (hasAnyNotIn(keyUsages, ['deriveKey', 'deriveBits'])) {
|
|
928
|
+
throw new Error(`Unsupported key usage for a ${name} key`);
|
|
929
|
+
}
|
|
930
|
+
switch (format) {
|
|
931
|
+
case 'raw':
|
|
932
|
+
{
|
|
933
|
+
const keyObject = createSecretKey(keyData);
|
|
934
|
+
return new CryptoKey(keyObject, {
|
|
935
|
+
name
|
|
936
|
+
}, keyUsages, extractable);
|
|
937
|
+
}
|
|
938
|
+
default:
|
|
939
|
+
throw new Error(`Unable to import ${name} key with format ${format}`);
|
|
940
|
+
}
|
|
941
|
+
};
|
|
781
942
|
const checkCryptoKeyPairUsages = pair => {
|
|
782
943
|
if (pair.privateKey && pair.privateKey instanceof CryptoKey && pair.privateKey.keyUsages && pair.privateKey.keyUsages.length > 0) {
|
|
783
944
|
return;
|
|
@@ -937,6 +1098,10 @@ const cipherOrWrap = async (mode, algorithm, key, data, op) => {
|
|
|
937
1098
|
// Fall through
|
|
938
1099
|
case 'AES-GCM':
|
|
939
1100
|
return aesCipher(mode, key, data, algorithm);
|
|
1101
|
+
case 'AES-KW':
|
|
1102
|
+
return aesKwCipher(mode, key, data);
|
|
1103
|
+
case 'ChaCha20-Poly1305':
|
|
1104
|
+
return chaCha20Poly1305Cipher(mode, key, data, algorithm);
|
|
940
1105
|
}
|
|
941
1106
|
};
|
|
942
1107
|
export class Subtle {
|
|
@@ -949,16 +1114,54 @@ export class Subtle {
|
|
|
949
1114
|
return asyncDigest(normalizedAlgorithm, data);
|
|
950
1115
|
}
|
|
951
1116
|
async deriveBits(algorithm, baseKey, length) {
|
|
952
|
-
|
|
953
|
-
|
|
1117
|
+
// Allow either deriveBits OR deriveKey usage (WebCrypto spec allows both)
|
|
1118
|
+
if (!baseKey.keyUsages.includes('deriveBits') && !baseKey.keyUsages.includes('deriveKey')) {
|
|
1119
|
+
throw new Error('baseKey does not have deriveBits or deriveKey usage');
|
|
954
1120
|
}
|
|
955
1121
|
if (baseKey.algorithm.name !== algorithm.name) throw new Error('Key algorithm mismatch');
|
|
956
1122
|
switch (algorithm.name) {
|
|
957
1123
|
case 'PBKDF2':
|
|
958
1124
|
return pbkdf2DeriveBits(algorithm, baseKey, length);
|
|
1125
|
+
case 'X25519':
|
|
1126
|
+
// Fall through
|
|
1127
|
+
case 'X448':
|
|
1128
|
+
return xDeriveBits(algorithm, baseKey, length);
|
|
1129
|
+
case 'HKDF':
|
|
1130
|
+
return hkdfDeriveBits(algorithm, baseKey, length);
|
|
959
1131
|
}
|
|
960
1132
|
throw new Error(`'subtle.deriveBits()' for ${algorithm.name} is not implemented.`);
|
|
961
1133
|
}
|
|
1134
|
+
async deriveKey(algorithm, baseKey, derivedKeyAlgorithm, extractable, keyUsages) {
|
|
1135
|
+
// Validate baseKey usage
|
|
1136
|
+
if (!baseKey.usages.includes('deriveKey') && !baseKey.usages.includes('deriveBits')) {
|
|
1137
|
+
throw lazyDOMException('baseKey does not have deriveKey or deriveBits usage', 'InvalidAccessError');
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
// Calculate required key length
|
|
1141
|
+
const length = getKeyLength(derivedKeyAlgorithm);
|
|
1142
|
+
|
|
1143
|
+
// Step 1: Derive bits
|
|
1144
|
+
let derivedBits;
|
|
1145
|
+
if (baseKey.algorithm.name !== algorithm.name) throw new Error('Key algorithm mismatch');
|
|
1146
|
+
switch (algorithm.name) {
|
|
1147
|
+
case 'PBKDF2':
|
|
1148
|
+
derivedBits = await pbkdf2DeriveBits(algorithm, baseKey, length);
|
|
1149
|
+
break;
|
|
1150
|
+
case 'X25519':
|
|
1151
|
+
// Fall through
|
|
1152
|
+
case 'X448':
|
|
1153
|
+
derivedBits = await xDeriveBits(algorithm, baseKey, length);
|
|
1154
|
+
break;
|
|
1155
|
+
case 'HKDF':
|
|
1156
|
+
derivedBits = hkdfDeriveBits(algorithm, baseKey, length);
|
|
1157
|
+
break;
|
|
1158
|
+
default:
|
|
1159
|
+
throw new Error(`'subtle.deriveKey()' for ${algorithm.name} is not implemented.`);
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
// Step 2: Import as key
|
|
1163
|
+
return this.importKey('raw', derivedBits, derivedKeyAlgorithm, extractable, keyUsages);
|
|
1164
|
+
}
|
|
962
1165
|
async encrypt(algorithm, key, data) {
|
|
963
1166
|
const normalizedAlgorithm = normalizeAlgorithm(algorithm, 'encrypt');
|
|
964
1167
|
return cipherOrWrap(CipherOrWrapMode.kWebCryptoCipherEncrypt, normalizedAlgorithm, key, bufferLikeToArrayBuffer(data), 'encrypt');
|
|
@@ -976,6 +1179,76 @@ export class Subtle {
|
|
|
976
1179
|
return exportKeyRaw(key);
|
|
977
1180
|
}
|
|
978
1181
|
}
|
|
1182
|
+
async wrapKey(format, key, wrappingKey, wrapAlgorithm) {
|
|
1183
|
+
// Validate wrappingKey usage
|
|
1184
|
+
if (!wrappingKey.usages.includes('wrapKey')) {
|
|
1185
|
+
throw lazyDOMException('wrappingKey does not have wrapKey usage', 'InvalidAccessError');
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
// Step 1: Export the key
|
|
1189
|
+
const exported = await this.exportKey(format, key);
|
|
1190
|
+
|
|
1191
|
+
// Step 2: Convert to ArrayBuffer if JWK
|
|
1192
|
+
let keyData;
|
|
1193
|
+
if (format === 'jwk') {
|
|
1194
|
+
const jwkString = JSON.stringify(exported);
|
|
1195
|
+
const buffer = SBuffer.from(jwkString, 'utf8');
|
|
1196
|
+
|
|
1197
|
+
// For AES-KW, pad to multiple of 8 bytes (accounting for null terminator)
|
|
1198
|
+
if (wrapAlgorithm.name === 'AES-KW') {
|
|
1199
|
+
const length = buffer.length;
|
|
1200
|
+
// Add 1 for null terminator, then pad to multiple of 8
|
|
1201
|
+
const paddedLength = Math.ceil((length + 1) / 8) * 8;
|
|
1202
|
+
const paddedBuffer = SBuffer.alloc(paddedLength);
|
|
1203
|
+
buffer.copy(paddedBuffer);
|
|
1204
|
+
// Null terminator for JSON string (remaining bytes are already zeros from alloc)
|
|
1205
|
+
paddedBuffer.writeUInt8(0, length);
|
|
1206
|
+
keyData = bufferLikeToArrayBuffer(paddedBuffer);
|
|
1207
|
+
} else {
|
|
1208
|
+
keyData = bufferLikeToArrayBuffer(buffer);
|
|
1209
|
+
}
|
|
1210
|
+
} else {
|
|
1211
|
+
keyData = exported;
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
// Step 3: Encrypt the exported key
|
|
1215
|
+
return cipherOrWrap(CipherOrWrapMode.kWebCryptoCipherEncrypt, wrapAlgorithm, wrappingKey, keyData, 'wrapKey');
|
|
1216
|
+
}
|
|
1217
|
+
async unwrapKey(format, wrappedKey, unwrappingKey, unwrapAlgorithm, unwrappedKeyAlgorithm, extractable, keyUsages) {
|
|
1218
|
+
// Validate unwrappingKey usage
|
|
1219
|
+
if (!unwrappingKey.usages.includes('unwrapKey')) {
|
|
1220
|
+
throw lazyDOMException('unwrappingKey does not have unwrapKey usage', 'InvalidAccessError');
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
// Step 1: Decrypt the wrapped key
|
|
1224
|
+
const decrypted = await cipherOrWrap(CipherOrWrapMode.kWebCryptoCipherDecrypt, unwrapAlgorithm, unwrappingKey, bufferLikeToArrayBuffer(wrappedKey), 'unwrapKey');
|
|
1225
|
+
|
|
1226
|
+
// Step 2: Convert to appropriate format
|
|
1227
|
+
let keyData;
|
|
1228
|
+
if (format === 'jwk') {
|
|
1229
|
+
const buffer = SBuffer.from(decrypted);
|
|
1230
|
+
// For AES-KW, the data may be padded - find the null terminator
|
|
1231
|
+
let jwkString;
|
|
1232
|
+
if (unwrapAlgorithm.name === 'AES-KW') {
|
|
1233
|
+
// Find the null terminator (if present) to get the original string
|
|
1234
|
+
const nullIndex = buffer.indexOf(0);
|
|
1235
|
+
if (nullIndex !== -1) {
|
|
1236
|
+
jwkString = buffer.toString('utf8', 0, nullIndex);
|
|
1237
|
+
} else {
|
|
1238
|
+
// No null terminator, try to parse the whole buffer
|
|
1239
|
+
jwkString = buffer.toString('utf8').trim();
|
|
1240
|
+
}
|
|
1241
|
+
} else {
|
|
1242
|
+
jwkString = buffer.toString('utf8');
|
|
1243
|
+
}
|
|
1244
|
+
keyData = JSON.parse(jwkString);
|
|
1245
|
+
} else {
|
|
1246
|
+
keyData = decrypted;
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
// Step 3: Import the key
|
|
1250
|
+
return this.importKey(format, keyData, unwrappedKeyAlgorithm, extractable, keyUsages);
|
|
1251
|
+
}
|
|
979
1252
|
async generateKey(algorithm, extractable, keyUsages) {
|
|
980
1253
|
algorithm = normalizeAlgorithm(algorithm, 'generateKey');
|
|
981
1254
|
let result;
|
|
@@ -1002,6 +1275,18 @@ export class Subtle {
|
|
|
1002
1275
|
case 'AES-KW':
|
|
1003
1276
|
result = await aesGenerateKey(algorithm, extractable, keyUsages);
|
|
1004
1277
|
break;
|
|
1278
|
+
case 'ChaCha20-Poly1305':
|
|
1279
|
+
{
|
|
1280
|
+
const length = algorithm.length ?? 256;
|
|
1281
|
+
if (length !== 256) {
|
|
1282
|
+
throw lazyDOMException('ChaCha20-Poly1305 only supports 256-bit keys', 'NotSupportedError');
|
|
1283
|
+
}
|
|
1284
|
+
result = await aesGenerateKey({
|
|
1285
|
+
name: 'ChaCha20-Poly1305',
|
|
1286
|
+
length: 256
|
|
1287
|
+
}, extractable, keyUsages);
|
|
1288
|
+
break;
|
|
1289
|
+
}
|
|
1005
1290
|
case 'HMAC':
|
|
1006
1291
|
result = await hmacGenerateKey(algorithm, extractable, keyUsages);
|
|
1007
1292
|
break;
|
|
@@ -1019,6 +1304,12 @@ export class Subtle {
|
|
|
1019
1304
|
result = await mldsa_generateKeyPairWebCrypto(algorithm.name, extractable, keyUsages);
|
|
1020
1305
|
checkCryptoKeyPairUsages(result);
|
|
1021
1306
|
break;
|
|
1307
|
+
case 'X25519':
|
|
1308
|
+
// Fall through
|
|
1309
|
+
case 'X448':
|
|
1310
|
+
result = await x_generateKeyPairWebCrypto(algorithm.name.toLowerCase(), extractable, keyUsages);
|
|
1311
|
+
checkCryptoKeyPairUsages(result);
|
|
1312
|
+
break;
|
|
1022
1313
|
default:
|
|
1023
1314
|
throw new Error(`'subtle.generateKey()' is not implemented for ${algorithm.name}.
|
|
1024
1315
|
Unrecognized algorithm name`);
|
|
@@ -1051,11 +1342,20 @@ export class Subtle {
|
|
|
1051
1342
|
case 'AES-GCM':
|
|
1052
1343
|
// Fall through
|
|
1053
1344
|
case 'AES-KW':
|
|
1345
|
+
// Fall through
|
|
1346
|
+
case 'ChaCha20-Poly1305':
|
|
1054
1347
|
result = await aesImportKey(normalizedAlgorithm, format, data, extractable, keyUsages);
|
|
1055
1348
|
break;
|
|
1056
1349
|
case 'PBKDF2':
|
|
1057
1350
|
result = await importGenericSecretKey(normalizedAlgorithm, format, data, extractable, keyUsages);
|
|
1058
1351
|
break;
|
|
1352
|
+
case 'HKDF':
|
|
1353
|
+
result = await hkdfImportKey(format, data, normalizedAlgorithm, extractable, keyUsages);
|
|
1354
|
+
break;
|
|
1355
|
+
case 'X25519':
|
|
1356
|
+
// Fall through
|
|
1357
|
+
case 'X448':
|
|
1358
|
+
// Fall through
|
|
1059
1359
|
case 'Ed25519':
|
|
1060
1360
|
// Fall through
|
|
1061
1361
|
case 'Ed448':
|
|
@@ -1077,11 +1377,104 @@ export class Subtle {
|
|
|
1077
1377
|
return result;
|
|
1078
1378
|
}
|
|
1079
1379
|
async sign(algorithm, key, data) {
|
|
1080
|
-
|
|
1380
|
+
const normalizedAlgorithm = normalizeAlgorithm(algorithm, 'sign');
|
|
1381
|
+
if (normalizedAlgorithm.name === 'HMAC') {
|
|
1382
|
+
// Validate key usage
|
|
1383
|
+
if (!key.usages.includes('sign')) {
|
|
1384
|
+
throw lazyDOMException('Key does not have sign usage', 'InvalidAccessError');
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
// Get hash algorithm from key or algorithm params
|
|
1388
|
+
// Hash can be either a string or an object with name property
|
|
1389
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1390
|
+
const alg = normalizedAlgorithm;
|
|
1391
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1392
|
+
const keyAlg = key.algorithm;
|
|
1393
|
+
let hashAlgorithm = 'SHA-256';
|
|
1394
|
+
if (typeof alg.hash === 'string') {
|
|
1395
|
+
hashAlgorithm = alg.hash;
|
|
1396
|
+
} else if (alg.hash?.name) {
|
|
1397
|
+
hashAlgorithm = alg.hash.name;
|
|
1398
|
+
} else if (typeof keyAlg.hash === 'string') {
|
|
1399
|
+
hashAlgorithm = keyAlg.hash;
|
|
1400
|
+
} else if (keyAlg.hash?.name) {
|
|
1401
|
+
hashAlgorithm = keyAlg.hash.name;
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
// Create HMAC and sign
|
|
1405
|
+
const keyData = key.keyObject.export();
|
|
1406
|
+
const hmac = createHmac(hashAlgorithm, keyData);
|
|
1407
|
+
hmac.update(bufferLikeToArrayBuffer(data));
|
|
1408
|
+
return bufferLikeToArrayBuffer(hmac.digest());
|
|
1409
|
+
}
|
|
1410
|
+
return signVerify(normalizedAlgorithm, key, data);
|
|
1081
1411
|
}
|
|
1082
1412
|
async verify(algorithm, key, signature, data) {
|
|
1083
|
-
|
|
1413
|
+
const normalizedAlgorithm = normalizeAlgorithm(algorithm, 'verify');
|
|
1414
|
+
if (normalizedAlgorithm.name === 'HMAC') {
|
|
1415
|
+
// Validate key usage
|
|
1416
|
+
if (!key.usages.includes('verify')) {
|
|
1417
|
+
throw lazyDOMException('Key does not have verify usage', 'InvalidAccessError');
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
// Get hash algorithm
|
|
1421
|
+
// Hash can be either a string or an object with name property
|
|
1422
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1423
|
+
const alg = normalizedAlgorithm;
|
|
1424
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1425
|
+
const keyAlg = key.algorithm;
|
|
1426
|
+
let hashAlgorithm = 'SHA-256';
|
|
1427
|
+
if (typeof alg.hash === 'string') {
|
|
1428
|
+
hashAlgorithm = alg.hash;
|
|
1429
|
+
} else if (alg.hash?.name) {
|
|
1430
|
+
hashAlgorithm = alg.hash.name;
|
|
1431
|
+
} else if (typeof keyAlg.hash === 'string') {
|
|
1432
|
+
hashAlgorithm = keyAlg.hash;
|
|
1433
|
+
} else if (keyAlg.hash?.name) {
|
|
1434
|
+
hashAlgorithm = keyAlg.hash.name;
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
// Create HMAC and compute expected signature
|
|
1438
|
+
const keyData = key.keyObject.export();
|
|
1439
|
+
const hmac = createHmac(hashAlgorithm, keyData);
|
|
1440
|
+
const dataBuffer = bufferLikeToArrayBuffer(data);
|
|
1441
|
+
hmac.update(dataBuffer);
|
|
1442
|
+
const expectedDigest = hmac.digest();
|
|
1443
|
+
const expected = new Uint8Array(bufferLikeToArrayBuffer(expectedDigest));
|
|
1444
|
+
|
|
1445
|
+
// Constant-time comparison
|
|
1446
|
+
const signatureArray = new Uint8Array(bufferLikeToArrayBuffer(signature));
|
|
1447
|
+
if (expected.length !== signatureArray.length) {
|
|
1448
|
+
return false;
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
// Manual constant-time comparison
|
|
1452
|
+
let result = 0;
|
|
1453
|
+
for (let i = 0; i < expected.length; i++) {
|
|
1454
|
+
result |= expected[i] ^ signatureArray[i];
|
|
1455
|
+
}
|
|
1456
|
+
return result === 0;
|
|
1457
|
+
}
|
|
1458
|
+
return signVerify(normalizedAlgorithm, key, data, signature);
|
|
1084
1459
|
}
|
|
1085
1460
|
}
|
|
1086
1461
|
export const subtle = new Subtle();
|
|
1462
|
+
function getKeyLength(algorithm) {
|
|
1463
|
+
const name = algorithm.name;
|
|
1464
|
+
switch (name) {
|
|
1465
|
+
case 'AES-CTR':
|
|
1466
|
+
case 'AES-CBC':
|
|
1467
|
+
case 'AES-GCM':
|
|
1468
|
+
case 'AES-KW':
|
|
1469
|
+
case 'ChaCha20-Poly1305':
|
|
1470
|
+
return algorithm.length || 256;
|
|
1471
|
+
case 'HMAC':
|
|
1472
|
+
{
|
|
1473
|
+
const hmacAlg = algorithm;
|
|
1474
|
+
return hmacAlg.length || 256;
|
|
1475
|
+
}
|
|
1476
|
+
default:
|
|
1477
|
+
throw lazyDOMException(`Cannot determine key length for ${name}`, 'NotSupportedError');
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1087
1480
|
//# sourceMappingURL=subtle.js.map
|