@onekeyfe/react-native-aes-crypto 3.0.33 → 3.0.34

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/AesCrypto.podspec CHANGED
@@ -15,7 +15,18 @@ Pod::Spec.new do |s|
15
15
 
16
16
  s.source_files = "ios/**/*.{h,m,mm,swift}"
17
17
 
18
- s.frameworks = 'Security'
18
+ s.frameworks = ['Security', 'CryptoKit']
19
+ # Workaround: `AesCrypto.mm` imports the generated `AesCrypto-Swift.h`
20
+ # bridging header for the `AesCryptoGcm` Swift helper. With Xcode 15+
21
+ # explicit Swift modules turned on, that bridging header conflicts with
22
+ # the explicit module map for `AesCryptoSpec` (the TurboModule C++ spec),
23
+ # producing module-map redefinition errors at pod build time. Disabling
24
+ # explicit modules on this pod target is the same pattern other RN modules
25
+ # mixing ObjC++ + Swift use; revisit once the Swift toolchain handles the
26
+ # combination natively.
27
+ s.pod_target_xcconfig = {
28
+ 'SWIFT_ENABLE_EXPLICIT_MODULES' => 'NO'
29
+ }
19
30
 
20
31
  install_modules_dependencies(s)
21
32
  end
@@ -9,6 +9,7 @@ import java.security.SecureRandom
9
9
  import java.util.UUID
10
10
  import javax.crypto.Cipher
11
11
  import javax.crypto.Mac
12
+ import javax.crypto.spec.GCMParameterSpec
12
13
  import javax.crypto.spec.IvParameterSpec
13
14
  import javax.crypto.spec.SecretKeySpec
14
15
  import org.spongycastle.crypto.Digest
@@ -31,12 +32,18 @@ class AesCryptoModule(reactContext: ReactApplicationContext) :
31
32
  const val NAME = "AesCrypto"
32
33
  private const val CIPHER_CBC_ALGORITHM = "AES/CBC/PKCS7Padding"
33
34
  private const val CIPHER_CTR_ALGORITHM = "AES/CTR/PKCS5Padding"
35
+ private const val CIPHER_GCM_ALGORITHM = "AES/GCM/NoPadding"
36
+ private const val GCM_AUTH_TAG_LENGTH_BITS = 128
37
+ // AES-GCM nonce length: NIST SP 800-38D recommends 96 bits (12 bytes).
38
+ // CryptoKit on iOS enforces 12 bytes for the default nonce, so locking
39
+ // Android to the same length avoids the platform mismatch where
40
+ // GCMParameterSpec would otherwise accept a non-12-byte nonce that
41
+ // CryptoKit cannot decrypt.
42
+ private const val GCM_NONCE_LENGTH_BYTES = 12
34
43
  private const val HMAC_SHA_256 = "HmacSHA256"
35
44
  private const val HMAC_SHA_512 = "HmacSHA512"
36
45
  private const val KEY_ALGORITHM = "AES"
37
46
 
38
- private val emptyIvSpec = IvParameterSpec(ByteArray(16) { 0x00 })
39
-
40
47
  @JvmStatic
41
48
  fun bytesToHex(bytes: ByteArray): String {
42
49
  val hexArray = "0123456789abcdef".toCharArray()
@@ -48,12 +55,47 @@ class AesCryptoModule(reactContext: ReactApplicationContext) :
48
55
  }
49
56
  return String(hexChars)
50
57
  }
58
+
59
+ // Reject empty strings at every native entry point. Callers MUST
60
+ // supply concrete bytes for every argument; an empty hex string is
61
+ // almost always an upstream bug (forgotten parameter, miswired AAD,
62
+ // truncated input) that we want to fail loudly instead of letting
63
+ // it sneak past the cipher layer.
64
+ private fun requireNonEmpty(value: String?, method: String, paramName: String): String {
65
+ if (value.isNullOrEmpty()) {
66
+ throw IllegalArgumentException("$method: $paramName must not be empty")
67
+ }
68
+ return value
69
+ }
70
+
71
+ // Numeric arguments from the JS side arrive as Double. Coerce them to
72
+ // a positive 32-bit integer, but reject NaN, infinities, fractional
73
+ // values, and anything outside Int range up front — otherwise toInt()
74
+ // would silently truncate (1.5 -> 1) or saturate (NaN -> 0), and the
75
+ // `<= 0` guard alone would let either path slip through into cipher /
76
+ // KDF code that subsequently treats the value as a buffer size.
77
+ private fun requirePositiveInt(value: Double, method: String, paramName: String): Int {
78
+ if (!value.isFinite()
79
+ || value <= 0.0
80
+ || value != Math.floor(value)
81
+ || value > Int.MAX_VALUE.toDouble()
82
+ ) {
83
+ throw IllegalArgumentException(
84
+ "$method: $paramName must be a positive finite integer (<= ${Int.MAX_VALUE})"
85
+ )
86
+ }
87
+ return value.toInt()
88
+ }
51
89
  }
52
90
 
53
91
  override fun getName(): String = NAME
54
92
 
