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 +15 -3
- package/android/build.gradle +2 -2
- package/android/src/main/java/expo/modules/devicecrypto/DeviceCryptoModule.kt +251 -190
- package/build/DeviceCrypto.types.d.ts +2 -1
- package/build/DeviceCrypto.types.d.ts.map +1 -1
- package/build/DeviceCrypto.types.js +1 -0
- package/build/DeviceCrypto.types.js.map +1 -1
- package/ios/DeviceCrypto.podspec +1 -1
- package/ios/DeviceCrypto.swift +192 -169
- package/package.json +1 -1
- package/src/DeviceCrypto.types.ts +1 -0
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
|
|
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
|
-
|
|
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.
|
package/android/build.gradle
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
apply plugin: 'com.android.library'
|
|
2
2
|
|
|
3
3
|
group = 'expo.modules.devicecrypto'
|
|
4
|
-
version = '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.
|
|
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
|
|
8
|
-
import
|
|
9
|
-
import
|
|
10
|
-
import
|
|
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
|
|
20
|
-
import
|
|
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
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
|
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
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
114
|
+
KEY_ALGORITHM_EC,
|
|
114
115
|
"AndroidKeyStore"
|
|
115
116
|
)
|
|
116
117
|
|
|
117
118
|
val parameterSpec: KeyGenParameterSpec = KeyGenParameterSpec.Builder(
|
|
118
119
|
alias,
|
|
119
|
-
|
|
120
|
+
PURPOSE_SIGN or PURPOSE_VERIFY
|
|
120
121
|
).run {
|
|
121
|
-
setDigests(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
143
|
+
KEY_ALGORITHM_RSA,
|
|
139
144
|
"AndroidKeyStore"
|
|
140
145
|
)
|
|
141
146
|
|
|
142
147
|
val parameterSpec: KeyGenParameterSpec = KeyGenParameterSpec.Builder(
|
|
143
148
|
alias,
|
|
144
|
-
|
|
149
|
+
PURPOSE_ENCRYPT or PURPOSE_DECRYPT
|
|
145
150
|
).run {
|
|
146
|
-
|
|
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
|
-
|
|
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 ->
|
|
204
|
-
AuthMethod.PASSCODE_OR_BIOMETRIC ->
|
|
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
|
-
|
|
214
|
-
|
|
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
|
-
|
|
217
|
-
|
|
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
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
|
|
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
|
-
|
|
253
|
+
return GenerateKeyPairResult.KEY_PAIR_GENERATED
|
|
254
|
+
}
|
|
240
255
|
|
|
241
|
-
|
|
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
|
-
|
|
245
|
-
|
|
246
|
-
|
|
262
|
+
ks.deleteEntry(alias)
|
|
263
|
+
return true
|
|
264
|
+
}
|
|
247
265
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
266
|
+
private fun getAliases() = KeyStore.getInstance("AndroidKeyStore").apply {
|
|
267
|
+
load(null)
|
|
268
|
+
}.aliases().toList()
|
|
251
269
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
-
|
|
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
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
-
}.
|
|
315
|
-
|
|
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
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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
|
-
|
|
330
|
-
|
|
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
|
-
|
|
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
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
promise
|
|
342
|
-
|
|
343
|
-
|
|
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
|
-
|
|
361
|
+
val reqAuth = isKeyStoreRequireAuthentication(entry)
|
|
349
362
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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
|
-
|
|
357
|
-
|
|
370
|
+
val cipher = Cipher.getInstance(algo)
|
|
371
|
+
val ciphertext = Base64.decode(data, Base64.NO_WRAP)
|
|
358
372
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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
|
-
|
|
366
|
-
|
|
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;
|
|
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,
|
|
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"]}
|
package/ios/DeviceCrypto.podspec
CHANGED
package/ios/DeviceCrypto.swift
CHANGED
|
@@ -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 .
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
272
|
-
|
|
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
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
224
|
+
let secKey = self.getSecKeyByAlias(alias, keyClass: kSecAttrKeyClassPrivate)
|
|
225
|
+
if secKey != nil {
|
|
226
|
+
return SecureSigningModuleResult.KEY_PAIR_ALREADY_EXISTS.rawValue
|
|
227
|
+
}
|
|
279
228
|
|
|
280
|
-
|
|
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: "
|
|
240
|
+
userInfo: [NSLocalizedDescriptionKey: "INVALID_ALGORITHM_TYPE"]
|
|
285
241
|
)
|
|
286
|
-
|
|
242
|
+
}
|
|
287
243
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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