expo-secure-store 11.0.3 → 11.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/CHANGELOG.md +5 -5
- package/README.md +1 -1
- package/android/build.gradle +4 -2
- package/android/src/main/java/expo/modules/securestore/AuthenticationCallback.kt +28 -0
- package/android/src/main/java/expo/modules/securestore/AuthenticationHelper.kt +212 -0
- package/android/src/main/java/expo/modules/securestore/EncryptionCallback.kt +18 -0
- package/android/src/main/java/expo/modules/securestore/PostEncryptionCallback.kt +11 -0
- package/android/src/main/java/expo/modules/securestore/SecureStoreModule.java +89 -48
- package/build/SecureStore.d.ts +14 -0
- package/build/SecureStore.js.map +1 -1
- package/ios/EXSecureStore/EXSecureStore.m +20 -2
- package/package.json +6 -5
- package/src/SecureStore.ts +14 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
### 🎉 New features
|
|
4
|
+
|
|
5
|
+
- Added `requireAuthentication` and `authenticationPrompt` parameters to `SecureStoreOptions` options object used in `SecureStore.{deleteItemAsync, getItemAsync, setItemAsync}` methods to enable user authentication while accessing Secure Store. ([#14512](https://github.com/expo/expo/pull/14512) by [@j-piasecki](https://github.com/j-piasecki))
|
|
6
|
+
|
|
3
7
|
## Unpublished
|
|
4
8
|
|
|
5
9
|
### 🛠 Breaking changes
|
|
@@ -10,11 +14,7 @@
|
|
|
10
14
|
|
|
11
15
|
### 💡 Others
|
|
12
16
|
|
|
13
|
-
## 11.0
|
|
14
|
-
|
|
15
|
-
_This version does not introduce any user-facing changes._
|
|
16
|
-
|
|
17
|
-
## 11.0.2 — 2021-10-15
|
|
17
|
+
## 11.1.0 — 2021-12-03
|
|
18
18
|
|
|
19
19
|
_This version does not introduce any user-facing changes._
|
|
20
20
|
|
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ For managed [managed](https://docs.expo.io/versions/latest/introduction/managed-
|
|
|
13
13
|
|
|
14
14
|
# Installation in bare React Native projects
|
|
15
15
|
|
|
16
|
-
For bare React Native projects, you must ensure that you have [installed and configured the `
|
|
16
|
+
For bare React Native projects, you must ensure that you have [installed and configured the `expo` package](https://docs.expo.dev/bare/installing-expo-modules/) before continuing.
|
|
17
17
|
|
|
18
18
|
### Add the package to your npm dependencies
|
|
19
19
|
|
package/android/build.gradle
CHANGED
|
@@ -3,7 +3,7 @@ apply plugin: 'kotlin-android'
|
|
|
3
3
|
apply plugin: 'maven'
|
|
4
4
|
|
|
5
5
|
group = 'host.exp.exponent'
|
|
6
|
-
version = '11.0
|
|
6
|
+
version = '11.1.0'
|
|
7
7
|
|
|
8
8
|
buildscript {
|
|
9
9
|
// Simple helper that allows the root project to override versions declared by this library.
|
|
@@ -57,7 +57,7 @@ android {
|
|
|
57
57
|
minSdkVersion safeExtGet("minSdkVersion", 21)
|
|
58
58
|
targetSdkVersion safeExtGet("targetSdkVersion", 30)
|
|
59
59
|
versionCode 17
|
|
60
|
-
versionName '11.0
|
|
60
|
+
versionName '11.1.0'
|
|
61
61
|
}
|
|
62
62
|
lintOptions {
|
|
63
63
|
abortOnError false
|
|
@@ -67,5 +67,7 @@ android {
|
|
|
67
67
|
dependencies {
|
|
68
68
|
implementation project(':expo-modules-core')
|
|
69
69
|
|
|
70
|
+
api "androidx.biometric:biometric:1.1.0"
|
|
71
|
+
|
|
70
72
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${safeExtGet('kotlinVersion', '1.4.21')}"
|
|
71
73
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
package expo.modules.securestore
|
|
2
|
+
|
|
3
|
+
import expo.modules.core.Promise
|
|
4
|
+
import expo.modules.core.arguments.ReadableArguments
|
|
5
|
+
import javax.crypto.Cipher
|
|
6
|
+
import javax.crypto.spec.GCMParameterSpec
|
|
7
|
+
|
|
8
|
+
// Interface used to pass the authentication logic
|
|
9
|
+
interface AuthenticationCallback {
|
|
10
|
+
fun checkAuthentication(
|
|
11
|
+
promise: Promise,
|
|
12
|
+
cipher: Cipher,
|
|
13
|
+
gcmParameterSpec: GCMParameterSpec,
|
|
14
|
+
options: ReadableArguments,
|
|
15
|
+
encryptionCallback: EncryptionCallback,
|
|
16
|
+
postEncryptionCallback: PostEncryptionCallback?
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
fun checkAuthentication(
|
|
20
|
+
promise: Promise,
|
|
21
|
+
requiresAuthentication: Boolean,
|
|
22
|
+
cipher: Cipher,
|
|
23
|
+
gcmParameterSpec: GCMParameterSpec,
|
|
24
|
+
options: ReadableArguments,
|
|
25
|
+
encryptionCallback: EncryptionCallback,
|
|
26
|
+
postEncryptionCallback: PostEncryptionCallback?
|
|
27
|
+
)
|
|
28
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
package expo.modules.securestore
|
|
2
|
+
|
|
3
|
+
import android.app.Activity
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import android.os.Build
|
|
6
|
+
import android.util.Log
|
|
7
|
+
import androidx.biometric.BiometricManager
|
|
8
|
+
import androidx.biometric.BiometricPrompt
|
|
9
|
+
import androidx.biometric.BiometricPrompt.PromptInfo
|
|
10
|
+
import androidx.core.content.ContextCompat
|
|
11
|
+
import androidx.fragment.app.FragmentActivity
|
|
12
|
+
import expo.modules.core.ModuleRegistry
|
|
13
|
+
import expo.modules.core.Promise
|
|
14
|
+
import expo.modules.core.arguments.ReadableArguments
|
|
15
|
+
import expo.modules.core.interfaces.ActivityProvider
|
|
16
|
+
import expo.modules.core.interfaces.services.UIManager
|
|
17
|
+
import org.json.JSONException
|
|
18
|
+
import org.json.JSONObject
|
|
19
|
+
import java.security.GeneralSecurityException
|
|
20
|
+
import javax.crypto.Cipher
|
|
21
|
+
import javax.crypto.spec.GCMParameterSpec
|
|
22
|
+
|
|
23
|
+
class AuthenticationHelper(
|
|
24
|
+
private val context: Context,
|
|
25
|
+
private val moduleRegistry: ModuleRegistry
|
|
26
|
+
) {
|
|
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
|
+
private var isAuthenticating = false
|
|
34
|
+
|
|
35
|
+
// Authentication callback decides whether the operation requires authentication (either by
|
|
36
|
+
// requiresAuthentication argument, or from options). When item needs to be encrypted/decrypted an
|
|
37
|
+
// instance of Authentication callback is passed to the relevant method.
|
|
38
|
+
// The method prepares the cipher and starts authentication callback with it. If the operation
|
|
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
|
+
)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private fun openAuthenticationPrompt(
|
|
105
|
+
promise: Promise,
|
|
106
|
+
options: ReadableArguments,
|
|
107
|
+
encryptionCallback: EncryptionCallback,
|
|
108
|
+
cipher: Cipher,
|
|
109
|
+
gcmParameterSpec: GCMParameterSpec,
|
|
110
|
+
postEncryptionCallback: PostEncryptionCallback?
|
|
111
|
+
) {
|
|
112
|
+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
|
113
|
+
promise.reject(
|
|
114
|
+
"ERR_SECURESTORE_AUTH_NOT_AVAILABLE",
|
|
115
|
+
"Biometric authentication requires Android API 23"
|
|
116
|
+
)
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
if (isAuthenticating) {
|
|
120
|
+
promise.reject(
|
|
121
|
+
"ERR_SECURESTORE_AUTH_IN_PROGRESS",
|
|
122
|
+
"Authentication is already in progress"
|
|
123
|
+
)
|
|
124
|
+
return
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
val biometricManager = BiometricManager.from(context)
|
|
128
|
+
when (biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG)) {
|
|
129
|
+
BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE, BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> {
|
|
130
|
+
promise.reject(
|
|
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
|
|
135
|
+
}
|
|
136
|
+
BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> {
|
|
137
|
+
promise.reject(
|
|
138
|
+
"ERR_SECURESTORE_AUTH_NOT_CONFIGURED",
|
|
139
|
+
"No biometrics are currently enrolled"
|
|
140
|
+
)
|
|
141
|
+
return
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
val title = options.getString(AUTHENTICATION_PROMPT_PROPERTY, " ")
|
|
146
|
+
|
|
147
|
+
val promptInfo = PromptInfo.Builder()
|
|
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))
|
|
204
|
+
}
|
|
205
|
+
)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
private fun getCurrentActivity(): Activity? {
|
|
209
|
+
val activityProvider: ActivityProvider = moduleRegistry.getModule(ActivityProvider::class.java)
|
|
210
|
+
return activityProvider.currentActivity
|
|
211
|
+
}
|
|
212
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
package expo.modules.securestore
|
|
2
|
+
|
|
3
|
+
import expo.modules.core.Promise
|
|
4
|
+
import org.json.JSONException
|
|
5
|
+
import java.security.GeneralSecurityException
|
|
6
|
+
import javax.crypto.Cipher
|
|
7
|
+
import javax.crypto.spec.GCMParameterSpec
|
|
8
|
+
|
|
9
|
+
// Interface used to pass encryption/decryption logic
|
|
10
|
+
fun interface EncryptionCallback {
|
|
11
|
+
@Throws(GeneralSecurityException::class, JSONException::class)
|
|
12
|
+
fun run(
|
|
13
|
+
promise: Promise,
|
|
14
|
+
cipher: Cipher,
|
|
15
|
+
gcmParameterSpec: GCMParameterSpec,
|
|
16
|
+
postEncryptionCallback: PostEncryptionCallback?
|
|
17
|
+
): Any
|
|
18
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
package expo.modules.securestore
|
|
2
|
+
|
|
3
|
+
import expo.modules.core.Promise
|
|
4
|
+
import org.json.JSONException
|
|
5
|
+
import java.security.GeneralSecurityException
|
|
6
|
+
|
|
7
|
+
// Interface used to pass logic that needs to happen after encryption/decryption
|
|
8
|
+
fun interface PostEncryptionCallback {
|
|
9
|
+
@Throws(JSONException::class, GeneralSecurityException::class)
|
|
10
|
+
fun run(promise: Promise, result: Any)
|
|
11
|
+
}
|
|
@@ -16,6 +16,7 @@ import android.util.Log;
|
|
|
16
16
|
import org.json.JSONException;
|
|
17
17
|
import org.json.JSONObject;
|
|
18
18
|
import expo.modules.core.ExportedModule;
|
|
19
|
+
import expo.modules.core.ModuleRegistry;
|
|
19
20
|
import expo.modules.core.Promise;
|
|
20
21
|
import expo.modules.core.arguments.ReadableArguments;
|
|
21
22
|
import expo.modules.core.interfaces.ExpoMethod;
|
|
@@ -46,7 +47,7 @@ import javax.crypto.spec.SecretKeySpec;
|
|
|
46
47
|
import javax.security.auth.x500.X500Principal;
|
|
47
48
|
|
|
48
49
|
public class SecureStoreModule extends ExportedModule {
|
|
49
|
-
|
|
50
|
+
static final String TAG = "ExpoSecureStore";
|
|
50
51
|
private static final String SHARED_PREFERENCES_NAME = "SecureStore";
|
|
51
52
|
private static final String KEYSTORE_PROVIDER = "AndroidKeyStore";
|
|
52
53
|
|
|
@@ -57,6 +58,7 @@ public class SecureStoreModule extends ExportedModule {
|
|
|
57
58
|
private KeyStore mKeyStore;
|
|
58
59
|
private AESEncrypter mAESEncrypter;
|
|
59
60
|
private HybridAESEncrypter mHybridAESEncrypter;
|
|
61
|
+
private AuthenticationHelper mAuthenticationHelper;
|
|
60
62
|
|
|
61
63
|
public SecureStoreModule(Context context) {
|
|
62
64
|
super(context);
|
|
@@ -64,6 +66,11 @@ public class SecureStoreModule extends ExportedModule {
|
|
|
64
66
|
mHybridAESEncrypter = new HybridAESEncrypter(context, mAESEncrypter);
|
|
65
67
|
}
|
|
66
68
|
|
|
69
|
+
@Override
|
|
70
|
+
public void onCreate(ModuleRegistry moduleRegistry) {
|
|
71
|
+
mAuthenticationHelper = new AuthenticationHelper(getContext(), moduleRegistry);
|
|
72
|
+
}
|
|
73
|
+
|
|
67
74
|
@Override
|
|
68
75
|
public String getName() {
|
|
69
76
|
return TAG;
|
|
@@ -81,7 +88,6 @@ public class SecureStoreModule extends ExportedModule {
|
|
|
81
88
|
}
|
|
82
89
|
}
|
|
83
90
|
|
|
84
|
-
@SuppressWarnings("ConstantConditions")
|
|
85
91
|
private void setItemImpl(String key, String value, ReadableArguments options, Promise promise) {
|
|
86
92
|
if (key == null) {
|
|
87
93
|
promise.reject("E_SECURESTORE_NULL_KEY", "SecureStore keys must not be null");
|
|
@@ -100,7 +106,6 @@ public class SecureStoreModule extends ExportedModule {
|
|
|
100
106
|
return;
|
|
101
107
|
}
|
|
102
108
|
|
|
103
|
-
JSONObject encryptedItem;
|
|
104
109
|
try {
|
|
105
110
|
KeyStore keyStore = getKeyStore();
|
|
106
111
|
|
|
@@ -110,12 +115,18 @@ public class SecureStoreModule extends ExportedModule {
|
|
|
110
115
|
// back a value.
|
|
111
116
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
112
117
|
KeyStore.SecretKeyEntry secretKeyEntry = getKeyEntry(KeyStore.SecretKeyEntry.class, mAESEncrypter, options);
|
|
113
|
-
|
|
114
|
-
|
|
118
|
+
mAESEncrypter.createEncryptedItem(promise, value, keyStore, secretKeyEntry, options, mAuthenticationHelper.getDefaultCallback(), (innerPromise, result) -> {
|
|
119
|
+
JSONObject obj = (JSONObject) result;
|
|
120
|
+
obj.put(SCHEME_PROPERTY, AESEncrypter.NAME);
|
|
121
|
+
saveEncryptedItem(innerPromise, obj, prefs, key);
|
|
122
|
+
});
|
|
115
123
|
} else {
|
|
116
124
|
KeyStore.PrivateKeyEntry privateKeyEntry = getKeyEntry(KeyStore.PrivateKeyEntry.class, mHybridAESEncrypter, options);
|
|
117
|
-
|
|
118
|
-
|
|
125
|
+
mHybridAESEncrypter.createEncryptedItem(promise, value, keyStore, privateKeyEntry, options, mAuthenticationHelper.getDefaultCallback(), (innerPromise, result) -> {
|
|
126
|
+
JSONObject obj = (JSONObject) result;
|
|
127
|
+
obj.put(SCHEME_PROPERTY, HybridAESEncrypter.NAME);
|
|
128
|
+
saveEncryptedItem(innerPromise, obj, prefs, key);
|
|
129
|
+
});
|
|
119
130
|
}
|
|
120
131
|
} catch (IOException e) {
|
|
121
132
|
Log.w(TAG, e);
|
|
@@ -130,7 +141,9 @@ public class SecureStoreModule extends ExportedModule {
|
|
|
130
141
|
promise.reject("E_SECURESTORE_ENCODE_ERROR", "Could not create an encrypted JSON item for SecureStore", e);
|
|
131
142
|
return;
|
|
132
143
|
}
|
|
144
|
+
}
|
|
133
145
|
|
|
146
|
+
private void saveEncryptedItem(Promise promise, JSONObject encryptedItem, SharedPreferences prefs, String key) {
|
|
134
147
|
String encryptedItemString = encryptedItem.toString();
|
|
135
148
|
if (encryptedItemString == null) { // lint warning suppressed, JSONObject#toString() may return null
|
|
136
149
|
promise.reject("E_SECURESTORE_JSON_ERROR", "Could not JSON-encode the encrypted item for SecureStore");
|
|
@@ -185,16 +198,15 @@ public class SecureStoreModule extends ExportedModule {
|
|
|
185
198
|
return;
|
|
186
199
|
}
|
|
187
200
|
|
|
188
|
-
String value;
|
|
189
201
|
try {
|
|
190
202
|
switch (scheme) {
|
|
191
203
|
case AESEncrypter.NAME:
|
|
192
204
|
KeyStore.SecretKeyEntry secretKeyEntry = getKeyEntry(KeyStore.SecretKeyEntry.class, mAESEncrypter, options);
|
|
193
|
-
|
|
205
|
+
mAESEncrypter.decryptItem(promise, encryptedItem, secretKeyEntry, options, mAuthenticationHelper.getDefaultCallback());
|
|
194
206
|
break;
|
|
195
207
|
case HybridAESEncrypter.NAME:
|
|
196
208
|
KeyStore.PrivateKeyEntry privateKeyEntry = getKeyEntry(KeyStore.PrivateKeyEntry.class, mHybridAESEncrypter, options);
|
|
197
|
-
|
|
209
|
+
mHybridAESEncrypter.decryptItem(promise, encryptedItem, privateKeyEntry, options, mAuthenticationHelper.getDefaultCallback());
|
|
198
210
|
break;
|
|
199
211
|
default:
|
|
200
212
|
String message = String.format("The item for key \"%s\" in SecureStore has an unknown encoding scheme (%s)", key, scheme);
|
|
@@ -215,8 +227,6 @@ public class SecureStoreModule extends ExportedModule {
|
|
|
215
227
|
promise.reject("E_SECURESTORE_DECODE_ERROR", "Could not decode the encrypted JSON item in SecureStore", e);
|
|
216
228
|
return;
|
|
217
229
|
}
|
|
218
|
-
|
|
219
|
-
promise.resolve(value);
|
|
220
230
|
}
|
|
221
231
|
|
|
222
232
|
private void readLegacySDK20Item(String key, ReadableArguments options, Promise promise) {
|
|
@@ -339,11 +349,12 @@ public class SecureStoreModule extends ExportedModule {
|
|
|
339
349
|
GeneralSecurityException;
|
|
340
350
|
|
|
341
351
|
@SuppressWarnings("unused")
|
|
342
|
-
|
|
352
|
+
void createEncryptedItem(Promise promise, String plaintextValue, KeyStore keyStore, E keyStoreEntry, ReadableArguments options,
|
|
353
|
+
AuthenticationCallback authenticationCallback, PostEncryptionCallback postEncryptionCallback) throws
|
|
343
354
|
GeneralSecurityException, JSONException;
|
|
344
355
|
|
|
345
356
|
@SuppressWarnings("unused")
|
|
346
|
-
|
|
357
|
+
void decryptItem(Promise promise, JSONObject encryptedItem, E keyStoreEntry, ReadableArguments options, AuthenticationCallback callback) throws
|
|
347
358
|
GeneralSecurityException, JSONException;
|
|
348
359
|
}
|
|
349
360
|
|
|
@@ -382,6 +393,7 @@ public class SecureStoreModule extends ExportedModule {
|
|
|
382
393
|
.setKeySize(AES_KEY_SIZE_BITS)
|
|
383
394
|
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
|
|
384
395
|
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
|
|
396
|
+
.setUserAuthenticationRequired(options.getBoolean(AuthenticationHelper.REQUIRE_AUTHENTICATION_PROPERTY, false))
|
|
385
397
|
.build();
|
|
386
398
|
|
|
387
399
|
KeyGenerator keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, keyStore.getProvider());
|
|
@@ -398,18 +410,23 @@ public class SecureStoreModule extends ExportedModule {
|
|
|
398
410
|
}
|
|
399
411
|
|
|
400
412
|
@Override
|
|
401
|
-
public
|
|
402
|
-
|
|
413
|
+
public void createEncryptedItem(Promise promise, String plaintextValue, KeyStore keyStore, KeyStore.SecretKeyEntry secretKeyEntry,
|
|
414
|
+
ReadableArguments options, AuthenticationCallback authenticationCallback, PostEncryptionCallback postEncryptionCallback) throws
|
|
415
|
+
GeneralSecurityException {
|
|
403
416
|
|
|
404
417
|
SecretKey secretKey = secretKeyEntry.getSecretKey();
|
|
405
418
|
Cipher cipher = Cipher.getInstance(AES_CIPHER);
|
|
406
419
|
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
|
|
407
420
|
GCMParameterSpec gcmSpec = cipher.getParameters().getParameterSpec(GCMParameterSpec.class);
|
|
408
421
|
|
|
409
|
-
|
|
422
|
+
authenticationCallback.checkAuthentication(promise, cipher, gcmSpec, options,
|
|
423
|
+
(promise1, cipher1, gcmParameterSpec, postEncryptionCallback1) ->
|
|
424
|
+
createEncryptedItem(promise1, plaintextValue, cipher1, gcmSpec, postEncryptionCallback1), postEncryptionCallback
|
|
425
|
+
);
|
|
410
426
|
}
|
|
411
427
|
|
|
412
|
-
/* package */ JSONObject createEncryptedItem(String plaintextValue, Cipher cipher,
|
|
428
|
+
/* package */ JSONObject createEncryptedItem(Promise promise, String plaintextValue, Cipher cipher,
|
|
429
|
+
GCMParameterSpec gcmSpec, PostEncryptionCallback postEncryptionCallback) throws
|
|
413
430
|
GeneralSecurityException, JSONException {
|
|
414
431
|
|
|
415
432
|
byte[] plaintextBytes = plaintextValue.getBytes(StandardCharsets.UTF_8);
|
|
@@ -419,14 +436,19 @@ public class SecureStoreModule extends ExportedModule {
|
|
|
419
436
|
String ivString = Base64.encodeToString(gcmSpec.getIV(), Base64.NO_WRAP);
|
|
420
437
|
int authenticationTagLength = gcmSpec.getTLen();
|
|
421
438
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
439
|
+
JSONObject result = new JSONObject()
|
|
440
|
+
.put(CIPHERTEXT_PROPERTY, ciphertext)
|
|
441
|
+
.put(IV_PROPERTY, ivString)
|
|
442
|
+
.put(GCM_AUTHENTICATION_TAG_LENGTH_PROPERTY, authenticationTagLength);
|
|
443
|
+
|
|
444
|
+
postEncryptionCallback.run(promise, result);
|
|
445
|
+
|
|
446
|
+
return result;
|
|
426
447
|
}
|
|
427
448
|
|
|
428
449
|
@Override
|
|
429
|
-
public
|
|
450
|
+
public void decryptItem(Promise promise, JSONObject encryptedItem, KeyStore.SecretKeyEntry secretKeyEntry, ReadableArguments options,
|
|
451
|
+
AuthenticationCallback callback) throws
|
|
430
452
|
GeneralSecurityException, JSONException {
|
|
431
453
|
|
|
432
454
|
String ciphertext = encryptedItem.getString(CIPHERTEXT_PROPERTY);
|
|
@@ -438,9 +460,15 @@ public class SecureStoreModule extends ExportedModule {
|
|
|
438
460
|
GCMParameterSpec gcmSpec = new GCMParameterSpec(authenticationTagLength, ivBytes);
|
|
439
461
|
Cipher cipher = Cipher.getInstance(AES_CIPHER);
|
|
440
462
|
cipher.init(Cipher.DECRYPT_MODE, secretKeyEntry.getSecretKey(), gcmSpec);
|
|
441
|
-
byte[] plaintextBytes = cipher.doFinal(ciphertextBytes);
|
|
442
463
|
|
|
443
|
-
|
|
464
|
+
callback.checkAuthentication(promise, encryptedItem.optBoolean(AuthenticationHelper.REQUIRE_AUTHENTICATION_PROPERTY), cipher, gcmSpec, options,
|
|
465
|
+
(promise1, cipher1, gcmParameterSpec, postEncryptionCallback) -> {
|
|
466
|
+
String result = new String(cipher1.doFinal(ciphertextBytes), StandardCharsets.UTF_8);
|
|
467
|
+
promise1.resolve(result);
|
|
468
|
+
return result;
|
|
469
|
+
},
|
|
470
|
+
null
|
|
471
|
+
);
|
|
444
472
|
}
|
|
445
473
|
}
|
|
446
474
|
|
|
@@ -515,7 +543,8 @@ public class SecureStoreModule extends ExportedModule {
|
|
|
515
543
|
}
|
|
516
544
|
|
|
517
545
|
@Override
|
|
518
|
-
public
|
|
546
|
+
public void createEncryptedItem(Promise promise, String plaintextValue, KeyStore keyStore, KeyStore.PrivateKeyEntry privateKeyEntry,
|
|
547
|
+
ReadableArguments options, AuthenticationCallback authenticationCallback, PostEncryptionCallback postEncryptionCallback) throws
|
|
519
548
|
GeneralSecurityException, JSONException {
|
|
520
549
|
|
|
521
550
|
// Generate the IV and symmetric key with which we encrypt the value
|
|
@@ -547,29 +576,41 @@ public class SecureStoreModule extends ExportedModule {
|
|
|
547
576
|
chosenSpec = gcmSpec;
|
|
548
577
|
}
|
|
549
578
|
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
579
|
+
authenticationCallback.checkAuthentication(promise, aesCipher, chosenSpec, options, new EncryptionCallback() {
|
|
580
|
+
@Override
|
|
581
|
+
public Object run(Promise promise, Cipher cipher, GCMParameterSpec gcmParameterSpec, PostEncryptionCallback postEncryptionCallback) throws
|
|
582
|
+
GeneralSecurityException, JSONException {
|
|
583
|
+
return mAESEncrypter.createEncryptedItem(promise, plaintextValue, cipher, gcmSpec, postEncryptionCallback);
|
|
584
|
+
}
|
|
585
|
+
}, new PostEncryptionCallback() {
|
|
586
|
+
@Override
|
|
587
|
+
public void run(Promise promise, Object result) throws JSONException, GeneralSecurityException {
|
|
588
|
+
JSONObject encryptedItem = (JSONObject) result;
|
|
589
|
+
|
|
590
|
+
// Ensure the IV in the encrypted item matches our generated IV
|
|
591
|
+
String ivString = encryptedItem.getString(AESEncrypter.IV_PROPERTY);
|
|
592
|
+
String expectedIVString = Base64.encodeToString(ivBytes, Base64.NO_WRAP);
|
|
593
|
+
if (!ivString.equals(expectedIVString)) {
|
|
594
|
+
Log.e(TAG, String.format("HybridAESEncrypter generated two different IVs: %s and %s", expectedIVString, ivString));
|
|
595
|
+
throw new IllegalStateException("HybridAESEncrypter must store the same IV as the one used to parameterize the secret key");
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// Encrypt the symmetric key with the asymmetric public key
|
|
599
|
+
byte[] secretKeyBytes = secretKey.getEncoded();
|
|
600
|
+
Cipher cipher = getRSACipher();
|
|
601
|
+
cipher.init(Cipher.ENCRYPT_MODE, privateKeyEntry.getCertificate());
|
|
602
|
+
byte[] encryptedSecretKeyBytes = cipher.doFinal(secretKeyBytes);
|
|
603
|
+
String encryptedSecretKeyString = Base64.encodeToString(encryptedSecretKeyBytes, Base64.NO_WRAP);
|
|
604
|
+
|
|
605
|
+
encryptedItem.put(ENCRYPTED_SECRET_KEY_PROPERTY, encryptedSecretKeyString);
|
|
606
|
+
|
|
607
|
+
postEncryptionCallback.run(promise, encryptedItem);
|
|
608
|
+
}
|
|
609
|
+
});
|
|
569
610
|
}
|
|
570
611
|
|
|
571
612
|
@Override
|
|
572
|
-
public
|
|
613
|
+
public void decryptItem(Promise promise, JSONObject encryptedItem, KeyStore.PrivateKeyEntry privateKeyEntry, ReadableArguments options, AuthenticationCallback callback) throws
|
|
573
614
|
GeneralSecurityException, JSONException {
|
|
574
615
|
|
|
575
616
|
// Decrypt the encrypted symmetric key
|
|
@@ -584,7 +625,8 @@ public class SecureStoreModule extends ExportedModule {
|
|
|
584
625
|
|
|
585
626
|
// Decrypt the value with the symmetric key
|
|
586
627
|
KeyStore.SecretKeyEntry secretKeyEntry = new KeyStore.SecretKeyEntry(secretKey);
|
|
587
|
-
|
|
628
|
+
|
|
629
|
+
mAESEncrypter.decryptItem(promise, encryptedItem, secretKeyEntry, options, callback);
|
|
588
630
|
}
|
|
589
631
|
|
|
590
632
|
private Cipher getRSACipher() throws NoSuchAlgorithmException, NoSuchProviderException, NoSuchPaddingException {
|
|
@@ -592,7 +634,6 @@ public class SecureStoreModule extends ExportedModule {
|
|
|
592
634
|
? Cipher.getInstance(RSA_CIPHER, RSA_CIPHER_LEGACY_PROVIDER)
|
|
593
635
|
: Cipher.getInstance(RSA_CIPHER);
|
|
594
636
|
}
|
|
595
|
-
|
|
596
637
|
}
|
|
597
638
|
|
|
598
639
|
/**
|
package/build/SecureStore.d.ts
CHANGED
|
@@ -40,6 +40,20 @@ export declare type SecureStoreOptions = {
|
|
|
40
40
|
* > If the item is set with the `keychainService` option, it will be required to later fetch the value.
|
|
41
41
|
*/
|
|
42
42
|
keychainService?: string;
|
|
43
|
+
/**
|
|
44
|
+
* Option responsible for enabling the usage of the user authentication methods available on the device while
|
|
45
|
+
* accessing data stored in SecureStore.
|
|
46
|
+
*
|
|
47
|
+
* - iOS: Equivalent to `kSecAccessControlUserPresence`
|
|
48
|
+
* - Android: Equivalent to `setUserAuthenticationRequired(true)` (requires API 23). Complete functionality
|
|
49
|
+
* is unlocked only with a freshly generated key - this would not work in tandem with the `keychainService`
|
|
50
|
+
* value used for the others non-authenticated operations.
|
|
51
|
+
*/
|
|
52
|
+
requireAuthentication?: boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Custom message displayed to the user while `requireAuthentication` option is turned on.
|
|
55
|
+
*/
|
|
56
|
+
authenticationPrompt?: string;
|
|
43
57
|
/**
|
|
44
58
|
* __(iOS only)__ Specifies when the stored entry is accessible, using iOS's `kSecAttrAccessible`
|
|
45
59
|
* property. See Apple's documentation on [keychain item accessibility](https://developer.apple.com/library/content/documentation/Security/Conceptual/keychainServConcepts/02concepts/concepts.html#//apple_ref/doc/uid/TP30000897-CH204-SW18).
|
package/build/SecureStore.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SecureStore.js","sourceRoot":"","sources":["../src/SecureStore.ts"],"names":[],"mappings":"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","sourcesContent":["import { UnavailabilityError } from 'expo-modules-core';\n\nimport ExpoSecureStore from './ExpoSecureStore';\n\nexport type KeychainAccessibilityConstant = number;\n\n// @needsAudit\n/**\n * The data in the keychain item cannot be accessed after a restart until the device has been\n * unlocked once by the user. This may be useful if you need to access the item when the phone\n * is locked.\n */\nexport const AFTER_FIRST_UNLOCK: KeychainAccessibilityConstant = ExpoSecureStore.AFTER_FIRST_UNLOCK;\n\n// @needsAudit\n/**\n * Similar to `AFTER_FIRST_UNLOCK`, except the entry is not migrated to a new device when restoring\n * from a backup.\n */\nexport const AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY: KeychainAccessibilityConstant =\n ExpoSecureStore.AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY;\n\n// @needsAudit\n/**\n * The data in the keychain item can always be accessed regardless of whether the device is locked.\n * This is the least secure option.\n */\nexport const ALWAYS: KeychainAccessibilityConstant = ExpoSecureStore.ALWAYS;\n\n// @needsAudit\n/**\n * Similar to `WHEN_UNLOCKED_THIS_DEVICE_ONLY`, except the user must have set a passcode in order to\n * store an entry. If the user removes their passcode, the entry will be deleted.\n */\nexport const WHEN_PASSCODE_SET_THIS_DEVICE_ONLY: KeychainAccessibilityConstant =\n ExpoSecureStore.WHEN_PASSCODE_SET_THIS_DEVICE_ONLY;\n\n// @needsAudit\n/**\n * Similar to `ALWAYS`, except the entry is not migrated to a new device when restoring from a backup.\n */\nexport const ALWAYS_THIS_DEVICE_ONLY: KeychainAccessibilityConstant =\n ExpoSecureStore.ALWAYS_THIS_DEVICE_ONLY;\n\n// @needsAudit\n/**\n * The data in the keychain item can be accessed only while the device is unlocked by the user.\n */\nexport const WHEN_UNLOCKED: KeychainAccessibilityConstant = ExpoSecureStore.WHEN_UNLOCKED;\n\n// @needsAudit\n/**\n * Similar to `WHEN_UNLOCKED`, except the entry is not migrated to a new device when restoring from\n * a backup.\n */\nexport const WHEN_UNLOCKED_THIS_DEVICE_ONLY: KeychainAccessibilityConstant =\n ExpoSecureStore.WHEN_UNLOCKED_THIS_DEVICE_ONLY;\n\nconst VALUE_BYTES_LIMIT = 2048;\n\n// @needsAudit\nexport type SecureStoreOptions = {\n /**\n * - iOS: The item's service, equivalent to `kSecAttrService`\n * - Android: Equivalent of the public/private key pair `Alias`\n * > If the item is set with the `keychainService` option, it will be required to later fetch the value.\n */\n keychainService?: string;\n /**\n * __(iOS only)__ Specifies when the stored entry is accessible, using iOS's `kSecAttrAccessible`\n * property. See Apple's documentation on [keychain item accessibility](https://developer.apple.com/library/content/documentation/Security/Conceptual/keychainServConcepts/02concepts/concepts.html#//apple_ref/doc/uid/TP30000897-CH204-SW18).\n * Default value: `SecureStore.WHEN_UNLOCKED`.\n */\n keychainAccessible?: KeychainAccessibilityConstant;\n};\n\n// @needsAudit\n/**\n * Returns whether the SecureStore API is enabled on the current device. This does not check the app\n * permissions.\n *\n * @return Promise which fulfils witch `boolean`, indicating whether the SecureStore API is available\n * on the current device. Currently this resolves `true` on iOS and Android only.\n */\nexport async function isAvailableAsync(): Promise<boolean> {\n return !!ExpoSecureStore.getValueWithKeyAsync;\n}\n\n// @needsAudit\n/**\n * Delete the value associated with the provided key.\n *\n * @param key The key that was used to store the associated value.\n * @param options An [`SecureStoreOptions`](#securestoreoptions) object.\n *\n * @return A promise that will reject if the value couldn't be deleted.\n */\nexport async function deleteItemAsync(\n key: string,\n options: SecureStoreOptions = {}\n): Promise<void> {\n _ensureValidKey(key);\n\n if (!ExpoSecureStore.deleteValueWithKeyAsync) {\n throw new UnavailabilityError('SecureStore', 'deleteItemAsync');\n }\n await ExpoSecureStore.deleteValueWithKeyAsync(key, options);\n}\n\n// @needsAudit\n/**\n * Fetch the stored value associated with the provided key.\n *\n * @param key The key that was used to store the associated value.\n * @param options An [`SecureStoreOptions`](#securestoreoptions) object.\n *\n * @return A promise that resolves to the previously stored value, or `null` if there is no entry\n * for the given key. The promise will reject if an error occurred while retrieving the value.\n */\nexport async function getItemAsync(\n key: string,\n options: SecureStoreOptions = {}\n): Promise<string | null> {\n _ensureValidKey(key);\n return await ExpoSecureStore.getValueWithKeyAsync(key, options);\n}\n\n// @needsAudit\n/**\n * Store a key–value pair.\n *\n * @param key The key to associate with the stored value. Keys may contain alphanumeric characters\n * `.`, `-`, and `_`.\n * @param value The value to store. Size limit is 2048 bytes.\n * @param options An [`SecureStoreOptions`](#securestoreoptions) object.\n *\n * @return A promise that will reject if value cannot be stored on the device.\n */\nexport async function setItemAsync(\n key: string,\n value: string,\n options: SecureStoreOptions = {}\n): Promise<void> {\n _ensureValidKey(key);\n if (!_isValidValue(value)) {\n throw new Error(\n `Invalid value provided to SecureStore. Values must be strings; consider JSON-encoding your values if they are serializable.`\n );\n }\n if (!ExpoSecureStore.setValueWithKeyAsync) {\n throw new UnavailabilityError('SecureStore', 'setItemAsync');\n }\n await ExpoSecureStore.setValueWithKeyAsync(value, key, options);\n}\n\nfunction _ensureValidKey(key: string) {\n if (!_isValidKey(key)) {\n throw new Error(\n `Invalid key provided to SecureStore. Keys must not be empty and contain only alphanumeric characters, \".\", \"-\", and \"_\".`\n );\n }\n}\n\nfunction _isValidKey(key: string) {\n return typeof key === 'string' && /^[\\w.-]+$/.test(key);\n}\n\nfunction _isValidValue(value: string) {\n if (typeof value !== 'string') {\n return false;\n }\n if (_byteCount(value) > VALUE_BYTES_LIMIT) {\n console.warn(\n 'Provided value to SecureStore is larger than 2048 bytes. An attempt to store such a value will throw an error in SDK 35.'\n );\n }\n return true;\n}\n\n// copy-pasted from https://stackoverflow.com/a/39488643\nfunction _byteCount(value: string) {\n let bytes = 0;\n\n for (let i = 0; i < value.length; i++) {\n const codePoint = value.charCodeAt(i);\n\n // Lone surrogates cannot be passed to encodeURI\n if (codePoint >= 0xd800 && codePoint < 0xe000) {\n if (codePoint < 0xdc00 && i + 1 < value.length) {\n const next = value.charCodeAt(i + 1);\n\n if (next >= 0xdc00 && next < 0xe000) {\n bytes += 4;\n i++;\n continue;\n }\n }\n }\n\n bytes += codePoint < 0x80 ? 1 : codePoint < 0x800 ? 2 : 3;\n }\n\n return bytes;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"SecureStore.js","sourceRoot":"","sources":["../src/SecureStore.ts"],"names":[],"mappings":"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","sourcesContent":["import { UnavailabilityError } from 'expo-modules-core';\n\nimport ExpoSecureStore from './ExpoSecureStore';\n\nexport type KeychainAccessibilityConstant = number;\n\n// @needsAudit\n/**\n * The data in the keychain item cannot be accessed after a restart until the device has been\n * unlocked once by the user. This may be useful if you need to access the item when the phone\n * is locked.\n */\nexport const AFTER_FIRST_UNLOCK: KeychainAccessibilityConstant = ExpoSecureStore.AFTER_FIRST_UNLOCK;\n\n// @needsAudit\n/**\n * Similar to `AFTER_FIRST_UNLOCK`, except the entry is not migrated to a new device when restoring\n * from a backup.\n */\nexport const AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY: KeychainAccessibilityConstant =\n ExpoSecureStore.AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY;\n\n// @needsAudit\n/**\n * The data in the keychain item can always be accessed regardless of whether the device is locked.\n * This is the least secure option.\n */\nexport const ALWAYS: KeychainAccessibilityConstant = ExpoSecureStore.ALWAYS;\n\n// @needsAudit\n/**\n * Similar to `WHEN_UNLOCKED_THIS_DEVICE_ONLY`, except the user must have set a passcode in order to\n * store an entry. If the user removes their passcode, the entry will be deleted.\n */\nexport const WHEN_PASSCODE_SET_THIS_DEVICE_ONLY: KeychainAccessibilityConstant =\n ExpoSecureStore.WHEN_PASSCODE_SET_THIS_DEVICE_ONLY;\n\n// @needsAudit\n/**\n * Similar to `ALWAYS`, except the entry is not migrated to a new device when restoring from a backup.\n */\nexport const ALWAYS_THIS_DEVICE_ONLY: KeychainAccessibilityConstant =\n ExpoSecureStore.ALWAYS_THIS_DEVICE_ONLY;\n\n// @needsAudit\n/**\n * The data in the keychain item can be accessed only while the device is unlocked by the user.\n */\nexport const WHEN_UNLOCKED: KeychainAccessibilityConstant = ExpoSecureStore.WHEN_UNLOCKED;\n\n// @needsAudit\n/**\n * Similar to `WHEN_UNLOCKED`, except the entry is not migrated to a new device when restoring from\n * a backup.\n */\nexport const WHEN_UNLOCKED_THIS_DEVICE_ONLY: KeychainAccessibilityConstant =\n ExpoSecureStore.WHEN_UNLOCKED_THIS_DEVICE_ONLY;\n\nconst VALUE_BYTES_LIMIT = 2048;\n\n// @needsAudit\nexport type SecureStoreOptions = {\n /**\n * - iOS: The item's service, equivalent to `kSecAttrService`\n * - Android: Equivalent of the public/private key pair `Alias`\n * > If the item is set with the `keychainService` option, it will be required to later fetch the value.\n */\n keychainService?: string;\n /**\n * Option responsible for enabling the usage of the user authentication methods available on the device while\n * accessing data stored in SecureStore.\n *\n * - iOS: Equivalent to `kSecAccessControlUserPresence`\n * - Android: Equivalent to `setUserAuthenticationRequired(true)` (requires API 23). Complete functionality\n * is unlocked only with a freshly generated key - this would not work in tandem with the `keychainService`\n * value used for the others non-authenticated operations.\n */\n requireAuthentication?: boolean;\n /**\n * Custom message displayed to the user while `requireAuthentication` option is turned on.\n */\n authenticationPrompt?: string;\n /**\n * __(iOS only)__ Specifies when the stored entry is accessible, using iOS's `kSecAttrAccessible`\n * property. See Apple's documentation on [keychain item accessibility](https://developer.apple.com/library/content/documentation/Security/Conceptual/keychainServConcepts/02concepts/concepts.html#//apple_ref/doc/uid/TP30000897-CH204-SW18).\n * Default value: `SecureStore.WHEN_UNLOCKED`.\n */\n keychainAccessible?: KeychainAccessibilityConstant;\n};\n\n// @needsAudit\n/**\n * Returns whether the SecureStore API is enabled on the current device. This does not check the app\n * permissions.\n *\n * @return Promise which fulfils witch `boolean`, indicating whether the SecureStore API is available\n * on the current device. Currently this resolves `true` on iOS and Android only.\n */\nexport async function isAvailableAsync(): Promise<boolean> {\n return !!ExpoSecureStore.getValueWithKeyAsync;\n}\n\n// @needsAudit\n/**\n * Delete the value associated with the provided key.\n *\n * @param key The key that was used to store the associated value.\n * @param options An [`SecureStoreOptions`](#securestoreoptions) object.\n *\n * @return A promise that will reject if the value couldn't be deleted.\n */\nexport async function deleteItemAsync(\n key: string,\n options: SecureStoreOptions = {}\n): Promise<void> {\n _ensureValidKey(key);\n\n if (!ExpoSecureStore.deleteValueWithKeyAsync) {\n throw new UnavailabilityError('SecureStore', 'deleteItemAsync');\n }\n await ExpoSecureStore.deleteValueWithKeyAsync(key, options);\n}\n\n// @needsAudit\n/**\n * Fetch the stored value associated with the provided key.\n *\n * @param key The key that was used to store the associated value.\n * @param options An [`SecureStoreOptions`](#securestoreoptions) object.\n *\n * @return A promise that resolves to the previously stored value, or `null` if there is no entry\n * for the given key. The promise will reject if an error occurred while retrieving the value.\n */\nexport async function getItemAsync(\n key: string,\n options: SecureStoreOptions = {}\n): Promise<string | null> {\n _ensureValidKey(key);\n return await ExpoSecureStore.getValueWithKeyAsync(key, options);\n}\n\n// @needsAudit\n/**\n * Store a key–value pair.\n *\n * @param key The key to associate with the stored value. Keys may contain alphanumeric characters\n * `.`, `-`, and `_`.\n * @param value The value to store. Size limit is 2048 bytes.\n * @param options An [`SecureStoreOptions`](#securestoreoptions) object.\n *\n * @return A promise that will reject if value cannot be stored on the device.\n */\nexport async function setItemAsync(\n key: string,\n value: string,\n options: SecureStoreOptions = {}\n): Promise<void> {\n _ensureValidKey(key);\n if (!_isValidValue(value)) {\n throw new Error(\n `Invalid value provided to SecureStore. Values must be strings; consider JSON-encoding your values if they are serializable.`\n );\n }\n if (!ExpoSecureStore.setValueWithKeyAsync) {\n throw new UnavailabilityError('SecureStore', 'setItemAsync');\n }\n await ExpoSecureStore.setValueWithKeyAsync(value, key, options);\n}\n\nfunction _ensureValidKey(key: string) {\n if (!_isValidKey(key)) {\n throw new Error(\n `Invalid key provided to SecureStore. Keys must not be empty and contain only alphanumeric characters, \".\", \"-\", and \"_\".`\n );\n }\n}\n\nfunction _isValidKey(key: string) {\n return typeof key === 'string' && /^[\\w.-]+$/.test(key);\n}\n\nfunction _isValidValue(value: string) {\n if (typeof value !== 'string') {\n return false;\n }\n if (_byteCount(value) > VALUE_BYTES_LIMIT) {\n console.warn(\n 'Provided value to SecureStore is larger than 2048 bytes. An attempt to store such a value will throw an error in SDK 35.'\n );\n }\n return true;\n}\n\n// copy-pasted from https://stackoverflow.com/a/39488643\nfunction _byteCount(value: string) {\n let bytes = 0;\n\n for (let i = 0; i < value.length; i++) {\n const codePoint = value.charCodeAt(i);\n\n // Lone surrogates cannot be passed to encodeURI\n if (codePoint >= 0xd800 && codePoint < 0xe000) {\n if (codePoint < 0xdc00 && i + 1 < value.length) {\n const next = value.charCodeAt(i + 1);\n\n if (next >= 0xdc00 && next < 0xe000) {\n bytes += 4;\n i++;\n continue;\n }\n }\n }\n\n bytes += codePoint < 0x80 ? 1 : codePoint < 0x800 ? 2 : 3;\n }\n\n return bytes;\n}\n"]}
|
|
@@ -28,11 +28,19 @@
|
|
|
28
28
|
NSMutableDictionary *dictionary = [self _queryWithKey:key
|
|
29
29
|
withOptions:options];
|
|
30
30
|
|
|
31
|
+
NSString *requireAuth = options[@"requireAuthentication"];
|
|
32
|
+
|
|
31
33
|
NSData *valueData = [value dataUsingEncoding:NSUTF8StringEncoding];
|
|
32
34
|
[dictionary setObject:valueData forKey:(__bridge id)kSecValueData];
|
|
33
35
|
|
|
34
36
|
CFStringRef accessibility = [self _accessibilityAttributeWithOptions:options];
|
|
35
|
-
|
|
37
|
+
|
|
38
|
+
if (![requireAuth boolValue]) {
|
|
39
|
+
[dictionary setObject:(__bridge id)accessibility forKey:(__bridge id)kSecAttrAccessible];
|
|
40
|
+
} else {
|
|
41
|
+
SecAccessControlRef accessOptions = SecAccessControlCreateWithFlags(nil, accessibility, kSecAccessControlUserPresence, nil);
|
|
42
|
+
[dictionary setObject:(__bridge_transfer id)accessOptions forKey:(__bridge id)kSecAttrAccessControl];
|
|
43
|
+
}
|
|
36
44
|
|
|
37
45
|
OSStatus status = SecItemAdd((__bridge CFDictionaryRef)dictionary, NULL);
|
|
38
46
|
|
|
@@ -74,6 +82,11 @@
|
|
|
74
82
|
withOptions:options];
|
|
75
83
|
NSData *valueData = [value dataUsingEncoding:NSUTF8StringEncoding];
|
|
76
84
|
NSDictionary *updateDictionary = @{(__bridge id)kSecValueData:valueData};
|
|
85
|
+
|
|
86
|
+
if ((NSString *) options[@"authenticationPrompt"]) {
|
|
87
|
+
NSString *promptText = options[@"authenticationPrompt"];
|
|
88
|
+
[searchDictionary setObject:promptText forKey:(__bridge id)kSecUseOperationPrompt];
|
|
89
|
+
}
|
|
77
90
|
|
|
78
91
|
OSStatus status = SecItemUpdate((__bridge CFDictionaryRef)searchDictionary,
|
|
79
92
|
(__bridge CFDictionaryRef)updateDictionary);
|
|
@@ -93,6 +106,11 @@
|
|
|
93
106
|
|
|
94
107
|
[searchDictionary setObject:(__bridge id)kSecMatchLimitOne forKey:(__bridge id)kSecMatchLimit];
|
|
95
108
|
[searchDictionary setObject:(__bridge id)kCFBooleanTrue forKey:(__bridge id)kSecReturnData];
|
|
109
|
+
|
|
110
|
+
if ((NSString *) options[@"authenticationPrompt"]) {
|
|
111
|
+
NSString *promptText = options[@"authenticationPrompt"];
|
|
112
|
+
[searchDictionary setObject:promptText forKey:(__bridge id)kSecUseOperationPrompt];
|
|
113
|
+
}
|
|
96
114
|
|
|
97
115
|
CFTypeRef foundDict = NULL;
|
|
98
116
|
OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)searchDictionary, &foundDict);
|
|
@@ -178,7 +196,7 @@
|
|
|
178
196
|
return @"Unable to decode the provided data.";
|
|
179
197
|
|
|
180
198
|
case errSecAuthFailed:
|
|
181
|
-
return @"
|
|
199
|
+
return @"Authentication failed. Provided passphrase/PIN is incorrect or there is no user authentication method configured for this device.";
|
|
182
200
|
|
|
183
201
|
default:
|
|
184
202
|
return error.localizedDescription;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "expo-secure-store",
|
|
3
|
-
"version": "11.0
|
|
3
|
+
"version": "11.1.0",
|
|
4
4
|
"description": "Provides a way to encrypt and securely store key–value pairs locally on the device.",
|
|
5
5
|
"main": "build/SecureStore.js",
|
|
6
6
|
"types": "build/SecureStore.d.ts",
|
|
@@ -35,11 +35,12 @@
|
|
|
35
35
|
"jest": {
|
|
36
36
|
"preset": "expo-module-scripts"
|
|
37
37
|
},
|
|
38
|
-
"dependencies": {
|
|
39
|
-
"expo-modules-core": "~0.4.4"
|
|
40
|
-
},
|
|
38
|
+
"dependencies": {},
|
|
41
39
|
"devDependencies": {
|
|
42
40
|
"expo-module-scripts": "^2.0.0"
|
|
43
41
|
},
|
|
44
|
-
"
|
|
42
|
+
"peerDependencies": {
|
|
43
|
+
"expo": "*"
|
|
44
|
+
},
|
|
45
|
+
"gitHead": "2e5c6983b86d5ecfca028ba64002897d8adc2cc4"
|
|
45
46
|
}
|
package/src/SecureStore.ts
CHANGED
|
@@ -66,6 +66,20 @@ export type SecureStoreOptions = {
|
|
|
66
66
|
* > If the item is set with the `keychainService` option, it will be required to later fetch the value.
|
|
67
67
|
*/
|
|
68
68
|
keychainService?: string;
|
|
69
|
+
/**
|
|
70
|
+
* Option responsible for enabling the usage of the user authentication methods available on the device while
|
|
71
|
+
* accessing data stored in SecureStore.
|
|
72
|
+
*
|
|
73
|
+
* - iOS: Equivalent to `kSecAccessControlUserPresence`
|
|
74
|
+
* - Android: Equivalent to `setUserAuthenticationRequired(true)` (requires API 23). Complete functionality
|
|
75
|
+
* is unlocked only with a freshly generated key - this would not work in tandem with the `keychainService`
|
|
76
|
+
* value used for the others non-authenticated operations.
|
|
77
|
+
*/
|
|
78
|
+
requireAuthentication?: boolean;
|
|
79
|
+
/**
|
|
80
|
+
* Custom message displayed to the user while `requireAuthentication` option is turned on.
|
|
81
|
+
*/
|
|
82
|
+
authenticationPrompt?: string;
|
|
69
83
|
/**
|
|
70
84
|
* __(iOS only)__ Specifies when the stored entry is accessible, using iOS's `kSecAttrAccessible`
|
|
71
85
|
* property. See Apple's documentation on [keychain item accessibility](https://developer.apple.com/library/content/documentation/Security/Conceptual/keychainServConcepts/02concepts/concepts.html#//apple_ref/doc/uid/TP30000897-CH204-SW18).
|