expo-device-crypto 0.2.0 → 0.2.1

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/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  > ⚠️ This module is currently in beta and is not suitable for production use.
4
4
 
5
- 🔒 Hardware-backed cryptography for Expo apps using [Android Keystore](https://developer.android.com/privacy-and-security/keystore) and Apple [Secure Enclave](https://developer.apple.com/documentation/security/protecting-keys-with-the-secure-enclave).
5
+ 🔒 Hardware-backed cryptography for Expo apps using [Android Keystore](https://developer.android.com/privacy-and-security/keystore) and Apple [Secure Enclave](https://developer.apple.com/documentation/security/protecting-keys-with-the-secure-enclave)/[Keychain](https://developer.apple.com/documentation/security/keychain-services).
6
6
 
7
7
  ## Installation
8
8
 
@@ -48,7 +48,7 @@ If you want to allow Face ID on iOS, add this to your app config:
48
48
  **Padding:** OAEP with SHA-1 and MGF1. </br>
49
49
  **Use it for:** Encrypting small secrets with stronger padding than PKCS#1 v1.5 when interoperating with OAEP SHA-1 systems.
50
50
 
51
- More coming soon
51
+ **More coming soon...**
52
52
 
53
53
  ## Example: ECDSA with Biometric
54
54
 
@@ -176,4 +176,10 @@ const decrypted = await DeviceCrypto.decrypt(alias, encrypted ?? "", {
176
176
  - Decrypts Base64 `data` with private key.
177
177
  - Defaults: `algorithmType = RSA_2048_PKCS1`, `promptTitle = "Unlock"`, `promptSubtitle = "Enter your PIN to continue"`, `authMethod = PASSCODE_OR_BIOMETRIC`.
178
178
 
179
- > ✅ JSDoc type definitions are available in `./src/DeviceCryptoModule.ts`.
179
+ > ✅ JSDoc type definitions are available in `./src/DeviceCryptoModule.ts`.
180
+
181
+ ## Important
182
+
183
+ ### RSA algorithms on iOS
184
+
185
+ Please note that RSA private-key operations are not performed in the [Secure Enclave processor](https://developer.apple.com/documentation/security/ksecattrtokenidsecureenclave?utm_source=chatgpt.com#Discussion). Instead, they are software-backed and handled by Apple’s system cryptographic services, which still provide strong isolation and protection.
@@ -1,7 +1,7 @@
1
1
  apply plugin: 'com.android.library'
2
2
 
3
3
  group = 'expo.modules.devicecrypto'
4
- version = '0.2.0'
4
+ version = '0.2.1'
5
5
 
6
6
  def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
7
7
  apply from: expoModulesCorePlugin
@@ -35,7 +35,7 @@ android {
35
35
  namespace "expo.modules.devicecrypto"
36
36
  defaultConfig {
37
37
  versionCode 1
38
- versionName "0.2.0"
38
+ versionName "0.2.1"
39
39
  }
40
40
  lintOptions {
41
41
  abortOnError false
@@ -81,21 +81,11 @@ class DeviceCryptoModule : Module() {
81
81
  }
82
82
  }
83
83
 
84
- private fun isAuthCheckAvailable(): AuthCheckResult {
85
- val biometricManager = BiometricManager.from(appContext.reactContext as Context)
86
- return when (biometricManager.canAuthenticate(BIOMETRIC_STRONG or DEVICE_CREDENTIAL)) {
87
- BiometricManager.BIOMETRIC_SUCCESS -> AuthCheckResult.AVAILABLE
88
- BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> AuthCheckResult.NO_HARDWARE
89
- BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE -> AuthCheckResult.UNAVAILABLE
90
- else -> AuthCheckResult.UNAVAILABLE
91
- }
92
- }
93
-
94
84
  private fun isKeyStoreAvailable(): Boolean {
95
85
  val pm = appContext.reactContext?.packageManager ?: return false
96
86
  val appAttestKeystore = pm.hasSystemFeature(PackageManager.FEATURE_KEYSTORE_APP_ATTEST_KEY)
97
87
  val hardwareKeystore = pm.hasSystemFeature(PackageManager.FEATURE_HARDWARE_KEYSTORE)
98
- return appAttestKeystore && hardwareKeystore
88
+ return appAttestKeystore || hardwareKeystore
99
89
  }
100
90
 
101
91
  private fun isStrongBoxAvailable(): Boolean {
@@ -104,10 +94,6 @@ class DeviceCryptoModule : Module() {
104
94
  return strongboxKeystore
105
95
  }
106
96
 
107
- private fun getAliases() = KeyStore.getInstance("AndroidKeyStore").apply {
108
- load(null)
109
- }.aliases().toList()
110
-
111
97
  private fun isKeyStoreRequireAuthentication(entry: KeyStore.PrivateKeyEntry): Boolean {
112
98
  val keyFactory = KeyFactory.getInstance(entry.privateKey.algorithm, "AndroidKeyStore")
113
99
  val keyInfo = keyFactory.getKeySpec(entry.privateKey, KeyInfo::class.java)
@@ -120,23 +106,6 @@ class DeviceCryptoModule : Module() {
120
106
  }.getEntry(alias, null)
121
107
  }
122
108
 
123
- private fun retrievePublicKey(ks: KeyStore.Entry?): String? {
124
- if (ks !is KeyStore.PrivateKeyEntry) return null
125
-
126
- val publicKeyBytes: ByteArray = ks.certificate?.publicKey?.encoded ?: return null
127
- return Base64.encodeToString(publicKeyBytes, Base64.NO_WRAP)
128
- }
129
-
130
- private fun removeKeyStoreEntry(alias: String): Boolean {
131
- val ks = KeyStore.getInstance("AndroidKeyStore").apply {
132
- load(null)
133
- }
134
- if (ks.getEntry(alias, null) == null) return false
135
-
136
- ks.deleteEntry(alias)
137
- return true
138
- }
139
-
140
109
  private fun buildECDSA(
141
110
  alias: String,
142
111
  digest: String,
@@ -249,191 +218,229 @@ class DeviceCryptoModule : Module() {
249
218
  }
250
219
  }
251
220
 
252
- override fun definition() = ModuleDefinition {
253
- Name("DeviceCrypto")
221
+ private fun isAuthCheckAvailable(): AuthCheckResult {
222
+ val biometricManager = BiometricManager.from(appContext.reactContext as Context)
223
+ return when (biometricManager.canAuthenticate(BIOMETRIC_STRONG or DEVICE_CREDENTIAL)) {
224
+ BiometricManager.BIOMETRIC_SUCCESS -> AuthCheckResult.AVAILABLE
225
+ BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> AuthCheckResult.NO_HARDWARE
226
+ BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE -> AuthCheckResult.UNAVAILABLE
227
+ else -> AuthCheckResult.UNAVAILABLE
228
+ }
229
+ }
254
230
 
255
- Function("isAuthCheckAvailable") { ->
256
- return@Function isAuthCheckAvailable()
231
+ private fun generateKeyPair(alias: String, o: Map<String, Any?>): GenerateKeyPairResult {
232
+ val reqAuth = o["reqAuth"] as Boolean
233
+ if (reqAuth && isAuthCheckAvailable() != AuthCheckResult.AVAILABLE) {
234
+ throw Exception("NO_AUTH_AVAILABLE")
235
+ }
236
+ if (!isKeyStoreAvailable()) {
237
+ return GenerateKeyPairResult.NOT_AVAILABLE
238
+ }
239
+ if (getKeyStoreEntry(alias) != null) {
240
+ return GenerateKeyPairResult.KEY_PAIR_ALREADY_EXISTS
257
241
  }
258
242
 
259
- Function("generateKeyPair") { alias: String, o: Map<String, Any?> ->
260
- val reqAuth = o["reqAuth"] as Boolean
261
- if (reqAuth && isAuthCheckAvailable() != AuthCheckResult.AVAILABLE) {
262
- throw Exception("NO_AUTH_AVAILABLE")
263
- }
264
- if (!isKeyStoreAvailable()) {
265
- return@Function GenerateKeyPairResult.NOT_AVAILABLE
266
- }
267
- if (getKeyStoreEntry(alias) != null) {
268
- return@Function GenerateKeyPairResult.KEY_PAIR_ALREADY_EXISTS
269
- }
243
+ val algoType = AlgorithmType.valueOf(o["algoType"] as String)
244
+ val kpg = when (algoType) {
245
+ AlgorithmType.ECDSA_SECP256R1_SHA256 -> buildECDSA(alias, DIGEST_SHA256, reqAuth)
246
+ AlgorithmType.RSA_2048_PKCS1 -> buildRSA(alias, null, ENCRYPTION_PADDING_RSA_PKCS1, reqAuth)
247
+ AlgorithmType.RSA_2048_OAEP_SHA1 -> buildRSA(alias, DIGEST_SHA1, ENCRYPTION_PADDING_RSA_OAEP, reqAuth)
248
+ else -> throw Exception("INVALID_ALGORITHM_TYPE")
249
+ }
270
250
 
271
- val algoType = AlgorithmType.valueOf(o["algoType"] as String)
272
- val kpg = when (algoType) {
273
- AlgorithmType.ECDSA_SECP256R1_SHA256 -> buildECDSA(alias, DIGEST_SHA256, reqAuth)
274
- AlgorithmType.RSA_2048_PKCS1 -> buildRSA(alias, null, ENCRYPTION_PADDING_RSA_PKCS1, reqAuth)
275
- AlgorithmType.RSA_2048_OAEP_SHA1 -> buildRSA(alias, DIGEST_SHA1, ENCRYPTION_PADDING_RSA_OAEP, reqAuth)
276
- else -> throw Exception("INVALID_ALGORITHM_TYPE")
277
- }
251
+ kpg.generateKeyPair()
278
252
 
279
- kpg.generateKeyPair()
253
+ return GenerateKeyPairResult.KEY_PAIR_GENERATED
254
+ }
280
255
 
281
- return@Function GenerateKeyPairResult.KEY_PAIR_GENERATED
256
+ private fun removeKeyStoreEntry(alias: String): Boolean {
257
+ val ks = KeyStore.getInstance("AndroidKeyStore").apply {
258
+ load(null)
282
259
  }
260
+ if (ks.getEntry(alias, null) == null) return false
283
261
 
284
- Function("removeKeyPair") { alias: String ->
285
- return@Function removeKeyStoreEntry(alias)
262
+ ks.deleteEntry(alias)
263
+ return true
264
+ }
265
+
266
+ private fun getAliases() = KeyStore.getInstance("AndroidKeyStore").apply {
267
+ load(null)
268
+ }.aliases().toList()
269
+
270
+ private fun getPublicKey(alias: String): String? {
271
+ val ks = getKeyStoreEntry(alias)
272
+ if (ks !is KeyStore.PrivateKeyEntry) return null
273
+
274
+ val publicKeyBytes: ByteArray = ks.certificate?.publicKey?.encoded ?: return null
275
+ return Base64.encodeToString(publicKeyBytes, Base64.NO_WRAP)
276
+ }
277
+
278
+ private fun sign(alias: String, data: String, o: Map<String, Any?>, promise: Promise) {
279
+ val entry = getKeyStoreEntry(alias)
280
+ if (entry !is KeyStore.PrivateKeyEntry) {
281
+ promise.resolve(null)
282
+ return
286
283
  }
287
284
 
288
- Function("aliases") {
289
- return@Function getAliases()
285
+ val reqAuth = isKeyStoreRequireAuthentication(entry)
286
+
287
+ val title = o["title"] as String
288
+ val subtitle = o["subtitle"] as String
289
+ val authMethod = AuthMethod.valueOf(o["authMethod"] as String)
290
+ val algoType = AlgorithmType.valueOf(o["algoType"] as String)
291
+ val algo = toAndroidAlgo(algoType)
292
+
293
+ if (!reqAuth) {
294
+ val signature: ByteArray = Signature.getInstance(algo).apply {
295
+ initSign(entry.privateKey)
296
+ update(data.toByteArray(Charsets.UTF_8))
297
+ }.sign()
298
+ promise.resolve(Base64.encodeToString(signature, Base64.NO_WRAP))
299
+ return
300
+ } else {
301
+ showAuthPrompt(
302
+ onSuccess = {
303
+ val signature: ByteArray = Signature.getInstance(algo).apply {
304
+ initSign(entry.privateKey)
305
+ update(data.toByteArray(Charsets.UTF_8))
306
+ }.sign()
307
+ promise.resolve(Base64.encodeToString(signature, Base64.NO_WRAP))
308
+ },
309
+ onError = { error ->
310
+ promise.reject("ERR_AUTH_FAILED", error, null)
311
+ },
312
+ title = title,
313
+ subtitle = subtitle,
314
+ authMethod = authMethod
315
+ )
290
316
  }
317
+ }
291
318
 
292
- Function("getPublicKey") { alias: String ->
293
- val entry = getKeyStoreEntry(alias)
294
- return@Function retrievePublicKey(entry)
319
+ private fun verify(alias: String, data: String, signature: String, o: Map<String, Any?>): Boolean? {
320
+ val entry = getKeyStoreEntry(alias)
321
+ if (entry !is KeyStore.PrivateKeyEntry) return null
322
+
323
+ val algoType = AlgorithmType.valueOf(o["algoType"] as String)
324
+ val algo = toAndroidAlgo(algoType)
325
+
326
+ val valid: Boolean = Signature.getInstance(algo).apply {
327
+ initVerify(entry.certificate)
328
+ update(data.toByteArray(Charsets.UTF_8))
329
+ }.verify(Base64.decode(signature, Base64.NO_WRAP))
330
+ return valid
331
+ }
332
+
333
+ private fun encrypt(alias: String, data: String, o: Map<String, Any?>, promise: Promise) {
334
+ val entry = getKeyStoreEntry(alias)
335
+ if (entry !is KeyStore.PrivateKeyEntry) {
336
+ promise.resolve(null)
337
+ return
295
338
  }
296
339
 
297
- AsyncFunction("sign") { alias: String,
298
- data: String,
299
- o: Map<String, Any?>,
300
- promise: Promise ->
301
- val entry = getKeyStoreEntry(alias)
302
- if (entry !is KeyStore.PrivateKeyEntry) {
303
- promise.resolve(null)
304
- return@AsyncFunction
305
- }
340
+ val algoType = AlgorithmType.valueOf(o["algoType"] as String)
341
+ val algo = toAndroidAlgo(algoType)
342
+ val oaepSpec = getOaepSpec(algoType)
306
343
 
307
- val reqAuth = isKeyStoreRequireAuthentication(entry);
308
-
309
- val title = o["title"] as String
310
- val subtitle = o["subtitle"] as String
311
- val authMethod = AuthMethod.valueOf(o["authMethod"] as String)
312
- val algoType = AlgorithmType.valueOf(o["algoType"] as String)
313
- val algo = toAndroidAlgo(algoType)
314
-
315
- if (!reqAuth) {
316
- val signature: ByteArray = Signature.getInstance(algo).apply {
317
- initSign(entry.privateKey)
318
- update(data.toByteArray(Charsets.UTF_8))
319
- }.sign()
320
- promise.resolve(Base64.encodeToString(signature, Base64.NO_WRAP))
321
- return@AsyncFunction
322
- } else {
323
- showAuthPrompt(
324
- onSuccess = {
325
- val signature: ByteArray = Signature.getInstance(algo).apply {
326
- initSign(entry.privateKey)
327
- update(data.toByteArray(Charsets.UTF_8))
328
- }.sign()
329
- promise.resolve(Base64.encodeToString(signature, Base64.NO_WRAP))
330
- },
331
- onError = { error ->
332
- promise.reject("ERR_AUTH_FAILED", error, null)
333
- },
334
- title = title,
335
- subtitle = subtitle,
336
- authMethod = authMethod
337
- )
338
- }
344
+ val cipher = Cipher.getInstance(algo)
345
+ if (oaepSpec != null) {
346
+ cipher.init(Cipher.ENCRYPT_MODE, entry.certificate.publicKey, oaepSpec)
347
+ } else {
348
+ cipher.init(Cipher.ENCRYPT_MODE, entry.certificate.publicKey)
339
349
  }
350
+ val encrypted: ByteArray = cipher.doFinal(data.toByteArray(Charsets.UTF_8))
351
+ promise.resolve(Base64.encodeToString(encrypted, Base64.NO_WRAP))
352
+ }
340
353
 
341
- Function("verify") { alias: String,
342
- data: String,
343
- signature: String,
344
- o: Map<String, Any?> ->
345
- val entry = getKeyStoreEntry(alias)
346
- if (entry !is KeyStore.PrivateKeyEntry) return@Function null
347
-
348
- val algoType = AlgorithmType.valueOf(o["algoType"] as String)
349
- val algo = toAndroidAlgo(algoType)
350
-
351
- val valid: Boolean = Signature.getInstance(algo).apply {
352
- initVerify(entry.certificate)
353
- update(data.toByteArray(Charsets.UTF_8))
354
- }.verify(Base64.decode(signature, Base64.NO_WRAP))
355
- return@Function true
354
+ private fun decrypt(alias: String, data: String, o: Map<String, Any?>, promise: Promise) {
355
+ val entry = getKeyStoreEntry(alias)
356
+ if (entry !is KeyStore.PrivateKeyEntry) {
357
+ promise.resolve(null)
358
+ return
356
359
  }
357
360
 
361
+ val reqAuth = isKeyStoreRequireAuthentication(entry)
358
362
 
359
- AsyncFunction("encrypt") { alias: String,
360
- data: String,
361
- o: Map<String, Any?>,
362
- promise: Promise ->
363
- val entry = getKeyStoreEntry(alias)
364
- if (entry !is KeyStore.PrivateKeyEntry) {
365
- promise.resolve(null)
366
- return@AsyncFunction
367
- }
363
+ val title = o["title"] as String
364
+ val subtitle = o["subtitle"] as String
365
+ val authMethod = o["authMethod"] as String
366
+ val algoType = AlgorithmType.valueOf(o["algoType"] as String)
367
+ val algo = toAndroidAlgo(algoType)
368
+ val oaepSpec = getOaepSpec(algoType)
368
369
 
369
- val algoType = AlgorithmType.valueOf(o["algoType"] as String)
370
- val algo = toAndroidAlgo(algoType)
371
- val oaepSpec = getOaepSpec(algoType)
370
+ val cipher = Cipher.getInstance(algo)
371
+ val ciphertext = Base64.decode(data, Base64.NO_WRAP)
372
372
 
373
- val cipher = Cipher.getInstance(algo)
373
+ if (!reqAuth) {
374
374
  if (oaepSpec != null) {
375
- cipher.init(Cipher.ENCRYPT_MODE, entry.certificate.publicKey, oaepSpec)
375
+ cipher.init(Cipher.DECRYPT_MODE, entry.privateKey, oaepSpec)
376
376
  } else {
377
- cipher.init(Cipher.ENCRYPT_MODE, entry.certificate.publicKey)
377
+ cipher.init(Cipher.DECRYPT_MODE, entry.privateKey)
378
378
  }
379
- val encrypted: ByteArray = cipher.doFinal(data.toByteArray(Charsets.UTF_8))
380
- promise.resolve(Base64.encodeToString(encrypted, Base64.NO_WRAP))
379
+ val decrypted: ByteArray = cipher.doFinal(ciphertext)
380
+ promise.resolve(String(decrypted, Charsets.UTF_8))
381
+ return
382
+ } else {
383
+ showAuthPrompt(
384
+ onSuccess = {
385
+ if (oaepSpec != null) {
386
+ cipher.init(Cipher.DECRYPT_MODE, entry.privateKey, oaepSpec)
387
+ } else {
388
+ cipher.init(Cipher.DECRYPT_MODE, entry.privateKey)
389
+ }
390
+ val decrypted: ByteArray = cipher.doFinal(ciphertext)
391
+ promise.resolve(String(decrypted, Charsets.UTF_8))
392
+ },
393
+ onError = { error ->
394
+ promise.reject("ERR_AUTH_FAILED", error, null)
395
+ },
396
+ title = title,
397
+ subtitle = subtitle,
398
+ authMethod = when (authMethod) {
399
+ "PASSCODE" -> AuthMethod.PASSCODE
400
+ "PASSCODE_OR_BIOMETRIC" -> AuthMethod.PASSCODE_OR_BIOMETRIC
401
+ else -> AuthMethod.PASSCODE_OR_BIOMETRIC
402
+ }
403
+ )
381
404
  }
405
+ }
382
406
 
383
- AsyncFunction("decrypt") { alias: String,
384
- data: String,
385
- o: Map<String, Any?>,
386
- promise: Promise ->
387
- val entry = getKeyStoreEntry(alias)
388
- if (entry !is KeyStore.PrivateKeyEntry) {
389
- promise.resolve(null)
390
- return@AsyncFunction
391
- }
407
+ override fun definition() = ModuleDefinition {
408
+ Name("DeviceCrypto")
392
409
 
393
- val reqAuth = isKeyStoreRequireAuthentication(entry)
410
+ Function("isAuthCheckAvailable") { ->
411
+ return@Function isAuthCheckAvailable()
412
+ }
394
413
 
395
- val title = o["title"] as String
396
- val subtitle = o["subtitle"] as String
397
- val authMethod = o["authMethod"] as String
398
- val algoType = AlgorithmType.valueOf(o["algoType"] as String)
399
- val algo = toAndroidAlgo(algoType)
400
- val oaepSpec = getOaepSpec(algoType)
414
+ Function("generateKeyPair") { alias: String, o: Map<String, Any?> ->
415
+ return@Function generateKeyPair(alias, o)
416
+ }
401
417
 
402
- val cipher = Cipher.getInstance(algo)
403
- val ciphertext = Base64.decode(data, Base64.NO_WRAP)
418
+ Function("removeKeyPair") { alias: String ->
419
+ return@Function removeKeyStoreEntry(alias)
420
+ }
404
421
 
405
- if (!reqAuth) {
406
- if (oaepSpec != null) {
407
- cipher.init(Cipher.DECRYPT_MODE, entry.privateKey, oaepSpec)
408
- } else {
409
- cipher.init(Cipher.DECRYPT_MODE, entry.privateKey)
410
- }
411
- val decrypted: ByteArray = cipher.doFinal(ciphertext)
412
- promise.resolve(String(decrypted, Charsets.UTF_8))
413
- return@AsyncFunction
414
- } else {
415
- showAuthPrompt(
416
- onSuccess = {
417
- if (oaepSpec != null) {
418
- cipher.init(Cipher.DECRYPT_MODE, entry.privateKey, oaepSpec)
419
- } else {
420
- cipher.init(Cipher.DECRYPT_MODE, entry.privateKey)
421
- }
422
- val decrypted: ByteArray = cipher.doFinal(ciphertext)
423
- promise.resolve(String(decrypted, Charsets.UTF_8))
424
- },
425
- onError = { error ->
426
- promise.reject("ERR_AUTH_FAILED", error, null)
427
- },
428
- title = title,
429
- subtitle = subtitle,
430
- authMethod = when (authMethod) {
431
- "PASSCODE" -> AuthMethod.PASSCODE
432
- "PASSCODE_OR_BIOMETRIC" -> AuthMethod.PASSCODE_OR_BIOMETRIC
433
- else -> AuthMethod.PASSCODE_OR_BIOMETRIC
434
- }
435
- )
436
- }
422
+ Function("aliases") {
423
+ return@Function getAliases()
424
+ }
425
+
426
+ Function("getPublicKey") { alias: String ->
427
+ return@Function getPublicKey(alias)
428
+ }
429
+
430
+ AsyncFunction("sign") { alias: String, data: String, o: Map<String, Any?>, promise: Promise ->
431
+ return@AsyncFunction sign(alias, data, o, promise)
432
+ }
433
+
434
+ Function("verify") { alias: String, data: String, signature: String, o: Map<String, Any?> ->
435
+ return@Function verify(alias, data, signature, o)
436
+ }
437
+
438
+ AsyncFunction("encrypt") { alias: String, data: String, o: Map<String, Any?>, promise: Promise ->
439
+ return@AsyncFunction encrypt(alias, data, o, promise)
440
+ }
441
+
442
+ AsyncFunction("decrypt") { alias: String, data: String, o: Map<String, Any?>, promise: Promise ->
443
+ return@AsyncFunction decrypt(alias, data, o, promise)
437
444
  }
438
445
  }
439
446
  }
@@ -1,6 +1,6 @@
1
1
  Pod::Spec.new do |s|
2
2
  s.name = 'DeviceCrypto'
3
- s.version = '0.2.0'
3
+ s.version = '0.2.1'
4
4
  s.summary = 'Device crypto'
5
5
  s.description = 'Device crypto for Expo using device hardware capabilities'
6
6
  s.author = 'Piotr Pietras'
@@ -103,38 +103,6 @@ public class DeviceCryptoModule: Module {
103
103
  return Data([0x80 | UInt8(bytes.count)]) + Data(bytes)
104
104
  }
105
105
 
106
- private func isAuthCheckAvailable() -> String {
107
- let context = LAContext()
108
- let available = context.canEvaluatePolicy(.deviceOwnerAuthentication, error: nil)
109
- if available {
110
- return AuthCheckResult.AVAILABLE.rawValue
111
- } else {
112
- return AuthCheckResult.UNAVAILABLE.rawValue
113
- }
114
- }
115
-
116
- private func getAliases() -> [String] {
117
- let query: [String: Any] = [
118
- kSecClass as String: kSecClassKey,
119
- kSecAttrKeyClass as String: kSecAttrKeyClassPublic,
120
- kSecMatchLimit as String: kSecMatchLimitAll,
121
- kSecReturnAttributes as String: true,
122
- ]
123
-
124
- var result: CFTypeRef?
125
- let status = SecItemCopyMatching(query as CFDictionary, &result)
126
- guard status == errSecSuccess else { return [] }
127
-
128
- let items = (result as? [[String: Any]]) ?? []
129
- return items.compactMap { attrs in
130
- let tagKey = kSecAttrApplicationTag as String
131
- if let tagString = attrs[tagKey] as? String {
132
- return tagString
133
- }
134
- return nil
135
- }
136
- }
137
-
138
106
  private func getSecKeyQuery(_ alias: String, keyClass: CFString, returnRef: Bool = true) -> [String: Any] {
139
107
  var query: [String: Any] = [
140
108
  kSecClass as String: kSecClassKey,
@@ -155,40 +123,6 @@ public class DeviceCryptoModule: Module {
155
123
  return (item as! SecKey)
156
124
  }
157
125
 
158
- private func retrievePublicKey(_ secKey: SecKey) -> String? {
159
- let publicKey = SecKeyCopyPublicKey(secKey)!
160
-
161
- guard
162
- let attrs = SecKeyCopyAttributes(publicKey) as? [String: Any],
163
- let keyType = attrs[kSecAttrKeyType as String] as? String
164
- else {
165
- return nil
166
- }
167
-
168
- if keyType == (kSecAttrKeyTypeECSECPrimeRandom as String) {
169
- return x962ECPointToP256SPKI(publicKey)?.base64EncodedString()
170
- }
171
-
172
- if keyType == (kSecAttrKeyTypeRSA as String) {
173
- return rsaPKCS1ToSPKI(publicKey)?.base64EncodedString()
174
- }
175
-
176
- guard let publicKeyData = SecKeyCopyExternalRepresentation(publicKey, nil) as Data? else {
177
- return nil
178
- }
179
- return publicKeyData.base64EncodedString()
180
- }
181
-
182
- private func removeKeyStoreEntry(_ alias: String) -> Bool {
183
- let privateQuery = self.getSecKeyQuery(alias, keyClass: kSecAttrKeyClassPrivate, returnRef: false)
184
- let privateStatus = SecItemDelete(privateQuery as CFDictionary)
185
-
186
- let publicQuery = self.getSecKeyQuery(alias, keyClass: kSecAttrKeyClassPublic, returnRef: false)
187
- let publicStatus = SecItemDelete(publicQuery as CFDictionary)
188
-
189
- return privateStatus == errSecSuccess || publicStatus == errSecSuccess
190
- }
191
-
192
126
  private func buildECDSA(alias: String, reqAuth: Bool, authMethod: AuthMethod) -> SecKey? {
193
127
  let accessFlags: SecAccessControlCreateFlags
194
128
  if reqAuth {
@@ -264,48 +198,205 @@ public class DeviceCryptoModule: Module {
264
198
  return SecKeyCreateRandomKey(attributes, nil)
265
199
  }
266
200
 
267
- public func definition() -> ModuleDefinition {
201
+ private func isAuthCheckAvailable() -> String {
202
+ let context = LAContext()
203
+ let available = context.canEvaluatePolicy(.deviceOwnerAuthentication, error: nil)
204
+ if available {
205
+ return AuthCheckResult.AVAILABLE.rawValue
206
+ } else {
207
+ return AuthCheckResult.UNAVAILABLE.rawValue
208
+ }
209
+ }
268
210
 
269
- Name("DeviceCrypto")
211
+ private func generateKeyPair(alias: String, o: [String: Any]) throws -> String {
212
+ let reqAuth = o["reqAuth"] as! Bool
213
+ let authMethod = AuthMethod(rawValue: o["authMethod"] as! String)
214
+ let algoType = AlgorithmType(rawValue: o["algoType"] as! String)
270
215
 
271
- Function("isAuthCheckAvailable") { () -> String in
272
- return self.isAuthCheckAvailable()
216
+ if reqAuth && self.isAuthCheckAvailable() != AuthCheckResult.AVAILABLE.rawValue {
217
+ throw NSError(
218
+ domain: "DeviceCrypto",
219
+ code: 1,
220
+ userInfo: [NSLocalizedDescriptionKey: "NO_AUTH_AVAILABLE"]
221
+ )
273
222
  }
274
223
 
275
- Function("generateKeyPair") { (alias: String, o: [String: Any]) -> String in
276
- let reqAuth = o["reqAuth"] as! Bool
277
- let authMethod = AuthMethod(rawValue: o["authMethod"] as! String)
278
- let algoType = AlgorithmType(rawValue: o["algoType"] as! String)
224
+ let secKey = self.getSecKeyByAlias(alias, keyClass: kSecAttrKeyClassPrivate)
225
+ if secKey != nil {
226
+ return SecureSigningModuleResult.KEY_PAIR_ALREADY_EXISTS.rawValue
227
+ }
279
228
 
280
- if reqAuth && self.isAuthCheckAvailable() != AuthCheckResult.AVAILABLE.rawValue {
229
+ switch algoType {
230
+ case .ECDSA_SECP256R1_SHA256:
231
+ self.buildECDSA(alias: alias, reqAuth: reqAuth, authMethod: authMethod!)
232
+ case .RSA_2048_PKCS1:
233
+ self.buildRSA(alias: alias, reqAuth: reqAuth, authMethod: authMethod!)
234
+ case .RSA_2048_OAEP_SHA1:
235
+ self.buildRSA(alias: alias, reqAuth: reqAuth, authMethod: authMethod!)
236
+ default:
281
237
  throw NSError(
282
238
  domain: "DeviceCrypto",
283
239
  code: 1,
284
- userInfo: [NSLocalizedDescriptionKey: "NO_AUTH_AVAILABLE"]
240
+ userInfo: [NSLocalizedDescriptionKey: "INVALID_ALGORITHM_TYPE"]
285
241
  )
286
- }
242
+ }
287
243
 
288
- let secKey = self.getSecKeyByAlias(alias, keyClass: kSecAttrKeyClassPrivate)
289
- if secKey != nil {
290
- return SecureSigningModuleResult.KEY_PAIR_ALREADY_EXISTS.rawValue
291
- }
244
+ return SecureSigningModuleResult.KEY_PAIR_GENERATED.rawValue
245
+ }
246
+
247
+ private func removeKeyStoreEntry(_ alias: String) -> Bool {
248
+ let privateQuery = self.getSecKeyQuery(alias, keyClass: kSecAttrKeyClassPrivate, returnRef: false)
249
+ let privateStatus = SecItemDelete(privateQuery as CFDictionary)
250
+
251
+ let publicQuery = self.getSecKeyQuery(alias, keyClass: kSecAttrKeyClassPublic, returnRef: false)
252
+ let publicStatus = SecItemDelete(publicQuery as CFDictionary)
253
+
254
+ return privateStatus == errSecSuccess || publicStatus == errSecSuccess
255
+ }
292
256
 
293
- switch algoType {
294
- case .ECDSA_SECP256R1_SHA256:
295
- self.buildECDSA(alias: alias, reqAuth: reqAuth, authMethod: authMethod!)
296
- case .RSA_2048_PKCS1:
297
- self.buildRSA(alias: alias, reqAuth: reqAuth, authMethod: authMethod!)
298
- case .RSA_2048_OAEP_SHA1:
299
- self.buildRSA(alias: alias, reqAuth: reqAuth, authMethod: authMethod!)
300
- default:
301
- throw NSError(
302
- domain: "DeviceCrypto",
303
- code: 1,
304
- userInfo: [NSLocalizedDescriptionKey: "INVALID_ALGORITHM_TYPE"]
305
- )
257
+ private func getAliases() -> [String] {
258
+ let query: [String: Any] = [
259
+ kSecClass as String: kSecClassKey,
260
+ kSecAttrKeyClass as String: kSecAttrKeyClassPublic,
261
+ kSecMatchLimit as String: kSecMatchLimitAll,
262
+ kSecReturnAttributes as String: true,
263
+ ]
264
+
265
+ var result: CFTypeRef?
266
+ let status = SecItemCopyMatching(query as CFDictionary, &result)
267
+ guard status == errSecSuccess else { return [] }
268
+
269
+ let items = (result as? [[String: Any]]) ?? []
270
+ return items.compactMap { attrs in
271
+ let tagKey = kSecAttrApplicationTag as String
272
+ if let tagString = attrs[tagKey] as? String {
273
+ return tagString
306
274
  }
275
+ return nil
276
+ }
277
+ }
278
+
279
+ private func getPublicKey(alias: String) -> String? {
280
+ let secKey = self.getSecKeyByAlias(alias, keyClass: kSecAttrKeyClassPublic)
281
+ guard let secKey else { return nil }
282
+
283
+ let publicKey = SecKeyCopyPublicKey(secKey)!
284
+
285
+ guard
286
+ let attrs = SecKeyCopyAttributes(publicKey) as? [String: Any],
287
+ let keyType = attrs[kSecAttrKeyType as String] as? String
288
+ else {
289
+ return nil
290
+ }
291
+
292
+ if keyType == (kSecAttrKeyTypeECSECPrimeRandom as String) {
293
+ return x962ECPointToP256SPKI(publicKey)?.base64EncodedString()
294
+ }
295
+
296
+ if keyType == (kSecAttrKeyTypeRSA as String) {
297
+ return rsaPKCS1ToSPKI(publicKey)?.base64EncodedString()
298
+ }
299
+
300
+ guard let publicKeyData = SecKeyCopyExternalRepresentation(publicKey, nil) as Data? else {
301
+ return nil
302
+ }
303
+ return publicKeyData.base64EncodedString()
304
+ }
305
+
306
+ private func sign(alias: String, data: String, o: [String: Any]) throws -> String? {
307
+ let secKey = self.getSecKeyByAlias(alias, keyClass: kSecAttrKeyClassPrivate)
308
+ guard let secKey else { return nil }
307
309
 
308
- return SecureSigningModuleResult.KEY_PAIR_GENERATED.rawValue
310
+ let algoType = AlgorithmType(rawValue: o["algoType"] as! String)
311
+ let algo = self.toiOSAlgo(algorithm: algoType!)
312
+
313
+ var signingError: Unmanaged<CFError>?
314
+ let signatureCF = SecKeyCreateSignature(
315
+ secKey,
316
+ algo,
317
+ Data(data.utf8) as CFData,
318
+ &signingError
319
+ )
320
+
321
+ if let error = signingError?.takeRetainedValue() {
322
+ throw error as Error
323
+ }
324
+
325
+ guard let signatureCF else { return nil }
326
+ let signature = signatureCF as Data
327
+ return signature.base64EncodedString()
328
+ }
329
+
330
+ private func verify(alias: String, data: String, signature: String, o: [String: Any]) -> Bool? {
331
+ let secKey = self.getSecKeyByAlias(alias, keyClass: kSecAttrKeyClassPrivate)
332
+ guard let secKey else { return nil }
333
+
334
+ guard let publicKey = SecKeyCopyPublicKey(secKey) else { return nil }
335
+ guard let signatureData = Data(base64Encoded: signature) else { return nil }
336
+
337
+ let algoType = AlgorithmType(rawValue: o["algoType"] as! String)
338
+ let algo = self.toiOSAlgo(algorithm: algoType!)
339
+
340
+ let valid = SecKeyVerifySignature(
341
+ publicKey,
342
+ algo,
343
+ Data(data.utf8) as CFData,
344
+ signatureData as CFData,
345
+ nil
346
+ )
347
+ return valid
348
+ }
349
+
350
+ private func encrypt(alias: String, data: String, o: [String: Any]) -> String? {
351
+ let secKey = self.getSecKeyByAlias(alias, keyClass: kSecAttrKeyClassPublic)
352
+ guard let secKey else { return nil }
353
+ let publicKey = SecKeyCopyPublicKey(secKey)!
354
+
355
+ let algoType = AlgorithmType(rawValue: o["algoType"] as! String)
356
+ let algo = self.toiOSAlgo(algorithm: algoType!)
357
+
358
+ guard let encrypted = SecKeyCreateEncryptedData(
359
+ publicKey,
360
+ algo,
361
+ Data(data.utf8) as CFData,
362
+ nil
363
+ ) as Data? else {
364
+ return nil
365
+ }
366
+
367
+ return encrypted.base64EncodedString()
368
+ }
369
+
370
+ private func decrypt(alias: String, data: String, o: [String: Any]) -> String? {
371
+ let secKey = self.getSecKeyByAlias(alias, keyClass: kSecAttrKeyClassPrivate)
372
+ guard let secKey else { return nil }
373
+ guard let encryptedData = Data(base64Encoded: data) else { return nil }
374
+
375
+ let algoType = AlgorithmType(rawValue: o["algoType"] as! String)
376
+ let algo = self.toiOSAlgo(algorithm: algoType!)
377
+
378
+ guard let decrypted = SecKeyCreateDecryptedData(
379
+ secKey,
380
+ algo,
381
+ encryptedData as CFData,
382
+ nil
383
+ ) as Data? else {
384
+ return nil
385
+ }
386
+
387
+ return String(data: decrypted, encoding: .utf8)
388
+ }
389
+
390
+ public func definition() -> ModuleDefinition {
391
+
392
+ Name("DeviceCrypto")
393
+
394
+ Function("isAuthCheckAvailable") { () -> String in
395
+ return self.isAuthCheckAvailable()
396
+ }
397
+
398
+ Function("generateKeyPair") { (alias: String, o: [String: Any]) -> String in
399
+ return try self.generateKeyPair(alias: alias, o: o)
309
400
  }
310
401
 
311
402
  Function("removeKeyPair") { (alias: String) -> Bool in
@@ -317,93 +408,23 @@ public class DeviceCryptoModule: Module {
317
408
  }
318
409
 
319
410
  Function("getPublicKey") { (alias: String) -> String? in
320
- let secKey = self.getSecKeyByAlias(alias, keyClass: kSecAttrKeyClassPublic)
321
- guard let secKey else { return nil }
322
- return self.retrievePublicKey(secKey)
411
+ return self.getPublicKey(alias:alias)
323
412
  }
324
413
 
325
414
  AsyncFunction("sign") { (alias: String, data: String, o: [String: Any]) -> String? in
326
- let secKey = self.getSecKeyByAlias(alias, keyClass: kSecAttrKeyClassPrivate)
327
- guard let secKey else { return nil }
328
-
329
- let algoType = AlgorithmType(rawValue: o["algoType"] as! String)
330
- let algo = self.toiOSAlgo(algorithm: algoType!)
331
-
332
- var signingError: Unmanaged<CFError>?
333
- let signatureCF = SecKeyCreateSignature(
334
- secKey,
335
- algo,
336
- Data(data.utf8) as CFData,
337
- &signingError
338
- )
339
-
340
- if let error = signingError?.takeRetainedValue() {
341
- throw error as Error
342
- }
343
-
344
- guard let signatureCF else { return nil }
345
- let signature = signatureCF as Data
346
- return signature.base64EncodedString()
415
+ return try self.sign(alias: alias, data: data, o: o)
347
416
  }
348
417
 
349
418
  Function("verify") { (alias: String, data: String, signature: String, o: [String: Any]) -> Bool? in
350
- let secKey = self.getSecKeyByAlias(alias, keyClass: kSecAttrKeyClassPrivate)
351
- guard let secKey else { return nil }
352
-
353
- guard let publicKey = SecKeyCopyPublicKey(secKey) else { return nil }
354
- guard let signatureData = Data(base64Encoded: signature) else { return nil }
355
-
356
- let algoType = AlgorithmType(rawValue: o["algoType"] as! String)
357
- let algo = self.toiOSAlgo(algorithm: algoType!)
358
-
359
- let valid = SecKeyVerifySignature(
360
- publicKey,
361
- algo,
362
- Data(data.utf8) as CFData,
363
- signatureData as CFData,
364
- nil
365
- )
366
- return valid
419
+ return self.verify(alias: alias, data: data, signature: signature, o: o)
367
420
  }
368
421
 
369
422
  AsyncFunction("encrypt") { (alias: String, data: String, o: [String: Any]) -> String? in
370
- let secKey = self.getSecKeyByAlias(alias, keyClass: kSecAttrKeyClassPublic)
371
- guard let secKey else { return nil }
372
- let publicKey = SecKeyCopyPublicKey(secKey)!
373
-
374
- let algoType = AlgorithmType(rawValue: o["algoType"] as! String)
375
- let algo = self.toiOSAlgo(algorithm: algoType!)
376
-
377
- guard let encrypted = SecKeyCreateEncryptedData(
378
- publicKey,
379
- algo,
380
- Data(data.utf8) as CFData,
381
- nil
382
- ) as Data? else {
383
- return nil
384
- }
385
-
386
- return encrypted.base64EncodedString()
423
+ return self.encrypt(alias: alias, data: data, o: o)
387
424
  }
388
425
 
389
426
  AsyncFunction("decrypt") { (alias: String, data: String, o: [String: Any]) -> String? in
390
- let secKey = self.getSecKeyByAlias(alias, keyClass: kSecAttrKeyClassPrivate)
391
- guard let secKey else { return nil }
392
- guard let encryptedData = Data(base64Encoded: data) else { return nil }
393
-
394
- let algoType = AlgorithmType(rawValue: o["algoType"] as! String)
395
- let algo = self.toiOSAlgo(algorithm: algoType!)
396
-
397
- guard let decrypted = SecKeyCreateDecryptedData(
398
- secKey,
399
- algo,
400
- encryptedData as CFData,
401
- nil
402
- ) as Data? else {
403
- return nil
404
- }
405
-
406
- return String(data: decrypted, encoding: .utf8)
427
+ return self.decrypt(alias: alias, data: data, o: o)
407
428
  }
408
429
  }
409
430
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-device-crypto",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Crypto for Expo using device hardware capabilities",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",