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.
Files changed (52) hide show
  1. package/QuickCrypto.podspec +11 -1
  2. package/android/CMakeLists.txt +2 -0
  3. package/cpp/cipher/GCMCipher.cpp +68 -0
  4. package/cpp/cipher/GCMCipher.hpp +14 -0
  5. package/cpp/cipher/HybridCipherFactory.hpp +8 -0
  6. package/cpp/cipher/HybridRsaCipher.cpp +229 -0
  7. package/cpp/cipher/HybridRsaCipher.hpp +23 -0
  8. package/cpp/keys/HybridKeyObjectHandle.cpp +508 -9
  9. package/cpp/keys/HybridKeyObjectHandle.hpp +10 -1
  10. package/cpp/utils/base64.h +309 -0
  11. package/lib/commonjs/ec.js +85 -17
  12. package/lib/commonjs/ec.js.map +1 -1
  13. package/lib/commonjs/specs/rsaCipher.nitro.js +6 -0
  14. package/lib/commonjs/specs/rsaCipher.nitro.js.map +1 -0
  15. package/lib/commonjs/subtle.js +420 -17
  16. package/lib/commonjs/subtle.js.map +1 -1
  17. package/lib/commonjs/utils/conversion.js +1 -1
  18. package/lib/commonjs/utils/conversion.js.map +1 -1
  19. package/lib/module/ec.js +86 -18
  20. package/lib/module/ec.js.map +1 -1
  21. package/lib/module/specs/rsaCipher.nitro.js +4 -0
  22. package/lib/module/specs/rsaCipher.nitro.js.map +1 -0
  23. package/lib/module/subtle.js +421 -18
  24. package/lib/module/subtle.js.map +1 -1
  25. package/lib/module/utils/conversion.js +1 -1
  26. package/lib/module/utils/conversion.js.map +1 -1
  27. package/lib/tsconfig.tsbuildinfo +1 -1
  28. package/lib/typescript/ec.d.ts.map +1 -1
  29. package/lib/typescript/specs/keyObjectHandle.nitro.d.ts +1 -0
  30. package/lib/typescript/specs/keyObjectHandle.nitro.d.ts.map +1 -1
  31. package/lib/typescript/specs/rsaCipher.nitro.d.ts +26 -0
  32. package/lib/typescript/specs/rsaCipher.nitro.d.ts.map +1 -0
  33. package/lib/typescript/subtle.d.ts.map +1 -1
  34. package/lib/typescript/utils/conversion.d.ts.map +1 -1
  35. package/lib/typescript/utils/types.d.ts +1 -1
  36. package/lib/typescript/utils/types.d.ts.map +1 -1
  37. package/nitrogen/generated/android/QuickCrypto+autolinking.cmake +1 -0
  38. package/nitrogen/generated/android/QuickCryptoOnLoad.cpp +10 -0
  39. package/nitrogen/generated/ios/QuickCryptoAutolinking.mm +10 -0
  40. package/nitrogen/generated/shared/c++/AsymmetricKeyType.hpp +104 -0
  41. package/nitrogen/generated/shared/c++/HybridKeyObjectHandleSpec.cpp +1 -0
  42. package/nitrogen/generated/shared/c++/HybridKeyObjectHandleSpec.hpp +5 -4
  43. package/nitrogen/generated/shared/c++/HybridRsaCipherSpec.cpp +22 -0
  44. package/nitrogen/generated/shared/c++/HybridRsaCipherSpec.hpp +70 -0
  45. package/package.json +1 -1
  46. package/src/ec.ts +122 -20
  47. package/src/specs/keyObjectHandle.nitro.ts +1 -0
  48. package/src/specs/rsaCipher.nitro.ts +35 -0
  49. package/src/subtle.ts +550 -45
  50. package/src/utils/conversion.ts +3 -1
  51. package/src/utils/types.ts +6 -6
  52. 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 { CryptoKey } from './keys';
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
- _key: CryptoKey,
67
- _format: KWebCryptoKeyFormat,
68
- ): ArrayBuffer {
69
- throw new Error('ecExportKey not implemented');
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
- _key: CryptoKey,
74
- _format: KWebCryptoKeyFormat,
97
+ key: CryptoKey,
98
+ format: KWebCryptoKeyFormat,
75
99
  ): ArrayBuffer {
76
- throw new Error('rsaExportKey not implemented');
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
- _mode: CipherOrWrapMode,
81
- _key: CryptoKey,
82
- _data: ArrayBuffer,
83
- _algorithm: EncryptDecryptParams,
115
+ async function rsaCipher(
116
+ mode: CipherOrWrapMode,
117
+ key: CryptoKey,
118
+ data: ArrayBuffer,
119
+ algorithm: EncryptDecryptParams,
84
120
  ): Promise<ArrayBuffer> {
85
- throw new Error('rsaCipher not implemented');
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
- _mode: CipherOrWrapMode,
90
- _key: CryptoKey,
91
- _data: ArrayBuffer,
92
- _algorithm: EncryptDecryptParams,
164
+ async function aesCipher(
165
+ mode: CipherOrWrapMode,
166
+ key: CryptoKey,
167
+ data: ArrayBuffer,
168
+ algorithm: EncryptDecryptParams,
93
169
  ): Promise<ArrayBuffer> {
94
- throw new Error('aesCipher not implemented');
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
- _algorithm: AesKeyGenParams,
99
- _extractable: boolean,
100
- _keyUsages: KeyUsage[],
363
+ algorithm: AesKeyGenParams,
364
+ extractable: boolean,
365
+ keyUsages: KeyUsage[],
101
366
  ): Promise<CryptoKey> {
102
- throw new Error('aesGenerateKey not implemented');
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
- _format: ImportFormat,
107
- _data: BufferLike | JWK,
108
- _algorithm: SubtleAlgorithm,
109
- _extractable: boolean,
110
- _keyUsages: KeyUsage[],
407
+ format: ImportFormat,
408
+ data: BufferLike | JWK,
409
+ algorithm: SubtleAlgorithm,
410
+ extractable: boolean,
411
+ keyUsages: KeyUsage[],
111
412
  ): CryptoKey {
112
- throw new Error('rsaImportKey not implemented');
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
- _algorithm: SubtleAlgorithm,
117
- _format: ImportFormat,
118
- _data: BufferLike | JWK,
119
- _extractable: boolean,
120
- _keyUsages: KeyUsage[],
497
+ algorithm: SubtleAlgorithm,
498
+ format: ImportFormat,
499
+ data: BufferLike | JWK,
500
+ extractable: boolean,
501
+ keyUsages: KeyUsage[],
121
502
  ): Promise<CryptoKey> {
122
- throw new Error('hmacImportKey not implemented');
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
- _algorithm: SubtleAlgorithm,
127
- _format: ImportFormat,
128
- _data: BufferLike | JWK,
129
- _extractable: boolean,
130
- _keyUsages: KeyUsage[],
562
+ algorithm: SubtleAlgorithm,
563
+ format: ImportFormat,
564
+ data: BufferLike | JWK,
565
+ extractable: boolean,
566
+ keyUsages: KeyUsage[],
131
567
  ): Promise<CryptoKey> {
132
- throw new Error('aesImportKey not implemented');
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
- return key.keyObject.export();
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(
@@ -138,7 +138,9 @@ export function binaryLikeToArrayBuffer(
138
138
  return input.handle.exportKey();
139
139
  }
140
140
 
141
- throw new Error('input could not be converted to ArrayBuffer');
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') {