@onekeyfe/react-native-aes-crypto 3.0.33 → 3.0.35
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 +12 -1
- package/android/src/main/java/com/aescrypto/AesCryptoModule.kt +132 -14
- package/ios/AesCrypto.h +23 -0
- package/ios/AesCrypto.mm +137 -18
- package/ios/AesCryptoGcm.swift +144 -0
- package/lib/module/index.js +42 -0
- package/lib/typescript/src/NativeAesCrypto.d.ts +2 -0
- package/lib/typescript/src/index.d.ts +40 -0
- package/package.json +1 -1
- package/src/NativeAesCrypto.ts +2 -0
- package/src/index.tsx +44 -0
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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(@"
|
|
211
|
+
reject(@"-1", @"Encrypt error", nil);
|
|
169
212
|
} else {
|
|
170
213
|
resolve(toHex(result));
|
|
171
214
|
}
|
|
172
215
|
} @catch (NSException *exception) {
|
|
173
|
-
reject(@"
|
|
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(@"
|
|
244
|
+
reject(@"-1", @"Decrypt failed", nil);
|
|
198
245
|
} else {
|
|
199
246
|
resolve(toHex(result));
|
|
200
247
|
}
|
|
201
248
|
} @catch (NSException *exception) {
|
|
202
|
-
reject(@"
|
|
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(@"
|
|
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(@"
|
|
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(@"
|
|
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(@"
|
|
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(@"
|
|
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(@"
|
|
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(@"
|
|
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(@"
|
|
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(@"
|
|
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(@"
|
|
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(@"
|
|
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(@"
|
|
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(@"
|
|
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(@"
|
|
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
|
+
}
|
package/lib/module/index.js
CHANGED
|
@@ -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
package/src/NativeAesCrypto.ts
CHANGED
|
@@ -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);
|