expo-secure-store 12.4.1 → 12.6.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/CHANGELOG.md +18 -0
- package/android/build.gradle +56 -34
- package/android/src/main/java/expo/modules/securestore/AuthenticationHelper.kt +48 -171
- package/android/src/main/java/expo/modules/securestore/AuthenticationPrompt.kt +44 -0
- package/android/src/main/java/expo/modules/securestore/SecureStoreExceptions.kt +13 -16
- package/android/src/main/java/expo/modules/securestore/SecureStoreModule.kt +352 -0
- package/android/src/main/java/expo/modules/securestore/SecureStoreOptions.kt +12 -0
- package/android/src/main/java/expo/modules/securestore/encryptors/AESEncryptor.kt +138 -0
- package/android/src/main/java/expo/modules/securestore/encryptors/HybridAESEncryptor.kt +107 -0
- package/android/src/main/java/expo/modules/securestore/encryptors/KeyBasedEncryptor.kt +38 -0
- package/build/SecureStore.d.ts +23 -4
- package/build/SecureStore.d.ts.map +1 -1
- package/build/SecureStore.js +42 -21
- package/build/SecureStore.js.map +1 -1
- package/expo-module.config.json +3 -0
- package/ios/SecureStoreModule.swift +1 -1
- package/package.json +2 -2
- package/src/SecureStore.ts +48 -22
- package/android/src/main/java/expo/modules/securestore/AuthenticationCallback.kt +0 -28
- package/android/src/main/java/expo/modules/securestore/EncryptionCallback.kt +0 -18
- package/android/src/main/java/expo/modules/securestore/PostEncryptionCallback.kt +0 -11
- package/android/src/main/java/expo/modules/securestore/SecureStoreModule.java +0 -692
- package/android/src/main/java/expo/modules/securestore/SecureStorePackage.java +0 -16
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,24 @@
|
|
|
10
10
|
|
|
11
11
|
### 💡 Others
|
|
12
12
|
|
|
13
|
+
## 12.6.0 — 2023-10-17
|
|
14
|
+
|
|
15
|
+
### 🛠 Breaking changes
|
|
16
|
+
|
|
17
|
+
- Dropped support for Android SDK 21 and 22. ([#24201](https://github.com/expo/expo/pull/24201) by [@behenate](https://github.com/behenate))
|
|
18
|
+
|
|
19
|
+
### 🐛 Bug fixes
|
|
20
|
+
|
|
21
|
+
- Fixed the 'WHEN_UNLOCKED_THIS_DEVICE_ONLY' constraint being incorrectly mapped to wrong secure store accessible ([#24831](https://github.com/expo/expo/pull/24831) by [@mmmguitar](https://github.com/mmmguitar))
|
|
22
|
+
|
|
23
|
+
## 12.5.0 — 2023-09-04
|
|
24
|
+
|
|
25
|
+
### 🎉 New features
|
|
26
|
+
|
|
27
|
+
- [Android] Migrated to Expo Modules API. ([#23804](https://github.com/expo/expo/pull/23804) by [@behenate](https://github.com/behenate))
|
|
28
|
+
- [Android] It is now possible to store values that require authentication and ones that don't under the same `keychainService`. ([#23804](https://github.com/expo/expo/pull/23804) by [@behenate](https://github.com/behenate))
|
|
29
|
+
- Added support for React Native 0.73. ([#24018](https://github.com/expo/expo/pull/24018) by [@kudo](https://github.com/kudo))
|
|
30
|
+
|
|
13
31
|
## 12.4.1 — 2023-08-02
|
|
14
32
|
|
|
15
33
|
_This version does not introduce any user-facing changes._
|
package/android/build.gradle
CHANGED
|
@@ -3,15 +3,20 @@ apply plugin: 'kotlin-android'
|
|
|
3
3
|
apply plugin: 'maven-publish'
|
|
4
4
|
|
|
5
5
|
group = 'host.exp.exponent'
|
|
6
|
-
version = '12.
|
|
6
|
+
version = '12.6.0'
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
|
|
9
|
+
if (expoModulesCorePlugin.exists()) {
|
|
10
|
+
apply from: expoModulesCorePlugin
|
|
11
|
+
applyKotlinExpoModulesCorePlugin()
|
|
12
|
+
// Remove this check, but keep the contents after SDK49 support is dropped
|
|
13
|
+
if (safeExtGet("expoProvidesDefaultConfig", false)) {
|
|
14
|
+
useExpoPublishing()
|
|
15
|
+
useCoreDependencies()
|
|
13
16
|
}
|
|
17
|
+
}
|
|
14
18
|
|
|
19
|
+
buildscript {
|
|
15
20
|
// Simple helper that allows the root project to override versions declared by this library.
|
|
16
21
|
ext.safeExtGet = { prop, fallback ->
|
|
17
22
|
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
|
|
@@ -35,54 +40,71 @@ buildscript {
|
|
|
35
40
|
}
|
|
36
41
|
}
|
|
37
42
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
+
// Remove this if and it's contents, when support for SDK49 is dropped
|
|
44
|
+
if (!safeExtGet("expoProvidesDefaultConfig", false)) {
|
|
45
|
+
afterEvaluate {
|
|
46
|
+
publishing {
|
|
47
|
+
publications {
|
|
48
|
+
release(MavenPublication) {
|
|
49
|
+
from components.release
|
|
50
|
+
}
|
|
43
51
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
52
|
+
repositories {
|
|
53
|
+
maven {
|
|
54
|
+
url = mavenLocal().url
|
|
55
|
+
}
|
|
48
56
|
}
|
|
49
57
|
}
|
|
50
58
|
}
|
|
51
59
|
}
|
|
52
60
|
|
|
53
61
|
android {
|
|
54
|
-
|
|
62
|
+
// Remove this if and it's contents, when support for SDK49 is dropped
|
|
63
|
+
if (!safeExtGet("expoProvidesDefaultConfig", false)) {
|
|
64
|
+
compileSdkVersion safeExtGet("compileSdkVersion", 33)
|
|
65
|
+
|
|
66
|
+
defaultConfig {
|
|
67
|
+
minSdkVersion safeExtGet("minSdkVersion", 23)
|
|
68
|
+
targetSdkVersion safeExtGet("targetSdkVersion", 33)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
publishing {
|
|
72
|
+
singleVariant("release") {
|
|
73
|
+
withSourcesJar()
|
|
74
|
+
}
|
|
75
|
+
}
|
|
55
76
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
77
|
+
lintOptions {
|
|
78
|
+
abortOnError false
|
|
79
|
+
}
|
|
59
80
|
}
|
|
60
81
|
|
|
61
|
-
|
|
62
|
-
|
|
82
|
+
def agpVersion = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION
|
|
83
|
+
if (agpVersion.tokenize('.')[0].toInteger() < 8) {
|
|
84
|
+
compileOptions {
|
|
85
|
+
sourceCompatibility JavaVersion.VERSION_11
|
|
86
|
+
targetCompatibility JavaVersion.VERSION_11
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
kotlinOptions {
|
|
90
|
+
jvmTarget = JavaVersion.VERSION_11.majorVersion
|
|
91
|
+
}
|
|
63
92
|
}
|
|
64
93
|
|
|
65
94
|
namespace "expo.modules.securestore"
|
|
66
95
|
defaultConfig {
|
|
67
|
-
minSdkVersion safeExtGet("minSdkVersion", 21)
|
|
68
|
-
targetSdkVersion safeExtGet("targetSdkVersion", 33)
|
|
69
96
|
versionCode 17
|
|
70
|
-
versionName '12.
|
|
71
|
-
}
|
|
72
|
-
lintOptions {
|
|
73
|
-
abortOnError false
|
|
74
|
-
}
|
|
75
|
-
publishing {
|
|
76
|
-
singleVariant("release") {
|
|
77
|
-
withSourcesJar()
|
|
78
|
-
}
|
|
97
|
+
versionName '12.6.0'
|
|
79
98
|
}
|
|
80
99
|
}
|
|
81
100
|
|
|
82
101
|
dependencies {
|
|
83
|
-
|
|
102
|
+
// Remove this if and it's contents, when support for SDK49 is dropped
|
|
103
|
+
if (!safeExtGet("expoProvidesDefaultConfig", false)) {
|
|
104
|
+
implementation project(':expo-modules-core')
|
|
105
|
+
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${getKotlinVersion()}"
|
|
106
|
+
}
|
|
84
107
|
|
|
85
108
|
api "androidx.biometric:biometric:1.1.0"
|
|
86
109
|
|
|
87
|
-
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${getKotlinVersion()}"
|
|
88
110
|
}
|
|
@@ -1,212 +1,89 @@
|
|
|
1
1
|
package expo.modules.securestore
|
|
2
2
|
|
|
3
|
+
import android.annotation.SuppressLint
|
|
3
4
|
import android.app.Activity
|
|
4
5
|
import android.content.Context
|
|
5
6
|
import android.os.Build
|
|
6
|
-
import android.util.Log
|
|
7
7
|
import androidx.biometric.BiometricManager
|
|
8
8
|
import androidx.biometric.BiometricPrompt
|
|
9
|
-
import androidx.biometric.BiometricPrompt.PromptInfo
|
|
10
|
-
import androidx.core.content.ContextCompat
|
|
11
9
|
import androidx.fragment.app.FragmentActivity
|
|
12
10
|
import expo.modules.core.ModuleRegistry
|
|
13
|
-
import expo.modules.core.Promise
|
|
14
|
-
import expo.modules.core.arguments.ReadableArguments
|
|
15
11
|
import expo.modules.core.interfaces.ActivityProvider
|
|
16
|
-
import
|
|
17
|
-
import
|
|
18
|
-
import org.json.JSONObject
|
|
19
|
-
import java.security.GeneralSecurityException
|
|
12
|
+
import kotlinx.coroutines.Dispatchers
|
|
13
|
+
import kotlinx.coroutines.withContext
|
|
20
14
|
import javax.crypto.Cipher
|
|
21
|
-
import javax.crypto.spec.GCMParameterSpec
|
|
22
15
|
|
|
23
16
|
class AuthenticationHelper(
|
|
24
17
|
private val context: Context,
|
|
25
18
|
private val moduleRegistry: ModuleRegistry
|
|
26
19
|
) {
|
|
27
|
-
companion object {
|
|
28
|
-
private const val AUTHENTICATION_PROMPT_PROPERTY = "authenticationPrompt"
|
|
29
|
-
const val REQUIRE_AUTHENTICATION_PROPERTY = "requireAuthentication"
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
private val uiManager = moduleRegistry.getModule(UIManager::class.java)
|
|
33
20
|
private var isAuthenticating = false
|
|
34
21
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
// requires authentication, the biometric prompt is shown, otherwise the encryption callback
|
|
40
|
-
// is called.
|
|
41
|
-
// When the user is authenticated the encryption callback is ran with the unlocked cipher and does
|
|
42
|
-
// encryption/decryption. Finally the PostEncryptionCallback may be ran with the object returned
|
|
43
|
-
// by previous callback (to save encrypted data to SharedPreferences).
|
|
44
|
-
|
|
45
|
-
val defaultCallback: AuthenticationCallback = object : AuthenticationCallback {
|
|
46
|
-
override fun checkAuthentication(
|
|
47
|
-
promise: Promise,
|
|
48
|
-
cipher: Cipher,
|
|
49
|
-
gcmParameterSpec: GCMParameterSpec,
|
|
50
|
-
options: ReadableArguments,
|
|
51
|
-
encryptionCallback: EncryptionCallback,
|
|
52
|
-
postEncryptionCallback: PostEncryptionCallback?
|
|
53
|
-
) {
|
|
54
|
-
val requiresAuthentication = options.getBoolean(REQUIRE_AUTHENTICATION_PROPERTY, false)
|
|
55
|
-
|
|
56
|
-
checkAuthentication(
|
|
57
|
-
promise, requiresAuthentication, cipher, gcmParameterSpec, options, encryptionCallback, postEncryptionCallback
|
|
58
|
-
)
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
override fun checkAuthentication(
|
|
62
|
-
promise: Promise,
|
|
63
|
-
requiresAuthentication: Boolean,
|
|
64
|
-
cipher: Cipher,
|
|
65
|
-
gcmParameterSpec: GCMParameterSpec,
|
|
66
|
-
options: ReadableArguments,
|
|
67
|
-
encryptionCallback: EncryptionCallback,
|
|
68
|
-
postEncryptionCallback: PostEncryptionCallback?
|
|
69
|
-
) {
|
|
70
|
-
if (requiresAuthentication) {
|
|
71
|
-
openAuthenticationPrompt(promise, options, encryptionCallback, cipher, gcmParameterSpec, postEncryptionCallback)
|
|
72
|
-
} else {
|
|
73
|
-
handleEncryptionCallback(promise, encryptionCallback, cipher, gcmParameterSpec, postEncryptionCallback)
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
fun handleEncryptionCallback(
|
|
79
|
-
promise: Promise,
|
|
80
|
-
encryptionCallback: EncryptionCallback,
|
|
81
|
-
cipher: Cipher,
|
|
82
|
-
gcmParameterSpec: GCMParameterSpec,
|
|
83
|
-
postEncryptionCallback: PostEncryptionCallback?
|
|
84
|
-
) {
|
|
85
|
-
try {
|
|
86
|
-
encryptionCallback.run(promise, cipher, gcmParameterSpec, postEncryptionCallback)
|
|
87
|
-
} catch (exception: GeneralSecurityException) {
|
|
88
|
-
Log.w(SecureStoreModule.TAG, exception)
|
|
89
|
-
promise.reject(
|
|
90
|
-
"ERR_SECURESTORE_ENCRYPT_FAILURE",
|
|
91
|
-
"Could not encrypt/decrypt the value for SecureStore",
|
|
92
|
-
exception
|
|
93
|
-
)
|
|
94
|
-
} catch (exception: JSONException) {
|
|
95
|
-
Log.w(SecureStoreModule.TAG, exception)
|
|
96
|
-
promise.reject(
|
|
97
|
-
"ERR_SECURESTORE_ENCODE_FAILURE",
|
|
98
|
-
"Could not create an encrypted JSON item for SecureStore",
|
|
99
|
-
exception
|
|
100
|
-
)
|
|
22
|
+
suspend fun authenticateCipher(cipher: Cipher, requiresAuthentication: Boolean, title: String): Cipher {
|
|
23
|
+
if (requiresAuthentication) {
|
|
24
|
+
return openAuthenticationPrompt(cipher, title).cryptoObject?.cipher
|
|
25
|
+
?: throw AuthenticationException("Couldn't get cipher from authentication result")
|
|
101
26
|
}
|
|
27
|
+
return cipher
|
|
102
28
|
}
|
|
103
29
|
|
|
104
|
-
private fun openAuthenticationPrompt(
|
|
105
|
-
promise: Promise,
|
|
106
|
-
options: ReadableArguments,
|
|
107
|
-
encryptionCallback: EncryptionCallback,
|
|
30
|
+
private suspend fun openAuthenticationPrompt(
|
|
108
31
|
cipher: Cipher,
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
) {
|
|
32
|
+
title: String
|
|
33
|
+
): BiometricPrompt.AuthenticationResult {
|
|
112
34
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
|
113
|
-
|
|
114
|
-
"ERR_SECURESTORE_AUTH_NOT_AVAILABLE",
|
|
115
|
-
"Biometric authentication requires Android API 23"
|
|
116
|
-
)
|
|
117
|
-
return
|
|
35
|
+
throw AuthenticationException("Biometric authentication requires Android API 23")
|
|
118
36
|
}
|
|
119
37
|
if (isAuthenticating) {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
38
|
+
throw AuthenticationException("Authentication is already in progress")
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
isAuthenticating = true
|
|
42
|
+
|
|
43
|
+
assertBiometricsSupport()
|
|
44
|
+
val fragmentActivity = getCurrentActivity() as? FragmentActivity
|
|
45
|
+
?: throw AuthenticationException("Cannot display biometric prompt when the app is not in the foreground")
|
|
46
|
+
|
|
47
|
+
val authenticationPrompt = AuthenticationPrompt(fragmentActivity, context, title)
|
|
48
|
+
|
|
49
|
+
return withContext(Dispatchers.Main.immediate) {
|
|
50
|
+
try {
|
|
51
|
+
return@withContext authenticationPrompt.authenticate(cipher)
|
|
52
|
+
?: throw AuthenticationException("Couldn't get the authentication result")
|
|
53
|
+
} finally {
|
|
54
|
+
isAuthenticating = false
|
|
55
|
+
}
|
|
125
56
|
}
|
|
57
|
+
}
|
|
126
58
|
|
|
59
|
+
fun assertBiometricsSupport() {
|
|
127
60
|
val biometricManager = BiometricManager.from(context)
|
|
61
|
+
@SuppressLint("SwitchIntDef") // BiometricManager.BIOMETRIC_SUCCESS shouldn't do anything
|
|
128
62
|
when (biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG)) {
|
|
129
63
|
BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE, BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> {
|
|
130
|
-
|
|
131
|
-
"ERR_SECURESTORE_AUTH_NOT_AVAILABLE",
|
|
132
|
-
"No hardware available for biometric authentication. Use expo-local-authentication to check if the device supports it."
|
|
133
|
-
)
|
|
134
|
-
return
|
|
64
|
+
throw AuthenticationException("No hardware available for biometric authentication. Use expo-local-authentication to check if the device supports it")
|
|
135
65
|
}
|
|
136
66
|
BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> {
|
|
137
|
-
|
|
138
|
-
"ERR_SECURESTORE_AUTH_NOT_CONFIGURED",
|
|
139
|
-
"No biometrics are currently enrolled"
|
|
140
|
-
)
|
|
141
|
-
return
|
|
67
|
+
throw AuthenticationException("No biometrics are currently enrolled")
|
|
142
68
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
.setTitle(title)
|
|
149
|
-
.setNegativeButtonText(context.getString(android.R.string.cancel))
|
|
150
|
-
.build()
|
|
151
|
-
val fragmentActivity = getCurrentActivity() as FragmentActivity?
|
|
152
|
-
if (fragmentActivity == null) {
|
|
153
|
-
promise.reject(
|
|
154
|
-
"ERR_SECURESTORE_APP_BACKGROUNDED",
|
|
155
|
-
"Cannot display biometric prompt when the app is not in the foreground"
|
|
156
|
-
)
|
|
157
|
-
return
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
uiManager.runOnUiQueueThread(
|
|
161
|
-
Runnable {
|
|
162
|
-
isAuthenticating = true
|
|
163
|
-
|
|
164
|
-
BiometricPrompt(
|
|
165
|
-
fragmentActivity,
|
|
166
|
-
ContextCompat.getMainExecutor(context),
|
|
167
|
-
object : BiometricPrompt.AuthenticationCallback() {
|
|
168
|
-
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
|
|
169
|
-
super.onAuthenticationSucceeded(result)
|
|
170
|
-
isAuthenticating = false
|
|
171
|
-
|
|
172
|
-
val cipher = result.cryptoObject!!.cipher!!
|
|
173
|
-
handleEncryptionCallback(
|
|
174
|
-
promise,
|
|
175
|
-
encryptionCallback,
|
|
176
|
-
cipher,
|
|
177
|
-
gcmParameterSpec,
|
|
178
|
-
{ promise, result ->
|
|
179
|
-
val obj = result as JSONObject
|
|
180
|
-
obj.put(REQUIRE_AUTHENTICATION_PROPERTY, true)
|
|
181
|
-
postEncryptionCallback?.run(promise, result)
|
|
182
|
-
}
|
|
183
|
-
)
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
|
|
187
|
-
super.onAuthenticationError(errorCode, errString)
|
|
188
|
-
isAuthenticating = false
|
|
189
|
-
|
|
190
|
-
if (errorCode == BiometricPrompt.ERROR_USER_CANCELED || errorCode == BiometricPrompt.ERROR_NEGATIVE_BUTTON) {
|
|
191
|
-
promise.reject(
|
|
192
|
-
"ERR_SECURESTORE_AUTH_CANCELLED",
|
|
193
|
-
"User canceled the authentication"
|
|
194
|
-
)
|
|
195
|
-
} else {
|
|
196
|
-
promise.reject(
|
|
197
|
-
"ERR_SECURESTORE_AUTH_FAILURE",
|
|
198
|
-
"Could not authenticate the user"
|
|
199
|
-
)
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
).authenticate(promptInfo, BiometricPrompt.CryptoObject(cipher))
|
|
69
|
+
BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED -> {
|
|
70
|
+
throw AuthenticationException("An update is required before the biometrics can be used")
|
|
71
|
+
}
|
|
72
|
+
BiometricManager.BIOMETRIC_ERROR_UNSUPPORTED -> {
|
|
73
|
+
throw AuthenticationException("Biometric authentication is unsupported")
|
|
204
74
|
}
|
|
205
|
-
|
|
75
|
+
BiometricManager.BIOMETRIC_STATUS_UNKNOWN -> {
|
|
76
|
+
throw AuthenticationException("Biometric authentication status is unknown")
|
|
77
|
+
}
|
|
78
|
+
}
|
|
206
79
|
}
|
|
207
80
|
|
|
208
81
|
private fun getCurrentActivity(): Activity? {
|
|
209
82
|
val activityProvider: ActivityProvider = moduleRegistry.getModule(ActivityProvider::class.java)
|
|
210
83
|
return activityProvider.currentActivity
|
|
211
84
|
}
|
|
85
|
+
|
|
86
|
+
companion object {
|
|
87
|
+
const val REQUIRE_AUTHENTICATION_PROPERTY = "requireAuthentication"
|
|
88
|
+
}
|
|
212
89
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
package expo.modules.securestore
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import androidx.biometric.BiometricPrompt
|
|
5
|
+
import androidx.biometric.BiometricPrompt.PromptInfo
|
|
6
|
+
import androidx.core.content.ContextCompat
|
|
7
|
+
import androidx.fragment.app.FragmentActivity
|
|
8
|
+
import java.util.concurrent.Executor
|
|
9
|
+
import javax.crypto.Cipher
|
|
10
|
+
import kotlin.coroutines.resume
|
|
11
|
+
import kotlin.coroutines.resumeWithException
|
|
12
|
+
import kotlin.coroutines.suspendCoroutine
|
|
13
|
+
|
|
14
|
+
class AuthenticationPrompt(private val currentActivity: FragmentActivity, context: Context, title: String) {
|
|
15
|
+
private var executor: Executor = ContextCompat.getMainExecutor(context)
|
|
16
|
+
private var promptInfo = PromptInfo.Builder()
|
|
17
|
+
.setTitle(title)
|
|
18
|
+
.setNegativeButtonText(context.getString(android.R.string.cancel))
|
|
19
|
+
.build()
|
|
20
|
+
|
|
21
|
+
suspend fun authenticate(cipher: Cipher): BiometricPrompt.AuthenticationResult? =
|
|
22
|
+
suspendCoroutine { continuation ->
|
|
23
|
+
BiometricPrompt(
|
|
24
|
+
currentActivity,
|
|
25
|
+
executor,
|
|
26
|
+
object : BiometricPrompt.AuthenticationCallback() {
|
|
27
|
+
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
|
|
28
|
+
super.onAuthenticationError(errorCode, errString)
|
|
29
|
+
|
|
30
|
+
if (errorCode == BiometricPrompt.ERROR_USER_CANCELED || errorCode == BiometricPrompt.ERROR_NEGATIVE_BUTTON) {
|
|
31
|
+
continuation.resumeWithException(AuthenticationException("User canceled the authentication"))
|
|
32
|
+
} else {
|
|
33
|
+
continuation.resumeWithException(AuthenticationException("Could not authenticate the user"))
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
|
|
38
|
+
super.onAuthenticationSucceeded(result)
|
|
39
|
+
continuation.resume(result)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
).authenticate(promptInfo, BiometricPrompt.CryptoObject(cipher))
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -1,27 +1,24 @@
|
|
|
1
1
|
package expo.modules.securestore
|
|
2
2
|
|
|
3
|
-
import expo.modules.
|
|
3
|
+
import expo.modules.kotlin.exception.CodedException
|
|
4
4
|
|
|
5
5
|
internal class NullKeyException :
|
|
6
6
|
CodedException("SecureStore keys must not be null")
|
|
7
7
|
|
|
8
|
-
internal class WriteException(message: String?, cause: Throwable?) :
|
|
9
|
-
CodedException(
|
|
8
|
+
internal class WriteException(message: String?, key: String, keychain: String, cause: Throwable? = null) :
|
|
9
|
+
CodedException("An error occurred when writing to key: '$key' under keychain: '$keychain'. Caused by: ${message ?: "unknown"}", cause)
|
|
10
10
|
|
|
11
|
-
internal class
|
|
12
|
-
CodedException("
|
|
11
|
+
internal class EncryptException(message: String?, key: String, keychain: String, cause: Throwable? = null) :
|
|
12
|
+
CodedException("Could not encrypt the value for key '$key' under keychain '$keychain'. Caused by: ${message ?: "unknown"}", cause)
|
|
13
13
|
|
|
14
|
-
internal class
|
|
15
|
-
CodedException("
|
|
14
|
+
internal class DecryptException(message: String?, key: String, keychain: String, cause: Throwable? = null) :
|
|
15
|
+
CodedException("Could not decrypt the value for key '$key' under keychain '$keychain'. Caused by: ${message ?: "unknown"}", cause)
|
|
16
16
|
|
|
17
|
-
internal class
|
|
18
|
-
CodedException(
|
|
17
|
+
internal class DeleteException(message: String?, key: String, keychain: String, cause: Throwable? = null) :
|
|
18
|
+
CodedException("Could not delete the value for key '$key' under keychain '$keychain'. Caused by: ${message ?: "unknown"}", cause)
|
|
19
19
|
|
|
20
|
-
internal class
|
|
21
|
-
CodedException(
|
|
20
|
+
internal class AuthenticationException(message: String?, cause: Throwable? = null) :
|
|
21
|
+
CodedException("Could not Authenticate the user: ${message ?: "unknown"}", cause)
|
|
22
22
|
|
|
23
|
-
internal class
|
|
24
|
-
CodedException(message
|
|
25
|
-
|
|
26
|
-
internal class DeleteException(message: String?, cause: Throwable?) :
|
|
27
|
-
CodedException(message ?: "An unexpected error occurred when deleting from SecureStore", cause)
|
|
23
|
+
internal class KeyStoreException(message: String?) :
|
|
24
|
+
CodedException("An error occurred when accessing the keystore: ${message ?: "unknown"}")
|