expo-device-crypto 0.2.0 → 0.2.2
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 +34 -3
- package/android/build.gradle +2 -2
- package/android/src/main/java/expo/modules/devicecrypto/DeviceCryptoModule.kt +206 -190
- package/build/DeviceCrypto.types.d.ts +10 -4
- package/build/DeviceCrypto.types.d.ts.map +1 -1
- package/build/DeviceCrypto.types.js.map +1 -1
- package/build/DeviceCryptoModule.d.ts +6 -15
- package/build/DeviceCryptoModule.d.ts.map +1 -1
- package/build/DeviceCryptoModule.js +7 -15
- package/build/DeviceCryptoModule.js.map +1 -1
- package/ios/DeviceCrypto.podspec +1 -1
- package/ios/DeviceCrypto.swift +191 -170
- package/package.json +1 -1
- package/src/DeviceCrypto.types.ts +10 -4
- package/src/DeviceCryptoModule.ts +8 -15
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> ⚠️ This module is currently in beta and is not suitable for production use.
|
|
4
4
|
|
|
5
|
-
🔒 Hardware-backed cryptography for Expo apps using [Android Keystore](https://developer.android.com/privacy-and-security/keystore) and Apple [Secure Enclave](https://developer.apple.com/documentation/security/protecting-keys-with-the-secure-enclave).
|
|
5
|
+
🔒 Hardware-backed cryptography for Expo apps using [Android Keystore](https://developer.android.com/privacy-and-security/keystore)/[Strong Box](https://developer.android.com/privacy-and-security/keystore#StrongBoxKeyMint) and Apple [Secure Enclave](https://developer.apple.com/documentation/security/protecting-keys-with-the-secure-enclave)/[Keychain](https://developer.apple.com/documentation/security/keychain-services).
|
|
6
6
|
|
|
7
7
|
## Installation
|
|
8
8
|
|
|
@@ -48,7 +48,7 @@ If you want to allow Face ID on iOS, add this to your app config:
|
|
|
48
48
|
**Padding:** OAEP with SHA-1 and MGF1. </br>
|
|
49
49
|
**Use it for:** Encrypting small secrets with stronger padding than PKCS#1 v1.5 when interoperating with OAEP SHA-1 systems.
|
|
50
50
|
|
|
51
|
-
More coming soon
|
|
51
|
+
**More coming soon...**
|
|
52
52
|
|
|
53
53
|
## Example: ECDSA with Biometric
|
|
54
54
|
|
|
@@ -176,4 +176,35 @@ const decrypted = await DeviceCrypto.decrypt(alias, encrypted ?? "", {
|
|
|
176
176
|
- Decrypts Base64 `data` with private key.
|
|
177
177
|
- Defaults: `algorithmType = RSA_2048_PKCS1`, `promptTitle = "Unlock"`, `promptSubtitle = "Enter your PIN to continue"`, `authMethod = PASSCODE_OR_BIOMETRIC`.
|
|
178
178
|
|
|
179
|
-
|
|
179
|
+
**Android only methods**
|
|
180
|
+
|
|
181
|
+
- `isStrongBoxAvailable(): boolean`
|
|
182
|
+
- Returns `true` if StrongBox Keystore is supported on the device.
|
|
183
|
+
|
|
184
|
+
> ✅ JSDoc type definitions are available in `./src/DeviceCryptoModule.ts`.
|
|
185
|
+
|
|
186
|
+
## Important
|
|
187
|
+
|
|
188
|
+
### Enable Strong Box on Android
|
|
189
|
+
|
|
190
|
+
Use `preferStrongBox: true` when generating a key pair to request [StrongBox-backed](https://developer.android.com/privacy-and-security/keystore#StrongBoxKeyMint) key storage when the device supports it.
|
|
191
|
+
|
|
192
|
+
```ts
|
|
193
|
+
import DeviceCrypto, { SigningAlgorithm } from "expo-device-crypto";
|
|
194
|
+
|
|
195
|
+
const alias = "user-signing-key";
|
|
196
|
+
|
|
197
|
+
// Optional: check availability first.
|
|
198
|
+
const hasStrongBox = DeviceCrypto.isStrongBoxAvailable();
|
|
199
|
+
|
|
200
|
+
await DeviceCrypto.generateKeyPair(alias, {
|
|
201
|
+
algorithmType: SigningAlgorithm.ECDSA_SECP256R1_SHA256,
|
|
202
|
+
preferStrongBox: true,
|
|
203
|
+
});
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
If StrongBox is not available, Android falls back to the Trusted Execution Environment (TEE).
|
|
207
|
+
|
|
208
|
+
### RSA algorithms on iOS
|
|
209
|
+
|
|
210
|
+
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.2.
|
|
4
|
+
version = '0.2.2'
|
|
5
5
|
|
|
6
6
|
def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
|
|
7
7
|
apply from: expoModulesCorePlugin
|
|
@@ -35,7 +35,7 @@ android {
|
|
|
35
35
|
namespace "expo.modules.devicecrypto"
|
|
36
36
|
defaultConfig {
|
|
37
37
|
versionCode 1
|
|
38
|
-
versionName "0.2.
|
|
38
|
+
versionName "0.2.2"
|
|
39
39
|
}
|
|
40
40
|
lintOptions {
|
|
41
41
|
abortOnError false
|
|
@@ -81,21 +81,11 @@ class DeviceCryptoModule : Module() {
|
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
private fun isAuthCheckAvailable(): AuthCheckResult {
|
|
85
|
-
val biometricManager = BiometricManager.from(appContext.reactContext as Context)
|
|
86
|
-
return when (biometricManager.canAuthenticate(BIOMETRIC_STRONG or DEVICE_CREDENTIAL)) {
|
|
87
|
-
BiometricManager.BIOMETRIC_SUCCESS -> AuthCheckResult.AVAILABLE
|
|
88
|
-
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> AuthCheckResult.NO_HARDWARE
|
|
89
|
-
BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE -> AuthCheckResult.UNAVAILABLE
|
|
90
|
-
else -> AuthCheckResult.UNAVAILABLE
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
84
|
private fun isKeyStoreAvailable(): Boolean {
|
|
95
85
|
val pm = appContext.reactContext?.packageManager ?: return false
|
|
96
86
|
val appAttestKeystore = pm.hasSystemFeature(PackageManager.FEATURE_KEYSTORE_APP_ATTEST_KEY)
|
|
97
87
|
val hardwareKeystore = pm.hasSystemFeature(PackageManager.FEATURE_HARDWARE_KEYSTORE)
|
|
98
|
-
return appAttestKeystore
|
|
88
|
+
return appAttestKeystore || hardwareKeystore
|
|
99
89
|
}
|
|
100
90
|
|
|
101
91
|
private fun isStrongBoxAvailable(): Boolean {
|
|
@@ -104,10 +94,6 @@ class DeviceCryptoModule : Module() {
|
|
|
104
94
|
return strongboxKeystore
|
|
105
95
|
}
|
|
106
96
|
|
|
107
|
-
private fun getAliases() = KeyStore.getInstance("AndroidKeyStore").apply {
|
|
108
|
-
load(null)
|
|
109
|
-
}.aliases().toList()
|
|
110
|
-
|
|
111
97
|
private fun isKeyStoreRequireAuthentication(entry: KeyStore.PrivateKeyEntry): Boolean {
|
|
112
98
|
val keyFactory = KeyFactory.getInstance(entry.privateKey.algorithm, "AndroidKeyStore")
|
|
113
99
|
val keyInfo = keyFactory.getKeySpec(entry.privateKey, KeyInfo::class.java)
|
|
@@ -120,27 +106,11 @@ class DeviceCryptoModule : Module() {
|
|
|
120
106
|
}.getEntry(alias, null)
|
|
121
107
|
}
|
|
122
108
|
|
|
123
|
-
private fun retrievePublicKey(ks: KeyStore.Entry?): String? {
|
|
124
|
-
if (ks !is KeyStore.PrivateKeyEntry) return null
|
|
125
|
-
|
|
126
|
-
val publicKeyBytes: ByteArray = ks.certificate?.publicKey?.encoded ?: return null
|
|
127
|
-
return Base64.encodeToString(publicKeyBytes, Base64.NO_WRAP)
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
private fun removeKeyStoreEntry(alias: String): Boolean {
|
|
131
|
-
val ks = KeyStore.getInstance("AndroidKeyStore").apply {
|
|
132
|
-
load(null)
|
|
133
|
-
}
|
|
134
|
-
if (ks.getEntry(alias, null) == null) return false
|
|
135
|
-
|
|
136
|
-
ks.deleteEntry(alias)
|
|
137
|
-
return true
|
|
138
|
-
}
|
|
139
|
-
|
|
140
109
|
private fun buildECDSA(
|
|
141
110
|
alias: String,
|
|
142
111
|
digest: String,
|
|
143
|
-
reqAuth: Boolean
|
|
112
|
+
reqAuth: Boolean,
|
|
113
|
+
strongBox: Boolean): KeyPairGenerator {
|
|
144
114
|
val kpg: KeyPairGenerator = KeyPairGenerator.getInstance(
|
|
145
115
|
KEY_ALGORITHM_EC,
|
|
146
116
|
"AndroidKeyStore"
|
|
@@ -151,7 +121,7 @@ class DeviceCryptoModule : Module() {
|
|
|
151
121
|
PURPOSE_SIGN or PURPOSE_VERIFY
|
|
152
122
|
).run {
|
|
153
123
|
setDigests(digest)
|
|
154
|
-
setIsStrongBoxBacked(
|
|
124
|
+
setIsStrongBoxBacked(strongBox)
|
|
155
125
|
setUserAuthenticationRequired(reqAuth)
|
|
156
126
|
if (reqAuth) {
|
|
157
127
|
setUserAuthenticationParameters(
|
|
@@ -169,7 +139,8 @@ class DeviceCryptoModule : Module() {
|
|
|
169
139
|
alias: String,
|
|
170
140
|
digest: String?,
|
|
171
141
|
padding: String,
|
|
172
|
-
reqAuth: Boolean
|
|
142
|
+
reqAuth: Boolean,
|
|
143
|
+
strongBox: Boolean): KeyPairGenerator {
|
|
173
144
|
val kpg: KeyPairGenerator = KeyPairGenerator.getInstance(
|
|
174
145
|
KEY_ALGORITHM_RSA,
|
|
175
146
|
"AndroidKeyStore"
|
|
@@ -183,7 +154,7 @@ class DeviceCryptoModule : Module() {
|
|
|
183
154
|
setDigests(digest)
|
|
184
155
|
}
|
|
185
156
|
setEncryptionPaddings(padding)
|
|
186
|
-
setIsStrongBoxBacked(
|
|
157
|
+
setIsStrongBoxBacked(strongBox)
|
|
187
158
|
setUserAuthenticationRequired(reqAuth)
|
|
188
159
|
if (reqAuth) {
|
|
189
160
|
setUserAuthenticationParameters(
|
|
@@ -249,191 +220,236 @@ class DeviceCryptoModule : Module() {
|
|
|
249
220
|
}
|
|
250
221
|
}
|
|
251
222
|
|
|
252
|
-
|
|
253
|
-
|
|
223
|
+
private fun isAuthCheckAvailable(): AuthCheckResult {
|
|
224
|
+
val biometricManager = BiometricManager.from(appContext.reactContext as Context)
|
|
225
|
+
return when (biometricManager.canAuthenticate(BIOMETRIC_STRONG or DEVICE_CREDENTIAL)) {
|
|
226
|
+
BiometricManager.BIOMETRIC_SUCCESS -> AuthCheckResult.AVAILABLE
|
|
227
|
+
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> AuthCheckResult.NO_HARDWARE
|
|
228
|
+
BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE -> AuthCheckResult.UNAVAILABLE
|
|
229
|
+
else -> AuthCheckResult.UNAVAILABLE
|
|
230
|
+
}
|
|
231
|
+
}
|
|
254
232
|
|
|
255
|
-
|
|
256
|
-
|
|
233
|
+
private fun generateKeyPair(alias: String, o: Map<String, Any?>): GenerateKeyPairResult {
|
|
234
|
+
val reqAuth = o["reqAuth"] as Boolean
|
|
235
|
+
if (reqAuth && isAuthCheckAvailable() != AuthCheckResult.AVAILABLE) {
|
|
236
|
+
throw Exception("NO_AUTH_AVAILABLE")
|
|
237
|
+
}
|
|
238
|
+
if (!isKeyStoreAvailable()) {
|
|
239
|
+
return GenerateKeyPairResult.NOT_AVAILABLE
|
|
240
|
+
}
|
|
241
|
+
if (getKeyStoreEntry(alias) != null) {
|
|
242
|
+
return GenerateKeyPairResult.KEY_PAIR_ALREADY_EXISTS
|
|
257
243
|
}
|
|
258
244
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
throw Exception("NO_AUTH_AVAILABLE")
|
|
263
|
-
}
|
|
264
|
-
if (!isKeyStoreAvailable()) {
|
|
265
|
-
return@Function GenerateKeyPairResult.NOT_AVAILABLE
|
|
266
|
-
}
|
|
267
|
-
if (getKeyStoreEntry(alias) != null) {
|
|
268
|
-
return@Function GenerateKeyPairResult.KEY_PAIR_ALREADY_EXISTS
|
|
269
|
-
}
|
|
245
|
+
val algoType = AlgorithmType.valueOf(o["algoType"] as String)
|
|
246
|
+
val preferStrongBox = o["preferStrongBox"] as Boolean
|
|
247
|
+
val strongBox = preferStrongBox && isStrongBoxAvailable()
|
|
270
248
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
249
|
+
val kpg = when (algoType) {
|
|
250
|
+
AlgorithmType.ECDSA_SECP256R1_SHA256 -> buildECDSA(alias, DIGEST_SHA256, reqAuth, strongBox)
|
|
251
|
+
AlgorithmType.RSA_2048_PKCS1 -> buildRSA(alias, null, ENCRYPTION_PADDING_RSA_PKCS1, reqAuth, strongBox)
|
|
252
|
+
AlgorithmType.RSA_2048_OAEP_SHA1 -> buildRSA(alias, DIGEST_SHA1, ENCRYPTION_PADDING_RSA_OAEP, reqAuth, strongBox)
|
|
253
|
+
else -> throw Exception("INVALID_ALGORITHM_TYPE")
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
kpg.generateKeyPair()
|
|
278
257
|
|
|
279
|
-
|
|
258
|
+
return GenerateKeyPairResult.KEY_PAIR_GENERATED
|
|
259
|
+
}
|
|
280
260
|
|
|
281
|
-
|
|
261
|
+
private fun removeKeyStoreEntry(alias: String): Boolean {
|
|
262
|
+
val ks = KeyStore.getInstance("AndroidKeyStore").apply {
|
|
263
|
+
load(null)
|
|
282
264
|
}
|
|
265
|
+
if (ks.getEntry(alias, null) == null) return false
|
|
283
266
|
|
|
284
|
-
|
|
285
|
-
|
|
267
|
+
ks.deleteEntry(alias)
|
|
268
|
+
return true
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
private fun getAliases() = KeyStore.getInstance("AndroidKeyStore").apply {
|
|
272
|
+
load(null)
|
|
273
|
+
}.aliases().toList()
|
|
274
|
+
|
|
275
|
+
private fun getPublicKey(alias: String): String? {
|
|
276
|
+
val ks = getKeyStoreEntry(alias)
|
|
277
|
+
if (ks !is KeyStore.PrivateKeyEntry) return null
|
|
278
|
+
|
|
279
|
+
val publicKeyBytes: ByteArray = ks.certificate?.publicKey?.encoded ?: return null
|
|
280
|
+
return Base64.encodeToString(publicKeyBytes, Base64.NO_WRAP)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
private fun sign(alias: String, data: String, o: Map<String, Any?>, promise: Promise) {
|
|
284
|
+
val entry = getKeyStoreEntry(alias)
|
|
285
|
+
if (entry !is KeyStore.PrivateKeyEntry) {
|
|
286
|
+
promise.resolve(null)
|
|
287
|
+
return
|
|
286
288
|
}
|
|
287
289
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
+
val reqAuth = isKeyStoreRequireAuthentication(entry)
|
|
291
|
+
|
|
292
|
+
val title = o["title"] as String
|
|
293
|
+
val subtitle = o["subtitle"] as String
|
|
294
|
+
val authMethod = AuthMethod.valueOf(o["authMethod"] as String)
|
|
295
|
+
val algoType = AlgorithmType.valueOf(o["algoType"] as String)
|
|
296
|
+
val algo = toAndroidAlgo(algoType)
|
|
297
|
+
|
|
298
|
+
if (!reqAuth) {
|
|
299
|
+
val signature: ByteArray = Signature.getInstance(algo).apply {
|
|
300
|
+
initSign(entry.privateKey)
|
|
301
|
+
update(data.toByteArray(Charsets.UTF_8))
|
|
302
|
+
}.sign()
|
|
303
|
+
promise.resolve(Base64.encodeToString(signature, Base64.NO_WRAP))
|
|
304
|
+
return
|
|
305
|
+
} else {
|
|
306
|
+
showAuthPrompt(
|
|
307
|
+
onSuccess = {
|
|
308
|
+
val signature: ByteArray = Signature.getInstance(algo).apply {
|
|
309
|
+
initSign(entry.privateKey)
|
|
310
|
+
update(data.toByteArray(Charsets.UTF_8))
|
|
311
|
+
}.sign()
|
|
312
|
+
promise.resolve(Base64.encodeToString(signature, Base64.NO_WRAP))
|
|
313
|
+
},
|
|
314
|
+
onError = { error ->
|
|
315
|
+
promise.reject("ERR_AUTH_FAILED", error, null)
|
|
316
|
+
},
|
|
317
|
+
title = title,
|
|
318
|
+
subtitle = subtitle,
|
|
319
|
+
authMethod = authMethod
|
|
320
|
+
)
|
|
290
321
|
}
|
|
322
|
+
}
|
|
291
323
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
324
|
+
private fun verify(alias: String, data: String, signature: String, o: Map<String, Any?>): Boolean? {
|
|
325
|
+
val entry = getKeyStoreEntry(alias)
|
|
326
|
+
if (entry !is KeyStore.PrivateKeyEntry) return null
|
|
327
|
+
|
|
328
|
+
val algoType = AlgorithmType.valueOf(o["algoType"] as String)
|
|
329
|
+
val algo = toAndroidAlgo(algoType)
|
|
330
|
+
|
|
331
|
+
val valid: Boolean = Signature.getInstance(algo).apply {
|
|
332
|
+
initVerify(entry.certificate)
|
|
333
|
+
update(data.toByteArray(Charsets.UTF_8))
|
|
334
|
+
}.verify(Base64.decode(signature, Base64.NO_WRAP))
|
|
335
|
+
return valid
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
private fun encrypt(alias: String, data: String, o: Map<String, Any?>, promise: Promise) {
|
|
339
|
+
val entry = getKeyStoreEntry(alias)
|
|
340
|
+
if (entry !is KeyStore.PrivateKeyEntry) {
|
|
341
|
+
promise.resolve(null)
|
|
342
|
+
return
|
|
295
343
|
}
|
|
296
344
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
promise: Promise ->
|
|
301
|
-
val entry = getKeyStoreEntry(alias)
|
|
302
|
-
if (entry !is KeyStore.PrivateKeyEntry) {
|
|
303
|
-
promise.resolve(null)
|
|
304
|
-
return@AsyncFunction
|
|
305
|
-
}
|
|
345
|
+
val algoType = AlgorithmType.valueOf(o["algoType"] as String)
|
|
346
|
+
val algo = toAndroidAlgo(algoType)
|
|
347
|
+
val oaepSpec = getOaepSpec(algoType)
|
|
306
348
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
val algoType = AlgorithmType.valueOf(o["algoType"] as String)
|
|
313
|
-
val algo = toAndroidAlgo(algoType)
|
|
314
|
-
|
|
315
|
-
if (!reqAuth) {
|
|
316
|
-
val signature: ByteArray = Signature.getInstance(algo).apply {
|
|
317
|
-
initSign(entry.privateKey)
|
|
318
|
-
update(data.toByteArray(Charsets.UTF_8))
|
|
319
|
-
}.sign()
|
|
320
|
-
promise.resolve(Base64.encodeToString(signature, Base64.NO_WRAP))
|
|
321
|
-
return@AsyncFunction
|
|
322
|
-
} else {
|
|
323
|
-
showAuthPrompt(
|
|
324
|
-
onSuccess = {
|
|
325
|
-
val signature: ByteArray = Signature.getInstance(algo).apply {
|
|
326
|
-
initSign(entry.privateKey)
|
|
327
|
-
update(data.toByteArray(Charsets.UTF_8))
|
|
328
|
-
}.sign()
|
|
329
|
-
promise.resolve(Base64.encodeToString(signature, Base64.NO_WRAP))
|
|
330
|
-
},
|
|
331
|
-
onError = { error ->
|
|
332
|
-
promise.reject("ERR_AUTH_FAILED", error, null)
|
|
333
|
-
},
|
|
334
|
-
title = title,
|
|
335
|
-
subtitle = subtitle,
|
|
336
|
-
authMethod = authMethod
|
|
337
|
-
)
|
|
338
|
-
}
|
|
349
|
+
val cipher = Cipher.getInstance(algo)
|
|
350
|
+
if (oaepSpec != null) {
|
|
351
|
+
cipher.init(Cipher.ENCRYPT_MODE, entry.certificate.publicKey, oaepSpec)
|
|
352
|
+
} else {
|
|
353
|
+
cipher.init(Cipher.ENCRYPT_MODE, entry.certificate.publicKey)
|
|
339
354
|
}
|
|
355
|
+
val encrypted: ByteArray = cipher.doFinal(data.toByteArray(Charsets.UTF_8))
|
|
356
|
+
promise.resolve(Base64.encodeToString(encrypted, Base64.NO_WRAP))
|
|
357
|
+
}
|
|
340
358
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
if (entry !is KeyStore.PrivateKeyEntry) return@Function null
|
|
347
|
-
|
|
348
|
-
val algoType = AlgorithmType.valueOf(o["algoType"] as String)
|
|
349
|
-
val algo = toAndroidAlgo(algoType)
|
|
350
|
-
|
|
351
|
-
val valid: Boolean = Signature.getInstance(algo).apply {
|
|
352
|
-
initVerify(entry.certificate)
|
|
353
|
-
update(data.toByteArray(Charsets.UTF_8))
|
|
354
|
-
}.verify(Base64.decode(signature, Base64.NO_WRAP))
|
|
355
|
-
return@Function true
|
|
359
|
+
private fun decrypt(alias: String, data: String, o: Map<String, Any?>, promise: Promise) {
|
|
360
|
+
val entry = getKeyStoreEntry(alias)
|
|
361
|
+
if (entry !is KeyStore.PrivateKeyEntry) {
|
|
362
|
+
promise.resolve(null)
|
|
363
|
+
return
|
|
356
364
|
}
|
|
357
365
|
|
|
366
|
+
val reqAuth = isKeyStoreRequireAuthentication(entry)
|
|
358
367
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
promise.resolve(null)
|
|
366
|
-
return@AsyncFunction
|
|
367
|
-
}
|
|
368
|
+
val title = o["title"] as String
|
|
369
|
+
val subtitle = o["subtitle"] as String
|
|
370
|
+
val authMethod = o["authMethod"] as String
|
|
371
|
+
val algoType = AlgorithmType.valueOf(o["algoType"] as String)
|
|
372
|
+
val algo = toAndroidAlgo(algoType)
|
|
373
|
+
val oaepSpec = getOaepSpec(algoType)
|
|
368
374
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
val oaepSpec = getOaepSpec(algoType)
|
|
375
|
+
val cipher = Cipher.getInstance(algo)
|
|
376
|
+
val ciphertext = Base64.decode(data, Base64.NO_WRAP)
|
|
372
377
|
|
|
373
|
-
|
|
378
|
+
if (!reqAuth) {
|
|
374
379
|
if (oaepSpec != null) {
|
|
375
|
-
cipher.init(Cipher.
|
|
380
|
+
cipher.init(Cipher.DECRYPT_MODE, entry.privateKey, oaepSpec)
|
|
376
381
|
} else {
|
|
377
|
-
cipher.init(Cipher.
|
|
382
|
+
cipher.init(Cipher.DECRYPT_MODE, entry.privateKey)
|
|
378
383
|
}
|
|
379
|
-
val
|
|
380
|
-
promise.resolve(
|
|
384
|
+
val decrypted: ByteArray = cipher.doFinal(ciphertext)
|
|
385
|
+
promise.resolve(String(decrypted, Charsets.UTF_8))
|
|
386
|
+
return
|
|
387
|
+
} else {
|
|
388
|
+
showAuthPrompt(
|
|
389
|
+
onSuccess = {
|
|
390
|
+
if (oaepSpec != null) {
|
|
391
|
+
cipher.init(Cipher.DECRYPT_MODE, entry.privateKey, oaepSpec)
|
|
392
|
+
} else {
|
|
393
|
+
cipher.init(Cipher.DECRYPT_MODE, entry.privateKey)
|
|
394
|
+
}
|
|
395
|
+
val decrypted: ByteArray = cipher.doFinal(ciphertext)
|
|
396
|
+
promise.resolve(String(decrypted, Charsets.UTF_8))
|
|
397
|
+
},
|
|
398
|
+
onError = { error ->
|
|
399
|
+
promise.reject("ERR_AUTH_FAILED", error, null)
|
|
400
|
+
},
|
|
401
|
+
title = title,
|
|
402
|
+
subtitle = subtitle,
|
|
403
|
+
authMethod = when (authMethod) {
|
|
404
|
+
"PASSCODE" -> AuthMethod.PASSCODE
|
|
405
|
+
"PASSCODE_OR_BIOMETRIC" -> AuthMethod.PASSCODE_OR_BIOMETRIC
|
|
406
|
+
else -> AuthMethod.PASSCODE_OR_BIOMETRIC
|
|
407
|
+
}
|
|
408
|
+
)
|
|
381
409
|
}
|
|
410
|
+
}
|
|
382
411
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
o: Map<String, Any?>,
|
|
386
|
-
promise: Promise ->
|
|
387
|
-
val entry = getKeyStoreEntry(alias)
|
|
388
|
-
if (entry !is KeyStore.PrivateKeyEntry) {
|
|
389
|
-
promise.resolve(null)
|
|
390
|
-
return@AsyncFunction
|
|
391
|
-
}
|
|
412
|
+
override fun definition() = ModuleDefinition {
|
|
413
|
+
Name("DeviceCrypto")
|
|
392
414
|
|
|
393
|
-
|
|
415
|
+
Function("isAuthCheckAvailable") { ->
|
|
416
|
+
return@Function isAuthCheckAvailable()
|
|
417
|
+
}
|
|
394
418
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
val algoType = AlgorithmType.valueOf(o["algoType"] as String)
|
|
399
|
-
val algo = toAndroidAlgo(algoType)
|
|
400
|
-
val oaepSpec = getOaepSpec(algoType)
|
|
419
|
+
Function("isStrongBoxAvailable") { ->
|
|
420
|
+
return@Function isStrongBoxAvailable()
|
|
421
|
+
}
|
|
401
422
|
|
|
402
|
-
|
|
403
|
-
|
|
423
|
+
Function("generateKeyPair") { alias: String, o: Map<String, Any?> ->
|
|
424
|
+
return@Function generateKeyPair(alias, o)
|
|
425
|
+
}
|
|
404
426
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
"PASSCODE" -> AuthMethod.PASSCODE
|
|
432
|
-
"PASSCODE_OR_BIOMETRIC" -> AuthMethod.PASSCODE_OR_BIOMETRIC
|
|
433
|
-
else -> AuthMethod.PASSCODE_OR_BIOMETRIC
|
|
434
|
-
}
|
|
435
|
-
)
|
|
436
|
-
}
|
|
427
|
+
Function("removeKeyPair") { alias: String ->
|
|
428
|
+
return@Function removeKeyStoreEntry(alias)
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
Function("aliases") {
|
|
432
|
+
return@Function getAliases()
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
Function("getPublicKey") { alias: String ->
|
|
436
|
+
return@Function getPublicKey(alias)
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
AsyncFunction("sign") { alias: String, data: String, o: Map<String, Any?>, promise: Promise ->
|
|
440
|
+
return@AsyncFunction sign(alias, data, o, promise)
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
Function("verify") { alias: String, data: String, signature: String, o: Map<String, Any?> ->
|
|
444
|
+
return@Function verify(alias, data, signature, o)
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
AsyncFunction("encrypt") { alias: String, data: String, o: Map<String, Any?>, promise: Promise ->
|
|
448
|
+
return@AsyncFunction encrypt(alias, data, o, promise)
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
AsyncFunction("decrypt") { alias: String, data: String, o: Map<String, Any?>, promise: Promise ->
|
|
452
|
+
return@AsyncFunction decrypt(alias, data, o, promise)
|
|
437
453
|
}
|
|
438
454
|
}
|
|
439
455
|
}
|
|
@@ -46,28 +46,34 @@ export interface GenerateKeyPairOptions {
|
|
|
46
46
|
* }
|
|
47
47
|
* ```
|
|
48
48
|
* @default AuthMethod.PASSCODE_OR_BIOMETRIC
|
|
49
|
-
* @platform ios
|
|
49
|
+
* @platform ios 🍏
|
|
50
50
|
*/
|
|
51
51
|
authMethod?: AuthMethod;
|
|
52
|
+
/**
|
|
53
|
+
* If the Strong Box is available, it will be used instead of the Trusted Execution Environment (TEE).
|
|
54
|
+
* @default false
|
|
55
|
+
* @platform android 🤖
|
|
56
|
+
*/
|
|
57
|
+
preferStrongBox?: boolean;
|
|
52
58
|
}
|
|
53
59
|
interface BaseSignDecryptOptions {
|
|
54
60
|
/**
|
|
55
61
|
* The title of the prompt to show when authentication is required.
|
|
56
62
|
* @default "Unlock"
|
|
57
|
-
* @platform android
|
|
63
|
+
* @platform android 🤖
|
|
58
64
|
*/
|
|
59
65
|
promptTitle?: string;
|
|
60
66
|
/**
|
|
61
67
|
* The subtitle of the prompt to show when authentication is required.
|
|
62
68
|
* @default "Enter your PIN to continue"
|
|
63
|
-
* @platform android
|
|
69
|
+
* @platform android 🤖
|
|
64
70
|
*/
|
|
65
71
|
promptSubtitle?: string;
|
|
66
72
|
/**
|
|
67
73
|
* The method of authentication to use.
|
|
68
74
|
* Note that on iOS you have to define it when generating the key pair.
|
|
69
75
|
* @default AuthMethod.PASSCODE_OR_BIOMETRIC
|
|
70
|
-
* @platform android
|
|
76
|
+
* @platform android 🤖
|
|
71
77
|
*/
|
|
72
78
|
authMethod?: AuthMethod;
|
|
73
79
|
}
|
|
@@ -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;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;
|
|
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;IACxB;;;;OAIG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;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 +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,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
|
|
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 * If the Strong Box is available, it will be used instead of the Trusted Execution Environment (TEE).\n * @default false\n * @platform android 🤖\n */\n preferStrongBox?: boolean;\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"]}
|
|
@@ -5,6 +5,12 @@ declare const _default: {
|
|
|
5
5
|
* @returns The result of the operation.
|
|
6
6
|
*/
|
|
7
7
|
isAuthCheckAvailable: () => AuthCheckResult;
|
|
8
|
+
/**
|
|
9
|
+
* Checks if the strong box is available .
|
|
10
|
+
* @returns The result of the operation.
|
|
11
|
+
* @platform android 🤖
|
|
12
|
+
*/
|
|
13
|
+
isStrongBoxAvailable: () => boolean;
|
|
8
14
|
/**
|
|
9
15
|
* Creates a new ECDSA P‑256 key pair for the given alias, if it doesn’t already exist.
|
|
10
16
|
* @param alias - The alias to use for the key pair.
|
|
@@ -34,12 +40,6 @@ declare const _default: {
|
|
|
34
40
|
* @param alias - The alias to use for the key pair.
|
|
35
41
|
* @param data - The data to sign.
|
|
36
42
|
* @param options - The options for the operation.
|
|
37
|
-
* @default {
|
|
38
|
-
* algoType: SigningAlgorithm.ECDSA_SHA256,
|
|
39
|
-
* promptTitle: "Unlock",
|
|
40
|
-
* promptSubtitle: "Enter your PIN to continue",
|
|
41
|
-
* authMethod: AuthMethod.PASSCODE_OR_BIOMETRIC
|
|
42
|
-
* }
|
|
43
43
|
* @returns The signature.
|
|
44
44
|
*/
|
|
45
45
|
sign: (alias: string, data: string, options?: SignOptions) => Promise<string | null>;
|
|
@@ -49,9 +49,6 @@ declare const _default: {
|
|
|
49
49
|
* @param data - The data to verify in UTF-8 format.
|
|
50
50
|
* @param signature - The signature to verify in Base64 format.
|
|
51
51
|
* @param options - The options for the operation.
|
|
52
|
-
* @default {
|
|
53
|
-
* algoType: SigningAlgorithm.ECDSA_SHA256,
|
|
54
|
-
* }
|
|
55
52
|
* @returns True if the signature is valid, false otherwise.
|
|
56
53
|
*/
|
|
57
54
|
verify: (alias: string, data: string, signature: string, options?: VerifyOptions) => Promise<boolean | null>;
|
|
@@ -71,12 +68,6 @@ declare const _default: {
|
|
|
71
68
|
* @param alias - The alias to use for the key pair.
|
|
72
69
|
* @param data - The data to decrypt in Base64 format.
|
|
73
70
|
* @param options - The options for the operation.
|
|
74
|
-
* @default {
|
|
75
|
-
* algoType: EncryptionAlgorithm.RSA_2048_PKCS1,
|
|
76
|
-
* promptTitle: "Unlock",
|
|
77
|
-
* promptSubtitle: "Enter your PIN to continue",
|
|
78
|
-
* authMethod: AuthMethod.PASSCODE_OR_BIOMETRIC
|
|
79
|
-
* }
|
|
80
71
|
* @returns The decrypted data.
|
|
81
72
|
*/
|
|
82
73
|
decrypt: (alias: string, data: string, options?: DecryptOptions) => Promise<string | null>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DeviceCryptoModule.d.ts","sourceRoot":"","sources":["../src/DeviceCryptoModule.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,eAAe,EACf,sBAAsB,EACtB,qBAAqB,EACrB,mBAAmB,EAEnB,WAAW,EACX,cAAc,EACd,aAAa,EAEb,cAAc,EAEf,MAAM,sBAAsB,CAAC;;
|
|
1
|
+
{"version":3,"file":"DeviceCryptoModule.d.ts","sourceRoot":"","sources":["../src/DeviceCryptoModule.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,eAAe,EACf,sBAAsB,EACtB,qBAAqB,EACrB,mBAAmB,EAEnB,WAAW,EACX,cAAc,EACd,aAAa,EAEb,cAAc,EAEf,MAAM,sBAAsB,CAAC;;IA8B5B;;;OAGG;gCAvBqB,eAAe;IAyBvC;;;;OAIG;gCA5BqB,OAAO;IA8B/B;;;;OAIG;6BAC4B,MAAM,YAAY,sBAAsB;IASvE;;;;;OAKG;0BACmB,MAAM,YAAY,mBAAmB;IAQ3D;;;;OAIG;2BA3DkB,MAAM,KAAG,OAAO;IA6DrC;;;OAGG;mBA/DQ,MAAM,EAAE;IAiEnB;;;;;;OAMG;kBACiB,MAAM,QAAQ,MAAM,YAAY,WAAW;IAS/D;;;;;;;OAOG;oBAEM,MAAM,QACP,MAAM,aACD,MAAM,YACP,aAAa;IAOzB;;;;;;;;;OASG;qBACoB,MAAM,QAAQ,MAAM,YAAY,cAAc;IAMrE;;;;;;OAMG;qBACoB,MAAM,QAAQ,MAAM,YAAY,cAAc;;AA7GvE,wBAsHE"}
|
|
@@ -12,6 +12,12 @@ export default {
|
|
|
12
12
|
* @returns The result of the operation.
|
|
13
13
|
*/
|
|
14
14
|
isAuthCheckAvailable: module.isAuthCheckAvailable,
|
|
15
|
+
/**
|
|
16
|
+
* Checks if the strong box is available .
|
|
17
|
+
* @returns The result of the operation.
|
|
18
|
+
* @platform android 🤖
|
|
19
|
+
*/
|
|
20
|
+
isStrongBoxAvailable: module.isStrongBoxAvailable,
|
|
15
21
|
/**
|
|
16
22
|
* Creates a new ECDSA P‑256 key pair for the given alias, if it doesn’t already exist.
|
|
17
23
|
* @param alias - The alias to use for the key pair.
|
|
@@ -22,6 +28,7 @@ export default {
|
|
|
22
28
|
algoType: options?.algorithmType,
|
|
23
29
|
reqAuth: options?.requireAuthentication ?? false,
|
|
24
30
|
authMethod: options?.authMethod ?? AuthMethod.PASSCODE_OR_BIOMETRIC,
|
|
31
|
+
preferStrongBox: options?.preferStrongBox ?? false,
|
|
25
32
|
};
|
|
26
33
|
return module.generateKeyPair(alias, o);
|
|
27
34
|
},
|
|
@@ -56,12 +63,6 @@ export default {
|
|
|
56
63
|
* @param alias - The alias to use for the key pair.
|
|
57
64
|
* @param data - The data to sign.
|
|
58
65
|
* @param options - The options for the operation.
|
|
59
|
-
* @default {
|
|
60
|
-
* algoType: SigningAlgorithm.ECDSA_SHA256,
|
|
61
|
-
* promptTitle: "Unlock",
|
|
62
|
-
* promptSubtitle: "Enter your PIN to continue",
|
|
63
|
-
* authMethod: AuthMethod.PASSCODE_OR_BIOMETRIC
|
|
64
|
-
* }
|
|
65
66
|
* @returns The signature.
|
|
66
67
|
*/
|
|
67
68
|
sign: async (alias, data, options) => {
|
|
@@ -79,9 +80,6 @@ export default {
|
|
|
79
80
|
* @param data - The data to verify in UTF-8 format.
|
|
80
81
|
* @param signature - The signature to verify in Base64 format.
|
|
81
82
|
* @param options - The options for the operation.
|
|
82
|
-
* @default {
|
|
83
|
-
* algoType: SigningAlgorithm.ECDSA_SHA256,
|
|
84
|
-
* }
|
|
85
83
|
* @returns True if the signature is valid, false otherwise.
|
|
86
84
|
*/
|
|
87
85
|
verify: async (alias, data, signature, options) => {
|
|
@@ -111,12 +109,6 @@ export default {
|
|
|
111
109
|
* @param alias - The alias to use for the key pair.
|
|
112
110
|
* @param data - The data to decrypt in Base64 format.
|
|
113
111
|
* @param options - The options for the operation.
|
|
114
|
-
* @default {
|
|
115
|
-
* algoType: EncryptionAlgorithm.RSA_2048_PKCS1,
|
|
116
|
-
* promptTitle: "Unlock",
|
|
117
|
-
* promptSubtitle: "Enter your PIN to continue",
|
|
118
|
-
* authMethod: AuthMethod.PASSCODE_OR_BIOMETRIC
|
|
119
|
-
* }
|
|
120
112
|
* @returns The decrypted data.
|
|
121
113
|
*/
|
|
122
114
|
decrypt: async (alias, data, options) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DeviceCryptoModule.js","sourceRoot":"","sources":["../src/DeviceCryptoModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,mBAAmB,EAAE,MAAM,MAAM,CAAC;AAEzD,OAAO,EAKL,UAAU,EAIV,gBAAgB,EAEhB,mBAAmB,GACpB,MAAM,sBAAsB,CAAC;AAE9B,SAAS,WAAW,CAAC,MAAc,EAAE,KAAK,GAAG,MAAM;IACjD,MAAM,UAAU,GAAG,MAAM,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IAC9C,MAAM,OAAO,GAAG,UAAU,CAAC,KAAK,CAAC,UAAU,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;IAE/D,OAAO,cAAc,KAAK,UAAU,OAAO,cAAc,KAAK,SAAS,CAAC;AAC1E,CAAC;
|
|
1
|
+
{"version":3,"file":"DeviceCryptoModule.js","sourceRoot":"","sources":["../src/DeviceCryptoModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,mBAAmB,EAAE,MAAM,MAAM,CAAC;AAEzD,OAAO,EAKL,UAAU,EAIV,gBAAgB,EAEhB,mBAAmB,GACpB,MAAM,sBAAsB,CAAC;AAE9B,SAAS,WAAW,CAAC,MAAc,EAAE,KAAK,GAAG,MAAM;IACjD,MAAM,UAAU,GAAG,MAAM,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IAC9C,MAAM,OAAO,GAAG,UAAU,CAAC,KAAK,CAAC,UAAU,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;IAE/D,OAAO,cAAc,KAAK,UAAU,OAAO,cAAc,KAAK,SAAS,CAAC;AAC1E,CAAC;AAoBD,MAAM,MAAM,GAAG,mBAAmB,CAAqB,cAAc,CAAC,CAAC;AAEvE,eAAe;IACb;;;OAGG;IACH,oBAAoB,EAAE,MAAM,CAAC,oBAAoB;IACjD;;;;OAIG;IACH,oBAAoB,EAAE,MAAM,CAAC,oBAAoB;IACjD;;;;OAIG;IACH,eAAe,EAAE,KAAK,EAAE,KAAa,EAAE,OAAgC,EAAE,EAAE;QACzE,MAAM,CAAC,GAAG;YACR,QAAQ,EAAE,OAAO,EAAE,aAAa;YAChC,OAAO,EAAE,OAAO,EAAE,qBAAqB,IAAI,KAAK;YAChD,UAAU,EAAE,OAAO,EAAE,UAAU,IAAI,UAAU,CAAC,qBAAqB;YACnE,eAAe,EAAE,OAAO,EAAE,eAAe,IAAI,KAAK;SACnD,CAAC;QACF,OAAO,MAAM,CAAC,eAAe,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;IAC1C,CAAC;IACD;;;;;OAKG;IACH,YAAY,EAAE,CAAC,KAAa,EAAE,OAA6B,EAAE,EAAE;QAC7D,MAAM,SAAS,GAAG,MAAM,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC;QAC7C,IAAI,CAAC,SAAS;YAAE,OAAO,IAAI,CAAC;QAC5B,IAAI,OAAO,EAAE,MAAM,KAAK,KAAK,EAAE,CAAC;YAC9B,OAAO,WAAW,CAAC,SAAS,EAAE,YAAY,CAAC,CAAC;QAC9C,CAAC;QACD,OAAO,WAAW,CAAC,SAAS,EAAE,YAAY,CAAC,CAAC;IAC9C,CAAC;IACD;;;;OAIG;IACH,aAAa,EAAE,MAAM,CAAC,aAAa;IACnC;;;OAGG;IACH,OAAO,EAAE,MAAM,CAAC,OAAO;IACvB;;;;;;OAMG;IACH,IAAI,EAAE,KAAK,EAAE,KAAa,EAAE,IAAY,EAAE,OAAqB,EAAE,EAAE;QACjE,MAAM,CAAC,GAAG;YACR,KAAK,EAAE,OAAO,EAAE,WAAW,IAAI,QAAQ;YACvC,QAAQ,EAAE,OAAO,EAAE,cAAc,IAAI,4BAA4B;YACjE,UAAU,EAAE,OAAO,EAAE,UAAU,IAAI,UAAU,CAAC,qBAAqB;YACnE,QAAQ,EAAE,OAAO,EAAE,aAAa,IAAI,gBAAgB,CAAC,sBAAsB;SAC5E,CAAC;QACF,OAAO,MAAM,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;IACrC,CAAC;IACD;;;;;;;OAOG;IACH,MAAM,EAAE,KAAK,EACX,KAAa,EACb,IAAY,EACZ,SAAiB,EACjB,OAAuB,EACvB,EAAE;QACF,MAAM,CAAC,GAAG;YACR,QAAQ,EAAE,OAAO,EAAE,aAAa,IAAI,gBAAgB,CAAC,sBAAsB;SAC5E,CAAC;QACF,OAAO,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC;IAClD,CAAC;IACD;;;;;;;;;OASG;IACH,OAAO,EAAE,KAAK,EAAE,KAAa,EAAE,IAAY,EAAE,OAAwB,EAAE,EAAE;QACvE,MAAM,CAAC,GAAG;YACR,QAAQ,EAAE,OAAO,EAAE,aAAa,IAAI,mBAAmB,CAAC,cAAc;SACvE,CAAC;QACF,OAAO,MAAM,CAAC,OAAO,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;IACxC,CAAC;IACD;;;;;;OAMG;IACH,OAAO,EAAE,KAAK,EAAE,KAAa,EAAE,IAAY,EAAE,OAAwB,EAAE,EAAE;QACvE,MAAM,CAAC,GAAG;YACR,KAAK,EAAE,OAAO,EAAE,WAAW,IAAI,QAAQ;YACvC,QAAQ,EAAE,OAAO,EAAE,cAAc,IAAI,4BAA4B;YACjE,UAAU,EAAE,OAAO,EAAE,UAAU,IAAI,UAAU,CAAC,qBAAqB;YACnE,QAAQ,EAAE,OAAO,EAAE,aAAa,IAAI,mBAAmB,CAAC,cAAc;SACvE,CAAC;QACF,OAAO,MAAM,CAAC,OAAO,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;IACxC,CAAC;CACF,CAAC","sourcesContent":["import { NativeModule, requireNativeModule } from \"expo\";\n\nimport {\n AuthCheckResult,\n GenerateKeyPairOptions,\n GenerateKeyPairResult,\n GetPublicKeyOptions,\n AuthMethod,\n SignOptions,\n DecryptOptions,\n VerifyOptions,\n SigningAlgorithm,\n EncryptOptions,\n EncryptionAlgorithm,\n} from \"./DeviceCrypto.types\";\n\nfunction base64ToPem(base64: string, label = \"DATA\") {\n const normalized = base64.replace(/\\s+/g, \"\");\n const wrapped = normalized.match(/.{1,64}/g)?.join(\"\\n\") ?? \"\";\n\n return `-----BEGIN ${label}-----\\n${wrapped}\\n-----END ${label}-----\\n`;\n}\n\ndeclare class DeviceCryptoModule extends NativeModule {\n isAuthCheckAvailable(): AuthCheckResult;\n isStrongBoxAvailable(): boolean;\n generateKeyPair(alias: string, o: any): GenerateKeyPairResult;\n getPublicKey(alias: string): string | null;\n removeKeyPair(alias: string): boolean;\n aliases(): string[];\n sign(alias: string, data: string, o: any): Promise<string | null>;\n verify(\n alias: string,\n data: string,\n signature: string,\n o: any\n ): boolean | null;\n encrypt(alias: string, data: string, o: any): Promise<string | null>;\n decrypt(alias: string, data: string, o: any): Promise<string | null>;\n}\n\nconst module = requireNativeModule<DeviceCryptoModule>(\"DeviceCrypto\");\n\nexport default {\n /**\n * Checks if the biometric or passcode authentication is available.\n * @returns The result of the operation.\n */\n isAuthCheckAvailable: module.isAuthCheckAvailable,\n /**\n * Checks if the strong box is available .\n * @returns The result of the operation.\n * @platform android 🤖\n */\n isStrongBoxAvailable: module.isStrongBoxAvailable,\n /**\n * Creates a new ECDSA P‑256 key pair for the given alias, if it doesn’t already exist.\n * @param alias - The alias to use for the key pair.\n * @returns The result of the operation.\n */\n generateKeyPair: async (alias: string, options?: GenerateKeyPairOptions) => {\n const o = {\n algoType: options?.algorithmType,\n reqAuth: options?.requireAuthentication ?? false,\n authMethod: options?.authMethod ?? AuthMethod.PASSCODE_OR_BIOMETRIC,\n preferStrongBox: options?.preferStrongBox ?? false,\n };\n return module.generateKeyPair(alias, o);\n },\n /**\n * Returns the public key for the given alias.\n * @param alias - The alias to use for the key pair.\n * @param options - The options for the operation.\n * @returns The public key.\n */\n getPublicKey: (alias: string, options?: GetPublicKeyOptions) => {\n const publicKey = module.getPublicKey(alias);\n if (!publicKey) return null;\n if (options?.format === \"PEM\") {\n return base64ToPem(publicKey, \"PUBLIC KEY\");\n }\n return base64ToPem(publicKey, \"PUBLIC KEY\");\n },\n /**\n * Removes the key pair for the given alias.\n * @param alias - The alias to use for the key pair.\n * @returns True if the key pair was removed, false otherwise.\n */\n removeKeyPair: module.removeKeyPair,\n /**\n * Lists the aliases for the key pairs.\n * @returns The aliases.\n */\n aliases: module.aliases,\n /**\n * Signs the given data with the private key for the given alias.\n * @param alias - The alias to use for the key pair.\n * @param data - The data to sign.\n * @param options - The options for the operation.\n * @returns The signature.\n */\n sign: async (alias: string, data: string, options?: SignOptions) => {\n const o = {\n title: options?.promptTitle ?? \"Unlock\",\n subtitle: options?.promptSubtitle ?? \"Enter your PIN to continue\",\n authMethod: options?.authMethod ?? AuthMethod.PASSCODE_OR_BIOMETRIC,\n algoType: options?.algorithmType ?? SigningAlgorithm.ECDSA_SECP256R1_SHA256,\n };\n return module.sign(alias, data, o);\n },\n /**\n * Verifies the given data with the signature for the given alias.\n * @param alias - The alias to use for the key pair.\n * @param data - The data to verify in UTF-8 format.\n * @param signature - The signature to verify in Base64 format.\n * @param options - The options for the operation.\n * @returns True if the signature is valid, false otherwise.\n */\n verify: async (\n alias: string,\n data: string,\n signature: string,\n options?: VerifyOptions\n ) => {\n const o = {\n algoType: options?.algorithmType ?? SigningAlgorithm.ECDSA_SECP256R1_SHA256,\n };\n return module.verify(alias, data, signature, o);\n },\n /**\n * Signs the given data with the private key for the given alias.\n * @param alias - The alias to use for the key pair.\n * @param data - The data to sign in UTF-8 format.\n * @param options - The options for the operation.\n * @default { \n * algoType: EncryptionAlgorithm.RSA_2048_PKCS1, \n * }\n * @returns The signature.\n */\n encrypt: async (alias: string, data: string, options?: EncryptOptions) => {\n const o = {\n algoType: options?.algorithmType ?? EncryptionAlgorithm.RSA_2048_PKCS1,\n };\n return module.encrypt(alias, data, o);\n },\n /**\n * Decrypts the given data with the private key for the given alias.\n * @param alias - The alias to use for the key pair.\n * @param data - The data to decrypt in Base64 format.\n * @param options - The options for the operation.\n * @returns The decrypted data.\n */\n decrypt: async (alias: string, data: string, options?: DecryptOptions) => {\n const o = {\n title: options?.promptTitle ?? \"Unlock\",\n subtitle: options?.promptSubtitle ?? \"Enter your PIN to continue\",\n authMethod: options?.authMethod ?? AuthMethod.PASSCODE_OR_BIOMETRIC,\n algoType: options?.algorithmType ?? EncryptionAlgorithm.RSA_2048_PKCS1,\n };\n return module.decrypt(alias, data, o);\n },\n};"]}
|
package/ios/DeviceCrypto.podspec
CHANGED
package/ios/DeviceCrypto.swift
CHANGED
|
@@ -103,38 +103,6 @@ public class DeviceCryptoModule: Module {
|
|
|
103
103
|
return Data([0x80 | UInt8(bytes.count)]) + Data(bytes)
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
private func isAuthCheckAvailable() -> String {
|
|
107
|
-
let context = LAContext()
|
|
108
|
-
let available = context.canEvaluatePolicy(.deviceOwnerAuthentication, error: nil)
|
|
109
|
-
if available {
|
|
110
|
-
return AuthCheckResult.AVAILABLE.rawValue
|
|
111
|
-
} else {
|
|
112
|
-
return AuthCheckResult.UNAVAILABLE.rawValue
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
private func getAliases() -> [String] {
|
|
117
|
-
let query: [String: Any] = [
|
|
118
|
-
kSecClass as String: kSecClassKey,
|
|
119
|
-
kSecAttrKeyClass as String: kSecAttrKeyClassPublic,
|
|
120
|
-
kSecMatchLimit as String: kSecMatchLimitAll,
|
|
121
|
-
kSecReturnAttributes as String: true,
|
|
122
|
-
]
|
|
123
|
-
|
|
124
|
-
var result: CFTypeRef?
|
|
125
|
-
let status = SecItemCopyMatching(query as CFDictionary, &result)
|
|
126
|
-
guard status == errSecSuccess else { return [] }
|
|
127
|
-
|
|
128
|
-
let items = (result as? [[String: Any]]) ?? []
|
|
129
|
-
return items.compactMap { attrs in
|
|
130
|
-
let tagKey = kSecAttrApplicationTag as String
|
|
131
|
-
if let tagString = attrs[tagKey] as? String {
|
|
132
|
-
return tagString
|
|
133
|
-
}
|
|
134
|
-
return nil
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
106
|
private func getSecKeyQuery(_ alias: String, keyClass: CFString, returnRef: Bool = true) -> [String: Any] {
|
|
139
107
|
var query: [String: Any] = [
|
|
140
108
|
kSecClass as String: kSecClassKey,
|
|
@@ -155,40 +123,6 @@ public class DeviceCryptoModule: Module {
|
|
|
155
123
|
return (item as! SecKey)
|
|
156
124
|
}
|
|
157
125
|
|
|
158
|
-
private func retrievePublicKey(_ secKey: SecKey) -> String? {
|
|
159
|
-
let publicKey = SecKeyCopyPublicKey(secKey)!
|
|
160
|
-
|
|
161
|
-
guard
|
|
162
|
-
let attrs = SecKeyCopyAttributes(publicKey) as? [String: Any],
|
|
163
|
-
let keyType = attrs[kSecAttrKeyType as String] as? String
|
|
164
|
-
else {
|
|
165
|
-
return nil
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
if keyType == (kSecAttrKeyTypeECSECPrimeRandom as String) {
|
|
169
|
-
return x962ECPointToP256SPKI(publicKey)?.base64EncodedString()
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
if keyType == (kSecAttrKeyTypeRSA as String) {
|
|
173
|
-
return rsaPKCS1ToSPKI(publicKey)?.base64EncodedString()
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
guard let publicKeyData = SecKeyCopyExternalRepresentation(publicKey, nil) as Data? else {
|
|
177
|
-
return nil
|
|
178
|
-
}
|
|
179
|
-
return publicKeyData.base64EncodedString()
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
private func removeKeyStoreEntry(_ alias: String) -> Bool {
|
|
183
|
-
let privateQuery = self.getSecKeyQuery(alias, keyClass: kSecAttrKeyClassPrivate, returnRef: false)
|
|
184
|
-
let privateStatus = SecItemDelete(privateQuery as CFDictionary)
|
|
185
|
-
|
|
186
|
-
let publicQuery = self.getSecKeyQuery(alias, keyClass: kSecAttrKeyClassPublic, returnRef: false)
|
|
187
|
-
let publicStatus = SecItemDelete(publicQuery as CFDictionary)
|
|
188
|
-
|
|
189
|
-
return privateStatus == errSecSuccess || publicStatus == errSecSuccess
|
|
190
|
-
}
|
|
191
|
-
|
|
192
126
|
private func buildECDSA(alias: String, reqAuth: Bool, authMethod: AuthMethod) -> SecKey? {
|
|
193
127
|
let accessFlags: SecAccessControlCreateFlags
|
|
194
128
|
if reqAuth {
|
|
@@ -264,48 +198,205 @@ public class DeviceCryptoModule: Module {
|
|
|
264
198
|
return SecKeyCreateRandomKey(attributes, nil)
|
|
265
199
|
}
|
|
266
200
|
|
|
267
|
-
|
|
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
|
-
|
|
304
|
-
|
|
305
|
-
|
|
257
|
+
private func getAliases() -> [String] {
|
|
258
|
+
let query: [String: Any] = [
|
|
259
|
+
kSecClass as String: kSecClassKey,
|
|
260
|
+
kSecAttrKeyClass as String: kSecAttrKeyClassPublic,
|
|
261
|
+
kSecMatchLimit as String: kSecMatchLimitAll,
|
|
262
|
+
kSecReturnAttributes as String: true,
|
|
263
|
+
]
|
|
264
|
+
|
|
265
|
+
var result: CFTypeRef?
|
|
266
|
+
let status = SecItemCopyMatching(query as CFDictionary, &result)
|
|
267
|
+
guard status == errSecSuccess else { return [] }
|
|
268
|
+
|
|
269
|
+
let items = (result as? [[String: Any]]) ?? []
|
|
270
|
+
return items.compactMap { attrs in
|
|
271
|
+
let tagKey = kSecAttrApplicationTag as String
|
|
272
|
+
if let tagString = attrs[tagKey] as? String {
|
|
273
|
+
return tagString
|
|
306
274
|
}
|
|
275
|
+
return nil
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
private func getPublicKey(alias: String) -> String? {
|
|
280
|
+
let secKey = self.getSecKeyByAlias(alias, keyClass: kSecAttrKeyClassPublic)
|
|
281
|
+
guard let secKey else { return nil }
|
|
282
|
+
|
|
283
|
+
let publicKey = SecKeyCopyPublicKey(secKey)!
|
|
284
|
+
|
|
285
|
+
guard
|
|
286
|
+
let attrs = SecKeyCopyAttributes(publicKey) as? [String: Any],
|
|
287
|
+
let keyType = attrs[kSecAttrKeyType as String] as? String
|
|
288
|
+
else {
|
|
289
|
+
return nil
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if keyType == (kSecAttrKeyTypeECSECPrimeRandom as String) {
|
|
293
|
+
return x962ECPointToP256SPKI(publicKey)?.base64EncodedString()
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if keyType == (kSecAttrKeyTypeRSA as String) {
|
|
297
|
+
return rsaPKCS1ToSPKI(publicKey)?.base64EncodedString()
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
guard let publicKeyData = SecKeyCopyExternalRepresentation(publicKey, nil) as Data? else {
|
|
301
|
+
return nil
|
|
302
|
+
}
|
|
303
|
+
return publicKeyData.base64EncodedString()
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
private func sign(alias: String, data: String, o: [String: Any]) throws -> String? {
|
|
307
|
+
let secKey = self.getSecKeyByAlias(alias, keyClass: kSecAttrKeyClassPrivate)
|
|
308
|
+
guard let secKey else { return nil }
|
|
307
309
|
|
|
308
|
-
|
|
310
|
+
let algoType = AlgorithmType(rawValue: o["algoType"] as! String)
|
|
311
|
+
let algo = self.toiOSAlgo(algorithm: algoType!)
|
|
312
|
+
|
|
313
|
+
var signingError: Unmanaged<CFError>?
|
|
314
|
+
let signatureCF = SecKeyCreateSignature(
|
|
315
|
+
secKey,
|
|
316
|
+
algo,
|
|
317
|
+
Data(data.utf8) as CFData,
|
|
318
|
+
&signingError
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
if let error = signingError?.takeRetainedValue() {
|
|
322
|
+
throw error as Error
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
guard let signatureCF else { return nil }
|
|
326
|
+
let signature = signatureCF as Data
|
|
327
|
+
return signature.base64EncodedString()
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
private func verify(alias: String, data: String, signature: String, o: [String: Any]) -> Bool? {
|
|
331
|
+
let secKey = self.getSecKeyByAlias(alias, keyClass: kSecAttrKeyClassPrivate)
|
|
332
|
+
guard let secKey else { return nil }
|
|
333
|
+
|
|
334
|
+
guard let publicKey = SecKeyCopyPublicKey(secKey) else { return nil }
|
|
335
|
+
guard let signatureData = Data(base64Encoded: signature) else { return nil }
|
|
336
|
+
|
|
337
|
+
let algoType = AlgorithmType(rawValue: o["algoType"] as! String)
|
|
338
|
+
let algo = self.toiOSAlgo(algorithm: algoType!)
|
|
339
|
+
|
|
340
|
+
let valid = SecKeyVerifySignature(
|
|
341
|
+
publicKey,
|
|
342
|
+
algo,
|
|
343
|
+
Data(data.utf8) as CFData,
|
|
344
|
+
signatureData as CFData,
|
|
345
|
+
nil
|
|
346
|
+
)
|
|
347
|
+
return valid
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
private func encrypt(alias: String, data: String, o: [String: Any]) -> String? {
|
|
351
|
+
let secKey = self.getSecKeyByAlias(alias, keyClass: kSecAttrKeyClassPublic)
|
|
352
|
+
guard let secKey else { return nil }
|
|
353
|
+
let publicKey = SecKeyCopyPublicKey(secKey)!
|
|
354
|
+
|
|
355
|
+
let algoType = AlgorithmType(rawValue: o["algoType"] as! String)
|
|
356
|
+
let algo = self.toiOSAlgo(algorithm: algoType!)
|
|
357
|
+
|
|
358
|
+
guard let encrypted = SecKeyCreateEncryptedData(
|
|
359
|
+
publicKey,
|
|
360
|
+
algo,
|
|
361
|
+
Data(data.utf8) as CFData,
|
|
362
|
+
nil
|
|
363
|
+
) as Data? else {
|
|
364
|
+
return nil
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return encrypted.base64EncodedString()
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
private func decrypt(alias: String, data: String, o: [String: Any]) -> String? {
|
|
371
|
+
let secKey = self.getSecKeyByAlias(alias, keyClass: kSecAttrKeyClassPrivate)
|
|
372
|
+
guard let secKey else { return nil }
|
|
373
|
+
guard let encryptedData = Data(base64Encoded: data) else { return nil }
|
|
374
|
+
|
|
375
|
+
let algoType = AlgorithmType(rawValue: o["algoType"] as! String)
|
|
376
|
+
let algo = self.toiOSAlgo(algorithm: algoType!)
|
|
377
|
+
|
|
378
|
+
guard let decrypted = SecKeyCreateDecryptedData(
|
|
379
|
+
secKey,
|
|
380
|
+
algo,
|
|
381
|
+
encryptedData as CFData,
|
|
382
|
+
nil
|
|
383
|
+
) as Data? else {
|
|
384
|
+
return nil
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return String(data: decrypted, encoding: .utf8)
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
public func definition() -> ModuleDefinition {
|
|
391
|
+
|
|
392
|
+
Name("DeviceCrypto")
|
|
393
|
+
|
|
394
|
+
Function("isAuthCheckAvailable") { () -> String in
|
|
395
|
+
return self.isAuthCheckAvailable()
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
Function("generateKeyPair") { (alias: String, o: [String: Any]) -> String in
|
|
399
|
+
return try self.generateKeyPair(alias: alias, o: o)
|
|
309
400
|
}
|
|
310
401
|
|
|
311
402
|
Function("removeKeyPair") { (alias: String) -> Bool in
|
|
@@ -317,93 +408,23 @@ public class DeviceCryptoModule: Module {
|
|
|
317
408
|
}
|
|
318
409
|
|
|
319
410
|
Function("getPublicKey") { (alias: String) -> String? in
|
|
320
|
-
|
|
321
|
-
guard let secKey else { return nil }
|
|
322
|
-
return self.retrievePublicKey(secKey)
|
|
411
|
+
return self.getPublicKey(alias:alias)
|
|
323
412
|
}
|
|
324
413
|
|
|
325
414
|
AsyncFunction("sign") { (alias: String, data: String, o: [String: Any]) -> String? in
|
|
326
|
-
|
|
327
|
-
guard let secKey else { return nil }
|
|
328
|
-
|
|
329
|
-
let algoType = AlgorithmType(rawValue: o["algoType"] as! String)
|
|
330
|
-
let algo = self.toiOSAlgo(algorithm: algoType!)
|
|
331
|
-
|
|
332
|
-
var signingError: Unmanaged<CFError>?
|
|
333
|
-
let signatureCF = SecKeyCreateSignature(
|
|
334
|
-
secKey,
|
|
335
|
-
algo,
|
|
336
|
-
Data(data.utf8) as CFData,
|
|
337
|
-
&signingError
|
|
338
|
-
)
|
|
339
|
-
|
|
340
|
-
if let error = signingError?.takeRetainedValue() {
|
|
341
|
-
throw error as Error
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
guard let signatureCF else { return nil }
|
|
345
|
-
let signature = signatureCF as Data
|
|
346
|
-
return signature.base64EncodedString()
|
|
415
|
+
return try self.sign(alias: alias, data: data, o: o)
|
|
347
416
|
}
|
|
348
417
|
|
|
349
418
|
Function("verify") { (alias: String, data: String, signature: String, o: [String: Any]) -> Bool? in
|
|
350
|
-
|
|
351
|
-
guard let secKey else { return nil }
|
|
352
|
-
|
|
353
|
-
guard let publicKey = SecKeyCopyPublicKey(secKey) else { return nil }
|
|
354
|
-
guard let signatureData = Data(base64Encoded: signature) else { return nil }
|
|
355
|
-
|
|
356
|
-
let algoType = AlgorithmType(rawValue: o["algoType"] as! String)
|
|
357
|
-
let algo = self.toiOSAlgo(algorithm: algoType!)
|
|
358
|
-
|
|
359
|
-
let valid = SecKeyVerifySignature(
|
|
360
|
-
publicKey,
|
|
361
|
-
algo,
|
|
362
|
-
Data(data.utf8) as CFData,
|
|
363
|
-
signatureData as CFData,
|
|
364
|
-
nil
|
|
365
|
-
)
|
|
366
|
-
return valid
|
|
419
|
+
return self.verify(alias: alias, data: data, signature: signature, o: o)
|
|
367
420
|
}
|
|
368
421
|
|
|
369
422
|
AsyncFunction("encrypt") { (alias: String, data: String, o: [String: Any]) -> String? in
|
|
370
|
-
|
|
371
|
-
guard let secKey else { return nil }
|
|
372
|
-
let publicKey = SecKeyCopyPublicKey(secKey)!
|
|
373
|
-
|
|
374
|
-
let algoType = AlgorithmType(rawValue: o["algoType"] as! String)
|
|
375
|
-
let algo = self.toiOSAlgo(algorithm: algoType!)
|
|
376
|
-
|
|
377
|
-
guard let encrypted = SecKeyCreateEncryptedData(
|
|
378
|
-
publicKey,
|
|
379
|
-
algo,
|
|
380
|
-
Data(data.utf8) as CFData,
|
|
381
|
-
nil
|
|
382
|
-
) as Data? else {
|
|
383
|
-
return nil
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
return encrypted.base64EncodedString()
|
|
423
|
+
return self.encrypt(alias: alias, data: data, o: o)
|
|
387
424
|
}
|
|
388
425
|
|
|
389
426
|
AsyncFunction("decrypt") { (alias: String, data: String, o: [String: Any]) -> String? in
|
|
390
|
-
|
|
391
|
-
guard let secKey else { return nil }
|
|
392
|
-
guard let encryptedData = Data(base64Encoded: data) else { return nil }
|
|
393
|
-
|
|
394
|
-
let algoType = AlgorithmType(rawValue: o["algoType"] as! String)
|
|
395
|
-
let algo = self.toiOSAlgo(algorithm: algoType!)
|
|
396
|
-
|
|
397
|
-
guard let decrypted = SecKeyCreateDecryptedData(
|
|
398
|
-
secKey,
|
|
399
|
-
algo,
|
|
400
|
-
encryptedData as CFData,
|
|
401
|
-
nil
|
|
402
|
-
) as Data? else {
|
|
403
|
-
return nil
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
return String(data: decrypted, encoding: .utf8)
|
|
427
|
+
return self.decrypt(alias: alias, data: data, o: o)
|
|
407
428
|
}
|
|
408
429
|
}
|
|
409
430
|
}
|
package/package.json
CHANGED
|
@@ -51,29 +51,35 @@ export interface GenerateKeyPairOptions {
|
|
|
51
51
|
* }
|
|
52
52
|
* ```
|
|
53
53
|
* @default AuthMethod.PASSCODE_OR_BIOMETRIC
|
|
54
|
-
* @platform ios
|
|
54
|
+
* @platform ios 🍏
|
|
55
55
|
*/
|
|
56
56
|
authMethod?: AuthMethod;
|
|
57
|
+
/**
|
|
58
|
+
* If the Strong Box is available, it will be used instead of the Trusted Execution Environment (TEE).
|
|
59
|
+
* @default false
|
|
60
|
+
* @platform android 🤖
|
|
61
|
+
*/
|
|
62
|
+
preferStrongBox?: boolean;
|
|
57
63
|
}
|
|
58
64
|
|
|
59
65
|
interface BaseSignDecryptOptions {
|
|
60
66
|
/**
|
|
61
67
|
* The title of the prompt to show when authentication is required.
|
|
62
68
|
* @default "Unlock"
|
|
63
|
-
* @platform android
|
|
69
|
+
* @platform android 🤖
|
|
64
70
|
*/
|
|
65
71
|
promptTitle?: string;
|
|
66
72
|
/**
|
|
67
73
|
* The subtitle of the prompt to show when authentication is required.
|
|
68
74
|
* @default "Enter your PIN to continue"
|
|
69
|
-
* @platform android
|
|
75
|
+
* @platform android 🤖
|
|
70
76
|
*/
|
|
71
77
|
promptSubtitle?: string;
|
|
72
78
|
/**
|
|
73
79
|
* The method of authentication to use.
|
|
74
80
|
* Note that on iOS you have to define it when generating the key pair.
|
|
75
81
|
* @default AuthMethod.PASSCODE_OR_BIOMETRIC
|
|
76
|
-
* @platform android
|
|
82
|
+
* @platform android 🤖
|
|
77
83
|
*/
|
|
78
84
|
authMethod?: AuthMethod;
|
|
79
85
|
}
|
|
@@ -23,6 +23,7 @@ function base64ToPem(base64: string, label = "DATA") {
|
|
|
23
23
|
|
|
24
24
|
declare class DeviceCryptoModule extends NativeModule {
|
|
25
25
|
isAuthCheckAvailable(): AuthCheckResult;
|
|
26
|
+
isStrongBoxAvailable(): boolean;
|
|
26
27
|
generateKeyPair(alias: string, o: any): GenerateKeyPairResult;
|
|
27
28
|
getPublicKey(alias: string): string | null;
|
|
28
29
|
removeKeyPair(alias: string): boolean;
|
|
@@ -46,6 +47,12 @@ export default {
|
|
|
46
47
|
* @returns The result of the operation.
|
|
47
48
|
*/
|
|
48
49
|
isAuthCheckAvailable: module.isAuthCheckAvailable,
|
|
50
|
+
/**
|
|
51
|
+
* Checks if the strong box is available .
|
|
52
|
+
* @returns The result of the operation.
|
|
53
|
+
* @platform android 🤖
|
|
54
|
+
*/
|
|
55
|
+
isStrongBoxAvailable: module.isStrongBoxAvailable,
|
|
49
56
|
/**
|
|
50
57
|
* Creates a new ECDSA P‑256 key pair for the given alias, if it doesn’t already exist.
|
|
51
58
|
* @param alias - The alias to use for the key pair.
|
|
@@ -56,6 +63,7 @@ export default {
|
|
|
56
63
|
algoType: options?.algorithmType,
|
|
57
64
|
reqAuth: options?.requireAuthentication ?? false,
|
|
58
65
|
authMethod: options?.authMethod ?? AuthMethod.PASSCODE_OR_BIOMETRIC,
|
|
66
|
+
preferStrongBox: options?.preferStrongBox ?? false,
|
|
59
67
|
};
|
|
60
68
|
return module.generateKeyPair(alias, o);
|
|
61
69
|
},
|
|
@@ -89,12 +97,6 @@ export default {
|
|
|
89
97
|
* @param alias - The alias to use for the key pair.
|
|
90
98
|
* @param data - The data to sign.
|
|
91
99
|
* @param options - The options for the operation.
|
|
92
|
-
* @default {
|
|
93
|
-
* algoType: SigningAlgorithm.ECDSA_SHA256,
|
|
94
|
-
* promptTitle: "Unlock",
|
|
95
|
-
* promptSubtitle: "Enter your PIN to continue",
|
|
96
|
-
* authMethod: AuthMethod.PASSCODE_OR_BIOMETRIC
|
|
97
|
-
* }
|
|
98
100
|
* @returns The signature.
|
|
99
101
|
*/
|
|
100
102
|
sign: async (alias: string, data: string, options?: SignOptions) => {
|
|
@@ -112,9 +114,6 @@ export default {
|
|
|
112
114
|
* @param data - The data to verify in UTF-8 format.
|
|
113
115
|
* @param signature - The signature to verify in Base64 format.
|
|
114
116
|
* @param options - The options for the operation.
|
|
115
|
-
* @default {
|
|
116
|
-
* algoType: SigningAlgorithm.ECDSA_SHA256,
|
|
117
|
-
* }
|
|
118
117
|
* @returns True if the signature is valid, false otherwise.
|
|
119
118
|
*/
|
|
120
119
|
verify: async (
|
|
@@ -149,12 +148,6 @@ export default {
|
|
|
149
148
|
* @param alias - The alias to use for the key pair.
|
|
150
149
|
* @param data - The data to decrypt in Base64 format.
|
|
151
150
|
* @param options - The options for the operation.
|
|
152
|
-
* @default {
|
|
153
|
-
* algoType: EncryptionAlgorithm.RSA_2048_PKCS1,
|
|
154
|
-
* promptTitle: "Unlock",
|
|
155
|
-
* promptSubtitle: "Enter your PIN to continue",
|
|
156
|
-
* authMethod: AuthMethod.PASSCODE_OR_BIOMETRIC
|
|
157
|
-
* }
|
|
158
151
|
* @returns The decrypted data.
|
|
159
152
|
*/
|
|
160
153
|
decrypt: async (alias: string, data: string, options?: DecryptOptions) => {
|