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 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.3 — 2021-10-21
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 `react-native-unimodules` package](https://github.com/expo/expo/tree/master/packages/react-native-unimodules) before continuing.
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
 
@@ -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.3'
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.3'
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
- private static final String TAG = "ExpoSecureStore";
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
- encryptedItem = mAESEncrypter.createEncryptedItem(value, keyStore, secretKeyEntry);
114
- encryptedItem.put(SCHEME_PROPERTY, AESEncrypter.NAME);
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
- encryptedItem = mHybridAESEncrypter.createEncryptedItem(value, keyStore, privateKeyEntry);
118
- encryptedItem.put(SCHEME_PROPERTY, HybridAESEncrypter.NAME);
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
- value = mAESEncrypter.decryptItem(encryptedItem, secretKeyEntry);
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
- value = mHybridAESEncrypter.decryptItem(encryptedItem, privateKeyEntry);
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
- JSONObject createEncryptedItem(String plaintextValue, KeyStore keyStore, E keyStoreEntry) throws
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
- String decryptItem(JSONObject encryptedItem, E keyStoreEntry) throws
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 JSONObject createEncryptedItem(String plaintextValue, KeyStore keyStore, KeyStore.SecretKeyEntry secretKeyEntry) throws
402
- GeneralSecurityException, JSONException {
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
- return createEncryptedItem(plaintextValue, cipher, gcmSpec);
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, GCMParameterSpec gcmSpec) throws
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
- return new JSONObject()
423
- .put(CIPHERTEXT_PROPERTY, ciphertext)
424
- .put(IV_PROPERTY, ivString)
425
- .put(GCM_AUTHENTICATION_TAG_LENGTH_PROPERTY, authenticationTagLength);
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 String decryptItem(JSONObject encryptedItem, KeyStore.SecretKeyEntry secretKeyEntry) throws
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
- return new String(plaintextBytes, StandardCharsets.UTF_8);
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 JSONObject createEncryptedItem(String plaintextValue, KeyStore keyStore, KeyStore.PrivateKeyEntry privateKeyEntry) throws
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
- JSONObject encryptedItem = mAESEncrypter.createEncryptedItem(plaintextValue, aesCipher, chosenSpec);
551
-
552
- // Ensure the IV in the encrypted item matches our generated IV
553
- String ivString = encryptedItem.getString(AESEncrypter.IV_PROPERTY);
554
- String expectedIVString = Base64.encodeToString(ivBytes, Base64.NO_WRAP);
555
- if (!ivString.equals(expectedIVString)) {
556
- Log.e(TAG, String.format("HybridAESEncrypter generated two different IVs: %s and %s", expectedIVString, ivString));
557
- throw new IllegalStateException("HybridAESEncrypter must store the same IV as the one used to parameterize the secret key");
558
- }
559
-
560
- // Encrypt the symmetric key with the asymmetric public key
561
- byte[] secretKeyBytes = secretKey.getEncoded();
562
- Cipher cipher = getRSACipher();
563
- cipher.init(Cipher.ENCRYPT_MODE, privateKeyEntry.getCertificate());
564
- byte[] encryptedSecretKeyBytes = cipher.doFinal(secretKeyBytes);
565
- String encryptedSecretKeyString = Base64.encodeToString(encryptedSecretKeyBytes, Base64.NO_WRAP);
566
-
567
- // Store the encrypted symmetric key in the encrypted item
568
- return encryptedItem.put(ENCRYPTED_SECRET_KEY_PROPERTY, encryptedSecretKeyString);
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 String decryptItem(JSONObject encryptedItem, KeyStore.PrivateKeyEntry privateKeyEntry) throws
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
- return mAESEncrypter.decryptItem(encryptedItem, secretKeyEntry);
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
  /**
@@ -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).
@@ -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
- [dictionary setObject:(__bridge id)accessibility forKey:(__bridge id)kSecAttrAccessible];
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 @"The user name or passphrase you entered is not correct.";
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",
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
- "gitHead": "4fa0497a180ae707fa860cb03858630ab7af19f4"
42
+ "peerDependencies": {
43
+ "expo": "*"
44
+ },
45
+ "gitHead": "2e5c6983b86d5ecfca028ba64002897d8adc2cc4"
45
46
  }
@@ -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).