55
93
  override fun encrypt(data: String, key: String, iv: String, algorithm: String, promise: Promise) {
56
94
  try {
95
+ requireNonEmpty(data, "encrypt", "data")
96
+ requireNonEmpty(key, "encrypt", "key")
97
+ requireNonEmpty(iv, "encrypt", "iv")
98
+ requireNonEmpty(algorithm, "encrypt", "algorithm")
57
99
  val cipherAlgorithm = if (algorithm.lowercase().contains("cbc")) CIPHER_CBC_ALGORITHM else CIPHER_CTR_ALGORITHM
58
100
  val result = encryptImpl(data, key, iv, cipherAlgorithm)
59
101
  promise.resolve(result)
@@ -64,6 +106,10 @@ class AesCryptoModule(reactContext: ReactApplicationContext) :
64
106
 
65
107
  override fun decrypt(base64: String, key: String, iv: String, algorithm: String, promise: Promise) {
66
108
  try {
109
+ requireNonEmpty(base64, "decrypt", "ciphertext")
110
+ requireNonEmpty(key, "decrypt", "key")
111
+ requireNonEmpty(iv, "decrypt", "iv")
112
+ requireNonEmpty(algorithm, "decrypt", "algorithm")
67
113
  val cipherAlgorithm = if (algorithm.lowercase().contains("cbc")) CIPHER_CBC_ALGORITHM else CIPHER_CTR_ALGORITHM
68
114
  val result = decryptImpl(base64, key, iv, cipherAlgorithm)
69
115
  promise.resolve(result)
@@ -72,9 +118,45 @@ class AesCryptoModule(reactContext: ReactApplicationContext) :
72
118
  }
73
119
  }
74
120
 
121
+ override fun aesGcmEncrypt(data: String, key: String, nonce: String, aad: String, promise: Promise) {
122
+ try {
123
+ // `data` is intentionally NOT required to be non-empty: AEAD with an
124
+ // empty plaintext is a legitimate operation that yields a 16-byte
125
+ // authentication tag, and CryptoKit on iOS supports it directly.
126
+ requireNonEmpty(key, "aesGcmEncrypt", "key")
127
+ requireNonEmpty(nonce, "aesGcmEncrypt", "nonce")
128
+ requireNonEmpty(aad, "aesGcmEncrypt", "aad")
129
+ val result = aesGcmEncryptImpl(data, key, nonce, aad)
130
+ promise.resolve(result)
131
+ } catch (e: Exception) {
132
+ promise.reject("-1", e.message)
133
+ }
134
+ }
135
+
136
+ override fun aesGcmDecrypt(ciphertextWithTag: String, key: String, nonce: String, aad: String, promise: Promise) {
137
+ try {
138
+ // `ciphertextWithTag` is intentionally NOT validated here for
139
+ // non-emptiness — `aesGcmDecryptImpl` enforces the stronger
140
+ // `>= 16 bytes` (auth-tag length) guard, which already covers the
141
+ // empty-string case.
142
+ requireNonEmpty(key, "aesGcmDecrypt", "key")
143
+ requireNonEmpty(nonce, "aesGcmDecrypt", "nonce")
144
+ requireNonEmpty(aad, "aesGcmDecrypt", "aad")
145
+ val result = aesGcmDecryptImpl(ciphertextWithTag, key, nonce, aad)
146
+ promise.resolve(result)
147
+ } catch (e: Exception) {
148
+ promise.reject("-1", e.message)
149
+ }
150
+ }
151
+
75
152
  override fun pbkdf2(password: String, salt: String, cost: Double, length: Double, algorithm: String, promise: Promise) {
76
153
  try {
77
- val result = pbkdf2Impl(password, salt, cost.toInt(), length.toInt(), algorithm)
154
+ requireNonEmpty(password, "pbkdf2", "password")
155
+ requireNonEmpty(salt, "pbkdf2", "salt")
156
+ requireNonEmpty(algorithm, "pbkdf2", "algorithm")
157
+ val costInt = requirePositiveInt(cost, "pbkdf2", "cost")
158
+ val lengthInt = requirePositiveInt(length, "pbkdf2", "length")
159
+ val result = pbkdf2Impl(password, salt, costInt, lengthInt, algorithm)
78
160
  promise.resolve(result)
79
161
  } catch (e: Exception) {
80
162
  promise.reject("-1", e.message)
@@ -83,6 +165,8 @@ class AesCryptoModule(reactContext: ReactApplicationContext) :
83
165
 
84
166
  override fun hmac256(data: String, key: String, promise: Promise) {
85
167
  try {
168
+ requireNonEmpty(data, "hmac256", "data")
169
+ requireNonEmpty(key, "hmac256", "key")
86
170
  val result = hmacX(data, key, HMAC_SHA_256)
87
171
  promise.resolve(result)
88
172
  } catch (e: Exception) {
@@ -92,6 +176,8 @@ class AesCryptoModule(reactContext: ReactApplicationContext) :
92
176
 
93
177
  override fun hmac512(data: String, key: String, promise: Promise) {
94
178
  try {
179
+ requireNonEmpty(data, "hmac512", "data")
180
+ requireNonEmpty(key, "hmac512", "key")
95
181
  val result = hmacX(data, key, HMAC_SHA_512)
96
182
  promise.resolve(result)
97
183
  } catch (e: Exception) {
@@ -101,6 +187,7 @@ class AesCryptoModule(reactContext: ReactApplicationContext) :
101
187
 
102
188
  override fun sha1(text: String, promise: Promise) {
103
189
  try {
190
+ requireNonEmpty(text, "sha1", "text")
104
191
  val result = shaX(text, "SHA-1")
105
192
  promise.resolve(result)
106
193
  } catch (e: Exception) {
@@ -110,6 +197,7 @@ class AesCryptoModule(reactContext: ReactApplicationContext) :
110
197
 
111
198
  override fun sha256(text: String, promise: Promise) {
112
199
  try {
200
+ requireNonEmpty(text, "sha256", "text")
113
201
  val result = shaX(text, "SHA-256")
114
202
  promise.resolve(result)
115
203
  } catch (e: Exception) {
@@ -119,6 +207,7 @@ class AesCryptoModule(reactContext: ReactApplicationContext) :
119
207
 
120
208
  override fun sha512(text: String, promise: Promise) {
121
209
  try {
210
+ requireNonEmpty(text, "sha512", "text")
122
211
  val result = shaX(text, "SHA-512")
123
212
  promise.resolve(result)
124
213
  } catch (e: Exception) {
@@ -136,7 +225,8 @@ class AesCryptoModule(reactContext: ReactApplicationContext) :
136
225
 
137
226
  override fun randomKey(length: Double, promise: Promise) {
138
227
  try {
139
- val key = ByteArray(length.toInt())
228
+ val keyLength = requirePositiveInt(length, "randomKey", "length")
229
+ val key = ByteArray(keyLength)
140
230
  SecureRandom().nextBytes(key)
141
231
  promise.resolve(bytesToHex(key))
142
232
  } catch (e: Exception) {
@@ -174,27 +264,55 @@ class AesCryptoModule(reactContext: ReactApplicationContext) :
174
264
  return bytesToHex(mac.doFinal(contentData))
175
265
  }
176
266
 
177
- private fun encryptImpl(text: String, hexKey: String, hexIv: String?, algorithm: String): String? {
178
- if (text.isEmpty()) return null
179
-
267
+ private fun encryptImpl(text: String, hexKey: String, hexIv: String, algorithm: String): String {
180
268
  val key = Hex.decode(hexKey)
181
269
  val secretKey = SecretKeySpec(key, KEY_ALGORITHM)
182
270
  val cipher = Cipher.getInstance(algorithm)
183
- val ivSpec = if (hexIv == null || hexIv.isEmpty()) emptyIvSpec else IvParameterSpec(Hex.decode(hexIv))
184
- cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivSpec)
271
+ cipher.init(Cipher.ENCRYPT_MODE, secretKey, IvParameterSpec(Hex.decode(hexIv)))
185
272
  val encrypted = cipher.doFinal(Hex.decode(text))
186
273
  return bytesToHex(encrypted)
187
274
  }
188
275
 
189
- private fun decryptImpl(ciphertext: String, hexKey: String, hexIv: String?, algorithm: String): String? {
190
- if (ciphertext.isEmpty()) return null
191
-
276
+ private fun decryptImpl(ciphertext: String, hexKey: String, hexIv: String, algorithm: String): String {
192
277
  val key = Hex.decode(hexKey)
193
278
  val secretKey = SecretKeySpec(key, KEY_ALGORITHM)
194
279
  val cipher = Cipher.getInstance(algorithm)
195
- val ivSpec = if (hexIv == null || hexIv.isEmpty()) emptyIvSpec else IvParameterSpec(Hex.decode(hexIv))
196
- cipher.init(Cipher.DECRYPT_MODE, secretKey, ivSpec)
280
+ cipher.init(Cipher.DECRYPT_MODE, secretKey, IvParameterSpec(Hex.decode(hexIv)))
197
281
  val decrypted = cipher.doFinal(Hex.decode(ciphertext))
198
282
  return bytesToHex(decrypted)
199
283
  }
284
+
285
+ private fun decodeGcmNonce(hexNonce: String): ByteArray {
286
+ val nonceBytes = Hex.decode(hexNonce)
287
+ if (nonceBytes.size != GCM_NONCE_LENGTH_BYTES) {
288
+ throw IllegalArgumentException(
289
+ "AES-GCM nonce must be exactly $GCM_NONCE_LENGTH_BYTES bytes (got ${nonceBytes.size})"
290
+ )
291
+ }
292
+ return nonceBytes
293
+ }
294
+
295
+ private fun aesGcmEncryptImpl(text: String, hexKey: String, hexNonce: String, aad: String): String {
296
+ val secretKey = SecretKeySpec(Hex.decode(hexKey), KEY_ALGORITHM)
297
+ val cipher = Cipher.getInstance(CIPHER_GCM_ALGORITHM)
298
+ val gcmSpec = GCMParameterSpec(GCM_AUTH_TAG_LENGTH_BITS, decodeGcmNonce(hexNonce))
299
+ cipher.init(Cipher.ENCRYPT_MODE, secretKey, gcmSpec)
300
+ cipher.updateAAD(Hex.decode(aad))
301
+ return bytesToHex(cipher.doFinal(Hex.decode(text)))
302
+ }
303
+
304
+ private fun aesGcmDecryptImpl(ciphertextWithTag: String, hexKey: String, hexNonce: String, aad: String): String {
305
+ val encrypted = Hex.decode(ciphertextWithTag)
306
+ val tagBytes = GCM_AUTH_TAG_LENGTH_BITS / 8
307
+ if (encrypted.size < tagBytes) {
308
+ throw IllegalArgumentException("AES-GCM ciphertext must include the ${tagBytes}-byte authentication tag")
309
+ }
310
+
311
+ val secretKey = SecretKeySpec(Hex.decode(hexKey), KEY_ALGORITHM)
312
+ val cipher = Cipher.getInstance(CIPHER_GCM_ALGORITHM)
313
+ val gcmSpec = GCMParameterSpec(GCM_AUTH_TAG_LENGTH_BITS, decodeGcmNonce(hexNonce))
314
+ cipher.init(Cipher.DECRYPT_MODE, secretKey, gcmSpec)
315
+ cipher.updateAAD(Hex.decode(aad))
316
+ return bytesToHex(cipher.doFinal(encrypted))
317
+ }
200
318
  }
package/ios/AesCrypto.h CHANGED
@@ -1,3 +1,11 @@
1
+ // The TurboModule spec header `<AesCryptoSpec/AesCryptoSpec.h>` is generated
2
+ // as ObjC++ (the `NativeAesCryptoSpecBase` base class uses C++ types from
3
+ // React Native's TurboModule runtime). The `@interface` is wrapped in the
4
+ // same `#ifdef __cplusplus` block as the import because it inherits from
5
+ // `NativeAesCryptoSpecBase`, which only resolves in an ObjC++ translation
6
+ // unit. Pure ObjC `.m` files therefore cannot include this header — that is
7
+ // intentional; AesCrypto.mm is the sole consumer.
8
+ #ifdef __cplusplus
1
9
  #import <AesCryptoSpec/AesCryptoSpec.h>
2
10
 
3
11
  @interface AesCrypto : NativeAesCryptoSpecBase <NativeAesCryptoSpec>
@@ -16,6 +24,20 @@
16
24
  resolve:(RCTPromiseResolveBlock)resolve
17
25
  reject:(RCTPromiseRejectBlock)reject;
18
26
 
27
+ - (void)aesGcmEncrypt:(NSString *)data
28
+ key:(NSString *)key
29
+ nonce:(NSString *)nonce
30
+ aad:(NSString *)aad
31
+ resolve:(RCTPromiseResolveBlock)resolve
32
+ reject:(RCTPromiseRejectBlock)reject;
33
+
34
+ - (void)aesGcmDecrypt:(NSString *)ciphertextWithTag
35
+ key:(NSString *)key
36
+ nonce:(NSString *)nonce
37
+ aad:(NSString *)aad
38
+ resolve:(RCTPromiseResolveBlock)resolve
39
+ reject:(RCTPromiseRejectBlock)reject;
40
+
19
41
  - (void)pbkdf2:(NSString *)password
20
42
  salt:(NSString *)salt
21
43
  cost:(double)cost
@@ -54,3 +76,4 @@
54
76
  reject:(RCTPromiseRejectBlock)reject;
55
77
 
56
78
  @end
79
+ #endif
package/ios/AesCrypto.mm CHANGED
@@ -1,9 +1,48 @@
1
1
  #import "AesCrypto.h"
2
+ #import "AesCrypto-Swift.h"
2
3
  #import <CommonCrypto/CommonCryptor.h>
3
4
  #import <CommonCrypto/CommonDigest.h>
4
5
  #import <CommonCrypto/CommonHMAC.h>
5
6
  #import <CommonCrypto/CommonKeyDerivation.h>
6
7
  #import <Security/Security.h>
8
+ #import <limits.h>
9
+ #import <math.h>
10
+
11
+ // Reject empty string / non-positive numeric arguments at every native
12
+ // entry point. An empty hex string is almost always an upstream bug —
13
+ // forgotten parameter, miswired AAD, truncated input. Fail loudly here
14
+ // instead of silently feeding zero-length bytes into the cipher layer.
15
+ #define ONEKEY_AES_REQUIRE_NON_EMPTY(value, method, paramName, reject) \
16
+ do { \
17
+ if ((value) == nil || [(value) length] == 0) { \
18
+ (reject)(@"-1", \
19
+ [NSString stringWithFormat:@"%@: %@ must not be empty", \
20
+ (method), (paramName)], \
21
+ nil); \
22
+ return; \
23
+ } \
24
+ } while (0)
25
+
26
+ // Numeric arguments arrive as `double` from the JS bridge. A plain `<= 0`
27
+ // check lets NaN, Infinity, and fractional values slip through (NaN compares
28
+ // false against every value, and 1.5 would later cast to 1). This guard
29
+ // rejects all of those — and anything larger than INT_MAX — up front so
30
+ // callers cannot smuggle a malformed numeric into a buffer allocation or
31
+ // KDF iteration count.
32
+ #define ONEKEY_AES_REQUIRE_POSITIVE(value, method, paramName, reject) \
33
+ do { \
34
+ double _onekeyAesValue = (double)(value); \
35
+ if (!isfinite(_onekeyAesValue) \
36
+ || _onekeyAesValue <= 0.0 \
37
+ || _onekeyAesValue != floor(_onekeyAesValue) \
38
+ || _onekeyAesValue > (double)INT_MAX) { \
39
+ (reject)(@"-1", \
40
+ [NSString stringWithFormat:@"%@: %@ must be a positive finite integer (<= %d)", \
41
+ (method), (paramName), INT_MAX], \
42
+ nil); \
43
+ return; \
44
+ } \
45
+ } while (0)
7
46
 
8
47
  // ---------------------------------------------------------------------------
9
48
  // MARK: - Internal helpers
@@ -155,6 +194,10 @@ static NSData *aesCTR(NSString *operation, NSData *inputData, NSString *key, NSS
155
194
  resolve:(RCTPromiseResolveBlock)resolve
156
195
  reject:(RCTPromiseRejectBlock)reject
157
196
  {
197
+ ONEKEY_AES_REQUIRE_NON_EMPTY(data, @"encrypt", @"data", reject);
198
+ ONEKEY_AES_REQUIRE_NON_EMPTY(key, @"encrypt", @"key", reject);
199
+ ONEKEY_AES_REQUIRE_NON_EMPTY(iv, @"encrypt", @"iv", reject);
200
+ ONEKEY_AES_REQUIRE_NON_EMPTY(algorithm, @"encrypt", @"algorithm", reject);
158
201
  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
159
202
  @try {
160
203
  NSData *inputData = fromHex(data);
@@ -165,12 +208,12 @@ static NSData *aesCTR(NSString *operation, NSData *inputData, NSString *key, NSS
165
208
  result = aesCBC(@"encrypt", inputData, key, iv, algorithm);
166
209
  }
167
210
  if (result == nil) {
168
- reject(@"encrypt_fail", @"Encrypt error", nil);
211
+ reject(@"-1", @"Encrypt error", nil);
169
212
  } else {
170
213
  resolve(toHex(result));
171
214
  }
172
215
  } @catch (NSException *exception) {
173
- reject(@"encrypt_fail", exception.reason, nil);
216
+ reject(@"-1", exception.reason, nil);
174
217
  }
175
218
  });
176
219
  }
@@ -184,6 +227,10 @@ static NSData *aesCTR(NSString *operation, NSData *inputData, NSString *key, NSS
184
227
  resolve:(RCTPromiseResolveBlock)resolve
185
228
  reject:(RCTPromiseRejectBlock)reject
186
229
  {
230
+ ONEKEY_AES_REQUIRE_NON_EMPTY(base64, @"decrypt", @"ciphertext", reject);
231
+ ONEKEY_AES_REQUIRE_NON_EMPTY(key, @"decrypt", @"key", reject);
232
+ ONEKEY_AES_REQUIRE_NON_EMPTY(iv, @"decrypt", @"iv", reject);
233
+ ONEKEY_AES_REQUIRE_NON_EMPTY(algorithm, @"decrypt", @"algorithm", reject);
187
234
  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
188
235
  @try {
189
236
  NSData *inputData = fromHex(base64);
@@ -194,12 +241,71 @@ static NSData *aesCTR(NSString *operation, NSData *inputData, NSString *key, NSS
194
241
  result = aesCBC(@"decrypt", inputData, key, iv, algorithm);
195
242
  }
196
243
  if (result == nil) {
197
- reject(@"decrypt_fail", @"Decrypt failed", nil);
244
+ reject(@"-1", @"Decrypt failed", nil);
198
245
  } else {
199
246
  resolve(toHex(result));
200
247
  }
201
248
  } @catch (NSException *exception) {
202
- reject(@"decrypt_fail", exception.reason, nil);
249
+ reject(@"-1", exception.reason, nil);
250
+ }
251
+ });
252
+ }
253
+
254
+ // MARK: - aesGcmEncrypt
255
+
256
+ - (void)aesGcmEncrypt:(NSString *)data
257
+ key:(NSString *)key
258
+ nonce:(NSString *)nonce
259
+ aad:(NSString *)aad
260
+ resolve:(RCTPromiseResolveBlock)resolve
261
+ reject:(RCTPromiseRejectBlock)reject
262
+ {
263
+ // `data` is intentionally NOT required to be non-empty: empty plaintext
264
+ // is a legitimate AEAD operation that yields the 16-byte auth tag.
265
+ ONEKEY_AES_REQUIRE_NON_EMPTY(key, @"aesGcmEncrypt", @"key", reject);
266
+ ONEKEY_AES_REQUIRE_NON_EMPTY(nonce, @"aesGcmEncrypt", @"nonce", reject);
267
+ ONEKEY_AES_REQUIRE_NON_EMPTY(aad, @"aesGcmEncrypt", @"aad", reject);
268
+ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
269
+ @try {
270
+ NSError *error = nil;
271
+ NSString *result = [AesCryptoGcm encryptWithDataHex:data keyHex:key nonceHex:nonce aadHex:aad error:&error];
272
+ if (result == nil) {
273
+ reject(@"-1", error.localizedDescription ?: @"AES-GCM encrypt error", error);
274
+ } else {
275
+ resolve(result);
276
+ }
277
+ } @catch (NSException *exception) {
278
+ reject(@"-1", exception.reason, nil);
279
+ }
280
+ });
281
+ }
282
+
283
+ // MARK: - aesGcmDecrypt
284
+
285
+ - (void)aesGcmDecrypt:(NSString *)ciphertextWithTag
286
+ key:(NSString *)key
287
+ nonce:(NSString *)nonce
288
+ aad:(NSString *)aad
289
+ resolve:(RCTPromiseResolveBlock)resolve
290
+ reject:(RCTPromiseRejectBlock)reject
291
+ {
292
+ // `ciphertextWithTag` is intentionally NOT required to be non-empty
293
+ // here — the Swift impl enforces a stronger `>= 16 bytes` guard which
294
+ // already covers the empty / truncated cases.
295
+ ONEKEY_AES_REQUIRE_NON_EMPTY(key, @"aesGcmDecrypt", @"key", reject);
296
+ ONEKEY_AES_REQUIRE_NON_EMPTY(nonce, @"aesGcmDecrypt", @"nonce", reject);
297
+ ONEKEY_AES_REQUIRE_NON_EMPTY(aad, @"aesGcmDecrypt", @"aad", reject);
298
+ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
299
+ @try {
300
+ NSError *error = nil;
301
+ NSString *result = [AesCryptoGcm decryptWithCiphertextWithTagHex:ciphertextWithTag keyHex:key nonceHex:nonce aadHex:aad error:&error];
302
+ if (result == nil) {
303
+ reject(@"-1", error.localizedDescription ?: @"AES-GCM decrypt failed", error);
304
+ } else {
305
+ resolve(result);
306
+ }
307
+ } @catch (NSException *exception) {
308
+ reject(@"-1", exception.reason, nil);
203
309
  }
204
310
  });
205
311
  }
@@ -214,6 +320,11 @@ static NSData *aesCTR(NSString *operation, NSData *inputData, NSString *key, NSS
214
320
  resolve:(RCTPromiseResolveBlock)resolve
215
321
  reject:(RCTPromiseRejectBlock)reject
216
322
  {
323
+ ONEKEY_AES_REQUIRE_NON_EMPTY(password, @"pbkdf2", @"password", reject);
324
+ ONEKEY_AES_REQUIRE_NON_EMPTY(salt, @"pbkdf2", @"salt", reject);
325
+ ONEKEY_AES_REQUIRE_NON_EMPTY(algorithm, @"pbkdf2", @"algorithm", reject);
326
+ ONEKEY_AES_REQUIRE_POSITIVE(cost, @"pbkdf2", @"cost", reject);
327
+ ONEKEY_AES_REQUIRE_POSITIVE(length, @"pbkdf2", @"length", reject);
217
328
  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
218
329
  @try {
219
330
  NSData *passwordData = fromHex(password);
@@ -241,12 +352,12 @@ static NSData *aesCTR(NSString *operation, NSData *inputData, NSString *key, NSS
241
352
  (uint8_t *)hashKeyData.mutableBytes, hashKeyData.length);
242
353
 
243
354
  if (status == kCCParamError) {
244
- reject(@"keygen_fail", @"Key derivation parameter error", nil);
355
+ reject(@"-1", @"Key derivation parameter error", nil);
245
356
  } else {
246
357
  resolve(toHex(hashKeyData));
247
358
  }
248
359
  } @catch (NSException *exception) {
249
- reject(@"keygen_fail", exception.reason, nil);
360
+ reject(@"-1", exception.reason, nil);
250
361
  }
251
362
  });
252
363
  }
@@ -258,13 +369,15 @@ static NSData *aesCTR(NSString *operation, NSData *inputData, NSString *key, NSS
258
369
  resolve:(RCTPromiseResolveBlock)resolve
259
370
  reject:(RCTPromiseRejectBlock)reject
260
371
  {
372
+ ONEKEY_AES_REQUIRE_NON_EMPTY(base64, @"hmac256", @"data", reject);
373
+ ONEKEY_AES_REQUIRE_NON_EMPTY(key, @"hmac256", @"key", reject);
261
374
  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
262
375
  @try {
263
376
  NSData *keyData = fromHex(key);
264
377
  NSData *inputData = fromHex(base64);
265
378
  void *buffer = malloc(CC_SHA256_DIGEST_LENGTH);
266
379
  if (!buffer) {
267
- reject(@"hmac_fail", @"Memory allocation error", nil);
380
+ reject(@"-1", @"Memory allocation error", nil);
268
381
  return;
269
382
  }
270
383
  CCHmac(kCCHmacAlgSHA256,
@@ -276,7 +389,7 @@ static NSData *aesCTR(NSString *operation, NSData *inputData, NSString *key, NSS
276
389
  freeWhenDone:YES];
277
390
  resolve(toHex(result));
278
391
  } @catch (NSException *exception) {
279
- reject(@"hmac_fail", exception.reason, nil);
392
+ reject(@"-1", exception.reason, nil);
280
393
  }
281
394
  });
282
395
  }
@@ -288,13 +401,15 @@ static NSData *aesCTR(NSString *operation, NSData *inputData, NSString *key, NSS
288
401
  resolve:(RCTPromiseResolveBlock)resolve
289
402
  reject:(RCTPromiseRejectBlock)reject
290
403
  {
404
+ ONEKEY_AES_REQUIRE_NON_EMPTY(base64, @"hmac512", @"data", reject);
405
+ ONEKEY_AES_REQUIRE_NON_EMPTY(key, @"hmac512", @"key", reject);
291
406
  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
292
407
  @try {
293
408
  NSData *keyData = fromHex(key);
294
409
  NSData *inputData = fromHex(base64);
295
410
  void *buffer = malloc(CC_SHA512_DIGEST_LENGTH);
296
411
  if (!buffer) {
297
- reject(@"hmac_fail", @"Memory allocation error", nil);
412
+ reject(@"-1", @"Memory allocation error", nil);
298
413
  return;
299
414
  }
300
415
  CCHmac(kCCHmacAlgSHA512,
@@ -306,7 +421,7 @@ static NSData *aesCTR(NSString *operation, NSData *inputData, NSString *key, NSS
306
421
  freeWhenDone:YES];
307
422
  resolve(toHex(result));
308
423
  } @catch (NSException *exception) {
309
- reject(@"hmac_fail", exception.reason, nil);
424
+ reject(@"-1", exception.reason, nil);
310
425
  }
311
426
  });
312
427
  }
@@ -317,6 +432,7 @@ static NSData *aesCTR(NSString *operation, NSData *inputData, NSString *key, NSS
317
432
  resolve:(RCTPromiseResolveBlock)resolve
318
433
  reject:(RCTPromiseRejectBlock)reject
319
434
  {
435
+ ONEKEY_AES_REQUIRE_NON_EMPTY(text, @"sha1", @"text", reject);
320
436
  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
321
437
  @try {
322
438
  NSData *inputData = fromHex(text);
@@ -324,7 +440,7 @@ static NSData *aesCTR(NSString *operation, NSData *inputData, NSString *key, NSS
324
440
  CC_SHA1((const void *)inputData.bytes, (CC_LONG)inputData.length, (unsigned char *)result.mutableBytes);
325
441
  resolve(toHex(result));
326
442
  } @catch (NSException *exception) {
327
- reject(@"sha1_fail", exception.reason, nil);
443
+ reject(@"-1", exception.reason, nil);
328
444
  }
329
445
  });
330
446
  }
@@ -335,12 +451,13 @@ static NSData *aesCTR(NSString *operation, NSData *inputData, NSString *key, NSS
335
451
  resolve:(RCTPromiseResolveBlock)resolve
336
452
  reject:(RCTPromiseRejectBlock)reject
337
453
  {
454
+ ONEKEY_AES_REQUIRE_NON_EMPTY(text, @"sha256", @"text", reject);
338
455
  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
339
456
  @try {
340
457
  NSData *inputData = fromHex(text);
341
458
  unsigned char *buffer = (unsigned char *)malloc(CC_SHA256_DIGEST_LENGTH);
342
459
  if (!buffer) {
343
- reject(@"sha256_fail", @"Memory allocation error", nil);
460
+ reject(@"-1", @"Memory allocation error", nil);
344
461
  return;
345
462
  }
346
463
  CC_SHA256((const void *)inputData.bytes, (CC_LONG)inputData.length, buffer);
@@ -349,7 +466,7 @@ static NSData *aesCTR(NSString *operation, NSData *inputData, NSString *key, NSS
349
466
  freeWhenDone:YES];
350
467
  resolve(toHex(result));
351
468
  } @catch (NSException *exception) {
352
- reject(@"sha256_fail", exception.reason, nil);
469
+ reject(@"-1", exception.reason, nil);
353
470
  }
354
471
  });
355
472
  }
@@ -360,12 +477,13 @@ static NSData *aesCTR(NSString *operation, NSData *inputData, NSString *key, NSS
360
477
  resolve:(RCTPromiseResolveBlock)resolve
361
478
  reject:(RCTPromiseRejectBlock)reject
362
479
  {
480
+ ONEKEY_AES_REQUIRE_NON_EMPTY(text, @"sha512", @"text", reject);
363
481
  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
364
482
  @try {
365
483
  NSData *inputData = fromHex(text);
366
484
  unsigned char *buffer = (unsigned char *)malloc(CC_SHA512_DIGEST_LENGTH);
367
485
  if (!buffer) {
368
- reject(@"sha512_fail", @"Memory allocation error", nil);
486
+ reject(@"-1", @"Memory allocation error", nil);
369
487
  return;
370
488
  }
371
489
  CC_SHA512((const void *)inputData.bytes, (CC_LONG)inputData.length, buffer);
@@ -374,7 +492,7 @@ static NSData *aesCTR(NSString *operation, NSData *inputData, NSString *key, NSS
374
492
  freeWhenDone:YES];
375
493
  resolve(toHex(result));
376
494
  } @catch (NSException *exception) {
377
- reject(@"sha512_fail", exception.reason, nil);
495
+ reject(@"-1", exception.reason, nil);
378
496
  }
379
497
  });
380
498
  }
@@ -389,7 +507,7 @@ static NSData *aesCTR(NSString *operation, NSData *inputData, NSString *key, NSS
389
507
  NSString *uuid = [[NSUUID UUID] UUIDString];
390
508
  resolve(uuid);
391
509
  } @catch (NSException *exception) {
392
- reject(@"uuid_fail", exception.reason, nil);
510
+ reject(@"-1", exception.reason, nil);
393
511
  }
394
512
  });
395
513
  }
@@ -400,18 +518,19 @@ static NSData *aesCTR(NSString *operation, NSData *inputData, NSString *key, NSS
400
518
  resolve:(RCTPromiseResolveBlock)resolve
401
519
  reject:(RCTPromiseRejectBlock)reject
402
520
  {
521
+ ONEKEY_AES_REQUIRE_POSITIVE(length, @"randomKey", @"length", reject);
403
522
  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
404
523
  @try {
405
524
  NSUInteger len = (NSUInteger)length;
406
525
  NSMutableData *data = [NSMutableData dataWithLength:len];
407
526
  int result = SecRandomCopyBytes(kSecRandomDefault, len, data.mutableBytes);
408
527
  if (result != errSecSuccess) {
409
- reject(@"random_fail", @"Random key generation error", nil);
528
+ reject(@"-1", @"Random key generation error", nil);
410
529
  } else {
411
530
  resolve(toHex(data));
412
531
  }
413
532
  } @catch (NSException *exception) {
414
- reject(@"random_fail", exception.reason, nil);
533
+ reject(@"-1", exception.reason, nil);
415
534
  }
416
535
  });
417
536
  }
@@ -0,0 +1,144 @@
1
+ import CryptoKit
2
+ import Foundation
3
+
4
+ @objc(AesCryptoGcm)
5
+ public final class AesCryptoGcm: NSObject {
6
+ // AES-GCM nonce length: NIST SP 800-38D recommends 96 bits (12 bytes), and
7
+ // `CryptoKit.AES.GCM.Nonce(data:)` is the canonical 12-byte path. Locking
8
+ // the bridge layer to 12 bytes keeps Android (where GCMParameterSpec would
9
+ // otherwise accept other lengths) and iOS byte-compatible.
10
+ private static let gcmNonceLengthBytes = 12
11
+
12
+ @objc(encryptWithDataHex:keyHex:nonceHex:aadHex:error:)
13
+ public static func encrypt(
14
+ dataHex: String,
15
+ keyHex: String,
16
+ nonceHex: String,
17
+ aadHex: String,
18
+ error: NSErrorPointer
19
+ ) -> String? {
20
+ do {
21
+ // `dataHex` is intentionally NOT required to be non-empty: empty
22
+ // plaintext is a legitimate AEAD operation (produces the 16-byte
23
+ // authentication tag).
24
+ try requireNonEmpty(keyHex, method: "aesGcmEncrypt", paramName: "key")
25
+ try requireNonEmpty(nonceHex, method: "aesGcmEncrypt", paramName: "nonce")
26
+ try requireNonEmpty(aadHex, method: "aesGcmEncrypt", paramName: "aad")
27
+ let plaintext = try dataFromHex(dataHex)
28
+ let key = SymmetricKey(data: try dataFromHex(keyHex))
29
+ let nonce = try gcmNonce(fromHex: nonceHex, method: "aesGcmEncrypt")
30
+ let aad = try dataFromHex(aadHex)
31
+ let sealedBox = try AES.GCM.seal(plaintext, using: key, nonce: nonce, authenticating: aad)
32
+ var ciphertextWithTag = Data(sealedBox.ciphertext)
33
+ ciphertextWithTag.append(sealedBox.tag)
34
+ return hexFromData(ciphertextWithTag)
35
+ } catch let caughtError as NSError {
36
+ error?.pointee = caughtError
37
+ return nil
38
+ }
39
+ }
40
+
41
+ @objc(decryptWithCiphertextWithTagHex:keyHex:nonceHex:aadHex:error:)
42
+ public static func decrypt(
43
+ ciphertextWithTagHex: String,
44
+ keyHex: String,
45
+ nonceHex: String,
46
+ aadHex: String,
47
+ error: NSErrorPointer
48
+ ) -> String? {
49
+ do {
50
+ // `ciphertextWithTagHex` is intentionally NOT validated for non-emptiness
51
+ // here — the `encrypted.count < 16` check below is stronger and covers
52
+ // the empty-hex case.
53
+ try requireNonEmpty(keyHex, method: "aesGcmDecrypt", paramName: "key")
54
+ try requireNonEmpty(nonceHex, method: "aesGcmDecrypt", paramName: "nonce")
55
+ try requireNonEmpty(aadHex, method: "aesGcmDecrypt", paramName: "aad")
56
+ let encrypted = try dataFromHex(ciphertextWithTagHex)
57
+ if encrypted.count < 16 {
58
+ throw AesCryptoGcmError.invalidCiphertext
59
+ }
60
+ let ciphertext = encrypted.prefix(encrypted.count - 16)
61
+ let tag = encrypted.suffix(16)
62
+ let key = SymmetricKey(data: try dataFromHex(keyHex))
63
+ let nonce = try gcmNonce(fromHex: nonceHex, method: "aesGcmDecrypt")
64
+ let aad = try dataFromHex(aadHex)
65
+ let sealedBox = try AES.GCM.SealedBox(nonce: nonce, ciphertext: ciphertext, tag: tag)
66
+ let plaintext = try AES.GCM.open(sealedBox, using: key, authenticating: aad)
67
+ return hexFromData(plaintext)
68
+ } catch let caughtError as NSError {
69
+ error?.pointee = caughtError
70
+ return nil
71
+ }
72
+ }
73
+
74
+ // The GCM entry points enforce a strict non-empty contract on key / nonce /
75
+ // aad; `data` and `ciphertextWithTag` are allowed to be empty hex (the
76
+ // `< 16-byte` guard in decrypt handles the truncated case explicitly).
77
+ private static func requireNonEmpty(_ value: String, method: String, paramName: String) throws {
78
+ if value.isEmpty {
79
+ throw NSError(
80
+ domain: "AesCryptoGcmError",
81
+ code: 1,
82
+ userInfo: [NSLocalizedDescriptionKey: "\(method): \(paramName) must not be empty"]
83
+ )
84
+ }
85
+ }
86
+
87
+ private static func gcmNonce(fromHex hex: String, method: String) throws -> AES.GCM.Nonce {
88
+ let bytes = try dataFromHex(hex)
89
+ if bytes.count != gcmNonceLengthBytes {
90
+ throw NSError(
91
+ domain: "AesCryptoGcmError",
92
+ code: 2,
93
+ userInfo: [NSLocalizedDescriptionKey:
94
+ "\(method): nonce must be exactly \(gcmNonceLengthBytes) bytes (got \(bytes.count))"]
95
+ )
96
+ }
97
+ return try AES.GCM.Nonce(data: bytes)
98
+ }
99
+
100
+ private static func dataFromHex(_ hex: String) throws -> Data {
101
+ // Empty hex is reachable when callers pass empty plaintext (encrypt) or
102
+ // when the `< 16-byte` ciphertext check fires (decrypt) — return an
103
+ // empty `Data` so AES.GCM.seal can produce the bare 16-byte tag.
104
+ if hex.isEmpty {
105
+ return Data()
106
+ }
107
+ if hex.count % 2 != 0 {
108
+ throw AesCryptoGcmError.invalidHex
109
+ }
110
+
111
+ var data = Data(capacity: hex.count / 2)
112
+ var index = hex.startIndex
113
+ while index < hex.endIndex {
114
+ let nextIndex = hex.index(index, offsetBy: 2)
115
+ guard let byte = UInt8(hex[index..<nextIndex], radix: 16) else {
116
+ throw AesCryptoGcmError.invalidHex
117
+ }
118
+ data.append(byte)
119
+ index = nextIndex
120
+ }
121
+ return data
122
+ }
123
+
124
+ private static func hexFromData(_ data: Data) -> String {
125
+ data.map { String(format: "%02x", $0) }.joined()
126
+ }
127
+ }
128
+
129
+ // Use `LocalizedError` so that when these enum cases bridge to `NSError`,
130
+ // `(error as NSError).localizedDescription` returns the message below instead
131
+ // of Cocoa's default "The operation couldn't be completed. (... error N.)".
132
+ private enum AesCryptoGcmError: LocalizedError {
133
+ case invalidHex
134
+ case invalidCiphertext
135
+
136
+ var errorDescription: String? {
137
+ switch self {
138
+ case .invalidHex:
139
+ return "AES-GCM: hex string must have even length and contain only [0-9a-fA-F]"
140
+ case .invalidCiphertext:
141
+ return "AES-GCM: ciphertextWithTag must be at least 16 bytes (the auth tag length)"
142
+ }
143
+ }
144
+ }
@@ -3,6 +3,48 @@
3
3
  import NativeAesCrypto from "./NativeAesCrypto.js";
4
4
  export const encrypt = NativeAesCrypto.encrypt.bind(NativeAesCrypto);
5
5
  export const decrypt = NativeAesCrypto.decrypt.bind(NativeAesCrypto);
6
+
7
+ /**
8
+ * AES-256-GCM authenticated encryption.
9
+ *
10
+ * Contract (intentionally tighter than RFC 5116 / NIST SP 800-38D):
11
+ * - `data`, `key`, `nonce`, `aad` are lowercase hex strings.
12
+ * - `key` must be non-empty hex and decode to a valid AES key length
13
+ * (16 / 24 / 32 bytes).
14
+ * - `nonce` must be **exactly** 12 bytes (24 hex chars). The standard
15
+ * permits other lengths, but `CryptoKit.AES.GCM.Nonce` on iOS is the
16
+ * 12-byte canonical path; Android is locked to the same length to keep
17
+ * ciphertexts byte-compatible across platforms.
18
+ * - `aad` must be non-empty. AEAD allows 0-byte AAD, but every consumer
19
+ * of this module supplies an explicit context binding (envelope header
20
+ * bytes / keyless AAD constants); empty AAD almost always means a
21
+ * forgotten parameter and is rejected.
22
+ * - `data` MAY be empty — that yields a 16-byte tag with no ciphertext.
23
+ *
24
+ * @returns hex string `ciphertext || tag` (tag is always the last 16 bytes).
25
+ *
26
+ * @throws Promise rejects with code `"-1"` for any contract violation
27
+ * (empty key / nonce / aad, wrong nonce length, malformed hex) or for
28
+ * the underlying CryptoKit / JCE error.
29
+ */
30
+ export const aesGcmEncrypt = NativeAesCrypto.aesGcmEncrypt.bind(NativeAesCrypto);
31
+
32
+ /**
33
+ * AES-256-GCM authenticated decryption with the same contract as
34
+ * {@link aesGcmEncrypt}.
35
+ *
36
+ * - `ciphertextWithTag` is the hex form of `ciphertext || tag` produced
37
+ * by {@link aesGcmEncrypt}; its decoded length must be `>= 16` bytes
38
+ * (i.e. at least the auth tag).
39
+ * - `key` / `nonce` / `aad` must match the encryption call **byte for
40
+ * byte**; any mismatch surfaces as a tag-verification rejection.
41
+ *
42
+ * @returns hex string of the recovered plaintext (may be empty).
43
+ *
44
+ * @throws Promise rejects with code `"-1"` for contract violations or
45
+ * GCM tag mismatch (tampered ciphertext / tag / AAD / wrong key).
46
+ */
47
+ export const aesGcmDecrypt = NativeAesCrypto.aesGcmDecrypt.bind(NativeAesCrypto);
6
48
  export const pbkdf2 = NativeAesCrypto.pbkdf2.bind(NativeAesCrypto);
7
49
  export const hmac256 = NativeAesCrypto.hmac256.bind(NativeAesCrypto);
8
50
  export const hmac512 = NativeAesCrypto.hmac512.bind(NativeAesCrypto);
@@ -2,6 +2,8 @@ import type { TurboModule } from 'react-native';
2
2
  export interface Spec extends TurboModule {
3
3
  encrypt(data: string, key: string, iv: string, algorithm: string): Promise<string>;
4
4
  decrypt(base64: string, key: string, iv: string, algorithm: string): Promise<string>;
5
+ aesGcmEncrypt(data: string, key: string, nonce: string, aad: string): Promise<string>;
6
+ aesGcmDecrypt(ciphertextWithTag: string, key: string, nonce: string, aad: string): Promise<string>;
5
7
  pbkdf2(password: string, salt: string, cost: number, length: number, algorithm: string): Promise<string>;
6
8
  hmac256(base64: string, key: string): Promise<string>;
7
9
  hmac512(base64: string, key: string): Promise<string>;
@@ -1,6 +1,46 @@
1
1
  import NativeAesCrypto from './NativeAesCrypto';
2
2
  export declare const encrypt: (data: string, key: string, iv: string, algorithm: string) => Promise<string>;
3
3
  export declare const decrypt: (base64: string, key: string, iv: string, algorithm: string) => Promise<string>;
4
+ /**
5
+ * AES-256-GCM authenticated encryption.
6
+ *
7
+ * Contract (intentionally tighter than RFC 5116 / NIST SP 800-38D):
8
+ * - `data`, `key`, `nonce`, `aad` are lowercase hex strings.
9
+ * - `key` must be non-empty hex and decode to a valid AES key length
10
+ * (16 / 24 / 32 bytes).
11
+ * - `nonce` must be **exactly** 12 bytes (24 hex chars). The standard
12
+ * permits other lengths, but `CryptoKit.AES.GCM.Nonce` on iOS is the
13
+ * 12-byte canonical path; Android is locked to the same length to keep
14
+ * ciphertexts byte-compatible across platforms.
15
+ * - `aad` must be non-empty. AEAD allows 0-byte AAD, but every consumer
16
+ * of this module supplies an explicit context binding (envelope header
17
+ * bytes / keyless AAD constants); empty AAD almost always means a
18
+ * forgotten parameter and is rejected.
19
+ * - `data` MAY be empty — that yields a 16-byte tag with no ciphertext.
20
+ *
21
+ * @returns hex string `ciphertext || tag` (tag is always the last 16 bytes).
22
+ *
23
+ * @throws Promise rejects with code `"-1"` for any contract violation
24
+ * (empty key / nonce / aad, wrong nonce length, malformed hex) or for
25
+ * the underlying CryptoKit / JCE error.
26
+ */
27
+ export declare const aesGcmEncrypt: (data: string, key: string, nonce: string, aad: string) => Promise<string>;
28
+ /**
29
+ * AES-256-GCM authenticated decryption with the same contract as
30
+ * {@link aesGcmEncrypt}.
31
+ *
32
+ * - `ciphertextWithTag` is the hex form of `ciphertext || tag` produced
33
+ * by {@link aesGcmEncrypt}; its decoded length must be `>= 16` bytes
34
+ * (i.e. at least the auth tag).
35
+ * - `key` / `nonce` / `aad` must match the encryption call **byte for
36
+ * byte**; any mismatch surfaces as a tag-verification rejection.
37
+ *
38
+ * @returns hex string of the recovered plaintext (may be empty).
39
+ *
40
+ * @throws Promise rejects with code `"-1"` for contract violations or
41
+ * GCM tag mismatch (tampered ciphertext / tag / AAD / wrong key).
42
+ */
43
+ export declare const aesGcmDecrypt: (ciphertextWithTag: string, key: string, nonce: string, aad: string) => Promise<string>;
4
44
  export declare const pbkdf2: (password: string, salt: string, cost: number, length: number, algorithm: string) => Promise<string>;
5
45
  export declare const hmac256: (base64: string, key: string) => Promise<string>;
6
46
  export declare const hmac512: (base64: string, key: string) => Promise<string>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/react-native-aes-crypto",
3
- "version": "3.0.33",
3
+ "version": "3.0.34",
4
4
  "description": "react-native-aes-crypto",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",
@@ -14,6 +14,8 @@ export interface Spec extends TurboModule {
14
14
  iv: string,
15
15
  algorithm: string,
16
16
  ): Promise<string>;
17
+ aesGcmEncrypt(data: string, key: string, nonce: string, aad: string): Promise<string>;
18
+ aesGcmDecrypt(ciphertextWithTag: string, key: string, nonce: string, aad: string): Promise<string>;
17
19
  pbkdf2(
18
20
  password: string,
19
21
  salt: string,
package/src/index.tsx CHANGED
@@ -2,6 +2,50 @@ import NativeAesCrypto from './NativeAesCrypto';
2
2
 
3
3
  export const encrypt = NativeAesCrypto.encrypt.bind(NativeAesCrypto);
4
4
  export const decrypt = NativeAesCrypto.decrypt.bind(NativeAesCrypto);
5
+
6
+ /**
7
+ * AES-256-GCM authenticated encryption.
8
+ *
9
+ * Contract (intentionally tighter than RFC 5116 / NIST SP 800-38D):
10
+ * - `data`, `key`, `nonce`, `aad` are lowercase hex strings.
11
+ * - `key` must be non-empty hex and decode to a valid AES key length
12
+ * (16 / 24 / 32 bytes).
13
+ * - `nonce` must be **exactly** 12 bytes (24 hex chars). The standard
14
+ * permits other lengths, but `CryptoKit.AES.GCM.Nonce` on iOS is the
15
+ * 12-byte canonical path; Android is locked to the same length to keep
16
+ * ciphertexts byte-compatible across platforms.
17
+ * - `aad` must be non-empty. AEAD allows 0-byte AAD, but every consumer
18
+ * of this module supplies an explicit context binding (envelope header
19
+ * bytes / keyless AAD constants); empty AAD almost always means a
20
+ * forgotten parameter and is rejected.
21
+ * - `data` MAY be empty — that yields a 16-byte tag with no ciphertext.
22
+ *
23
+ * @returns hex string `ciphertext || tag` (tag is always the last 16 bytes).
24
+ *
25
+ * @throws Promise rejects with code `"-1"` for any contract violation
26
+ * (empty key / nonce / aad, wrong nonce length, malformed hex) or for
27
+ * the underlying CryptoKit / JCE error.
28
+ */
29
+ export const aesGcmEncrypt =
30
+ NativeAesCrypto.aesGcmEncrypt.bind(NativeAesCrypto);
31
+
32
+ /**
33
+ * AES-256-GCM authenticated decryption with the same contract as
34
+ * {@link aesGcmEncrypt}.
35
+ *
36
+ * - `ciphertextWithTag` is the hex form of `ciphertext || tag` produced
37
+ * by {@link aesGcmEncrypt}; its decoded length must be `>= 16` bytes
38
+ * (i.e. at least the auth tag).
39
+ * - `key` / `nonce` / `aad` must match the encryption call **byte for
40
+ * byte**; any mismatch surfaces as a tag-verification rejection.
41
+ *
42
+ * @returns hex string of the recovered plaintext (may be empty).
43
+ *
44
+ * @throws Promise rejects with code `"-1"` for contract violations or
45
+ * GCM tag mismatch (tampered ciphertext / tag / AAD / wrong key).
46
+ */
47
+ export const aesGcmDecrypt =
48
+ NativeAesCrypto.aesGcmDecrypt.bind(NativeAesCrypto);
5
49
  export const pbkdf2 = NativeAesCrypto.pbkdf2.bind(NativeAesCrypto);
6
50
  export const hmac256 = NativeAesCrypto.hmac256.bind(NativeAesCrypto);
7
51
  export const hmac512 = NativeAesCrypto.hmac512.bind(NativeAesCrypto);