expo-device-crypto 0.1.1 → 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)/Keychain where available.
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
 
@@ -42,7 +42,13 @@ If you want to allow Face ID on iOS, add this to your app config:
42
42
  **Padding:** PKCS#1 v1.5. </br>
43
43
  **Use it for:** Encrypting small secrets that only the private key holder should decrypt.
44
44
 
45
- More coming soon
45
+ - #### `RSA_2048_OAEP_SHA1`
46
+
47
+ **Key size:** 2048 bits. </br>
48
+ **Padding:** OAEP with SHA-1 and MGF1. </br>
49
+ **Use it for:** Encrypting small secrets with stronger padding than PKCS#1 v1.5 when interoperating with OAEP SHA-1 systems.
50
+
51
+ **More coming soon...**
46
52
 
47
53
  ## Example: ECDSA with Biometric
48
54
 
@@ -170,4 +176,10 @@ const decrypted = await DeviceCrypto.decrypt(alias, encrypted ?? "", {
170
176
  - Decrypts Base64 `data` with private key.
171
177
  - Defaults: `algorithmType = RSA_2048_PKCS1`, `promptTitle = "Unlock"`, `promptSubtitle = "Enter your PIN to continue"`, `authMethod = PASSCODE_OR_BIOMETRIC`.
172
178
 
173
- > ✅ 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.1.1'
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.1.1"
38
+ versionName "0.2.1"
39
39
  }
40
40
  lintOptions {
41
41
  abortOnError false
@@ -1,26 +1,41 @@
1
1
  package expo.modules.devicecrypto
2
2
 
3
+ import android.content.Context
3
4
  import android.content.pm.PackageManager
4
5
  import android.security.keystore.KeyGenParameterSpec
5
6
  import android.security.keystore.KeyInfo
6
- import android.security.keystore.KeyProperties
7
- import expo.modules.kotlin.modules.Module
8
- import expo.modules.kotlin.modules.ModuleDefinition
9
- import java.security.KeyPairGenerator
10
- import java.security.KeyStore
7
+ import android.security.keystore.KeyProperties.AUTH_BIOMETRIC_STRONG
8
+ import android.security.keystore.KeyProperties.AUTH_DEVICE_CREDENTIAL
9
+ import android.security.keystore.KeyProperties.DIGEST_SHA1
10
+ import android.security.keystore.KeyProperties.DIGEST_SHA256
11
+ import android.security.keystore.KeyProperties.ENCRYPTION_PADDING_RSA_OAEP
12
+ import android.security.keystore.KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1
13
+ import android.security.keystore.KeyProperties.KEY_ALGORITHM_EC
14
+ import android.security.keystore.KeyProperties.KEY_ALGORITHM_RSA
15
+ import android.security.keystore.KeyProperties.PURPOSE_DECRYPT
16
+ import android.security.keystore.KeyProperties.PURPOSE_ENCRYPT
17
+ import android.security.keystore.KeyProperties.PURPOSE_SIGN
18
+ import android.security.keystore.KeyProperties.PURPOSE_VERIFY
11
19
  import android.util.Base64
12
20
  import androidx.biometric.BiometricManager
13
- import androidx.biometric.BiometricPrompt
14
21
  import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG
15
22
  import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL
23
+ import androidx.biometric.BiometricPrompt
16
24
  import androidx.core.content.ContextCompat
17
25
  import androidx.fragment.app.FragmentActivity
18
26
  import expo.modules.kotlin.Promise
19
- import java.security.Signature
20
- import android.widget.Toast
21
- import android.content.Context
27
+ import expo.modules.kotlin.modules.Module
28
+ import expo.modules.kotlin.modules.ModuleDefinition
22
29
  import java.security.KeyFactory
30
+ import java.security.KeyPairGenerator
31
+ import java.security.KeyStore
32
+ import java.security.Signature
33
+ import java.security.spec.AlgorithmParameterSpec
34
+ import java.security.spec.MGF1ParameterSpec
23
35
  import javax.crypto.Cipher
36
+ import javax.crypto.spec.OAEPParameterSpec
37
+ import javax.crypto.spec.PSource
38
+
24
39
 
25
40
  enum class GenerateKeyPairResult {
26
41
  KEY_PAIR_GENERATED,
@@ -42,6 +57,7 @@ enum class AuthMethod {
42
57
  enum class AlgorithmType {
43
58
  ECDSA_SECP256R1_SHA256,
44
59
  RSA_2048_PKCS1,
60
+ RSA_2048_OAEP_SHA1,
45
61
  }
46
62
 
47
63
  class DeviceCryptoModule : Module() {
@@ -49,16 +65,19 @@ class DeviceCryptoModule : Module() {
49
65
  return when (algorithm) {
50
66
  AlgorithmType.ECDSA_SECP256R1_SHA256 -> "SHA256withECDSA"
51
67
  AlgorithmType.RSA_2048_PKCS1 -> "RSA/ECB/PKCS1Padding"
68
+ AlgorithmType.RSA_2048_OAEP_SHA1 -> "RSA/ECB/OAEPwithSHA-1AndMGF1Padding"
52
69
  }
53
70
  }
54
71
 
55
- private fun isAuthCheckAvailable(): AuthCheckResult {
56
- val biometricManager = BiometricManager.from(appContext.reactContext as Context)
57
- return when (biometricManager.canAuthenticate(BIOMETRIC_STRONG or DEVICE_CREDENTIAL)) {
58
- BiometricManager.BIOMETRIC_SUCCESS -> AuthCheckResult.AVAILABLE
59
- BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> AuthCheckResult.NO_HARDWARE
60
- BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE -> AuthCheckResult.UNAVAILABLE
61
- else -> AuthCheckResult.UNAVAILABLE
72
+ private fun getOaepSpec(algoType: AlgorithmType): AlgorithmParameterSpec? {
73
+ return when (algoType) {
74
+ AlgorithmType.RSA_2048_OAEP_SHA1 -> OAEPParameterSpec(
75
+ "SHA-1",
76
+ "MGF1",
77
+ MGF1ParameterSpec.SHA1,
78
+ PSource.PSpecified.DEFAULT
79
+ )
80
+ else -> null
62
81
  }
63
82
  }
64
83
 
@@ -66,7 +85,7 @@ class DeviceCryptoModule : Module() {
66
85
  val pm = appContext.reactContext?.packageManager ?: return false
67
86
  val appAttestKeystore = pm.hasSystemFeature(PackageManager.FEATURE_KEYSTORE_APP_ATTEST_KEY)
68
87
  val hardwareKeystore = pm.hasSystemFeature(PackageManager.FEATURE_HARDWARE_KEYSTORE)
69
- return appAttestKeystore && hardwareKeystore
88
+ return appAttestKeystore || hardwareKeystore
70
89
  }
71
90
 
72
91
  private fun isStrongBoxAvailable(): Boolean {
@@ -75,10 +94,6 @@ class DeviceCryptoModule : Module() {
75
94
  return strongboxKeystore
76
95
  }
77
96
 
78
- private fun getAliases() = KeyStore.getInstance("AndroidKeyStore").apply {
79
- load(null)
80
- }.aliases().toList()
81
-
82
97
  private fun isKeyStoreRequireAuthentication(entry: KeyStore.PrivateKeyEntry): Boolean {
83
98
  val keyFactory = KeyFactory.getInstance(entry.privateKey.algorithm, "AndroidKeyStore")
84
99
  val keyInfo = keyFactory.getKeySpec(entry.privateKey, KeyInfo::class.java)
@@ -91,40 +106,26 @@ class DeviceCryptoModule : Module() {
91
106
  }.getEntry(alias, null)
92
107
  }
93
108
 
94
- private fun retrievePublicKey(ks: KeyStore.Entry?): String? {
95
- if (ks !is KeyStore.PrivateKeyEntry) return null
96
-
97
- val publicKeyBytes: ByteArray = ks.certificate?.publicKey?.encoded ?: return null
98
- return Base64.encodeToString(publicKeyBytes, Base64.NO_WRAP)
99
- }
100
-
101
- private fun removeKeyStoreEntry(alias: String): Boolean {
102
- val ks = KeyStore.getInstance("AndroidKeyStore").apply {
103
- load(null)
104
- }
105
- if (ks.getEntry(alias, null) == null) return false
106
-
107
- ks.deleteEntry(alias)
108
- return true
109
- }
110
-
111
- private fun buildECDSA_SECP256R1_SHA256(alias: String, reqAuth: Boolean): KeyPairGenerator {
109
+ private fun buildECDSA(
110
+ alias: String,
111
+ digest: String,
112
+ reqAuth: Boolean): KeyPairGenerator {
112
113
  val kpg: KeyPairGenerator = KeyPairGenerator.getInstance(
113
- KeyProperties.KEY_ALGORITHM_EC,
114
+ KEY_ALGORITHM_EC,
114
115
  "AndroidKeyStore"
115
116
  )
116
117
 
117
118
  val parameterSpec: KeyGenParameterSpec = KeyGenParameterSpec.Builder(
118
119
  alias,
119
- KeyProperties.PURPOSE_SIGN or KeyProperties.PURPOSE_VERIFY
120
+ PURPOSE_SIGN or PURPOSE_VERIFY
120
121
  ).run {
121
- setDigests(KeyProperties.DIGEST_SHA256)
122
+ setDigests(digest)
122
123
  setIsStrongBoxBacked(false) // TODO: Prefer strong box if available
123
124
  setUserAuthenticationRequired(reqAuth)
124
125
  if (reqAuth) {
125
126
  setUserAuthenticationParameters(
126
127
  30, // 30 seconds
127
- KeyProperties.AUTH_BIOMETRIC_STRONG or KeyProperties.AUTH_DEVICE_CREDENTIAL
128
+ AUTH_BIOMETRIC_STRONG or AUTH_DEVICE_CREDENTIAL
128
129
  )
129
130
  }
130
131
  build()
@@ -133,23 +134,30 @@ class DeviceCryptoModule : Module() {
133
134
  return kpg
134
135
  }
135
136
 
136
- private fun buildRSA_2048_PKCS1(alias: String, reqAuth: Boolean): KeyPairGenerator {
137
+ private fun buildRSA(
138
+ alias: String,
139
+ digest: String?,
140
+ padding: String,
141
+ reqAuth: Boolean): KeyPairGenerator {
137
142
  val kpg: KeyPairGenerator = KeyPairGenerator.getInstance(
138
- KeyProperties.KEY_ALGORITHM_RSA,
143
+ KEY_ALGORITHM_RSA,
139
144
  "AndroidKeyStore"
140
145
  )
141
146
 
142
147
  val parameterSpec: KeyGenParameterSpec = KeyGenParameterSpec.Builder(
143
148
  alias,
144
- KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
149
+ PURPOSE_ENCRYPT or PURPOSE_DECRYPT
145
150
  ).run {
146
- setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1)
151
+ if (digest != null) {
152
+ setDigests(digest)
153
+ }
154
+ setEncryptionPaddings(padding)
147
155
  setIsStrongBoxBacked(false) // TODO: Prefer strong box if available
148
156
  setUserAuthenticationRequired(reqAuth)
149
157
  if (reqAuth) {
150
158
  setUserAuthenticationParameters(
151
159
  30, // 30 seconds
152
- KeyProperties.AUTH_BIOMETRIC_STRONG or KeyProperties.AUTH_DEVICE_CREDENTIAL
160
+ AUTH_BIOMETRIC_STRONG or AUTH_DEVICE_CREDENTIAL
153
161
  )
154
162
  }
155
163
  build()
@@ -200,8 +208,8 @@ class DeviceCryptoModule : Module() {
200
208
  .setSubtitle(subtitle)
201
209
  .setAllowedAuthenticators(
202
210
  when (authMethod) {
203
- AuthMethod.PASSCODE -> BiometricManager.Authenticators.DEVICE_CREDENTIAL
204
- AuthMethod.PASSCODE_OR_BIOMETRIC -> BiometricManager.Authenticators.BIOMETRIC_STRONG or BiometricManager.Authenticators.DEVICE_CREDENTIAL
211
+ AuthMethod.PASSCODE -> DEVICE_CREDENTIAL
212
+ AuthMethod.PASSCODE_OR_BIOMETRIC -> BIOMETRIC_STRONG or DEVICE_CREDENTIAL
205
213
  }
206
214
  )
207
215
  .build()
@@ -210,176 +218,229 @@ class DeviceCryptoModule : Module() {
210
218
  }
211
219
  }
212
220
 
213
- override fun definition() = ModuleDefinition {
214
- 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
+ }
215
230
 
216
- Function("isAuthCheckAvailable") { ->
217
- 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
218
241
  }
219
242
 
220
- Function("generateKeyPair") { alias: String, o: Map<String, Any?> ->
221
- val reqAuth = o["reqAuth"] as Boolean
222
- if (reqAuth && isAuthCheckAvailable() != AuthCheckResult.AVAILABLE) {
223
- throw Exception("NO_AUTH_AVAILABLE")
224
- }
225
- if (!isKeyStoreAvailable()) {
226
- return@Function GenerateKeyPairResult.NOT_AVAILABLE
227
- }
228
- if (getKeyStoreEntry(alias) != null) {
229
- return@Function GenerateKeyPairResult.KEY_PAIR_ALREADY_EXISTS
230
- }
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
+ }
231
250
 
232
- val algoType = AlgorithmType.valueOf(o["algoType"] as String)
233
- val kpg = when (algoType) {
234
- AlgorithmType.ECDSA_SECP256R1_SHA256 -> buildECDSA_SECP256R1_SHA256(alias, reqAuth)
235
- AlgorithmType.RSA_2048_PKCS1 -> buildRSA_2048_PKCS1(alias, reqAuth)
236
- else -> throw Exception("INVALID_ALGORITHM_TYPE")
237
- }
251
+ kpg.generateKeyPair()
238
252
 
239
- kpg.generateKeyPair()
253
+ return GenerateKeyPairResult.KEY_PAIR_GENERATED
254
+ }
240
255
 
241
- return@Function GenerateKeyPairResult.KEY_PAIR_GENERATED
256
+ private fun removeKeyStoreEntry(alias: String): Boolean {
257
+ val ks = KeyStore.getInstance("AndroidKeyStore").apply {
258
+ load(null)
242
259
  }
260
+ if (ks.getEntry(alias, null) == null) return false
243
261
 
244
- Function("removeKeyPair") { alias: String ->
245
- return@Function removeKeyStoreEntry(alias)
246
- }
262
+ ks.deleteEntry(alias)
263
+ return true
264
+ }
247
265
 
248
- Function("aliases") {
249
- return@Function getAliases()
250
- }
266
+ private fun getAliases() = KeyStore.getInstance("AndroidKeyStore").apply {
267
+ load(null)
268
+ }.aliases().toList()
251
269
 
252
- Function("getPublicKey") { alias: String ->
253
- val entry = getKeyStoreEntry(alias)
254
- return@Function retrievePublicKey(entry)
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
255
283
  }
256
284
 
257
- AsyncFunction("sign") { alias: String,
258
- data: String,
259
- o: Map<String, Any?>,
260
- promise: Promise ->
261
- val entry = getKeyStoreEntry(alias)
262
- if (entry !is KeyStore.PrivateKeyEntry) {
263
- promise.resolve(null)
264
- return@AsyncFunction
265
- }
285
+ val reqAuth = isKeyStoreRequireAuthentication(entry)
266
286
 
267
- val reqAuth = isKeyStoreRequireAuthentication(entry);
268
-
269
- val title = o["title"] as String
270
- val subtitle = o["subtitle"] as String
271
- val authMethod = AuthMethod.valueOf(o["authMethod"] as String)
272
- val algoType = AlgorithmType.valueOf(o["algoType"] as String)
273
- val algo = toAndroidAlgo(algoType)
274
-
275
- if (!reqAuth) {
276
- val signature: ByteArray = Signature.getInstance(algo).apply {
277
- initSign(entry.privateKey)
278
- update(data.toByteArray(Charsets.UTF_8))
279
- }.sign()
280
- promise.resolve(Base64.encodeToString(signature, Base64.NO_WRAP))
281
- return@AsyncFunction
282
- } else {
283
- showAuthPrompt(
284
- onSuccess = {
285
- val signature: ByteArray = Signature.getInstance(algo).apply {
286
- initSign(entry.privateKey)
287
- update(data.toByteArray(Charsets.UTF_8))
288
- }.sign()
289
- promise.resolve(Base64.encodeToString(signature, Base64.NO_WRAP))
290
- },
291
- onError = { error ->
292
- promise.reject("ERR_AUTH_FAILED", error, null)
293
- },
294
- title = title,
295
- subtitle = subtitle,
296
- authMethod = authMethod
297
- )
298
- }
299
- }
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)
300
292
 
301
- Function("verify") { alias: String,
302
- data: String,
303
- signature: String,
304
- o: Map<String, Any?> ->
305
- val entry = getKeyStoreEntry(alias)
306
- if (entry !is KeyStore.PrivateKeyEntry) return@Function null
307
-
308
- val algoType = AlgorithmType.valueOf(o["algoType"] as String)
309
- val algo = toAndroidAlgo(algoType)
310
-
311
- val valid: Boolean = Signature.getInstance(algo).apply {
312
- initVerify(entry.certificate)
293
+ if (!reqAuth) {
294
+ val signature: ByteArray = Signature.getInstance(algo).apply {
295
+ initSign(entry.privateKey)
313
296
  update(data.toByteArray(Charsets.UTF_8))
314
- }.verify(Base64.decode(signature, Base64.NO_WRAP))
315
- return@Function true
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
+ )
316
316
  }
317
+ }
317
318
 
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
318
322
 
319
- AsyncFunction("encrypt") { alias: String,
320
- data: String,
321
- o: Map<String, Any?>,
322
- promise: Promise ->
323
- val entry = getKeyStoreEntry(alias)
324
- if (entry !is KeyStore.PrivateKeyEntry) {
325
- promise.resolve(null)
326
- return@AsyncFunction
327
- }
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
+ }
328
332
 
329
- val algoType = AlgorithmType.valueOf(o["algoType"] as String)
330
- val algo = toAndroidAlgo(algoType)
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
338
+ }
339
+
340
+ val algoType = AlgorithmType.valueOf(o["algoType"] as String)
341
+ val algo = toAndroidAlgo(algoType)
342
+ val oaepSpec = getOaepSpec(algoType)
331
343
 
332
- val cipher = Cipher.getInstance(algo)
344
+ val cipher = Cipher.getInstance(algo)
345
+ if (oaepSpec != null) {
346
+ cipher.init(Cipher.ENCRYPT_MODE, entry.certificate.publicKey, oaepSpec)
347
+ } else {
333
348
  cipher.init(Cipher.ENCRYPT_MODE, entry.certificate.publicKey)
334
- val encrypted: ByteArray = cipher.doFinal(data.toByteArray(Charsets.UTF_8))
335
- promise.resolve(Base64.encodeToString(encrypted, Base64.NO_WRAP))
336
349
  }
350
+ val encrypted: ByteArray = cipher.doFinal(data.toByteArray(Charsets.UTF_8))
351
+ promise.resolve(Base64.encodeToString(encrypted, Base64.NO_WRAP))
352
+ }
337
353
 
338
- AsyncFunction("decrypt") { alias: String,
339
- data: String,
340
- o: Map<String, Any?>,
341
- promise: Promise ->
342
- val entry = getKeyStoreEntry(alias)
343
- if (entry !is KeyStore.PrivateKeyEntry) {
344
- promise.resolve(null)
345
- return@AsyncFunction
346
- }
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
359
+ }
347
360
 
348
- val reqAuth = isKeyStoreRequireAuthentication(entry)
361
+ val reqAuth = isKeyStoreRequireAuthentication(entry)
349
362
 
350
- val title = o["title"] as String
351
- val subtitle = o["subtitle"] as String
352
- val authMethod = o["authMethod"] as String
353
- val algoType = AlgorithmType.valueOf(o["algoType"] as String)
354
- val algo = toAndroidAlgo(algoType)
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)
355
369
 
356
- val cipher = Cipher.getInstance(algo)
357
- val ciphertext = Base64.decode(data, Base64.NO_WRAP)
370
+ val cipher = Cipher.getInstance(algo)
371
+ val ciphertext = Base64.decode(data, Base64.NO_WRAP)
358
372
 
359
- if (!reqAuth) {
360
- cipher.init(Cipher.DECRYPT_MODE, entry.privateKey)
361
- val decrypted: ByteArray = cipher.doFinal(ciphertext)
362
- promise.resolve(String(decrypted, Charsets.UTF_8))
363
- return@AsyncFunction
373
+ if (!reqAuth) {
374
+ if (oaepSpec != null) {
375
+ cipher.init(Cipher.DECRYPT_MODE, entry.privateKey, oaepSpec)
364
376
  } else {
365
- showAuthPrompt(
366
- onSuccess = {
377
+ cipher.init(Cipher.DECRYPT_MODE, entry.privateKey)
378
+ }
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 {
367
388
  cipher.init(Cipher.DECRYPT_MODE, entry.privateKey)
368
- val decrypted: ByteArray = cipher.doFinal(ciphertext)
369
- promise.resolve(String(decrypted, Charsets.UTF_8))
370
- },
371
- onError = { error ->
372
- promise.reject("ERR_AUTH_FAILED", error, null)
373
- },
374
- title = title,
375
- subtitle = subtitle,
376
- authMethod = when (authMethod) {
377
- "PASSCODE" -> AuthMethod.PASSCODE
378
- "PASSCODE_OR_BIOMETRIC" -> AuthMethod.PASSCODE_OR_BIOMETRIC
379
- else -> AuthMethod.PASSCODE_OR_BIOMETRIC
380
389
  }
381
- )
382
- }
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
+ )
404
+ }
405
+ }
406
+
407
+ override fun definition() = ModuleDefinition {
408
+ Name("DeviceCrypto")
409
+
410
+ Function("isAuthCheckAvailable") { ->
411
+ return@Function isAuthCheckAvailable()
412
+ }
413
+
414
+ Function("generateKeyPair") { alias: String, o: Map<String, Any?> ->
415
+ return@Function generateKeyPair(alias, o)
416
+ }
417
+
418
+ Function("removeKeyPair") { alias: String ->
419
+ return@Function removeKeyStoreEntry(alias)
420
+ }
421
+
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)
383
444
  }
384
445
  }
385
446
  }
@@ -16,7 +16,8 @@ export declare enum SigningAlgorithm {
16
16
  ECDSA_SECP256R1_SHA256 = "ECDSA_SECP256R1_SHA256"
17
17
  }
18
18
  export declare enum EncryptionAlgorithm {
19
- RSA_2048_PKCS1 = "RSA_2048_PKCS1"
19
+ RSA_2048_PKCS1 = "RSA_2048_PKCS1",
20
+ RSA_2048_OAEP_SHA1 = "RSA_2048_OAEP_SHA1"
20
21
  }
21
22
  export interface GenerateKeyPairOptions {
22
23
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"DeviceCrypto.types.d.ts","sourceRoot":"","sources":["../src/DeviceCrypto.types.ts"],"names":[],"mappings":"AAAA,oBAAY,qBAAqB;IAC/B,kBAAkB,uBAAuB;IACzC,uBAAuB,4BAA4B;IACnD,aAAa,kBAAkB;CAChC;AAED,oBAAY,eAAe;IACzB,SAAS,cAAc;IACvB,WAAW,gBAAgB;IAC3B,WAAW,gBAAgB;CAC5B;AAED,oBAAY,UAAU;IACpB,QAAQ,aAAa;IACrB,qBAAqB,0BAA0B;CAChD;AAED,oBAAY,gBAAgB;IAC1B,sBAAsB,2BAA2B;CAClD;AAED,oBAAY,mBAAmB;IAC7B,cAAc,mBAAmB;CAClC;AAED,MAAM,WAAW,sBAAsB;IACrC;;OAEG;IACH,aAAa,EAAE,gBAAgB,GAAG,mBAAmB,CAAC;IACtD;;;;;;;OAOG;IACH,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC;;;;;;;;;;;;;;OAcG;IACH,UAAU,CAAC,EAAE,UAAU,CAAC;CACzB;AAED,UAAU,sBAAsB;IAC9B;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;;OAKG;IACH,UAAU,CAAC,EAAE,UAAU,CAAC;CACzB;AAED,UAAU,kBAAkB;IAC1B;;OAEG;IACH,aAAa,CAAC,EAAE,gBAAgB,CAAC;CAClC;AAED,UAAU,qBAAqB;IAC7B;;OAEG;IACH,aAAa,CAAC,EAAE,mBAAmB,CAAC;CACrC;AAED,MAAM,WAAW,WAAY,SAAQ,sBAAsB,EAAE,kBAAkB;CAAG;AAClF,MAAM,WAAW,aAAc,SAAQ,kBAAkB;CAAG;AAE5D,MAAM,WAAW,cAAe,SAAQ,sBAAsB,EAAE,qBAAqB;CAAG;AACxF,MAAM,WAAW,cAAe,SAAQ,qBAAqB;CAAG;AAEhE,MAAM,WAAW,mBAAmB;IAClC;;;OAGG;IACH,MAAM,CAAC,EAAE,KAAK,CAAC;CAChB"}
1
+ {"version":3,"file":"DeviceCrypto.types.d.ts","sourceRoot":"","sources":["../src/DeviceCrypto.types.ts"],"names":[],"mappings":"AAAA,oBAAY,qBAAqB;IAC/B,kBAAkB,uBAAuB;IACzC,uBAAuB,4BAA4B;IACnD,aAAa,kBAAkB;CAChC;AAED,oBAAY,eAAe;IACzB,SAAS,cAAc;IACvB,WAAW,gBAAgB;IAC3B,WAAW,gBAAgB;CAC5B;AAED,oBAAY,UAAU;IACpB,QAAQ,aAAa;IACrB,qBAAqB,0BAA0B;CAChD;AAED,oBAAY,gBAAgB;IAC1B,sBAAsB,2BAA2B;CAClD;AAED,oBAAY,mBAAmB;IAC7B,cAAc,mBAAmB;IACjC,kBAAkB,uBAAuB;CAC1C;AAED,MAAM,WAAW,sBAAsB;IACrC;;OAEG;IACH,aAAa,EAAE,gBAAgB,GAAG,mBAAmB,CAAC;IACtD;;;;;;;OAOG;IACH,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC;;;;;;;;;;;;;;OAcG;IACH,UAAU,CAAC,EAAE,UAAU,CAAC;CACzB;AAED,UAAU,sBAAsB;IAC9B;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;;OAKG;IACH,UAAU,CAAC,EAAE,UAAU,CAAC;CACzB;AAED,UAAU,kBAAkB;IAC1B;;OAEG;IACH,aAAa,CAAC,EAAE,gBAAgB,CAAC;CAClC;AAED,UAAU,qBAAqB;IAC7B;;OAEG;IACH,aAAa,CAAC,EAAE,mBAAmB,CAAC;CACrC;AAED,MAAM,WAAW,WAAY,SAAQ,sBAAsB,EAAE,kBAAkB;CAAG;AAClF,MAAM,WAAW,aAAc,SAAQ,kBAAkB;CAAG;AAE5D,MAAM,WAAW,cAAe,SAAQ,sBAAsB,EAAE,qBAAqB;CAAG;AACxF,MAAM,WAAW,cAAe,SAAQ,qBAAqB;CAAG;AAEhE,MAAM,WAAW,mBAAmB;IAClC;;;OAGG;IACH,MAAM,CAAC,EAAE,KAAK,CAAC;CAChB"}
@@ -22,5 +22,6 @@ export var SigningAlgorithm;
22
22
  export var EncryptionAlgorithm;
23
23
  (function (EncryptionAlgorithm) {
24
24
  EncryptionAlgorithm["RSA_2048_PKCS1"] = "RSA_2048_PKCS1";
25
+ EncryptionAlgorithm["RSA_2048_OAEP_SHA1"] = "RSA_2048_OAEP_SHA1";
25
26
  })(EncryptionAlgorithm || (EncryptionAlgorithm = {}));
26
27
  //# sourceMappingURL=DeviceCrypto.types.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"DeviceCrypto.types.js","sourceRoot":"","sources":["../src/DeviceCrypto.types.ts"],"names":[],"mappings":"AAAA,MAAM,CAAN,IAAY,qBAIX;AAJD,WAAY,qBAAqB;IAC/B,kEAAyC,CAAA;IACzC,4EAAmD,CAAA;IACnD,wDAA+B,CAAA;AACjC,CAAC,EAJW,qBAAqB,KAArB,qBAAqB,QAIhC;AAED,MAAM,CAAN,IAAY,eAIX;AAJD,WAAY,eAAe;IACzB,0CAAuB,CAAA;IACvB,8CAA2B,CAAA;IAC3B,8CAA2B,CAAA;AAC7B,CAAC,EAJW,eAAe,KAAf,eAAe,QAI1B;AAED,MAAM,CAAN,IAAY,UAGX;AAHD,WAAY,UAAU;IACpB,mCAAqB,CAAA;IACrB,6DAA+C,CAAA;AACjD,CAAC,EAHW,UAAU,KAAV,UAAU,QAGrB;AAED,MAAM,CAAN,IAAY,gBAEX;AAFD,WAAY,gBAAgB;IAC1B,qEAAiD,CAAA;AACnD,CAAC,EAFW,gBAAgB,KAAhB,gBAAgB,QAE3B;AAED,MAAM,CAAN,IAAY,mBAEX;AAFD,WAAY,mBAAmB;IAC7B,wDAAiC,CAAA;AACnC,CAAC,EAFW,mBAAmB,KAAnB,mBAAmB,QAE9B","sourcesContent":["export enum GenerateKeyPairResult {\n KEY_PAIR_GENERATED = \"KEY_PAIR_GENERATED\",\n KEY_PAIR_ALREADY_EXISTS = \"KEY_PAIR_ALREADY_EXISTS\",\n NOT_AVAILABLE = \"NOT_AVAILABLE\",\n}\n\nexport enum AuthCheckResult {\n AVAILABLE = \"AVAILABLE\",\n NO_HARDWARE = \"NO_HARDWARE\",\n UNAVAILABLE = \"UNAVAILABLE\",\n}\n\nexport enum AuthMethod {\n PASSCODE = \"PASSCODE\",\n PASSCODE_OR_BIOMETRIC = \"PASSCODE_OR_BIOMETRIC\",\n}\n\nexport enum SigningAlgorithm {\n ECDSA_SECP256R1_SHA256 = \"ECDSA_SECP256R1_SHA256\",\n}\n\nexport enum EncryptionAlgorithm {\n RSA_2048_PKCS1 = \"RSA_2048_PKCS1\",\n}\n\nexport interface GenerateKeyPairOptions {\n /**\n * The algorithm type of the key to generate.\n */\n algorithmType: SigningAlgorithm | EncryptionAlgorithm;\n /**\n * Whether to require authentication to sign.\n * Setting this to true will prompt biometric or passcode authentication before signing.\n *\n * Please check the *`isAuthCheckAvailable`* function to see if authentication is available.\n * If authentication is not available, generating a key pair will throw an error.\n * @default false\n */\n requireAuthentication?: boolean;\n /**\n * The method of authentication to use.\n * Note that on Android you have to define it when signing.\n *\n * If you want to allow to use Face ID, you need to add the following to your app.json config file:\n * ```json\n * \"ios\": {\n * \"infoPlist\": {\n * \"NSFaceIDUsageDescription\": \"We use Face ID to secure your data.\",\n * }\n * }\n * ```\n * @default AuthMethod.PASSCODE_OR_BIOMETRIC\n * @platform ios\n */\n authMethod?: AuthMethod;\n}\n\ninterface BaseSignDecryptOptions {\n /**\n * The title of the prompt to show when authentication is required.\n * @default \"Unlock\"\n * @platform android\n */\n promptTitle?: string;\n /**\n * The subtitle of the prompt to show when authentication is required.\n * @default \"Enter your PIN to continue\"\n * @platform android\n */\n promptSubtitle?: string;\n /**\n * The method of authentication to use.\n * Note that on iOS you have to define it when generating the key pair.\n * @default AuthMethod.PASSCODE_OR_BIOMETRIC\n * @platform android\n */\n authMethod?: AuthMethod;\n}\n\ninterface BaseSigningOptions {\n /**\n * The algorithm type of the key to use for signing.\n */\n algorithmType?: SigningAlgorithm;\n}\n\ninterface BaseEncryptionOptions {\n /**\n * The algorithm type of the key to use for encrypting.\n */\n algorithmType?: EncryptionAlgorithm;\n}\n\nexport interface SignOptions extends BaseSignDecryptOptions, BaseSigningOptions {}\nexport interface VerifyOptions extends BaseSigningOptions {}\n\nexport interface DecryptOptions extends BaseSignDecryptOptions, BaseEncryptionOptions {}\nexport interface EncryptOptions extends BaseEncryptionOptions {}\n\nexport interface GetPublicKeyOptions {\n /**\n * The format of the public key to return.\n * @default \"PEM\" (Base64 of PEM-encoded SubjectPublicKeyInfo (SPKI) for P‑256)\n */\n format?: \"PEM\";\n}\n"]}
1
+ {"version":3,"file":"DeviceCrypto.types.js","sourceRoot":"","sources":["../src/DeviceCrypto.types.ts"],"names":[],"mappings":"AAAA,MAAM,CAAN,IAAY,qBAIX;AAJD,WAAY,qBAAqB;IAC/B,kEAAyC,CAAA;IACzC,4EAAmD,CAAA;IACnD,wDAA+B,CAAA;AACjC,CAAC,EAJW,qBAAqB,KAArB,qBAAqB,QAIhC;AAED,MAAM,CAAN,IAAY,eAIX;AAJD,WAAY,eAAe;IACzB,0CAAuB,CAAA;IACvB,8CAA2B,CAAA;IAC3B,8CAA2B,CAAA;AAC7B,CAAC,EAJW,eAAe,KAAf,eAAe,QAI1B;AAED,MAAM,CAAN,IAAY,UAGX;AAHD,WAAY,UAAU;IACpB,mCAAqB,CAAA;IACrB,6DAA+C,CAAA;AACjD,CAAC,EAHW,UAAU,KAAV,UAAU,QAGrB;AAED,MAAM,CAAN,IAAY,gBAEX;AAFD,WAAY,gBAAgB;IAC1B,qEAAiD,CAAA;AACnD,CAAC,EAFW,gBAAgB,KAAhB,gBAAgB,QAE3B;AAED,MAAM,CAAN,IAAY,mBAGX;AAHD,WAAY,mBAAmB;IAC7B,wDAAiC,CAAA;IACjC,gEAAyC,CAAA;AAC3C,CAAC,EAHW,mBAAmB,KAAnB,mBAAmB,QAG9B","sourcesContent":["export enum GenerateKeyPairResult {\n KEY_PAIR_GENERATED = \"KEY_PAIR_GENERATED\",\n KEY_PAIR_ALREADY_EXISTS = \"KEY_PAIR_ALREADY_EXISTS\",\n NOT_AVAILABLE = \"NOT_AVAILABLE\",\n}\n\nexport enum AuthCheckResult {\n AVAILABLE = \"AVAILABLE\",\n NO_HARDWARE = \"NO_HARDWARE\",\n UNAVAILABLE = \"UNAVAILABLE\",\n}\n\nexport enum AuthMethod {\n PASSCODE = \"PASSCODE\",\n PASSCODE_OR_BIOMETRIC = \"PASSCODE_OR_BIOMETRIC\",\n}\n\nexport enum SigningAlgorithm {\n ECDSA_SECP256R1_SHA256 = \"ECDSA_SECP256R1_SHA256\",\n}\n\nexport enum EncryptionAlgorithm {\n RSA_2048_PKCS1 = \"RSA_2048_PKCS1\",\n RSA_2048_OAEP_SHA1 = \"RSA_2048_OAEP_SHA1\",\n}\n\nexport interface GenerateKeyPairOptions {\n /**\n * The algorithm type of the key to generate.\n */\n algorithmType: SigningAlgorithm | EncryptionAlgorithm;\n /**\n * Whether to require authentication to sign.\n * Setting this to true will prompt biometric or passcode authentication before signing.\n *\n * Please check the *`isAuthCheckAvailable`* function to see if authentication is available.\n * If authentication is not available, generating a key pair will throw an error.\n * @default false\n */\n requireAuthentication?: boolean;\n /**\n * The method of authentication to use.\n * Note that on Android you have to define it when signing.\n *\n * If you want to allow to use Face ID, you need to add the following to your app.json config file:\n * ```json\n * \"ios\": {\n * \"infoPlist\": {\n * \"NSFaceIDUsageDescription\": \"We use Face ID to secure your data.\",\n * }\n * }\n * ```\n * @default AuthMethod.PASSCODE_OR_BIOMETRIC\n * @platform ios\n */\n authMethod?: AuthMethod;\n}\n\ninterface BaseSignDecryptOptions {\n /**\n * The title of the prompt to show when authentication is required.\n * @default \"Unlock\"\n * @platform android\n */\n promptTitle?: string;\n /**\n * The subtitle of the prompt to show when authentication is required.\n * @default \"Enter your PIN to continue\"\n * @platform android\n */\n promptSubtitle?: string;\n /**\n * The method of authentication to use.\n * Note that on iOS you have to define it when generating the key pair.\n * @default AuthMethod.PASSCODE_OR_BIOMETRIC\n * @platform android\n */\n authMethod?: AuthMethod;\n}\n\ninterface BaseSigningOptions {\n /**\n * The algorithm type of the key to use for signing.\n */\n algorithmType?: SigningAlgorithm;\n}\n\ninterface BaseEncryptionOptions {\n /**\n * The algorithm type of the key to use for encrypting.\n */\n algorithmType?: EncryptionAlgorithm;\n}\n\nexport interface SignOptions extends BaseSignDecryptOptions, BaseSigningOptions {}\nexport interface VerifyOptions extends BaseSigningOptions {}\n\nexport interface DecryptOptions extends BaseSignDecryptOptions, BaseEncryptionOptions {}\nexport interface EncryptOptions extends BaseEncryptionOptions {}\n\nexport interface GetPublicKeyOptions {\n /**\n * The format of the public key to return.\n * @default \"PEM\" (Base64 of PEM-encoded SubjectPublicKeyInfo (SPKI) for P‑256)\n */\n format?: \"PEM\";\n}\n"]}
@@ -1,6 +1,6 @@
1
1
  Pod::Spec.new do |s|
2
2
  s.name = 'DeviceCrypto'
3
- s.version = '0.1.1'
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'
@@ -34,7 +34,7 @@ public class DeviceCryptoModule: Module {
34
34
  case .RSA_2048_PKCS1:
35
35
  return .rsaEncryptionPKCS1
36
36
  case .RSA_2048_OAEP_SHA1:
37
- return .rsaEncryptionOAEP
37
+ return .rsaEncryptionOAEPSHA1
38
38
  }
39
39
  }
40
40
 
@@ -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,46 +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
- default:
299
- throw NSError(
300
- domain: "DeviceCrypto",
301
- code: 1,
302
- userInfo: [NSLocalizedDescriptionKey: "INVALID_ALGORITHM_TYPE"]
303
- )
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
304
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 }
305
309
 
306
- 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)
307
400
  }
