expo-secure-store 12.1.1 β†’ 12.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -10,6 +10,22 @@
10
10
 
11
11
  ### πŸ’‘ Others
12
12
 
13
+ ## 12.3.0 β€” 2023-06-13
14
+
15
+ ### πŸ› Bug fixes
16
+
17
+ - Fixed Android build warnings for Gradle version 8. ([#22537](https://github.com/expo/expo/pull/22537), [#22609](https://github.com/expo/expo/pull/22609) by [@kudo](https://github.com/kudo))
18
+
19
+ ### πŸ’‘ Others
20
+
21
+ - Added automatic invalidated key handling on Android. ([#22716](https://github.com/expo/expo/pull/22716) by [@behenate](https://github.com/behenate))
22
+
23
+ ## 12.2.0 β€” 2023-05-08
24
+
25
+ ### πŸŽ‰ New features
26
+
27
+ - Migrated iOS codebase to use Expo modules API. ([#21393](https://github.com/expo/expo/pull/21393) by [@alanjhughes](https://github.com/alanjhughes))
28
+
13
29
  ## 12.1.1 β€” 2023-02-09
14
30
 
15
31
  _This version does not introduce any user-facing changes._
package/README.md CHANGED
@@ -16,7 +16,7 @@ Provides a way to encrypt and securely store key–value pairs locally on the de
16
16
 
17
17
  # Installation in managed Expo projects
18
18
 
19
- For [managed](https://docs.expo.dev/versions/latest/introduction/managed-vs-bare/) Expo projects, please follow the installation instructions in the [API documentation for the latest stable release](https://docs.expo.dev/versions/latest/sdk/securestore/).
19
+ For [managed](https://docs.expo.dev/archive/managed-vs-bare/) Expo projects, please follow the installation instructions in the [API documentation for the latest stable release](https://docs.expo.dev/versions/latest/sdk/securestore/).
20
20
 
21
21
  # Installation in bare React Native projects
22
22
 
@@ -25,7 +25,7 @@ For bare React Native projects, you must ensure that you have [installed and con
25
25
  ### Add the package to your npm dependencies
26
26
 
27
27
  ```
28
- expo install expo-secure-store
28
+ npx expo install expo-secure-store
29
29
  ```
30
30
 
31
31
  ### Configure for iOS
@@ -3,7 +3,7 @@ apply plugin: 'kotlin-android'
3
3
  apply plugin: 'maven-publish'
4
4
 
5
5
  group = 'host.exp.exponent'
6
- version = '12.1.1'
6
+ version = '12.3.0'
7
7
 
8
8
  buildscript {
9
9
  def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
@@ -35,19 +35,11 @@ buildscript {
35
35
  }
36
36
  }
37
37
 
38
- // Creating sources with comments
39
- task androidSourcesJar(type: Jar) {
40
- classifier = 'sources'
41
- from android.sourceSets.main.java.srcDirs
42
- }
43
-
44
38
  afterEvaluate {
45
39
  publishing {
46
40
  publications {
47
41
  release(MavenPublication) {
48
42
  from components.release
49
- // Add additional sourcesJar to artifacts
50
- artifact(androidSourcesJar)
51
43
  }
52
44
  }
53
45
  repositories {
@@ -70,15 +62,21 @@ android {
70
62
  jvmTarget = JavaVersion.VERSION_11.majorVersion
71
63
  }
72
64
 
65
+ namespace "expo.modules.securestore"
73
66
  defaultConfig {
74
67
  minSdkVersion safeExtGet("minSdkVersion", 21)
75
68
  targetSdkVersion safeExtGet("targetSdkVersion", 33)
76
69
  versionCode 17
77
- versionName '12.1.1'
70
+ versionName '12.3.0'
78
71
  }
79
72
  lintOptions {
80
73
  abortOnError false
81
74
  }
75
+ publishing {
76
+ singleVariant("release") {
77
+ withSourcesJar()
78
+ }
79
+ }
82
80
  }
83
81
 
84
82
  dependencies {
@@ -1,2 +1,2 @@
1
- <manifest package="expo.modules.securestore">
1
+ <manifest>
2
2
  </manifest>
@@ -0,0 +1,27 @@
1
+ package expo.modules.securestore
2
+
3
+ import expo.modules.core.errors.CodedException
4
+
5
+ internal class NullKeyException :
6
+ CodedException("SecureStore keys must not be null")
7
+
8
+ internal class WriteException(message: String?, cause: Throwable?) :
9
+ CodedException(message ?: "An unexpected error occurred when writing to SecureStore", cause)
10
+
11
+ internal class ReadException(cause: Throwable?) :
12
+ CodedException("An unexpected error occurred when reading from SecureStore", cause)
13
+
14
+ internal class SecureStoreIOException(cause: Throwable?) :
15
+ CodedException("There was an I/O error loading the keystore for SecureStore", cause)
16
+
17
+ internal class EncryptException(message: String?, cause: Throwable?) :
18
+ CodedException(message ?: "Could not encrypt the value for SecureStore", cause)
19
+
20
+ internal class DecryptException(message: String?, cause: Throwable?) :
21
+ CodedException(message ?: "Could not decrypt the value for SecureStore", cause)
22
+
23
+ internal class SecureStoreJSONException(message: String?, cause: Throwable?) :
24
+ CodedException(message, cause)
25
+
26
+ internal class DeleteException(message: String?, cause: Throwable?) :
27
+ CodedException(message ?: "An unexpected error occurred when deleting from SecureStore", cause)
@@ -8,6 +8,7 @@ import android.os.Build;
8
8
  import android.preference.PreferenceManager;
9
9
  import android.security.KeyPairGeneratorSpec;
10
10
  import android.security.keystore.KeyGenParameterSpec;
11
+ import android.security.keystore.KeyPermanentlyInvalidatedException;
11
12
  import android.security.keystore.KeyProperties;
12
13
  import android.text.TextUtils;
13
14
  import android.util.Base64;
@@ -39,6 +40,7 @@ import java.security.spec.InvalidParameterSpecException;
39
40
  import java.util.Date;
40
41
 
41
42
  import javax.crypto.Cipher;
43
+ import javax.crypto.IllegalBlockSizeException;
42
44
  import javax.crypto.KeyGenerator;
43
45
  import javax.crypto.NoSuchPaddingException;
44
46
  import javax.crypto.SecretKey;
@@ -81,16 +83,16 @@ public class SecureStoreModule extends ExportedModule {
81
83
  @SuppressWarnings("unused")
82
84
  public void setValueWithKeyAsync(String value, String key, ReadableArguments options, Promise promise) {
83
85
  try {
84
- setItemImpl(key, value, options, promise);
86
+ setItemImpl(key, value, options, promise, false);
85
87
  } catch (Exception e) {
86
88
  Log.e(TAG, "Caught unexpected exception when writing to SecureStore", e);
87
- promise.reject("E_SECURESTORE_WRITE_ERROR", "An unexpected error occurred when writing to SecureStore", e);
89
+ promise.reject(new WriteException(null, e));
88
90
  }
89
91
  }
90
92
 
91
- private void setItemImpl(String key, String value, ReadableArguments options, Promise promise) {
93
+ private void setItemImpl(String key, String value, ReadableArguments options, Promise promise, boolean keyIsInvalidated) {
92
94
  if (key == null) {
93
- promise.reject("E_SECURESTORE_NULL_KEY", "SecureStore keys must not be null");
95
+ promise.reject(new NullKeyException());
94
96
  return;
95
97
  }
96
98
 
@@ -101,7 +103,7 @@ public class SecureStoreModule extends ExportedModule {
101
103
  if (success) {
102
104
  promise.resolve(null);
103
105
  } else {
104
- promise.reject("E_SECURESTORE_WRITE_ERROR", "Could not write a null value to SecureStore");
106
+ promise.reject(new WriteException("Could not write a null value to SecureStore", null));
105
107
  }
106
108
  return;
107
109
  }
@@ -114,6 +116,10 @@ public class SecureStoreModule extends ExportedModule {
114
116
  // use in the encrypted JSON item so that we know how to decode and decrypt it when reading
115
117
  // back a value.
116
118
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
119
+ if (keyIsInvalidated) {
120
+ String alias = mAESEncrypter.getKeyStoreAlias(options);
121
+ keyStore.deleteEntry(alias);
122
+ }
117
123
  KeyStore.SecretKeyEntry secretKeyEntry = getKeyEntry(KeyStore.SecretKeyEntry.class, mAESEncrypter, options);
118
124
  mAESEncrypter.createEncryptedItem(promise, value, keyStore, secretKeyEntry, options, mAuthenticationHelper.getDefaultCallback(), (innerPromise, result) -> {
119
125
  JSONObject obj = (JSONObject) result;
@@ -130,23 +136,45 @@ public class SecureStoreModule extends ExportedModule {
130
136
  }
131
137
  } catch (IOException e) {
132
138
  Log.w(TAG, e);
133
- promise.reject("E_SECURESTORE_IO_ERROR", "There was an I/O error loading the keystore for SecureStore", e);
134
- return;
135
- } catch (GeneralSecurityException e) {
139
+ promise.reject(new SecureStoreIOException(e));
140
+ } catch (IllegalBlockSizeException e) {
141
+ // Sometimes, android throws IllegalBlockSizeException when the fingerprint has been changed.
142
+ // https://github.com/expo/expo/issues/22312. It should be handled the same way as KeyPermanentlyInvalidatedException
143
+ boolean isInvalidationException = e.getCause() != null && e.getCause().getMessage() != null && e.getCause().getMessage().contains("Key user not authenticated");
144
+
145
+ if(isInvalidationException && !keyIsInvalidated) {
146
+ setItemImpl(key, value, options, promise, true);
147
+ Log.w(TAG, "IllegalBlockSizeException, retrying with the key deleted");
148
+ return;
149
+ }
150
+ // If the issue persists after deleting the key it is likely not related to invalidation
151
+ promise.reject(new EncryptException(null, e));
136
152
  Log.w(TAG, e);
137
- promise.reject("E_SECURESTORE_ENCRYPT_ERROR", "Could not encrypt the value for SecureStore", e);
138
- return;
153
+ } catch (GeneralSecurityException e) {
154
+ boolean isInvalidationException = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && e instanceof KeyPermanentlyInvalidatedException;
155
+
156
+ if (isInvalidationException && !keyIsInvalidated) {
157
+ // If the key has been invalidated by the OS we try to reinitialize it.
158
+ Log.w(TAG, "Key has been invalidated, retrying with the key deleted");
159
+ setItemImpl(key, value, options, promise, true);
160
+ } else if (isInvalidationException) {
161
+ Log.w(TAG, e);
162
+ // If reinitialization of the key fails, reject the promise
163
+ promise.reject(new EncryptException("Encryption Failed. The key has been permanently invalidated and cannot be reinitialized", e));
164
+ } else {
165
+ Log.w(TAG, e);
166
+ promise.reject(new EncryptException(null, e));
167
+ }
139
168
  } catch (JSONException e) {
140
169
  Log.w(TAG, e);
141
- promise.reject("E_SECURESTORE_ENCODE_ERROR", "Could not create an encrypted JSON item for SecureStore", e);
142
- return;
170
+ promise.reject(new SecureStoreJSONException("Could not create an encrypted JSON item for SecureStore", e));
143
171
  }
144
172
  }
145
173
 
146
174
  private void saveEncryptedItem(Promise promise, JSONObject encryptedItem, SharedPreferences prefs, String key) {
147
175
  String encryptedItemString = encryptedItem.toString();
148
176
  if (encryptedItemString == null) { // lint warning suppressed, JSONObject#toString() may return null
149
- promise.reject("E_SECURESTORE_JSON_ERROR", "Could not JSON-encode the encrypted item for SecureStore");
177
+ promise.reject(new SecureStoreJSONException("Could not JSON-encode the encrypted item for SecureStore", null));
150
178
  return;
151
179
  }
152
180
 
@@ -154,7 +182,7 @@ public class SecureStoreModule extends ExportedModule {
154
182
  if (success) {
155
183
  promise.resolve(null);
156
184
  } else {
157
- promise.reject("E_SECURESTORE_WRITE_ERROR", "Could not write encrypted JSON to SecureStore");
185
+ promise.reject(new WriteException("Could not write encrypted JSON to SecureStore", null));
158
186
  }
159
187
  }
160
188
 
@@ -165,7 +193,7 @@ public class SecureStoreModule extends ExportedModule {
165
193
  getItemImpl(key, options, promise);
166
194
  } catch (Exception e) {
167
195
  Log.e(TAG, "Caught unexpected exception when reading from SecureStore", e);
168
- promise.reject("E_SECURESTORE_READ_ERROR", "An unexpected error occurred when reading from SecureStore", e);
196
+ promise.reject(new ReadException(e));
169
197
  }
170
198
  }
171
199
 
@@ -187,14 +215,14 @@ public class SecureStoreModule extends ExportedModule {
187
215
  encryptedItem = new JSONObject(encryptedItemString);
188
216
  } catch (JSONException e) {
189
217
  Log.e(TAG, String.format("Could not parse stored value as JSON (key = %s, value = %s)", key, encryptedItemString), e);
190
- promise.reject("E_SECURESTORE_JSON_ERROR", "Could not parse the encrypted JSON item in SecureStore");
218
+ promise.reject(new SecureStoreJSONException("Could not parse the encrypted JSON item in SecureStore", e));
191
219
  return;
192
220
  }
193
221
 
194
222
  String scheme = encryptedItem.optString(SCHEME_PROPERTY);
195
223
  if (scheme == null) {
196
224
  Log.e(TAG, String.format("Stored JSON object is missing a scheme (key = %s, value = %s)", key, encryptedItemString));
197
- promise.reject("E_SECURESTORE_DECODE_ERROR", "Could not find the encryption scheme used for SecureStore item");
225
+ promise.reject(new DecryptException("Could not find the encryption scheme used for SecureStore item", null));
198
226
  return;
199
227
  }
200
228
 
@@ -211,21 +239,22 @@ public class SecureStoreModule extends ExportedModule {
211
239
  default:
212
240
  String message = String.format("The item for key \"%s\" in SecureStore has an unknown encoding scheme (%s)", key, scheme);
213
241
  Log.e(TAG, message);
214
- promise.reject("E_SECURESTORE_DECODE_ERROR", message);
215
- return;
242
+ promise.reject(new DecryptException(message, null));
216
243
  }
217
244
  } catch (IOException e) {
218
245
  Log.w(TAG, e);
219
- promise.reject("E_SECURESTORE_IO_ERROR", "There was an I/O error loading the keystore for SecureStore", e);
220
- return;
246
+ promise.reject(new SecureStoreIOException(e));
221
247
  } catch (GeneralSecurityException e) {
248
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && e instanceof KeyPermanentlyInvalidatedException) {
249
+ Log.w(TAG, "The requested key has been permanently invalidated. Returning null");
250
+ promise.resolve(null);
251
+ return;
252
+ }
222
253
  Log.w(TAG, e);
223
- promise.reject("E_SECURESTORE_DECRYPT_ERROR", "Could not decrypt the item in SecureStore", e);
224
- return;
254
+ promise.reject(new DecryptException(null, e));
225
255
  } catch (JSONException e) {
226
256
  Log.w(TAG, e);
227
- promise.reject("E_SECURESTORE_DECODE_ERROR", "Could not decode the encrypted JSON item in SecureStore", e);
228
- return;
257
+ promise.reject(new SecureStoreJSONException("Could not decode the encrypted JSON item in SecureStore", e));
229
258
  }
230
259
  }
231
260
 
@@ -248,24 +277,24 @@ public class SecureStoreModule extends ExportedModule {
248
277
  String keystoreAlias = encrypter.getKeyStoreAlias(options);
249
278
 
250
279
  if (!keyStore.containsAlias(keystoreAlias)) {
251
- promise.reject("E_SECURESTORE_DECRYPT_ERROR", "Could not find the keystore entry to decrypt the legacy item in SecureStore");
280
+ promise.reject(new DecryptException("Could not find the keystore entry to decrypt the legacy item in SecureStore", null));
252
281
  return;
253
282
  }
254
283
 
255
284
  KeyStore.Entry keyStoreEntry = keyStore.getEntry(keystoreAlias, null);
256
285
  if (!(keyStoreEntry instanceof KeyStore.PrivateKeyEntry)) {
257
- promise.reject("E_SECURESTORE_DECRYPT_ERROR", "The keystore entry for the legacy item is not a private key entry");
286
+ promise.reject(new DecryptException("The keystore entry for the legacy item is not a private key entry", null));
258
287
  return;
259
288
  }
260
289
 
261
290
  value = encrypter.decryptItem(encryptedItem, (KeyStore.PrivateKeyEntry) keyStoreEntry);
262
291
  } catch (IOException e) {
263
292
  Log.w(TAG, e);
264
- promise.reject("E_SECURESTORE_IO_ERROR", "There was an I/O error loading the keystore for SecureStore", e);
293
+ promise.reject(new SecureStoreIOException(e));
265
294
  return;
266
295
  } catch (GeneralSecurityException e) {
267
296
  Log.w(TAG, e);
268
- promise.reject("E_SECURESTORE_DECRYPT_ERROR", "Could not decrypt the item in SecureStore", e);
297
+ promise.reject(new DecryptException(null, e));
269
298
  return;
270
299
  }
271
300
 
@@ -279,7 +308,7 @@ public class SecureStoreModule extends ExportedModule {
279
308
  deleteItemImpl(key, promise);
280
309
  } catch (Exception e) {
281
310
  Log.e(TAG, "Caught unexpected exception when deleting from SecureStore", e);
282
- promise.reject("E_SECURESTORE_DELETE_ERROR", "An unexpected error occurred when deleting item from SecureStore", e);
311
+ promise.reject(new DeleteException(null, e));
283
312
  }
284
313
  }
285
314
 
@@ -298,7 +327,7 @@ public class SecureStoreModule extends ExportedModule {
298
327
  if (success) {
299
328
  promise.resolve(null);
300
329
  } else {
301
- promise.reject("E_SECURESTORE_DELETE_ERROR", "Could not delete the item from SecureStore");
330
+ promise.reject(new DeleteException("Could not delete the item from SecureStore", null));
302
331
  }
303
332
  }
304
333
 
@@ -1,3 +1,3 @@
1
- declare const _default: import("expo-modules-core").ProxyNativeModule;
1
+ declare const _default: any;
2
2
  export default _default;
3
3
  //# sourceMappingURL=ExpoSecureStore.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ExpoSecureStore.d.ts","sourceRoot":"","sources":["../src/ExpoSecureStore.ts"],"names":[],"mappings":";AACA,wBAAwD"}
1
+ {"version":3,"file":"ExpoSecureStore.d.ts","sourceRoot":"","sources":["../src/ExpoSecureStore.ts"],"names":[],"mappings":";AACA,wBAAsD"}
@@ -1,3 +1,3 @@
1
- import { NativeModulesProxy } from 'expo-modules-core';
2
- export default NativeModulesProxy.ExpoSecureStore || {};
1
+ import { requireNativeModule } from 'expo-modules-core';
2
+ export default requireNativeModule('ExpoSecureStore');
3
3
  //# sourceMappingURL=ExpoSecureStore.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"ExpoSecureStore.js","sourceRoot":"","sources":["../src/ExpoSecureStore.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AACvD,eAAe,kBAAkB,CAAC,eAAe,IAAI,EAAE,CAAC","sourcesContent":["import { NativeModulesProxy } from 'expo-modules-core';\nexport default NativeModulesProxy.ExpoSecureStore || {};\n"]}
1
+ {"version":3,"file":"ExpoSecureStore.js","sourceRoot":"","sources":["../src/ExpoSecureStore.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AACxD,eAAe,mBAAmB,CAAC,iBAAiB,CAAC,CAAC","sourcesContent":["import { requireNativeModule } from 'expo-modules-core';\nexport default requireNativeModule('ExpoSecureStore');\n"]}
@@ -35,16 +35,17 @@ export declare const WHEN_UNLOCKED: KeychainAccessibilityConstant;
35
35
  export declare const WHEN_UNLOCKED_THIS_DEVICE_ONLY: KeychainAccessibilityConstant;
36
36
  export type SecureStoreOptions = {
37
37
  /**
38
- * - iOS: The item's service, equivalent to `kSecAttrService`
39
- * - Android: Equivalent of the public/private key pair `Alias`
38
+ * - Android: Equivalent of the public/private key pair `Alias`.
39
+ * - iOS: The item's service, equivalent to [`kSecAttrService`](https://developer.apple.com/documentation/security/ksecattrservice/).
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
43
  /**
44
44
  * Option responsible for enabling the usage of the user authentication methods available on the device while
45
45
  * accessing data stored in SecureStore.
46
- * - iOS: Equivalent to `kSecAccessControlBiometryCurrentSet`
47
- * - Android: Equivalent to `setUserAuthenticationRequired(true)` (requires API 23).
46
+ * - Android: Equivalent to [`setUserAuthenticationRequired(true)`](https://developer.android.com/reference/android/security/keystore/KeyGenParameterSpec.Builder#setUserAuthenticationRequired(boolean))
47
+ * (requires API 23).
48
+ * - iOS: Equivalent to [`kSecAccessControlBiometryCurrentSet`](https://developer.apple.com/documentation/security/secaccesscontrolcreateflags/ksecaccesscontrolbiometrycurrentset/).
48
49
  * Complete functionality is unlocked only with a freshly generated key - this would not work in tandem with the `keychainService`
49
50
  * value used for the others non-authenticated operations.
50
51
  */
@@ -55,7 +56,7 @@ export type SecureStoreOptions = {
55
56
  authenticationPrompt?: string;
56
57
  /**
57
58
  * Specifies when the stored entry is accessible, using iOS's `kSecAttrAccessible` property.
58
- * @see Apple's documentation on [keychain item accessibility](https://developer.apple.com/library/content/documentation/Security/Conceptual/keychainServConcepts/02concepts/concepts.html).
59
+ * @see Apple's documentation on [keychain item accessibility](https://developer.apple.com/documentation/security/ksecattraccessible/).
59
60
  * @default SecureStore.WHEN_UNLOCKED
60
61
  * @platform ios
61
62
  */
@@ -66,7 +67,7 @@ export type SecureStoreOptions = {
66
67
  * permissions.
67
68
  *
68
69
  * @return Promise which fulfils witch `boolean`, indicating whether the SecureStore API is available
69
- * on the current device. Currently this resolves `true` on iOS and Android only.
70
+ * on the current device. Currently, this resolves `true` on Android and iOS only.
70
71
  */
71
72
  export declare function isAvailableAsync(): Promise<boolean>;
72
73
  /**
@@ -84,8 +85,12 @@ export declare function deleteItemAsync(key: string, options?: SecureStoreOption
84
85
  * @param key The key that was used to store the associated value.
85
86
  * @param options An [`SecureStoreOptions`](#securestoreoptions) object.
86
87
  *
87
- * @return A promise that resolves to the previously stored value, or `null` if there is no entry
88
- * for the given key. The promise will reject if an error occurred while retrieving the value.
88
+ * @return A promise that resolves to the previously stored value. It will return `null` if there is no entry
89
+ * for the given key or if the key has been invalidated. It will reject if an error occurs while retrieving the value.
90
+ *
91
+ * > Keys are invalidated by the system when biometrics change, such as adding a new fingerprint or changing the face profile used for face recognition.
92
+ * > After a key has been invalidated, it becomes impossible to read its value.
93
+ * > This only applies to values stored with `requireAuthentication` set to `true`.
89
94
  */
90
95
  export declare function getItemAsync(key: string, options?: SecureStoreOptions): Promise<string | null>;
91
96
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"SecureStore.d.ts","sourceRoot":"","sources":["../src/SecureStore.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,6BAA6B,GAAG,MAAM,CAAC;AAGnD;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,EAAE,6BAAkE,CAAC;AAGpG;;;GAGG;AACH,eAAO,MAAM,mCAAmC,EAAE,6BACG,CAAC;AAGtD;;;GAGG;AACH,eAAO,MAAM,MAAM,EAAE,6BAAsD,CAAC;AAG5E;;;GAGG;AACH,eAAO,MAAM,kCAAkC,EAAE,6BACG,CAAC;AAGrD;;GAEG;AACH,eAAO,MAAM,uBAAuB,EAAE,6BACG,CAAC;AAG1C;;GAEG;AACH,eAAO,MAAM,aAAa,EAAE,6BAA6D,CAAC;AAG1F;;;GAGG;AACH,eAAO,MAAM,8BAA8B,EAAE,6BACG,CAAC;AAKjD,MAAM,MAAM,kBAAkB,GAAG;IAC/B;;;;OAIG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;;;OAOG;IACH,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC;;OAEG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B;;;;;OAKG;IACH,kBAAkB,CAAC,EAAE,6BAA6B,CAAC;CACpD,CAAC;AAGF;;;;;;GAMG;AACH,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,OAAO,CAAC,CAEzD;AAGD;;;;;;;GAOG;AACH,wBAAsB,eAAe,CACnC,GAAG,EAAE,MAAM,EACX,OAAO,GAAE,kBAAuB,GAC/B,OAAO,CAAC,IAAI,CAAC,CAOf;AAGD;;;;;;;;GAQG;AACH,wBAAsB,YAAY,CAChC,GAAG,EAAE,MAAM,EACX,OAAO,GAAE,kBAAuB,GAC/B,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAGxB;AAGD;;;;;;;;;GASG;AACH,wBAAsB,YAAY,CAChC,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,kBAAuB,GAC/B,OAAO,CAAC,IAAI,CAAC,CAWf"}
1
+ {"version":3,"file":"SecureStore.d.ts","sourceRoot":"","sources":["../src/SecureStore.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,6BAA6B,GAAG,MAAM,CAAC;AAGnD;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,EAAE,6BAAkE,CAAC;AAGpG;;;GAGG;AACH,eAAO,MAAM,mCAAmC,EAAE,6BACG,CAAC;AAGtD;;;GAGG;AACH,eAAO,MAAM,MAAM,EAAE,6BAAsD,CAAC;AAG5E;;;GAGG;AACH,eAAO,MAAM,kCAAkC,EAAE,6BACG,CAAC;AAGrD;;GAEG;AACH,eAAO,MAAM,uBAAuB,EAAE,6BACG,CAAC;AAG1C;;GAEG;AACH,eAAO,MAAM,aAAa,EAAE,6BAA6D,CAAC;AAG1F;;;GAGG;AACH,eAAO,MAAM,8BAA8B,EAAE,6BACG,CAAC;AAKjD,MAAM,MAAM,kBAAkB,GAAG;IAC/B;;;;OAIG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;;;;OAQG;IACH,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC;;OAEG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B;;;;;OAKG;IACH,kBAAkB,CAAC,EAAE,6BAA6B,CAAC;CACpD,CAAC;AAGF;;;;;;GAMG;AACH,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,OAAO,CAAC,CAEzD;AAGD;;;;;;;GAOG;AACH,wBAAsB,eAAe,CACnC,GAAG,EAAE,MAAM,EACX,OAAO,GAAE,kBAAuB,GAC/B,OAAO,CAAC,IAAI,CAAC,CAOf;AAGD;;;;;;;;;;;;GAYG;AACH,wBAAsB,YAAY,CAChC,GAAG,EAAE,MAAM,EACX,OAAO,GAAE,kBAAuB,GAC/B,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAGxB;AAGD;;;;;;;;;GASG;AACH,wBAAsB,YAAY,CAChC,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,kBAAuB,GAC/B,OAAO,CAAC,IAAI,CAAC,CAWf"}
@@ -48,7 +48,7 @@ const VALUE_BYTES_LIMIT = 2048;
48
48
  * permissions.
49
49
  *
50
50
  * @return Promise which fulfils witch `boolean`, indicating whether the SecureStore API is available
51
- * on the current device. Currently this resolves `true` on iOS and Android only.
51
+ * on the current device. Currently, this resolves `true` on Android and iOS only.
52
52
  */
53
53
  export async function isAvailableAsync() {
54
54
  return !!ExpoSecureStore.getValueWithKeyAsync;
@@ -76,8 +76,12 @@ export async function deleteItemAsync(key, options = {}) {
76
76
  * @param key The key that was used to store the associated value.
77
77
  * @param options An [`SecureStoreOptions`](#securestoreoptions) object.
78
78
  *
79
- * @return A promise that resolves to the previously stored value, or `null` if there is no entry
80
- * for the given key. The promise will reject if an error occurred while retrieving the value.
79
+ * @return A promise that resolves to the previously stored value. It will return `null` if there is no entry
80
+ * for the given key or if the key has been invalidated. It will reject if an error occurs while retrieving the value.
81
+ *
82
+ * > Keys are invalidated by the system when biometrics change, such as adding a new fingerprint or changing the face profile used for face recognition.
83
+ * > After a key has been invalidated, it becomes impossible to read its value.
84
+ * > This only applies to values stored with `requireAuthentication` set to `true`.
81
85
  */
82
86
  export async function getItemAsync(key, options = {}) {
83
87
  _ensureValidKey(key);
@@ -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 * Option responsible for enabling the usage of the user authentication methods available on the device while\n * accessing data stored in SecureStore.\n * - iOS: Equivalent to `kSecAccessControlBiometryCurrentSet`\n * - Android: Equivalent to `setUserAuthenticationRequired(true)` (requires API 23).\n * Complete functionality 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 * Specifies when the stored entry is accessible, using iOS's `kSecAttrAccessible` property.\n * @see Apple's documentation on [keychain item accessibility](https://developer.apple.com/library/content/documentation/Security/Conceptual/keychainServConcepts/02concepts/concepts.html).\n * @default SecureStore.WHEN_UNLOCKED\n * @platform ios\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 * - Android: Equivalent of the public/private key pair `Alias`.\n * - iOS: The item's service, equivalent to [`kSecAttrService`](https://developer.apple.com/documentation/security/ksecattrservice/).\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 * - Android: Equivalent to [`setUserAuthenticationRequired(true)`](https://developer.android.com/reference/android/security/keystore/KeyGenParameterSpec.Builder#setUserAuthenticationRequired(boolean))\n * (requires API 23).\n * - iOS: Equivalent to [`kSecAccessControlBiometryCurrentSet`](https://developer.apple.com/documentation/security/secaccesscontrolcreateflags/ksecaccesscontrolbiometrycurrentset/).\n * Complete functionality 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 * Specifies when the stored entry is accessible, using iOS's `kSecAttrAccessible` property.\n * @see Apple's documentation on [keychain item accessibility](https://developer.apple.com/documentation/security/ksecattraccessible/).\n * @default SecureStore.WHEN_UNLOCKED\n * @platform ios\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 Android and iOS 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. It will return `null` if there is no entry\n * for the given key or if the key has been invalidated. It will reject if an error occurs while retrieving the value.\n *\n * > Keys are invalidated by the system when biometrics change, such as adding a new fingerprint or changing the face profile used for face recognition.\n * > After a key has been invalidated, it becomes impossible to read its value.\n * > This only applies to values stored with `requireAuthentication` set to `true`.\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"]}
@@ -0,0 +1,6 @@
1
+ {
2
+ "platforms": ["ios", "android"],
3
+ "ios": {
4
+ "modules": ["SecureStoreModule"]
5
+ }
6
+ }
@@ -3,7 +3,7 @@ require 'json'
3
3
  package = JSON.parse(File.read(File.join(__dir__, '..', 'package.json')))
4
4
 
5
5
  Pod::Spec.new do |s|
6
- s.name = 'EXSecureStore'
6
+ s.name = 'ExpoSecureStore'
7
7
  s.version = package['version']
8
8
  s.summary = package['description']
9
9
  s.description = package['description']
@@ -11,15 +11,22 @@ Pod::Spec.new do |s|
11
11
  s.author = package['author']
12
12
  s.homepage = package['homepage']
13
13
  s.platform = :ios, '13.0'
14
+ s.swift_version = '5.4'
14
15
  s.source = { git: 'https://github.com/expo/expo.git' }
15
16
  s.static_framework = true
16
17
 
17
18
  s.dependency 'ExpoModulesCore'
18
19
 
20
+ # Swift/Objective-C compatibility
21
+ s.pod_target_xcconfig = {
22
+ 'DEFINES_MODULE' => 'YES',
23
+ 'SWIFT_COMPILATION_MODE' => 'wholemodule'
24
+ }
25
+
19
26
  if !$ExpoUseSources&.include?(package['name']) && ENV['EXPO_USE_SOURCE'].to_i == 0 && File.exist?("#{s.name}.xcframework") && Gem::Version.new(Pod::VERSION) >= Gem::Version.new('1.10.0')
20
- s.source_files = "#{s.name}/**/*.h"
27
+ s.source_files = "**/*.h"
21
28
  s.vendored_frameworks = "#{s.name}.xcframework"
22
29
  else
23
- s.source_files = "#{s.name}/**/*.{h,m}"
30
+ s.source_files = "**/*.{h,m,swift}"
24
31
  end
25
32
  end
@@ -0,0 +1,11 @@
1
+ import ExpoModulesCore
2
+
3
+ enum SecureStoreAccessible: Int, Enumerable {
4
+ case afterFirstUnlock = 0
5
+ case afterFirstUnlockThisDeviceOnly = 1
6
+ case always = 2
7
+ case whenPasscodeSetThisDeviceOnly = 3
8
+ case alwaysThisDeviceOnly = 4
9
+ case whenUnlocked = 5
10
+ case whenUnlockedThisDeviceOnly = 6
11
+ }
@@ -0,0 +1,55 @@
1
+ import ExpoModulesCore
2
+
3
+ internal class InvalidKeyException: Exception {
4
+ override var reason: String {
5
+ "Invalid key"
6
+ }
7
+ }
8
+
9
+ internal class KeyChainException: GenericException<OSStatus> {
10
+ override var reason: String {
11
+ switch param {
12
+ case errSecUnimplemented:
13
+ return "Function or operation not implemented."
14
+
15
+ case errSecIO:
16
+ return "I/O error."
17
+
18
+ case errSecOpWr:
19
+ return "File already open with with write permission."
20
+
21
+ case errSecParam:
22
+ return "One or more parameters passed to a function where not valid."
23
+
24
+ case errSecAllocate:
25
+ return "Failed to allocate memory."
26
+
27
+ case errSecUserCanceled:
28
+ return "User canceled the operation."
29
+
30
+ case errSecBadReq:
31
+ return "Bad parameter or invalid state for operation."
32
+
33
+ case errSecNotAvailable:
34
+ return "No keychain is available. You may need to restart your computer."
35
+
36
+ case errSecDuplicateItem:
37
+ return "The specified item already exists in the keychain."
38
+
39
+ case errSecItemNotFound:
40
+ return "The specified item could not be found in the keychain."
41
+
42
+ case errSecInteractionNotAllowed:
43
+ return "User interaction is not allowed."
44
+
45
+ case errSecDecode:
46
+ return "Unable to decode the provided data."
47
+
48
+ case errSecAuthFailed:
49
+ return "Authentication failed. Provided passphrase/PIN is incorrect or there is no user authentication method configured for this device."
50
+
51
+ default:
52
+ return "Unknown Keychain Error."
53
+ }
54
+ }
55
+ }
@@ -0,0 +1,160 @@
1
+ import ExpoModulesCore
2
+ import LocalAuthentication
3
+ import Security
4
+
5
+ public final class SecureStoreModule: Module {
6
+ public func definition() -> ModuleDefinition {
7
+ Name("ExpoSecureStore")
8
+
9
+ Constants([
10
+ "AFTER_FIRST_UNLOCK": SecureStoreAccessible.afterFirstUnlock.rawValue,
11
+ "AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY": SecureStoreAccessible.afterFirstUnlockThisDeviceOnly.rawValue,
12
+ "ALWAYS": SecureStoreAccessible.always.rawValue,
13
+ "WHEN_PASSCODE_SET_THIS_DEVICE_ONLY": SecureStoreAccessible.whenPasscodeSetThisDeviceOnly.rawValue,
14
+ "ALWAYS_THIS_DEVICE_ONLY": SecureStoreAccessible.alwaysThisDeviceOnly.rawValue,
15
+ "WHEN_UNLOCKED": SecureStoreAccessible.whenUnlocked.rawValue,
16
+ "WHEN_UNLOCKED_THIS_DEVICE_ONLY": SecureStoreAccessible.whenPasscodeSetThisDeviceOnly.rawValue
17
+ ])
18
+
19
+ AsyncFunction("getValueWithKeyAsync") { (key: String, options: SecureStoreOptions) -> String? in
20
+ guard let key = validate(for: key) else {
21
+ throw InvalidKeyException()
22
+ }
23
+
24
+ let data = try searchKeyChain(with: key, options: options)
25
+
26
+ guard let data = data else {
27
+ return nil
28
+ }
29
+
30
+ return String(data: data, encoding: .utf8)
31
+ }
32
+
33
+ AsyncFunction("setValueWithKeyAsync") { (value: String, key: String, options: SecureStoreOptions) -> Bool in
34
+ guard let key = validate(for: key) else {
35
+ throw InvalidKeyException()
36
+ }
37
+
38
+ return try set(value: value, with: key, options: options)
39
+ }
40
+
41
+ AsyncFunction("deleteValueWithKeyAsync") { (key: String, options: SecureStoreOptions) in
42
+ var searchDictionary = query(with: key, options: options)
43
+ SecItemDelete(searchDictionary as CFDictionary)
44
+ }
45
+ }
46
+
47
+ private func set(value: String, with key: String, options: SecureStoreOptions) throws -> Bool {
48
+ var query = query(with: key, options: options)
49
+
50
+ let valueData = value.data(using: .utf8)
51
+ query[kSecValueData as String] = valueData
52
+
53
+ let accessibility = attributeWith(options: options)
54
+
55
+ if !options.requireAuthentication {
56
+ query[kSecAttrAccessible as String] = accessibility
57
+ } else {
58
+ let accessOptions = SecAccessControlCreateWithFlags(kCFAllocatorDefault, accessibility, SecAccessControlCreateFlags.biometryCurrentSet, nil)
59
+ query[kSecAttrAccessControl as String] = accessOptions
60
+ }
61
+
62
+ let status = SecItemAdd(query as CFDictionary, nil)
63
+
64
+ switch status {
65
+ case errSecSuccess:
66
+ return true
67
+ case errSecDuplicateItem:
68
+ return try update(value: value, with: key, options: options)
69
+ default:
70
+ throw KeyChainException(status)
71
+ }
72
+ }
73
+
74
+ private func update(value: String, with key: String, options: SecureStoreOptions) throws -> Bool {
75
+ var query = query(with: key, options: options)
76
+
77
+ let valueData = value.data(using: .utf8)
78
+ let updateDictionary = [kSecValueData as String: valueData]
79
+
80
+ if let authPrompt = options.authenticationPrompt {
81
+ query[kSecUseOperationPrompt as String] = authPrompt
82
+ }
83
+
84
+ let status = SecItemUpdate(query as CFDictionary, updateDictionary as CFDictionary)
85
+
86
+ if status == errSecSuccess {
87
+ return true
88
+ } else {
89
+ throw KeyChainException(status)
90
+ }
91
+ }
92
+
93
+ private func searchKeyChain(with key: String, options: SecureStoreOptions) throws -> Data? {
94
+ var query = query(with: key, options: options)
95
+
96
+ query[kSecMatchLimit as String] = kSecMatchLimitOne
97
+ query[kSecReturnData as String] = kCFBooleanTrue
98
+
99
+ if let authPrompt = options.authenticationPrompt {
100
+ query[kSecUseOperationPrompt as String] = authPrompt
101
+ }
102
+
103
+ var item: CFTypeRef?
104
+ let status = SecItemCopyMatching(query as CFDictionary, &item)
105
+
106
+ switch status {
107
+ case errSecSuccess:
108
+ guard let item = item as? Data else {
109
+ return nil
110
+ }
111
+
112
+ return item
113
+ case errSecItemNotFound:
114
+ return nil
115
+ default:
116
+ throw KeyChainException(status)
117
+ }
118
+ }
119
+
120
+ private func query(with key: String, options: SecureStoreOptions) -> [String: Any] {
121
+ let service = options.keychainService ?? "app"
122
+ let encodedKey = key.data(using: .utf8)
123
+
124
+ return [
125
+ kSecClass as String: kSecClassGenericPassword,
126
+ kSecAttrService as String: service,
127
+ kSecAttrGeneric as String: encodedKey,
128
+ kSecAttrAccount as String: encodedKey
129
+ ]
130
+ }
131
+
132
+ private func attributeWith(options: SecureStoreOptions) -> CFString {
133
+ switch options.keychainAccessible {
134
+ case .afterFirstUnlock:
135
+ return kSecAttrAccessibleAfterFirstUnlock
136
+ case .afterFirstUnlockThisDeviceOnly:
137
+ return kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
138
+ case .always:
139
+ return kSecAttrAccessibleAlways
140
+ case .whenPasscodeSetThisDeviceOnly:
141
+ return kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly
142
+ case .whenUnlocked:
143
+ return kSecAttrAccessibleWhenUnlocked
144
+ case .alwaysThisDeviceOnly:
145
+ return kSecAttrAccessibleAlwaysThisDeviceOnly
146
+ case .whenUnlockedThisDeviceOnly:
147
+ return kSecAttrAccessibleWhenUnlockedThisDeviceOnly
148
+ default:
149
+ return kSecAttrAccessibleWhenUnlocked
150
+ }
151
+ }
152
+
153
+ private func validate(for key: String) -> String? {
154
+ let trimmedKey = key.trimmingCharacters(in: .whitespaces)
155
+ if trimmedKey.isEmpty {
156
+ return nil
157
+ }
158
+ return key
159
+ }
160
+ }
@@ -0,0 +1,15 @@
1
+ import ExpoModulesCore
2
+
3
+ internal struct SecureStoreOptions: Record {
4
+ @Field
5
+ var authenticationPrompt: String?
6
+
7
+ @Field
8
+ var keychainAccessible: SecureStoreAccessible = .whenUnlocked
9
+
10
+ @Field
11
+ var keychainService: String?
12
+
13
+ @Field
14
+ var requireAuthentication: Bool
15
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-secure-store",
3
- "version": "12.1.1",
3
+ "version": "12.3.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",
@@ -42,5 +42,5 @@
42
42
  "peerDependencies": {
43
43
  "expo": "*"
44
44
  },
45
- "gitHead": "1f8a6a09570fd451378565ca34933018ce48454e"
45
+ "gitHead": "3ccd2edee9cbfed217557675cb50f0ba5e55a9e4"
46
46
  }
@@ -1,2 +1,2 @@
1
- import { NativeModulesProxy } from 'expo-modules-core';
2
- export default NativeModulesProxy.ExpoSecureStore || {};
1
+ import { requireNativeModule } from 'expo-modules-core';
2
+ export default requireNativeModule('ExpoSecureStore');
@@ -61,16 +61,17 @@ const VALUE_BYTES_LIMIT = 2048;
61
61
  // @needsAudit
62
62
  export type SecureStoreOptions = {
63
63
  /**
64
- * - iOS: The item's service, equivalent to `kSecAttrService`
65
- * - Android: Equivalent of the public/private key pair `Alias`
64
+ * - Android: Equivalent of the public/private key pair `Alias`.
65
+ * - iOS: The item's service, equivalent to [`kSecAttrService`](https://developer.apple.com/documentation/security/ksecattrservice/).
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
69
  /**
70
70
  * Option responsible for enabling the usage of the user authentication methods available on the device while
71
71
  * accessing data stored in SecureStore.
72
- * - iOS: Equivalent to `kSecAccessControlBiometryCurrentSet`
73
- * - Android: Equivalent to `setUserAuthenticationRequired(true)` (requires API 23).
72
+ * - Android: Equivalent to [`setUserAuthenticationRequired(true)`](https://developer.android.com/reference/android/security/keystore/KeyGenParameterSpec.Builder#setUserAuthenticationRequired(boolean))
73
+ * (requires API 23).
74
+ * - iOS: Equivalent to [`kSecAccessControlBiometryCurrentSet`](https://developer.apple.com/documentation/security/secaccesscontrolcreateflags/ksecaccesscontrolbiometrycurrentset/).
74
75
  * Complete functionality is unlocked only with a freshly generated key - this would not work in tandem with the `keychainService`
75
76
  * value used for the others non-authenticated operations.
76
77
  */
@@ -81,7 +82,7 @@ export type SecureStoreOptions = {
81
82
  authenticationPrompt?: string;
82
83
  /**
83
84
  * Specifies when the stored entry is accessible, using iOS's `kSecAttrAccessible` property.
84
- * @see Apple's documentation on [keychain item accessibility](https://developer.apple.com/library/content/documentation/Security/Conceptual/keychainServConcepts/02concepts/concepts.html).
85
+ * @see Apple's documentation on [keychain item accessibility](https://developer.apple.com/documentation/security/ksecattraccessible/).
85
86
  * @default SecureStore.WHEN_UNLOCKED
86
87
  * @platform ios
87
88
  */
@@ -94,7 +95,7 @@ export type SecureStoreOptions = {
94
95
  * permissions.
95
96
  *
96
97
  * @return Promise which fulfils witch `boolean`, indicating whether the SecureStore API is available
97
- * on the current device. Currently this resolves `true` on iOS and Android only.
98
+ * on the current device. Currently, this resolves `true` on Android and iOS only.
98
99
  */
99
100
  export async function isAvailableAsync(): Promise<boolean> {
100
101
  return !!ExpoSecureStore.getValueWithKeyAsync;
@@ -128,8 +129,12 @@ export async function deleteItemAsync(
128
129
  * @param key The key that was used to store the associated value.
129
130
  * @param options An [`SecureStoreOptions`](#securestoreoptions) object.
130
131
  *
131
- * @return A promise that resolves to the previously stored value, or `null` if there is no entry
132
- * for the given key. The promise will reject if an error occurred while retrieving the value.
132
+ * @return A promise that resolves to the previously stored value. It will return `null` if there is no entry
133
+ * for the given key or if the key has been invalidated. It will reject if an error occurs while retrieving the value.
134
+ *
135
+ * > Keys are invalidated by the system when biometrics change, such as adding a new fingerprint or changing the face profile used for face recognition.
136
+ * > After a key has been invalidated, it becomes impossible to read its value.
137
+ * > This only applies to values stored with `requireAuthentication` set to `true`.
133
138
  */
134
139
  export async function getItemAsync(
135
140
  key: string,
@@ -1,18 +0,0 @@
1
- // Copyright Β© 2018 650 Industries. All rights reserved.
2
-
3
- #import <ExpoModulesCore/EXExportedModule.h>
4
- #import <ExpoModulesCore/EXModuleRegistryConsumer.h>
5
-
6
- typedef NS_ENUM(NSInteger, EXSecureStoreAccessible) {
7
- EXSecureStoreAccessibleAfterFirstUnlock = 0,
8
- EXSecureStoreAccessibleAfterFirstUnlockThisDeviceOnly = 1,
9
- EXSecureStoreAccessibleAlways = 2,
10
- EXSecureStoreAccessibleWhenPasscodeSetThisDeviceOnly = 3,
11
- EXSecureStoreAccessibleAlwaysThisDeviceOnly = 4,
12
- EXSecureStoreAccessibleWhenUnlocked = 5,
13
- EXSecureStoreAccessibleWhenUnlockedThisDeviceOnly = 6
14
- };
15
-
16
- @interface EXSecureStore : EXExportedModule
17
-
18
- @end
@@ -1,298 +0,0 @@
1
- // Copyright Β© 2018 650 Industries. All rights reserved.
2
-
3
- #import <EXSecureStore/EXSecureStore.h>
4
-
5
- #import <CommonCrypto/CommonHMAC.h>
6
- #import <Security/Security.h>
7
-
8
- @implementation EXSecureStore
9
-
10
- #pragma mark - internal
11
-
12
- - (NSMutableDictionary *)_queryWithKey:(NSString *)key withOptions:(NSDictionary *)options
13
- {
14
- NSString *service = (NSString *) options[@"keychainService"] ? (NSString *) options[@"keychainService"]:@"app";
15
- NSData *encodedKey = [key dataUsingEncoding:NSUTF8StringEncoding];
16
- NSMutableDictionary *query = [@{
17
- (__bridge id)kSecClass:(__bridge id)kSecClassGenericPassword,
18
- (__bridge id)kSecAttrService:service,
19
- (__bridge id)kSecAttrGeneric:encodedKey,
20
- (__bridge id)kSecAttrAccount:encodedKey
21
- } mutableCopy];
22
-
23
- return query;
24
- }
25
-
26
- - (BOOL)_setValue:(NSString *)value withKey:(NSString *)key withOptions:(NSDictionary *)options error:(NSError **)error
27
- {
28
- NSMutableDictionary *dictionary = [self _queryWithKey:key
29
- withOptions:options];
30
-
31
- NSString *requireAuth = options[@"requireAuthentication"];
32
-
33
- NSData *valueData = [value dataUsingEncoding:NSUTF8StringEncoding];
34
- [dictionary setObject:valueData forKey:(__bridge id)kSecValueData];
35
-
36
- CFStringRef accessibility = [self _accessibilityAttributeWithOptions:options];
37
-
38
- if (![requireAuth boolValue]) {
39
- [dictionary setObject:(__bridge id)accessibility forKey:(__bridge id)kSecAttrAccessible];
40
- } else {
41
- SecAccessControlRef accessOptions = SecAccessControlCreateWithFlags(nil, accessibility, kSecAccessControlBiometryCurrentSet, nil);
42
- [dictionary setObject:(__bridge_transfer id)accessOptions forKey:(__bridge id)kSecAttrAccessControl];
43
- }
44
-
45
- OSStatus status = SecItemAdd((__bridge CFDictionaryRef)dictionary, NULL);
46
-
47
- if (status == errSecSuccess) {
48
- return YES;
49
- } else if (status == errSecDuplicateItem){
50
- NSError *updateError;
51
- BOOL update = [self _updateValue:value
52
- withKey:key
53
- withOptions:options
54
- error:&updateError];
55
- *error = updateError;
56
- return update;
57
- } else {
58
- *error = [NSError errorWithDomain:NSOSStatusErrorDomain code:status userInfo:nil];
59
- return NO;
60
- }
61
- }
62
-
63
- - (NSString *)_getValueWithKey:(NSString *)key withOptions:(NSDictionary *)options error:(NSError **)error
64
- {
65
- NSError *searchError;
66
- NSData *data = [self _searchKeychainWithKey:key
67
- withOptions:options
68
- error:&searchError];
69
- if (data) {
70
- NSString *value = [[NSString alloc] initWithData:data
71
- encoding:NSUTF8StringEncoding];
72
- return value;
73
- } else {
74
- *error = searchError;
75
- return nil;
76
- }
77
- }
78
-
79
- - (BOOL)_updateValue:(NSString *)value withKey:(NSString *)key withOptions:(NSDictionary *)options error:(NSError **)error
80
- {
81
- NSMutableDictionary *searchDictionary = [self _queryWithKey:key
82
- withOptions:options];
83
- NSData *valueData = [value dataUsingEncoding:NSUTF8StringEncoding];
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
- }
90
-
91
- OSStatus status = SecItemUpdate((__bridge CFDictionaryRef)searchDictionary,
92
- (__bridge CFDictionaryRef)updateDictionary);
93
-
94
- if (status == errSecSuccess) {
95
- return YES;
96
- } else {
97
- *error = [NSError errorWithDomain:NSOSStatusErrorDomain code:status userInfo:nil];
98
- return NO;
99
- }
100
- }
101
-
102
- - (NSData *)_searchKeychainWithKey:(NSString *)key withOptions:(NSDictionary *)options error:(NSError **)error
103
- {
104
- NSMutableDictionary *searchDictionary = [self _queryWithKey:key
105
- withOptions:options];
106
-
107
- [searchDictionary setObject:(__bridge id)kSecMatchLimitOne forKey:(__bridge id)kSecMatchLimit];
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
- }
114
-
115
- CFTypeRef foundDict = NULL;
116
- OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)searchDictionary, &foundDict);
117
-
118
- if (status == noErr) {
119
- NSData *result = (__bridge_transfer NSData *)foundDict;
120
- return result;
121
- } else {
122
- *error = [NSError errorWithDomain:NSOSStatusErrorDomain code:status userInfo:nil];
123
- return nil;
124
- }
125
- }
126
-
127
- - (void)_deleteValueWithKey:(NSString *)key withOptions:(NSDictionary *)options
128
- {
129
- NSMutableDictionary *searchDictionary = [self _queryWithKey:key
130
- withOptions:options];
131
- CFDictionaryRef dictionary = (__bridge CFDictionaryRef)searchDictionary;
132
-
133
- SecItemDelete(dictionary);
134
- }
135
-
136
- - (CFStringRef)_accessibilityAttributeWithOptions:(NSDictionary *)options
137
- {
138
- NSInteger accessibility = [options[@"keychainAccessible"] integerValue];
139
- switch (accessibility) {
140
- case EXSecureStoreAccessibleAfterFirstUnlock:
141
- return kSecAttrAccessibleAfterFirstUnlock;
142
- case EXSecureStoreAccessibleAfterFirstUnlockThisDeviceOnly:
143
- return kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly;
144
- case EXSecureStoreAccessibleAlways:
145
- return kSecAttrAccessibleAlways;
146
- case EXSecureStoreAccessibleWhenPasscodeSetThisDeviceOnly:
147
- return kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly;
148
- case EXSecureStoreAccessibleWhenUnlocked:
149
- return kSecAttrAccessibleWhenUnlocked;
150
- case EXSecureStoreAccessibleAlwaysThisDeviceOnly:
151
- return kSecAttrAccessibleAlwaysThisDeviceOnly;
152
- case EXSecureStoreAccessibleWhenUnlockedThisDeviceOnly:
153
- return kSecAttrAccessibleWhenUnlockedThisDeviceOnly;
154
- default:
155
- return kSecAttrAccessibleWhenUnlocked;
156
- }
157
- }
158
-
159
- + (NSString *) _messageForError:(NSError *)error
160
- {
161
- switch (error.code) {
162
- case errSecUnimplemented:
163
- return @"Function or operation not implemented.";
164
-
165
- case errSecIO:
166
- return @"I/O error.";
167
-
168
- case errSecOpWr:
169
- return @"File already open with with write permission.";
170
-
171
- case errSecParam:
172
- return @"One or more parameters passed to a function where not valid.";
173
-
174
- case errSecAllocate:
175
- return @"Failed to allocate memory.";
176
-
177
- case errSecUserCanceled:
178
- return @"User canceled the operation.";
179
-
180
- case errSecBadReq:
181
- return @"Bad parameter or invalid state for operation.";
182
-
183
- case errSecNotAvailable:
184
- return @"No keychain is available. You may need to restart your computer.";
185
-
186
- case errSecDuplicateItem:
187
- return @"The specified item already exists in the keychain.";
188
-
189
- case errSecItemNotFound:
190
- return @"The specified item could not be found in the keychain.";
191
-
192
- case errSecInteractionNotAllowed:
193
- return @"User interaction is not allowed.";
194
-
195
- case errSecDecode:
196
- return @"Unable to decode the provided data.";
197
-
198
- case errSecAuthFailed:
199
- return @"Authentication failed. Provided passphrase/PIN is incorrect or there is no user authentication method configured for this device.";
200
-
201
- default:
202
- return error.localizedDescription;
203
- }
204
- }
205
-
206
- #pragma mark - SecureStore API
207
-
208
- - (NSDictionary *)constantsToExport
209
- {
210
- return @{
211
- @"AFTER_FIRST_UNLOCK":@(EXSecureStoreAccessibleAfterFirstUnlock),
212
- @"AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY":@(EXSecureStoreAccessibleAfterFirstUnlockThisDeviceOnly),
213
- @"ALWAYS":@(EXSecureStoreAccessibleAlways),
214
- @"WHEN_PASSCODE_SET_THIS_DEVICE_ONLY":@(EXSecureStoreAccessibleWhenPasscodeSetThisDeviceOnly),
215
- @"ALWAYS_THIS_DEVICE_ONLY":@(EXSecureStoreAccessibleAlwaysThisDeviceOnly),
216
- @"WHEN_UNLOCKED":@(EXSecureStoreAccessibleWhenUnlocked),
217
- @"WHEN_UNLOCKED_THIS_DEVICE_ONLY":@(EXSecureStoreAccessibleWhenUnlockedThisDeviceOnly)
218
- };
219
- };
220
-
221
- EX_EXPORT_MODULE(ExpoSecureStore);
222
-
223
- EX_EXPORT_METHOD_AS(setValueWithKeyAsync,
224
- setValueWithKeyAsync:(NSString *)value
225
- key:(NSString *)key
226
- options:(NSDictionary *)options
227
- resolver:(EXPromiseResolveBlock)resolve
228
- rejecter:(EXPromiseRejectBlock)reject)
229
- {
230
- NSString *validatedKey = [self validatedKey:key];
231
- if (!validatedKey) {
232
- reject(@"E_SECURESTORE_SETVALUEFAIL", @"Invalid key", nil);
233
- } else {
234
- NSError *error;
235
- BOOL setValue = [self _setValue:value
236
- withKey:validatedKey
237
- withOptions:options
238
- error:&error];
239
- if (setValue) {
240
- resolve(nil);
241
- } else {
242
- reject(@"E_SECURESTORE_SETVALUEFAIL", [[self class] _messageForError:error], nil);
243
- }
244
- }
245
- }
246
-
247
- EX_EXPORT_METHOD_AS(getValueWithKeyAsync,
248
- getValueWithKeyAsync:(NSString *)key
249
- options:(NSDictionary *)options
250
- resolver:(EXPromiseResolveBlock)resolve
251
- rejecter:(EXPromiseRejectBlock)reject)
252
- {
253
- NSString *validatedKey = [self validatedKey:key];
254
- if (!validatedKey) {
255
- reject(@"E_SECURESTORE_GETVALUEFAIL", @"Invalid key", nil);
256
- } else {
257
- NSError *error;
258
- NSString *value = [self _getValueWithKey:validatedKey
259
- withOptions:options
260
- error:&error];
261
- if (error) {
262
- if (error.code == errSecItemNotFound) {
263
- resolve([NSNull null]);
264
- } else {
265
- reject(@"E_SECURESTORE_GETVALUEFAIL", [[self class] _messageForError:error], nil);
266
- }
267
- } else {
268
- resolve(value);
269
- }
270
- }
271
- }
272
-
273
- EX_EXPORT_METHOD_AS(deleteValueWithKeyAsync,
274
- deleteValueWithKeyAsync:(NSString *)key
275
- options:(NSDictionary *)options
276
- resolver:(EXPromiseResolveBlock)resolve
277
- rejecter:(EXPromiseRejectBlock)reject)
278
- {
279
- NSString *validatedKey = [self validatedKey:key];
280
- if (!validatedKey) {
281
- reject(@"E_SECURESTORE_DELETEVALUEFAIL", @"Invalid key", nil);
282
- } else {
283
- [self _deleteValueWithKey:validatedKey
284
- withOptions:options];
285
- resolve(nil);
286
- }
287
- }
288
-
289
- - (NSString *)validatedKey:(NSString *)key
290
- {
291
- NSString *trimmedKey = [key stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
292
- if (!key || trimmedKey.length == 0) {
293
- return nil;
294
- }
295
- return key;
296
- }
297
-
298
- @end
package/unimodule.json DELETED
@@ -1,4 +0,0 @@
1
- {
2
- "name": "expo-secure-store",
3
- "platforms": ["ios", "android"]
4
- }