react-native-quick-crypto 1.0.0-beta.21 → 1.0.0-beta.22
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 +11 -1
- package/android/CMakeLists.txt +2 -0
- package/cpp/cipher/GCMCipher.cpp +68 -0
- package/cpp/cipher/GCMCipher.hpp +14 -0
- package/cpp/cipher/HybridCipherFactory.hpp +8 -0
- package/cpp/cipher/HybridRsaCipher.cpp +229 -0
- package/cpp/cipher/HybridRsaCipher.hpp +23 -0
- package/cpp/keys/HybridKeyObjectHandle.cpp +508 -9
- package/cpp/keys/HybridKeyObjectHandle.hpp +10 -1
- package/cpp/utils/base64.h +309 -0
- package/lib/commonjs/ec.js +85 -17
- package/lib/commonjs/ec.js.map +1 -1
- package/lib/commonjs/specs/rsaCipher.nitro.js +6 -0
- package/lib/commonjs/specs/rsaCipher.nitro.js.map +1 -0
- package/lib/commonjs/subtle.js +420 -17
- package/lib/commonjs/subtle.js.map +1 -1
- package/lib/commonjs/utils/conversion.js +1 -1
- package/lib/commonjs/utils/conversion.js.map +1 -1
- package/lib/module/ec.js +86 -18
- package/lib/module/ec.js.map +1 -1
- package/lib/module/specs/rsaCipher.nitro.js +4 -0
- package/lib/module/specs/rsaCipher.nitro.js.map +1 -0
- package/lib/module/subtle.js +421 -18
- package/lib/module/subtle.js.map +1 -1
- package/lib/module/utils/conversion.js +1 -1
- package/lib/module/utils/conversion.js.map +1 -1
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/lib/typescript/ec.d.ts.map +1 -1
- package/lib/typescript/specs/keyObjectHandle.nitro.d.ts +1 -0
- package/lib/typescript/specs/keyObjectHandle.nitro.d.ts.map +1 -1
- package/lib/typescript/specs/rsaCipher.nitro.d.ts +26 -0
- package/lib/typescript/specs/rsaCipher.nitro.d.ts.map +1 -0
- package/lib/typescript/subtle.d.ts.map +1 -1
- package/lib/typescript/utils/conversion.d.ts.map +1 -1
- package/lib/typescript/utils/types.d.ts +1 -1
- 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 +104 -0
- package/nitrogen/generated/shared/c++/HybridKeyObjectHandleSpec.cpp +1 -0
- package/nitrogen/generated/shared/c++/HybridKeyObjectHandleSpec.hpp +5 -4
- package/nitrogen/generated/shared/c++/HybridRsaCipherSpec.cpp +22 -0
- package/nitrogen/generated/shared/c++/HybridRsaCipherSpec.hpp +70 -0
- package/package.json +1 -1
- package/src/ec.ts +122 -20
- package/src/specs/keyObjectHandle.nitro.ts +1 -0
- package/src/specs/rsaCipher.nitro.ts +35 -0
- package/src/subtle.ts +550 -45
- package/src/utils/conversion.ts +3 -1
- package/src/utils/types.ts +6 -6
- package/nitrogen/generated/shared/c++/CFRGKeyPairType.hpp +0 -84
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { HybridObject } from 'react-native-nitro-modules';
|
|
2
|
+
import type { KeyObjectHandle } from './keyObjectHandle.nitro';
|
|
3
|
+
|
|
4
|
+
export interface RsaCipher
|
|
5
|
+
extends HybridObject<{ ios: 'c++'; android: 'c++' }> {
|
|
6
|
+
/**
|
|
7
|
+
* Encrypt data using RSA-OAEP
|
|
8
|
+
* @param keyHandle The public key handle
|
|
9
|
+
* @param data The data to encrypt
|
|
10
|
+
* @param hashAlgorithm The hash algorithm (e.g., 'SHA-256')
|
|
11
|
+
* @param label Optional label for OAEP
|
|
12
|
+
* @returns Encrypted data
|
|
13
|
+
*/
|
|
14
|
+
encrypt(
|
|
15
|
+
keyHandle: KeyObjectHandle,
|
|
16
|
+
data: ArrayBuffer,
|
|
17
|
+
hashAlgorithm: string,
|
|
18
|
+
label?: ArrayBuffer,
|
|
19
|
+
): ArrayBuffer;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Decrypt data using RSA-OAEP
|
|
23
|
+
* @param keyHandle The private key handle
|
|
24
|
+
* @param data The data to decrypt
|
|
25
|
+
* @param hashAlgorithm The hash algorithm (e.g., 'SHA-256')
|
|
26
|
+
* @param label Optional label for OAEP
|
|
27
|
+
* @returns Decrypted data
|
|
28
|
+
*/
|
|
29
|
+
decrypt(
|
|
30
|
+
keyHandle: KeyObjectHandle,
|
|
31
|
+
data: ArrayBuffer,
|
|
32
|
+
hashAlgorithm: string,
|
|
33
|
+
label?: ArrayBuffer,
|
|
34
|
+
): ArrayBuffer;
|
|
35
|
+
}
|
package/src/subtle.ts
CHANGED
|
@@ -11,8 +11,18 @@ import type {
|
|
|
11
11
|
AesKeyGenParams,
|
|
12
12
|
EncryptDecryptParams,
|
|
13
13
|
Operation,
|
|
14
|
+
AesCtrParams,
|
|
15
|
+
AesCbcParams,
|
|
16
|
+
AesGcmParams,
|
|
17
|
+
RsaOaepParams,
|
|
14
18
|
} from './utils';
|
|
15
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
CryptoKey,
|
|
21
|
+
KeyObject,
|
|
22
|
+
PublicKeyObject,
|
|
23
|
+
PrivateKeyObject,
|
|
24
|
+
SecretKeyObject,
|
|
25
|
+
} from './keys';
|
|
16
26
|
import type { CryptoKeyPair } from './utils/types';
|
|
17
27
|
import { bufferLikeToArrayBuffer } from './utils/conversion';
|
|
18
28
|
import { lazyDOMException } from './utils/errors';
|
|
@@ -20,9 +30,14 @@ import { normalizeHashName, HashContext } from './utils/hashnames';
|
|
|
20
30
|
import { validateMaxBufferLength } from './utils/validation';
|
|
21
31
|
import { asyncDigest } from './hash';
|
|
22
32
|
import { createSecretKey } from './keys';
|
|
33
|
+
import { NitroModules } from 'react-native-nitro-modules';
|
|
34
|
+
import type { KeyObjectHandle } from './specs/keyObjectHandle.nitro';
|
|
35
|
+
import type { RsaCipher } from './specs/rsaCipher.nitro';
|
|
36
|
+
import type { CipherFactory } from './specs/cipher.nitro';
|
|
23
37
|
import { pbkdf2DeriveBits } from './pbkdf2';
|
|
24
38
|
import { ecImportKey, ecdsaSignVerify, ec_generateKeyPair } from './ec';
|
|
25
39
|
import { rsa_generateKeyPair } from './rsa';
|
|
40
|
+
import { getRandomValues } from './random';
|
|
26
41
|
// import { pbkdf2DeriveBits } from './pbkdf2';
|
|
27
42
|
// import { aesCipher, aesGenerateKey, aesImportKey, getAlgorithmName } from './aes';
|
|
28
43
|
// import { rsaCipher, rsaExportKey, rsaImportKey, rsaKeyGenerate } from './rsa';
|
|
@@ -62,74 +77,558 @@ function getAlgorithmName(name: string, length: number): string {
|
|
|
62
77
|
}
|
|
63
78
|
|
|
64
79
|
// Placeholder implementations for missing functions
|
|
65
|
-
function ecExportKey(
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
)
|
|
69
|
-
|
|
80
|
+
function ecExportKey(key: CryptoKey, format: KWebCryptoKeyFormat): ArrayBuffer {
|
|
81
|
+
const keyObject = key.keyObject;
|
|
82
|
+
|
|
83
|
+
if (format === KWebCryptoKeyFormat.kWebCryptoKeyFormatSPKI) {
|
|
84
|
+
// Export public key in SPKI format
|
|
85
|
+
const exported = keyObject.export({ format: 'der', type: 'spki' });
|
|
86
|
+
return bufferLikeToArrayBuffer(exported);
|
|
87
|
+
} else if (format === KWebCryptoKeyFormat.kWebCryptoKeyFormatPKCS8) {
|
|
88
|
+
// Export private key in PKCS8 format
|
|
89
|
+
const exported = keyObject.export({ format: 'der', type: 'pkcs8' });
|
|
90
|
+
return bufferLikeToArrayBuffer(exported);
|
|
91
|
+
} else {
|
|
92
|
+
throw new Error(`Unsupported EC export format: ${format}`);
|
|
93
|
+
}
|
|
70
94
|
}
|
|
71
95
|
|
|
72
96
|
function rsaExportKey(
|
|
73
|
-
|
|
74
|
-
|
|
97
|
+
key: CryptoKey,
|
|
98
|
+
format: KWebCryptoKeyFormat,
|
|
75
99
|
): ArrayBuffer {
|
|
76
|
-
|
|
100
|
+
const keyObject = key.keyObject;
|
|
101
|
+
|
|
102
|
+
if (format === KWebCryptoKeyFormat.kWebCryptoKeyFormatSPKI) {
|
|
103
|
+
// Export public key in SPKI format
|
|
104
|
+
const exported = keyObject.export({ format: 'der', type: 'spki' });
|
|
105
|
+
return bufferLikeToArrayBuffer(exported);
|
|
106
|
+
} else if (format === KWebCryptoKeyFormat.kWebCryptoKeyFormatPKCS8) {
|
|
107
|
+
// Export private key in PKCS8 format
|
|
108
|
+
const exported = keyObject.export({ format: 'der', type: 'pkcs8' });
|
|
109
|
+
return bufferLikeToArrayBuffer(exported);
|
|
110
|
+
} else {
|
|
111
|
+
throw new Error(`Unsupported RSA export format: ${format}`);
|
|
112
|
+
}
|
|
77
113
|
}
|
|
78
114
|
|
|
79
|
-
function rsaCipher(
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
115
|
+
async function rsaCipher(
|
|
116
|
+
mode: CipherOrWrapMode,
|
|
117
|
+
key: CryptoKey,
|
|
118
|
+
data: ArrayBuffer,
|
|
119
|
+
algorithm: EncryptDecryptParams,
|
|
84
120
|
): Promise<ArrayBuffer> {
|
|
85
|
-
|
|
121
|
+
const rsaParams = algorithm as RsaOaepParams;
|
|
122
|
+
|
|
123
|
+
// Validate key type matches operation
|
|
124
|
+
const expectedType =
|
|
125
|
+
mode === CipherOrWrapMode.kWebCryptoCipherEncrypt ? 'public' : 'private';
|
|
126
|
+
if (key.type !== expectedType) {
|
|
127
|
+
throw lazyDOMException(
|
|
128
|
+
'The requested operation is not valid for the provided key',
|
|
129
|
+
'InvalidAccessError',
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Get hash algorithm from key
|
|
134
|
+
const hashAlgorithm = normalizeHashName(key.algorithm.hash);
|
|
135
|
+
|
|
136
|
+
// Prepare label (optional)
|
|
137
|
+
const label = rsaParams.label
|
|
138
|
+
? bufferLikeToArrayBuffer(rsaParams.label)
|
|
139
|
+
: undefined;
|
|
140
|
+
|
|
141
|
+
// Create RSA cipher instance
|
|
142
|
+
const rsaCipherModule =
|
|
143
|
+
NitroModules.createHybridObject<RsaCipher>('RsaCipher');
|
|
144
|
+
|
|
145
|
+
if (mode === CipherOrWrapMode.kWebCryptoCipherEncrypt) {
|
|
146
|
+
// Encrypt with public key
|
|
147
|
+
return rsaCipherModule.encrypt(
|
|
148
|
+
key.keyObject.handle,
|
|
149
|
+
data,
|
|
150
|
+
hashAlgorithm,
|
|
151
|
+
label,
|
|
152
|
+
);
|
|
153
|
+
} else {
|
|
154
|
+
// Decrypt with private key
|
|
155
|
+
return rsaCipherModule.decrypt(
|
|
156
|
+
key.keyObject.handle,
|
|
157
|
+
data,
|
|
158
|
+
hashAlgorithm,
|
|
159
|
+
label,
|
|
160
|
+
);
|
|
161
|
+
}
|
|
86
162
|
}
|
|
87
163
|
|
|
88
|
-
function aesCipher(
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
164
|
+
async function aesCipher(
|
|
165
|
+
mode: CipherOrWrapMode,
|
|
166
|
+
key: CryptoKey,
|
|
167
|
+
data: ArrayBuffer,
|
|
168
|
+
algorithm: EncryptDecryptParams,
|
|
93
169
|
): Promise<ArrayBuffer> {
|
|
94
|
-
|
|
170
|
+
const { name } = algorithm;
|
|
171
|
+
|
|
172
|
+
switch (name) {
|
|
173
|
+
case 'AES-CTR':
|
|
174
|
+
return aesCtrCipher(mode, key, data, algorithm as AesCtrParams);
|
|
175
|
+
case 'AES-CBC':
|
|
176
|
+
return aesCbcCipher(mode, key, data, algorithm as AesCbcParams);
|
|
177
|
+
case 'AES-GCM':
|
|
178
|
+
return aesGcmCipher(mode, key, data, algorithm as AesGcmParams);
|
|
179
|
+
default:
|
|
180
|
+
throw lazyDOMException(
|
|
181
|
+
`Unsupported AES algorithm: ${name}`,
|
|
182
|
+
'NotSupportedError',
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function aesCtrCipher(
|
|
188
|
+
mode: CipherOrWrapMode,
|
|
189
|
+
key: CryptoKey,
|
|
190
|
+
data: ArrayBuffer,
|
|
191
|
+
algorithm: AesCtrParams,
|
|
192
|
+
): Promise<ArrayBuffer> {
|
|
193
|
+
// Validate counter and length
|
|
194
|
+
if (!algorithm.counter || algorithm.counter.byteLength !== 16) {
|
|
195
|
+
throw lazyDOMException(
|
|
196
|
+
'AES-CTR algorithm.counter must be 16 bytes',
|
|
197
|
+
'OperationError',
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (algorithm.length < 1 || algorithm.length > 128) {
|
|
202
|
+
throw lazyDOMException(
|
|
203
|
+
'AES-CTR algorithm.length must be between 1 and 128',
|
|
204
|
+
'OperationError',
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Get cipher type based on key length
|
|
209
|
+
const keyLength = (key.algorithm as { length: number }).length;
|
|
210
|
+
const cipherType = `aes-${keyLength}-ctr`;
|
|
211
|
+
|
|
212
|
+
// Create cipher
|
|
213
|
+
const factory =
|
|
214
|
+
NitroModules.createHybridObject<CipherFactory>('CipherFactory');
|
|
215
|
+
const cipher = factory.createCipher({
|
|
216
|
+
isCipher: mode === CipherOrWrapMode.kWebCryptoCipherEncrypt,
|
|
217
|
+
cipherType,
|
|
218
|
+
cipherKey: bufferLikeToArrayBuffer(key.keyObject.export()),
|
|
219
|
+
iv: bufferLikeToArrayBuffer(algorithm.counter),
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// Process data
|
|
223
|
+
const updated = cipher.update(data);
|
|
224
|
+
const final = cipher.final();
|
|
225
|
+
|
|
226
|
+
// Concatenate results
|
|
227
|
+
const result = new Uint8Array(updated.byteLength + final.byteLength);
|
|
228
|
+
result.set(new Uint8Array(updated), 0);
|
|
229
|
+
result.set(new Uint8Array(final), updated.byteLength);
|
|
230
|
+
|
|
231
|
+
return result.buffer;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async function aesCbcCipher(
|
|
235
|
+
mode: CipherOrWrapMode,
|
|
236
|
+
key: CryptoKey,
|
|
237
|
+
data: ArrayBuffer,
|
|
238
|
+
algorithm: AesCbcParams,
|
|
239
|
+
): Promise<ArrayBuffer> {
|
|
240
|
+
// Validate IV
|
|
241
|
+
const iv = bufferLikeToArrayBuffer(algorithm.iv);
|
|
242
|
+
if (iv.byteLength !== 16) {
|
|
243
|
+
throw lazyDOMException(
|
|
244
|
+
'algorithm.iv must contain exactly 16 bytes',
|
|
245
|
+
'OperationError',
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Get cipher type based on key length
|
|
250
|
+
const keyLength = (key.algorithm as { length: number }).length;
|
|
251
|
+
const cipherType = `aes-${keyLength}-cbc`;
|
|
252
|
+
|
|
253
|
+
// Create cipher
|
|
254
|
+
const factory =
|
|
255
|
+
NitroModules.createHybridObject<CipherFactory>('CipherFactory');
|
|
256
|
+
const cipher = factory.createCipher({
|
|
257
|
+
isCipher: mode === CipherOrWrapMode.kWebCryptoCipherEncrypt,
|
|
258
|
+
cipherType,
|
|
259
|
+
cipherKey: bufferLikeToArrayBuffer(key.keyObject.export()),
|
|
260
|
+
iv,
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// Process data
|
|
264
|
+
const updated = cipher.update(data);
|
|
265
|
+
const final = cipher.final();
|
|
266
|
+
|
|
267
|
+
// Concatenate results
|
|
268
|
+
const result = new Uint8Array(updated.byteLength + final.byteLength);
|
|
269
|
+
result.set(new Uint8Array(updated), 0);
|
|
270
|
+
result.set(new Uint8Array(final), updated.byteLength);
|
|
271
|
+
|
|
272
|
+
return result.buffer;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async function aesGcmCipher(
|
|
276
|
+
mode: CipherOrWrapMode,
|
|
277
|
+
key: CryptoKey,
|
|
278
|
+
data: ArrayBuffer,
|
|
279
|
+
algorithm: AesGcmParams,
|
|
280
|
+
): Promise<ArrayBuffer> {
|
|
281
|
+
const { tagLength = 128 } = algorithm;
|
|
282
|
+
|
|
283
|
+
// Validate tag length
|
|
284
|
+
const validTagLengths = [32, 64, 96, 104, 112, 120, 128];
|
|
285
|
+
if (!validTagLengths.includes(tagLength)) {
|
|
286
|
+
throw lazyDOMException(
|
|
287
|
+
`${tagLength} is not a valid AES-GCM tag length`,
|
|
288
|
+
'OperationError',
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const tagByteLength = tagLength / 8;
|
|
293
|
+
|
|
294
|
+
// Get cipher type based on key length
|
|
295
|
+
const keyLength = (key.algorithm as { length: number }).length;
|
|
296
|
+
const cipherType = `aes-${keyLength}-gcm`;
|
|
297
|
+
|
|
298
|
+
// Create cipher
|
|
299
|
+
const factory =
|
|
300
|
+
NitroModules.createHybridObject<CipherFactory>('CipherFactory');
|
|
301
|
+
const cipher = factory.createCipher({
|
|
302
|
+
isCipher: mode === CipherOrWrapMode.kWebCryptoCipherEncrypt,
|
|
303
|
+
cipherType,
|
|
304
|
+
cipherKey: bufferLikeToArrayBuffer(key.keyObject.export()),
|
|
305
|
+
iv: bufferLikeToArrayBuffer(algorithm.iv),
|
|
306
|
+
authTagLen: tagByteLength,
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
let processData: ArrayBuffer;
|
|
310
|
+
let authTag: ArrayBuffer | undefined;
|
|
311
|
+
|
|
312
|
+
if (mode === CipherOrWrapMode.kWebCryptoCipherDecrypt) {
|
|
313
|
+
// For decryption, extract auth tag from end of data
|
|
314
|
+
const dataView = new Uint8Array(data);
|
|
315
|
+
|
|
316
|
+
if (dataView.byteLength < tagByteLength) {
|
|
317
|
+
throw lazyDOMException(
|
|
318
|
+
'The provided data is too small.',
|
|
319
|
+
'OperationError',
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Split data and tag
|
|
324
|
+
const ciphertextLength = dataView.byteLength - tagByteLength;
|
|
325
|
+
processData = dataView.slice(0, ciphertextLength).buffer;
|
|
326
|
+
authTag = dataView.slice(ciphertextLength).buffer;
|
|
327
|
+
|
|
328
|
+
// Set auth tag for verification
|
|
329
|
+
cipher.setAuthTag(authTag);
|
|
330
|
+
} else {
|
|
331
|
+
processData = data;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Set additional authenticated data if provided
|
|
335
|
+
if (algorithm.additionalData) {
|
|
336
|
+
cipher.setAAD(bufferLikeToArrayBuffer(algorithm.additionalData));
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Process data
|
|
340
|
+
const updated = cipher.update(processData);
|
|
341
|
+
const final = cipher.final();
|
|
342
|
+
|
|
343
|
+
if (mode === CipherOrWrapMode.kWebCryptoCipherEncrypt) {
|
|
344
|
+
// For encryption, append auth tag to result
|
|
345
|
+
const tag = cipher.getAuthTag();
|
|
346
|
+
const result = new Uint8Array(
|
|
347
|
+
updated.byteLength + final.byteLength + tag.byteLength,
|
|
348
|
+
);
|
|
349
|
+
result.set(new Uint8Array(updated), 0);
|
|
350
|
+
result.set(new Uint8Array(final), updated.byteLength);
|
|
351
|
+
result.set(new Uint8Array(tag), updated.byteLength + final.byteLength);
|
|
352
|
+
return result.buffer;
|
|
353
|
+
} else {
|
|
354
|
+
// For decryption, just concatenate plaintext
|
|
355
|
+
const result = new Uint8Array(updated.byteLength + final.byteLength);
|
|
356
|
+
result.set(new Uint8Array(updated), 0);
|
|
357
|
+
result.set(new Uint8Array(final), updated.byteLength);
|
|
358
|
+
return result.buffer;
|
|
359
|
+
}
|
|
95
360
|
}
|
|
96
361
|
|
|
97
362
|
async function aesGenerateKey(
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
363
|
+
algorithm: AesKeyGenParams,
|
|
364
|
+
extractable: boolean,
|
|
365
|
+
keyUsages: KeyUsage[],
|
|
101
366
|
): Promise<CryptoKey> {
|
|
102
|
-
|
|
367
|
+
const { length } = algorithm;
|
|
368
|
+
const name = algorithm.name;
|
|
369
|
+
|
|
370
|
+
if (!name) {
|
|
371
|
+
throw lazyDOMException('Algorithm name is required', 'OperationError');
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Validate key length
|
|
375
|
+
if (![128, 192, 256].includes(length)) {
|
|
376
|
+
throw lazyDOMException(
|
|
377
|
+
`Invalid AES key length: ${length}. Must be 128, 192, or 256.`,
|
|
378
|
+
'OperationError',
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Validate usages
|
|
383
|
+
const validUsages: KeyUsage[] = [
|
|
384
|
+
'encrypt',
|
|
385
|
+
'decrypt',
|
|
386
|
+
'wrapKey',
|
|
387
|
+
'unwrapKey',
|
|
388
|
+
];
|
|
389
|
+
if (hasAnyNotIn(keyUsages, validUsages)) {
|
|
390
|
+
throw lazyDOMException(`Unsupported key usage for ${name}`, 'SyntaxError');
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Generate random key bytes
|
|
394
|
+
const keyBytes = new Uint8Array(length / 8);
|
|
395
|
+
getRandomValues(keyBytes);
|
|
396
|
+
|
|
397
|
+
// Create secret key
|
|
398
|
+
const keyObject = createSecretKey(keyBytes);
|
|
399
|
+
|
|
400
|
+
// Construct algorithm object with guaranteed name
|
|
401
|
+
const keyAlgorithm: SubtleAlgorithm = { name, length };
|
|
402
|
+
|
|
403
|
+
return new CryptoKey(keyObject, keyAlgorithm, keyUsages, extractable);
|
|
103
404
|
}
|
|
104
405
|
|
|
105
406
|
function rsaImportKey(
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
407
|
+
format: ImportFormat,
|
|
408
|
+
data: BufferLike | JWK,
|
|
409
|
+
algorithm: SubtleAlgorithm,
|
|
410
|
+
extractable: boolean,
|
|
411
|
+
keyUsages: KeyUsage[],
|
|
111
412
|
): CryptoKey {
|
|
112
|
-
|
|
413
|
+
const { name } = algorithm;
|
|
414
|
+
|
|
415
|
+
// Validate usages
|
|
416
|
+
let checkSet: KeyUsage[];
|
|
417
|
+
switch (name) {
|
|
418
|
+
case 'RSASSA-PKCS1-v1_5':
|
|
419
|
+
case 'RSA-PSS':
|
|
420
|
+
checkSet = ['sign', 'verify'];
|
|
421
|
+
break;
|
|
422
|
+
case 'RSA-OAEP':
|
|
423
|
+
checkSet = ['encrypt', 'decrypt', 'wrapKey', 'unwrapKey'];
|
|
424
|
+
break;
|
|
425
|
+
default:
|
|
426
|
+
throw new Error(`Unsupported RSA algorithm: ${name}`);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (hasAnyNotIn(keyUsages, checkSet)) {
|
|
430
|
+
throw new Error(`Unsupported key usage for ${name}`);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
let keyObject: KeyObject;
|
|
434
|
+
|
|
435
|
+
if (format === 'jwk') {
|
|
436
|
+
const jwk = data as JWK;
|
|
437
|
+
|
|
438
|
+
// Validate JWK
|
|
439
|
+
if (jwk.kty !== 'RSA') {
|
|
440
|
+
throw new Error('Invalid JWK format for RSA key');
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const handle =
|
|
444
|
+
NitroModules.createHybridObject<KeyObjectHandle>('KeyObjectHandle');
|
|
445
|
+
const keyType = handle.initJwk(jwk, undefined);
|
|
446
|
+
|
|
447
|
+
if (keyType === undefined) {
|
|
448
|
+
throw new Error('Failed to import RSA JWK');
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Create the appropriate KeyObject based on type
|
|
452
|
+
if (keyType === 1) {
|
|
453
|
+
keyObject = new PublicKeyObject(handle);
|
|
454
|
+
} else if (keyType === 2) {
|
|
455
|
+
keyObject = new PrivateKeyObject(handle);
|
|
456
|
+
} else {
|
|
457
|
+
throw new Error('Unexpected key type from RSA JWK import');
|
|
458
|
+
}
|
|
459
|
+
} else if (format === 'spki') {
|
|
460
|
+
const keyData = bufferLikeToArrayBuffer(data as BufferLike);
|
|
461
|
+
keyObject = KeyObject.createKeyObject('public', keyData, 'der', 'spki');
|
|
462
|
+
} else if (format === 'pkcs8') {
|
|
463
|
+
const keyData = bufferLikeToArrayBuffer(data as BufferLike);
|
|
464
|
+
keyObject = KeyObject.createKeyObject('private', keyData, 'der', 'pkcs8');
|
|
465
|
+
} else {
|
|
466
|
+
throw new Error(`Unsupported format for RSA import: ${format}`);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Get the modulus length from the key and add it to the algorithm
|
|
470
|
+
const keyDetails = (keyObject as PublicKeyObject | PrivateKeyObject)
|
|
471
|
+
.asymmetricKeyDetails;
|
|
472
|
+
|
|
473
|
+
// Convert publicExponent number to big-endian byte array
|
|
474
|
+
let publicExponentBytes: Uint8Array | undefined;
|
|
475
|
+
if (keyDetails?.publicExponent) {
|
|
476
|
+
const exp = keyDetails.publicExponent;
|
|
477
|
+
// Convert number to big-endian bytes
|
|
478
|
+
const bytes: number[] = [];
|
|
479
|
+
let value = exp;
|
|
480
|
+
while (value > 0) {
|
|
481
|
+
bytes.unshift(value & 0xff);
|
|
482
|
+
value = Math.floor(value / 256);
|
|
483
|
+
}
|
|
484
|
+
publicExponentBytes = new Uint8Array(bytes.length > 0 ? bytes : [0]);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const algorithmWithDetails = {
|
|
488
|
+
...algorithm,
|
|
489
|
+
modulusLength: keyDetails?.modulusLength,
|
|
490
|
+
publicExponent: publicExponentBytes,
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
return new CryptoKey(keyObject, algorithmWithDetails, keyUsages, extractable);
|
|
113
494
|
}
|
|
114
495
|
|
|
115
496
|
async function hmacImportKey(
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
497
|
+
algorithm: SubtleAlgorithm,
|
|
498
|
+
format: ImportFormat,
|
|
499
|
+
data: BufferLike | JWK,
|
|
500
|
+
extractable: boolean,
|
|
501
|
+
keyUsages: KeyUsage[],
|
|
121
502
|
): Promise<CryptoKey> {
|
|
122
|
-
|
|
503
|
+
// Validate usages
|
|
504
|
+
if (hasAnyNotIn(keyUsages, ['sign', 'verify'])) {
|
|
505
|
+
throw new Error('Unsupported key usage for an HMAC key');
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
let keyObject: KeyObject;
|
|
509
|
+
|
|
510
|
+
if (format === 'jwk') {
|
|
511
|
+
const jwk = data as JWK;
|
|
512
|
+
|
|
513
|
+
// Validate JWK
|
|
514
|
+
if (!jwk || typeof jwk !== 'object') {
|
|
515
|
+
throw new Error('Invalid keyData');
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
if (jwk.kty !== 'oct') {
|
|
519
|
+
throw new Error('Invalid JWK format for HMAC key');
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Validate key length if specified
|
|
523
|
+
if (algorithm.length !== undefined) {
|
|
524
|
+
if (!jwk.k) {
|
|
525
|
+
throw new Error('JWK missing key data');
|
|
526
|
+
}
|
|
527
|
+
// Decode to check length
|
|
528
|
+
const decoded = SBuffer.from(jwk.k, 'base64');
|
|
529
|
+
const keyBitLength = decoded.length * 8;
|
|
530
|
+
if (algorithm.length === 0) {
|
|
531
|
+
throw new Error('Zero-length key is not supported');
|
|
532
|
+
}
|
|
533
|
+
if (algorithm.length !== keyBitLength) {
|
|
534
|
+
throw new Error('Invalid key length');
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const handle =
|
|
539
|
+
NitroModules.createHybridObject<KeyObjectHandle>('KeyObjectHandle');
|
|
540
|
+
const keyType = handle.initJwk(jwk, undefined);
|
|
541
|
+
|
|
542
|
+
if (keyType === undefined || keyType !== 0) {
|
|
543
|
+
throw new Error('Failed to import HMAC JWK');
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
keyObject = new SecretKeyObject(handle);
|
|
547
|
+
} else if (format === 'raw') {
|
|
548
|
+
keyObject = createSecretKey(data as BinaryLike);
|
|
549
|
+
} else {
|
|
550
|
+
throw new Error(`Unable to import HMAC key with format ${format}`);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
return new CryptoKey(
|
|
554
|
+
keyObject,
|
|
555
|
+
{ ...algorithm, name: 'HMAC' },
|
|
556
|
+
keyUsages,
|
|
557
|
+
extractable,
|
|
558
|
+
);
|
|
123
559
|
}
|
|
124
560
|
|
|
125
561
|
async function aesImportKey(
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
562
|
+
algorithm: SubtleAlgorithm,
|
|
563
|
+
format: ImportFormat,
|
|
564
|
+
data: BufferLike | JWK,
|
|
565
|
+
extractable: boolean,
|
|
566
|
+
keyUsages: KeyUsage[],
|
|
131
567
|
): Promise<CryptoKey> {
|
|
132
|
-
|
|
568
|
+
const { name, length } = algorithm;
|
|
569
|
+
|
|
570
|
+
// Validate usages
|
|
571
|
+
const validUsages: KeyUsage[] = [
|
|
572
|
+
'encrypt',
|
|
573
|
+
'decrypt',
|
|
574
|
+
'wrapKey',
|
|
575
|
+
'unwrapKey',
|
|
576
|
+
];
|
|
577
|
+
if (hasAnyNotIn(keyUsages, validUsages)) {
|
|
578
|
+
throw new Error(`Unsupported key usage for ${name}`);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
let keyObject: KeyObject;
|
|
582
|
+
let actualLength: number;
|
|
583
|
+
|
|
584
|
+
if (format === 'jwk') {
|
|
585
|
+
const jwk = data as JWK;
|
|
586
|
+
|
|
587
|
+
// Validate JWK
|
|
588
|
+
if (jwk.kty !== 'oct') {
|
|
589
|
+
throw new Error('Invalid JWK format for AES key');
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
const handle =
|
|
593
|
+
NitroModules.createHybridObject<KeyObjectHandle>('KeyObjectHandle');
|
|
594
|
+
const keyType = handle.initJwk(jwk, undefined);
|
|
595
|
+
|
|
596
|
+
if (keyType === undefined || keyType !== 0) {
|
|
597
|
+
throw new Error('Failed to import AES JWK');
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
keyObject = new SecretKeyObject(handle);
|
|
601
|
+
|
|
602
|
+
// Get actual key length from imported key
|
|
603
|
+
const exported = keyObject.export();
|
|
604
|
+
actualLength = exported.byteLength * 8;
|
|
605
|
+
} else if (format === 'raw') {
|
|
606
|
+
const keyData = bufferLikeToArrayBuffer(data as BufferLike);
|
|
607
|
+
actualLength = keyData.byteLength * 8;
|
|
608
|
+
|
|
609
|
+
// Validate key length
|
|
610
|
+
if (![128, 192, 256].includes(actualLength)) {
|
|
611
|
+
throw new Error('Invalid AES key length');
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
keyObject = createSecretKey(keyData);
|
|
615
|
+
} else {
|
|
616
|
+
throw new Error(`Unsupported format for AES import: ${format}`);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// Validate length if specified
|
|
620
|
+
if (length !== undefined && length !== actualLength) {
|
|
621
|
+
throw new Error(
|
|
622
|
+
`Key length mismatch: expected ${length}, got ${actualLength}`,
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
return new CryptoKey(
|
|
627
|
+
keyObject,
|
|
628
|
+
{ name, length: actualLength },
|
|
629
|
+
keyUsages,
|
|
630
|
+
extractable,
|
|
631
|
+
);
|
|
133
632
|
}
|
|
134
633
|
|
|
135
634
|
const exportKeySpki = async (
|
|
@@ -203,8 +702,14 @@ const exportKeyRaw = (key: CryptoKey): ArrayBuffer | unknown => {
|
|
|
203
702
|
// Fall through
|
|
204
703
|
case 'AES-KW':
|
|
205
704
|
// Fall through
|
|
206
|
-
case 'HMAC':
|
|
207
|
-
|
|
705
|
+
case 'HMAC': {
|
|
706
|
+
const exported = key.keyObject.export();
|
|
707
|
+
// Convert Buffer to ArrayBuffer
|
|
708
|
+
return exported.buffer.slice(
|
|
709
|
+
exported.byteOffset,
|
|
710
|
+
exported.byteOffset + exported.byteLength,
|
|
711
|
+
);
|
|
712
|
+
}
|
|
208
713
|
}
|
|
209
714
|
|
|
210
715
|
throw lazyDOMException(
|
package/src/utils/conversion.ts
CHANGED
|
@@ -138,7 +138,9 @@ export function binaryLikeToArrayBuffer(
|
|
|
138
138
|
return input.handle.exportKey();
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
-
throw new Error(
|
|
141
|
+
throw new Error(
|
|
142
|
+
'Invalid argument type for "key". Need ArrayBuffer, TypedArray, KeyObject, CryptoKey, string',
|
|
143
|
+
);
|
|
142
144
|
}
|
|
143
145
|
|
|
144
146
|
export function ab2str(buf: ArrayBuffer, encoding: string = 'hex') {
|