expo-secure-store 13.0.1 → 13.0.2

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,13 @@
10
10
 
11
11
  ### 💡 Others
12
12
 
13
+ ## 13.0.2 — 2024-06-27
14
+
15
+ ### 🐛 Bug fixes
16
+
17
+ - [iOS] Improve error message for unhandled errors ([#29394](https://github.com/expo/expo/pull/29394) by [@hassankhan](https://github.com/hassankhan))
18
+ - [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))
19
+
13
20
  ## 13.0.1 — 2024-04-23
14
21
 
15
22
  _This version does not introduce any user-facing changes._
@@ -1,7 +1,7 @@
1
1
  apply plugin: 'com.android.library'
2
2
 
3
3
  group = 'host.exp.exponent'
4
- version = '13.0.1'
4
+ version = '13.0.2'
5
5
 
6
6
  def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
7
7
  apply from: expoModulesCorePlugin
@@ -14,7 +14,7 @@ android {
14
14
  namespace "expo.modules.securestore"
15
15
  defaultConfig {
16
16
  versionCode 17
17
- versionName '13.0.1'
17
+ versionName '13.0.2'
18
18
  }
19
19
  }
20
20
 
@@ -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,
@@ -55,6 +55,9 @@ internal class KeyChainException: GenericException<OSStatus> {
55
55
  return "Authentication failed. Provided passphrase/PIN is incorrect or there is no user authentication method configured for this device."
56
56
 
57
57
  default:
58
+ if let errorMessage = SecCopyErrorMessageString(param, nil) as? String {
59
+ return errorMessage
60
+ }
58
61
  return "Unknown Keychain Error."
59
62
  }
60
63
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-secure-store",
3
- "version": "13.0.1",
3
+ "version": "13.0.2",
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": "ee4f30ef3b5fa567ad1bf94794197f7683fdd481"
45
+ "gitHead": "09b2d97bbc0f70f7c811ff9b6c9ad8808c5ad84b"
46
46
  }