expo-secure-store 13.0.2-canary-20240625-2333e70 β†’ 13.1.0-canary-20240627-1402f4b

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
@@ -6,12 +6,17 @@
6
6
 
7
7
  ### πŸŽ‰ New features
8
8
 
9
+ - [Android] Add a config plugin for configuring the Android backup system. ([#29944](https://github.com/expo/expo/pull/29944) by [@behenate](https://github.com/behenate))
10
+
9
11
  ### πŸ› Bug fixes
10
12
 
11
13
  - [iOS] Improve error message for unhandled errors ([#29394](https://github.com/expo/expo/pull/29394) by [@hassankhan](https://github.com/hassankhan))
14
+ - [Android] Fix decryption errors after Android Auto Backup has restored `expo-secure-store` data. ([#29943](https://github.com/expo/expo/pull/29943) by [@behenate](https://github.com/behenate))
12
15
 
13
16
  ### πŸ’‘ Others
14
17
 
18
+ - [iOS] check return value of SecAccessControlCreateWithFlags ([#29983](https://github.com/expo/expo/pull/29983) by [@vonovak](https://github.com/vonovak))
19
+
15
20
  ## 13.0.1 β€” 2024-04-23
16
21
 
17
22
  _This version does not introduce any user-facing changes._
package/README.md CHANGED
@@ -11,8 +11,8 @@ Provides a way to encrypt and securely store key–value pairs locally on the de
11
11
 
12
12
  # API documentation
13
13
 
14
- - [Documentation for the main branch](https://github.com/expo/expo/blob/main/docs/pages/versions/unversioned/sdk/securestore.mdx)
15
14
  - [Documentation for the latest stable release](https://docs.expo.dev/versions/latest/sdk/securestore/)
15
+ - [Documentation for the main branch](https://docs.expo.dev/versions/unversioned/sdk/securestore/)
16
16
 
17
17
  # Installation in managed Expo projects
18
18
 
@@ -28,14 +28,14 @@ For bare React Native projects, you must ensure that you have [installed and con
28
28
  npx expo install expo-secure-store
29
29
  ```
30
30
 
31
- ### Configure for iOS
32
-
33
- Run `npx pod-install` after installing the npm package.
34
-
35
31
  ### Configure for Android
36
32
 
37
33
  No additional set up necessary.
38
34
 
35
+ ### Configure for iOS
36
+
37
+ Run `npx pod-install` after installing the npm package.
38
+
39
39
  # Contributing
40
40
 
41
41
  Contributions are very welcome! Please refer to guidelines described in the [contributing guide](https://github.com/expo/expo#contributing).
@@ -133,13 +133,20 @@ open class SecureStoreModule : Module() {
133
133
  try {
134
134
  when (scheme) {
135
135
  AESEncryptor.NAME -> {
136
- val secretKeyEntry = getPreferredKeyEntry(SecretKeyEntry::class.java, mAESEncryptor, options, requireAuthentication, usesKeystoreSuffix)
137
- ?: throw DecryptException("Could not find a keychain for key $key$legacyReadFailedWarning", key, options.keychainService)
136
+ val secretKeyEntry = getKeyEntryCompat(SecretKeyEntry::class.java, mAESEncryptor, options, requireAuthentication, usesKeystoreSuffix) ?: run {
137
+ Log.w(
138
+ TAG,
139
+ "An entry was found for key $key under keychain ${options.keychainService}, but there is no corresponding KeyStore key. " +
140
+ "This situation occurs when the app is reinstalled. The value will be removed to avoid future errors. Returning null"
141
+ )
142
+ deleteItemImpl(key, options)
143
+ return null
144
+ }
138
145
  return mAESEncryptor.decryptItem(key, encryptedItem, secretKeyEntry, options, authenticationHelper)
139
146
  }
140
147
  HybridAESEncryptor.NAME -> {
141
- val privateKeyEntry = getPreferredKeyEntry(PrivateKeyEntry::class.java, hybridAESEncryptor, options, requireAuthentication, usesKeystoreSuffix)
142
- ?: throw DecryptException("Could not find a keychain for key $key$legacyReadFailedWarning", key, options.keychainService)
148
+ val privateKeyEntry = getKeyEntryCompat(PrivateKeyEntry::class.java, hybridAESEncryptor, options, requireAuthentication, usesKeystoreSuffix)
149
+ ?: return null
143
150
  return hybridAESEncryptor.decryptItem(key, encryptedItem, privateKeyEntry, options, authenticationHelper)
144
151
  }
145
152
  else -> {
@@ -150,7 +157,15 @@ open class SecureStoreModule : Module() {
150
157
  Log.w(TAG, "The requested key has been permanently invalidated. Returning null")
151
158
  return null
152
159
  } catch (e: BadPaddingException) {
153
- throw (DecryptException("Could not decrypt the value with provided keychain $legacyReadFailedWarning", key, options.keychainService, e))
160
+ // The key from the KeyStore is unable to decode the entry. This is because a new key was generated, but the entries are encrypted using the old one.
161
+ // This usually means that the user has reinstalled the app. We can safely remove the old value and return null as it's impossible to decrypt it.
162
+ Log.w(
163
+ TAG,
164
+ "Failed to decrypt the entry for $key under keychain ${options.keychainService}. " +
165
+ "The entry in shared preferences is out of sync with the keystore. It will be removed, returning null."
166
+ )
167
+ deleteItemImpl(key, options)
168
+ return null
154
169
  } catch (e: GeneralSecurityException) {
155
170
  throw (DecryptException(e.message, key, options.keychainService, e))
156
171
  } catch (e: CodedException) {
@@ -185,7 +200,7 @@ open class SecureStoreModule : Module() {
185
200
  use in the encrypted JSON item so that we know how to decode and decrypt it when reading
186
201
  back a value.
187
202
  */
188
- val secretKeyEntry: SecretKeyEntry = getKeyEntry(SecretKeyEntry::class.java, mAESEncryptor, options, options.requireAuthentication)
203
+ val secretKeyEntry: SecretKeyEntry = getOrCreateKeyEntry(SecretKeyEntry::class.java, mAESEncryptor, options, options.requireAuthentication)
189
204
  val encryptedItem = mAESEncryptor.createEncryptedItem(value, secretKeyEntry, options.requireAuthentication, options.authenticationPrompt, authenticationHelper)
190
205
  encryptedItem.put(SCHEME_PROPERTY, AESEncryptor.NAME)
191
206
  saveEncryptedItem(encryptedItem, prefs, keychainAwareKey, options.requireAuthentication, options.keychainService)
@@ -249,11 +264,14 @@ open class SecureStoreModule : Module() {
249
264
  }
250
265
 
251
266
  private fun removeKeyFromKeystore(keyStoreAlias: String, keychainService: String) {
267
+ keyStore.deleteEntry(keyStoreAlias)
268
+ removeAllEntriesUnderKeychainService(keychainService)
269
+ }
270
+
271
+ private fun removeAllEntriesUnderKeychainService(keychainService: String) {
252
272
  val sharedPreferences = getSharedPreferences()
253
273
  val allEntries: Map<String, *> = sharedPreferences.all
254
274
 
255
- keyStore.deleteEntry(keyStoreAlias)
256
-
257
275
  // In order to avoid decryption failures we need to remove all entries that are using the deleted encryption key
258
276
  for ((key: String, value) in allEntries) {
259
277
  val valueString = value as? String ?: continue
@@ -302,26 +320,36 @@ open class SecureStoreModule : Module() {
302
320
  encryptor: KeyBasedEncryptor<E>,
303
321
  options: SecureStoreOptions,
304
322
  requireAuthentication: Boolean
305
- ): E {
323
+ ): E? {
306
324
  val keystoreAlias = encryptor.getExtendedKeyStoreAlias(options, requireAuthentication)
307
- val keyStoreEntry = if (!keyStore.containsAlias(keystoreAlias)) {
308
- // Android won't allow us to generate the keys if the device doesn't support biometrics or no biometrics are enrolled
309
- if (requireAuthentication) {
310
- authenticationHelper.assertBiometricsSupport()
311
- }
312
- encryptor.initializeKeyStoreEntry(keyStore, options)
313
- } else {
325
+ return if (keyStore.containsAlias(keystoreAlias)) {
314
326
  val entry = keyStore.getEntry(keystoreAlias, null)
315
327
  if (!keyStoreEntryClass.isInstance(entry)) {
316
328
  throw KeyStoreException("The entry for the keystore alias \"$keystoreAlias\" is not a ${keyStoreEntryClass.simpleName}")
317
329
  }
318
330
  keyStoreEntryClass.cast(entry)
319
331
  ?: throw KeyStoreException("The entry for the keystore alias \"$keystoreAlias\" couldn't be cast to correct class")
332
+ } else {
333
+ null
334
+ }
335
+ }
336
+
337
+ private fun <E : KeyStore.Entry> getOrCreateKeyEntry(
338
+ keyStoreEntryClass: Class<E>,
339
+ encryptor: KeyBasedEncryptor<E>,
340
+ options: SecureStoreOptions,
341
+ requireAuthentication: Boolean
342
+ ): E {
343
+ return getKeyEntry(keyStoreEntryClass, encryptor, options, requireAuthentication) ?: run {
344
+ // Android won't allow us to generate the keys if the device doesn't support biometrics or no biometrics are enrolled
345
+ if (requireAuthentication) {
346
+ authenticationHelper.assertBiometricsSupport()
347
+ }
348
+ encryptor.initializeKeyStoreEntry(keyStore, options)
320
349
  }
321
- return keyStoreEntry
322
350
  }
323
351
 
324
- private fun <E : KeyStore.Entry> getPreferredKeyEntry(
352
+ private fun <E : KeyStore.Entry> getKeyEntryCompat(
325
353
  keyStoreEntryClass: Class<E>,
326
354
  encryptor: KeyBasedEncryptor<E>,
327
355
  options: SecureStoreOptions,
@@ -0,0 +1,7 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+
3
+ <!-- Auto Backup configuration for Android 11 and lower -->
4
+ <full-backup-content>
5
+ <include domain="sharedpref" path="."/>
6
+ <exclude domain="sharedpref" path="SecureStore"/>
7
+ </full-backup-content>
@@ -0,0 +1,13 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+
3
+ <!-- Auto Backup configuration for Android 12 and higher -->
4
+ <data-extraction-rules>
5
+ <cloud-backup>
6
+ <include domain="sharedpref" path="."/>
7
+ <exclude domain="sharedpref" path="SecureStore"/>
8
+ </cloud-backup>
9
+ <device-transfer>
10
+ <include domain="sharedpref" path="."/>
11
+ <exclude domain="sharedpref" path="SecureStore"/>
12
+ </device-transfer>
13
+ </data-extraction-rules>
@@ -13,6 +13,8 @@ export declare const AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY: KeychainAccessibilityC
13
13
  /**
14
14
  * The data in the keychain item can always be accessed regardless of whether the device is locked.
15
15
  * This is the least secure option.
16
+ *
17
+ * @deprecated Use an accessibility level that provides some user protection, such as `AFTER_FIRST_UNLOCK`.
16
18
  */
17
19
  export declare const ALWAYS: KeychainAccessibilityConstant;
18
20
  /**
@@ -22,6 +24,8 @@ export declare const ALWAYS: KeychainAccessibilityConstant;
22
24
  export declare const WHEN_PASSCODE_SET_THIS_DEVICE_ONLY: KeychainAccessibilityConstant;
23
25
  /**
24
26
  * Similar to `ALWAYS`, except the entry is not migrated to a new device when restoring from a backup.
27
+ *
28
+ * @deprecated Use an accessibility level that provides some user protection, such as `AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY`.
25
29
  */
26
30
  export declare const ALWAYS_THIS_DEVICE_ONLY: KeychainAccessibilityConstant;
27
31
  /**
@@ -45,10 +49,13 @@ export type SecureStoreOptions = {
45
49
  * accessing data stored in SecureStore.
46
50
  * - Android: Equivalent to [`setUserAuthenticationRequired(true)`](https://developer.android.com/reference/android/security/keystore/KeyGenParameterSpec.Builder#setUserAuthenticationRequired(boolean))
47
51
  * (requires API 23).
48
- * - iOS: Equivalent to [`kSecAccessControlBiometryCurrentSet`](https://developer.apple.com/documentation/security/secaccesscontrolcreateflags/ksecaccesscontrolbiometrycurrentset/).
52
+ * - iOS: Equivalent to [`biometryCurrentSet`](https://developer.apple.com/documentation/security/secaccesscontrolcreateflags/2937192-biometrycurrentset).
49
53
  * Complete functionality is unlocked only with a freshly generated key - this would not work in tandem with the `keychainService`
50
54
  * value used for the others non-authenticated operations.
51
55
  *
56
+ * This option works slightly differently across platforms: On Android, user authentication is required for all operations.
57
+ * On iOS, the user is prompted to authenticate only when reading or updating an existing value (not when creating a new one).
58
+ *
52
59
  * Warning: This option is not supported in Expo Go when biometric authentication is available due to a missing NSFaceIDUsageDescription.
53
60
  * In release builds or when using continuous native generation, make sure to use the `expo-secure-store` config plugin.
54
61
  *
@@ -80,7 +87,7 @@ export declare function isAvailableAsync(): Promise<boolean>;
80
87
  * @param key The key that was used to store the associated value.
81
88
  * @param options An [`SecureStoreOptions`](#securestoreoptions) object.
82
89
  *
83
- * @return A promise that will reject if the value couldn't be deleted.
90
+ * @return A promise that rejects if the value can't be deleted.
84
91
  */
85
92
  export declare function deleteItemAsync(key: string, options?: SecureStoreOptions): Promise<void>;
86
93
  /**
@@ -89,8 +96,8 @@ export declare function deleteItemAsync(key: string, options?: SecureStoreOption
89
96
  * @param key The key that was used to store the associated value.
90
97
  * @param options An [`SecureStoreOptions`](#securestoreoptions) object.
91
98
  *
92
- * @return A promise that resolves to the previously stored value. It will return `null` if there is no entry
93
- * for the given key or if the key has been invalidated. It will reject if an error occurs while retrieving the value.
99
+ * @return A promise that resolves to the previously stored value. It resolves with `null` if there is no entry
100
+ * for the given key or if the key has been invalidated. It rejects if an error occurs while retrieving the value.
94
101
  *
95
102
  * > Keys are invalidated by the system when biometrics change, such as adding a new fingerprint or changing the face profile used for face recognition.
96
103
  * > After a key has been invalidated, it becomes impossible to read its value.
@@ -104,7 +111,7 @@ export declare function getItemAsync(key: string, options?: SecureStoreOptions):
104
111
  * @param value The value to store. Size limit is 2048 bytes.
105
112
  * @param options An [`SecureStoreOptions`](#securestoreoptions) object.
106
113
  *
107
- * @return A promise that will reject if value cannot be stored on the device.
114
+ * @return A promise that rejects if value cannot be stored on the device.
108
115
  */
109
116
  export declare function setItemAsync(key: string, value: string, options?: SecureStoreOptions): Promise<void>;
110
117
  /**
@@ -124,7 +131,8 @@ export declare function setItem(key: string, value: string, options?: SecureStor
124
131
  * @param key The key that was used to store the associated value.
125
132
  * @param options An [`SecureStoreOptions`](#securestoreoptions) object.
126
133
  *
127
- * @return Previously stored value. It will return `null` if there is no entry for the given key or if the key has been invalidated.
134
+ * @return Previously stored value. It resolves with `null` if there is no entry
135
+ * for the given key or if the key has been invalidated.
128
136
  */
129
137
  export declare function getItem(key: string, options?: SecureStoreOptions): string | null;
130
138
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"SecureStore.d.ts","sourceRoot":"","sources":["../src/SecureStore.ts"],"names":[],"mappings":"AAEA,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;;;;;;;;;;;;OAYG;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,CAIf;AAGD;;;;;;;;;;;;GAYG;AACH,wBAAsB,YAAY,CAChC,GAAG,EAAE,MAAM,EACX,OAAO,GAAE,kBAAuB,GAC/B,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAGxB;AAGD;;;;;;;;GAQG;AACH,wBAAsB,YAAY,CAChC,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,kBAAuB,GAC/B,OAAO,CAAC,IAAI,CAAC,CASf;AAED;;;;;;;;GAQG;AACH,wBAAgB,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,GAAE,kBAAuB,GAAG,IAAI,CAS1F;AAED;;;;;;;;GAQG;AACH,wBAAgB,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,kBAAuB,GAAG,MAAM,GAAG,IAAI,CAGpF;AAED;;;GAGG;AACH,wBAAgB,6BAA6B,IAAI,OAAO,CAEvD"}
1
+ {"version":3,"file":"SecureStore.d.ts","sourceRoot":"","sources":["../src/SecureStore.ts"],"names":[],"mappings":"AAGA,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;;;;;GAKG;AACH,eAAO,MAAM,MAAM,EAAE,6BAAsD,CAAC;AAG5E;;;GAGG;AACH,eAAO,MAAM,kCAAkC,EAAE,6BACG,CAAC;AAGrD;;;;GAIG;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;AAGjD,MAAM,MAAM,kBAAkB,GAAG;IAC/B;;;;OAIG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;;;;;;;;;;;OAeG;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,CAIf;AAGD;;;;;;;;;;;;GAYG;AACH,wBAAsB,YAAY,CAChC,GAAG,EAAE,MAAM,EACX,OAAO,GAAE,kBAAuB,GAC/B,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAGxB;AAGD;;;;;;;;GAQG;AACH,wBAAsB,YAAY,CAChC,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,kBAAuB,GAC/B,OAAO,CAAC,IAAI,CAAC,CASf;AAED;;;;;;;;GAQG;AACH,wBAAgB,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,GAAE,kBAAuB,GAAG,IAAI,CAS1F;AAED;;;;;;;;;GASG;AACH,wBAAgB,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,kBAAuB,GAAG,MAAM,GAAG,IAAI,CAGpF;AAED;;;GAGG;AACH,wBAAgB,6BAA6B,IAAI,OAAO,CAEvD"}
@@ -1,4 +1,5 @@
1
1
  import ExpoSecureStore from './ExpoSecureStore';
2
+ import { byteCountOverLimit, VALUE_BYTES_LIMIT } from './byteCounter';
2
3
  // @needsAudit
3
4
  /**
4
5
  * The data in the keychain item cannot be accessed after a restart until the device has been
@@ -16,6 +17,8 @@ export const AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY = ExpoSecureStore.AFTER_FIRST_U
16
17
  /**
17
18
  * The data in the keychain item can always be accessed regardless of whether the device is locked.
18
19
  * This is the least secure option.
20
+ *
21
+ * @deprecated Use an accessibility level that provides some user protection, such as `AFTER_FIRST_UNLOCK`.
19
22
  */
20
23
  export const ALWAYS = ExpoSecureStore.ALWAYS;
21
24
  // @needsAudit
@@ -27,6 +30,8 @@ export const WHEN_PASSCODE_SET_THIS_DEVICE_ONLY = ExpoSecureStore.WHEN_PASSCODE_
27
30
  // @needsAudit
28
31
  /**
29
32
  * Similar to `ALWAYS`, except the entry is not migrated to a new device when restoring from a backup.
33
+ *
34
+ * @deprecated Use an accessibility level that provides some user protection, such as `AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY`.
30
35
  */
31
36
  export const ALWAYS_THIS_DEVICE_ONLY = ExpoSecureStore.ALWAYS_THIS_DEVICE_ONLY;
32
37
  // @needsAudit
@@ -40,7 +45,6 @@ export const WHEN_UNLOCKED = ExpoSecureStore.WHEN_UNLOCKED;
40
45
  * a backup.
41
46
  */
42
47
  export const WHEN_UNLOCKED_THIS_DEVICE_ONLY = ExpoSecureStore.WHEN_UNLOCKED_THIS_DEVICE_ONLY;
43
- const VALUE_BYTES_LIMIT = 2048;
44
48
  // @needsAudit
45
49
  /**
46
50
  * Returns whether the SecureStore API is enabled on the current device. This does not check the app
@@ -59,7 +63,7 @@ export async function isAvailableAsync() {
59
63
  * @param key The key that was used to store the associated value.
60
64
  * @param options An [`SecureStoreOptions`](#securestoreoptions) object.
61
65
  *
62
- * @return A promise that will reject if the value couldn't be deleted.
66
+ * @return A promise that rejects if the value can't be deleted.
63
67
  */
64
68
  export async function deleteItemAsync(key, options = {}) {
65
69
  ensureValidKey(key);
@@ -72,8 +76,8 @@ export async function deleteItemAsync(key, options = {}) {
72
76
  * @param key The key that was used to store the associated value.
73
77
  * @param options An [`SecureStoreOptions`](#securestoreoptions) object.
74
78
  *
75
- * @return A promise that resolves to the previously stored value. It will return `null` if there is no entry
76
- * for the given key or if the key has been invalidated. It will reject if an error occurs while retrieving the value.
79
+ * @return A promise that resolves to the previously stored value. It resolves with `null` if there is no entry
80
+ * for the given key or if the key has been invalidated. It rejects if an error occurs while retrieving the value.
77
81
  *
78
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.
79
83
  * > After a key has been invalidated, it becomes impossible to read its value.
@@ -91,7 +95,7 @@ export async function getItemAsync(key, options = {}) {
91
95
  * @param value The value to store. Size limit is 2048 bytes.
92
96
  * @param options An [`SecureStoreOptions`](#securestoreoptions) object.
93
97
  *
94
- * @return A promise that will reject if value cannot be stored on the device.
98
+ * @return A promise that rejects if value cannot be stored on the device.
95
99
  */
96
100
  export async function setItemAsync(key, value, options = {}) {
97
101
  ensureValidKey(key);
@@ -123,7 +127,8 @@ export function setItem(key, value, options = {}) {
123
127
  * @param key The key that was used to store the associated value.
124
128
  * @param options An [`SecureStoreOptions`](#securestoreoptions) object.
125
129
  *
126
- * @return Previously stored value. It will return `null` if there is no entry for the given key or if the key has been invalidated.
130
+ * @return Previously stored value. It resolves with `null` if there is no entry
131
+ * for the given key or if the key has been invalidated.
127
132
  */
128
133
  export function getItem(key, options = {}) {
129
134
  ensureValidKey(key);
@@ -148,29 +153,9 @@ function isValidValue(value) {
148
153
  if (typeof value !== 'string') {
149
154
  return false;
150
155
  }
151
- if (byteCount(value) > VALUE_BYTES_LIMIT) {
152
- console.warn('Value being stored in SecureStore is larger than 2048 bytes and it may not be stored successfully. In a future SDK version, this call may throw an error.');
156
+ if (byteCountOverLimit(value, VALUE_BYTES_LIMIT)) {
157
+ console.warn(`Value being stored in SecureStore is larger than ${VALUE_BYTES_LIMIT} bytes and it may not be stored successfully. In a future SDK version, this call may throw an error.`);
153
158
  }
154
159
  return true;
155
160
  }
156
- // copy-pasted from https://stackoverflow.com/a/39488643
157
- function byteCount(value) {
158
- let bytes = 0;
159
- for (let i = 0; i < value.length; i++) {
160
- const codePoint = value.charCodeAt(i);
161
- // Lone surrogates cannot be passed to encodeURI
162
- if (codePoint >= 0xd800 && codePoint < 0xe000) {
163
- if (codePoint < 0xdc00 && i + 1 < value.length) {
164
- const next = value.charCodeAt(i + 1);
165
- if (next >= 0xdc00 && next < 0xe000) {
166
- bytes += 4;
167
- i++;
168
- continue;
169
- }
170
- }
171
- }
172
- bytes += codePoint < 0x80 ? 1 : codePoint < 0x800 ? 2 : 3;
173
- }
174
- return bytes;
175
- }
176
161
  //# sourceMappingURL=SecureStore.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"SecureStore.js","sourceRoot":"","sources":["../src/SecureStore.ts"],"names":[],"mappings":"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","sourcesContent":["import 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 * Warning: This option is not supported in Expo Go when biometric authentication is available due to a missing NSFaceIDUsageDescription.\n * In release builds or when using continuous native generation, make sure to use the `expo-secure-store` config plugin.\n *\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 await ExpoSecureStore.deleteValueWithKeyAsync(key, options);\n}\n\n// @needsAudit\n/**\n * Reads 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 * Stores a key–value pair.\n *\n * @param key The key to associate with the stored value. Keys may contain alphanumeric characters, `.`, `-`, 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\n await ExpoSecureStore.setValueWithKeyAsync(value, key, options);\n}\n\n/**\n * Stores a key–value pair synchronously.\n * > **Note:** This function blocks the JavaScript thread, so the application may not be interactive when the `requireAuthentication` option is set to `true` until the user authenticates.\n *\n * @param key The key to associate with the stored value. Keys may contain alphanumeric characters, `.`, `-`, and `_`.\n * @param value The value to store. Size limit is 2048 bytes.\n * @param options An [`SecureStoreOptions`](#securestoreoptions) object.\n *\n */\nexport function setItem(key: string, value: string, options: SecureStoreOptions = {}): 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\n return ExpoSecureStore.setValueWithKeySync(value, key, options);\n}\n\n/**\n * Synchronously reads the stored value associated with the provided key.\n * > **Note:** This function blocks the JavaScript thread, so the application may not be interactive when reading a value with `requireAuthentication`\n * > option set to `true` until the user authenticates.\n * @param key The key that was used to store the associated value.\n * @param options An [`SecureStoreOptions`](#securestoreoptions) object.\n *\n * @return Previously stored value. It will return `null` if there is no entry for the given key or if the key has been invalidated.\n */\nexport function getItem(key: string, options: SecureStoreOptions = {}): string | null {\n ensureValidKey(key);\n return ExpoSecureStore.getValueWithKeySync(key, options);\n}\n\n/**\n * Checks if the value can be saved with `requireAuthentication` option enabled.\n * @return `true` if the device supports biometric authentication and the enrolled method is sufficiently secure. Otherwise, returns `false`.\n */\nexport function canUseBiometricAuthentication(): boolean {\n return ExpoSecureStore.canUseBiometricAuthentication();\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 'Value being stored in SecureStore is larger than 2048 bytes and it may not be stored successfully. In a future SDK version, this call may throw an error.'\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 ExpoSecureStore from './ExpoSecureStore';\nimport { byteCountOverLimit, VALUE_BYTES_LIMIT } from './byteCounter';\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 *\n * @deprecated Use an accessibility level that provides some user protection, such as `AFTER_FIRST_UNLOCK`.\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 *\n * @deprecated Use an accessibility level that provides some user protection, such as `AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY`.\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\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 [`biometryCurrentSet`](https://developer.apple.com/documentation/security/secaccesscontrolcreateflags/2937192-biometrycurrentset).\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 * This option works slightly differently across platforms: On Android, user authentication is required for all operations.\n * On iOS, the user is prompted to authenticate only when reading or updating an existing value (not when creating a new one).\n *\n * Warning: This option is not supported in Expo Go when biometric authentication is available due to a missing NSFaceIDUsageDescription.\n * In release builds or when using continuous native generation, make sure to use the `expo-secure-store` config plugin.\n *\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 rejects if the value can't be deleted.\n */\nexport async function deleteItemAsync(\n key: string,\n options: SecureStoreOptions = {}\n): Promise<void> {\n ensureValidKey(key);\n\n await ExpoSecureStore.deleteValueWithKeyAsync(key, options);\n}\n\n// @needsAudit\n/**\n * Reads 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 resolves with `null` if there is no entry\n * for the given key or if the key has been invalidated. It rejects 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 * Stores a key–value pair.\n *\n * @param key The key to associate with the stored value. Keys may contain alphanumeric characters, `.`, `-`, 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 rejects 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\n await ExpoSecureStore.setValueWithKeyAsync(value, key, options);\n}\n\n/**\n * Stores a key–value pair synchronously.\n * > **Note:** This function blocks the JavaScript thread, so the application may not be interactive when the `requireAuthentication` option is set to `true` until the user authenticates.\n *\n * @param key The key to associate with the stored value. Keys may contain alphanumeric characters, `.`, `-`, and `_`.\n * @param value The value to store. Size limit is 2048 bytes.\n * @param options An [`SecureStoreOptions`](#securestoreoptions) object.\n *\n */\nexport function setItem(key: string, value: string, options: SecureStoreOptions = {}): 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\n return ExpoSecureStore.setValueWithKeySync(value, key, options);\n}\n\n/**\n * Synchronously reads the stored value associated with the provided key.\n * > **Note:** This function blocks the JavaScript thread, so the application may not be interactive when reading a value with `requireAuthentication`\n * > option set to `true` until the user authenticates.\n * @param key The key that was used to store the associated value.\n * @param options An [`SecureStoreOptions`](#securestoreoptions) object.\n *\n * @return Previously stored value. It resolves with `null` if there is no entry\n * for the given key or if the key has been invalidated.\n */\nexport function getItem(key: string, options: SecureStoreOptions = {}): string | null {\n ensureValidKey(key);\n return ExpoSecureStore.getValueWithKeySync(key, options);\n}\n\n/**\n * Checks if the value can be saved with `requireAuthentication` option enabled.\n * @return `true` if the device supports biometric authentication and the enrolled method is sufficiently secure. Otherwise, returns `false`.\n */\nexport function canUseBiometricAuthentication(): boolean {\n return ExpoSecureStore.canUseBiometricAuthentication();\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 (byteCountOverLimit(value, VALUE_BYTES_LIMIT)) {\n console.warn(\n `Value being stored in SecureStore is larger than ${VALUE_BYTES_LIMIT} bytes and it may not be stored successfully. In a future SDK version, this call may throw an error.`\n );\n }\n return true;\n}\n"]}
@@ -0,0 +1,3 @@
1
+ export declare const VALUE_BYTES_LIMIT = 2048;
2
+ export declare function byteCountOverLimit(value: string, limit: number): boolean;
3
+ //# sourceMappingURL=byteCounter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"byteCounter.d.ts","sourceRoot":"","sources":["../src/byteCounter.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,iBAAiB,OAAO,CAAC;AAItC,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CA6BxE"}
@@ -0,0 +1,29 @@
1
+ export const VALUE_BYTES_LIMIT = 2048;
2
+ // note this probably could be JS-engine dependent
3
+ // inspired by https://stackoverflow.com/a/39488643
4
+ export function byteCountOverLimit(value, limit) {
5
+ let bytes = 0;
6
+ for (let i = 0; i < value.length; i++) {
7
+ const codePoint = value.charCodeAt(i);
8
+ // Lone surrogates cannot be passed to encodeURI
9
+ if (codePoint >= 0xd800 && codePoint < 0xe000) {
10
+ if (codePoint < 0xdc00 && i + 1 < value.length) {
11
+ const next = value.charCodeAt(i + 1);
12
+ if (next >= 0xdc00 && next < 0xe000) {
13
+ bytes += 4;
14
+ if (bytes > limit) {
15
+ return true;
16
+ }
17
+ i++;
18
+ continue;
19
+ }
20
+ }
21
+ }
22
+ bytes += codePoint < 0x80 ? 1 : codePoint < 0x800 ? 2 : 3;
23
+ if (bytes > limit) {
24
+ return true;
25
+ }
26
+ }
27
+ return bytes > limit;
28
+ }
29
+ //# sourceMappingURL=byteCounter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"byteCounter.js","sourceRoot":"","sources":["../src/byteCounter.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,iBAAiB,GAAG,IAAI,CAAC;AAEtC,kDAAkD;AAClD,mDAAmD;AACnD,MAAM,UAAU,kBAAkB,CAAC,KAAa,EAAE,KAAa;IAC7D,IAAI,KAAK,GAAG,CAAC,CAAC;IAEd,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE;QACrC,MAAM,SAAS,GAAG,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;QAEtC,gDAAgD;QAChD,IAAI,SAAS,IAAI,MAAM,IAAI,SAAS,GAAG,MAAM,EAAE;YAC7C,IAAI,SAAS,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE;gBAC9C,MAAM,IAAI,GAAG,KAAK,CAAC,UAAU,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;gBAErC,IAAI,IAAI,IAAI,MAAM,IAAI,IAAI,GAAG,MAAM,EAAE;oBACnC,KAAK,IAAI,CAAC,CAAC;oBACX,IAAI,KAAK,GAAG,KAAK,EAAE;wBACjB,OAAO,IAAI,CAAC;qBACb;oBACD,CAAC,EAAE,CAAC;oBACJ,SAAS;iBACV;aACF;SACF;QAED,KAAK,IAAI,SAAS,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAC1D,IAAI,KAAK,GAAG,KAAK,EAAE;YACjB,OAAO,IAAI,CAAC;SACb;KACF;IAED,OAAO,KAAK,GAAG,KAAK,CAAC;AACvB,CAAC","sourcesContent":["export const VALUE_BYTES_LIMIT = 2048;\n\n// note this probably could be JS-engine dependent\n// inspired by https://stackoverflow.com/a/39488643\nexport function byteCountOverLimit(value: string, limit: number): boolean {\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 if (bytes > limit) {\n return true;\n }\n i++;\n continue;\n }\n }\n }\n\n bytes += codePoint < 0x80 ? 1 : codePoint < 0x800 ? 2 : 3;\n if (bytes > limit) {\n return true;\n }\n }\n\n return bytes > limit;\n}\n"]}
@@ -12,6 +12,12 @@ internal class MissingPlistKeyException: Exception {
12
12
  }
13
13
  }
14
14
 
15
+ internal class SecAccessControlError: GenericException<Int?> {
16
+ override var reason: String {
17
+ return "Unable to construct SecAccessControl: \(param.map { "code " + String($0) } ?? "unknown error")"
18
+ }
19
+ }
20
+
15
21
  internal class KeyChainException: GenericException<OSStatus> {
16
22
  override var reason: String {
17
23
  switch param {
@@ -96,7 +96,12 @@ public final class SecureStoreModule: Module {
96
96
  guard let _ = Bundle.main.infoDictionary?["NSFaceIDUsageDescription"] as? String else {
97
97
  throw MissingPlistKeyException()
98
98
  }
99
- let accessOptions = SecAccessControlCreateWithFlags(kCFAllocatorDefault, accessibility, SecAccessControlCreateFlags.biometryCurrentSet, nil)
99
+
100
+ var error: Unmanaged<CFError>? = nil
101
+ guard let accessOptions = SecAccessControlCreateWithFlags(kCFAllocatorDefault, accessibility, .biometryCurrentSet, &error) else {
102
+ let errorCode = error.map { CFErrorGetCode($0.takeRetainedValue()) }
103
+ throw SecAccessControlError(errorCode)
104
+ }
100
105
  setItemQuery[kSecAttrAccessControl as String] = accessOptions
101
106
  }
102
107
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-secure-store",
3
- "version": "13.0.2-canary-20240625-2333e70",
3
+ "version": "13.1.0-canary-20240627-1402f4b",
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",
@@ -37,10 +37,10 @@
37
37
  },
38
38
  "dependencies": {},
39
39
  "devDependencies": {
40
- "expo-module-scripts": "3.6.0-canary-20240625-2333e70"
40
+ "expo-module-scripts": "3.6.0-canary-20240627-1402f4b"
41
41
  },
42
42
  "peerDependencies": {
43
43
  "expo": "*"
44
44
  },
45
- "gitHead": "2333e70a4bd3ac91895402dac77ae8ae0ed25995"
45
+ "gitHead": "1402f4bcfdfcec328fc1e20cf1656bbefe7c3b7b"
46
46
  }
@@ -1,5 +1,6 @@
1
- import { ConfigPlugin } from '@expo/config-plugins';
1
+ import { ConfigPlugin } from 'expo/config-plugins';
2
2
  declare const _default: ConfigPlugin<void | {
3
3
  faceIDPermission?: string | false | undefined;
4
+ configureAndroidBackup?: boolean | undefined;
4
5
  }>;
5
6
  export default _default;
@@ -1,13 +1,37 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- const config_plugins_1 = require("@expo/config-plugins");
3
+ const config_plugins_1 = require("expo/config-plugins");
4
4
  const pkg = require('expo-secure-store/package.json');
5
+ const BACKUP_RULES_PATH = '@xml/secure_store_backup_rules';
6
+ const EXTRACTION_RULES_PATH = '@xml/secure_store_data_extraction_rules';
5
7
  const FACEID_USAGE = 'Allow $(PRODUCT_NAME) to access your Face ID biometric data.';
6
- const withSecureStore = (config, { faceIDPermission } = {}) => {
7
- return config_plugins_1.IOSConfig.Permissions.createPermissionsPlugin({
8
+ const withSecureStore = (config, { faceIDPermission, configureAndroidBackup = true } = {}) => {
9
+ config_plugins_1.IOSConfig.Permissions.createPermissionsPlugin({
8
10
  NSFaceIDUsageDescription: FACEID_USAGE,
9
11
  })(config, {
10
12
  NSFaceIDUsageDescription: faceIDPermission,
11
13
  });
14
+ (0, config_plugins_1.withAndroidManifest)(config, (config) => {
15
+ const mainApplication = config_plugins_1.AndroidConfig.Manifest.getMainApplicationOrThrow(config.modResults);
16
+ const backupConfig = mainApplication.$['android:fullBackupContent']; // SDK <= 30
17
+ const dataExtractionRules = mainApplication.$['android:dataExtractionRules']; // SDK >= 31
18
+ if (!configureAndroidBackup) {
19
+ backupConfig === BACKUP_RULES_PATH && delete mainApplication.$['android:fullBackupContent'];
20
+ dataExtractionRules === EXTRACTION_RULES_PATH &&
21
+ delete mainApplication.$['android:dataExtractionRules'];
22
+ return config;
23
+ }
24
+ const canApplyBackupConfig = !backupConfig || backupConfig === BACKUP_RULES_PATH;
25
+ const canApplyDataExtractionRules = !dataExtractionRules || dataExtractionRules === EXTRACTION_RULES_PATH;
26
+ if (canApplyBackupConfig && canApplyDataExtractionRules) {
27
+ mainApplication.$['android:fullBackupContent'] = BACKUP_RULES_PATH;
28
+ mainApplication.$['android:dataExtractionRules'] = EXTRACTION_RULES_PATH;
29
+ return config;
30
+ }
31
+ console.warn('Expo-secure-store tried to apply Android Auto Backup rules, but other backup rules are already present. ' +
32
+ 'Refer to the expo-secure-store docs (https://docs.expo.dev/versions/latest/sdk/securestore/) to configure your backup rules.');
33
+ return config;
34
+ });
35
+ return config;
12
36
  };
13
37
  exports.default = (0, config_plugins_1.createRunOncePlugin)(withSecureStore, pkg.name, pkg.version);
@@ -1,19 +1,58 @@
1
- import { ConfigPlugin, IOSConfig, createRunOncePlugin } from '@expo/config-plugins';
1
+ import {
2
+ ConfigPlugin,
3
+ IOSConfig,
4
+ createRunOncePlugin,
5
+ withAndroidManifest,
6
+ AndroidConfig,
7
+ } from 'expo/config-plugins';
2
8
 
3
9
  const pkg = require('expo-secure-store/package.json');
4
10
 
11
+ const BACKUP_RULES_PATH = '@xml/secure_store_backup_rules';
12
+ const EXTRACTION_RULES_PATH = '@xml/secure_store_data_extraction_rules';
5
13
  const FACEID_USAGE = 'Allow $(PRODUCT_NAME) to access your Face ID biometric data.';
6
14
 
7
15
  const withSecureStore: ConfigPlugin<
8
16
  {
9
17
  faceIDPermission?: string | false;
18
+ configureAndroidBackup?: boolean;
10
19
  } | void
11
- > = (config, { faceIDPermission } = {}) => {
12
- return IOSConfig.Permissions.createPermissionsPlugin({
20
+ > = (config, { faceIDPermission, configureAndroidBackup = true } = {}) => {
21
+ IOSConfig.Permissions.createPermissionsPlugin({
13
22
  NSFaceIDUsageDescription: FACEID_USAGE,
14
23
  })(config, {
15
24
  NSFaceIDUsageDescription: faceIDPermission,
16
25
  });
26
+
27
+ withAndroidManifest(config, (config) => {
28
+ const mainApplication = AndroidConfig.Manifest.getMainApplicationOrThrow(config.modResults);
29
+ const backupConfig = mainApplication.$['android:fullBackupContent']; // SDK <= 30
30
+ const dataExtractionRules = mainApplication.$['android:dataExtractionRules']; // SDK >= 31
31
+
32
+ if (!configureAndroidBackup) {
33
+ backupConfig === BACKUP_RULES_PATH && delete mainApplication.$['android:fullBackupContent'];
34
+ dataExtractionRules === EXTRACTION_RULES_PATH &&
35
+ delete mainApplication.$['android:dataExtractionRules'];
36
+ return config;
37
+ }
38
+
39
+ const canApplyBackupConfig = !backupConfig || backupConfig === BACKUP_RULES_PATH;
40
+ const canApplyDataExtractionRules =
41
+ !dataExtractionRules || dataExtractionRules === EXTRACTION_RULES_PATH;
42
+
43
+ if (canApplyBackupConfig && canApplyDataExtractionRules) {
44
+ mainApplication.$['android:fullBackupContent'] = BACKUP_RULES_PATH;
45
+ mainApplication.$['android:dataExtractionRules'] = EXTRACTION_RULES_PATH;
46
+ return config;
47
+ }
48
+
49
+ console.warn(
50
+ 'Expo-secure-store tried to apply Android Auto Backup rules, but other backup rules are already present. ' +
51
+ 'Refer to the expo-secure-store docs (https://docs.expo.dev/versions/latest/sdk/securestore/) to configure your backup rules.'
52
+ );
53
+ return config;
54
+ });
55
+ return config;
17
56
  };
18
57
 
19
58
  export default createRunOncePlugin(withSecureStore, pkg.name, pkg.version);
@@ -1,4 +1,5 @@
1
1
  import ExpoSecureStore from './ExpoSecureStore';
2
+ import { byteCountOverLimit, VALUE_BYTES_LIMIT } from './byteCounter';
2
3
 
3
4
  export type KeychainAccessibilityConstant = number;
4
5
 
@@ -22,6 +23,8 @@ export const AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY: KeychainAccessibilityConstant
22
23
  /**
23
24
  * The data in the keychain item can always be accessed regardless of whether the device is locked.
24
25
  * This is the least secure option.
26
+ *
27
+ * @deprecated Use an accessibility level that provides some user protection, such as `AFTER_FIRST_UNLOCK`.
25
28
  */
26
29
  export const ALWAYS: KeychainAccessibilityConstant = ExpoSecureStore.ALWAYS;
27
30
 
@@ -36,6 +39,8 @@ export const WHEN_PASSCODE_SET_THIS_DEVICE_ONLY: KeychainAccessibilityConstant =
36
39
  // @needsAudit
37
40
  /**
38
41
  * Similar to `ALWAYS`, except the entry is not migrated to a new device when restoring from a backup.
42
+ *
43
+ * @deprecated Use an accessibility level that provides some user protection, such as `AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY`.
39
44
  */
40
45
  export const ALWAYS_THIS_DEVICE_ONLY: KeychainAccessibilityConstant =
41
46
  ExpoSecureStore.ALWAYS_THIS_DEVICE_ONLY;
@@ -54,8 +59,6 @@ export const WHEN_UNLOCKED: KeychainAccessibilityConstant = ExpoSecureStore.WHEN
54
59
  export const WHEN_UNLOCKED_THIS_DEVICE_ONLY: KeychainAccessibilityConstant =
55
60
  ExpoSecureStore.WHEN_UNLOCKED_THIS_DEVICE_ONLY;
56
61
 
57
- const VALUE_BYTES_LIMIT = 2048;
58
-
59
62
  // @needsAudit
60
63
  export type SecureStoreOptions = {
61
64
  /**
@@ -69,10 +72,13 @@ export type SecureStoreOptions = {
69
72
  * accessing data stored in SecureStore.
70
73
  * - Android: Equivalent to [`setUserAuthenticationRequired(true)`](https://developer.android.com/reference/android/security/keystore/KeyGenParameterSpec.Builder#setUserAuthenticationRequired(boolean))
71
74
  * (requires API 23).
72
- * - iOS: Equivalent to [`kSecAccessControlBiometryCurrentSet`](https://developer.apple.com/documentation/security/secaccesscontrolcreateflags/ksecaccesscontrolbiometrycurrentset/).
75
+ * - iOS: Equivalent to [`biometryCurrentSet`](https://developer.apple.com/documentation/security/secaccesscontrolcreateflags/2937192-biometrycurrentset).
73
76
  * Complete functionality is unlocked only with a freshly generated key - this would not work in tandem with the `keychainService`
74
77
  * value used for the others non-authenticated operations.
75
78
  *
79
+ * This option works slightly differently across platforms: On Android, user authentication is required for all operations.
80
+ * On iOS, the user is prompted to authenticate only when reading or updating an existing value (not when creating a new one).
81
+ *
76
82
  * Warning: This option is not supported in Expo Go when biometric authentication is available due to a missing NSFaceIDUsageDescription.
77
83
  * In release builds or when using continuous native generation, make sure to use the `expo-secure-store` config plugin.
78
84
  *
@@ -110,7 +116,7 @@ export async function isAvailableAsync(): Promise<boolean> {
110
116
  * @param key The key that was used to store the associated value.
111
117
  * @param options An [`SecureStoreOptions`](#securestoreoptions) object.
112
118
  *
113
- * @return A promise that will reject if the value couldn't be deleted.
119
+ * @return A promise that rejects if the value can't be deleted.
114
120
  */
115
121
  export async function deleteItemAsync(
116
122
  key: string,
@@ -128,8 +134,8 @@ export async function deleteItemAsync(
128
134
  * @param key The key that was used to store the associated value.
129
135
  * @param options An [`SecureStoreOptions`](#securestoreoptions) object.
130
136
  *
131
- * @return A promise that resolves to the previously stored value. It will return `null` if there is no entry
132
- * for the given key or if the key has been invalidated. It will reject if an error occurs while retrieving the value.
137
+ * @return A promise that resolves to the previously stored value. It resolves with `null` if there is no entry
138
+ * for the given key or if the key has been invalidated. It rejects if an error occurs while retrieving the value.
133
139
  *
134
140
  * > Keys are invalidated by the system when biometrics change, such as adding a new fingerprint or changing the face profile used for face recognition.
135
141
  * > After a key has been invalidated, it becomes impossible to read its value.
@@ -151,7 +157,7 @@ export async function getItemAsync(
151
157
  * @param value The value to store. Size limit is 2048 bytes.
152
158
  * @param options An [`SecureStoreOptions`](#securestoreoptions) object.
153
159
  *
154
- * @return A promise that will reject if value cannot be stored on the device.
160
+ * @return A promise that rejects if value cannot be stored on the device.
155
161
  */
156
162
  export async function setItemAsync(
157
163
  key: string,
@@ -195,7 +201,8 @@ export function setItem(key: string, value: string, options: SecureStoreOptions
195
201
  * @param key The key that was used to store the associated value.
196
202
  * @param options An [`SecureStoreOptions`](#securestoreoptions) object.
197
203
  *
198
- * @return Previously stored value. It will return `null` if there is no entry for the given key or if the key has been invalidated.
204
+ * @return Previously stored value. It resolves with `null` if there is no entry
205
+ * for the given key or if the key has been invalidated.
199
206
  */
200
207
  export function getItem(key: string, options: SecureStoreOptions = {}): string | null {
201
208
  ensureValidKey(key);
@@ -226,36 +233,10 @@ function isValidValue(value: string) {
226
233
  if (typeof value !== 'string') {
227
234
  return false;
228
235
  }
229
- if (byteCount(value) > VALUE_BYTES_LIMIT) {
236
+ if (byteCountOverLimit(value, VALUE_BYTES_LIMIT)) {
230
237
  console.warn(
231
- 'Value being stored in SecureStore is larger than 2048 bytes and it may not be stored successfully. In a future SDK version, this call may throw an error.'
238
+ `Value being stored in SecureStore is larger than ${VALUE_BYTES_LIMIT} bytes and it may not be stored successfully. In a future SDK version, this call may throw an error.`
232
239
  );
233
240
  }
234
241
  return true;
235
242
  }
236
-
237
- // copy-pasted from https://stackoverflow.com/a/39488643
238
- function byteCount(value: string) {
239
- let bytes = 0;
240
-
241
- for (let i = 0; i < value.length; i++) {
242
- const codePoint = value.charCodeAt(i);
243
-
244
- // Lone surrogates cannot be passed to encodeURI
245
- if (codePoint >= 0xd800 && codePoint < 0xe000) {
246
- if (codePoint < 0xdc00 && i + 1 < value.length) {
247
- const next = value.charCodeAt(i + 1);
248
-
249
- if (next >= 0xdc00 && next < 0xe000) {
250
- bytes += 4;
251
- i++;
252
- continue;
253
- }
254
- }
255
- }
256
-
257
- bytes += codePoint < 0x80 ? 1 : codePoint < 0x800 ? 2 : 3;
258
- }
259
-
260
- return bytes;
261
- }
@@ -0,0 +1,34 @@
1
+ export const VALUE_BYTES_LIMIT = 2048;
2
+
3
+ // note this probably could be JS-engine dependent
4
+ // inspired by https://stackoverflow.com/a/39488643
5
+ export function byteCountOverLimit(value: string, limit: number): boolean {
6
+ let bytes = 0;
7
+
8
+ for (let i = 0; i < value.length; i++) {
9
+ const codePoint = value.charCodeAt(i);
10
+
11
+ // Lone surrogates cannot be passed to encodeURI
12
+ if (codePoint >= 0xd800 && codePoint < 0xe000) {
13
+ if (codePoint < 0xdc00 && i + 1 < value.length) {
14
+ const next = value.charCodeAt(i + 1);
15
+
16
+ if (next >= 0xdc00 && next < 0xe000) {
17
+ bytes += 4;
18
+ if (bytes > limit) {
19
+ return true;
20
+ }
21
+ i++;
22
+ continue;
23
+ }
24
+ }
25
+ }
26
+
27
+ bytes += codePoint < 0x80 ? 1 : codePoint < 0x800 ? 2 : 3;
28
+ if (bytes > limit) {
29
+ return true;
30
+ }
31
+ }
32
+
33
+ return bytes > limit;
34
+ }