expo-device-crypto 0.2.3 → 0.3.0
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 +13 -10
- package/android/build.gradle +2 -2
- package/android/src/main/java/expo/modules/devicecrypto/DeviceCryptoModule.kt +188 -32
- package/build/DeviceCrypto.types.d.ts +32 -20
- 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/build/DeviceCryptoModule.d.ts.map +1 -1
- package/build/DeviceCryptoModule.js +13 -0
- package/build/DeviceCryptoModule.js.map +1 -1
- package/ios/DeviceCrypto.podspec +1 -1
- package/ios/DeviceCrypto.swift +126 -6
- package/package.json +2 -5
- package/src/DeviceCrypto.types.ts +34 -13
- package/src/DeviceCryptoModule.ts +20 -5
package/README.md
CHANGED
|
@@ -28,25 +28,28 @@ If you want to allow Face ID on iOS, add this to your app config:
|
|
|
28
28
|
|
|
29
29
|
### Signature Algorithms
|
|
30
30
|
|
|
31
|
-
-
|
|
31
|
+
- `ECDSA_SECP256R1_SHA256`
|
|
32
32
|
|
|
33
33
|
**Curve:** P-256 / secp256r1. </br>
|
|
34
34
|
**Hash:** SHA-256. </br>
|
|
35
|
-
**Use it for:** Authentication challenges, message signing, and tamper detection.
|
|
36
35
|
|
|
37
36
|
### Encryption Algorithms
|
|
38
37
|
|
|
39
|
-
-
|
|
38
|
+
- `RSA_2048_PKCS1`
|
|
40
39
|
|
|
41
40
|
**Key size:** 2048 bits. </br>
|
|
42
41
|
**Padding:** PKCS#1 v1.5. </br>
|
|
43
|
-
**Use it for:** Encrypting small secrets that only the private key holder should decrypt.
|
|
44
42
|
|
|
45
|
-
-
|
|
43
|
+
- `RSA_2048_OAEP_SHA1`
|
|
46
44
|
|
|
47
45
|
**Key size:** 2048 bits. </br>
|
|
48
46
|
**Padding:** OAEP with SHA-1 and MGF1. </br>
|
|
49
|
-
|
|
47
|
+
|
|
48
|
+
- `ECIES_P256_AES256_GCM`
|
|
49
|
+
|
|
50
|
+
**Curve:** P-256 (secp256r1). </br>
|
|
51
|
+
**Symmetric cipher:** AES-256-GCM. </br>
|
|
52
|
+
**Key derivation:** HKDF-SHA256 (32-byte key from ECDH shared secret). </br>
|
|
50
53
|
|
|
51
54
|
**More coming soon...**
|
|
52
55
|
|
|
@@ -171,18 +174,18 @@ const decrypted = await DeviceCrypto.decrypt(alias, encrypted ?? "", {
|
|
|
171
174
|
- `encrypt(alias: string, data: string, options?: EncryptOptions): Promise<string | null>`
|
|
172
175
|
- Encrypts UTF-8 `data` with public key.
|
|
173
176
|
- Default: `algorithmType = RSA_2048_PKCS1`.
|
|
177
|
+
- For `algorithmType = ECIES_P256_AES256_GCM`, `peerPublicKey` is required in `options`.
|
|
174
178
|
|
|
175
179
|
- `decrypt(alias: string, data: string, options?: DecryptOptions): Promise<string | null>`
|
|
176
180
|
- Decrypts Base64 `data` with private key.
|
|
177
181
|
- Defaults: `algorithmType = RSA_2048_PKCS1`, `promptTitle = "Unlock"`, `promptSubtitle = "Enter your PIN to continue"`, `authMethod = PASSCODE_OR_BIOMETRIC`.
|
|
182
|
+
- For `algorithmType = ECIES_P256_AES256_GCM`, `peerPublicKey` is required in `options`.
|
|
178
183
|
|
|
179
184
|
**Android only methods**
|
|
180
185
|
|
|
181
186
|
- `isStrongBoxAvailable(): boolean`
|
|
182
187
|
- Returns `true` if StrongBox Keystore is supported on the device.
|
|
183
188
|
|
|
184
|
-
> ✅ JSDoc type definitions are available in `./src/DeviceCryptoModule.ts`.
|
|
185
|
-
|
|
186
189
|
## Important
|
|
187
190
|
|
|
188
191
|
### Enable Strong Box on Android
|
|
@@ -205,6 +208,6 @@ await DeviceCrypto.generateKeyPair(alias, {
|
|
|
205
208
|
|
|
206
209
|
If StrongBox is not available, Android falls back to the Trusted Execution Environment (TEE).
|
|
207
210
|
|
|
208
|
-
###
|
|
211
|
+
### Secure Enclave support on iOS
|
|
209
212
|
|
|
210
|
-
|
|
213
|
+
On iOS, only ECDSA keys can use the [Secure Enclave processor](https://developer.apple.com/documentation/security/ksecattrtokenidsecureenclave?utm_source=chatgpt.com#Discussion). RSA private-key operations 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.3.0'
|
|
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.3.0"
|
|
39
39
|
}
|
|
40
40
|
lintOptions {
|
|
41
41
|
abortOnError false
|
|
@@ -16,6 +16,7 @@ import android.security.keystore.KeyProperties.PURPOSE_DECRYPT
|
|
|
16
16
|
import android.security.keystore.KeyProperties.PURPOSE_ENCRYPT
|
|
17
17
|
import android.security.keystore.KeyProperties.PURPOSE_SIGN
|
|
18
18
|
import android.security.keystore.KeyProperties.PURPOSE_VERIFY
|
|
19
|
+
import android.security.keystore.KeyProperties.PURPOSE_AGREE_KEY
|
|
19
20
|
import android.util.Base64
|
|
20
21
|
import androidx.biometric.BiometricManager
|
|
21
22
|
import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG
|
|
@@ -32,9 +33,14 @@ import java.security.KeyStore
|
|
|
32
33
|
import java.security.Signature
|
|
33
34
|
import java.security.spec.AlgorithmParameterSpec
|
|
34
35
|
import java.security.spec.MGF1ParameterSpec
|
|
36
|
+
import java.security.spec.X509EncodedKeySpec
|
|
35
37
|
import javax.crypto.Cipher
|
|
38
|
+
import javax.crypto.KeyAgreement
|
|
39
|
+
import javax.crypto.Mac
|
|
40
|
+
import javax.crypto.spec.GCMParameterSpec
|
|
36
41
|
import javax.crypto.spec.OAEPParameterSpec
|
|
37
42
|
import javax.crypto.spec.PSource
|
|
43
|
+
import javax.crypto.spec.SecretKeySpec
|
|
38
44
|
|
|
39
45
|
|
|
40
46
|
enum class GenerateKeyPairResult {
|
|
@@ -58,6 +64,7 @@ enum class AlgorithmType {
|
|
|
58
64
|
ECDSA_SECP256R1_SHA256,
|
|
59
65
|
RSA_2048_PKCS1,
|
|
60
66
|
RSA_2048_OAEP_SHA1,
|
|
67
|
+
ECIES_P256_AES256_GCM,
|
|
61
68
|
}
|
|
62
69
|
|
|
63
70
|
class DeviceCryptoModule : Module() {
|
|
@@ -66,9 +73,46 @@ class DeviceCryptoModule : Module() {
|
|
|
66
73
|
AlgorithmType.ECDSA_SECP256R1_SHA256 -> "SHA256withECDSA"
|
|
67
74
|
AlgorithmType.RSA_2048_PKCS1 -> "RSA/ECB/PKCS1Padding"
|
|
68
75
|
AlgorithmType.RSA_2048_OAEP_SHA1 -> "RSA/ECB/OAEPwithSHA-1AndMGF1Padding"
|
|
76
|
+
AlgorithmType.ECIES_P256_AES256_GCM -> "AES/GCM/NoPadding"
|
|
77
|
+
else -> throw Exception("INVALID_ALGORITHM_TYPE")
|
|
69
78
|
}
|
|
70
79
|
}
|
|
71
80
|
|
|
81
|
+
private fun hkdfSha256(
|
|
82
|
+
ikm: ByteArray,
|
|
83
|
+
length: Int,
|
|
84
|
+
salt: ByteArray? = null,
|
|
85
|
+
info: ByteArray? = null): ByteArray {
|
|
86
|
+
val mac = Mac.getInstance("HmacSHA256")
|
|
87
|
+
val actualSalt = salt ?: ByteArray(32)
|
|
88
|
+
mac.init(SecretKeySpec(actualSalt, "HmacSHA256"))
|
|
89
|
+
val prk = mac.doFinal(ikm)
|
|
90
|
+
|
|
91
|
+
val result = ByteArray(length)
|
|
92
|
+
var t = ByteArray(0)
|
|
93
|
+
var offset = 0
|
|
94
|
+
var counter = 1
|
|
95
|
+
|
|
96
|
+
while (offset < length) {
|
|
97
|
+
val macExpand = Mac.getInstance("HmacSHA256")
|
|
98
|
+
macExpand.init(SecretKeySpec(prk, "HmacSHA256"))
|
|
99
|
+
|
|
100
|
+
macExpand.update(t)
|
|
101
|
+
if (info != null) macExpand.update(info)
|
|
102
|
+
macExpand.update(counter.toByte())
|
|
103
|
+
|
|
104
|
+
t = macExpand.doFinal()
|
|
105
|
+
|
|
106
|
+
val toCopy = minOf(t.size, length - offset)
|
|
107
|
+
System.arraycopy(t, 0, result, offset, toCopy)
|
|
108
|
+
|
|
109
|
+
offset += toCopy
|
|
110
|
+
counter++
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return result
|
|
114
|
+
}
|
|
115
|
+
|
|
72
116
|
private fun getOaepSpec(algoType: AlgorithmType): AlgorithmParameterSpec? {
|
|
73
117
|
return when (algoType) {
|
|
74
118
|
AlgorithmType.RSA_2048_OAEP_SHA1 -> OAEPParameterSpec(
|
|
@@ -107,7 +151,7 @@ class DeviceCryptoModule : Module() {
|
|
|
107
151
|
|
|
108
152
|
private fun buildECDSA(
|
|
109
153
|
alias: String,
|
|
110
|
-
digest: String
|
|
154
|
+
digest: String?,
|
|
111
155
|
reqAuth: Boolean,
|
|
112
156
|
strongBox: Boolean): KeyPairGenerator {
|
|
113
157
|
val kpg: KeyPairGenerator = KeyPairGenerator.getInstance(
|
|
@@ -119,7 +163,9 @@ class DeviceCryptoModule : Module() {
|
|
|
119
163
|
alias,
|
|
120
164
|
PURPOSE_SIGN or PURPOSE_VERIFY
|
|
121
165
|
).run {
|
|
122
|
-
|
|
166
|
+
if (digest != null) {
|
|
167
|
+
setDigests(digest)
|
|
168
|
+
}
|
|
123
169
|
setIsStrongBoxBacked(strongBox)
|
|
124
170
|
setUserAuthenticationRequired(reqAuth)
|
|
125
171
|
if (reqAuth) {
|
|
@@ -167,6 +213,37 @@ class DeviceCryptoModule : Module() {
|
|
|
167
213
|
return kpg
|
|
168
214
|
}
|
|
169
215
|
|
|
216
|
+
private fun buildECIES(
|
|
217
|
+
alias: String,
|
|
218
|
+
digest: String?,
|
|
219
|
+
reqAuth: Boolean,
|
|
220
|
+
strongBox: Boolean): KeyPairGenerator {
|
|
221
|
+
val kpg: KeyPairGenerator = KeyPairGenerator.getInstance(
|
|
222
|
+
KEY_ALGORITHM_EC,
|
|
223
|
+
"AndroidKeyStore"
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
val parameterSpec: KeyGenParameterSpec = KeyGenParameterSpec.Builder(
|
|
227
|
+
alias,
|
|
228
|
+
PURPOSE_AGREE_KEY
|
|
229
|
+
).run {
|
|
230
|
+
if (digest != null) {
|
|
231
|
+
setDigests(digest)
|
|
232
|
+
}
|
|
233
|
+
setIsStrongBoxBacked(strongBox)
|
|
234
|
+
setUserAuthenticationRequired(reqAuth)
|
|
235
|
+
if (reqAuth) {
|
|
236
|
+
setUserAuthenticationParameters(
|
|
237
|
+
30, // 30 seconds
|
|
238
|
+
AUTH_BIOMETRIC_STRONG or AUTH_DEVICE_CREDENTIAL
|
|
239
|
+
)
|
|
240
|
+
}
|
|
241
|
+
build()
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
kpg.initialize(parameterSpec)
|
|
245
|
+
return kpg
|
|
246
|
+
}
|
|
170
247
|
|
|
171
248
|
private fun showAuthPrompt(
|
|
172
249
|
onSuccess: () -> Unit,
|
|
@@ -249,6 +326,7 @@ class DeviceCryptoModule : Module() {
|
|
|
249
326
|
AlgorithmType.ECDSA_SECP256R1_SHA256 -> buildECDSA(alias, DIGEST_SHA256, reqAuth, strongBox)
|
|
250
327
|
AlgorithmType.RSA_2048_PKCS1 -> buildRSA(alias, null, ENCRYPTION_PADDING_RSA_PKCS1, reqAuth, strongBox)
|
|
251
328
|
AlgorithmType.RSA_2048_OAEP_SHA1 -> buildRSA(alias, DIGEST_SHA1, ENCRYPTION_PADDING_RSA_OAEP, reqAuth, strongBox)
|
|
329
|
+
AlgorithmType.ECIES_P256_AES256_GCM -> buildECIES(alias, null, reqAuth, strongBox)
|
|
252
330
|
else -> throw Exception("INVALID_ALGORITHM_TYPE")
|
|
253
331
|
}
|
|
254
332
|
|
|
@@ -279,6 +357,20 @@ class DeviceCryptoModule : Module() {
|
|
|
279
357
|
return Base64.encodeToString(publicKeyBytes, Base64.NO_WRAP)
|
|
280
358
|
}
|
|
281
359
|
|
|
360
|
+
private fun deriveSharedSecret(entry: KeyStore.PrivateKeyEntry, publicKey: String): ByteArray {
|
|
361
|
+
val peerPublicKeyBytes = Base64.decode(publicKey, Base64.NO_WRAP)
|
|
362
|
+
val peerPublicKeySpec = X509EncodedKeySpec(peerPublicKeyBytes)
|
|
363
|
+
val peerPublicKey = KeyFactory
|
|
364
|
+
.getInstance(KEY_ALGORITHM_EC)
|
|
365
|
+
.generatePublic(peerPublicKeySpec)
|
|
366
|
+
|
|
367
|
+
return KeyAgreement.getInstance("ECDH", "AndroidKeyStore").run {
|
|
368
|
+
init(entry.privateKey)
|
|
369
|
+
doPhase(peerPublicKey, true)
|
|
370
|
+
generateSecret()
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
282
374
|
private fun sign(alias: String, data: String, o: Map<String, Any?>, promise: Promise) {
|
|
283
375
|
val entry = getKeyStoreEntry(alias)
|
|
284
376
|
if (entry !is KeyStore.PrivateKeyEntry) {
|
|
@@ -294,24 +386,24 @@ class DeviceCryptoModule : Module() {
|
|
|
294
386
|
val algoType = AlgorithmType.valueOf(o["algoType"] as String)
|
|
295
387
|
val algo = toAndroidAlgo(algoType)
|
|
296
388
|
|
|
297
|
-
|
|
389
|
+
fun runSigning() {
|
|
298
390
|
val signature: ByteArray = Signature.getInstance(algo).apply {
|
|
299
391
|
initSign(entry.privateKey)
|
|
300
392
|
update(data.toByteArray(Charsets.UTF_8))
|
|
301
393
|
}.sign()
|
|
302
394
|
promise.resolve(Base64.encodeToString(signature, Base64.NO_WRAP))
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (!reqAuth) {
|
|
398
|
+
runSigning()
|
|
303
399
|
return
|
|
304
400
|
} else {
|
|
305
401
|
showAuthPrompt(
|
|
306
402
|
onSuccess = {
|
|
307
|
-
|
|
308
|
-
initSign(entry.privateKey)
|
|
309
|
-
update(data.toByteArray(Charsets.UTF_8))
|
|
310
|
-
}.sign()
|
|
311
|
-
promise.resolve(Base64.encodeToString(signature, Base64.NO_WRAP))
|
|
403
|
+
runSigning()
|
|
312
404
|
},
|
|
313
405
|
onError = { error ->
|
|
314
|
-
promise.reject("
|
|
406
|
+
promise.reject("AUTH_FAILED", error, null)
|
|
315
407
|
},
|
|
316
408
|
title = title,
|
|
317
409
|
subtitle = subtitle,
|
|
@@ -341,18 +433,62 @@ class DeviceCryptoModule : Module() {
|
|
|
341
433
|
return
|
|
342
434
|
}
|
|
343
435
|
|
|
436
|
+
val reqAuth = isKeyStoreRequireAuthentication(entry)
|
|
437
|
+
|
|
438
|
+
val title = o["title"] as String
|
|
439
|
+
val subtitle = o["subtitle"] as String
|
|
440
|
+
val authMethod = AuthMethod.valueOf(o["authMethod"] as String)
|
|
344
441
|
val algoType = AlgorithmType.valueOf(o["algoType"] as String)
|
|
345
442
|
val algo = toAndroidAlgo(algoType)
|
|
346
|
-
val oaepSpec = getOaepSpec(algoType)
|
|
347
|
-
|
|
348
443
|
val cipher = Cipher.getInstance(algo)
|
|
349
|
-
|
|
350
|
-
|
|
444
|
+
|
|
445
|
+
fun runEncryption(cipher: Cipher) {
|
|
446
|
+
when (algoType) {
|
|
447
|
+
AlgorithmType.RSA_2048_OAEP_SHA1 -> {
|
|
448
|
+
val oaepSpec = getOaepSpec(algoType)
|
|
449
|
+
cipher.init(Cipher.ENCRYPT_MODE, entry.certificate.publicKey, oaepSpec)
|
|
450
|
+
val encrypted = cipher.doFinal(data.toByteArray(Charsets.UTF_8))
|
|
451
|
+
promise.resolve(Base64.encodeToString(encrypted, Base64.NO_WRAP))
|
|
452
|
+
}
|
|
453
|
+
AlgorithmType.RSA_2048_PKCS1 -> {
|
|
454
|
+
cipher.init(Cipher.ENCRYPT_MODE, entry.certificate.publicKey)
|
|
455
|
+
val encrypted = cipher.doFinal(data.toByteArray(Charsets.UTF_8))
|
|
456
|
+
promise.resolve(Base64.encodeToString(encrypted, Base64.NO_WRAP))
|
|
457
|
+
}
|
|
458
|
+
AlgorithmType.ECIES_P256_AES256_GCM -> {
|
|
459
|
+
val peerPublicKey = o["peerPublicKey"] as String
|
|
460
|
+
val sharedSecret = deriveSharedSecret(entry, peerPublicKey)
|
|
461
|
+
val aesKey = SecretKeySpec(hkdfSha256(sharedSecret, 32), "AES")
|
|
462
|
+
cipher.init(Cipher.ENCRYPT_MODE, aesKey)
|
|
463
|
+
val encrypted = cipher.doFinal(data.toByteArray(Charsets.UTF_8))
|
|
464
|
+
val payload = cipher.iv + encrypted
|
|
465
|
+
promise.resolve(Base64.encodeToString(payload, Base64.NO_WRAP))
|
|
466
|
+
}
|
|
467
|
+
else -> {
|
|
468
|
+
throw Exception("INVALID_ALGORITHM_TYPE")
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
if (algoType != AlgorithmType.ECIES_P256_AES256_GCM) {
|
|
474
|
+
runEncryption(cipher)
|
|
475
|
+
return
|
|
476
|
+
} else if (!reqAuth) {
|
|
477
|
+
runEncryption(cipher)
|
|
478
|
+
return
|
|
351
479
|
} else {
|
|
352
|
-
|
|
480
|
+
showAuthPrompt(
|
|
481
|
+
onSuccess = {
|
|
482
|
+
runEncryption(cipher)
|
|
483
|
+
},
|
|
484
|
+
onError = { error ->
|
|
485
|
+
promise.reject("AUTH_FAILED", error, null)
|
|
486
|
+
},
|
|
487
|
+
title = title,
|
|
488
|
+
subtitle = subtitle,
|
|
489
|
+
authMethod = authMethod
|
|
490
|
+
)
|
|
353
491
|
}
|
|
354
|
-
val encrypted: ByteArray = cipher.doFinal(data.toByteArray(Charsets.UTF_8))
|
|
355
|
-
promise.resolve(Base64.encodeToString(encrypted, Base64.NO_WRAP))
|
|
356
492
|
}
|
|
357
493
|
|
|
358
494
|
private fun decrypt(alias: String, data: String, o: Map<String, Any?>, promise: Promise) {
|
|
@@ -369,33 +505,53 @@ class DeviceCryptoModule : Module() {
|
|
|
369
505
|
val authMethod = o["authMethod"] as String
|
|
370
506
|
val algoType = AlgorithmType.valueOf(o["algoType"] as String)
|
|
371
507
|
val algo = toAndroidAlgo(algoType)
|
|
372
|
-
val oaepSpec = getOaepSpec(algoType)
|
|
373
508
|
|
|
374
509
|
val cipher = Cipher.getInstance(algo)
|
|
375
510
|
val ciphertext = Base64.decode(data, Base64.NO_WRAP)
|
|
376
511
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
512
|
+
fun runDecryption(cipher: Cipher) {
|
|
513
|
+
when (algoType) {
|
|
514
|
+
AlgorithmType.RSA_2048_OAEP_SHA1 -> {
|
|
515
|
+
val oaepSpec = getOaepSpec(algoType)
|
|
516
|
+
cipher.init(Cipher.DECRYPT_MODE, entry.privateKey, oaepSpec)
|
|
517
|
+
val decrypted = cipher.doFinal(ciphertext)
|
|
518
|
+
promise.resolve(String(decrypted, Charsets.UTF_8))
|
|
519
|
+
return
|
|
520
|
+
}
|
|
521
|
+
AlgorithmType.RSA_2048_PKCS1 -> {
|
|
522
|
+
cipher.init(Cipher.DECRYPT_MODE, entry.privateKey)
|
|
523
|
+
val decrypted = cipher.doFinal(ciphertext)
|
|
524
|
+
promise.resolve(String(decrypted, Charsets.UTF_8))
|
|
525
|
+
return
|
|
526
|
+
}
|
|
527
|
+
AlgorithmType.ECIES_P256_AES256_GCM -> {
|
|
528
|
+
val peerPublicKey = o["peerPublicKey"] as String
|
|
529
|
+
val sharedSecret = deriveSharedSecret(entry, peerPublicKey)
|
|
530
|
+
val aesKey = SecretKeySpec(hkdfSha256(sharedSecret, 32), "AES")
|
|
531
|
+
val iv = ciphertext.copyOfRange(0, 12)
|
|
532
|
+
val encryptedPayload = ciphertext.copyOfRange(12, ciphertext.size)
|
|
533
|
+
val gcmSpec = GCMParameterSpec(128, iv)
|
|
534
|
+
cipher.init(Cipher.DECRYPT_MODE, aesKey, gcmSpec)
|
|
535
|
+
val decrypted = cipher.doFinal(encryptedPayload)
|
|
536
|
+
promise.resolve(String(decrypted, Charsets.UTF_8))
|
|
537
|
+
return
|
|
538
|
+
}
|
|
539
|
+
else -> {
|
|
540
|
+
throw Exception("INVALID_ALGORITHM_TYPE")
|
|
541
|
+
}
|
|
382
542
|
}
|
|
383
|
-
|
|
384
|
-
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
if (!reqAuth) {
|
|
546
|
+
runDecryption(cipher)
|
|
385
547
|
return
|
|
386
548
|
} else {
|
|
387
549
|
showAuthPrompt(
|
|
388
550
|
onSuccess = {
|
|
389
|
-
|
|
390
|
-
cipher.init(Cipher.DECRYPT_MODE, entry.privateKey, oaepSpec)
|
|
391
|
-
} else {
|
|
392
|
-
cipher.init(Cipher.DECRYPT_MODE, entry.privateKey)
|
|
393
|
-
}
|
|
394
|
-
val decrypted: ByteArray = cipher.doFinal(ciphertext)
|
|
395
|
-
promise.resolve(String(decrypted, Charsets.UTF_8))
|
|
551
|
+
runDecryption(cipher)
|
|
396
552
|
},
|
|
397
553
|
onError = { error ->
|
|
398
|
-
promise.reject("
|
|
554
|
+
promise.reject("AUTH_FAILED", error, null)
|
|
399
555
|
},
|
|
400
556
|
title = title,
|
|
401
557
|
subtitle = subtitle,
|
|
@@ -15,11 +15,14 @@ export declare enum AuthMethod {
|
|
|
15
15
|
export declare enum SigningAlgorithm {
|
|
16
16
|
ECDSA_SECP256R1_SHA256 = "ECDSA_SECP256R1_SHA256"
|
|
17
17
|
}
|
|
18
|
+
type EciesAlgorithm = "ECIES_P256_AES256_GCM";
|
|
19
|
+
type RsaAlgorithm = "RSA_2048_PKCS1" | "RSA_2048_OAEP_SHA1";
|
|
18
20
|
export declare enum EncryptionAlgorithm {
|
|
19
21
|
RSA_2048_PKCS1 = "RSA_2048_PKCS1",
|
|
20
|
-
RSA_2048_OAEP_SHA1 = "RSA_2048_OAEP_SHA1"
|
|
22
|
+
RSA_2048_OAEP_SHA1 = "RSA_2048_OAEP_SHA1",
|
|
23
|
+
ECIES_P256_AES256_GCM = "ECIES_P256_AES256_GCM"
|
|
21
24
|
}
|
|
22
|
-
export
|
|
25
|
+
export type GenerateKeyPairOptions = {
|
|
23
26
|
/**
|
|
24
27
|
* The algorithm type of the key to generate.
|
|
25
28
|
*/
|
|
@@ -55,8 +58,8 @@ export interface GenerateKeyPairOptions {
|
|
|
55
58
|
* @platform android 🤖
|
|
56
59
|
*/
|
|
57
60
|
preferStrongBox?: boolean;
|
|
58
|
-
}
|
|
59
|
-
|
|
61
|
+
};
|
|
62
|
+
type BaseAuthOptions = {
|
|
60
63
|
/**
|
|
61
64
|
* The title of the prompt to show when authentication is required.
|
|
62
65
|
* @default "Unlock"
|
|
@@ -76,33 +79,42 @@ interface BaseSignDecryptOptions {
|
|
|
76
79
|
* @platform android 🤖
|
|
77
80
|
*/
|
|
78
81
|
authMethod?: AuthMethod;
|
|
79
|
-
}
|
|
80
|
-
|
|
82
|
+
};
|
|
83
|
+
type BaseSigningOptions = {
|
|
81
84
|
/**
|
|
82
85
|
* The algorithm type of the key to use for signing.
|
|
83
86
|
*/
|
|
84
|
-
algorithmType
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
+
algorithmType: SigningAlgorithm;
|
|
88
|
+
};
|
|
89
|
+
type BaseVerifyOptions = BaseSigningOptions;
|
|
90
|
+
type BaseRsaEncryptionOptions = {
|
|
87
91
|
/**
|
|
88
92
|
* The algorithm type of the key to use for encrypting.
|
|
89
93
|
*/
|
|
90
|
-
algorithmType
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
94
|
+
algorithmType: RsaAlgorithm;
|
|
95
|
+
};
|
|
96
|
+
type BaseRsaDecryptionOptions = BaseRsaEncryptionOptions;
|
|
97
|
+
type BaseEciesEncryptionOptions = {
|
|
98
|
+
/**
|
|
99
|
+
* The algorithm type of the key to use for encrypting.
|
|
100
|
+
*/
|
|
101
|
+
algorithmType: EciesAlgorithm;
|
|
102
|
+
/**
|
|
103
|
+
* The peer public key to use for ECIES.
|
|
104
|
+
*/
|
|
105
|
+
peerPublicKey: string;
|
|
106
|
+
};
|
|
107
|
+
type BaseEciesDecryptionOptions = BaseEciesEncryptionOptions;
|
|
108
|
+
export type SignOptions = BaseAuthOptions & BaseSigningOptions;
|
|
109
|
+
export type VerifyOptions = BaseVerifyOptions;
|
|
110
|
+
export type DecryptOptions = (BaseAuthOptions & BaseEciesDecryptionOptions) | (BaseAuthOptions & BaseRsaDecryptionOptions);
|
|
111
|
+
export type EncryptOptions = (BaseAuthOptions & BaseEciesEncryptionOptions) | BaseRsaEncryptionOptions;
|
|
100
112
|
export interface GetPublicKeyOptions {
|
|
101
113
|
/**
|
|
102
114
|
* The format of the public key to return.
|
|
103
115
|
* @default "PEM" (Base64 of PEM-encoded SubjectPublicKeyInfo (SPKI) for P‑256)
|
|
104
116
|
*/
|
|
105
|
-
format?: "PEM";
|
|
117
|
+
format?: "PEM" | "BASE64";
|
|
106
118
|
}
|
|
107
119
|
export {};
|
|
108
120
|
//# sourceMappingURL=DeviceCrypto.types.d.ts.map
|
|
@@ -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;
|
|
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,KAAK,cAAc,GAAG,uBAAuB,CAAC;AAC9C,KAAK,YAAY,GAAG,gBAAgB,GAAG,oBAAoB,CAAC;AAC5D,oBAAY,mBAAmB;IAC7B,cAAc,mBAAmB;IACjC,kBAAkB,uBAAuB;IACzC,qBAAqB,0BAA0B;CAChD;AAED,MAAM,MAAM,sBAAsB,GAAG;IACnC;;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,CAAA;AAED,KAAK,eAAe,GAAG;IACrB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;;OAKG;IACH,UAAU,CAAC,EAAE,UAAU,CAAC;CACzB,CAAA;AAED,KAAK,kBAAkB,GAAG;IACxB;;OAEG;IACH,aAAa,EAAE,gBAAgB,CAAC;CACjC,CAAC;AACF,KAAK,iBAAiB,GAAG,kBAAkB,CAAC;AAE5C,KAAK,wBAAwB,GAAG;IAC9B;;OAEG;IACH,aAAa,EAAE,YAAY,CAAC;CAC7B,CAAC;AACF,KAAK,wBAAwB,GAAG,wBAAwB,CAAC;AAEzD,KAAK,0BAA0B,GAAG;IAChC;;OAEG;IACH,aAAa,EAAE,cAAc,CAAC;IAC9B;;OAEG;IACH,aAAa,EAAE,MAAM,CAAC;CACvB,CAAC;AACF,KAAK,0BAA0B,GAAG,0BAA0B,CAAC;AAE7D,MAAM,MAAM,WAAW,GAAG,eAAe,GAAG,kBAAkB,CAAC;AAC/D,MAAM,MAAM,aAAa,GAAG,iBAAiB,CAAC;AAE9C,MAAM,MAAM,cAAc,GACtB,CAAC,eAAe,GAAG,0BAA0B,CAAC,GAC9C,CAAC,eAAe,GAAG,wBAAwB,CAAC,CAAC;AACjD,MAAM,MAAM,cAAc,GACtB,CAAC,eAAe,GAAG,0BAA0B,CAAC,GAC9C,wBAAwB,CAAC;AAE7B,MAAM,WAAW,mBAAmB;IAClC;;;OAGG;IACH,MAAM,CAAC,EAAE,KAAK,GAAG,QAAQ,CAAC;CAC3B"}
|
|
@@ -23,5 +23,6 @@ export var EncryptionAlgorithm;
|
|
|
23
23
|
(function (EncryptionAlgorithm) {
|
|
24
24
|
EncryptionAlgorithm["RSA_2048_PKCS1"] = "RSA_2048_PKCS1";
|
|
25
25
|
EncryptionAlgorithm["RSA_2048_OAEP_SHA1"] = "RSA_2048_OAEP_SHA1";
|
|
26
|
+
EncryptionAlgorithm["ECIES_P256_AES256_GCM"] = "ECIES_P256_AES256_GCM";
|
|
26
27
|
})(EncryptionAlgorithm || (EncryptionAlgorithm = {}));
|
|
27
28
|
//# 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;
|
|
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;AAID,MAAM,CAAN,IAAY,mBAIX;AAJD,WAAY,mBAAmB;IAC7B,wDAAiC,CAAA;IACjC,gEAAyC,CAAA;IACzC,sEAA+C,CAAA;AACjD,CAAC,EAJW,mBAAmB,KAAnB,mBAAmB,QAI9B","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\ntype EciesAlgorithm = \"ECIES_P256_AES256_GCM\";\ntype RsaAlgorithm = \"RSA_2048_PKCS1\" | \"RSA_2048_OAEP_SHA1\";\nexport enum EncryptionAlgorithm {\n RSA_2048_PKCS1 = \"RSA_2048_PKCS1\",\n RSA_2048_OAEP_SHA1 = \"RSA_2048_OAEP_SHA1\",\n ECIES_P256_AES256_GCM = \"ECIES_P256_AES256_GCM\",\n}\n\nexport type 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\ntype BaseAuthOptions = {\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\ntype BaseSigningOptions = {\n /**\n * The algorithm type of the key to use for signing.\n */\n algorithmType: SigningAlgorithm;\n};\ntype BaseVerifyOptions = BaseSigningOptions;\n\ntype BaseRsaEncryptionOptions = {\n /**\n * The algorithm type of the key to use for encrypting.\n */\n algorithmType: RsaAlgorithm;\n};\ntype BaseRsaDecryptionOptions = BaseRsaEncryptionOptions;\n\ntype BaseEciesEncryptionOptions = {\n /**\n * The algorithm type of the key to use for encrypting.\n */\n algorithmType: EciesAlgorithm;\n /**\n * The peer public key to use for ECIES.\n */\n peerPublicKey: string;\n};\ntype BaseEciesDecryptionOptions = BaseEciesEncryptionOptions;\n\nexport type SignOptions = BaseAuthOptions & BaseSigningOptions;\nexport type VerifyOptions = BaseVerifyOptions;\n\nexport type DecryptOptions =\n | (BaseAuthOptions & BaseEciesDecryptionOptions)\n | (BaseAuthOptions & BaseRsaDecryptionOptions);\nexport type EncryptOptions =\n | (BaseAuthOptions & BaseEciesEncryptionOptions)\n | BaseRsaEncryptionOptions;\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\" | \"BASE64\";\n}\n"]}
|
|
@@ -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;;IA8B5B;;;OAGG;gCAvBqB,eAAe;IAyBvC;;;;OAIG;gCA5BqB,OAAO;IA8B/B;;;;OAIG;6BAC4B,MAAM,YAAY,sBAAsB;IASvE;;;;;OAKG;0BACmB,MAAM,YAAY,mBAAmB;
|
|
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;IAW3D;;;;OAIG;2BA9DkB,MAAM,KAAG,OAAO;IAgErC;;;OAGG;mBAlEQ,MAAM,EAAE;IAoEnB;;;;;;OAMG;kBACiB,MAAM,QAAQ,MAAM,YAAY,WAAW;IAU/D;;;;;;;OAOG;oBAEM,MAAM,QACP,MAAM,aACD,MAAM,YACP,aAAa;IAQzB;;;;;;;;;OASG;qBACoB,MAAM,QAAQ,MAAM,YAAY,cAAc;IAcrE;;;;;;OAMG;qBACoB,MAAM,QAAQ,MAAM,YAAY,cAAc;;AA1HvE,wBAqIE"}
|
|
@@ -45,6 +45,9 @@ export default {
|
|
|
45
45
|
if (options?.format === "PEM") {
|
|
46
46
|
return base64ToPem(publicKey, "PUBLIC KEY");
|
|
47
47
|
}
|
|
48
|
+
else if (options?.format === "BASE64") {
|
|
49
|
+
return publicKey;
|
|
50
|
+
}
|
|
48
51
|
return base64ToPem(publicKey, "PUBLIC KEY");
|
|
49
52
|
},
|
|
50
53
|
/**
|
|
@@ -101,6 +104,14 @@ export default {
|
|
|
101
104
|
encrypt: async (alias, data, options) => {
|
|
102
105
|
const o = {
|
|
103
106
|
algoType: options?.algorithmType ?? EncryptionAlgorithm.RSA_2048_PKCS1,
|
|
107
|
+
// @ts-ignore
|
|
108
|
+
title: options?.promptTitle ?? "Unlock",
|
|
109
|
+
// @ts-ignore
|
|
110
|
+
subtitle: options?.promptSubtitle ?? "Enter your PIN to continue",
|
|
111
|
+
// @ts-ignore
|
|
112
|
+
authMethod: options?.authMethod ?? AuthMethod.PASSCODE_OR_BIOMETRIC,
|
|
113
|
+
// @ts-ignore
|
|
114
|
+
peerPublicKey: options?.peerPublicKey ?? "",
|
|
104
115
|
};
|
|
105
116
|
return module.encrypt(alias, data, o);
|
|
106
117
|
},
|
|
@@ -117,6 +128,8 @@ export default {
|
|
|
117
128
|
subtitle: options?.promptSubtitle ?? "Enter your PIN to continue",
|
|
118
129
|
authMethod: options?.authMethod ?? AuthMethod.PASSCODE_OR_BIOMETRIC,
|
|
119
130
|
algoType: options?.algorithmType ?? EncryptionAlgorithm.RSA_2048_PKCS1,
|
|
131
|
+
// @ts-ignore
|
|
132
|
+
peerPublicKey: options?.peerPublicKey ?? "",
|
|
120
133
|
};
|
|
121
134
|
return module.decrypt(alias, data, o);
|
|
122
135
|
},
|
|
@@ -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;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;
|
|
1
|
+
{"version":3,"file":"DeviceCryptoModule.js","sourceRoot":"","sources":["../src/DeviceCryptoModule.ts"],"names":[],"mappings":"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","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 } else if (options?.format === \"BASE64\") {\n return publicKey;\n }\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:\n 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:\n 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 // @ts-ignore\n title: options?.promptTitle ?? \"Unlock\",\n // @ts-ignore\n subtitle: options?.promptSubtitle ?? \"Enter your PIN to continue\",\n // @ts-ignore\n authMethod: options?.authMethod ?? AuthMethod.PASSCODE_OR_BIOMETRIC,\n // @ts-ignore\n peerPublicKey: options?.peerPublicKey ?? \"\",\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 // @ts-ignore\n peerPublicKey: options?.peerPublicKey ?? \"\",\n };\n return module.decrypt(alias, data, o);\n },\n};\n"]}
|
package/ios/DeviceCrypto.podspec
CHANGED
package/ios/DeviceCrypto.swift
CHANGED
|
@@ -2,6 +2,7 @@ import ExpoModulesCore
|
|
|
2
2
|
import Security
|
|
3
3
|
import Foundation
|
|
4
4
|
import LocalAuthentication
|
|
5
|
+
import CryptoKit
|
|
5
6
|
|
|
6
7
|
enum SecureSigningModuleResult: String {
|
|
7
8
|
case KEY_PAIR_GENERATED = "KEY_PAIR_GENERATED"
|
|
@@ -24,6 +25,7 @@ enum AlgorithmType: String {
|
|
|
24
25
|
case ECDSA_SECP256R1_SHA256 = "ECDSA_SECP256R1_SHA256"
|
|
25
26
|
case RSA_2048_PKCS1 = "RSA_2048_PKCS1"
|
|
26
27
|
case RSA_2048_OAEP_SHA1 = "RSA_2048_OAEP_SHA1"
|
|
28
|
+
case ECIES_P256_AES256_GCM = "ECIES_P256_AES256_GCM"
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
public class DeviceCryptoModule: Module {
|
|
@@ -35,6 +37,8 @@ public class DeviceCryptoModule: Module {
|
|
|
35
37
|
return .rsaEncryptionPKCS1
|
|
36
38
|
case .RSA_2048_OAEP_SHA1:
|
|
37
39
|
return .rsaEncryptionOAEPSHA1
|
|
40
|
+
case .ECIES_P256_AES256_GCM:
|
|
41
|
+
return .eciesEncryptionCofactorX963SHA256AESGCM
|
|
38
42
|
}
|
|
39
43
|
}
|
|
40
44
|
|
|
@@ -103,6 +107,20 @@ public class DeviceCryptoModule: Module {
|
|
|
103
107
|
return Data([0x80 | UInt8(bytes.count)]) + Data(bytes)
|
|
104
108
|
}
|
|
105
109
|
|
|
110
|
+
// Converts P‑256 SPKI DER format to ANSI x962 EC point format.
|
|
111
|
+
private func spkiP256ToX962(_ spki: Data) -> Data? {
|
|
112
|
+
let prefix = Data([
|
|
113
|
+
0x30, 0x59,
|
|
114
|
+
0x30, 0x13,
|
|
115
|
+
0x06, 0x07, 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x02, 0x01,
|
|
116
|
+
0x06, 0x08, 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x03, 0x01, 0x07,
|
|
117
|
+
0x03, 0x42, 0x00
|
|
118
|
+
])
|
|
119
|
+
guard spki.count == prefix.count + 65 else { return nil }
|
|
120
|
+
guard spki.starts(with: prefix) else { return nil }
|
|
121
|
+
return spki.suffix(65)
|
|
122
|
+
}
|
|
123
|
+
|
|
106
124
|
private func getSecKeyQuery(_ alias: String, keyClass: CFString, returnRef: Bool = true) -> [String: Any] {
|
|
107
125
|
var query: [String: Any] = [
|
|
108
126
|
kSecClass as String: kSecClassKey,
|
|
@@ -198,6 +216,76 @@ public class DeviceCryptoModule: Module {
|
|
|
198
216
|
return SecKeyCreateRandomKey(attributes, nil)
|
|
199
217
|
}
|
|
200
218
|
|
|
219
|
+
private func buildECIES(alias: String, reqAuth: Bool, authMethod: AuthMethod) -> SecKey? {
|
|
220
|
+
let accessFlags: SecAccessControlCreateFlags
|
|
221
|
+
if reqAuth {
|
|
222
|
+
switch authMethod {
|
|
223
|
+
case .PASSCODE:
|
|
224
|
+
accessFlags = [.privateKeyUsage, .devicePasscode]
|
|
225
|
+
case .PASSCODE_OR_BIOMETRIC:
|
|
226
|
+
accessFlags = [.privateKeyUsage, .userPresence]
|
|
227
|
+
}
|
|
228
|
+
} else {
|
|
229
|
+
accessFlags = .privateKeyUsage
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
let access = SecAccessControlCreateWithFlags(
|
|
233
|
+
kCFAllocatorDefault,
|
|
234
|
+
kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
|
|
235
|
+
accessFlags,
|
|
236
|
+
nil
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
let attributes: NSDictionary = [
|
|
240
|
+
kSecAttrKeyType: kSecAttrKeyTypeECSECPrimeRandom,
|
|
241
|
+
kSecAttrKeySizeInBits: 256,
|
|
242
|
+
kSecAttrTokenID: kSecAttrTokenIDSecureEnclave,
|
|
243
|
+
kSecPrivateKeyAttrs: [
|
|
244
|
+
kSecAttrIsPermanent: true,
|
|
245
|
+
kSecAttrApplicationTag: alias,
|
|
246
|
+
kSecAttrAccessControl: access
|
|
247
|
+
],
|
|
248
|
+
kSecPublicKeyAttrs: [
|
|
249
|
+
kSecAttrIsPermanent: true,
|
|
250
|
+
kSecAttrApplicationTag: alias
|
|
251
|
+
]
|
|
252
|
+
]
|
|
253
|
+
|
|
254
|
+
return SecKeyCreateRandomKey(attributes, nil)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
private func secKeyFromPeerPublicKey(_ peerPublicKey: String) -> SecKey? {
|
|
258
|
+
guard
|
|
259
|
+
let peerPublicKeyData = Data(base64Encoded: peerPublicKey),
|
|
260
|
+
let x962 = spkiP256ToX962(peerPublicKeyData)
|
|
261
|
+
else {
|
|
262
|
+
return nil
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
let attrs: [String: Any] = [
|
|
266
|
+
kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom,
|
|
267
|
+
kSecAttrKeyClass as String: kSecAttrKeyClassPublic,
|
|
268
|
+
kSecAttrKeySizeInBits as String: 256
|
|
269
|
+
]
|
|
270
|
+
|
|
271
|
+
return SecKeyCreateWithData(x962 as CFData, attrs as CFDictionary, nil)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
private func deriveSharedSecret(privateKey: SecKey, peerPublicKey: String) -> Data? {
|
|
275
|
+
guard let peerSecKey = secKeyFromPeerPublicKey(peerPublicKey) else { return nil }
|
|
276
|
+
let algorithm = SecKeyAlgorithm.ecdhKeyExchangeStandard
|
|
277
|
+
guard SecKeyIsAlgorithmSupported(privateKey, .keyExchange, algorithm) else { return nil }
|
|
278
|
+
|
|
279
|
+
let params: [String: Any] = [:]
|
|
280
|
+
return SecKeyCopyKeyExchangeResult(
|
|
281
|
+
privateKey,
|
|
282
|
+
algorithm,
|
|
283
|
+
peerSecKey,
|
|
284
|
+
params as CFDictionary,
|
|
285
|
+
nil
|
|
286
|
+
) as Data?
|
|
287
|
+
}
|
|
288
|
+
|
|
201
289
|
private func isAuthCheckAvailable() -> String {
|
|
202
290
|
let context = LAContext()
|
|
203
291
|
let available = context.canEvaluatePolicy(.deviceOwnerAuthentication, error: nil)
|
|
@@ -233,6 +321,8 @@ public class DeviceCryptoModule: Module {
|
|
|
233
321
|
self.buildRSA(alias: alias, reqAuth: reqAuth, authMethod: authMethod!)
|
|
234
322
|
case .RSA_2048_OAEP_SHA1:
|
|
235
323
|
self.buildRSA(alias: alias, reqAuth: reqAuth, authMethod: authMethod!)
|
|
324
|
+
case .ECIES_P256_AES256_GCM:
|
|
325
|
+
self.buildECIES(alias: alias, reqAuth: reqAuth, authMethod: authMethod!)
|
|
236
326
|
default:
|
|
237
327
|
throw NSError(
|
|
238
328
|
domain: "DeviceCrypto",
|
|
@@ -347,12 +437,27 @@ public class DeviceCryptoModule: Module {
|
|
|
347
437
|
return valid
|
|
348
438
|
}
|
|
349
439
|
|
|
350
|
-
private func encrypt(alias: String, data: String, o: [String: Any]) -> String? {
|
|
440
|
+
private func encrypt(alias: String, data: String, o: [String: Any]) throws -> String? {
|
|
441
|
+
let algoType = AlgorithmType(rawValue: o["algoType"] as! String)
|
|
442
|
+
|
|
443
|
+
if algoType == .ECIES_P256_AES256_GCM {
|
|
444
|
+
let secKey = self.getSecKeyByAlias(alias, keyClass: kSecAttrKeyClassPrivate)
|
|
445
|
+
guard let secKey else { return nil }
|
|
446
|
+
let peerPublicKey = o["peerPublicKey"] as! String
|
|
447
|
+
let sharedSecret = deriveSharedSecret(privateKey: secKey, peerPublicKey: peerPublicKey)
|
|
448
|
+
guard let sharedSecret else { return nil }
|
|
449
|
+
|
|
450
|
+
let symmetricKey = HKDF<SHA256>.deriveKey(
|
|
451
|
+
inputKeyMaterial: SymmetricKey(data: sharedSecret),
|
|
452
|
+
outputByteCount: 32
|
|
453
|
+
)
|
|
454
|
+
let sealed = try AES.GCM.seal(Data(data.utf8), using: symmetricKey)
|
|
455
|
+
return sealed.combined!.base64EncodedString()
|
|
456
|
+
}
|
|
457
|
+
|
|
351
458
|
let secKey = self.getSecKeyByAlias(alias, keyClass: kSecAttrKeyClassPublic)
|
|
352
459
|
guard let secKey else { return nil }
|
|
353
460
|
let publicKey = SecKeyCopyPublicKey(secKey)!
|
|
354
|
-
|
|
355
|
-
let algoType = AlgorithmType(rawValue: o["algoType"] as! String)
|
|
356
461
|
let algo = self.toiOSAlgo(algorithm: algoType!)
|
|
357
462
|
|
|
358
463
|
guard let encrypted = SecKeyCreateEncryptedData(
|
|
@@ -367,12 +472,27 @@ public class DeviceCryptoModule: Module {
|
|
|
367
472
|
return encrypted.base64EncodedString()
|
|
368
473
|
}
|
|
369
474
|
|
|
370
|
-
private func decrypt(alias: String, data: String, o: [String: Any]) -> String? {
|
|
475
|
+
private func decrypt(alias: String, data: String, o: [String: Any]) throws -> String? {
|
|
371
476
|
let secKey = self.getSecKeyByAlias(alias, keyClass: kSecAttrKeyClassPrivate)
|
|
372
477
|
guard let secKey else { return nil }
|
|
373
478
|
guard let encryptedData = Data(base64Encoded: data) else { return nil }
|
|
374
479
|
|
|
375
480
|
let algoType = AlgorithmType(rawValue: o["algoType"] as! String)
|
|
481
|
+
|
|
482
|
+
if algoType == .ECIES_P256_AES256_GCM {
|
|
483
|
+
let peerPublicKey = o["peerPublicKey"] as! String
|
|
484
|
+
let sharedSecret = deriveSharedSecret(privateKey: secKey, peerPublicKey: peerPublicKey)
|
|
485
|
+
guard let sharedSecret else { return nil }
|
|
486
|
+
|
|
487
|
+
let symmetricKey = HKDF<SHA256>.deriveKey(
|
|
488
|
+
inputKeyMaterial: SymmetricKey(data: sharedSecret),
|
|
489
|
+
outputByteCount: 32
|
|
490
|
+
)
|
|
491
|
+
let sealedBox = try AES.GCM.SealedBox(combined: encryptedData)
|
|
492
|
+
let decrypted = try AES.GCM.open(sealedBox, using: symmetricKey)
|
|
493
|
+
return String(data: decrypted, encoding: .utf8)
|
|
494
|
+
}
|
|
495
|
+
|
|
376
496
|
let algo = self.toiOSAlgo(algorithm: algoType!)
|
|
377
497
|
|
|
378
498
|
guard let decrypted = SecKeyCreateDecryptedData(
|
|
@@ -420,11 +540,11 @@ public class DeviceCryptoModule: Module {
|
|
|
420
540
|
}
|
|
421
541
|
|
|
422
542
|
AsyncFunction("encrypt") { (alias: String, data: String, o: [String: Any]) -> String? in
|
|
423
|
-
return self.encrypt(alias: alias, data: data, o: o)
|
|
543
|
+
return try self.encrypt(alias: alias, data: data, o: o)
|
|
424
544
|
}
|
|
425
545
|
|
|
426
546
|
AsyncFunction("decrypt") { (alias: String, data: String, o: [String: Any]) -> String? in
|
|
427
|
-
return self.decrypt(alias: alias, data: data, o: o)
|
|
547
|
+
return try self.decrypt(alias: alias, data: data, o: o)
|
|
428
548
|
}
|
|
429
549
|
}
|
|
430
550
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "expo-device-crypto",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Crypto for Expo using device hardware capabilities",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"types": "build/index.d.ts",
|
|
@@ -19,17 +19,14 @@
|
|
|
19
19
|
"expo",
|
|
20
20
|
"expo-module",
|
|
21
21
|
"react-native",
|
|
22
|
-
"native-module",
|
|
23
22
|
"crypto",
|
|
24
23
|
"cryptography",
|
|
25
24
|
"device-crypto",
|
|
26
25
|
"hardware-backed",
|
|
27
26
|
"secure-enclave",
|
|
28
27
|
"android-keystore",
|
|
29
|
-
"ios-keychain",
|
|
30
|
-
"key-management",
|
|
31
|
-
"secure-key-storage",
|
|
32
28
|
"digital-signature",
|
|
29
|
+
"ecies",
|
|
33
30
|
"ecdsa",
|
|
34
31
|
"secp256r1",
|
|
35
32
|
"rsa",
|
|
@@ -19,12 +19,15 @@ export enum SigningAlgorithm {
|
|
|
19
19
|
ECDSA_SECP256R1_SHA256 = "ECDSA_SECP256R1_SHA256",
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
type EciesAlgorithm = "ECIES_P256_AES256_GCM";
|
|
23
|
+
type RsaAlgorithm = "RSA_2048_PKCS1" | "RSA_2048_OAEP_SHA1";
|
|
22
24
|
export enum EncryptionAlgorithm {
|
|
23
25
|
RSA_2048_PKCS1 = "RSA_2048_PKCS1",
|
|
24
26
|
RSA_2048_OAEP_SHA1 = "RSA_2048_OAEP_SHA1",
|
|
27
|
+
ECIES_P256_AES256_GCM = "ECIES_P256_AES256_GCM",
|
|
25
28
|
}
|
|
26
29
|
|
|
27
|
-
export
|
|
30
|
+
export type GenerateKeyPairOptions = {
|
|
28
31
|
/**
|
|
29
32
|
* The algorithm type of the key to generate.
|
|
30
33
|
*/
|
|
@@ -62,7 +65,7 @@ export interface GenerateKeyPairOptions {
|
|
|
62
65
|
preferStrongBox?: boolean;
|
|
63
66
|
}
|
|
64
67
|
|
|
65
|
-
|
|
68
|
+
type BaseAuthOptions = {
|
|
66
69
|
/**
|
|
67
70
|
* The title of the prompt to show when authentication is required.
|
|
68
71
|
* @default "Unlock"
|
|
@@ -84,30 +87,48 @@ interface BaseSignDecryptOptions {
|
|
|
84
87
|
authMethod?: AuthMethod;
|
|
85
88
|
}
|
|
86
89
|
|
|
87
|
-
|
|
90
|
+
type BaseSigningOptions = {
|
|
88
91
|
/**
|
|
89
92
|
* The algorithm type of the key to use for signing.
|
|
90
93
|
*/
|
|
91
|
-
algorithmType
|
|
92
|
-
}
|
|
94
|
+
algorithmType: SigningAlgorithm;
|
|
95
|
+
};
|
|
96
|
+
type BaseVerifyOptions = BaseSigningOptions;
|
|
93
97
|
|
|
94
|
-
|
|
98
|
+
type BaseRsaEncryptionOptions = {
|
|
95
99
|
/**
|
|
96
100
|
* The algorithm type of the key to use for encrypting.
|
|
97
101
|
*/
|
|
98
|
-
algorithmType
|
|
99
|
-
}
|
|
102
|
+
algorithmType: RsaAlgorithm;
|
|
103
|
+
};
|
|
104
|
+
type BaseRsaDecryptionOptions = BaseRsaEncryptionOptions;
|
|
105
|
+
|
|
106
|
+
type BaseEciesEncryptionOptions = {
|
|
107
|
+
/**
|
|
108
|
+
* The algorithm type of the key to use for encrypting.
|
|
109
|
+
*/
|
|
110
|
+
algorithmType: EciesAlgorithm;
|
|
111
|
+
/**
|
|
112
|
+
* The peer public key to use for ECIES.
|
|
113
|
+
*/
|
|
114
|
+
peerPublicKey: string;
|
|
115
|
+
};
|
|
116
|
+
type BaseEciesDecryptionOptions = BaseEciesEncryptionOptions;
|
|
100
117
|
|
|
101
|
-
export
|
|
102
|
-
export
|
|
118
|
+
export type SignOptions = BaseAuthOptions & BaseSigningOptions;
|
|
119
|
+
export type VerifyOptions = BaseVerifyOptions;
|
|
103
120
|
|
|
104
|
-
export
|
|
105
|
-
|
|
121
|
+
export type DecryptOptions =
|
|
122
|
+
| (BaseAuthOptions & BaseEciesDecryptionOptions)
|
|
123
|
+
| (BaseAuthOptions & BaseRsaDecryptionOptions);
|
|
124
|
+
export type EncryptOptions =
|
|
125
|
+
| (BaseAuthOptions & BaseEciesEncryptionOptions)
|
|
126
|
+
| BaseRsaEncryptionOptions;
|
|
106
127
|
|
|
107
128
|
export interface GetPublicKeyOptions {
|
|
108
129
|
/**
|
|
109
130
|
* The format of the public key to return.
|
|
110
131
|
* @default "PEM" (Base64 of PEM-encoded SubjectPublicKeyInfo (SPKI) for P‑256)
|
|
111
132
|
*/
|
|
112
|
-
format?: "PEM";
|
|
133
|
+
format?: "PEM" | "BASE64";
|
|
113
134
|
}
|
|
@@ -78,7 +78,10 @@ export default {
|
|
|
78
78
|
if (!publicKey) return null;
|
|
79
79
|
if (options?.format === "PEM") {
|
|
80
80
|
return base64ToPem(publicKey, "PUBLIC KEY");
|
|
81
|
+
} else if (options?.format === "BASE64") {
|
|
82
|
+
return publicKey;
|
|
81
83
|
}
|
|
84
|
+
|
|
82
85
|
return base64ToPem(publicKey, "PUBLIC KEY");
|
|
83
86
|
},
|
|
84
87
|
/**
|
|
@@ -104,7 +107,8 @@ export default {
|
|
|
104
107
|
title: options?.promptTitle ?? "Unlock",
|
|
105
108
|
subtitle: options?.promptSubtitle ?? "Enter your PIN to continue",
|
|
106
109
|
authMethod: options?.authMethod ?? AuthMethod.PASSCODE_OR_BIOMETRIC,
|
|
107
|
-
algoType:
|
|
110
|
+
algoType:
|
|
111
|
+
options?.algorithmType ?? SigningAlgorithm.ECDSA_SECP256R1_SHA256,
|
|
108
112
|
};
|
|
109
113
|
return module.sign(alias, data, o);
|
|
110
114
|
},
|
|
@@ -123,7 +127,8 @@ export default {
|
|
|
123
127
|
options?: VerifyOptions
|
|
124
128
|
) => {
|
|
125
129
|
const o = {
|
|
126
|
-
algoType:
|
|
130
|
+
algoType:
|
|
131
|
+
options?.algorithmType ?? SigningAlgorithm.ECDSA_SECP256R1_SHA256,
|
|
127
132
|
};
|
|
128
133
|
return module.verify(alias, data, signature, o);
|
|
129
134
|
},
|
|
@@ -132,14 +137,22 @@ export default {
|
|
|
132
137
|
* @param alias - The alias to use for the key pair.
|
|
133
138
|
* @param data - The data to sign in UTF-8 format.
|
|
134
139
|
* @param options - The options for the operation.
|
|
135
|
-
* @default {
|
|
136
|
-
* algoType: EncryptionAlgorithm.RSA_2048_PKCS1,
|
|
140
|
+
* @default {
|
|
141
|
+
* algoType: EncryptionAlgorithm.RSA_2048_PKCS1,
|
|
137
142
|
* }
|
|
138
143
|
* @returns The signature.
|
|
139
144
|
*/
|
|
140
145
|
encrypt: async (alias: string, data: string, options?: EncryptOptions) => {
|
|
141
146
|
const o = {
|
|
142
147
|
algoType: options?.algorithmType ?? EncryptionAlgorithm.RSA_2048_PKCS1,
|
|
148
|
+
// @ts-ignore
|
|
149
|
+
title: options?.promptTitle ?? "Unlock",
|
|
150
|
+
// @ts-ignore
|
|
151
|
+
subtitle: options?.promptSubtitle ?? "Enter your PIN to continue",
|
|
152
|
+
// @ts-ignore
|
|
153
|
+
authMethod: options?.authMethod ?? AuthMethod.PASSCODE_OR_BIOMETRIC,
|
|
154
|
+
// @ts-ignore
|
|
155
|
+
peerPublicKey: options?.peerPublicKey ?? "",
|
|
143
156
|
};
|
|
144
157
|
return module.encrypt(alias, data, o);
|
|
145
158
|
},
|
|
@@ -156,7 +169,9 @@ export default {
|
|
|
156
169
|
subtitle: options?.promptSubtitle ?? "Enter your PIN to continue",
|
|
157
170
|
authMethod: options?.authMethod ?? AuthMethod.PASSCODE_OR_BIOMETRIC,
|
|
158
171
|
algoType: options?.algorithmType ?? EncryptionAlgorithm.RSA_2048_PKCS1,
|
|
172
|
+
// @ts-ignore
|
|
173
|
+
peerPublicKey: options?.peerPublicKey ?? "",
|
|
159
174
|
};
|
|
160
175
|
return module.decrypt(alias, data, o);
|
|
161
176
|
},
|
|
162
|
-
};
|
|
177
|
+
};
|