308
401
 
309
402
  Function("removeKeyPair") { (alias: String) -> Bool in
@@ -315,93 +408,23 @@ public class DeviceCryptoModule: Module {
315
408
  }
316
409
 
317
410
  Function("getPublicKey") { (alias: String) -> String? in
318
- let secKey = self.getSecKeyByAlias(alias, keyClass: kSecAttrKeyClassPublic)
319
- guard let secKey else { return nil }
320
- return self.retrievePublicKey(secKey)
411
+ return self.getPublicKey(alias:alias)
321
412
  }
322
413
 
323
414
  AsyncFunction("sign") { (alias: String, data: String, o: [String: Any]) -> String? in
324
- let secKey = self.getSecKeyByAlias(alias, keyClass: kSecAttrKeyClassPrivate)
325
- guard let secKey else { return nil }
326
-
327
- let algoType = AlgorithmType(rawValue: o["algoType"] as! String)
328
- let algo = self.toiOSAlgo(algorithm: algoType!)
329
-
330
- var signingError: Unmanaged<CFError>?
331
- let signatureCF = SecKeyCreateSignature(
332
- secKey,
333
- algo,
334
- Data(data.utf8) as CFData,
335
- &signingError
336
- )
337
-
338
- if let error = signingError?.takeRetainedValue() {
339
- throw error as Error
340
- }
341
-
342
- guard let signatureCF else { return nil }
343
- let signature = signatureCF as Data
344
- return signature.base64EncodedString()
415
+ return try self.sign(alias: alias, data: data, o: o)
345
416
  }
346
417
 
347
418
  Function("verify") { (alias: String, data: String, signature: String, o: [String: Any]) -> Bool? in
348
- let secKey = self.getSecKeyByAlias(alias, keyClass: kSecAttrKeyClassPrivate)
349
- guard let secKey else { return nil }
350
-
351
- guard let publicKey = SecKeyCopyPublicKey(secKey) else { return nil }
352
- guard let signatureData = Data(base64Encoded: signature) else { return nil }
353
-
354
- let algoType = AlgorithmType(rawValue: o["algoType"] as! String)
355
- let algo = self.toiOSAlgo(algorithm: algoType!)
356
-
357
- let valid = SecKeyVerifySignature(
358
- publicKey,
359
- algo,
360
- Data(data.utf8) as CFData,
361
- signatureData as CFData,
362
- nil
363
- )
364
- return valid
419
+ return self.verify(alias: alias, data: data, signature: signature, o: o)
365
420
  }
366
421
 
367
422
  AsyncFunction("encrypt") { (alias: String, data: String, o: [String: Any]) -> String? in
368
- let secKey = self.getSecKeyByAlias(alias, keyClass: kSecAttrKeyClassPublic)
369
- guard let secKey else { return nil }
370
- let publicKey = SecKeyCopyPublicKey(secKey)!
371
-
372
- let algoType = AlgorithmType(rawValue: o["algoType"] as! String)
373
- let algo = self.toiOSAlgo(algorithm: algoType!)
374
-
375
- guard let encrypted = SecKeyCreateEncryptedData(
376
- publicKey,
377
- algo,
378
- Data(data.utf8) as CFData,
379
- nil
380
- ) as Data? else {
381
- return nil
382
- }
383
-
384
- return encrypted.base64EncodedString()
423
+ return self.encrypt(alias: alias, data: data, o: o)
385
424
  }
386
425
 
387
426
  AsyncFunction("decrypt") { (alias: String, data: String, o: [String: Any]) -> String? in
388
- let secKey = self.getSecKeyByAlias(alias, keyClass: kSecAttrKeyClassPrivate)
389
- guard let secKey else { return nil }
390
- guard let encryptedData = Data(base64Encoded: data) else { return nil }
391
-
392
- let algoType = AlgorithmType(rawValue: o["algoType"] as! String)
393
- let algo = self.toiOSAlgo(algorithm: algoType!)
394
-
395
- guard let decrypted = SecKeyCreateDecryptedData(
396
- secKey,
397
- algo,
398
- encryptedData as CFData,
399
- nil
400
- ) as Data? else {
401
- return nil
402
- }
403
-
404
- return String(data: decrypted, encoding: .utf8)
427
+ return self.decrypt(alias: alias, data: data, o: o)
405
428
  }
406
429
  }
407
430
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-device-crypto",
3
- "version": "0.1.1",
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",
@@ -21,6 +21,7 @@ export enum SigningAlgorithm {
21
21
 
22
22
  export enum EncryptionAlgorithm {
23
23
  RSA_2048_PKCS1 = "RSA_2048_PKCS1",
24
+ RSA_2048_OAEP_SHA1 = "RSA_2048_OAEP_SHA1",
24
25
  }
25
26
 
26
27
  export interface GenerateKeyPairOptions {