expo-device-crypto 0.1.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/.eslintrc.js +5 -0
- package/README.md +132 -0
- package/android/build.gradle +47 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/expo/modules/devicecrypto/DeviceCryptoModule.kt +385 -0
- package/build/DeviceCrypto.types.d.ts +101 -0
- package/build/DeviceCrypto.types.d.ts.map +1 -0
- package/build/DeviceCrypto.types.js +26 -0
- package/build/DeviceCrypto.types.js.map +1 -0
- package/build/DeviceCryptoModule.d.ts +86 -0
- package/build/DeviceCryptoModule.d.ts.map +1 -0
- package/build/DeviceCryptoModule.js +132 -0
- package/build/DeviceCryptoModule.js.map +1 -0
- package/build/index.d.ts +3 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +3 -0
- package/build/index.js.map +1 -0
- package/expo-module.config.json +9 -0
- package/ios/DeviceCrypto.podspec +23 -0
- package/ios/DeviceCrypto.swift +404 -0
- package/package.json +55 -0
- package/src/DeviceCrypto.types.ts +106 -0
- package/src/DeviceCryptoModule.ts +171 -0
- package/src/index.ts +2 -0
- package/tsconfig.json +9 -0
package/.eslintrc.js
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
## `expo-device-crypto`
|
|
2
|
+
|
|
3
|
+
> ⚠️ This module is currently in beta and is not suitable for production use.
|
|
4
|
+
|
|
5
|
+
Hardware-backed cryptography for Expo apps using Android Keystore and Apple Secure Enclave/Keychain where available.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx expo install expo-device-crypto
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
If you want to allow Face ID on iOS, add this to your app config:
|
|
14
|
+
|
|
15
|
+
```json
|
|
16
|
+
{
|
|
17
|
+
"expo": {
|
|
18
|
+
"ios": {
|
|
19
|
+
"infoPlist": {
|
|
20
|
+
"NSFaceIDUsageDescription": "We use Face ID to protect your cryptographic keys."
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Supported Algorithms
|
|
28
|
+
|
|
29
|
+
### ECDSA (`ECDSA_SECP256R1_SHA256`)
|
|
30
|
+
|
|
31
|
+
- **Type:** Digital signature algorithm (asymmetric).
|
|
32
|
+
- **Curve:** P-256 / secp256r1.
|
|
33
|
+
- **Hash:** SHA-256.
|
|
34
|
+
- **Use it for:** Authentication challenges, message signing, and tamper detection.
|
|
35
|
+
|
|
36
|
+
### RSA (`RSA_2048_PKCS1`)
|
|
37
|
+
|
|
38
|
+
- **Type:** Asymmetric encryption algorithm.
|
|
39
|
+
- **Key size:** 2048 bits.
|
|
40
|
+
- **Padding:** PKCS#1 v1.5.
|
|
41
|
+
- **Use it for:** Encrypting small secrets that only the private key holder should decrypt.
|
|
42
|
+
|
|
43
|
+
## Biometric and Passcode Protection
|
|
44
|
+
|
|
45
|
+
You can bind private key usage to user authentication with:
|
|
46
|
+
|
|
47
|
+
- `requireAuthentication: true` when creating a key pair. (set to `false` to allow signing and decryption without authentication prompts)
|
|
48
|
+
- Use `AuthMethod.PASSCODE` or `AuthMethod.PASSCODE_OR_BIOMETRIC` to specify whether authentication should require only a passcode or allow biometrics when available.
|
|
49
|
+
|
|
50
|
+
Before using authentication-protected keys, verify that device authentication is available. If the user has not enabled at least a passcode, this function returns `AuthCheckResult.UNAVAILABLE`.
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
import DeviceCrypto, { AuthCheckResult } from "expo-device-crypto";
|
|
54
|
+
|
|
55
|
+
const authStatus = DeviceCrypto.isAuthCheckAvailable();
|
|
56
|
+
if (authStatus !== AuthCheckResult.AVAILABLE) {
|
|
57
|
+
throw new Error(`Authentication unavailable: ${authStatus}`);
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Example: ECDSA with Biometric
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
import DeviceCrypto, {
|
|
65
|
+
AuthMethod,
|
|
66
|
+
SigningAlgorithm,
|
|
67
|
+
} from "expo-device-crypto";
|
|
68
|
+
|
|
69
|
+
const alias = "user-signing-key";
|
|
70
|
+
const payload = "Sign this challenge payload";
|
|
71
|
+
const algorithmType = SigningAlgorithm.ECDSA_SECP256R1_SHA256;
|
|
72
|
+
const authMethod = AuthMethod.PASSCODE_OR_BIOMETRIC;
|
|
73
|
+
|
|
74
|
+
// 1) Generate an auth-protected ECDSA key pair
|
|
75
|
+
await DeviceCrypto.generateKeyPair(alias, {
|
|
76
|
+
requireAuthentication: true,
|
|
77
|
+
authMethod, // iOS: defined at key generation;
|
|
78
|
+
algorithmType,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// 2) Optional: share public key with backend
|
|
82
|
+
const publicKeyPem = DeviceCrypto.getPublicKey(alias, { format: "PEM" });
|
|
83
|
+
|
|
84
|
+
// 3) Sign with private key (prompts biometric/passcode on protected keys)
|
|
85
|
+
// Note: The data to sign must be in UTF-8 format.
|
|
86
|
+
const signature = await DeviceCrypto.sign(alias, payload, {
|
|
87
|
+
algorithmType,
|
|
88
|
+
authMethod, // Android: defined when signing
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// 4) Verify locally (usually done server-side with stored public key)
|
|
92
|
+
const isValid = await DeviceCrypto.verify(alias, payload, signature ?? "", {
|
|
93
|
+
algorithmType,
|
|
94
|
+
});
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Example: RSA with Biometric
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
import DeviceCrypto, {
|
|
101
|
+
AuthMethod,
|
|
102
|
+
EncryptionAlgorithm,
|
|
103
|
+
} from "expo-device-crypto";
|
|
104
|
+
|
|
105
|
+
const alias = "user-encryption-key";
|
|
106
|
+
const secret = "Sensitive token";
|
|
107
|
+
const algorithmType = EncryptionAlgorithm.RSA_2048_PKCS1;
|
|
108
|
+
const authMethod = AuthMethod.PASSCODE_OR_BIOMETRIC;
|
|
109
|
+
|
|
110
|
+
// 1) Generate an auth-protected RSA key pair
|
|
111
|
+
await DeviceCrypto.generateKeyPair(alias, {
|
|
112
|
+
algorithmType,
|
|
113
|
+
requireAuthentication: true,
|
|
114
|
+
authMethod, // iOS: defined at key generation;
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// 2) Optional: retrieve/share public key with backend
|
|
118
|
+
const publicKeyPem = DeviceCrypto.getPublicKey(alias, { format: "PEM" });
|
|
119
|
+
|
|
120
|
+
// 3) Encrypt with public key
|
|
121
|
+
const encrypted = await DeviceCrypto.encrypt(alias, secret, {
|
|
122
|
+
algorithmType,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// 4) Decrypt with private key (prompts biometric/passcode on protected keys)
|
|
126
|
+
// Note: Data to encrypt must be in Base64 format.
|
|
127
|
+
const decrypted = await DeviceCrypto.decrypt(alias, encrypted ?? "", {
|
|
128
|
+
algorithmType,
|
|
129
|
+
authMethod, // Android: defined when decrypting
|
|
130
|
+
});
|
|
131
|
+
```
|
|
132
|
+
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
apply plugin: 'com.android.library'
|
|
2
|
+
|
|
3
|
+
group = 'expo.modules.devicecrypto'
|
|
4
|
+
version = '0.1.0'
|
|
5
|
+
|
|
6
|
+
def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
|
|
7
|
+
apply from: expoModulesCorePlugin
|
|
8
|
+
applyKotlinExpoModulesCorePlugin()
|
|
9
|
+
useCoreDependencies()
|
|
10
|
+
useExpoPublishing()
|
|
11
|
+
|
|
12
|
+
// If you want to use the managed Android SDK versions from expo-modules-core, set this to true.
|
|
13
|
+
// The Android SDK versions will be bumped from time to time in SDK releases and may introduce breaking changes in your module code.
|
|
14
|
+
// Most of the time, you may like to manage the Android SDK versions yourself.
|
|
15
|
+
def useManagedAndroidSdkVersions = false
|
|
16
|
+
if (useManagedAndroidSdkVersions) {
|
|
17
|
+
useDefaultAndroidSdkVersions()
|
|
18
|
+
} else {
|
|
19
|
+
buildscript {
|
|
20
|
+
// Simple helper that allows the root project to override versions declared by this library.
|
|
21
|
+
ext.safeExtGet = { prop, fallback ->
|
|
22
|
+
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
project.android {
|
|
26
|
+
compileSdkVersion safeExtGet("compileSdkVersion", 36)
|
|
27
|
+
defaultConfig {
|
|
28
|
+
minSdkVersion safeExtGet("minSdkVersion", 24)
|
|
29
|
+
targetSdkVersion safeExtGet("targetSdkVersion", 36)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
android {
|
|
35
|
+
namespace "expo.modules.devicecrypto"
|
|
36
|
+
defaultConfig {
|
|
37
|
+
versionCode 1
|
|
38
|
+
versionName "0.1.0"
|
|
39
|
+
}
|
|
40
|
+
lintOptions {
|
|
41
|
+
abortOnError false
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
dependencies {
|
|
46
|
+
implementation "androidx.biometric:biometric:1.1.0"
|
|
47
|
+
}
|
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
package expo.modules.devicecrypto
|
|
2
|
+
|
|
3
|
+
import android.content.pm.PackageManager
|
|
4
|
+
import android.security.keystore.KeyGenParameterSpec
|
|
5
|
+
import android.security.keystore.KeyInfo
|
|
6
|
+
import android.security.keystore.KeyProperties
|
|
7
|
+
import expo.modules.kotlin.modules.Module
|
|
8
|
+
import expo.modules.kotlin.modules.ModuleDefinition
|
|
9
|
+
import java.security.KeyPairGenerator
|
|
10
|
+
import java.security.KeyStore
|
|
11
|
+
import android.util.Base64
|
|
12
|
+
import androidx.biometric.BiometricManager
|
|
13
|
+
import androidx.biometric.BiometricPrompt
|
|
14
|
+
import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG
|
|
15
|
+
import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL
|
|
16
|
+
import androidx.core.content.ContextCompat
|
|
17
|
+
import androidx.fragment.app.FragmentActivity
|
|
18
|
+
import expo.modules.kotlin.Promise
|
|
19
|
+
import java.security.Signature
|
|
20
|
+
import android.widget.Toast
|
|
21
|
+
import android.content.Context
|
|
22
|
+
import java.security.KeyFactory
|
|
23
|
+
import javax.crypto.Cipher
|
|
24
|
+
|
|
25
|
+
enum class GenerateKeyPairResult {
|
|
26
|
+
KEY_PAIR_GENERATED,
|
|
27
|
+
KEY_PAIR_ALREADY_EXISTS,
|
|
28
|
+
NOT_AVAILABLE,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
enum class AuthCheckResult {
|
|
32
|
+
AVAILABLE,
|
|
33
|
+
UNAVAILABLE,
|
|
34
|
+
NO_HARDWARE,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
enum class AuthMethod {
|
|
38
|
+
PASSCODE,
|
|
39
|
+
PASSCODE_OR_BIOMETRIC,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
enum class AlgorithmType {
|
|
43
|
+
ECDSA_SECP256R1_SHA256,
|
|
44
|
+
RSA_2048_PKCS1,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
class DeviceCryptoModule : Module() {
|
|
48
|
+
private fun toAndroidAlgo(algorithm: AlgorithmType): String {
|
|
49
|
+
return when (algorithm) {
|
|
50
|
+
AlgorithmType.ECDSA_SECP256R1_SHA256 -> "SHA256withECDSA"
|
|
51
|
+
AlgorithmType.RSA_2048_PKCS1 -> "RSA/ECB/PKCS1Padding"
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private fun isAuthCheckAvailable(): AuthCheckResult {
|
|
56
|
+
val biometricManager = BiometricManager.from(appContext.reactContext as Context)
|
|
57
|
+
return when (biometricManager.canAuthenticate(BIOMETRIC_STRONG or DEVICE_CREDENTIAL)) {
|
|
58
|
+
BiometricManager.BIOMETRIC_SUCCESS -> AuthCheckResult.AVAILABLE
|
|
59
|
+
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> AuthCheckResult.NO_HARDWARE
|
|
60
|
+
BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE -> AuthCheckResult.UNAVAILABLE
|
|
61
|
+
else -> AuthCheckResult.UNAVAILABLE
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private fun isKeyStoreAvailable(): Boolean {
|
|
66
|
+
val pm = appContext.reactContext?.packageManager ?: return false
|
|
67
|
+
val appAttestKeystore = pm.hasSystemFeature(PackageManager.FEATURE_KEYSTORE_APP_ATTEST_KEY)
|
|
68
|
+
val hardwareKeystore = pm.hasSystemFeature(PackageManager.FEATURE_HARDWARE_KEYSTORE)
|
|
69
|
+
return appAttestKeystore && hardwareKeystore
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private fun isStrongBoxAvailable(): Boolean {
|
|
73
|
+
val pm = appContext.reactContext?.packageManager ?: return false
|
|
74
|
+
val strongboxKeystore = pm.hasSystemFeature(PackageManager.FEATURE_STRONGBOX_KEYSTORE)
|
|
75
|
+
return strongboxKeystore
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private fun getAliases() = KeyStore.getInstance("AndroidKeyStore").apply {
|
|
79
|
+
load(null)
|
|
80
|
+
}.aliases().toList()
|
|
81
|
+
|
|
82
|
+
private fun isKeyStoreRequireAuthentication(entry: KeyStore.PrivateKeyEntry): Boolean {
|
|
83
|
+
val keyFactory = KeyFactory.getInstance(entry.privateKey.algorithm, "AndroidKeyStore")
|
|
84
|
+
val keyInfo = keyFactory.getKeySpec(entry.privateKey, KeyInfo::class.java)
|
|
85
|
+
return keyInfo.isUserAuthenticationRequired
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private fun getKeyStoreEntry(alias: String): KeyStore.Entry? {
|
|
89
|
+
return KeyStore.getInstance("AndroidKeyStore").apply {
|
|
90
|
+
load(null)
|
|
91
|
+
}.getEntry(alias, null)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private fun retrievePublicKey(ks: KeyStore.Entry?): String? {
|
|
95
|
+
if (ks !is KeyStore.PrivateKeyEntry) return null
|
|
96
|
+
|
|
97
|
+
val publicKeyBytes: ByteArray = ks.certificate?.publicKey?.encoded ?: return null
|
|
98
|
+
return Base64.encodeToString(publicKeyBytes, Base64.NO_WRAP)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private fun removeKeyStoreEntry(alias: String): Boolean {
|
|
102
|
+
val ks = KeyStore.getInstance("AndroidKeyStore").apply {
|
|
103
|
+
load(null)
|
|
104
|
+
}
|
|
105
|
+
if (ks.getEntry(alias, null) == null) return false
|
|
106
|
+
|
|
107
|
+
ks.deleteEntry(alias)
|
|
108
|
+
return true
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private fun buildECDSA_SECP256R1_SHA256(alias: String, reqAuth: Boolean): KeyPairGenerator {
|
|
112
|
+
val kpg: KeyPairGenerator = KeyPairGenerator.getInstance(
|
|
113
|
+
KeyProperties.KEY_ALGORITHM_EC,
|
|
114
|
+
"AndroidKeyStore"
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
val parameterSpec: KeyGenParameterSpec = KeyGenParameterSpec.Builder(
|
|
118
|
+
alias,
|
|
119
|
+
KeyProperties.PURPOSE_SIGN or KeyProperties.PURPOSE_VERIFY
|
|
120
|
+
).run {
|
|
121
|
+
setDigests(KeyProperties.DIGEST_SHA256)
|
|
122
|
+
setIsStrongBoxBacked(false) // TODO: Prefer strong box if available
|
|
123
|
+
setUserAuthenticationRequired(reqAuth)
|
|
124
|
+
if (reqAuth) {
|
|
125
|
+
setUserAuthenticationParameters(
|
|
126
|
+
30, // 30 seconds
|
|
127
|
+
KeyProperties.AUTH_BIOMETRIC_STRONG or KeyProperties.AUTH_DEVICE_CREDENTIAL
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
build()
|
|
131
|
+
}
|
|
132
|
+
kpg.initialize(parameterSpec)
|
|
133
|
+
return kpg
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private fun buildRSA_2048_PKCS1(alias: String, reqAuth: Boolean): KeyPairGenerator {
|
|
137
|
+
val kpg: KeyPairGenerator = KeyPairGenerator.getInstance(
|
|
138
|
+
KeyProperties.KEY_ALGORITHM_RSA,
|
|
139
|
+
"AndroidKeyStore"
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
val parameterSpec: KeyGenParameterSpec = KeyGenParameterSpec.Builder(
|
|
143
|
+
alias,
|
|
144
|
+
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
|
|
145
|
+
).run {
|
|
146
|
+
setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1)
|
|
147
|
+
setIsStrongBoxBacked(false) // TODO: Prefer strong box if available
|
|
148
|
+
setUserAuthenticationRequired(reqAuth)
|
|
149
|
+
if (reqAuth) {
|
|
150
|
+
setUserAuthenticationParameters(
|
|
151
|
+
30, // 30 seconds
|
|
152
|
+
KeyProperties.AUTH_BIOMETRIC_STRONG or KeyProperties.AUTH_DEVICE_CREDENTIAL
|
|
153
|
+
)
|
|
154
|
+
}
|
|
155
|
+
build()
|
|
156
|
+
}
|
|
157
|
+
kpg.initialize(parameterSpec)
|
|
158
|
+
return kpg
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
private fun showAuthPrompt(
|
|
163
|
+
onSuccess: () -> Unit,
|
|
164
|
+
onError: (String) -> Unit,
|
|
165
|
+
title: String = "Unlock",
|
|
166
|
+
subtitle: String = "Enter your PIN to continue",
|
|
167
|
+
authMethod: AuthMethod
|
|
168
|
+
) {
|
|
169
|
+
val activity = appContext.currentActivity as? FragmentActivity ?: run {
|
|
170
|
+
onError("No active FragmentActivity found")
|
|
171
|
+
return
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
activity.runOnUiThread {
|
|
175
|
+
val executor = ContextCompat.getMainExecutor(activity)
|
|
176
|
+
val biometricPrompt = BiometricPrompt(
|
|
177
|
+
activity,
|
|
178
|
+
executor,
|
|
179
|
+
object : BiometricPrompt.AuthenticationCallback() {
|
|
180
|
+
|
|
181
|
+
override fun onAuthenticationSucceeded(
|
|
182
|
+
result: BiometricPrompt.AuthenticationResult
|
|
183
|
+
) {
|
|
184
|
+
super.onAuthenticationSucceeded(result)
|
|
185
|
+
onSuccess()
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
override fun onAuthenticationError(
|
|
189
|
+
errorCode: Int,
|
|
190
|
+
errString: CharSequence
|
|
191
|
+
) {
|
|
192
|
+
super.onAuthenticationError(errorCode, errString)
|
|
193
|
+
onError(errString.toString())
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
val promptInfo = BiometricPrompt.PromptInfo.Builder()
|
|
199
|
+
.setTitle(title)
|
|
200
|
+
.setSubtitle(subtitle)
|
|
201
|
+
.setAllowedAuthenticators(
|
|
202
|
+
when (authMethod) {
|
|
203
|
+
AuthMethod.PASSCODE -> BiometricManager.Authenticators.DEVICE_CREDENTIAL
|
|
204
|
+
AuthMethod.PASSCODE_OR_BIOMETRIC -> BiometricManager.Authenticators.BIOMETRIC_STRONG or BiometricManager.Authenticators.DEVICE_CREDENTIAL
|
|
205
|
+
}
|
|
206
|
+
)
|
|
207
|
+
.build()
|
|
208
|
+
|
|
209
|
+
biometricPrompt.authenticate(promptInfo)
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
override fun definition() = ModuleDefinition {
|
|
214
|
+
Name("DeviceCrypto")
|
|
215
|
+
|
|
216
|
+
Function("isAuthCheckAvailable") { ->
|
|
217
|
+
return@Function isAuthCheckAvailable()
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
Function("generateKeyPair") { alias: String, o: Map<String, Any?> ->
|
|
221
|
+
val reqAuth = o["reqAuth"] as Boolean
|
|
222
|
+
if (reqAuth && isAuthCheckAvailable() != AuthCheckResult.AVAILABLE) {
|
|
223
|
+
throw Exception("NO_AUTH_AVAILABLE")
|
|
224
|
+
}
|
|
225
|
+
if (!isKeyStoreAvailable()) {
|
|
226
|
+
return@Function GenerateKeyPairResult.NOT_AVAILABLE
|
|
227
|
+
}
|
|
228
|
+
if (getKeyStoreEntry(alias) != null) {
|
|
229
|
+
return@Function GenerateKeyPairResult.KEY_PAIR_ALREADY_EXISTS
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
val algoType = AlgorithmType.valueOf(o["algoType"] as String)
|
|
233
|
+
val kpg = when (algoType) {
|
|
234
|
+
AlgorithmType.ECDSA_SECP256R1_SHA256 -> buildECDSA_SECP256R1_SHA256(alias, reqAuth)
|
|
235
|
+
AlgorithmType.RSA_2048_PKCS1 -> buildRSA_2048_PKCS1(alias, reqAuth)
|
|
236
|
+
else -> throw Exception("INVALID_ALGORITHM_TYPE")
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
kpg.generateKeyPair()
|
|
240
|
+
|
|
241
|
+
return@Function GenerateKeyPairResult.KEY_PAIR_GENERATED
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
Function("removeKeyPair") { alias: String ->
|
|
245
|
+
return@Function removeKeyStoreEntry(alias)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
Function("aliases") {
|
|
249
|
+
return@Function getAliases()
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
Function("getPublicKey") { alias: String ->
|
|
253
|
+
val entry = getKeyStoreEntry(alias)
|
|
254
|
+
return@Function retrievePublicKey(entry)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
AsyncFunction("sign") { alias: String,
|
|
258
|
+
data: String,
|
|
259
|
+
o: Map<String, Any?>,
|
|
260
|
+
promise: Promise ->
|
|
261
|
+
val entry = getKeyStoreEntry(alias)
|
|
262
|
+
if (entry !is KeyStore.PrivateKeyEntry) {
|
|
263
|
+
promise.resolve(null)
|
|
264
|
+
return@AsyncFunction
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
val reqAuth = isKeyStoreRequireAuthentication(entry);
|
|
268
|
+
|
|
269
|
+
val title = o["title"] as String
|
|
270
|
+
val subtitle = o["subtitle"] as String
|
|
271
|
+
val authMethod = AuthMethod.valueOf(o["authMethod"] as String)
|
|
272
|
+
val algoType = AlgorithmType.valueOf(o["algoType"] as String)
|
|
273
|
+
val algo = toAndroidAlgo(algoType)
|
|
274
|
+
|
|
275
|
+
if (!reqAuth) {
|
|
276
|
+
val signature: ByteArray = Signature.getInstance(algo).apply {
|
|
277
|
+
initSign(entry.privateKey)
|
|
278
|
+
update(data.toByteArray(Charsets.UTF_8))
|
|
279
|
+
}.sign()
|
|
280
|
+
promise.resolve(Base64.encodeToString(signature, Base64.NO_WRAP))
|
|
281
|
+
return@AsyncFunction
|
|
282
|
+
} else {
|
|
283
|
+
showAuthPrompt(
|
|
284
|
+
onSuccess = {
|
|
285
|
+
val signature: ByteArray = Signature.getInstance(algo).apply {
|
|
286
|
+
initSign(entry.privateKey)
|
|
287
|
+
update(data.toByteArray(Charsets.UTF_8))
|
|
288
|
+
}.sign()
|
|
289
|
+
promise.resolve(Base64.encodeToString(signature, Base64.NO_WRAP))
|
|
290
|
+
},
|
|
291
|
+
onError = { error ->
|
|
292
|
+
promise.reject("ERR_AUTH_FAILED", error, null)
|
|
293
|
+
},
|
|
294
|
+
title = title,
|
|
295
|
+
subtitle = subtitle,
|
|
296
|
+
authMethod = authMethod
|
|
297
|
+
)
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
Function("verify") { alias: String,
|
|
302
|
+
data: String,
|
|
303
|
+
signature: String,
|
|
304
|
+
o: Map<String, Any?> ->
|
|
305
|
+
val entry = getKeyStoreEntry(alias)
|
|
306
|
+
if (entry !is KeyStore.PrivateKeyEntry) return@Function null
|
|
307
|
+
|
|
308
|
+
val algoType = AlgorithmType.valueOf(o["algoType"] as String)
|
|
309
|
+
val algo = toAndroidAlgo(algoType)
|
|
310
|
+
|
|
311
|
+
val valid: Boolean = Signature.getInstance(algo).apply {
|
|
312
|
+
initVerify(entry.certificate)
|
|
313
|
+
update(data.toByteArray(Charsets.UTF_8))
|
|
314
|
+
}.verify(Base64.decode(signature, Base64.NO_WRAP))
|
|
315
|
+
return@Function true
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
AsyncFunction("encrypt") { alias: String,
|
|
320
|
+
data: String,
|
|
321
|
+
o: Map<String, Any?>,
|
|
322
|
+
promise: Promise ->
|
|
323
|
+
val entry = getKeyStoreEntry(alias)
|
|
324
|
+
if (entry !is KeyStore.PrivateKeyEntry) {
|
|
325
|
+
promise.resolve(null)
|
|
326
|
+
return@AsyncFunction
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
val algoType = AlgorithmType.valueOf(o["algoType"] as String)
|
|
330
|
+
val algo = toAndroidAlgo(algoType)
|
|
331
|
+
|
|
332
|
+
val cipher = Cipher.getInstance(algo)
|
|
333
|
+
cipher.init(Cipher.ENCRYPT_MODE, entry.certificate.publicKey)
|
|
334
|
+
val encrypted: ByteArray = cipher.doFinal(data.toByteArray(Charsets.UTF_8))
|
|
335
|
+
promise.resolve(Base64.encodeToString(encrypted, Base64.NO_WRAP))
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
AsyncFunction("decrypt") { alias: String,
|
|
339
|
+
data: String,
|
|
340
|
+
o: Map<String, Any?>,
|
|
341
|
+
promise: Promise ->
|
|
342
|
+
val entry = getKeyStoreEntry(alias)
|
|
343
|
+
if (entry !is KeyStore.PrivateKeyEntry) {
|
|
344
|
+
promise.resolve(null)
|
|
345
|
+
return@AsyncFunction
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
val reqAuth = isKeyStoreRequireAuthentication(entry)
|
|
349
|
+
|
|
350
|
+
val title = o["title"] as String
|
|
351
|
+
val subtitle = o["subtitle"] as String
|
|
352
|
+
val authMethod = o["authMethod"] as String
|
|
353
|
+
val algoType = AlgorithmType.valueOf(o["algoType"] as String)
|
|
354
|
+
val algo = toAndroidAlgo(algoType)
|
|
355
|
+
|
|
356
|
+
val cipher = Cipher.getInstance(algo)
|
|
357
|
+
val ciphertext = Base64.decode(data, Base64.NO_WRAP)
|
|
358
|
+
|
|
359
|
+
if (!reqAuth) {
|
|
360
|
+
cipher.init(Cipher.DECRYPT_MODE, entry.privateKey)
|
|
361
|
+
val decrypted: ByteArray = cipher.doFinal(ciphertext)
|
|
362
|
+
promise.resolve(String(decrypted, Charsets.UTF_8))
|
|
363
|
+
return@AsyncFunction
|
|
364
|
+
} else {
|
|
365
|
+
showAuthPrompt(
|
|
366
|
+
onSuccess = {
|
|
367
|
+
cipher.init(Cipher.DECRYPT_MODE, entry.privateKey)
|
|
368
|
+
val decrypted: ByteArray = cipher.doFinal(ciphertext)
|
|
369
|
+
promise.resolve(String(decrypted, Charsets.UTF_8))
|
|
370
|
+
},
|
|
371
|
+
onError = { error ->
|
|
372
|
+
promise.reject("ERR_AUTH_FAILED", error, null)
|
|
373
|
+
},
|
|
374
|
+
title = title,
|
|
375
|
+
subtitle = subtitle,
|
|
376
|
+
authMethod = when (authMethod) {
|
|
377
|
+
"PASSCODE" -> AuthMethod.PASSCODE
|
|
378
|
+
"PASSCODE_OR_BIOMETRIC" -> AuthMethod.PASSCODE_OR_BIOMETRIC
|
|
379
|
+
else -> AuthMethod.PASSCODE_OR_BIOMETRIC
|
|
380
|
+
}
|
|
381
|
+
)
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
export declare enum GenerateKeyPairResult {
|
|
2
|
+
KEY_PAIR_GENERATED = "KEY_PAIR_GENERATED",
|
|
3
|
+
KEY_PAIR_ALREADY_EXISTS = "KEY_PAIR_ALREADY_EXISTS",
|
|
4
|
+
NOT_AVAILABLE = "NOT_AVAILABLE"
|
|
5
|
+
}
|
|
6
|
+
export declare enum AuthCheckResult {
|
|
7
|
+
AVAILABLE = "AVAILABLE",
|
|
8
|
+
NO_HARDWARE = "NO_HARDWARE",
|
|
9
|
+
UNAVAILABLE = "UNAVAILABLE"
|
|
10
|
+
}
|
|
11
|
+
export declare enum AuthMethod {
|
|
12
|
+
PASSCODE = "PASSCODE",
|
|
13
|
+
PASSCODE_OR_BIOMETRIC = "PASSCODE_OR_BIOMETRIC"
|
|
14
|
+
}
|
|
15
|
+
export declare enum SigningAlgorithm {
|
|
16
|
+
ECDSA_SECP256R1_SHA256 = "ECDSA_SECP256R1_SHA256"
|
|
17
|
+
}
|
|
18
|
+
export declare enum EncryptionAlgorithm {
|
|
19
|
+
RSA_2048_PKCS1 = "RSA_2048_PKCS1"
|
|
20
|
+
}
|
|
21
|
+
export interface GenerateKeyPairOptions {
|
|
22
|
+
/**
|
|
23
|
+
* The algorithm type of the key to generate.
|
|
24
|
+
*/
|
|
25
|
+
algorithmType: SigningAlgorithm | EncryptionAlgorithm;
|
|
26
|
+
/**
|
|
27
|
+
* Whether to require authentication to sign.
|
|
28
|
+
* Setting this to true will prompt biometric or passcode authentication before signing.
|
|
29
|
+
*
|
|
30
|
+
* Please check the *`isAuthCheckAvailable`* function to see if authentication is available.
|
|
31
|
+
* If authentication is not available, generating a key pair will throw an error.
|
|
32
|
+
* @default false
|
|
33
|
+
*/
|
|
34
|
+
requireAuthentication?: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* The method of authentication to use.
|
|
37
|
+
* Note that on Android you have to define it when signing.
|
|
38
|
+
*
|
|
39
|
+
* If you want to allow to use Face ID, you need to add the following to your app.json config file:
|
|
40
|
+
* ```json
|
|
41
|
+
* "ios": {
|
|
42
|
+
* "infoPlist": {
|
|
43
|
+
* "NSFaceIDUsageDescription": "We use Face ID to secure your data.",
|
|
44
|
+
* }
|
|
45
|
+
* }
|
|
46
|
+
* ```
|
|
47
|
+
* @default AuthMethod.PASSCODE_OR_BIOMETRIC
|
|
48
|
+
* @platform ios
|
|
49
|
+
*/
|
|
50
|
+
authMethod?: AuthMethod;
|
|
51
|
+
}
|
|
52
|
+
interface BaseSignDecryptOptions {
|
|
53
|
+
/**
|
|
54
|
+
* The title of the prompt to show when authentication is required.
|
|
55
|
+
* @default "Unlock"
|
|
56
|
+
* @platform android
|
|
57
|
+
*/
|
|
58
|
+
promptTitle?: string;
|
|
59
|
+
/**
|
|
60
|
+
* The subtitle of the prompt to show when authentication is required.
|
|
61
|
+
* @default "Enter your PIN to continue"
|
|
62
|
+
* @platform android
|
|
63
|
+
*/
|
|
64
|
+
promptSubtitle?: string;
|
|
65
|
+
/**
|
|
66
|
+
* The method of authentication to use.
|
|
67
|
+
* Note that on iOS you have to define it when generating the key pair.
|
|
68
|
+
* @default AuthMethod.PASSCODE_OR_BIOMETRIC
|
|
69
|
+
* @platform android
|
|
70
|
+
*/
|
|
71
|
+
authMethod?: AuthMethod;
|
|
72
|
+
}
|
|
73
|
+
interface BaseSigningOptions {
|
|
74
|
+
/**
|
|
75
|
+
* The algorithm type of the key to use for signing.
|
|
76
|
+
*/
|
|
77
|
+
algorithmType?: SigningAlgorithm;
|
|
78
|
+
}
|
|
79
|
+
interface BaseEncryptionOptions {
|
|
80
|
+
/**
|
|
81
|
+
* The algorithm type of the key to use for encrypting.
|
|
82
|
+
*/
|
|
83
|
+
algorithmType?: EncryptionAlgorithm;
|
|
84
|
+
}
|
|
85
|
+
export interface SignOptions extends BaseSignDecryptOptions, BaseSigningOptions {
|
|
86
|
+
}
|
|
87
|
+
export interface VerifyOptions extends BaseSigningOptions {
|
|
88
|
+
}
|
|
89
|
+
export interface DecryptOptions extends BaseSignDecryptOptions, BaseEncryptionOptions {
|
|
90
|
+
}
|
|
91
|
+
export interface EncryptOptions extends BaseEncryptionOptions {
|
|
92
|
+
}
|
|
93
|
+
export interface GetPublicKeyOptions {
|
|
94
|
+
/**
|
|
95
|
+
* The format of the public key to return.
|
|
96
|
+
* @default "PEM" (Base64 of PEM-encoded SubjectPublicKeyInfo (SPKI) for P‑256)
|
|
97
|
+
*/
|
|
98
|
+
format?: "PEM";
|
|
99
|
+
}
|
|
100
|
+
export {};
|
|
101
|
+
//# sourceMappingURL=DeviceCrypto.types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DeviceCrypto.types.d.ts","sourceRoot":"","sources":["../src/DeviceCrypto.types.ts"],"names":[],"mappings":"AAAA,oBAAY,qBAAqB;IAC/B,kBAAkB,uBAAuB;IACzC,uBAAuB,4BAA4B;IACnD,aAAa,kBAAkB;CAChC;AAED,oBAAY,eAAe;IACzB,SAAS,cAAc;IACvB,WAAW,gBAAgB;IAC3B,WAAW,gBAAgB;CAC5B;AAED,oBAAY,UAAU;IACpB,QAAQ,aAAa;IACrB,qBAAqB,0BAA0B;CAChD;AAED,oBAAY,gBAAgB;IAC1B,sBAAsB,2BAA2B;CAClD;AAED,oBAAY,mBAAmB;IAC7B,cAAc,mBAAmB;CAClC;AAED,MAAM,WAAW,sBAAsB;IACrC;;OAEG;IACH,aAAa,EAAE,gBAAgB,GAAG,mBAAmB,CAAC;IACtD;;;;;;;OAOG;IACH,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC;;;;;;;;;;;;;;OAcG;IACH,UAAU,CAAC,EAAE,UAAU,CAAC;CACzB;AAED,UAAU,sBAAsB;IAC9B;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;;OAKG;IACH,UAAU,CAAC,EAAE,UAAU,CAAC;CACzB;AAED,UAAU,kBAAkB;IAC1B;;OAEG;IACH,aAAa,CAAC,EAAE,gBAAgB,CAAC;CAClC;AAED,UAAU,qBAAqB;IAC7B;;OAEG;IACH,aAAa,CAAC,EAAE,mBAAmB,CAAC;CACrC;AAED,MAAM,WAAW,WAAY,SAAQ,sBAAsB,EAAE,kBAAkB;CAAG;AAClF,MAAM,WAAW,aAAc,SAAQ,kBAAkB;CAAG;AAE5D,MAAM,WAAW,cAAe,SAAQ,sBAAsB,EAAE,qBAAqB;CAAG;AACxF,MAAM,WAAW,cAAe,SAAQ,qBAAqB;CAAG;AAEhE,MAAM,WAAW,mBAAmB;IAClC;;;OAGG;IACH,MAAM,CAAC,EAAE,KAAK,CAAC;CAChB"}
|