From 3416fee4eb2d4d4f9cc9f2876b2d8b3a7a0f8510 Mon Sep 17 00:00:00 2001 From: Jakub Korytko Date: Fri, 28 Nov 2025 16:08:16 +0100 Subject: [PATCH 1/7] Split expo-secure-store patch --- patches/expo-secure-store/details.md | 72 +- ...re+14.2.4+001+enable-device-fallback.patch | 388 ++++++++++ ...e-store+14.2.4+002+return-auth-type.patch} | 685 ++++-------------- ...2.4+003+force-authentication-on-save.patch | 129 ++++ ...cure-store+14.2.4+004+fail-on-update.patch | 99 +++ ...ce-read-authentication-on-simulators.patch | 78 ++ 6 files changed, 905 insertions(+), 546 deletions(-) create mode 100644 patches/expo-secure-store/expo-secure-store+14.2.4+001+enable-device-fallback.patch rename patches/expo-secure-store/{expo-secure-store+14.2.4+001+additional-config-options.patch => expo-secure-store+14.2.4+002+return-auth-type.patch} (57%) create mode 100644 patches/expo-secure-store/expo-secure-store+14.2.4+003+force-authentication-on-save.patch create mode 100644 patches/expo-secure-store/expo-secure-store+14.2.4+004+fail-on-update.patch create mode 100644 patches/expo-secure-store/expo-secure-store+14.2.4+005+force-read-authentication-on-simulators.patch diff --git a/patches/expo-secure-store/details.md b/patches/expo-secure-store/details.md index e9ef686113ea..aaf1bb1ab02f 100644 --- a/patches/expo-secure-store/details.md +++ b/patches/expo-secure-store/details.md @@ -1,22 +1,72 @@ # `expo-secure-store` patches -### [expo-secure-store+14.2.4+001+additional-config-options.patch](expo-secure-store+14.2.4+001+additional-config-options.patch) +### [expo-secure-store+14.2.4+001+enable-device-fallback.patch](expo-secure-store+14.2.4+001+enable-device-fallback.patch) - Reason: ``` - Expo SecureStore is a great library, but it lacks some of the functionality we need, i.e.: + We need to enable users to use any device screen lock instead of biometrics. + This is not enabled in the SecureStore, so this patch changes the required input to either biometrics or screen lock knowledge if the 'enableDeviceFallback' flag is set to true. + Additionally, support for screen locks can be checked using the new 'canUseDeviceCredentialsAuthentication' method. + ``` + +- Upstream PR/issue: TBA +- E/App issue: https://github.com/Expensify/App/issues/75225 +- PR introducing patch: https://github.com/Expensify/App/pull/76288 + +### [expo-secure-store+14.2.4+002+return-auth-type.patch](expo-secure-store+14.2.4+002+return-auth-type.patch) + +- Reason: + + ``` + This patch makes the read and write methods return the authentication type used to access the store. + It uses a pre-defined constant that mimics an enum and can also be imported directly from the app. + ``` + +- Upstream PR/issue: TBA +- E/App issue: https://github.com/Expensify/App/issues/75225 +- PR introducing patch: https://github.com/Expensify/App/pull/76288 + +### [expo-secure-store+14.2.4+003+force-authentication-on-save.patch](expo-secure-store+14.2.4+003+force-authentication-on-save.patch) + +- Reason: + + ``` + The iOS does not require authentication when a value is saved to the keychain. We cannot force iOS to do so. + However, to maintain consistency with Android, setting the 'forceAuthenticationOnSave' flag to true results in a prompt appearing + before the value is saved. This only works in the asynchronous version of the save method. + ``` - - Returning the type of authentication used for accessing the store - - Allowing the fallback to device credentials - - Enabling use of device credentials if no biometrics are enrolled/supported. - - Enforcing iOS to always ask for the authentication when accessing the store (by default iOS skips the authentication if, for example, user unlocked the screen recently with biometrics) - - Letting user know if the key entry is already stored instead of overwriting it without any feedback +- Upstream PR/issue: TBA +- E/App issue: https://github.com/Expensify/App/issues/75225 +- PR introducing patch: https://github.com/Expensify/App/pull/76288 +### [expo-secure-store+14.2.4+004+fail-on-update.patch](expo-secure-store+14.2.4+004+fail-on-update.patch) + +- Reason: - This is fixed with this patch, but instead of hardcoding solution for above needs, the patch adds it into the SecureStore options ``` + If a value already exists in the SecureStore and a new one is saved, the existing value is simply overwritten. + To avoid unexpected behaviour, set the 'failOnUpdate' flag to true to trigger an error when the given key is already in the store. + ``` + +- Upstream PR/issue: TBA +- E/App issue: https://github.com/Expensify/App/issues/75225 +- PR introducing patch: https://github.com/Expensify/App/pull/76288 + +### [expo-secure-store+14.2.4+005+force-read-authentication-on-simulators.patch](expo-secure-store+14.2.4+005+force-read-authentication-on-simulators.patch) + +- Reason: + + ``` + The LocalAuthentication behaves slightly differently on iOS simulators. + In numerous cases, the authentication prompts are skipped on simulators (as opposed to real devices). + Setting 'forceReadAuthenticationOnSimulators' flag to true forces the prompt to appear on simulators when a value with the `requireAuthentication` flag set to true is read. + The flag is added is purely for testing the app on simulators, in cases where the prompt does not appear when the value is read. + It has no effect on real devices. + ``` + +- Upstream PR/issue: TBA +- E/App issue: https://github.com/Expensify/App/issues/75225 +- PR introducing patch: https://github.com/Expensify/App/pull/76288 -- Upstream PR/issue: 🛑 -- E/App issue: No issue, this patch adjust the library for our needs. -- PR introducing patch: https://github.com/Expensify/App/pull/69863 \ No newline at end of file diff --git a/patches/expo-secure-store/expo-secure-store+14.2.4+001+enable-device-fallback.patch b/patches/expo-secure-store/expo-secure-store+14.2.4+001+enable-device-fallback.patch new file mode 100644 index 000000000000..9a0b432e66d9 --- /dev/null +++ b/patches/expo-secure-store/expo-secure-store+14.2.4+001+enable-device-fallback.patch @@ -0,0 +1,388 @@ +diff --git a/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/AuthenticationHelper.kt b/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/AuthenticationHelper.kt +index a281b88..4a1a009 100644 +--- a/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/AuthenticationHelper.kt ++++ b/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/AuthenticationHelper.kt +@@ -2,6 +2,7 @@ package expo.modules.securestore + + import android.annotation.SuppressLint + import android.app.Activity ++import android.app.KeyguardManager + import android.content.Context + import android.os.Build + import androidx.biometric.BiometricManager +@@ -19,9 +20,9 @@ class AuthenticationHelper( + ) { + private var isAuthenticating = false + +- suspend fun authenticateCipher(cipher: Cipher, requiresAuthentication: Boolean, title: String): Cipher { ++ suspend fun authenticateCipher(cipher: Cipher, requiresAuthentication: Boolean, title: String, enableDeviceFallback: Boolean): Cipher { + if (requiresAuthentication) { +- return openAuthenticationPrompt(cipher, title).cryptoObject?.cipher ++ return openAuthenticationPrompt(cipher, title, enableDeviceFallback).cryptoObject?.cipher + ?: throw AuthenticationException("Couldn't get cipher from authentication result") + } + return cipher +@@ -29,7 +30,8 @@ class AuthenticationHelper( + + private suspend fun openAuthenticationPrompt( + cipher: Cipher, +- title: String ++ title: String, ++ enableDeviceFallback: Boolean + ): BiometricPrompt.AuthenticationResult { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + throw AuthenticationException("Biometric authentication requires Android API 23") +@@ -41,11 +43,16 @@ class AuthenticationHelper( + isAuthenticating = true + + try { +- assertBiometricsSupport() ++ if (enableDeviceFallback) { ++ assertDeviceSecurity() ++ } else { ++ assertBiometricsSupport() ++ } ++ + val fragmentActivity = getCurrentActivity() as? FragmentActivity + ?: throw AuthenticationException("Cannot display biometric prompt when the app is not in the foreground") + +- val authenticationPrompt = AuthenticationPrompt(fragmentActivity, context, title) ++ val authenticationPrompt = AuthenticationPrompt(fragmentActivity, context, title, enableDeviceFallback) + + return withContext(Dispatchers.Main.immediate) { + return@withContext authenticationPrompt.authenticate(cipher) +@@ -56,6 +63,14 @@ class AuthenticationHelper( + } + } + ++ fun assertDeviceSecurity() { ++ val manager = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager ++ val isSecure = manager.isDeviceSecure ++ if (!isSecure) { ++ throw AuthenticationException("No authentication method available") ++ } ++ } ++ + fun assertBiometricsSupport() { + val biometricManager = BiometricManager.from(context) + @SuppressLint("SwitchIntDef") // BiometricManager.BIOMETRIC_SUCCESS shouldn't do anything +diff --git a/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/AuthenticationPrompt.kt b/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/AuthenticationPrompt.kt +index e5729cc..2543b0c 100644 +--- a/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/AuthenticationPrompt.kt ++++ b/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/AuthenticationPrompt.kt +@@ -1,5 +1,7 @@ + package expo.modules.securestore + ++import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG ++import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL + import android.content.Context + import androidx.biometric.BiometricPrompt + import androidx.biometric.BiometricPrompt.PromptInfo +@@ -11,11 +13,12 @@ import kotlin.coroutines.resume + import kotlin.coroutines.resumeWithException + import kotlin.coroutines.suspendCoroutine + +-class AuthenticationPrompt(private val currentActivity: FragmentActivity, context: Context, title: String) { ++class AuthenticationPrompt(private val currentActivity: FragmentActivity, context: Context, title: String, enableDeviceFallback: Boolean) { ++ private var authType: Int = if (enableDeviceFallback) BIOMETRIC_STRONG or DEVICE_CREDENTIAL else BIOMETRIC_STRONG + private var executor: Executor = ContextCompat.getMainExecutor(context) + private var promptInfo = PromptInfo.Builder() + .setTitle(title) +- .setNegativeButtonText(context.getString(android.R.string.cancel)) ++ .setAllowedAuthenticators(authType) + .build() + + suspend fun authenticate(cipher: Cipher): BiometricPrompt.AuthenticationResult? = +diff --git a/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/SecureStoreModule.kt b/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/SecureStoreModule.kt +index 0fef884..3f21552 100644 +--- a/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/SecureStoreModule.kt ++++ b/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/SecureStoreModule.kt +@@ -75,6 +75,15 @@ open class SecureStoreModule : Module() { + } + } + ++ Function("canUseDeviceCredentialsAuthentication") { ++ return@Function try { ++ authenticationHelper.assertDeviceSecurity() ++ true ++ } catch (e: AuthenticationException) { ++ false ++ } ++ } ++ + OnCreate { + authenticationHelper = AuthenticationHelper(reactContext, appContext.legacyModuleRegistry) + hybridAESEncryptor = HybridAESEncryptor(reactContext, mAESEncryptor) +@@ -201,7 +210,7 @@ open class SecureStoreModule : Module() { + back a value. + */ + val secretKeyEntry: SecretKeyEntry = getOrCreateKeyEntry(SecretKeyEntry::class.java, mAESEncryptor, options, options.requireAuthentication) +- val encryptedItem = mAESEncryptor.createEncryptedItem(value, secretKeyEntry, options.requireAuthentication, options.authenticationPrompt, authenticationHelper) ++ val encryptedItem = mAESEncryptor.createEncryptedItem(value, secretKeyEntry, options.requireAuthentication, options.authenticationPrompt, authenticationHelper, options.enableDeviceFallback) + encryptedItem.put(SCHEME_PROPERTY, AESEncryptor.NAME) + saveEncryptedItem(encryptedItem, prefs, keychainAwareKey, options.requireAuthentication, options.keychainService) + +@@ -343,7 +352,11 @@ open class SecureStoreModule : Module() { + return getKeyEntry(keyStoreEntryClass, encryptor, options, requireAuthentication) ?: run { + // Android won't allow us to generate the keys if the device doesn't support biometrics or no biometrics are enrolled + if (requireAuthentication) { +- authenticationHelper.assertBiometricsSupport() ++ if (options.enableDeviceFallback) { ++ authenticationHelper.assertDeviceSecurity() ++ } else { ++ authenticationHelper.assertBiometricsSupport() ++ } + } + encryptor.initializeKeyStoreEntry(keyStore, options) + } +diff --git a/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/SecureStoreOptions.kt b/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/SecureStoreOptions.kt +index 79a600f..0455fd6 100644 +--- a/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/SecureStoreOptions.kt ++++ b/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/SecureStoreOptions.kt +@@ -8,5 +8,6 @@ class SecureStoreOptions( + // Prompt can't be an empty string + @Field var authenticationPrompt: String = " ", + @Field var keychainService: String = SecureStoreModule.DEFAULT_KEYSTORE_ALIAS, +- @Field var requireAuthentication: Boolean = false ++ @Field var requireAuthentication: Boolean = false, ++ @Field var enableDeviceFallback: Boolean = false + ) : Record, Serializable +diff --git a/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/encryptors/AESEncryptor.kt b/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/encryptors/AESEncryptor.kt +index 3a12dc9..1cd187f 100644 +--- a/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/encryptors/AESEncryptor.kt ++++ b/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/encryptors/AESEncryptor.kt +@@ -1,9 +1,10 @@ + package expo.modules.securestore.encryptors + +-import android.annotation.TargetApi ++import android.os.Build + import android.security.keystore.KeyGenParameterSpec + import android.security.keystore.KeyProperties + import android.util.Base64 ++import androidx.annotation.RequiresApi + import expo.modules.securestore.AuthenticationHelper + import expo.modules.securestore.DecryptException + import expo.modules.securestore.SecureStoreModule +@@ -50,17 +51,22 @@ class AESEncryptor : KeyBasedEncryptor { + return "${getKeyStoreAlias(options)}:$suffix" + } + +- @TargetApi(23) ++ @RequiresApi(Build.VERSION_CODES.R) + @Throws(GeneralSecurityException::class) + override fun initializeKeyStoreEntry(keyStore: KeyStore, options: SecureStoreOptions): KeyStore.SecretKeyEntry { + val extendedKeystoreAlias = getExtendedKeyStoreAlias(options, options.requireAuthentication) + val keyPurposes = KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT + ++ val authType = ++ if (options.enableDeviceFallback) KeyProperties.AUTH_BIOMETRIC_STRONG or KeyProperties.AUTH_DEVICE_CREDENTIAL ++ else KeyProperties.AUTH_BIOMETRIC_STRONG ++ + val algorithmSpec: AlgorithmParameterSpec = KeyGenParameterSpec.Builder(extendedKeystoreAlias, keyPurposes) + .setKeySize(AES_KEY_SIZE_BITS) + .setBlockModes(KeyProperties.BLOCK_MODE_GCM) + .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) + .setUserAuthenticationRequired(options.requireAuthentication) ++ .setUserAuthenticationParameters(0, authType) + .build() + + val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, keyStore.provider) +@@ -78,14 +84,15 @@ class AESEncryptor : KeyBasedEncryptor { + keyStoreEntry: KeyStore.SecretKeyEntry, + requireAuthentication: Boolean, + authenticationPrompt: String, +- authenticationHelper: AuthenticationHelper ++ authenticationHelper: AuthenticationHelper, ++ enableDeviceFallback: Boolean, + ): JSONObject { + val secretKey = keyStoreEntry.secretKey + val cipher = Cipher.getInstance(AES_CIPHER) + cipher.init(Cipher.ENCRYPT_MODE, secretKey) + + val gcmSpec = cipher.parameters.getParameterSpec(GCMParameterSpec::class.java) +- val authenticatedCipher = authenticationHelper.authenticateCipher(cipher, requireAuthentication, authenticationPrompt) ++ val authenticatedCipher = authenticationHelper.authenticateCipher(cipher, requireAuthentication, authenticationPrompt, enableDeviceFallback) + + return createEncryptedItemWithCipher(plaintextValue, authenticatedCipher, gcmSpec) + } +@@ -128,7 +135,7 @@ class AESEncryptor : KeyBasedEncryptor { + throw DecryptException("Authentication tag length must be at least $MIN_GCM_AUTHENTICATION_TAG_LENGTH bits long", key, options.keychainService) + } + cipher.init(Cipher.DECRYPT_MODE, keyStoreEntry.secretKey, gcmSpec) +- val unlockedCipher = authenticationHelper.authenticateCipher(cipher, requiresAuthentication, options.authenticationPrompt) ++ val unlockedCipher = authenticationHelper.authenticateCipher(cipher, requiresAuthentication, options.authenticationPrompt, options.enableDeviceFallback) + return String(unlockedCipher.doFinal(ciphertextBytes), StandardCharsets.UTF_8) + } + +diff --git a/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/encryptors/HybridAESEncryptor.kt b/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/encryptors/HybridAESEncryptor.kt +index fb42599..5f8bbfd 100644 +--- a/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/encryptors/HybridAESEncryptor.kt ++++ b/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/encryptors/HybridAESEncryptor.kt +@@ -69,7 +69,8 @@ class HybridAESEncryptor(private var mContext: Context, private val mAESEncrypto + keyStoreEntry: KeyStore.PrivateKeyEntry, + requireAuthentication: Boolean, + authenticationPrompt: String, +- authenticationHelper: AuthenticationHelper ++ authenticationHelper: AuthenticationHelper, ++ enableDeviceFallback: Boolean, + ): JSONObject { + // This should never be called after we dropped Android SDK 22 support. + throw EncryptException( +diff --git a/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/encryptors/KeyBasedEncryptor.kt b/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/encryptors/KeyBasedEncryptor.kt +index e493467..39459ff 100644 +--- a/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/encryptors/KeyBasedEncryptor.kt ++++ b/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/encryptors/KeyBasedEncryptor.kt +@@ -25,7 +25,8 @@ interface KeyBasedEncryptor { + keyStoreEntry: E, + requireAuthentication: Boolean, + authenticationPrompt: String, +- authenticationHelper: AuthenticationHelper ++ authenticationHelper: AuthenticationHelper, ++ enableDeviceFallback: Boolean, + ): JSONObject + + @Throws(GeneralSecurityException::class, JSONException::class) +diff --git a/node_modules/expo-secure-store/build/SecureStore.d.ts b/node_modules/expo-secure-store/build/SecureStore.d.ts +index d5cd157..be9acfe 100644 +--- a/node_modules/expo-secure-store/build/SecureStore.d.ts ++++ b/node_modules/expo-secure-store/build/SecureStore.d.ts +@@ -78,6 +78,16 @@ export type SecureStoreOptions = { + * @platform ios + */ + accessGroup?: string; ++ /** ++ * This flag enables users to authenticate using Lock Screen Knowledge Factor (e.g. PIN, pattern or password). ++ * For sensitive apps, it is recommended not having biometric fall back to such factor. ++ * @see: https://developer.android.com/security/fraud-prevention/authentication ++ * ++ * @default false ++ * @platform android ++ * @platform ios ++ */ ++ enableDeviceFallback?: boolean; + }; + /** + * Returns whether the SecureStore API is enabled on the current device. This does not check the app +@@ -148,4 +158,11 @@ export declare function getItem(key: string, options?: SecureStoreOptions): stri + * @platform ios + */ + export declare function canUseBiometricAuthentication(): boolean; ++/** ++ * Checks whether any device credentials are configured on the device. ++ * @return `true` if the device has device credentials configured. Otherwise, returns `false`. ++ * @platform android ++ * @platform ios ++ */ ++export declare function canUseDeviceCredentialsAuthentication(): boolean; + //# sourceMappingURL=SecureStore.d.ts.map +diff --git a/node_modules/expo-secure-store/build/SecureStore.js b/node_modules/expo-secure-store/build/SecureStore.js +index 4d87b38..e11bd94 100644 +--- a/node_modules/expo-secure-store/build/SecureStore.js ++++ b/node_modules/expo-secure-store/build/SecureStore.js +@@ -143,6 +143,15 @@ export function getItem(key, options = {}) { + export function canUseBiometricAuthentication() { + return ExpoSecureStore.canUseBiometricAuthentication(); + } ++/** ++ * Checks whether any device credentials are configured on the device. ++ * @return `true` if the device has device credentials configured. Otherwise, returns `false`. ++ * @platform android ++ * @platform ios ++ */ ++export function canUseDeviceCredentialsAuthentication() { ++ return ExpoSecureStore.canUseDeviceCredentialsAuthentication(); ++} + function ensureValidKey(key) { + if (!isValidKey(key)) { + throw new Error(`Invalid key provided to SecureStore. Keys must not be empty and contain only alphanumeric characters, ".", "-", and "_".`); +diff --git a/node_modules/expo-secure-store/ios/SecureStoreModule.swift b/node_modules/expo-secure-store/ios/SecureStoreModule.swift +index 439b08d..1268979 100644 +--- a/node_modules/expo-secure-store/ios/SecureStoreModule.swift ++++ b/node_modules/expo-secure-store/ios/SecureStoreModule.swift +@@ -66,6 +66,14 @@ public final class SecureStoreModule: Module { + return isBiometricsSupported + #endif + } ++ ++ Function("canUseDeviceCredentialsAuthentication") { () -> Bool in ++ return areDeviceCredentialsEnabled() ++ } ++ } ++ ++ private func areDeviceCredentialsEnabled() -> Bool { ++ return LAContext().canEvaluatePolicy(LAPolicy.deviceOwnerAuthentication, error: nil) + } + + private func get(with key: String, options: SecureStoreOptions) throws -> String? { +@@ -99,12 +107,17 @@ public final class SecureStoreModule: Module { + if !options.requireAuthentication { + setItemQuery[kSecAttrAccessible as String] = accessibility + } else { +- guard let _ = Bundle.main.infoDictionary?["NSFaceIDUsageDescription"] as? String else { +- throw MissingPlistKeyException() ++ if (!options.enableDeviceFallback) { ++ guard let _ = Bundle.main.infoDictionary?["NSFaceIDUsageDescription"] as? String else { ++ throw MissingPlistKeyException() ++ } + } + + var error: Unmanaged? = nil +- guard let accessOptions = SecAccessControlCreateWithFlags(kCFAllocatorDefault, accessibility, .biometryCurrentSet, &error) else { ++ ++ let accessControlFlag: SecAccessControlCreateFlags = options.enableDeviceFallback ? .userPresence : .biometryCurrentSet ++ ++ guard let accessOptions = SecAccessControlCreateWithFlags(kCFAllocatorDefault, accessibility, accessControlFlag, &error) else { + let errorCode = error.map { CFErrorGetCode($0.takeRetainedValue()) } + throw SecAccessControlError(errorCode) + } +diff --git a/node_modules/expo-secure-store/ios/SecureStoreOptions.swift b/node_modules/expo-secure-store/ios/SecureStoreOptions.swift +index 7e3fa4d..c9e843b 100644 +--- a/node_modules/expo-secure-store/ios/SecureStoreOptions.swift ++++ b/node_modules/expo-secure-store/ios/SecureStoreOptions.swift +@@ -15,4 +15,7 @@ internal struct SecureStoreOptions: Record { + + @Field + var accessGroup: String? ++ ++ @Field ++ var enableDeviceFallback: Bool = false + } +diff --git a/node_modules/expo-secure-store/src/SecureStore.ts b/node_modules/expo-secure-store/src/SecureStore.ts +index ee43e04..4f253a1 100644 +--- a/node_modules/expo-secure-store/src/SecureStore.ts ++++ b/node_modules/expo-secure-store/src/SecureStore.ts +@@ -102,6 +102,17 @@ export type SecureStoreOptions = { + * @platform ios + */ + accessGroup?: string; ++ ++ /** ++ * This flag enables users to authenticate using Lock Screen Knowledge Factor (e.g. PIN, pattern or password). ++ * For sensitive apps, it is recommended not having biometric fall back to such factor. ++ * @see: https://developer.android.com/security/fraud-prevention/authentication ++ * ++ * @default false ++ * @platform android ++ * @platform ios ++ */ ++ enableDeviceFallback?: boolean; + }; + + // @needsAudit +@@ -226,6 +237,16 @@ export function canUseBiometricAuthentication(): boolean { + return ExpoSecureStore.canUseBiometricAuthentication(); + } + ++/** ++ * Checks whether any device credentials are configured on the device. ++ * @return `true` if the device has device credentials configured. Otherwise, returns `false`. ++ * @platform android ++ * @platform ios ++ */ ++export function canUseDeviceCredentialsAuthentication(): boolean { ++ return ExpoSecureStore.canUseDeviceCredentialsAuthentication(); ++} ++ + function ensureValidKey(key: string) { + if (!isValidKey(key)) { + throw new Error( diff --git a/patches/expo-secure-store/expo-secure-store+14.2.4+001+additional-config-options.patch b/patches/expo-secure-store/expo-secure-store+14.2.4+002+return-auth-type.patch similarity index 57% rename from patches/expo-secure-store/expo-secure-store+14.2.4+001+additional-config-options.patch rename to patches/expo-secure-store/expo-secure-store+14.2.4+002+return-auth-type.patch index d766eb5c0716..6a0171d3e5d0 100644 --- a/patches/expo-secure-store/expo-secure-store+14.2.4+001+additional-config-options.patch +++ b/patches/expo-secure-store/expo-secure-store+14.2.4+002+return-auth-type.patch @@ -1,25 +1,17 @@ diff --git a/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/AuthenticationHelper.kt b/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/AuthenticationHelper.kt -index a281b88..dc84eb4 100644 +index 4a1a009..980ef0a 100644 --- a/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/AuthenticationHelper.kt +++ b/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/AuthenticationHelper.kt -@@ -2,6 +2,7 @@ package expo.modules.securestore - - import android.annotation.SuppressLint - import android.app.Activity -+import android.app.KeyguardManager - import android.content.Context - import android.os.Build - import androidx.biometric.BiometricManager -@@ -19,17 +20,18 @@ class AuthenticationHelper( +@@ -20,12 +20,12 @@ class AuthenticationHelper( ) { private var isAuthenticating = false -- suspend fun authenticateCipher(cipher: Cipher, requiresAuthentication: Boolean, title: String): Cipher { +- suspend fun authenticateCipher(cipher: Cipher, requiresAuthentication: Boolean, title: String, enableDeviceFallback: Boolean): Cipher { - if (requiresAuthentication) { -- return openAuthenticationPrompt(cipher, title).cryptoObject?.cipher +- return openAuthenticationPrompt(cipher, title, enableDeviceFallback).cryptoObject?.cipher - ?: throw AuthenticationException("Couldn't get cipher from authentication result") -+ suspend fun authenticateCipher(cipher: Cipher, title: String, enableCredentialsAlternative: Boolean): BiometricPrompt.AuthenticationResult { -+ val promptResult = openAuthenticationPrompt(cipher, title, enableCredentialsAlternative) ++ suspend fun authenticateCipher(cipher: Cipher, title: String, enableDeviceFallback: Boolean): BiometricPrompt.AuthenticationResult { ++ val promptResult = openAuthenticationPrompt(cipher, title, enableDeviceFallback) + if (promptResult.cryptoObject?.cipher == null) { + throw AuthenticationException("Couldn't get cipher from authentication result") } @@ -28,76 +20,8 @@ index a281b88..dc84eb4 100644 } private suspend fun openAuthenticationPrompt( - cipher: Cipher, -- title: String -+ title: String, -+ enableCredentialsAlternative: Boolean - ): BiometricPrompt.AuthenticationResult { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { - throw AuthenticationException("Biometric authentication requires Android API 23") -@@ -41,11 +43,16 @@ class AuthenticationHelper( - isAuthenticating = true - - try { -- assertBiometricsSupport() -+ if (enableCredentialsAlternative) { -+ assertDeviceSecurity() -+ } else { -+ assertBiometricsSupport() -+ } -+ - val fragmentActivity = getCurrentActivity() as? FragmentActivity - ?: throw AuthenticationException("Cannot display biometric prompt when the app is not in the foreground") - -- val authenticationPrompt = AuthenticationPrompt(fragmentActivity, context, title) -+ val authenticationPrompt = AuthenticationPrompt(fragmentActivity, context, title, enableCredentialsAlternative) - - return withContext(Dispatchers.Main.immediate) { - return@withContext authenticationPrompt.authenticate(cipher) -@@ -56,6 +63,14 @@ class AuthenticationHelper( - } - } - -+ fun assertDeviceSecurity() { -+ val manager = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager -+ val isSecure = manager.isDeviceSecure -+ if (!isSecure) { -+ throw AuthenticationException("No authentication method available") -+ } -+ } -+ - fun assertBiometricsSupport() { - val biometricManager = BiometricManager.from(context) - @SuppressLint("SwitchIntDef") // BiometricManager.BIOMETRIC_SUCCESS shouldn't do anything -diff --git a/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/AuthenticationPrompt.kt b/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/AuthenticationPrompt.kt -index e5729cc..0fd4242 100644 ---- a/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/AuthenticationPrompt.kt -+++ b/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/AuthenticationPrompt.kt -@@ -1,5 +1,7 @@ - package expo.modules.securestore - -+import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG -+import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL - import android.content.Context - import androidx.biometric.BiometricPrompt - import androidx.biometric.BiometricPrompt.PromptInfo -@@ -11,11 +13,12 @@ import kotlin.coroutines.resume - import kotlin.coroutines.resumeWithException - import kotlin.coroutines.suspendCoroutine - --class AuthenticationPrompt(private val currentActivity: FragmentActivity, context: Context, title: String) { -+class AuthenticationPrompt(private val currentActivity: FragmentActivity, context: Context, title: String, enableCredentialsAlternative: Boolean) { -+ private var authType: Int = if (enableCredentialsAlternative) BIOMETRIC_STRONG or DEVICE_CREDENTIAL else BIOMETRIC_STRONG - private var executor: Executor = ContextCompat.getMainExecutor(context) - private var promptInfo = PromptInfo.Builder() - .setTitle(title) -- .setNegativeButtonText(context.getString(android.R.string.cancel)) -+ .setAllowedAuthenticators(authType) - .build() - - suspend fun authenticate(cipher: Cipher): BiometricPrompt.AuthenticationResult? = diff --git a/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/SecureStoreModule.kt b/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/SecureStoreModule.kt -index 0fef884..822caa5 100644 +index 3f21552..866d47d 100644 --- a/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/SecureStoreModule.kt +++ b/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/SecureStoreModule.kt @@ -36,23 +36,23 @@ open class SecureStoreModule : Module() { @@ -128,7 +52,7 @@ index 0fef884..822caa5 100644 } } -@@ -85,7 +85,7 @@ open class SecureStoreModule : Module() { +@@ -94,7 +94,7 @@ open class SecureStoreModule : Module() { } } @@ -137,7 +61,7 @@ index 0fef884..822caa5 100644 // We use a SecureStore-specific shared preferences file, which lets us do things like enumerate // its entries or clear all of them val prefs: SharedPreferences = getSharedPreferences() -@@ -95,10 +95,10 @@ open class SecureStoreModule : Module() { +@@ -104,10 +104,10 @@ open class SecureStoreModule : Module() { } else if (prefs.contains(key)) { // For backwards-compatibility try to read using the old key format return readJSONEncodedItem(key, prefs, options) } @@ -150,7 +74,7 @@ index 0fef884..822caa5 100644 val keychainAwareKey = createKeychainAwareKey(key, options.keychainService) val legacyEncryptedItemString = prefs.getString(key, null) -@@ -117,7 +117,7 @@ open class SecureStoreModule : Module() { +@@ -126,7 +126,7 @@ open class SecureStoreModule : Module() { "" } @@ -159,28 +83,23 @@ index 0fef884..822caa5 100644 val encryptedItem: JSONObject = try { JSONObject(encryptedItemString) -@@ -140,22 +140,26 @@ open class SecureStoreModule : Module() { +@@ -149,13 +149,13 @@ open class SecureStoreModule : Module() { "This situation occurs when the app is reinstalled. The value will be removed to avoid future errors. Returning null" ) deleteItemImpl(key, options) - return null + return SecureStoreFeedback(null) } -+ return mAESEncryptor.decryptItem(key, encryptedItem, secretKeyEntry, options, authenticationHelper) } -+ HybridAESEncryptor.NAME -> { val privateKeyEntry = getKeyEntryCompat(PrivateKeyEntry::class.java, hybridAESEncryptor, options, requireAuthentication, usesKeystoreSuffix) - ?: return null + ?: return SecureStoreFeedback(null) -+ return hybridAESEncryptor.decryptItem(key, encryptedItem, privateKeyEntry, options, authenticationHelper) } -+ else -> { - throw DecryptException("The item for key $key in SecureStore has an unknown encoding scheme $scheme)", key, options.keychainService) - } +@@ -164,7 +164,7 @@ open class SecureStoreModule : Module() { } } catch (e: KeyPermanentlyInvalidatedException) { Log.w(TAG, "The requested key has been permanently invalidated. Returning null") @@ -189,7 +108,7 @@ index 0fef884..822caa5 100644 } catch (e: BadPaddingException) { // 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. // 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. -@@ -165,7 +169,7 @@ open class SecureStoreModule : Module() { +@@ -174,7 +174,7 @@ open class SecureStoreModule : Module() { "The entry in shared preferences is out of sync with the keystore. It will be removed, returning null." ) deleteItemImpl(key, options) @@ -198,7 +117,7 @@ index 0fef884..822caa5 100644 } catch (e: GeneralSecurityException) { throw (DecryptException(e.message, key, options.keychainService, e)) } catch (e: CodedException) { -@@ -175,7 +179,7 @@ open class SecureStoreModule : Module() { +@@ -184,7 +184,7 @@ open class SecureStoreModule : Module() { } } @@ -207,34 +126,26 @@ index 0fef884..822caa5 100644 val keychainAwareKey = createKeychainAwareKey(key, options.keychainService) val prefs: SharedPreferences = getSharedPreferences() -@@ -184,7 +188,11 @@ open class SecureStoreModule : Module() { +@@ -193,7 +193,7 @@ open class SecureStoreModule : Module() { if (!success) { throw WriteException("Could not write a null value to SecureStore", key, options.keychainService) } - return + return SecureStoreAuthType.NONE -+ } -+ -+ if (prefs.contains(keychainAwareKey) && options.failOnDuplicate) { -+ throw WriteException("Key already exists", key, options.keychainService) } try { -@@ -200,8 +208,11 @@ open class SecureStoreModule : Module() { - use in the encrypted JSON item so that we know how to decode and decrypt it when reading +@@ -210,7 +210,8 @@ open class SecureStoreModule : Module() { back a value. */ -- val secretKeyEntry: SecretKeyEntry = getOrCreateKeyEntry(SecretKeyEntry::class.java, mAESEncryptor, options, options.requireAuthentication) -- val encryptedItem = mAESEncryptor.createEncryptedItem(value, secretKeyEntry, options.requireAuthentication, options.authenticationPrompt, authenticationHelper) -+ val secretKeyEntry: SecretKeyEntry = getOrCreateKeyEntry( -+ SecretKeyEntry::class.java, mAESEncryptor, options, options.requireAuthentication -+ ) -+ val encryptResult = mAESEncryptor.createEncryptedItem(value, secretKeyEntry, options.requireAuthentication, options.authenticationPrompt, authenticationHelper, options.enableCredentialsAlternative) + val secretKeyEntry: SecretKeyEntry = getOrCreateKeyEntry(SecretKeyEntry::class.java, mAESEncryptor, options, options.requireAuthentication) +- val encryptedItem = mAESEncryptor.createEncryptedItem(value, secretKeyEntry, options.requireAuthentication, options.authenticationPrompt, authenticationHelper, options.enableDeviceFallback) ++ val encryptResult = mAESEncryptor.createEncryptedItem(value, secretKeyEntry, options.requireAuthentication, options.authenticationPrompt, authenticationHelper, options.enableDeviceFallback) + val encryptedItem = encryptResult.value encryptedItem.put(SCHEME_PROPERTY, AESEncryptor.NAME) saveEncryptedItem(encryptedItem, prefs, keychainAwareKey, options.requireAuthentication, options.keychainService) -@@ -209,6 +220,9 @@ open class SecureStoreModule : Module() { +@@ -218,6 +219,9 @@ open class SecureStoreModule : Module() { if (prefs.contains(key)) { prefs.edit().remove(key).apply() } @@ -244,21 +155,8 @@ index 0fef884..822caa5 100644 } catch (e: KeyPermanentlyInvalidatedException) { if (!keyIsInvalidated) { Log.w(TAG, "Key has been invalidated, retrying with the key deleted") -@@ -343,7 +357,11 @@ open class SecureStoreModule : Module() { - return getKeyEntry(keyStoreEntryClass, encryptor, options, requireAuthentication) ?: run { - // Android won't allow us to generate the keys if the device doesn't support biometrics or no biometrics are enrolled - if (requireAuthentication) { -- authenticationHelper.assertBiometricsSupport() -+ if (options.enableCredentialsAlternative) { -+ authenticationHelper.assertDeviceSecurity() -+ } else { -+ authenticationHelper.assertBiometricsSupport() -+ } - } - encryptor.initializeKeyStoreEntry(keyStore, options) - } diff --git a/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/SecureStoreOptions.kt b/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/SecureStoreOptions.kt -index 79a600f..ad78439 100644 +index 0455fd6..d38650c 100644 --- a/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/SecureStoreOptions.kt +++ b/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/SecureStoreOptions.kt @@ -1,5 +1,6 @@ @@ -268,14 +166,9 @@ index 79a600f..ad78439 100644 import expo.modules.kotlin.records.Field import expo.modules.kotlin.records.Record import java.io.Serializable -@@ -8,5 +9,39 @@ class SecureStoreOptions( - // Prompt can't be an empty string - @Field var authenticationPrompt: String = " ", - @Field var keychainService: String = SecureStoreModule.DEFAULT_KEYSTORE_ALIAS, -- @Field var requireAuthentication: Boolean = false -+ @Field var requireAuthentication: Boolean = false, -+ @Field var failOnDuplicate: Boolean = false, -+ @Field var enableCredentialsAlternative: Boolean = false +@@ -11,3 +12,35 @@ class SecureStoreOptions( + @Field var requireAuthentication: Boolean = false, + @Field var enableDeviceFallback: Boolean = false ) : Record, Serializable + +enum class SecureStoreAuthType(index: Int) { @@ -309,20 +202,14 @@ index 79a600f..ad78439 100644 + /** Used to return easily convertible values to JS code */ + @Field var values: Pair = Pair(value, authType.ordinal) +} -\ No newline at end of file diff --git a/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/encryptors/AESEncryptor.kt b/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/encryptors/AESEncryptor.kt -index 3a12dc9..d5b797b 100644 +index 1cd187f..44c84c3 100644 --- a/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/encryptors/AESEncryptor.kt +++ b/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/encryptors/AESEncryptor.kt -@@ -1,11 +1,14 @@ - package expo.modules.securestore.encryptors - --import android.annotation.TargetApi -+import android.os.Build - import android.security.keystore.KeyGenParameterSpec +@@ -5,8 +5,10 @@ import android.security.keystore.KeyGenParameterSpec import android.security.keystore.KeyProperties import android.util.Base64 -+import androidx.annotation.RequiresApi + import androidx.annotation.RequiresApi +import androidx.biometric.BiometricPrompt import expo.modules.securestore.AuthenticationHelper import expo.modules.securestore.DecryptException @@ -330,64 +217,34 @@ index 3a12dc9..d5b797b 100644 import expo.modules.securestore.SecureStoreModule import expo.modules.securestore.SecureStoreOptions import org.json.JSONException -@@ -50,17 +53,24 @@ class AESEncryptor : KeyBasedEncryptor { - return "${getKeyStoreAlias(options)}:$suffix" - } - -- @TargetApi(23) -+ @RequiresApi(Build.VERSION_CODES.R) - @Throws(GeneralSecurityException::class) -- override fun initializeKeyStoreEntry(keyStore: KeyStore, options: SecureStoreOptions): KeyStore.SecretKeyEntry { -+ override fun initializeKeyStoreEntry( -+ keyStore: KeyStore, options: SecureStoreOptions -+ ): KeyStore.SecretKeyEntry { - val extendedKeystoreAlias = getExtendedKeyStoreAlias(options, options.requireAuthentication) - val keyPurposes = KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT - -+ val authType = -+ if (options.enableCredentialsAlternative) KeyProperties.AUTH_BIOMETRIC_STRONG or KeyProperties.AUTH_DEVICE_CREDENTIAL -+ else KeyProperties.AUTH_BIOMETRIC_STRONG -+ - val algorithmSpec: AlgorithmParameterSpec = KeyGenParameterSpec.Builder(extendedKeystoreAlias, keyPurposes) - .setKeySize(AES_KEY_SIZE_BITS) - .setBlockModes(KeyProperties.BLOCK_MODE_GCM) - .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) - .setUserAuthenticationRequired(options.requireAuthentication) -+ .setUserAuthenticationParameters(0, authType) - .build() - - val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, keyStore.provider) -@@ -78,16 +88,25 @@ class AESEncryptor : KeyBasedEncryptor { - keyStoreEntry: KeyStore.SecretKeyEntry, - requireAuthentication: Boolean, +@@ -86,15 +88,23 @@ class AESEncryptor : KeyBasedEncryptor { authenticationPrompt: String, -- authenticationHelper: AuthenticationHelper + authenticationHelper: AuthenticationHelper, + enableDeviceFallback: Boolean, - ): JSONObject { -+ authenticationHelper: AuthenticationHelper, -+ enableCredentialsAlternative: Boolean, + ): SecureStoreFeedback { val secretKey = keyStoreEntry.secretKey val cipher = Cipher.getInstance(AES_CIPHER) cipher.init(Cipher.ENCRYPT_MODE, secretKey) val gcmSpec = cipher.parameters.getParameterSpec(GCMParameterSpec::class.java) -- val authenticatedCipher = authenticationHelper.authenticateCipher(cipher, requireAuthentication, authenticationPrompt) +- val authenticatedCipher = authenticationHelper.authenticateCipher(cipher, requireAuthentication, authenticationPrompt, enableDeviceFallback) + var promptResult: BiometricPrompt.AuthenticationResult? = null + val authenticatedCipher: Cipher -+ + +- return createEncryptedItemWithCipher(plaintextValue, authenticatedCipher, gcmSpec) + if (requireAuthentication) { -+ promptResult = authenticationHelper.authenticateCipher(cipher, authenticationPrompt, enableCredentialsAlternative) ++ promptResult = authenticationHelper.authenticateCipher(cipher, authenticationPrompt, enableDeviceFallback) + authenticatedCipher = promptResult.cryptoObject?.cipher ?: cipher + } else { + authenticatedCipher = cipher + } - -- return createEncryptedItemWithCipher(plaintextValue, authenticatedCipher, gcmSpec) ++ + return SecureStoreFeedback(createEncryptedItemWithCipher(plaintextValue, authenticatedCipher, gcmSpec), promptResult) } internal fun createEncryptedItemWithCipher( -@@ -114,7 +133,7 @@ class AESEncryptor : KeyBasedEncryptor { +@@ -121,7 +131,7 @@ class AESEncryptor : KeyBasedEncryptor { keyStoreEntry: KeyStore.SecretKeyEntry, options: SecureStoreOptions, authenticationHelper: AuthenticationHelper @@ -396,18 +253,18 @@ index 3a12dc9..d5b797b 100644 val ciphertext = encryptedItem.getString(CIPHERTEXT_PROPERTY) val ivString = encryptedItem.getString(IV_PROPERTY) val authenticationTagLength = encryptedItem.getInt(GCM_AUTHENTICATION_TAG_LENGTH_PROPERTY) -@@ -128,8 +147,18 @@ class AESEncryptor : KeyBasedEncryptor { +@@ -135,8 +145,18 @@ class AESEncryptor : KeyBasedEncryptor { throw DecryptException("Authentication tag length must be at least $MIN_GCM_AUTHENTICATION_TAG_LENGTH bits long", key, options.keychainService) } cipher.init(Cipher.DECRYPT_MODE, keyStoreEntry.secretKey, gcmSpec) -- val unlockedCipher = authenticationHelper.authenticateCipher(cipher, requiresAuthentication, options.authenticationPrompt) +- val unlockedCipher = authenticationHelper.authenticateCipher(cipher, requiresAuthentication, options.authenticationPrompt, options.enableDeviceFallback) - return String(unlockedCipher.doFinal(ciphertextBytes), StandardCharsets.UTF_8) + + var promptResult: BiometricPrompt.AuthenticationResult? = null + val unlockedCipher: Cipher + + if (requiresAuthentication) { -+ promptResult = authenticationHelper.authenticateCipher(cipher, options.authenticationPrompt, options.enableCredentialsAlternative) ++ promptResult = authenticationHelper.authenticateCipher(cipher, options.authenticationPrompt, options.enableDeviceFallback) + unlockedCipher = promptResult.cryptoObject?.cipher ?: cipher + } else { + unlockedCipher = cipher @@ -418,7 +275,7 @@ index 3a12dc9..d5b797b 100644 companion object { diff --git a/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/encryptors/HybridAESEncryptor.kt b/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/encryptors/HybridAESEncryptor.kt -index fb42599..0055b62 100644 +index 5f8bbfd..0b3ae0a 100644 --- a/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/encryptors/HybridAESEncryptor.kt +++ b/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/encryptors/HybridAESEncryptor.kt @@ -7,6 +7,7 @@ import android.util.Base64 @@ -429,31 +286,26 @@ index fb42599..0055b62 100644 import expo.modules.securestore.SecureStoreModule import expo.modules.securestore.SecureStoreOptions import org.json.JSONException -@@ -69,8 +70,9 @@ class HybridAESEncryptor(private var mContext: Context, private val mAESEncrypto - keyStoreEntry: KeyStore.PrivateKeyEntry, - requireAuthentication: Boolean, +@@ -71,7 +72,7 @@ class HybridAESEncryptor(private var mContext: Context, private val mAESEncrypto authenticationPrompt: String, -- authenticationHelper: AuthenticationHelper + authenticationHelper: AuthenticationHelper, + enableDeviceFallback: Boolean, - ): JSONObject { -+ authenticationHelper: AuthenticationHelper, -+ enableCredentialsAlternative: Boolean, + ): SecureStoreFeedback { // This should never be called after we dropped Android SDK 22 support. throw EncryptException( "HybridAESEncryption should not be used on Android SDK >= 23. This shouldn't happen. " + -@@ -86,8 +88,8 @@ class HybridAESEncryptor(private var mContext: Context, private val mAESEncrypto - encryptedItem: JSONObject, +@@ -88,7 +89,7 @@ class HybridAESEncryptor(private var mContext: Context, private val mAESEncrypto keyStoreEntry: KeyStore.PrivateKeyEntry, options: SecureStoreOptions, -- authenticationHelper: AuthenticationHelper + authenticationHelper: AuthenticationHelper - ): String { -+ authenticationHelper: AuthenticationHelper, + ): SecureStoreFeedback { // Decrypt the encrypted symmetric key val encryptedSecretKeyString = encryptedItem.getString(ENCRYPTED_SECRET_KEY_PROPERTY) val encryptedSecretKeyBytes = Base64.decode(encryptedSecretKeyString, Base64.DEFAULT) diff --git a/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/encryptors/KeyBasedEncryptor.kt b/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/encryptors/KeyBasedEncryptor.kt -index e493467..c880ee0 100644 +index 39459ff..66ba986 100644 --- a/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/encryptors/KeyBasedEncryptor.kt +++ b/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/encryptors/KeyBasedEncryptor.kt @@ -1,6 +1,7 @@ @@ -464,29 +316,24 @@ index e493467..c880ee0 100644 import expo.modules.securestore.SecureStoreOptions import org.json.JSONException import org.json.JSONObject -@@ -25,8 +26,9 @@ interface KeyBasedEncryptor { - keyStoreEntry: E, - requireAuthentication: Boolean, +@@ -27,7 +28,7 @@ interface KeyBasedEncryptor { authenticationPrompt: String, -- authenticationHelper: AuthenticationHelper + authenticationHelper: AuthenticationHelper, + enableDeviceFallback: Boolean, - ): JSONObject -+ authenticationHelper: AuthenticationHelper, -+ enableCredentialsAlternative: Boolean, + ): SecureStoreFeedback @Throws(GeneralSecurityException::class, JSONException::class) suspend fun decryptItem( -@@ -34,6 +36,6 @@ interface KeyBasedEncryptor { - encryptedItem: JSONObject, +@@ -36,5 +37,5 @@ interface KeyBasedEncryptor { keyStoreEntry: E, options: SecureStoreOptions, -- authenticationHelper: AuthenticationHelper + authenticationHelper: AuthenticationHelper - ): String -+ authenticationHelper: AuthenticationHelper, + ): SecureStoreFeedback } diff --git a/node_modules/expo-secure-store/build/SecureStore.d.ts b/node_modules/expo-secure-store/build/SecureStore.d.ts -index d5cd157..ca1cd76 100644 +index 835a4d4..77895d0 100644 --- a/node_modules/expo-secure-store/build/SecureStore.d.ts +++ b/node_modules/expo-secure-store/build/SecureStore.d.ts @@ -1,4 +1,53 @@ @@ -543,53 +390,7 @@ index d5cd157..ca1cd76 100644 /** * The data in the keychain item cannot be accessed after a restart until the device has been * unlocked once by the user. This may be useful if you need to access the item when the phone -@@ -78,6 +127,45 @@ export type SecureStoreOptions = { - * @platform ios - */ - accessGroup?: string; -+ -+ /** -+ * If value already exists, throw an error -+ * -+ * On Android it throws an error before the prompt for authentication. -+ * On iOS it throws an error after authentication. -+ */ -+ failOnDuplicate?: boolean; -+ /** -+ * On iOS, the system sometimes skips authentication if it has been done recently. -+ * This also applies to actions such as unlocking the screen. -+ * Setting this flag to true will ensure that authentication is always required before accessing the store. -+ * -+ * Use this wisely, when the user did not authenticated recently, setting this to true will lead to prompt -+ * being displayed twice when accessing the store value. -+ * The best use-case for this flag is when testing the behavior on iOS simulator, -+ * since the auth there may not be prompted at all without this flag. -+ * -+ * @warning This flag works only for async version of the save/read SecureStore methods. -+ * @platform ios -+ */ -+ alwaysAskForAuth?: boolean; -+ /** -+ * On iOS, the system does not ask for auth when saving the value to the SecureStore. -+ * The Android however, displays the authentication prompt when saving the value. -+ * To keep the behavior on every platform similar as much as possible, -+ * setting this flag to true will ensure that authentication is required when saving a value to the store. -+ * -+ * @warning This flag works only for async version of the save/read SecureStore methods. -+ * @platform ios -+ */ -+ askForAuthOnSave?: boolean; -+ /** -+ * On Android, there is often no fallback option if biometrics fail. -+ * Therefore, we may want to allow the user to authenticate using credentials if they prefer to. -+ * -+ * @platform android -+ */ -+ enableCredentialsAlternative?: boolean; - }; - /** - * Returns whether the SecureStore API is enabled on the current device. This does not check the app -@@ -109,7 +197,7 @@ export declare function deleteItemAsync(key: string, options?: SecureStoreOption +@@ -119,7 +168,7 @@ export declare function deleteItemAsync(key: string, options?: SecureStoreOption * > After a key has been invalidated, it becomes impossible to read its value. * > This only applies to values stored with `requireAuthentication` set to `true`. */ @@ -598,7 +399,7 @@ index d5cd157..ca1cd76 100644 /** * Stores a key–value pair. * -@@ -119,7 +207,7 @@ export declare function getItemAsync(key: string, options?: SecureStoreOptions): +@@ -129,7 +178,7 @@ export declare function getItemAsync(key: string, options?: SecureStoreOptions): * * @return A promise that rejects if value cannot be stored on the device. */ @@ -607,7 +408,7 @@ index d5cd157..ca1cd76 100644 /** * Stores a key–value pair synchronously. * > **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. -@@ -129,7 +217,7 @@ export declare function setItemAsync(key: string, value: string, options?: Secur +@@ -139,7 +188,7 @@ export declare function setItemAsync(key: string, value: string, options?: Secur * @param options An [`SecureStoreOptions`](#securestoreoptions) object. * */ @@ -616,7 +417,7 @@ index d5cd157..ca1cd76 100644 /** * Synchronously reads the stored value associated with the provided key. * > **Note:** This function blocks the JavaScript thread, so the application may not be interactive when reading a value with `requireAuthentication` -@@ -140,7 +228,7 @@ export declare function setItem(key: string, value: string, options?: SecureStor +@@ -150,7 +199,7 @@ export declare function setItem(key: string, value: string, options?: SecureStor * @return Previously stored value. It resolves with `null` if there is no entry * for the given key or if the key has been invalidated. */ @@ -626,7 +427,7 @@ index d5cd157..ca1cd76 100644 * Checks if the value can be saved with `requireAuthentication` option enabled. * @return `true` if the device supports biometric authentication and the enrolled method is sufficiently secure. Otherwise, returns `false`. Always returns false on tvOS. diff --git a/node_modules/expo-secure-store/build/SecureStore.js b/node_modules/expo-secure-store/build/SecureStore.js -index 4d87b38..4399e5e 100644 +index e11bd94..2371b88 100644 --- a/node_modules/expo-secure-store/build/SecureStore.js +++ b/node_modules/expo-secure-store/build/SecureStore.js @@ -1,6 +1,52 @@ @@ -692,65 +493,16 @@ index 4d87b38..4399e5e 100644 /** * Stores a key–value pair synchronously. diff --git a/node_modules/expo-secure-store/ios/SecureStoreModule.swift b/node_modules/expo-secure-store/ios/SecureStoreModule.swift -index 439b08d..98dab3e 100644 +index 1268979..e4c3f2b 100644 --- a/node_modules/expo-secure-store/ios/SecureStoreModule.swift +++ b/node_modules/expo-secure-store/ios/SecureStoreModule.swift -@@ -4,6 +4,42 @@ import LocalAuthentication - #endif - import Security - -+@available(iOS 11.2, *) -+public enum AuthType: Int, @unchecked Sendable { -+ /// The device does not support biometry. -+ case none = 0 -+ -+ /// The device supports device credentials -+ case credentials = 1 -+ -+ /// Generic type, not specified whether it was a faceID or touchID -+ case biometrics = 2 -+ -+ /// The device supports Face ID. -+ case faceID = 3 -+ -+ /// The device supports Touch ID. -+ case touchID = 4 -+ -+ /// The device supports Optic ID -+ case opticID = 5 -+} -+ -+struct SecureStoreFeedback { -+ var value: T -+ var authType: Int = AuthType.none.rawValue -+ var values: Array { -+ get { return [value, authType] } -+ } -+} -+ -+struct SecureStoreRuntimeError: LocalizedError { -+ let errorDescription: String? -+ init(_ description: String) { -+ self.errorDescription = description -+ } -+} -+ - public final class SecureStoreModule: Module { - public func definition() -> ModuleDefinition { - Name("ExpoSecureStore") -@@ -18,57 +54,137 @@ public final class SecureStoreModule: Module { +@@ -18,28 +18,40 @@ public final class SecureStoreModule: Module { "WHEN_UNLOCKED_THIS_DEVICE_ONLY": SecureStoreAccessible.whenUnlockedThisDeviceOnly.rawValue ]) - AsyncFunction("getValueWithKeyAsync") { (key: String, options: SecureStoreOptions) -> String? in - return try get(with: key, options: options) + AsyncFunction("getValueWithKeyAsync") { (key: String, options: SecureStoreOptions) in -+ if options.requireAuthentication && options.alwaysAskForAuth { -+ try await askForAuth( -+ prompt: options.authenticationPrompt -+ ?? "Authentication required" -+ ) -+ } + return getSecureStoreFeedback(value: try get(with: key, options: options)).values } @@ -767,15 +519,6 @@ index 439b08d..98dab3e 100644 } - return try set(value: value, with: key, options: options) -+ if options.requireAuthentication && ( -+ options.alwaysAskForAuth || options.askForAuthOnSave -+ ) { -+ try await askForAuth( -+ prompt: options.authenticationPrompt -+ ?? "Authentication required" -+ ) -+ } -+ + let result = try set(value: value, with: key, options: options) + + if !result { @@ -802,84 +545,78 @@ index 439b08d..98dab3e 100644 } AsyncFunction("deleteValueWithKeyAsync") { (key: String, options: SecureStoreOptions) in -- let noAuthSearchDictionary = query(with: key, options: options, requireAuthentication: false) -- let authSearchDictionary = query(with: key, options: options, requireAuthentication: true) -- let legacySearchDictionary = query(with: key, options: options) -+ let noAuthSearchDictionary = try query(with: key, options: options, requireAuthentication: false) -+ let authSearchDictionary = try query(with: key, options: options, requireAuthentication: true) -+ let legacySearchDictionary = try query(with: key, options: options) - - SecItemDelete(legacySearchDictionary as CFDictionary) - SecItemDelete(authSearchDictionary as CFDictionary) - SecItemDelete(noAuthSearchDictionary as CFDictionary) +@@ -53,18 +65,7 @@ public final class SecureStoreModule: Module { } -- Function("canUseBiometricAuthentication") {() -> Bool in + Function("canUseBiometricAuthentication") {() -> Bool in - #if os(tvOS) - return false - #else - let context = LAContext() - var error: NSError? - let isBiometricsSupported: Bool = context.canEvaluatePolicy(LAPolicy.deviceOwnerAuthenticationWithBiometrics, error: &error) -+ Function("canUseBiometricAuthentication") { () -> Bool in -+ return areBiometricsEnabled() -+ } -+ } - +- - if error != nil { - return false - } - return isBiometricsSupported - #endif -+ private func askForAuth(prompt: String) async throws { -+ let context = LAContext() -+ context.localizedReason = prompt -+ -+ guard context.canEvaluatePolicy(.deviceOwnerAuthentication, error: nil) else { -+ throw SecureStoreRuntimeError("No authentication method available") ++ return areBiometricsEnabled() } -+ -+ let result = try await context.evaluatePolicy( -+ LAPolicy.deviceOwnerAuthentication, -+ localizedReason: context.localizedReason -+ ) -+ -+ if !result { -+ throw SecureStoreRuntimeError("Unable to authenticate") -+ } -+ } -+ + + Function("canUseDeviceCredentialsAuthentication") { () -> Bool in +@@ -72,6 +73,31 @@ public final class SecureStoreModule: Module { + } + } + + private func getAuthType() -> AuthType { + if !areBiometricsEnabled() {return AuthType.credentials} + let biometryType = LAContext().biometryType + + switch biometryType { -+ case .faceID: return .faceID -+ case .touchID: return .touchID -+ case .opticID: return .opticID // available since iOS 17 -+ case .none: fallthrough // this one continues to the next line -+ @unknown default: return .credentials ++ case .faceID: return .faceID ++ case .touchID: return .touchID ++ case .opticID: return .opticID // available since iOS 17 ++ case .none: fallthrough // this one continues to the next line ++ @unknown default: return .credentials + } + } + + private func getSecureStoreFeedback(value: T) -> SecureStoreFeedback { + return SecureStoreFeedback(value: value, authType: getAuthType().rawValue) ++ } ++ ++ private func areBiometricsEnabled() -> Bool { ++ #if os(tvOS) ++ return false ++ #else ++ return LAContext().canEvaluatePolicy(LAPolicy.deviceOwnerAuthenticationWithBiometrics, error: nil) ++ #endif ++ } ++ + private func areDeviceCredentialsEnabled() -> Bool { + return LAContext().canEvaluatePolicy(LAPolicy.deviceOwnerAuthentication, error: nil) + } +@@ -96,6 +122,29 @@ public final class SecureStoreModule: Module { + return nil } -- private func get(with key: String, options: SecureStoreOptions) throws -> String? { -+ private func getAccessControl(accessibility: CFString) throws -> SecAccessControl -+ { -+ guard -+ Bundle.main.infoDictionary?["NSFaceIDUsageDescription"] as? String -+ != nil -+ else { ++ private func NSFaceIDUsageEntryGuard(options: SecureStoreOptions) throws { ++ if (options.enableDeviceFallback) { ++ return; ++ } ++ ++ guard let _ = Bundle.main.infoDictionary?["NSFaceIDUsageDescription"] as? String else { + throw MissingPlistKeyException() + } ++ } + ++ private func getAccessOptions(options: SecureStoreOptions, accessibility: CFString) throws -> SecAccessControl { + var error: Unmanaged? = nil -+ guard -+ let accessOptions = SecAccessControlCreateWithFlags(kCFAllocatorDefault, accessibility, .userPresence, &error) -+ else { ++ ++ let accessControlFlag: SecAccessControlCreateFlags = options.enableDeviceFallback ? .userPresence : .biometryCurrentSet ++ ++ guard let accessOptions = SecAccessControlCreateWithFlags(kCFAllocatorDefault, accessibility, accessControlFlag, &error) else { + let errorCode = error.map { CFErrorGetCode($0.takeRetainedValue()) } + throw SecAccessControlError(errorCode) + } @@ -887,147 +624,72 @@ index 439b08d..98dab3e 100644 + return accessOptions + } + -+ private func areBiometricsEnabled() -> Bool { -+ #if os(tvOS) -+ return false -+ #else -+ return LAContext().canEvaluatePolicy(LAPolicy.deviceOwnerAuthenticationWithBiometrics, error: nil) -+ #endif -+ } -+ -+ private func get(with key: String, options: SecureStoreOptions) throws -> String? -+ { - guard let key = validate(for: key) else { - throw InvalidKeyException() - } -@@ -89,7 +205,7 @@ public final class SecureStoreModule: Module { - } - private func set(value: String, with key: String, options: SecureStoreOptions) throws -> Bool { -- var setItemQuery = query(with: key, options: options, requireAuthentication: options.requireAuthentication) -+ var setItemQuery = try query(with: key, options: options, requireAuthentication: options.requireAuthentication) - - let valueData = value.data(using: .utf8) - setItemQuery[kSecValueData as String] = valueData -@@ -98,17 +214,6 @@ public final class SecureStoreModule: Module { + var setItemQuery = query(with: key, options: options, requireAuthentication: options.requireAuthentication) +@@ -107,21 +156,8 @@ public final class SecureStoreModule: Module { if !options.requireAuthentication { setItemQuery[kSecAttrAccessible as String] = accessibility -- } else { -- guard let _ = Bundle.main.infoDictionary?["NSFaceIDUsageDescription"] as? String else { -- throw MissingPlistKeyException() + } else { +- if (!options.enableDeviceFallback) { +- guard let _ = Bundle.main.infoDictionary?["NSFaceIDUsageDescription"] as? String else { +- throw MissingPlistKeyException() +- } - } - - var error: Unmanaged? = nil -- guard let accessOptions = SecAccessControlCreateWithFlags(kCFAllocatorDefault, accessibility, .biometryCurrentSet, &error) else { +- +- let accessControlFlag: SecAccessControlCreateFlags = options.enableDeviceFallback ? .userPresence : .biometryCurrentSet +- +- guard let accessOptions = SecAccessControlCreateWithFlags(kCFAllocatorDefault, accessibility, accessControlFlag, &error) else { - let errorCode = error.map { CFErrorGetCode($0.takeRetainedValue()) } - throw SecAccessControlError(errorCode) - } - setItemQuery[kSecAttrAccessControl as String] = accessOptions ++ try NSFaceIDUsageEntryGuard(options: options) ++ setItemQuery[kSecAttrAccessControl as String] = try getAccessOptions(options: options, accessibility: accessibility) } let status = SecItemAdd(setItemQuery as CFDictionary, nil) -@@ -116,10 +221,13 @@ public final class SecureStoreModule: Module { - switch status { - case errSecSuccess: - // On success we want to remove the other key alias and legacy key (if they exist) to avoid conflicts during reads -- SecItemDelete(query(with: key, options: options) as CFDictionary) -- SecItemDelete(query(with: key, options: options, requireAuthentication: !options.requireAuthentication) as CFDictionary) -+ SecItemDelete(try query(with: key, options: options) as CFDictionary) -+ SecItemDelete(try query(with: key, options: options,requireAuthentication: !options.requireAuthentication) as CFDictionary) - return true - case errSecDuplicateItem: -+ if options.failOnDuplicate { -+ throw SecureStoreRuntimeError("Key already exists") -+ } - return try update(value: value, with: key, options: options) - default: - throw KeyChainException(status) -@@ -127,15 +235,11 @@ public final class SecureStoreModule: Module { - } - - private func update(value: String, with key: String, options: SecureStoreOptions) throws -> Bool { -- var query = query(with: key, options: options, requireAuthentication: options.requireAuthentication) -+ let query = try query(with: key, options: options, requireAuthentication: options.requireAuthentication) - - let valueData = value.data(using: .utf8) - let updateDictionary = [kSecValueData as String: valueData] - -- if let authPrompt = options.authenticationPrompt { -- query[kSecUseOperationPrompt as String] = authPrompt -- } -- - let status = SecItemUpdate(query as CFDictionary, updateDictionary as CFDictionary) - - if status == errSecSuccess { -@@ -146,15 +250,11 @@ public final class SecureStoreModule: Module { - } - - private func searchKeyChain(with key: String, options: SecureStoreOptions, requireAuthentication: Bool? = nil) throws -> Data? { -- var query = query(with: key, options: options, requireAuthentication: requireAuthentication) -+ var query = try query(with: key, options: options, requireAuthentication: requireAuthentication) - - query[kSecMatchLimit as String] = kSecMatchLimitOne - query[kSecReturnData as String] = kCFBooleanTrue - -- if let authPrompt = options.authenticationPrompt { -- query[kSecUseOperationPrompt as String] = authPrompt -- } -- - var item: CFTypeRef? - let status = SecItemCopyMatching(query as CFDictionary, &item) - -@@ -171,21 +271,28 @@ public final class SecureStoreModule: Module { - } - } - -- private func query(with key: String, options: SecureStoreOptions, requireAuthentication: Bool? = nil) -> [String: Any] { -+ private func query(with key: String, options: SecureStoreOptions, requireAuthentication: Bool? = nil) throws -> [String: Any] { - var service = options.keychainService ?? "app" - if let requireAuthentication { - service.append(":\(requireAuthentication ? "auth" : "no-auth")") - } - - let encodedKey = Data(key.utf8) -+ let accessibility = attributeWith(options: options) - - var query: [String: Any] = [ - kSecClass as String: kSecClassGenericPassword, - kSecAttrService as String: service, - kSecAttrGeneric as String: encodedKey, -- kSecAttrAccount as String: encodedKey -+ kSecAttrAccount as String: encodedKey, - ] - -+ if options.requireAuthentication { -+ query[kSecAttrAccessControl as String] = try getAccessControl( -+ accessibility: accessibility -+ ) -+ } -+ - if let accessGroup = options.accessGroup { - query[kSecAttrAccessGroup as String] = accessGroup - } diff --git a/node_modules/expo-secure-store/ios/SecureStoreOptions.swift b/node_modules/expo-secure-store/ios/SecureStoreOptions.swift -index 7e3fa4d..481b1fe 100644 +index c9e843b..13e15be 100644 --- a/node_modules/expo-secure-store/ios/SecureStoreOptions.swift +++ b/node_modules/expo-secure-store/ios/SecureStoreOptions.swift -@@ -15,4 +15,13 @@ internal struct SecureStoreOptions: Record { - +@@ -19,3 +19,32 @@ internal struct SecureStoreOptions: Record { @Field - var accessGroup: String? + var enableDeviceFallback: Bool = false + } + -+ @Field -+ var failOnDuplicate: Bool = false ++@available(iOS 11.2, *) ++public enum AuthType: Int, @unchecked Sendable { ++ /// The device does not support biometry. ++ case none = 0 + -+ @Field -+ var alwaysAskForAuth: Bool = false ++ /// The device supports device credentials ++ case credentials = 1 + -+ @Field -+ var askForAuthOnSave: Bool = false - } ++ /// Generic type, not specified whether it was a faceID or touchID ++ case biometrics = 2 ++ ++ /// The device supports Face ID. ++ case faceID = 3 ++ ++ /// The device supports Touch ID. ++ case touchID = 4 ++ ++ /// The device supports Optic ID ++ case opticID = 5 ++} ++ ++struct SecureStoreFeedback { ++ var value: T ++ var authType: Int = AuthType.none.rawValue ++ var values: Array { ++ get { return [value, authType] } ++ } ++} diff --git a/node_modules/expo-secure-store/src/SecureStore.ts b/node_modules/expo-secure-store/src/SecureStore.ts -index ee43e04..ccfd2a9 100644 +index 4f253a1..c1cc118 100644 --- a/node_modules/expo-secure-store/src/SecureStore.ts +++ b/node_modules/expo-secure-store/src/SecureStore.ts @@ -3,6 +3,54 @@ import { byteCountOverLimit, VALUE_BYTES_LIMIT } from './byteCounter'; @@ -1085,53 +747,7 @@ index ee43e04..ccfd2a9 100644 // @needsAudit /** * The data in the keychain item cannot be accessed after a restart until the device has been -@@ -102,6 +150,45 @@ export type SecureStoreOptions = { - * @platform ios - */ - accessGroup?: string; -+ -+ /** -+ * If value already exists, throw an error -+ * -+ * On Android it throws an error before the prompt for authentication. -+ * On iOS it throws an error after authentication. -+ */ -+ failOnDuplicate?: boolean; -+ /** -+ * On iOS, the system sometimes skips authentication if it has been done recently. -+ * This also applies to actions such as unlocking the screen. -+ * Setting this flag to true will ensure that authentication is always required before accessing the store. -+ * -+ * Use this wisely, when the user did not authenticated recently, setting this to true will lead to prompt -+ * being displayed twice when accessing the store value. -+ * The best use-case for this flag is when testing the behavior on iOS simulator, -+ * since the auth there may not be prompted at all without this flag. -+ * -+ * @warning This flag works only for async version of the save/read SecureStore methods. -+ * @platform ios -+ */ -+ alwaysAskForAuth?: boolean; -+ /** -+ * On iOS, the system does not ask for auth when saving the value to the SecureStore. -+ * The Android however, displays the authentication prompt when saving the value. -+ * To keep the behavior on every platform similar as much as possible, -+ * setting this flag to true will ensure that authentication is required when saving a value to the store. -+ * -+ * @warning This flag works only for async version of the save/read SecureStore methods. -+ * @platform ios -+ */ -+ askForAuthOnSave?: boolean; -+ /** -+ * On Android, there is often no fallback option if biometrics fail. -+ * Therefore, we may want to allow the user to authenticate using credentials if they prefer to. -+ * -+ * @platform android -+ */ -+ enableCredentialsAlternative?: boolean; - }; - - // @needsAudit -@@ -151,7 +238,7 @@ export async function deleteItemAsync( +@@ -162,7 +210,7 @@ export async function deleteItemAsync( export async function getItemAsync( key: string, options: SecureStoreOptions = {} @@ -1140,7 +756,7 @@ index ee43e04..ccfd2a9 100644 ensureValidKey(key); return await ExpoSecureStore.getValueWithKeyAsync(key, options); } -@@ -170,15 +257,15 @@ export async function setItemAsync( +@@ -181,7 +229,7 @@ export async function setItemAsync( key: string, value: string, options: SecureStoreOptions = {} @@ -1149,8 +765,7 @@ index ee43e04..ccfd2a9 100644 ensureValidKey(key); if (!isValidValue(value)) { throw new Error( -- `Invalid value provided to SecureStore. Values must be strings; consider JSON-encoding your values if they are serializable.` -+ `Invalid value provided to SecureStore. Values must be strings; consider JSON-encoding your values if they are serializable.`, +@@ -189,7 +237,7 @@ export async function setItemAsync( ); } @@ -1159,7 +774,7 @@ index ee43e04..ccfd2a9 100644 } /** -@@ -190,7 +277,7 @@ export async function setItemAsync( +@@ -201,7 +249,7 @@ export async function setItemAsync( * @param options An [`SecureStoreOptions`](#securestoreoptions) object. * */ @@ -1168,7 +783,7 @@ index ee43e04..ccfd2a9 100644 ensureValidKey(key); if (!isValidValue(value)) { throw new Error( -@@ -211,7 +298,7 @@ export function setItem(key: string, value: string, options: SecureStoreOptions +@@ -222,7 +270,7 @@ export function setItem(key: string, value: string, options: SecureStoreOptions * @return Previously stored value. It resolves with `null` if there is no entry * for the given key or if the key has been invalidated. */ diff --git a/patches/expo-secure-store/expo-secure-store+14.2.4+003+force-authentication-on-save.patch b/patches/expo-secure-store/expo-secure-store+14.2.4+003+force-authentication-on-save.patch new file mode 100644 index 000000000000..bc17960405a2 --- /dev/null +++ b/patches/expo-secure-store/expo-secure-store+14.2.4+003+force-authentication-on-save.patch @@ -0,0 +1,129 @@ +diff --git a/node_modules/expo-secure-store/build/SecureStore.d.ts b/node_modules/expo-secure-store/build/SecureStore.d.ts +index 77895d0..2bb1b8f 100644 +--- a/node_modules/expo-secure-store/build/SecureStore.d.ts ++++ b/node_modules/expo-secure-store/build/SecureStore.d.ts +@@ -137,6 +137,20 @@ export type SecureStoreOptions = { + * @platform ios + */ + enableDeviceFallback?: boolean; ++ /** ++ * On iOS, the system does not ask for auth when saving the value to the SecureStore. ++ * The Android however, displays the authentication prompt when saving the value. ++ * To keep the behavior on every platform similar as much as possible, ++ * setting this flag to true will ensure that authentication is required when saving a value to the store. ++ * ++ * @warning: This flag only works for the asynchronous version of the SecureStore save method. ++ * It should only be considered an improvement to the user experience; it does not prevent the user ++ * from saving the value without authenticating using other methods (e.g. by directly modifying the keychain). ++ * For it to take effect, the 'requireAuthentication' flag must be set to true. ++ * @default false ++ * @platform ios ++ */ ++ forceAuthenticationOnSave?: boolean; + }; + /** + * Returns whether the SecureStore API is enabled on the current device. This does not check the app +diff --git a/node_modules/expo-secure-store/ios/SecureStoreModule.swift b/node_modules/expo-secure-store/ios/SecureStoreModule.swift +index e4c3f2b..9848dc1 100644 +--- a/node_modules/expo-secure-store/ios/SecureStoreModule.swift ++++ b/node_modules/expo-secure-store/ios/SecureStoreModule.swift +@@ -31,6 +31,10 @@ public final class SecureStoreModule: Module { + throw InvalidKeyException() + } + ++ if options.requireAuthentication && options.forceAuthenticationOnSave { ++ try await triggerPolicy(options: options) ++ } ++ + let result = try set(value: value, with: key, options: options) + + if !result { +@@ -73,6 +77,32 @@ public final class SecureStoreModule: Module { + } + } + ++ @MainActor ++ private func triggerPolicy(options: SecureStoreOptions) async throws { ++ let isPolicyAvailable = options.enableDeviceFallback ? areDeviceCredentialsEnabled() : areBiometricsEnabled() ++ ++ guard isPolicyAvailable else { ++ throw SecureStoreRuntimeError("No authentication method available") ++ } ++ ++ let localAuthPolicy: LAPolicy = options.enableDeviceFallback ? .deviceOwnerAuthentication : .deviceOwnerAuthenticationWithBiometrics ++ let localizedReason: String = options.authenticationPrompt ?? "Authentication required" ++ ++ let success: Bool = try await withCheckedThrowingContinuation { continuation in ++ LAContext().evaluatePolicy(localAuthPolicy, localizedReason: localizedReason) { success, error in ++ if let error = error { ++ continuation.resume(throwing: error) ++ } else { ++ continuation.resume(returning: success) ++ } ++ } ++ } ++ ++ guard success else { ++ throw SecureStoreRuntimeError("Unable to authenticate") ++ } ++ } ++ + private func getAuthType() -> AuthType { + if !areBiometricsEnabled() {return AuthType.credentials} + let biometryType = LAContext().biometryType +@@ -269,3 +299,4 @@ public final class SecureStoreModule: Module { + return key + } + } ++ +diff --git a/node_modules/expo-secure-store/ios/SecureStoreOptions.swift b/node_modules/expo-secure-store/ios/SecureStoreOptions.swift +index 13e15be..504c36b 100644 +--- a/node_modules/expo-secure-store/ios/SecureStoreOptions.swift ++++ b/node_modules/expo-secure-store/ios/SecureStoreOptions.swift +@@ -18,6 +18,9 @@ internal struct SecureStoreOptions: Record { + + @Field + var enableDeviceFallback: Bool = false ++ ++ @Field ++ var forceAuthenticationOnSave: Bool = false + } + + @available(iOS 11.2, *) +@@ -48,3 +51,10 @@ struct SecureStoreFeedback { + get { return [value, authType] } + } + } ++ ++struct SecureStoreRuntimeError: LocalizedError { ++ let errorDescription: String? ++ init(_ description: String) { ++ self.errorDescription = description ++ } ++} +diff --git a/node_modules/expo-secure-store/src/SecureStore.ts b/node_modules/expo-secure-store/src/SecureStore.ts +index c1cc118..1f2ed59 100644 +--- a/node_modules/expo-secure-store/src/SecureStore.ts ++++ b/node_modules/expo-secure-store/src/SecureStore.ts +@@ -161,6 +161,21 @@ export type SecureStoreOptions = { + * @platform ios + */ + enableDeviceFallback?: boolean; ++ ++ /** ++ * On iOS, the system does not ask for auth when saving the value to the SecureStore. ++ * The Android however, displays the authentication prompt when saving the value. ++ * To keep the behavior on every platform similar as much as possible, ++ * setting this flag to true will ensure that authentication is required when saving a value to the store. ++ * ++ * @warning: This flag only works for the asynchronous version of the SecureStore save method. ++ * It should only be considered an improvement to the user experience; it does not prevent the user ++ * from saving the value without authenticating using other methods (e.g. by directly modifying the keychain). ++ * For it to take effect, the 'requireAuthentication' flag must be set to true. ++ * @default false ++ * @platform ios ++ */ ++ forceAuthenticationOnSave?: boolean; + }; + + // @needsAudit diff --git a/patches/expo-secure-store/expo-secure-store+14.2.4+004+fail-on-update.patch b/patches/expo-secure-store/expo-secure-store+14.2.4+004+fail-on-update.patch new file mode 100644 index 000000000000..8b912f97d1e1 --- /dev/null +++ b/patches/expo-secure-store/expo-secure-store+14.2.4+004+fail-on-update.patch @@ -0,0 +1,99 @@ +diff --git a/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/SecureStoreModule.kt b/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/SecureStoreModule.kt +index 866d47d..1be213f 100644 +--- a/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/SecureStoreModule.kt ++++ b/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/SecureStoreModule.kt +@@ -196,6 +196,10 @@ open class SecureStoreModule : Module() { + return SecureStoreAuthType.NONE + } + ++ if (prefs.contains(keychainAwareKey) && options.failOnUpdate) { ++ throw WriteException("Key already exists", key, options.keychainService) ++ } ++ + try { + if (keyIsInvalidated) { + // Invalidated keys will block writing even though it's not possible to re-validate them +diff --git a/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/SecureStoreOptions.kt b/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/SecureStoreOptions.kt +index d38650c..3ce02db 100644 +--- a/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/SecureStoreOptions.kt ++++ b/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/SecureStoreOptions.kt +@@ -10,6 +10,7 @@ class SecureStoreOptions( + @Field var authenticationPrompt: String = " ", + @Field var keychainService: String = SecureStoreModule.DEFAULT_KEYSTORE_ALIAS, + @Field var requireAuthentication: Boolean = false, ++ @Field var failOnUpdate: Boolean = false, + @Field var enableDeviceFallback: Boolean = false + ) : Record, Serializable + +diff --git a/node_modules/expo-secure-store/build/SecureStore.d.ts b/node_modules/expo-secure-store/build/SecureStore.d.ts +index d6aa1c8..b920e4d 100644 +--- a/node_modules/expo-secure-store/build/SecureStore.d.ts ++++ b/node_modules/expo-secure-store/build/SecureStore.d.ts +@@ -151,6 +151,17 @@ export type SecureStoreOptions = { + * @platform ios + */ + forceAuthenticationOnSave?: boolean; ++ ++ /** ++ * If the key has already been stored, the save function will throw an error instead of overwriting it. ++ * The behaviour differs slightly depending on the platform. ++ * On Android, an error is thrown before the authentication prompt. On iOS, an error is thrown after authentication. ++ * ++ * @default false ++ * @platform ios ++ * @platform android ++ */ ++ failOnUpdate?: boolean; + }; + /** + * Returns whether the SecureStore API is enabled on the current device. This does not check the app +diff --git a/node_modules/expo-secure-store/ios/SecureStoreModule.swift b/node_modules/expo-secure-store/ios/SecureStoreModule.swift +index 9848dc1..3ecabf8 100644 +--- a/node_modules/expo-secure-store/ios/SecureStoreModule.swift ++++ b/node_modules/expo-secure-store/ios/SecureStoreModule.swift +@@ -199,6 +199,9 @@ public final class SecureStoreModule: Module { + SecItemDelete(query(with: key, options: options, requireAuthentication: !options.requireAuthentication) as CFDictionary) + return true + case errSecDuplicateItem: ++ if options.failOnUpdate { ++ throw SecureStoreRuntimeError("Key already exists") ++ } + return try update(value: value, with: key, options: options) + default: + throw KeyChainException(status) +diff --git a/node_modules/expo-secure-store/ios/SecureStoreOptions.swift b/node_modules/expo-secure-store/ios/SecureStoreOptions.swift +index 504c36b..4c490b1 100644 +--- a/node_modules/expo-secure-store/ios/SecureStoreOptions.swift ++++ b/node_modules/expo-secure-store/ios/SecureStoreOptions.swift +@@ -21,6 +21,9 @@ internal struct SecureStoreOptions: Record { + + @Field + var forceAuthenticationOnSave: Bool = false ++ ++ @Field ++ var failOnUpdate: Bool = false + } + + @available(iOS 11.2, *) +diff --git a/node_modules/expo-secure-store/src/SecureStore.ts b/node_modules/expo-secure-store/src/SecureStore.ts +index 1f2ed59..db64fd2 100644 +--- a/node_modules/expo-secure-store/src/SecureStore.ts ++++ b/node_modules/expo-secure-store/src/SecureStore.ts +@@ -176,6 +176,17 @@ export type SecureStoreOptions = { + * @platform ios + */ + forceAuthenticationOnSave?: boolean; ++ ++ /** ++ * If the key has already been stored, the save function will throw an error instead of overwriting it. ++ * The behaviour differs slightly depending on the platform. ++ * On Android, an error is thrown before the authentication prompt. On iOS, an error is thrown after authentication. ++ * ++ * @default false ++ * @platform ios ++ * @platform android ++ */ ++ failOnUpdate?: boolean; + }; + + // @needsAudit diff --git a/patches/expo-secure-store/expo-secure-store+14.2.4+005+force-read-authentication-on-simulators.patch b/patches/expo-secure-store/expo-secure-store+14.2.4+005+force-read-authentication-on-simulators.patch new file mode 100644 index 000000000000..100829ae041c --- /dev/null +++ b/patches/expo-secure-store/expo-secure-store+14.2.4+005+force-read-authentication-on-simulators.patch @@ -0,0 +1,78 @@ +diff --git a/node_modules/expo-secure-store/build/SecureStore.d.ts b/node_modules/expo-secure-store/build/SecureStore.d.ts +index bf5290c..cc981d3 100644 +--- a/node_modules/expo-secure-store/build/SecureStore.d.ts ++++ b/node_modules/expo-secure-store/build/SecureStore.d.ts +@@ -162,6 +162,19 @@ export type SecureStoreOptions = { + * @platform android + */ + failOnUpdate?: boolean; ++ ++ /** ++ * The LocalAuthentication behaves slightly differently on iOS simulators. ++ * In numerous cases, the authentication prompts are skipped on simulators (as opposed to real devices). ++ * Setting this flag to true forces the prompt to appear on simulators when a value with the `requireAuthentication` flag set to true is read. ++ * This is purely for testing the app on simulators, in cases where the prompt does not appear when the value is read. ++ * This has no effect on real devices. ++ * ++ * @warning: This flag only works for the asynchronous version of the SecureStore read method. ++ * @default false ++ * @platform ios ++ */ ++ forceReadAuthenticationOnSimulators?: boolean; + }; + /** + * Returns whether the SecureStore API is enabled on the current device. This does not check the app +diff --git a/node_modules/expo-secure-store/ios/SecureStoreModule.swift b/node_modules/expo-secure-store/ios/SecureStoreModule.swift +index 4bfe51f..cbc4615 100644 +--- a/node_modules/expo-secure-store/ios/SecureStoreModule.swift ++++ b/node_modules/expo-secure-store/ios/SecureStoreModule.swift +@@ -19,6 +19,11 @@ public final class SecureStoreModule: Module { + ]) + + AsyncFunction("getValueWithKeyAsync") { (key: String, options: SecureStoreOptions) in ++ #if targetEnvironment(simulator) ++ if options.requireAuthentication && options.forceReadAuthenticationOnSimulators { ++ try await triggerPolicy(options: options) ++ } ++ #endif + return getSecureStoreFeedback(value: try get(with: key, options: options)).values + } + +diff --git a/node_modules/expo-secure-store/ios/SecureStoreOptions.swift b/node_modules/expo-secure-store/ios/SecureStoreOptions.swift +index 4c490b1..aee90ed 100644 +--- a/node_modules/expo-secure-store/ios/SecureStoreOptions.swift ++++ b/node_modules/expo-secure-store/ios/SecureStoreOptions.swift +@@ -24,6 +24,9 @@ internal struct SecureStoreOptions: Record { + + @Field + var failOnUpdate: Bool = false ++ ++ @Field ++ var forceReadAuthenticationOnSimulators: Bool = false + } + + @available(iOS 11.2, *) +diff --git a/node_modules/expo-secure-store/src/SecureStore.ts b/node_modules/expo-secure-store/src/SecureStore.ts +index db64fd2..69541ce 100644 +--- a/node_modules/expo-secure-store/src/SecureStore.ts ++++ b/node_modules/expo-secure-store/src/SecureStore.ts +@@ -187,6 +187,19 @@ export type SecureStoreOptions = { + * @platform android + */ + failOnUpdate?: boolean; ++ ++ /** ++ * The LocalAuthentication behaves slightly differently on iOS simulators. ++ * In numerous cases, the authentication prompts are skipped on simulators (as opposed to real devices). ++ * Setting this flag to true forces the prompt to appear on simulators when a value with the `requireAuthentication` flag set to true is read. ++ * This is purely for testing the app on simulators, in cases where the prompt does not appear when the value is read. ++ * This has no effect on real devices. ++ * ++ * @warning: This flag only works for the asynchronous version of the SecureStore read method. ++ * @default false ++ * @platform ios ++ */ ++ forceReadAuthenticationOnSimulators?: boolean; + }; + + // @needsAudit From 5feffab709fab42e5229a74aa9db98c75e98c7d6 Mon Sep 17 00:00:00 2001 From: Jakub Korytko Date: Tue, 2 Dec 2025 12:42:58 +0100 Subject: [PATCH 2/7] Improve expo-secure-store patches --- ...re-store+14.2.4+002+return-auth-type.patch | 936 ++++++++++++------ ...2.4+003+force-authentication-on-save.patch | 77 +- ...cure-store+14.2.4+004+fail-on-update.patch | 71 +- ...ce-read-authentication-on-simulators.patch | 61 +- 4 files changed, 770 insertions(+), 375 deletions(-) diff --git a/patches/expo-secure-store/expo-secure-store+14.2.4+002+return-auth-type.patch b/patches/expo-secure-store/expo-secure-store+14.2.4+002+return-auth-type.patch index 6a0171d3e5d0..8689d910d7eb 100644 --- a/patches/expo-secure-store/expo-secure-store+14.2.4+002+return-auth-type.patch +++ b/patches/expo-secure-store/expo-secure-store+14.2.4+002+return-auth-type.patch @@ -5,7 +5,7 @@ index 4a1a009..980ef0a 100644 @@ -20,12 +20,12 @@ class AuthenticationHelper( ) { private var isAuthenticating = false - + - suspend fun authenticateCipher(cipher: Cipher, requiresAuthentication: Boolean, title: String, enableDeviceFallback: Boolean): Cipher { - if (requiresAuthentication) { - return openAuthenticationPrompt(cipher, title, enableDeviceFallback).cryptoObject?.cipher @@ -18,124 +18,140 @@ index 4a1a009..980ef0a 100644 - return cipher + return promptResult } - + private suspend fun openAuthenticationPrompt( diff --git a/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/SecureStoreModule.kt b/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/SecureStoreModule.kt -index 3f21552..866d47d 100644 +index 3f21552..d209efc 100644 --- a/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/SecureStoreModule.kt +++ b/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/SecureStoreModule.kt @@ -36,23 +36,23 @@ open class SecureStoreModule : Module() { - + AsyncFunction("setValueWithKeyAsync") Coroutine { value: String?, key: String?, options: SecureStoreOptions -> key ?: throw NullKeyException() - return@Coroutine setItemImpl(key, value, options, false) -+ return@Coroutine setItemImpl(key, value, options, false).ordinal ++ return@Coroutine narrowSecureStoreFeedback(SecureStoreFeedbackAction.SET, setItemImpl(key, value, options, false), options) } - + AsyncFunction("getValueWithKeyAsync") Coroutine { key: String, options: SecureStoreOptions -> - return@Coroutine getItemImpl(key, options) -+ return@Coroutine getItemImpl(key, options).values ++ return@Coroutine narrowSecureStoreFeedback(SecureStoreFeedbackAction.GET,getItemImpl(key, options), options) } - + Function("setValueWithKeySync") { value: String?, key: String?, options: SecureStoreOptions -> key ?: throw NullKeyException() return@Function runBlocking { - return@runBlocking setItemImpl(key, value, options, keyIsInvalidated = false) -+ return@runBlocking setItemImpl(key, value, options, keyIsInvalidated = false).ordinal ++ return@runBlocking narrowSecureStoreFeedback(SecureStoreFeedbackAction.SET, setItemImpl(key, value, options, keyIsInvalidated = false), options).value } } - + Function("getValueWithKeySync") { key: String, options: SecureStoreOptions -> return@Function runBlocking { - return@runBlocking getItemImpl(key, options) -+ return@runBlocking getItemImpl(key, options).values ++ return@runBlocking narrowSecureStoreFeedback(SecureStoreFeedbackAction.GET, getItemImpl(key, options), options).value } } - -@@ -94,7 +94,7 @@ open class SecureStoreModule : Module() { + +@@ -94,7 +94,23 @@ open class SecureStoreModule : Module() { } } - + - private suspend fun getItemImpl(key: String, options: SecureStoreOptions): String? { -+ private suspend fun getItemImpl(key: String, options: SecureStoreOptions): SecureStoreFeedback { ++ private suspend fun narrowSecureStoreFeedback(action: String, feedback: SecureStoreOriginalFeedback, options: SecureStoreOptions): SecureStoreNarrowedFeedback { ++ if (!options.returnUsedAuthenticationType) { ++ return feedback ++ } ++ ++ if (action == SecureStoreFeedbackAction.GET) { ++ return SecureStoreGetFeedback(feedback.source, feedback.authenticationResult) ++ } ++ ++ if (action == SecureStoreFeedbackAction.SET) { ++ return SecureStoreSetFeedback(feedback.source, feedback.authenticationResult) ++ } ++ ++ return feedback ++ } ++ ++ private suspend fun getItemImpl(key: String, options: SecureStoreOptions): SecureStoreOriginalFeedback { // We use a SecureStore-specific shared preferences file, which lets us do things like enumerate // its entries or clear all of them val prefs: SharedPreferences = getSharedPreferences() -@@ -104,10 +104,10 @@ open class SecureStoreModule : Module() { +@@ -104,10 +120,10 @@ open class SecureStoreModule : Module() { } else if (prefs.contains(key)) { // For backwards-compatibility try to read using the old key format return readJSONEncodedItem(key, prefs, options) } - return null -+ return SecureStoreFeedback(null) ++ return SecureStoreOriginalFeedback(null) } - + - private suspend fun readJSONEncodedItem(key: String, prefs: SharedPreferences, options: SecureStoreOptions): String? { -+ private suspend fun readJSONEncodedItem(key: String, prefs: SharedPreferences, options: SecureStoreOptions): SecureStoreFeedback { ++ private suspend fun readJSONEncodedItem(key: String, prefs: SharedPreferences, options: SecureStoreOptions): SecureStoreOriginalFeedback { val keychainAwareKey = createKeychainAwareKey(key, options.keychainService) - + val legacyEncryptedItemString = prefs.getString(key, null) -@@ -126,7 +126,7 @@ open class SecureStoreModule : Module() { +@@ -126,7 +142,7 @@ open class SecureStoreModule : Module() { "" } - + - encryptedItemString ?: return null -+ encryptedItemString ?: return SecureStoreFeedback(null) - ++ encryptedItemString ?: return SecureStoreOriginalFeedback(null) + val encryptedItem: JSONObject = try { JSONObject(encryptedItemString) -@@ -149,13 +149,13 @@ open class SecureStoreModule : Module() { +@@ -149,13 +165,13 @@ open class SecureStoreModule : Module() { "This situation occurs when the app is reinstalled. The value will be removed to avoid future errors. Returning null" ) deleteItemImpl(key, options) - return null -+ return SecureStoreFeedback(null) ++ return SecureStoreOriginalFeedback(null) } return mAESEncryptor.decryptItem(key, encryptedItem, secretKeyEntry, options, authenticationHelper) } HybridAESEncryptor.NAME -> { val privateKeyEntry = getKeyEntryCompat(PrivateKeyEntry::class.java, hybridAESEncryptor, options, requireAuthentication, usesKeystoreSuffix) - ?: return null -+ ?: return SecureStoreFeedback(null) ++ ?: return SecureStoreOriginalFeedback(null) return hybridAESEncryptor.decryptItem(key, encryptedItem, privateKeyEntry, options, authenticationHelper) } else -> { -@@ -164,7 +164,7 @@ open class SecureStoreModule : Module() { +@@ -164,7 +180,7 @@ open class SecureStoreModule : Module() { } } catch (e: KeyPermanentlyInvalidatedException) { Log.w(TAG, "The requested key has been permanently invalidated. Returning null") - return null -+ return SecureStoreFeedback(null) ++ return SecureStoreOriginalFeedback(null) } catch (e: BadPaddingException) { // 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. // 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. -@@ -174,7 +174,7 @@ open class SecureStoreModule : Module() { +@@ -174,7 +190,7 @@ open class SecureStoreModule : Module() { "The entry in shared preferences is out of sync with the keystore. It will be removed, returning null." ) deleteItemImpl(key, options) - return null -+ return SecureStoreFeedback(null) ++ return SecureStoreOriginalFeedback(null) } catch (e: GeneralSecurityException) { throw (DecryptException(e.message, key, options.keychainService, e)) } catch (e: CodedException) { -@@ -184,7 +184,7 @@ open class SecureStoreModule : Module() { +@@ -184,7 +200,7 @@ open class SecureStoreModule : Module() { } } - + - private suspend fun setItemImpl(key: String, value: String?, options: SecureStoreOptions, keyIsInvalidated: Boolean) { -+ private suspend fun setItemImpl(key: String, value: String?, options: SecureStoreOptions, keyIsInvalidated: Boolean): SecureStoreAuthType { ++ private suspend fun setItemImpl(key: String, value: String?, options: SecureStoreOptions, keyIsInvalidated: Boolean): SecureStoreOriginalFeedback { val keychainAwareKey = createKeychainAwareKey(key, options.keychainService) val prefs: SharedPreferences = getSharedPreferences() - -@@ -193,7 +193,7 @@ open class SecureStoreModule : Module() { + +@@ -193,7 +209,7 @@ open class SecureStoreModule : Module() { if (!success) { throw WriteException("Could not write a null value to SecureStore", key, options.keychainService) } - return -+ return SecureStoreAuthType.NONE ++ return SecureStoreOriginalFeedback(null) } - + try { -@@ -210,7 +210,8 @@ open class SecureStoreModule : Module() { +@@ -210,7 +226,8 @@ open class SecureStoreModule : Module() { back a value. */ val secretKeyEntry: SecretKeyEntry = getOrCreateKeyEntry(SecretKeyEntry::class.java, mAESEncryptor, options, options.requireAuthentication) @@ -144,31 +160,35 @@ index 3f21552..866d47d 100644 + val encryptedItem = encryptResult.value encryptedItem.put(SCHEME_PROPERTY, AESEncryptor.NAME) saveEncryptedItem(encryptedItem, prefs, keychainAwareKey, options.requireAuthentication, options.keychainService) - -@@ -218,6 +219,9 @@ open class SecureStoreModule : Module() { + +@@ -218,6 +235,9 @@ open class SecureStoreModule : Module() { if (prefs.contains(key)) { prefs.edit().remove(key).apply() } + -+ return encryptResult.authType ++ return encryptResult + } catch (e: KeyPermanentlyInvalidatedException) { if (!keyIsInvalidated) { Log.w(TAG, "Key has been invalidated, retrying with the key deleted") diff --git a/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/SecureStoreOptions.kt b/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/SecureStoreOptions.kt -index 0455fd6..d38650c 100644 +index 0455fd6..4e30440 100644 --- a/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/SecureStoreOptions.kt +++ b/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/SecureStoreOptions.kt @@ -1,5 +1,6 @@ package expo.modules.securestore - + +import androidx.biometric.BiometricPrompt import expo.modules.kotlin.records.Field import expo.modules.kotlin.records.Record import java.io.Serializable -@@ -11,3 +12,35 @@ class SecureStoreOptions( +@@ -9,5 +10,58 @@ class SecureStoreOptions( + @Field var authenticationPrompt: String = " ", + @Field var keychainService: String = SecureStoreModule.DEFAULT_KEYSTORE_ALIAS, @Field var requireAuthentication: Boolean = false, - @Field var enableDeviceFallback: Boolean = false +- @Field var enableDeviceFallback: Boolean = false ++ @Field var enableDeviceFallback: Boolean = false, ++ @Field var returnUsedAuthenticationType: Boolean = false ) : Record, Serializable + +enum class SecureStoreAuthType(index: Int) { @@ -180,9 +200,16 @@ index 0455fd6..d38650c 100644 + NONE(0) +} + -+data class SecureStoreFeedback( -+ val value: T, -+ val authenticationResult: BiometricPrompt.AuthenticationResult? = null ++open class SecureStoreFeedbackAction { ++ companion object { ++ const val GET = "GET" ++ const val SET = "SET" ++ } ++} ++ ++abstract class SecureStoreFeedback( ++ val source: T, ++ val authenticationResult: BiometricPrompt.AuthenticationResult? +) { + @Field var authType: SecureStoreAuthType = when (authenticationResult?.authenticationType) { + BiometricPrompt.AUTHENTICATION_RESULT_TYPE_UNKNOWN -> { @@ -198,68 +225,82 @@ index 0455fd6..d38650c 100644 + SecureStoreAuthType.NONE + } + } ++ abstract val value: R ++} + ++class SecureStoreGetFeedback(source: T, authenticationResult: BiometricPrompt.AuthenticationResult? = null): SecureStoreFeedback>(source, authenticationResult) { + /** Used to return easily convertible values to JS code */ -+ @Field var values: Pair = Pair(value, authType.ordinal) ++ @Field override var value: Pair = Pair(source, authType.ordinal) ++} ++ ++class SecureStoreSetFeedback(source: T, authenticationResult: BiometricPrompt.AuthenticationResult? = null): SecureStoreFeedback(source, authenticationResult) { ++ @Field override var value: Int = authType.ordinal ++} ++ ++class SecureStoreOriginalFeedback(source: T, authenticationResult: BiometricPrompt.AuthenticationResult? = null): SecureStoreFeedback(source, authenticationResult) { ++ @Field override var value: T = source +} ++ ++typealias SecureStoreNarrowedFeedback = SecureStoreFeedback diff --git a/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/encryptors/AESEncryptor.kt b/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/encryptors/AESEncryptor.kt -index 1cd187f..44c84c3 100644 +index 1cd187f..d927ff1 100644 --- a/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/encryptors/AESEncryptor.kt +++ b/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/encryptors/AESEncryptor.kt -@@ -5,8 +5,10 @@ import android.security.keystore.KeyGenParameterSpec +@@ -5,10 +5,12 @@ import android.security.keystore.KeyGenParameterSpec import android.security.keystore.KeyProperties import android.util.Base64 import androidx.annotation.RequiresApi +import androidx.biometric.BiometricPrompt import expo.modules.securestore.AuthenticationHelper import expo.modules.securestore.DecryptException -+import expo.modules.securestore.SecureStoreFeedback import expo.modules.securestore.SecureStoreModule import expo.modules.securestore.SecureStoreOptions ++import expo.modules.securestore.SecureStoreOriginalFeedback import org.json.JSONException + import org.json.JSONObject + import java.nio.charset.StandardCharsets @@ -86,15 +88,23 @@ class AESEncryptor : KeyBasedEncryptor { authenticationPrompt: String, authenticationHelper: AuthenticationHelper, enableDeviceFallback: Boolean, - ): JSONObject { -+ ): SecureStoreFeedback { ++ ): SecureStoreOriginalFeedback { val secretKey = keyStoreEntry.secretKey val cipher = Cipher.getInstance(AES_CIPHER) cipher.init(Cipher.ENCRYPT_MODE, secretKey) - + val gcmSpec = cipher.parameters.getParameterSpec(GCMParameterSpec::class.java) - val authenticatedCipher = authenticationHelper.authenticateCipher(cipher, requireAuthentication, authenticationPrompt, enableDeviceFallback) + var promptResult: BiometricPrompt.AuthenticationResult? = null + val authenticatedCipher: Cipher - -- return createEncryptedItemWithCipher(plaintextValue, authenticatedCipher, gcmSpec) ++ + if (requireAuthentication) { + promptResult = authenticationHelper.authenticateCipher(cipher, authenticationPrompt, enableDeviceFallback) + authenticatedCipher = promptResult.cryptoObject?.cipher ?: cipher + } else { + authenticatedCipher = cipher + } -+ -+ return SecureStoreFeedback(createEncryptedItemWithCipher(plaintextValue, authenticatedCipher, gcmSpec), promptResult) + +- return createEncryptedItemWithCipher(plaintextValue, authenticatedCipher, gcmSpec) ++ return SecureStoreOriginalFeedback(createEncryptedItemWithCipher(plaintextValue, authenticatedCipher, gcmSpec), promptResult) } - + internal fun createEncryptedItemWithCipher( @@ -121,7 +131,7 @@ class AESEncryptor : KeyBasedEncryptor { keyStoreEntry: KeyStore.SecretKeyEntry, options: SecureStoreOptions, authenticationHelper: AuthenticationHelper - ): String { -+ ): SecureStoreFeedback { ++ ): SecureStoreOriginalFeedback { val ciphertext = encryptedItem.getString(CIPHERTEXT_PROPERTY) val ivString = encryptedItem.getString(IV_PROPERTY) val authenticationTagLength = encryptedItem.getInt(GCM_AUTHENTICATION_TAG_LENGTH_PROPERTY) -@@ -135,8 +145,18 @@ class AESEncryptor : KeyBasedEncryptor { +@@ -135,8 +145,17 @@ class AESEncryptor : KeyBasedEncryptor { throw DecryptException("Authentication tag length must be at least $MIN_GCM_AUTHENTICATION_TAG_LENGTH bits long", key, options.keychainService) } cipher.init(Cipher.DECRYPT_MODE, keyStoreEntry.secretKey, gcmSpec) - val unlockedCipher = authenticationHelper.authenticateCipher(cipher, requiresAuthentication, options.authenticationPrompt, options.enableDeviceFallback) - return String(unlockedCipher.doFinal(ciphertextBytes), StandardCharsets.UTF_8) -+ + var promptResult: BiometricPrompt.AuthenticationResult? = null + val unlockedCipher: Cipher + @@ -270,28 +311,28 @@ index 1cd187f..44c84c3 100644 + unlockedCipher = cipher + } + -+ return SecureStoreFeedback(String(unlockedCipher.doFinal(ciphertextBytes), StandardCharsets.UTF_8), promptResult) ++ return SecureStoreOriginalFeedback(String(unlockedCipher.doFinal(ciphertextBytes), StandardCharsets.UTF_8), promptResult) } - + companion object { diff --git a/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/encryptors/HybridAESEncryptor.kt b/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/encryptors/HybridAESEncryptor.kt -index 5f8bbfd..0b3ae0a 100644 +index 5f8bbfd..7527001 100644 --- a/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/encryptors/HybridAESEncryptor.kt +++ b/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/encryptors/HybridAESEncryptor.kt -@@ -7,6 +7,7 @@ import android.util.Base64 - import expo.modules.securestore.AuthenticationHelper - import expo.modules.securestore.EncryptException +@@ -9,6 +9,7 @@ import expo.modules.securestore.EncryptException import expo.modules.securestore.KeyStoreException -+import expo.modules.securestore.SecureStoreFeedback import expo.modules.securestore.SecureStoreModule import expo.modules.securestore.SecureStoreOptions ++import expo.modules.securestore.SecureStoreOriginalFeedback import org.json.JSONException + import org.json.JSONObject + import java.security.GeneralSecurityException @@ -71,7 +72,7 @@ class HybridAESEncryptor(private var mContext: Context, private val mAESEncrypto authenticationPrompt: String, authenticationHelper: AuthenticationHelper, enableDeviceFallback: Boolean, - ): JSONObject { -+ ): SecureStoreFeedback { ++ ): SecureStoreOriginalFeedback { // This should never be called after we dropped Android SDK 22 support. throw EncryptException( "HybridAESEncryption should not be used on Android SDK >= 23. This shouldn't happen. " + @@ -300,29 +341,29 @@ index 5f8bbfd..0b3ae0a 100644 options: SecureStoreOptions, authenticationHelper: AuthenticationHelper - ): String { -+ ): SecureStoreFeedback { ++ ): SecureStoreOriginalFeedback { // Decrypt the encrypted symmetric key val encryptedSecretKeyString = encryptedItem.getString(ENCRYPTED_SECRET_KEY_PROPERTY) val encryptedSecretKeyBytes = Base64.decode(encryptedSecretKeyString, Base64.DEFAULT) diff --git a/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/encryptors/KeyBasedEncryptor.kt b/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/encryptors/KeyBasedEncryptor.kt -index 39459ff..66ba986 100644 +index 39459ff..a531aff 100644 --- a/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/encryptors/KeyBasedEncryptor.kt +++ b/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/encryptors/KeyBasedEncryptor.kt -@@ -1,6 +1,7 @@ - package expo.modules.securestore.encryptors - +@@ -2,6 +2,7 @@ package expo.modules.securestore.encryptors + import expo.modules.securestore.AuthenticationHelper -+import expo.modules.securestore.SecureStoreFeedback import expo.modules.securestore.SecureStoreOptions ++import expo.modules.securestore.SecureStoreOriginalFeedback import org.json.JSONException import org.json.JSONObject + import java.security.GeneralSecurityException @@ -27,7 +28,7 @@ interface KeyBasedEncryptor { authenticationPrompt: String, authenticationHelper: AuthenticationHelper, enableDeviceFallback: Boolean, - ): JSONObject -+ ): SecureStoreFeedback - ++ ): SecureStoreOriginalFeedback + @Throws(GeneralSecurityException::class, JSONException::class) suspend fun decryptItem( @@ -36,5 +37,5 @@ interface KeyBasedEncryptor { @@ -330,19 +371,21 @@ index 39459ff..66ba986 100644 options: SecureStoreOptions, authenticationHelper: AuthenticationHelper - ): String -+ ): SecureStoreFeedback ++ ): SecureStoreOriginalFeedback } diff --git a/node_modules/expo-secure-store/build/SecureStore.d.ts b/node_modules/expo-secure-store/build/SecureStore.d.ts -index 835a4d4..77895d0 100644 +index 835a4d4..7bfcc98 100644 --- a/node_modules/expo-secure-store/build/SecureStore.d.ts +++ b/node_modules/expo-secure-store/build/SecureStore.d.ts @@ -1,4 +1,53 @@ ++type EmptyObject = Record; ++type SecureStoreSetFeedback = R['returnUsedAuthenticationType'] extends true ? AuthType : void; ++type SecureStoreGetFeedback = R['returnUsedAuthenticationType'] extends true ? [T | null, AuthType] : T | null; export type KeychainAccessibilityConstant = number; -+ +/** + * Authentication type returned by the SecureStore after reading item or saving it to the store. + */ -+export declare const AUTH_TYPE = { ++export declare const AUTH_TYPE: { + /** + * This is purely for backwards compatibility. + * Although it is not listed as a return value of the getAuthenticationType() method, @@ -351,86 +394,127 @@ index 835a4d4..77895d0 100644 + * @see https://developer.android.com/reference/androidx/biometric/BiometricPrompt#AUTHENTICATION_RESULT_TYPE_UNKNOWN() + * @platform android + */ -+ UNKNOWN: -1, ++ readonly UNKNOWN: -1; + /** + * Returned when the authentication fails + * @platform android + * @platform ios + */ -+ NONE: 0, ++ readonly NONE: 0; + /** + * Generic type, not specified whether it was a passcode or pattern. + * @platform android + * @platform ios + */ -+ CREDENTIALS: 1, ++ readonly CREDENTIALS: 1; + /** + * Generic type, not specified whether it was a face scan or a fingerprint + * @platform android + */ -+ BIOMETRICS: 2, ++ readonly BIOMETRICS: 2; + /** + * FaceID was used to authenticate + * @platform ios + */ -+ FACE_ID: 3, ++ readonly FACE_ID: 3; + /** + * TouchID was used to authenticate + * @platform ios + */ -+ TOUCH_ID: 4, ++ readonly TOUCH_ID: 4; + /** + * OpticID was used to authenticate (reserved by apple, used on Apple Vision Pro, not iOS) + */ -+ OPTIC_ID: 5 -+} as const; -+ ++ readonly OPTIC_ID: 5; ++}; +type AuthType = (typeof AUTH_TYPE)[keyof typeof AUTH_TYPE]; -+ /** * The data in the keychain item cannot be accessed after a restart until the device has been * unlocked once by the user. This may be useful if you need to access the item when the phone -@@ -119,7 +168,7 @@ export declare function deleteItemAsync(key: string, options?: SecureStoreOption +@@ -59,6 +108,7 @@ export type SecureStoreOptions = { + * Warning: This option is not supported in Expo Go when biometric authentication is available due to a missing NSFaceIDUsageDescription. + * In release builds or when using continuous native generation, make sure to use the `expo-secure-store` config plugin. + * ++ * > **Note:** This library requires a real device for testing since emulators/simulators do not require biometric authentication when retrieving secrets, unlike real iOS devices. + */ + requireAuthentication?: boolean; + /** +@@ -88,6 +138,22 @@ export type SecureStoreOptions = { + * @platform ios + */ + enableDeviceFallback?: boolean; ++ /** ++ * When this flag is set to true, the get methods of SecureStore will return a two-element array. The first value will be the original value returned when this flag is set to false. ++ * The second value is the authentication type used to read the value from the AUTH_TYPE object. ++ * As for the set function, the returned value will simply be AUTH_TYPE. ++ * ++ * @warning ++ * If the iOS device supports biometrics and the user falls back to device credentials, it will not be detected. ++ * This is not the case on Android, but we cannot specify the exact type of biometrics (e.g. fingerprint or face scan). ++ * Whether the type is detected correctly depends on the platform and its native implementation. ++ * This should be treated as more of a hint. ++ * ++ * @default false ++ * @platform android ++ * @platform ios ++ */ ++ returnUsedAuthenticationType?: boolean; + }; + /** + * Returns whether the SecureStore API is enabled on the current device. This does not check the app +@@ -119,27 +185,27 @@ export declare function deleteItemAsync(key: string, options?: SecureStoreOption * > After a key has been invalidated, it becomes impossible to read its value. * > This only applies to values stored with `requireAuthentication` set to `true`. */ -export declare function getItemAsync(key: string, options?: SecureStoreOptions): Promise; -+export declare function getItemAsync(key: string, options?: SecureStoreOptions): Promise<[string | null, AuthType]>; ++export declare function getItemAsync(key: string, options?: R | EmptyObject): Promise>; /** * Stores a key–value pair. * -@@ -129,7 +178,7 @@ export declare function getItemAsync(key: string, options?: SecureStoreOptions): + * @param key The key to associate with the stored value. Keys may contain alphanumeric characters, `.`, `-`, and `_`. +- * @param value The value to store. Size limit is 2048 bytes. ++ * @param value The value to store. + * @param options An [`SecureStoreOptions`](#securestoreoptions) object. * * @return A promise that rejects if value cannot be stored on the device. */ -export declare function setItemAsync(key: string, value: string, options?: SecureStoreOptions): Promise; -+export declare function setItemAsync(key: string, value: string, options?: SecureStoreOptions): Promise; ++export declare function setItemAsync(key: string, value: string, options?: R | EmptyObject): Promise>; /** * Stores a key–value pair synchronously. * > **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. -@@ -139,7 +188,7 @@ export declare function setItemAsync(key: string, value: string, options?: Secur + * + * @param key The key to associate with the stored value. Keys may contain alphanumeric characters, `.`, `-`, and `_`. +- * @param value The value to store. Size limit is 2048 bytes. ++ * @param value The value to store. * @param options An [`SecureStoreOptions`](#securestoreoptions) object. * */ -export declare function setItem(key: string, value: string, options?: SecureStoreOptions): void; -+export declare function setItem(key: string, value: string, options?: SecureStoreOptions): AuthType; ++export declare function setItem(key: string, value: string, options?: R | EmptyObject): SecureStoreSetFeedback; /** * Synchronously reads the stored value associated with the provided key. * > **Note:** This function blocks the JavaScript thread, so the application may not be interactive when reading a value with `requireAuthentication` -@@ -150,7 +199,7 @@ export declare function setItem(key: string, value: string, options?: SecureStor +@@ -150,7 +216,7 @@ export declare function setItem(key: string, value: string, options?: SecureStor * @return Previously stored value. It resolves with `null` if there is no entry * for the given key or if the key has been invalidated. */ -export declare function getItem(key: string, options?: SecureStoreOptions): string | null; -+export declare function getItem(key: string, options?: SecureStoreOptions): [string | null, AuthType]; ++export declare function getItem(key: string, options?: R | EmptyObject): SecureStoreGetFeedback; /** * Checks if the value can be saved with `requireAuthentication` option enabled. * @return `true` if the device supports biometric authentication and the enrolled method is sufficiently secure. Otherwise, returns `false`. Always returns false on tvOS. +@@ -165,4 +231,5 @@ export declare function canUseBiometricAuthentication(): boolean; + * @platform ios + */ + export declare function canUseDeviceCredentialsAuthentication(): boolean; ++export {}; + //# sourceMappingURL=SecureStore.d.ts.map diff --git a/node_modules/expo-secure-store/build/SecureStore.js b/node_modules/expo-secure-store/build/SecureStore.js -index e11bd94..2371b88 100644 +index e11bd94..04569ac 100644 --- a/node_modules/expo-secure-store/build/SecureStore.js +++ b/node_modules/expo-secure-store/build/SecureStore.js -@@ -1,6 +1,52 @@ +@@ -1,6 +1,53 @@ import ExpoSecureStore from './ExpoSecureStore'; import { byteCountOverLimit, VALUE_BYTES_LIMIT } from './byteCounter'; // @needsAudit @@ -478,12 +562,22 @@ index e11bd94..2371b88 100644 + /** + * OpticID was used to authenticate (reserved by apple, used on Apple Vision Pro, not iOS) + */ -+ OPTIC_ID: 5 ++ OPTIC_ID: 5, +}; ++// @needsAudit /** * The data in the keychain item cannot be accessed after a restart until the device has been * unlocked once by the user. This may be useful if you need to access the item when the phone -@@ -102,7 +148,7 @@ export async function setItemAsync(key, value, options = {}) { +@@ -92,7 +139,7 @@ export async function getItemAsync(key, options = {}) { + * Stores a key–value pair. + * + * @param key The key to associate with the stored value. Keys may contain alphanumeric characters, `.`, `-`, and `_`. +- * @param value The value to store. Size limit is 2048 bytes. ++ * @param value The value to store. + * @param options An [`SecureStoreOptions`](#securestoreoptions) object. + * + * @return A promise that rejects if value cannot be stored on the device. +@@ -102,14 +149,14 @@ export async function setItemAsync(key, value, options = {}) { if (!isValidValue(value)) { throw new Error(`Invalid value provided to SecureStore. Values must be strings; consider JSON-encoding your values if they are serializable.`); } @@ -492,62 +586,84 @@ index e11bd94..2371b88 100644 } /** * Stores a key–value pair synchronously. + * > **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. + * + * @param key The key to associate with the stored value. Keys may contain alphanumeric characters, `.`, `-`, and `_`. +- * @param value The value to store. Size limit is 2048 bytes. ++ * @param value The value to store. + * @param options An [`SecureStoreOptions`](#securestoreoptions) object. + * + */ diff --git a/node_modules/expo-secure-store/ios/SecureStoreModule.swift b/node_modules/expo-secure-store/ios/SecureStoreModule.swift -index 1268979..e4c3f2b 100644 +index 1268979..5280020 100644 --- a/node_modules/expo-secure-store/ios/SecureStoreModule.swift +++ b/node_modules/expo-secure-store/ios/SecureStoreModule.swift -@@ -18,28 +18,40 @@ public final class SecureStoreModule: Module { - "WHEN_UNLOCKED_THIS_DEVICE_ONLY": SecureStoreAccessible.whenUnlockedThisDeviceOnly.rawValue - ]) - +@@ -8,38 +8,44 @@ public final class SecureStoreModule: Module { + public func definition() -> ModuleDefinition { + Name("ExpoSecureStore") + +- Constants([ +- "AFTER_FIRST_UNLOCK": SecureStoreAccessible.afterFirstUnlock.rawValue, +- "AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY": SecureStoreAccessible.afterFirstUnlockThisDeviceOnly.rawValue, +- "ALWAYS": SecureStoreAccessible.always.rawValue, +- "WHEN_PASSCODE_SET_THIS_DEVICE_ONLY": SecureStoreAccessible.whenPasscodeSetThisDeviceOnly.rawValue, +- "ALWAYS_THIS_DEVICE_ONLY": SecureStoreAccessible.alwaysThisDeviceOnly.rawValue, +- "WHEN_UNLOCKED": SecureStoreAccessible.whenUnlocked.rawValue, +- "WHEN_UNLOCKED_THIS_DEVICE_ONLY": SecureStoreAccessible.whenUnlockedThisDeviceOnly.rawValue +- ]) +- - AsyncFunction("getValueWithKeyAsync") { (key: String, options: SecureStoreOptions) -> String? in - return try get(with: key, options: options) ++ Constant("AFTER_FIRST_UNLOCK") { SecureStoreAccessible.afterFirstUnlock.rawValue } ++ Constant("AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY") { SecureStoreAccessible.afterFirstUnlockThisDeviceOnly.rawValue } ++ Constant("ALWAYS") { SecureStoreAccessible.always.rawValue } ++ Constant("WHEN_PASSCODE_SET_THIS_DEVICE_ONLY") { SecureStoreAccessible.whenPasscodeSetThisDeviceOnly.rawValue } ++ Constant("ALWAYS_THIS_DEVICE_ONLY") { SecureStoreAccessible.alwaysThisDeviceOnly.rawValue } ++ Constant("WHEN_UNLOCKED") { SecureStoreAccessible.whenUnlocked.rawValue } ++ Constant("WHEN_UNLOCKED_THIS_DEVICE_ONLY") { SecureStoreAccessible.whenUnlockedThisDeviceOnly.rawValue } ++ + AsyncFunction("getValueWithKeyAsync") { (key: String, options: SecureStoreOptions) in -+ return getSecureStoreFeedback(value: try get(with: key, options: options)).values ++ let result = try get(with: key, options: options) ++ ++ return wrapResultWithFeedback(action: .get, result: result, options: options).value } - + - Function("getValueWithKeySync") { (key: String, options: SecureStoreOptions) -> String? in - return try get(with: key, options: options) + Function("getValueWithKeySync") { (key: String, options: SecureStoreOptions) in -+ return getSecureStoreFeedback(value: try get(with: key, options: options)).values ++ let result = try get(with: key, options: options) ++ ++ return wrapResultWithFeedback(action: .get, result: result, options: options).value } - + - AsyncFunction("setValueWithKeyAsync") { (value: String, key: String, options: SecureStoreOptions) -> Bool in -+ AsyncFunction("setValueWithKeyAsync") { (value: String, key: String, options: SecureStoreOptions) -> Int in ++ AsyncFunction("setValueWithKeyAsync") { (value: String, key: String, options: SecureStoreOptions) in guard let key = validate(for: key) else { throw InvalidKeyException() } - + - return try set(value: value, with: key, options: options) + let result = try set(value: value, with: key, options: options) + -+ if !result { -+ return AuthType.none.rawValue -+ } -+ -+ return getSecureStoreFeedback(value: true).authType ++ return wrapResultWithFeedback(action: .set, result: result, options: options).value } - + - Function("setValueWithKeySync") {(value: String, key: String, options: SecureStoreOptions) -> Bool in -+ Function("setValueWithKeySync") {(value: String, key: String, options: SecureStoreOptions) -> Int in ++ Function("setValueWithKeySync") {(value: String, key: String, options: SecureStoreOptions) in guard let key = validate(for: key) else { throw InvalidKeyException() } - + - return try set(value: value, with: key, options: options) + let result = try set(value: value, with: key, options: options) + -+ if !result { -+ return AuthType.none.rawValue -+ } -+ -+ return getSecureStoreFeedback(value: true).authType ++ return wrapResultWithFeedback(action: .set, result: result, options: options).value } - + AsyncFunction("deleteValueWithKeyAsync") { (key: String, options: SecureStoreOptions) in -@@ -53,18 +65,7 @@ public final class SecureStoreModule: Module { +@@ -53,18 +59,7 @@ public final class SecureStoreModule: Module { } - + Function("canUseBiometricAuthentication") {() -> Bool in - #if os(tvOS) - return false @@ -563,12 +679,12 @@ index 1268979..e4c3f2b 100644 - #endif + return areBiometricsEnabled() } - + Function("canUseDeviceCredentialsAuthentication") { () -> Bool in -@@ -72,6 +73,31 @@ public final class SecureStoreModule: Module { - } +@@ -76,6 +71,41 @@ public final class SecureStoreModule: Module { + return LAContext().canEvaluatePolicy(LAPolicy.deviceOwnerAuthentication, error: nil) } - + + private func getAuthType() -> AuthType { + if !areBiometricsEnabled() {return AuthType.credentials} + let biometryType = LAContext().biometryType @@ -582,8 +698,18 @@ index 1268979..e4c3f2b 100644 + } + } + -+ private func getSecureStoreFeedback(value: T) -> SecureStoreFeedback { -+ return SecureStoreFeedback(value: value, authType: getAuthType().rawValue) ++ private func wrapResultWithFeedback(action: SecureStoreFeedbackAction, result: T, options: SecureStoreOptions) -> any SecureStoreFeedback { ++ let authType = getAuthType().rawValue ++ ++ if (!options.returnUsedAuthenticationType) { ++ return SecureStoreOriginalFeedback(source: result, authType: authType) ++ } ++ ++ if (action == .get) { ++ return SecureStoreGetFeedback(source: result, authType: authType) ++ } ++ ++ return SecureStoreSetFeedback(source: result, authType: authType) + } + + private func areBiometricsEnabled() -> Bool { @@ -594,71 +720,21 @@ index 1268979..e4c3f2b 100644 + #endif + } + - private func areDeviceCredentialsEnabled() -> Bool { - return LAContext().canEvaluatePolicy(LAPolicy.deviceOwnerAuthentication, error: nil) - } -@@ -96,6 +122,29 @@ public final class SecureStoreModule: Module { - return nil - } - -+ private func NSFaceIDUsageEntryGuard(options: SecureStoreOptions) throws { -+ if (options.enableDeviceFallback) { -+ return; -+ } -+ -+ guard let _ = Bundle.main.infoDictionary?["NSFaceIDUsageDescription"] as? String else { -+ throw MissingPlistKeyException() -+ } -+ } -+ -+ private func getAccessOptions(options: SecureStoreOptions, accessibility: CFString) throws -> SecAccessControl { -+ var error: Unmanaged? = nil -+ -+ let accessControlFlag: SecAccessControlCreateFlags = options.enableDeviceFallback ? .userPresence : .biometryCurrentSet -+ -+ guard let accessOptions = SecAccessControlCreateWithFlags(kCFAllocatorDefault, accessibility, accessControlFlag, &error) else { -+ let errorCode = error.map { CFErrorGetCode($0.takeRetainedValue()) } -+ throw SecAccessControlError(errorCode) -+ } -+ -+ return accessOptions -+ } -+ - private func set(value: String, with key: String, options: SecureStoreOptions) throws -> Bool { - var setItemQuery = query(with: key, options: options, requireAuthentication: options.requireAuthentication) - -@@ -107,21 +156,8 @@ public final class SecureStoreModule: Module { - if !options.requireAuthentication { - setItemQuery[kSecAttrAccessible as String] = accessibility - } else { -- if (!options.enableDeviceFallback) { -- guard let _ = Bundle.main.infoDictionary?["NSFaceIDUsageDescription"] as? String else { -- throw MissingPlistKeyException() -- } -- } -- -- var error: Unmanaged? = nil -- -- let accessControlFlag: SecAccessControlCreateFlags = options.enableDeviceFallback ? .userPresence : .biometryCurrentSet -- -- guard let accessOptions = SecAccessControlCreateWithFlags(kCFAllocatorDefault, accessibility, accessControlFlag, &error) else { -- let errorCode = error.map { CFErrorGetCode($0.takeRetainedValue()) } -- throw SecAccessControlError(errorCode) -- } -- setItemQuery[kSecAttrAccessControl as String] = accessOptions -+ try NSFaceIDUsageEntryGuard(options: options) -+ setItemQuery[kSecAttrAccessControl as String] = try getAccessOptions(options: options, accessibility: accessibility) - } - - let status = SecItemAdd(setItemQuery as CFDictionary, nil) + private func get(with key: String, options: SecureStoreOptions) throws -> String? { + guard let key = validate(for: key) else { + throw InvalidKeyException() diff --git a/node_modules/expo-secure-store/ios/SecureStoreOptions.swift b/node_modules/expo-secure-store/ios/SecureStoreOptions.swift -index c9e843b..13e15be 100644 +index c9e843b..b95eca7 100644 --- a/node_modules/expo-secure-store/ios/SecureStoreOptions.swift +++ b/node_modules/expo-secure-store/ios/SecureStoreOptions.swift -@@ -19,3 +19,32 @@ internal struct SecureStoreOptions: Record { +@@ -18,4 +18,71 @@ internal struct SecureStoreOptions: Record { + @Field var enableDeviceFallback: Bool = false - } ++ ++ @Field ++ var returnUsedAuthenticationType: Bool = false ++} + +@available(iOS 11.2, *) +public enum AuthType: Int, @unchecked Sendable { @@ -681,65 +757,109 @@ index c9e843b..13e15be 100644 + case opticID = 5 +} + -+struct SecureStoreFeedback { -+ var value: T ++public enum SecureStoreFeedbackAction: String { ++ case set ++ case get ++} ++ ++protocol SecureStoreFeedback { ++ associatedtype Source ++ associatedtype Value ++ var source: Source { get } ++ var authType: Int { get } ++ var value: Value { get } ++} ++ ++struct SecureStoreGetFeedback: SecureStoreFeedback { ++ typealias Source = T ++ typealias Value = Array ++ var source: Source ++ var authType: Int = AuthType.none.rawValue ++ var value: Value { ++ get { return [source, authType] } ++ } ++} ++ ++struct SecureStoreOriginalFeedback: SecureStoreFeedback { ++ typealias Source = T ++ typealias Value = T ++ var source: Source + var authType: Int = AuthType.none.rawValue -+ var values: Array { -+ get { return [value, authType] } ++ var value: Value { ++ get { return source } + } +} ++ ++struct SecureStoreSetFeedback: SecureStoreFeedback { ++ typealias Source = T ++ typealias Value = Int ++ var source: Source ++ var authType: Int = AuthType.none.rawValue ++ var value: Value { ++ get { return authType } ++ } + } diff --git a/node_modules/expo-secure-store/src/SecureStore.ts b/node_modules/expo-secure-store/src/SecureStore.ts -index 4f253a1..c1cc118 100644 +index 4f253a1..e32ebdf 100644 --- a/node_modules/expo-secure-store/src/SecureStore.ts +++ b/node_modules/expo-secure-store/src/SecureStore.ts -@@ -3,6 +3,54 @@ import { byteCountOverLimit, VALUE_BYTES_LIMIT } from './byteCounter'; - +@@ -1,8 +1,63 @@ + import ExpoSecureStore from './ExpoSecureStore'; + import { byteCountOverLimit, VALUE_BYTES_LIMIT } from './byteCounter'; + ++type EmptyObject = Record; ++type SecureStoreSetFeedback = ++ R['returnUsedAuthenticationType'] extends true ? AuthType : void; ++type SecureStoreGetFeedback< ++ T, ++ R extends SecureStoreOptions, ++> = R['returnUsedAuthenticationType'] extends true ? [T | null, AuthType] : T | null; export type KeychainAccessibilityConstant = number; - + +/** + * Authentication type returned by the SecureStore after reading item or saving it to the store. + */ +export const AUTH_TYPE = { -+ /** -+ * This is purely for backwards compatibility. -+ * Although it is not listed as a return value of the getAuthenticationType() method, -+ * it is still present in the Android code. -+ * @see https://developer.android.com/reference/android/hardware/biometrics/BiometricPrompt.AuthenticationResult#getAuthenticationType() -+ * @see https://developer.android.com/reference/androidx/biometric/BiometricPrompt#AUTHENTICATION_RESULT_TYPE_UNKNOWN() -+ * @platform android -+ */ -+ UNKNOWN: -1, -+ /** -+ * Returned when the authentication fails -+ * @platform android -+ * @platform ios -+ */ -+ NONE: 0, -+ /** -+ * Generic type, not specified whether it was a passcode or pattern. -+ * @platform android -+ * @platform ios -+ */ -+ CREDENTIALS: 1, -+ /** -+ * Generic type, not specified whether it was a face scan or a fingerprint -+ * @platform android -+ */ -+ BIOMETRICS: 2, -+ /** -+ * FaceID was used to authenticate -+ * @platform ios -+ */ -+ FACE_ID: 3, -+ /** -+ * TouchID was used to authenticate -+ * @platform ios -+ */ -+ TOUCH_ID: 4, -+ /** -+ * OpticID was used to authenticate (reserved by apple, used on Apple Vision Pro, not iOS) -+ */ -+ OPTIC_ID: 5 ++ /** ++ * This is purely for backwards compatibility. ++ * Although it is not listed as a return value of the getAuthenticationType() method, ++ * it is still present in the Android code. ++ * @see https://developer.android.com/reference/android/hardware/biometrics/BiometricPrompt.AuthenticationResult#getAuthenticationType() ++ * @see https://developer.android.com/reference/androidx/biometric/BiometricPrompt#AUTHENTICATION_RESULT_TYPE_UNKNOWN() ++ * @platform android ++ */ ++ UNKNOWN: -1, ++ /** ++ * Returned when the authentication fails ++ * @platform android ++ * @platform ios ++ */ ++ NONE: 0, ++ /** ++ * Generic type, not specified whether it was a passcode or pattern. ++ * @platform android ++ * @platform ios ++ */ ++ CREDENTIALS: 1, ++ /** ++ * Generic type, not specified whether it was a face scan or a fingerprint ++ * @platform android ++ */ ++ BIOMETRICS: 2, ++ /** ++ * FaceID was used to authenticate ++ * @platform ios ++ */ ++ FACE_ID: 3, ++ /** ++ * TouchID was used to authenticate ++ * @platform ios ++ */ ++ TOUCH_ID: 4, ++ /** ++ * OpticID was used to authenticate (reserved by apple, used on Apple Vision Pro, not iOS) ++ */ ++ OPTIC_ID: 5, +} as const; + +type AuthType = (typeof AUTH_TYPE)[keyof typeof AUTH_TYPE]; @@ -747,48 +867,312 @@ index 4f253a1..c1cc118 100644 // @needsAudit /** * The data in the keychain item cannot be accessed after a restart until the device has been -@@ -162,7 +210,7 @@ export async function deleteItemAsync( - export async function getItemAsync( - key: string, - options: SecureStoreOptions = {} +@@ -17,7 +72,7 @@ export const AFTER_FIRST_UNLOCK: KeychainAccessibilityConstant = ExpoSecureStore + * from a backup. + */ + export const AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY: KeychainAccessibilityConstant = +- ExpoSecureStore.AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY; ++ ExpoSecureStore.AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY; + + // @needsAudit + /** +@@ -34,7 +89,7 @@ export const ALWAYS: KeychainAccessibilityConstant = ExpoSecureStore.ALWAYS; + * store an entry. If the user removes their passcode, the entry will be deleted. + */ + export const WHEN_PASSCODE_SET_THIS_DEVICE_ONLY: KeychainAccessibilityConstant = +- ExpoSecureStore.WHEN_PASSCODE_SET_THIS_DEVICE_ONLY; ++ ExpoSecureStore.WHEN_PASSCODE_SET_THIS_DEVICE_ONLY; + + // @needsAudit + /** +@@ -43,7 +98,7 @@ export const WHEN_PASSCODE_SET_THIS_DEVICE_ONLY: KeychainAccessibilityConstant = + * @deprecated Use an accessibility level that provides some user protection, such as `AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY`. + */ + export const ALWAYS_THIS_DEVICE_ONLY: KeychainAccessibilityConstant = +- ExpoSecureStore.ALWAYS_THIS_DEVICE_ONLY; ++ ExpoSecureStore.ALWAYS_THIS_DEVICE_ONLY; + + // @needsAudit + /** +@@ -57,62 +112,80 @@ export const WHEN_UNLOCKED: KeychainAccessibilityConstant = ExpoSecureStore.WHEN + * a backup. + */ + export const WHEN_UNLOCKED_THIS_DEVICE_ONLY: KeychainAccessibilityConstant = +- ExpoSecureStore.WHEN_UNLOCKED_THIS_DEVICE_ONLY; ++ ExpoSecureStore.WHEN_UNLOCKED_THIS_DEVICE_ONLY; + + // @needsAudit + export type SecureStoreOptions = { +- /** +- * - Android: Equivalent of the public/private key pair `Alias`. +- * - iOS: The item's service, equivalent to [`kSecAttrService`](https://developer.apple.com/documentation/security/ksecattrservice/). +- * > If the item is set with the `keychainService` option, it will be required to later fetch the value. +- */ +- keychainService?: string; +- /** +- * Option responsible for enabling the usage of the user authentication methods available on the device while +- * accessing data stored in SecureStore. +- * - Android: Equivalent to [`setUserAuthenticationRequired(true)`](https://developer.android.com/reference/android/security/keystore/KeyGenParameterSpec.Builder#setUserAuthenticationRequired(boolean)) +- * (requires API 23). +- * - iOS: Equivalent to [`biometryCurrentSet`](https://developer.apple.com/documentation/security/secaccesscontrolcreateflags/2937192-biometrycurrentset). +- * Complete functionality is unlocked only with a freshly generated key - this would not work in tandem with the `keychainService` +- * value used for the others non-authenticated operations. +- * +- * This option works slightly differently across platforms: On Android, user authentication is required for all operations. +- * On iOS, the user is prompted to authenticate only when reading or updating an existing value (not when creating a new one). +- * +- * Warning: This option is not supported in Expo Go when biometric authentication is available due to a missing NSFaceIDUsageDescription. +- * In release builds or when using continuous native generation, make sure to use the `expo-secure-store` config plugin. +- * +- */ +- requireAuthentication?: boolean; +- /** +- * Custom message displayed to the user while `requireAuthentication` option is turned on. +- */ +- authenticationPrompt?: string; +- /** +- * Specifies when the stored entry is accessible, using iOS's `kSecAttrAccessible` property. +- * @see Apple's documentation on [keychain item accessibility](https://developer.apple.com/documentation/security/ksecattraccessible/). +- * @default SecureStore.WHEN_UNLOCKED +- * @platform ios +- */ +- keychainAccessible?: KeychainAccessibilityConstant; ++ /** ++ * - Android: Equivalent of the public/private key pair `Alias`. ++ * - iOS: The item's service, equivalent to [`kSecAttrService`](https://developer.apple.com/documentation/security/ksecattrservice/). ++ * > If the item is set with the `keychainService` option, it will be required to later fetch the value. ++ */ ++ keychainService?: string; ++ /** ++ * Option responsible for enabling the usage of the user authentication methods available on the device while ++ * accessing data stored in SecureStore. ++ * - Android: Equivalent to [`setUserAuthenticationRequired(true)`](https://developer.android.com/reference/android/security/keystore/KeyGenParameterSpec.Builder#setUserAuthenticationRequired(boolean)) ++ * (requires API 23). ++ * - iOS: Equivalent to [`biometryCurrentSet`](https://developer.apple.com/documentation/security/secaccesscontrolcreateflags/2937192-biometrycurrentset). ++ * Complete functionality is unlocked only with a freshly generated key - this would not work in tandem with the `keychainService` ++ * value used for the others non-authenticated operations. ++ * ++ * This option works slightly differently across platforms: On Android, user authentication is required for all operations. ++ * On iOS, the user is prompted to authenticate only when reading or updating an existing value (not when creating a new one). ++ * ++ * Warning: This option is not supported in Expo Go when biometric authentication is available due to a missing NSFaceIDUsageDescription. ++ * In release builds or when using continuous native generation, make sure to use the `expo-secure-store` config plugin. ++ * ++ * > **Note:** This library requires a real device for testing since emulators/simulators do not require biometric authentication when retrieving secrets, unlike real iOS devices. ++ */ ++ requireAuthentication?: boolean; ++ /** ++ * Custom message displayed to the user while `requireAuthentication` option is turned on. ++ */ ++ authenticationPrompt?: string; ++ /** ++ * Specifies when the stored entry is accessible, using iOS's `kSecAttrAccessible` property. ++ * @see Apple's documentation on [keychain item accessibility](https://developer.apple.com/documentation/security/ksecattraccessible/). ++ * @default SecureStore.WHEN_UNLOCKED ++ * @platform ios ++ */ ++ keychainAccessible?: KeychainAccessibilityConstant; ++ ++ /** ++ * Specifies the access group the stored entry belongs to. ++ * @see Apple's documentation on [Sharing access to keychain items among a collection of apps](https://developer.apple.com/documentation/security/sharing-access-to-keychain-items-among-a-collection-of-apps). ++ * @platform ios ++ */ ++ accessGroup?: string; + +- /** +- * Specifies the access group the stored entry belongs to. +- * @see Apple's documentation on [Sharing access to keychain items among a collection of apps](https://developer.apple.com/documentation/security/sharing-access-to-keychain-items-among-a-collection-of-apps). +- * @platform ios +- */ +- accessGroup?: string; ++ /** ++ * This flag enables users to authenticate using Lock Screen Knowledge Factor (e.g. PIN, pattern or password). ++ * For sensitive apps, it is recommended not having biometric fall back to such factor. ++ * @see: https://developer.android.com/security/fraud-prevention/authentication ++ * ++ * @default false ++ * @platform android ++ * @platform ios ++ */ ++ enableDeviceFallback?: boolean; + +- /** +- * This flag enables users to authenticate using Lock Screen Knowledge Factor (e.g. PIN, pattern or password). +- * For sensitive apps, it is recommended not having biometric fall back to such factor. +- * @see: https://developer.android.com/security/fraud-prevention/authentication +- * +- * @default false +- * @platform android +- * @platform ios +- */ +- enableDeviceFallback?: boolean; ++ /** ++ * When this flag is set to true, the get methods of SecureStore will return a two-element array. The first value will be the original value returned when this flag is set to false. ++ * The second value is the authentication type used to read the value from the AUTH_TYPE object. ++ * As for the set function, the returned value will simply be AUTH_TYPE. ++ * ++ * @warning ++ * If the iOS device supports biometrics and the user falls back to device credentials, it will not be detected. ++ * This is not the case on Android, but we cannot specify the exact type of biometrics (e.g. fingerprint or face scan). ++ * Whether the type is detected correctly depends on the platform and its native implementation. ++ * This should be treated as more of a hint. ++ * ++ * @default false ++ * @platform android ++ * @platform ios ++ */ ++ returnUsedAuthenticationType?: boolean; + }; + + // @needsAudit +@@ -124,7 +197,7 @@ export type SecureStoreOptions = { + * on the current device. Currently, this resolves `true` on Android and iOS only. + */ + export async function isAvailableAsync(): Promise { +- return !!ExpoSecureStore.getValueWithKeyAsync; ++ return !!ExpoSecureStore.getValueWithKeyAsync; + } + + // @needsAudit +@@ -137,12 +210,12 @@ export async function isAvailableAsync(): Promise { + * @return A promise that rejects if the value can't be deleted. + */ + export async function deleteItemAsync( +- key: string, +- options: SecureStoreOptions = {} ++ key: string, ++ options: SecureStoreOptions = {} + ): Promise { +- ensureValidKey(key); ++ ensureValidKey(key); + +- await ExpoSecureStore.deleteValueWithKeyAsync(key, options); ++ await ExpoSecureStore.deleteValueWithKeyAsync(key, options); + } + + // @needsAudit +@@ -159,12 +232,12 @@ export async function deleteItemAsync( + * > After a key has been invalidated, it becomes impossible to read its value. + * > This only applies to values stored with `requireAuthentication` set to `true`. + */ +-export async function getItemAsync( +- key: string, +- options: SecureStoreOptions = {} -): Promise { -+): Promise<[string | null, AuthType]> { - ensureValidKey(key); - return await ExpoSecureStore.getValueWithKeyAsync(key, options); +- ensureValidKey(key); +- return await ExpoSecureStore.getValueWithKeyAsync(key, options); ++export async function getItemAsync( ++ key: string, ++ options: R | EmptyObject = {} ++): Promise> { ++ ensureValidKey(key); ++ return await ExpoSecureStore.getValueWithKeyAsync(key, options); } -@@ -181,7 +229,7 @@ export async function setItemAsync( - key: string, - value: string, - options: SecureStoreOptions = {} + + // @needsAudit +@@ -172,24 +245,24 @@ export async function getItemAsync( + * Stores a key–value pair. + * + * @param key The key to associate with the stored value. Keys may contain alphanumeric characters, `.`, `-`, and `_`. +- * @param value The value to store. Size limit is 2048 bytes. ++ * @param value The value to store. + * @param options An [`SecureStoreOptions`](#securestoreoptions) object. + * + * @return A promise that rejects if value cannot be stored on the device. + */ +-export async function setItemAsync( +- key: string, +- value: string, +- options: SecureStoreOptions = {} -): Promise { -+): Promise { - ensureValidKey(key); - if (!isValidValue(value)) { - throw new Error( -@@ -189,7 +237,7 @@ export async function setItemAsync( - ); - } - +- ensureValidKey(key); +- if (!isValidValue(value)) { +- throw new Error( +- `Invalid value provided to SecureStore. Values must be strings; consider JSON-encoding your values if they are serializable.` +- ); +- } ++export async function setItemAsync( ++ key: string, ++ value: string, ++ options: R | EmptyObject = {} ++): Promise> { ++ ensureValidKey(key); ++ if (!isValidValue(value)) { ++ throw new Error( ++ `Invalid value provided to SecureStore. Values must be strings; consider JSON-encoding your values if they are serializable.` ++ ); ++ } + - await ExpoSecureStore.setValueWithKeyAsync(value, key, options); -+ return await ExpoSecureStore.setValueWithKeyAsync(value, key, options); ++ return await ExpoSecureStore.setValueWithKeyAsync(value, key, options); } - + /** -@@ -201,7 +249,7 @@ export async function setItemAsync( +@@ -197,19 +270,23 @@ export async function setItemAsync( + * > **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. + * + * @param key The key to associate with the stored value. Keys may contain alphanumeric characters, `.`, `-`, and `_`. +- * @param value The value to store. Size limit is 2048 bytes. ++ * @param value The value to store. * @param options An [`SecureStoreOptions`](#securestoreoptions) object. * */ -export function setItem(key: string, value: string, options: SecureStoreOptions = {}): void { -+export function setItem(key: string, value: string, options: SecureStoreOptions = {}): AuthType { - ensureValidKey(key); - if (!isValidValue(value)) { - throw new Error( -@@ -222,7 +270,7 @@ export function setItem(key: string, value: string, options: SecureStoreOptions +- ensureValidKey(key); +- if (!isValidValue(value)) { +- throw new Error( +- `Invalid value provided to SecureStore. Values must be strings; consider JSON-encoding your values if they are serializable.` +- ); +- } ++export function setItem( ++ key: string, ++ value: string, ++ options: R | EmptyObject = {} ++): SecureStoreSetFeedback { ++ ensureValidKey(key); ++ if (!isValidValue(value)) { ++ throw new Error( ++ `Invalid value provided to SecureStore. Values must be strings; consider JSON-encoding your values if they are serializable.` ++ ); ++ } + +- return ExpoSecureStore.setValueWithKeySync(value, key, options); ++ return ExpoSecureStore.setValueWithKeySync(value, key, options); + } + + /** +@@ -222,9 +299,12 @@ export function setItem(key: string, value: string, options: SecureStoreOptions * @return Previously stored value. It resolves with `null` if there is no entry * for the given key or if the key has been invalidated. */ -export function getItem(key: string, options: SecureStoreOptions = {}): string | null { -+export function getItem(key: string, options: SecureStoreOptions = {}): [string | null, AuthType] { - ensureValidKey(key); - return ExpoSecureStore.getValueWithKeySync(key, options); +- ensureValidKey(key); +- return ExpoSecureStore.getValueWithKeySync(key, options); ++export function getItem( ++ key: string, ++ options: R | EmptyObject = {} ++): SecureStoreGetFeedback { ++ ensureValidKey(key); ++ return ExpoSecureStore.getValueWithKeySync(key, options); + } + + /** +@@ -234,7 +314,7 @@ export function getItem(key: string, options: SecureStoreOptions = {}): string | + * @platform ios + */ + export function canUseBiometricAuthentication(): boolean { +- return ExpoSecureStore.canUseBiometricAuthentication(); ++ return ExpoSecureStore.canUseBiometricAuthentication(); + } + + /** +@@ -244,7 +324,7 @@ export function canUseBiometricAuthentication(): boolean { + * @platform ios + */ + export function canUseDeviceCredentialsAuthentication(): boolean { +- return ExpoSecureStore.canUseDeviceCredentialsAuthentication(); ++ return ExpoSecureStore.canUseDeviceCredentialsAuthentication(); } + + function ensureValidKey(key: string) { diff --git a/patches/expo-secure-store/expo-secure-store+14.2.4+003+force-authentication-on-save.patch b/patches/expo-secure-store/expo-secure-store+14.2.4+003+force-authentication-on-save.patch index bc17960405a2..fc085c205b11 100644 --- a/patches/expo-secure-store/expo-secure-store+14.2.4+003+force-authentication-on-save.patch +++ b/patches/expo-secure-store/expo-secure-store+14.2.4+003+force-authentication-on-save.patch @@ -1,11 +1,11 @@ diff --git a/node_modules/expo-secure-store/build/SecureStore.d.ts b/node_modules/expo-secure-store/build/SecureStore.d.ts -index 77895d0..2bb1b8f 100644 +index 40ec045..be49e3b 100644 --- a/node_modules/expo-secure-store/build/SecureStore.d.ts +++ b/node_modules/expo-secure-store/build/SecureStore.d.ts -@@ -137,6 +137,20 @@ export type SecureStoreOptions = { +@@ -154,6 +154,20 @@ export type SecureStoreOptions = { * @platform ios */ - enableDeviceFallback?: boolean; + returnUsedAuthenticationType?: boolean; + /** + * On iOS, the system does not ask for auth when saving the value to the SecureStore. + * The Android however, displays the authentication prompt when saving the value. @@ -24,21 +24,21 @@ index 77895d0..2bb1b8f 100644 /** * Returns whether the SecureStore API is enabled on the current device. This does not check the app diff --git a/node_modules/expo-secure-store/ios/SecureStoreModule.swift b/node_modules/expo-secure-store/ios/SecureStoreModule.swift -index e4c3f2b..9848dc1 100644 +index 5280020..3060cbd 100644 --- a/node_modules/expo-secure-store/ios/SecureStoreModule.swift +++ b/node_modules/expo-secure-store/ios/SecureStoreModule.swift -@@ -31,6 +31,10 @@ public final class SecureStoreModule: Module { +@@ -33,6 +33,10 @@ public final class SecureStoreModule: Module { throw InvalidKeyException() } -+ if options.requireAuthentication && options.forceAuthenticationOnSave { ++ if options.requireAuthentication && options.forceAuthenticationOnSave { + try await triggerPolicy(options: options) + } + let result = try set(value: value, with: key, options: options) - if !result { -@@ -73,6 +77,32 @@ public final class SecureStoreModule: Module { + return wrapResultWithFeedback(action: .set, result: result, options: options).value +@@ -188,6 +192,32 @@ public final class SecureStoreModule: Module { } } @@ -62,36 +62,31 @@ index e4c3f2b..9848dc1 100644 + } + } + } -+ ++ + guard success else { + throw SecureStoreRuntimeError("Unable to authenticate") + } + } + - private func getAuthType() -> AuthType { - if !areBiometricsEnabled() {return AuthType.credentials} - let biometryType = LAContext().biometryType -@@ -269,3 +299,4 @@ public final class SecureStoreModule: Module { - return key - } - } -+ + private func searchKeyChain(with key: String, options: SecureStoreOptions, requireAuthentication: Bool? = nil) throws -> Data? { + var query = query(with: key, options: options, requireAuthentication: requireAuthentication) + diff --git a/node_modules/expo-secure-store/ios/SecureStoreOptions.swift b/node_modules/expo-secure-store/ios/SecureStoreOptions.swift -index 13e15be..504c36b 100644 +index b95eca7..321c7e5 100644 --- a/node_modules/expo-secure-store/ios/SecureStoreOptions.swift +++ b/node_modules/expo-secure-store/ios/SecureStoreOptions.swift -@@ -18,6 +18,9 @@ internal struct SecureStoreOptions: Record { +@@ -21,6 +21,9 @@ internal struct SecureStoreOptions: Record { @Field - var enableDeviceFallback: Bool = false + var returnUsedAuthenticationType: Bool = false + + @Field + var forceAuthenticationOnSave: Bool = false } @available(iOS 11.2, *) -@@ -48,3 +51,10 @@ struct SecureStoreFeedback { - get { return [value, authType] } +@@ -86,3 +89,10 @@ struct SecureStoreSetFeedback: SecureStoreFeedback { + get { return authType } } } + @@ -102,28 +97,28 @@ index 13e15be..504c36b 100644 + } +} diff --git a/node_modules/expo-secure-store/src/SecureStore.ts b/node_modules/expo-secure-store/src/SecureStore.ts -index c1cc118..1f2ed59 100644 +index e32ebdf..3fbaf02 100644 --- a/node_modules/expo-secure-store/src/SecureStore.ts +++ b/node_modules/expo-secure-store/src/SecureStore.ts -@@ -161,6 +161,21 @@ export type SecureStoreOptions = { - * @platform ios - */ - enableDeviceFallback?: boolean; +@@ -186,6 +186,21 @@ export type SecureStoreOptions = { + * @platform ios + */ + returnUsedAuthenticationType?: boolean; + -+ /** -+ * On iOS, the system does not ask for auth when saving the value to the SecureStore. -+ * The Android however, displays the authentication prompt when saving the value. -+ * To keep the behavior on every platform similar as much as possible, -+ * setting this flag to true will ensure that authentication is required when saving a value to the store. -+ * -+ * @warning: This flag only works for the asynchronous version of the SecureStore save method. -+ * It should only be considered an improvement to the user experience; it does not prevent the user -+ * from saving the value without authenticating using other methods (e.g. by directly modifying the keychain). -+ * For it to take effect, the 'requireAuthentication' flag must be set to true. -+ * @default false -+ * @platform ios -+ */ -+ forceAuthenticationOnSave?: boolean; ++ /** ++ * On iOS, the system does not ask for auth when saving the value to the SecureStore. ++ * The Android however, displays the authentication prompt when saving the value. ++ * To keep the behavior on every platform similar as much as possible, ++ * setting this flag to true will ensure that authentication is required when saving a value to the store. ++ * ++ * @warning: This flag only works for the asynchronous version of the SecureStore save method. ++ * It should only be considered an improvement to the user experience; it does not prevent the user ++ * from saving the value without authenticating using other methods (e.g. by directly modifying the keychain). ++ * For it to take effect, the 'requireAuthentication' flag must be set to true. ++ * @default false ++ * @platform ios ++ */ ++ forceAuthenticationOnSave?: boolean; }; // @needsAudit diff --git a/patches/expo-secure-store/expo-secure-store+14.2.4+004+fail-on-update.patch b/patches/expo-secure-store/expo-secure-store+14.2.4+004+fail-on-update.patch index 8b912f97d1e1..175b02cb71d8 100644 --- a/patches/expo-secure-store/expo-secure-store+14.2.4+004+fail-on-update.patch +++ b/patches/expo-secure-store/expo-secure-store+14.2.4+004+fail-on-update.patch @@ -1,9 +1,9 @@ diff --git a/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/SecureStoreModule.kt b/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/SecureStoreModule.kt -index 866d47d..1be213f 100644 +index d209efc..e362aef 100644 --- a/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/SecureStoreModule.kt +++ b/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/SecureStoreModule.kt -@@ -196,6 +196,10 @@ open class SecureStoreModule : Module() { - return SecureStoreAuthType.NONE +@@ -212,6 +212,10 @@ open class SecureStoreModule : Module() { + return SecureStoreOriginalFeedback(null) } + if (prefs.contains(keychainAwareKey) && options.failOnUpdate) { @@ -14,26 +14,27 @@ index 866d47d..1be213f 100644 if (keyIsInvalidated) { // Invalidated keys will block writing even though it's not possible to re-validate them diff --git a/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/SecureStoreOptions.kt b/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/SecureStoreOptions.kt -index d38650c..3ce02db 100644 +index 4e30440..16674b8 100644 --- a/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/SecureStoreOptions.kt +++ b/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/SecureStoreOptions.kt -@@ -10,6 +10,7 @@ class SecureStoreOptions( - @Field var authenticationPrompt: String = " ", +@@ -11,7 +11,8 @@ class SecureStoreOptions( @Field var keychainService: String = SecureStoreModule.DEFAULT_KEYSTORE_ALIAS, @Field var requireAuthentication: Boolean = false, -+ @Field var failOnUpdate: Boolean = false, - @Field var enableDeviceFallback: Boolean = false + @Field var enableDeviceFallback: Boolean = false, +- @Field var returnUsedAuthenticationType: Boolean = false ++ @Field var returnUsedAuthenticationType: Boolean = false, ++ @Field var failOnUpdate: Boolean = false ) : Record, Serializable + enum class SecureStoreAuthType(index: Int) { diff --git a/node_modules/expo-secure-store/build/SecureStore.d.ts b/node_modules/expo-secure-store/build/SecureStore.d.ts -index d6aa1c8..b920e4d 100644 +index 4543001..0f3ef89 100644 --- a/node_modules/expo-secure-store/build/SecureStore.d.ts +++ b/node_modules/expo-secure-store/build/SecureStore.d.ts -@@ -151,6 +151,17 @@ export type SecureStoreOptions = { +@@ -168,6 +168,16 @@ export type SecureStoreOptions = { * @platform ios */ forceAuthenticationOnSave?: boolean; -+ + /** + * If the key has already been stored, the save function will throw an error instead of overwriting it. + * The behaviour differs slightly depending on the platform. @@ -48,24 +49,24 @@ index d6aa1c8..b920e4d 100644 /** * Returns whether the SecureStore API is enabled on the current device. This does not check the app diff --git a/node_modules/expo-secure-store/ios/SecureStoreModule.swift b/node_modules/expo-secure-store/ios/SecureStoreModule.swift -index 9848dc1..3ecabf8 100644 +index 3060cbd..5a81b11 100644 --- a/node_modules/expo-secure-store/ios/SecureStoreModule.swift +++ b/node_modules/expo-secure-store/ios/SecureStoreModule.swift -@@ -199,6 +199,9 @@ public final class SecureStoreModule: Module { +@@ -167,6 +167,9 @@ public final class SecureStoreModule: Module { SecItemDelete(query(with: key, options: options, requireAuthentication: !options.requireAuthentication) as CFDictionary) return true case errSecDuplicateItem: -+ if options.failOnUpdate { ++ if options.failOnUpdate { + throw SecureStoreRuntimeError("Key already exists") + } return try update(value: value, with: key, options: options) default: throw KeyChainException(status) diff --git a/node_modules/expo-secure-store/ios/SecureStoreOptions.swift b/node_modules/expo-secure-store/ios/SecureStoreOptions.swift -index 504c36b..4c490b1 100644 +index 321c7e5..6a787b0 100644 --- a/node_modules/expo-secure-store/ios/SecureStoreOptions.swift +++ b/node_modules/expo-secure-store/ios/SecureStoreOptions.swift -@@ -21,6 +21,9 @@ internal struct SecureStoreOptions: Record { +@@ -24,6 +24,9 @@ internal struct SecureStoreOptions: Record { @Field var forceAuthenticationOnSave: Bool = false @@ -76,24 +77,32 @@ index 504c36b..4c490b1 100644 @available(iOS 11.2, *) diff --git a/node_modules/expo-secure-store/src/SecureStore.ts b/node_modules/expo-secure-store/src/SecureStore.ts -index 1f2ed59..db64fd2 100644 +index 3fbaf02..9c3463c 100644 --- a/node_modules/expo-secure-store/src/SecureStore.ts +++ b/node_modules/expo-secure-store/src/SecureStore.ts -@@ -176,6 +176,17 @@ export type SecureStoreOptions = { - * @platform ios - */ - forceAuthenticationOnSave?: boolean; +@@ -201,6 +201,17 @@ export type SecureStoreOptions = { + * @platform ios + */ + forceAuthenticationOnSave?: boolean; + -+ /** -+ * If the key has already been stored, the save function will throw an error instead of overwriting it. -+ * The behaviour differs slightly depending on the platform. -+ * On Android, an error is thrown before the authentication prompt. On iOS, an error is thrown after authentication. -+ * -+ * @default false -+ * @platform ios -+ * @platform android -+ */ -+ failOnUpdate?: boolean; ++ /** ++ * If the key has already been stored, the save function will throw an error instead of overwriting it. ++ * The behaviour differs slightly depending on the platform. ++ * On Android, an error is thrown before the authentication prompt. On iOS, an error is thrown after authentication. ++ * ++ * @default false ++ * @platform ios ++ * @platform android ++ */ ++ failOnUpdate?: boolean; }; // @needsAudit +@@ -341,7 +352,6 @@ export function canUseBiometricAuthentication(): boolean { + export function canUseDeviceCredentialsAuthentication(): boolean { + return ExpoSecureStore.canUseDeviceCredentialsAuthentication(); + } +- + function ensureValidKey(key: string) { + if (!isValidKey(key)) { + throw new Error( diff --git a/patches/expo-secure-store/expo-secure-store+14.2.4+005+force-read-authentication-on-simulators.patch b/patches/expo-secure-store/expo-secure-store+14.2.4+005+force-read-authentication-on-simulators.patch index 100829ae041c..062542442189 100644 --- a/patches/expo-secure-store/expo-secure-store+14.2.4+005+force-read-authentication-on-simulators.patch +++ b/patches/expo-secure-store/expo-secure-store+14.2.4+005+force-read-authentication-on-simulators.patch @@ -1,12 +1,11 @@ diff --git a/node_modules/expo-secure-store/build/SecureStore.d.ts b/node_modules/expo-secure-store/build/SecureStore.d.ts -index bf5290c..cc981d3 100644 +index a6856a9..59bdee0 100644 --- a/node_modules/expo-secure-store/build/SecureStore.d.ts +++ b/node_modules/expo-secure-store/build/SecureStore.d.ts -@@ -162,6 +162,19 @@ export type SecureStoreOptions = { +@@ -178,6 +178,18 @@ export type SecureStoreOptions = { * @platform android */ failOnUpdate?: boolean; -+ + /** + * The LocalAuthentication behaves slightly differently on iOS simulators. + * In numerous cases, the authentication prompts are skipped on simulators (as opposed to real devices). @@ -23,11 +22,11 @@ index bf5290c..cc981d3 100644 /** * Returns whether the SecureStore API is enabled on the current device. This does not check the app diff --git a/node_modules/expo-secure-store/ios/SecureStoreModule.swift b/node_modules/expo-secure-store/ios/SecureStoreModule.swift -index 4bfe51f..cbc4615 100644 +index 5a81b11..35a339a 100644 --- a/node_modules/expo-secure-store/ios/SecureStoreModule.swift +++ b/node_modules/expo-secure-store/ios/SecureStoreModule.swift -@@ -19,6 +19,11 @@ public final class SecureStoreModule: Module { - ]) +@@ -17,6 +17,11 @@ public final class SecureStoreModule: Module { + Constant("WHEN_UNLOCKED_THIS_DEVICE_ONLY") { SecureStoreAccessible.whenUnlockedThisDeviceOnly.rawValue } AsyncFunction("getValueWithKeyAsync") { (key: String, options: SecureStoreOptions) in + #if targetEnvironment(simulator) @@ -35,14 +34,14 @@ index 4bfe51f..cbc4615 100644 + try await triggerPolicy(options: options) + } + #endif - return getSecureStoreFeedback(value: try get(with: key, options: options)).values - } + let result = try get(with: key, options: options) + return wrapResultWithFeedback(action: .get, result: result, options: options).value diff --git a/node_modules/expo-secure-store/ios/SecureStoreOptions.swift b/node_modules/expo-secure-store/ios/SecureStoreOptions.swift -index 4c490b1..aee90ed 100644 +index 6a787b0..8f110dd 100644 --- a/node_modules/expo-secure-store/ios/SecureStoreOptions.swift +++ b/node_modules/expo-secure-store/ios/SecureStoreOptions.swift -@@ -24,6 +24,9 @@ internal struct SecureStoreOptions: Record { +@@ -27,6 +27,9 @@ internal struct SecureStoreOptions: Record { @Field var failOnUpdate: Bool = false @@ -53,26 +52,34 @@ index 4c490b1..aee90ed 100644 @available(iOS 11.2, *) diff --git a/node_modules/expo-secure-store/src/SecureStore.ts b/node_modules/expo-secure-store/src/SecureStore.ts -index db64fd2..69541ce 100644 +index 9c3463c..52fc89d 100644 --- a/node_modules/expo-secure-store/src/SecureStore.ts +++ b/node_modules/expo-secure-store/src/SecureStore.ts -@@ -187,6 +187,19 @@ export type SecureStoreOptions = { - * @platform android - */ - failOnUpdate?: boolean; +@@ -212,6 +212,19 @@ export type SecureStoreOptions = { + * @platform android + */ + failOnUpdate?: boolean; + -+ /** -+ * The LocalAuthentication behaves slightly differently on iOS simulators. -+ * In numerous cases, the authentication prompts are skipped on simulators (as opposed to real devices). -+ * Setting this flag to true forces the prompt to appear on simulators when a value with the `requireAuthentication` flag set to true is read. -+ * This is purely for testing the app on simulators, in cases where the prompt does not appear when the value is read. -+ * This has no effect on real devices. -+ * -+ * @warning: This flag only works for the asynchronous version of the SecureStore read method. -+ * @default false -+ * @platform ios -+ */ -+ forceReadAuthenticationOnSimulators?: boolean; ++ /** ++ * The LocalAuthentication behaves slightly differently on iOS simulators. ++ * In numerous cases, the authentication prompts are skipped on simulators (as opposed to real devices). ++ * Setting this flag to true forces the prompt to appear on simulators when a value with the `requireAuthentication` flag set to true is read. ++ * This is purely for testing the app on simulators, in cases where the prompt does not appear when the value is read. ++ * This has no effect on real devices. ++ * ++ * @warning: This flag only works for the asynchronous version of the SecureStore read method. ++ * @default false ++ * @platform ios ++ */ ++ forceReadAuthenticationOnSimulators?: boolean; }; // @needsAudit +@@ -352,6 +365,7 @@ export function canUseBiometricAuthentication(): boolean { + export function canUseDeviceCredentialsAuthentication(): boolean { + return ExpoSecureStore.canUseDeviceCredentialsAuthentication(); + } ++ + function ensureValidKey(key: string) { + if (!isValidKey(key)) { + throw new Error( From 3635bd766e642efa39c46117d41e2fbb98efb8a0 Mon Sep 17 00:00:00 2001 From: Jakub Korytko Date: Tue, 2 Dec 2025 13:37:51 +0100 Subject: [PATCH 3/7] Remove whitespaces from patch files --- ...re-store+14.2.4+002+return-auth-type.patch | 409 +++--------------- ...2.4+003+force-authentication-on-save.patch | 50 +-- ...cure-store+14.2.4+004+fail-on-update.patch | 48 +- ...ce-read-authentication-on-simulators.patch | 52 +-- 4 files changed, 133 insertions(+), 426 deletions(-) diff --git a/patches/expo-secure-store/expo-secure-store+14.2.4+002+return-auth-type.patch b/patches/expo-secure-store/expo-secure-store+14.2.4+002+return-auth-type.patch index 8689d910d7eb..74b39840a335 100644 --- a/patches/expo-secure-store/expo-secure-store+14.2.4+002+return-auth-type.patch +++ b/patches/expo-secure-store/expo-secure-store+14.2.4+002+return-auth-type.patch @@ -374,7 +374,7 @@ index 39459ff..a531aff 100644 + ): SecureStoreOriginalFeedback } diff --git a/node_modules/expo-secure-store/build/SecureStore.d.ts b/node_modules/expo-secure-store/build/SecureStore.d.ts -index 835a4d4..7bfcc98 100644 +index 835a4d4..d2da8d9 100644 --- a/node_modules/expo-secure-store/build/SecureStore.d.ts +++ b/node_modules/expo-secure-store/build/SecureStore.d.ts @@ -1,4 +1,53 @@ @@ -431,15 +431,7 @@ index 835a4d4..7bfcc98 100644 /** * The data in the keychain item cannot be accessed after a restart until the device has been * unlocked once by the user. This may be useful if you need to access the item when the phone -@@ -59,6 +108,7 @@ export type SecureStoreOptions = { - * Warning: This option is not supported in Expo Go when biometric authentication is available due to a missing NSFaceIDUsageDescription. - * In release builds or when using continuous native generation, make sure to use the `expo-secure-store` config plugin. - * -+ * > **Note:** This library requires a real device for testing since emulators/simulators do not require biometric authentication when retrieving secrets, unlike real iOS devices. - */ - requireAuthentication?: boolean; - /** -@@ -88,6 +138,22 @@ export type SecureStoreOptions = { +@@ -88,6 +137,22 @@ export type SecureStoreOptions = { * @platform ios */ enableDeviceFallback?: boolean; @@ -462,7 +454,7 @@ index 835a4d4..7bfcc98 100644 }; /** * Returns whether the SecureStore API is enabled on the current device. This does not check the app -@@ -119,27 +185,27 @@ export declare function deleteItemAsync(key: string, options?: SecureStoreOption +@@ -119,7 +184,7 @@ export declare function deleteItemAsync(key: string, options?: SecureStoreOption * > After a key has been invalidated, it becomes impossible to read its value. * > This only applies to values stored with `requireAuthentication` set to `true`. */ @@ -471,10 +463,7 @@ index 835a4d4..7bfcc98 100644 /** * Stores a key–value pair. * - * @param key The key to associate with the stored value. Keys may contain alphanumeric characters, `.`, `-`, and `_`. -- * @param value The value to store. Size limit is 2048 bytes. -+ * @param value The value to store. - * @param options An [`SecureStoreOptions`](#securestoreoptions) object. +@@ -129,7 +194,7 @@ export declare function getItemAsync(key: string, options?: SecureStoreOptions): * * @return A promise that rejects if value cannot be stored on the device. */ @@ -483,10 +472,7 @@ index 835a4d4..7bfcc98 100644 /** * Stores a key–value pair synchronously. * > **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. - * - * @param key The key to associate with the stored value. Keys may contain alphanumeric characters, `.`, `-`, and `_`. -- * @param value The value to store. Size limit is 2048 bytes. -+ * @param value The value to store. +@@ -139,7 +204,7 @@ export declare function setItemAsync(key: string, value: string, options?: Secur * @param options An [`SecureStoreOptions`](#securestoreoptions) object. * */ @@ -495,7 +481,7 @@ index 835a4d4..7bfcc98 100644 /** * Synchronously reads the stored value associated with the provided key. * > **Note:** This function blocks the JavaScript thread, so the application may not be interactive when reading a value with `requireAuthentication` -@@ -150,7 +216,7 @@ export declare function setItem(key: string, value: string, options?: SecureStor +@@ -150,7 +215,7 @@ export declare function setItem(key: string, value: string, options?: SecureStor * @return Previously stored value. It resolves with `null` if there is no entry * for the given key or if the key has been invalidated. */ @@ -504,14 +490,8 @@ index 835a4d4..7bfcc98 100644 /** * Checks if the value can be saved with `requireAuthentication` option enabled. * @return `true` if the device supports biometric authentication and the enrolled method is sufficiently secure. Otherwise, returns `false`. Always returns false on tvOS. -@@ -165,4 +231,5 @@ export declare function canUseBiometricAuthentication(): boolean; - * @platform ios - */ - export declare function canUseDeviceCredentialsAuthentication(): boolean; -+export {}; - //# sourceMappingURL=SecureStore.d.ts.map diff --git a/node_modules/expo-secure-store/build/SecureStore.js b/node_modules/expo-secure-store/build/SecureStore.js -index e11bd94..04569ac 100644 +index e11bd94..940fc61 100644 --- a/node_modules/expo-secure-store/build/SecureStore.js +++ b/node_modules/expo-secure-store/build/SecureStore.js @@ -1,6 +1,53 @@ @@ -568,16 +548,7 @@ index e11bd94..04569ac 100644 /** * The data in the keychain item cannot be accessed after a restart until the device has been * unlocked once by the user. This may be useful if you need to access the item when the phone -@@ -92,7 +139,7 @@ export async function getItemAsync(key, options = {}) { - * Stores a key–value pair. - * - * @param key The key to associate with the stored value. Keys may contain alphanumeric characters, `.`, `-`, and `_`. -- * @param value The value to store. Size limit is 2048 bytes. -+ * @param value The value to store. - * @param options An [`SecureStoreOptions`](#securestoreoptions) object. - * - * @return A promise that rejects if value cannot be stored on the device. -@@ -102,14 +149,14 @@ export async function setItemAsync(key, value, options = {}) { +@@ -102,7 +149,7 @@ export async function setItemAsync(key, value, options = {}) { if (!isValidValue(value)) { throw new Error(`Invalid value provided to SecureStore. Values must be strings; consider JSON-encoding your values if they are serializable.`); } @@ -586,42 +557,16 @@ index e11bd94..04569ac 100644 } /** * Stores a key–value pair synchronously. - * > **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. - * - * @param key The key to associate with the stored value. Keys may contain alphanumeric characters, `.`, `-`, and `_`. -- * @param value The value to store. Size limit is 2048 bytes. -+ * @param value The value to store. - * @param options An [`SecureStoreOptions`](#securestoreoptions) object. - * - */ diff --git a/node_modules/expo-secure-store/ios/SecureStoreModule.swift b/node_modules/expo-secure-store/ios/SecureStoreModule.swift -index 1268979..5280020 100644 +index 1268979..7764c8e 100644 --- a/node_modules/expo-secure-store/ios/SecureStoreModule.swift +++ b/node_modules/expo-secure-store/ios/SecureStoreModule.swift -@@ -8,38 +8,44 @@ public final class SecureStoreModule: Module { - public func definition() -> ModuleDefinition { - Name("ExpoSecureStore") +@@ -18,28 +18,36 @@ public final class SecureStoreModule: Module { + "WHEN_UNLOCKED_THIS_DEVICE_ONLY": SecureStoreAccessible.whenUnlockedThisDeviceOnly.rawValue + ]) -- Constants([ -- "AFTER_FIRST_UNLOCK": SecureStoreAccessible.afterFirstUnlock.rawValue, -- "AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY": SecureStoreAccessible.afterFirstUnlockThisDeviceOnly.rawValue, -- "ALWAYS": SecureStoreAccessible.always.rawValue, -- "WHEN_PASSCODE_SET_THIS_DEVICE_ONLY": SecureStoreAccessible.whenPasscodeSetThisDeviceOnly.rawValue, -- "ALWAYS_THIS_DEVICE_ONLY": SecureStoreAccessible.alwaysThisDeviceOnly.rawValue, -- "WHEN_UNLOCKED": SecureStoreAccessible.whenUnlocked.rawValue, -- "WHEN_UNLOCKED_THIS_DEVICE_ONLY": SecureStoreAccessible.whenUnlockedThisDeviceOnly.rawValue -- ]) -- - AsyncFunction("getValueWithKeyAsync") { (key: String, options: SecureStoreOptions) -> String? in - return try get(with: key, options: options) -+ Constant("AFTER_FIRST_UNLOCK") { SecureStoreAccessible.afterFirstUnlock.rawValue } -+ Constant("AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY") { SecureStoreAccessible.afterFirstUnlockThisDeviceOnly.rawValue } -+ Constant("ALWAYS") { SecureStoreAccessible.always.rawValue } -+ Constant("WHEN_PASSCODE_SET_THIS_DEVICE_ONLY") { SecureStoreAccessible.whenPasscodeSetThisDeviceOnly.rawValue } -+ Constant("ALWAYS_THIS_DEVICE_ONLY") { SecureStoreAccessible.alwaysThisDeviceOnly.rawValue } -+ Constant("WHEN_UNLOCKED") { SecureStoreAccessible.whenUnlocked.rawValue } -+ Constant("WHEN_UNLOCKED_THIS_DEVICE_ONLY") { SecureStoreAccessible.whenUnlockedThisDeviceOnly.rawValue } -+ + AsyncFunction("getValueWithKeyAsync") { (key: String, options: SecureStoreOptions) in + let result = try get(with: key, options: options) + @@ -661,7 +606,7 @@ index 1268979..5280020 100644 } AsyncFunction("deleteValueWithKeyAsync") { (key: String, options: SecureStoreOptions) in -@@ -53,18 +59,7 @@ public final class SecureStoreModule: Module { +@@ -53,18 +61,7 @@ public final class SecureStoreModule: Module { } Function("canUseBiometricAuthentication") {() -> Bool in @@ -681,7 +626,7 @@ index 1268979..5280020 100644 } Function("canUseDeviceCredentialsAuthentication") { () -> Bool in -@@ -76,6 +71,41 @@ public final class SecureStoreModule: Module { +@@ -76,6 +73,41 @@ public final class SecureStoreModule: Module { return LAContext().canEvaluatePolicy(LAPolicy.deviceOwnerAuthentication, error: nil) } @@ -800,7 +745,7 @@ index c9e843b..b95eca7 100644 + } } diff --git a/node_modules/expo-secure-store/src/SecureStore.ts b/node_modules/expo-secure-store/src/SecureStore.ts -index 4f253a1..e32ebdf 100644 +index 4f253a1..d44c0bb 100644 --- a/node_modules/expo-secure-store/src/SecureStore.ts +++ b/node_modules/expo-secure-store/src/SecureStore.ts @@ -1,8 +1,63 @@ @@ -867,312 +812,90 @@ index 4f253a1..e32ebdf 100644 // @needsAudit /** * The data in the keychain item cannot be accessed after a restart until the device has been -@@ -17,7 +72,7 @@ export const AFTER_FIRST_UNLOCK: KeychainAccessibilityConstant = ExpoSecureStore - * from a backup. - */ - export const AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY: KeychainAccessibilityConstant = -- ExpoSecureStore.AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY; -+ ExpoSecureStore.AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY; - - // @needsAudit - /** -@@ -34,7 +89,7 @@ export const ALWAYS: KeychainAccessibilityConstant = ExpoSecureStore.ALWAYS; - * store an entry. If the user removes their passcode, the entry will be deleted. - */ - export const WHEN_PASSCODE_SET_THIS_DEVICE_ONLY: KeychainAccessibilityConstant = -- ExpoSecureStore.WHEN_PASSCODE_SET_THIS_DEVICE_ONLY; -+ ExpoSecureStore.WHEN_PASSCODE_SET_THIS_DEVICE_ONLY; - - // @needsAudit - /** -@@ -43,7 +98,7 @@ export const WHEN_PASSCODE_SET_THIS_DEVICE_ONLY: KeychainAccessibilityConstant = - * @deprecated Use an accessibility level that provides some user protection, such as `AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY`. - */ - export const ALWAYS_THIS_DEVICE_ONLY: KeychainAccessibilityConstant = -- ExpoSecureStore.ALWAYS_THIS_DEVICE_ONLY; -+ ExpoSecureStore.ALWAYS_THIS_DEVICE_ONLY; - - // @needsAudit - /** -@@ -57,62 +112,80 @@ export const WHEN_UNLOCKED: KeychainAccessibilityConstant = ExpoSecureStore.WHEN - * a backup. - */ - export const WHEN_UNLOCKED_THIS_DEVICE_ONLY: KeychainAccessibilityConstant = -- ExpoSecureStore.WHEN_UNLOCKED_THIS_DEVICE_ONLY; -+ ExpoSecureStore.WHEN_UNLOCKED_THIS_DEVICE_ONLY; - - // @needsAudit - export type SecureStoreOptions = { -- /** -- * - Android: Equivalent of the public/private key pair `Alias`. -- * - iOS: The item's service, equivalent to [`kSecAttrService`](https://developer.apple.com/documentation/security/ksecattrservice/). -- * > If the item is set with the `keychainService` option, it will be required to later fetch the value. -- */ -- keychainService?: string; -- /** -- * Option responsible for enabling the usage of the user authentication methods available on the device while -- * accessing data stored in SecureStore. -- * - Android: Equivalent to [`setUserAuthenticationRequired(true)`](https://developer.android.com/reference/android/security/keystore/KeyGenParameterSpec.Builder#setUserAuthenticationRequired(boolean)) -- * (requires API 23). -- * - iOS: Equivalent to [`biometryCurrentSet`](https://developer.apple.com/documentation/security/secaccesscontrolcreateflags/2937192-biometrycurrentset). -- * Complete functionality is unlocked only with a freshly generated key - this would not work in tandem with the `keychainService` -- * value used for the others non-authenticated operations. -- * -- * This option works slightly differently across platforms: On Android, user authentication is required for all operations. -- * On iOS, the user is prompted to authenticate only when reading or updating an existing value (not when creating a new one). -- * -- * Warning: This option is not supported in Expo Go when biometric authentication is available due to a missing NSFaceIDUsageDescription. -- * In release builds or when using continuous native generation, make sure to use the `expo-secure-store` config plugin. -- * -- */ -- requireAuthentication?: boolean; -- /** -- * Custom message displayed to the user while `requireAuthentication` option is turned on. -- */ -- authenticationPrompt?: string; -- /** -- * Specifies when the stored entry is accessible, using iOS's `kSecAttrAccessible` property. -- * @see Apple's documentation on [keychain item accessibility](https://developer.apple.com/documentation/security/ksecattraccessible/). -- * @default SecureStore.WHEN_UNLOCKED -- * @platform ios -- */ -- keychainAccessible?: KeychainAccessibilityConstant; -+ /** -+ * - Android: Equivalent of the public/private key pair `Alias`. -+ * - iOS: The item's service, equivalent to [`kSecAttrService`](https://developer.apple.com/documentation/security/ksecattrservice/). -+ * > If the item is set with the `keychainService` option, it will be required to later fetch the value. -+ */ -+ keychainService?: string; -+ /** -+ * Option responsible for enabling the usage of the user authentication methods available on the device while -+ * accessing data stored in SecureStore. -+ * - Android: Equivalent to [`setUserAuthenticationRequired(true)`](https://developer.android.com/reference/android/security/keystore/KeyGenParameterSpec.Builder#setUserAuthenticationRequired(boolean)) -+ * (requires API 23). -+ * - iOS: Equivalent to [`biometryCurrentSet`](https://developer.apple.com/documentation/security/secaccesscontrolcreateflags/2937192-biometrycurrentset). -+ * Complete functionality is unlocked only with a freshly generated key - this would not work in tandem with the `keychainService` -+ * value used for the others non-authenticated operations. -+ * -+ * This option works slightly differently across platforms: On Android, user authentication is required for all operations. -+ * On iOS, the user is prompted to authenticate only when reading or updating an existing value (not when creating a new one). -+ * -+ * Warning: This option is not supported in Expo Go when biometric authentication is available due to a missing NSFaceIDUsageDescription. -+ * In release builds or when using continuous native generation, make sure to use the `expo-secure-store` config plugin. -+ * -+ * > **Note:** This library requires a real device for testing since emulators/simulators do not require biometric authentication when retrieving secrets, unlike real iOS devices. -+ */ -+ requireAuthentication?: boolean; -+ /** -+ * Custom message displayed to the user while `requireAuthentication` option is turned on. -+ */ -+ authenticationPrompt?: string; -+ /** -+ * Specifies when the stored entry is accessible, using iOS's `kSecAttrAccessible` property. -+ * @see Apple's documentation on [keychain item accessibility](https://developer.apple.com/documentation/security/ksecattraccessible/). -+ * @default SecureStore.WHEN_UNLOCKED -+ * @platform ios -+ */ -+ keychainAccessible?: KeychainAccessibilityConstant; -+ -+ /** -+ * Specifies the access group the stored entry belongs to. -+ * @see Apple's documentation on [Sharing access to keychain items among a collection of apps](https://developer.apple.com/documentation/security/sharing-access-to-keychain-items-among-a-collection-of-apps). -+ * @platform ios -+ */ -+ accessGroup?: string; - -- /** -- * Specifies the access group the stored entry belongs to. -- * @see Apple's documentation on [Sharing access to keychain items among a collection of apps](https://developer.apple.com/documentation/security/sharing-access-to-keychain-items-among-a-collection-of-apps). -- * @platform ios -- */ -- accessGroup?: string; -+ /** -+ * This flag enables users to authenticate using Lock Screen Knowledge Factor (e.g. PIN, pattern or password). -+ * For sensitive apps, it is recommended not having biometric fall back to such factor. -+ * @see: https://developer.android.com/security/fraud-prevention/authentication -+ * -+ * @default false -+ * @platform android -+ * @platform ios -+ */ -+ enableDeviceFallback?: boolean; - -- /** -- * This flag enables users to authenticate using Lock Screen Knowledge Factor (e.g. PIN, pattern or password). -- * For sensitive apps, it is recommended not having biometric fall back to such factor. -- * @see: https://developer.android.com/security/fraud-prevention/authentication -- * -- * @default false -- * @platform android -- * @platform ios -- */ -- enableDeviceFallback?: boolean; -+ /** -+ * When this flag is set to true, the get methods of SecureStore will return a two-element array. The first value will be the original value returned when this flag is set to false. -+ * The second value is the authentication type used to read the value from the AUTH_TYPE object. -+ * As for the set function, the returned value will simply be AUTH_TYPE. -+ * -+ * @warning -+ * If the iOS device supports biometrics and the user falls back to device credentials, it will not be detected. -+ * This is not the case on Android, but we cannot specify the exact type of biometrics (e.g. fingerprint or face scan). -+ * Whether the type is detected correctly depends on the platform and its native implementation. -+ * This should be treated as more of a hint. -+ * -+ * @default false -+ * @platform android -+ * @platform ios -+ */ -+ returnUsedAuthenticationType?: boolean; +@@ -113,6 +168,23 @@ export type SecureStoreOptions = { + * @platform ios + */ + enableDeviceFallback?: boolean; ++ ++ /** ++ * When this flag is set to true, the get methods of SecureStore will return a two-element array. The first value will be the original value returned when this flag is set to false. ++ * The second value is the authentication type used to read the value from the AUTH_TYPE object. ++ * As for the set function, the returned value will simply be AUTH_TYPE. ++ * ++ * @warning ++ * If the iOS device supports biometrics and the user falls back to device credentials, it will not be detected. ++ * This is not the case on Android, but we cannot specify the exact type of biometrics (e.g. fingerprint or face scan). ++ * Whether the type is detected correctly depends on the platform and its native implementation. ++ * This should be treated as more of a hint. ++ * ++ * @default false ++ * @platform android ++ * @platform ios ++ */ ++ returnUsedAuthenticationType?: boolean; }; // @needsAudit -@@ -124,7 +197,7 @@ export type SecureStoreOptions = { - * on the current device. Currently, this resolves `true` on Android and iOS only. - */ - export async function isAvailableAsync(): Promise { -- return !!ExpoSecureStore.getValueWithKeyAsync; -+ return !!ExpoSecureStore.getValueWithKeyAsync; - } - - // @needsAudit -@@ -137,12 +210,12 @@ export async function isAvailableAsync(): Promise { - * @return A promise that rejects if the value can't be deleted. - */ - export async function deleteItemAsync( -- key: string, -- options: SecureStoreOptions = {} -+ key: string, -+ options: SecureStoreOptions = {} - ): Promise { -- ensureValidKey(key); -+ ensureValidKey(key); - -- await ExpoSecureStore.deleteValueWithKeyAsync(key, options); -+ await ExpoSecureStore.deleteValueWithKeyAsync(key, options); - } - - // @needsAudit -@@ -159,12 +232,12 @@ export async function deleteItemAsync( +@@ -159,10 +231,10 @@ export async function deleteItemAsync( * > After a key has been invalidated, it becomes impossible to read its value. * > This only applies to values stored with `requireAuthentication` set to `true`. */ -export async function getItemAsync( -- key: string, ++export async function getItemAsync( + key: string, - options: SecureStoreOptions = {} -): Promise { -- ensureValidKey(key); -- return await ExpoSecureStore.getValueWithKeyAsync(key, options); -+export async function getItemAsync( -+ key: string, -+ options: R | EmptyObject = {} ++ options: R | EmptyObject = {} +): Promise> { -+ ensureValidKey(key); -+ return await ExpoSecureStore.getValueWithKeyAsync(key, options); + ensureValidKey(key); + return await ExpoSecureStore.getValueWithKeyAsync(key, options); } - - // @needsAudit -@@ -172,24 +245,24 @@ export async function getItemAsync( - * Stores a key–value pair. - * - * @param key The key to associate with the stored value. Keys may contain alphanumeric characters, `.`, `-`, and `_`. -- * @param value The value to store. Size limit is 2048 bytes. -+ * @param value The value to store. - * @param options An [`SecureStoreOptions`](#securestoreoptions) object. +@@ -177,11 +249,11 @@ export async function getItemAsync( * * @return A promise that rejects if value cannot be stored on the device. */ -export async function setItemAsync( -- key: string, -- value: string, ++export async function setItemAsync( + key: string, + value: string, - options: SecureStoreOptions = {} -): Promise { -- ensureValidKey(key); -- if (!isValidValue(value)) { -- throw new Error( -- `Invalid value provided to SecureStore. Values must be strings; consider JSON-encoding your values if they are serializable.` -- ); -- } -+export async function setItemAsync( -+ key: string, -+ value: string, -+ options: R | EmptyObject = {} ++ options: R | EmptyObject = {} +): Promise> { -+ ensureValidKey(key); -+ if (!isValidValue(value)) { -+ throw new Error( -+ `Invalid value provided to SecureStore. Values must be strings; consider JSON-encoding your values if they are serializable.` -+ ); -+ } + ensureValidKey(key); + if (!isValidValue(value)) { + throw new Error( +@@ -189,7 +261,7 @@ export async function setItemAsync( + ); + } - await ExpoSecureStore.setValueWithKeyAsync(value, key, options); -+ return await ExpoSecureStore.setValueWithKeyAsync(value, key, options); ++ return await ExpoSecureStore.setValueWithKeyAsync(value, key, options); } /** -@@ -197,19 +270,23 @@ export async function setItemAsync( - * > **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. - * - * @param key The key to associate with the stored value. Keys may contain alphanumeric characters, `.`, `-`, and `_`. -- * @param value The value to store. Size limit is 2048 bytes. -+ * @param value The value to store. +@@ -201,7 +273,11 @@ export async function setItemAsync( * @param options An [`SecureStoreOptions`](#securestoreoptions) object. * */ -export function setItem(key: string, value: string, options: SecureStoreOptions = {}): void { -- ensureValidKey(key); -- if (!isValidValue(value)) { -- throw new Error( -- `Invalid value provided to SecureStore. Values must be strings; consider JSON-encoding your values if they are serializable.` -- ); -- } +export function setItem( -+ key: string, -+ value: string, -+ options: R | EmptyObject = {} ++ key: string, ++ value: string, ++ options: R | EmptyObject = {} +): SecureStoreSetFeedback { -+ ensureValidKey(key); -+ if (!isValidValue(value)) { -+ throw new Error( -+ `Invalid value provided to SecureStore. Values must be strings; consider JSON-encoding your values if they are serializable.` -+ ); -+ } - -- return ExpoSecureStore.setValueWithKeySync(value, key, options); -+ return ExpoSecureStore.setValueWithKeySync(value, key, options); - } - - /** -@@ -222,9 +299,12 @@ export function setItem(key: string, value: string, options: SecureStoreOptions + ensureValidKey(key); + if (!isValidValue(value)) { + throw new Error( +@@ -222,7 +298,10 @@ export function setItem(key: string, value: string, options: SecureStoreOptions * @return Previously stored value. It resolves with `null` if there is no entry * for the given key or if the key has been invalidated. */ -export function getItem(key: string, options: SecureStoreOptions = {}): string | null { -- ensureValidKey(key); -- return ExpoSecureStore.getValueWithKeySync(key, options); +export function getItem( -+ key: string, -+ options: R | EmptyObject = {} ++ key: string, ++ options: R | EmptyObject = {} +): SecureStoreGetFeedback { -+ ensureValidKey(key); -+ return ExpoSecureStore.getValueWithKeySync(key, options); + ensureValidKey(key); + return ExpoSecureStore.getValueWithKeySync(key, options); } - - /** -@@ -234,7 +314,7 @@ export function getItem(key: string, options: SecureStoreOptions = {}): string | - * @platform ios - */ - export function canUseBiometricAuthentication(): boolean { -- return ExpoSecureStore.canUseBiometricAuthentication(); -+ return ExpoSecureStore.canUseBiometricAuthentication(); - } - - /** -@@ -244,7 +324,7 @@ export function canUseBiometricAuthentication(): boolean { - * @platform ios - */ - export function canUseDeviceCredentialsAuthentication(): boolean { -- return ExpoSecureStore.canUseDeviceCredentialsAuthentication(); -+ return ExpoSecureStore.canUseDeviceCredentialsAuthentication(); - } - - function ensureValidKey(key: string) { diff --git a/patches/expo-secure-store/expo-secure-store+14.2.4+003+force-authentication-on-save.patch b/patches/expo-secure-store/expo-secure-store+14.2.4+003+force-authentication-on-save.patch index fc085c205b11..59e74054b162 100644 --- a/patches/expo-secure-store/expo-secure-store+14.2.4+003+force-authentication-on-save.patch +++ b/patches/expo-secure-store/expo-secure-store+14.2.4+003+force-authentication-on-save.patch @@ -1,8 +1,8 @@ diff --git a/node_modules/expo-secure-store/build/SecureStore.d.ts b/node_modules/expo-secure-store/build/SecureStore.d.ts -index 40ec045..be49e3b 100644 +index 7097a8a..975d75b 100644 --- a/node_modules/expo-secure-store/build/SecureStore.d.ts +++ b/node_modules/expo-secure-store/build/SecureStore.d.ts -@@ -154,6 +154,20 @@ export type SecureStoreOptions = { +@@ -153,6 +153,20 @@ export type SecureStoreOptions = { * @platform ios */ returnUsedAuthenticationType?: boolean; @@ -24,21 +24,21 @@ index 40ec045..be49e3b 100644 /** * Returns whether the SecureStore API is enabled on the current device. This does not check the app diff --git a/node_modules/expo-secure-store/ios/SecureStoreModule.swift b/node_modules/expo-secure-store/ios/SecureStoreModule.swift -index 5280020..3060cbd 100644 +index 7764c8e..02f837c 100644 --- a/node_modules/expo-secure-store/ios/SecureStoreModule.swift +++ b/node_modules/expo-secure-store/ios/SecureStoreModule.swift -@@ -33,6 +33,10 @@ public final class SecureStoreModule: Module { +@@ -35,6 +35,10 @@ public final class SecureStoreModule: Module { throw InvalidKeyException() } -+ if options.requireAuthentication && options.forceAuthenticationOnSave { ++ if options.requireAuthentication && options.forceAuthenticationOnSave { + try await triggerPolicy(options: options) + } + let result = try set(value: value, with: key, options: options) return wrapResultWithFeedback(action: .set, result: result, options: options).value -@@ -188,6 +192,32 @@ public final class SecureStoreModule: Module { +@@ -190,6 +194,32 @@ public final class SecureStoreModule: Module { } } @@ -97,28 +97,28 @@ index b95eca7..321c7e5 100644 + } +} diff --git a/node_modules/expo-secure-store/src/SecureStore.ts b/node_modules/expo-secure-store/src/SecureStore.ts -index e32ebdf..3fbaf02 100644 +index d44c0bb..c4b95ae 100644 --- a/node_modules/expo-secure-store/src/SecureStore.ts +++ b/node_modules/expo-secure-store/src/SecureStore.ts -@@ -186,6 +186,21 @@ export type SecureStoreOptions = { - * @platform ios - */ - returnUsedAuthenticationType?: boolean; +@@ -185,6 +185,21 @@ export type SecureStoreOptions = { + * @platform ios + */ + returnUsedAuthenticationType?: boolean; + -+ /** -+ * On iOS, the system does not ask for auth when saving the value to the SecureStore. -+ * The Android however, displays the authentication prompt when saving the value. -+ * To keep the behavior on every platform similar as much as possible, -+ * setting this flag to true will ensure that authentication is required when saving a value to the store. -+ * -+ * @warning: This flag only works for the asynchronous version of the SecureStore save method. -+ * It should only be considered an improvement to the user experience; it does not prevent the user -+ * from saving the value without authenticating using other methods (e.g. by directly modifying the keychain). -+ * For it to take effect, the 'requireAuthentication' flag must be set to true. -+ * @default false -+ * @platform ios -+ */ -+ forceAuthenticationOnSave?: boolean; ++ /** ++ * On iOS, the system does not ask for auth when saving the value to the SecureStore. ++ * The Android however, displays the authentication prompt when saving the value. ++ * To keep the behavior on every platform similar as much as possible, ++ * setting this flag to true will ensure that authentication is required when saving a value to the store. ++ * ++ * @warning: This flag only works for the asynchronous version of the SecureStore save method. ++ * It should only be considered an improvement to the user experience; it does not prevent the user ++ * from saving the value without authenticating using other methods (e.g. by directly modifying the keychain). ++ * For it to take effect, the 'requireAuthentication' flag must be set to true. ++ * @default false ++ * @platform ios ++ */ ++ forceAuthenticationOnSave?: boolean; }; // @needsAudit diff --git a/patches/expo-secure-store/expo-secure-store+14.2.4+004+fail-on-update.patch b/patches/expo-secure-store/expo-secure-store+14.2.4+004+fail-on-update.patch index 175b02cb71d8..c3acba54c616 100644 --- a/patches/expo-secure-store/expo-secure-store+14.2.4+004+fail-on-update.patch +++ b/patches/expo-secure-store/expo-secure-store+14.2.4+004+fail-on-update.patch @@ -28,10 +28,10 @@ index 4e30440..16674b8 100644 enum class SecureStoreAuthType(index: Int) { diff --git a/node_modules/expo-secure-store/build/SecureStore.d.ts b/node_modules/expo-secure-store/build/SecureStore.d.ts -index 4543001..0f3ef89 100644 +index f1e3610..8954c59 100644 --- a/node_modules/expo-secure-store/build/SecureStore.d.ts +++ b/node_modules/expo-secure-store/build/SecureStore.d.ts -@@ -168,6 +168,16 @@ export type SecureStoreOptions = { +@@ -167,6 +167,16 @@ export type SecureStoreOptions = { * @platform ios */ forceAuthenticationOnSave?: boolean; @@ -49,14 +49,14 @@ index 4543001..0f3ef89 100644 /** * Returns whether the SecureStore API is enabled on the current device. This does not check the app diff --git a/node_modules/expo-secure-store/ios/SecureStoreModule.swift b/node_modules/expo-secure-store/ios/SecureStoreModule.swift -index 3060cbd..5a81b11 100644 +index 02f837c..47282db 100644 --- a/node_modules/expo-secure-store/ios/SecureStoreModule.swift +++ b/node_modules/expo-secure-store/ios/SecureStoreModule.swift -@@ -167,6 +167,9 @@ public final class SecureStoreModule: Module { +@@ -169,6 +169,9 @@ public final class SecureStoreModule: Module { SecItemDelete(query(with: key, options: options, requireAuthentication: !options.requireAuthentication) as CFDictionary) return true case errSecDuplicateItem: -+ if options.failOnUpdate { ++ if options.failOnUpdate { + throw SecureStoreRuntimeError("Key already exists") + } return try update(value: value, with: key, options: options) @@ -77,32 +77,24 @@ index 321c7e5..6a787b0 100644 @available(iOS 11.2, *) diff --git a/node_modules/expo-secure-store/src/SecureStore.ts b/node_modules/expo-secure-store/src/SecureStore.ts -index 3fbaf02..9c3463c 100644 +index c4b95ae..0bedcaa 100644 --- a/node_modules/expo-secure-store/src/SecureStore.ts +++ b/node_modules/expo-secure-store/src/SecureStore.ts -@@ -201,6 +201,17 @@ export type SecureStoreOptions = { - * @platform ios - */ - forceAuthenticationOnSave?: boolean; +@@ -200,6 +200,17 @@ export type SecureStoreOptions = { + * @platform ios + */ + forceAuthenticationOnSave?: boolean; + -+ /** -+ * If the key has already been stored, the save function will throw an error instead of overwriting it. -+ * The behaviour differs slightly depending on the platform. -+ * On Android, an error is thrown before the authentication prompt. On iOS, an error is thrown after authentication. -+ * -+ * @default false -+ * @platform ios -+ * @platform android -+ */ -+ failOnUpdate?: boolean; ++ /** ++ * If the key has already been stored, the save function will throw an error instead of overwriting it. ++ * The behaviour differs slightly depending on the platform. ++ * On Android, an error is thrown before the authentication prompt. On iOS, an error is thrown after authentication. ++ * ++ * @default false ++ * @platform ios ++ * @platform android ++ */ ++ failOnUpdate?: boolean; }; // @needsAudit -@@ -341,7 +352,6 @@ export function canUseBiometricAuthentication(): boolean { - export function canUseDeviceCredentialsAuthentication(): boolean { - return ExpoSecureStore.canUseDeviceCredentialsAuthentication(); - } -- - function ensureValidKey(key: string) { - if (!isValidKey(key)) { - throw new Error( diff --git a/patches/expo-secure-store/expo-secure-store+14.2.4+005+force-read-authentication-on-simulators.patch b/patches/expo-secure-store/expo-secure-store+14.2.4+005+force-read-authentication-on-simulators.patch index 062542442189..f65e0a31540a 100644 --- a/patches/expo-secure-store/expo-secure-store+14.2.4+005+force-read-authentication-on-simulators.patch +++ b/patches/expo-secure-store/expo-secure-store+14.2.4+005+force-read-authentication-on-simulators.patch @@ -1,8 +1,8 @@ diff --git a/node_modules/expo-secure-store/build/SecureStore.d.ts b/node_modules/expo-secure-store/build/SecureStore.d.ts -index a6856a9..59bdee0 100644 +index 836958c..29531c5 100644 --- a/node_modules/expo-secure-store/build/SecureStore.d.ts +++ b/node_modules/expo-secure-store/build/SecureStore.d.ts -@@ -178,6 +178,18 @@ export type SecureStoreOptions = { +@@ -177,6 +177,18 @@ export type SecureStoreOptions = { * @platform android */ failOnUpdate?: boolean; @@ -22,11 +22,11 @@ index a6856a9..59bdee0 100644 /** * Returns whether the SecureStore API is enabled on the current device. This does not check the app diff --git a/node_modules/expo-secure-store/ios/SecureStoreModule.swift b/node_modules/expo-secure-store/ios/SecureStoreModule.swift -index 5a81b11..35a339a 100644 +index 47282db..dcbe242 100644 --- a/node_modules/expo-secure-store/ios/SecureStoreModule.swift +++ b/node_modules/expo-secure-store/ios/SecureStoreModule.swift -@@ -17,6 +17,11 @@ public final class SecureStoreModule: Module { - Constant("WHEN_UNLOCKED_THIS_DEVICE_ONLY") { SecureStoreAccessible.whenUnlockedThisDeviceOnly.rawValue } +@@ -19,6 +19,11 @@ public final class SecureStoreModule: Module { + ]) AsyncFunction("getValueWithKeyAsync") { (key: String, options: SecureStoreOptions) in + #if targetEnvironment(simulator) @@ -52,34 +52,26 @@ index 6a787b0..8f110dd 100644 @available(iOS 11.2, *) diff --git a/node_modules/expo-secure-store/src/SecureStore.ts b/node_modules/expo-secure-store/src/SecureStore.ts -index 9c3463c..52fc89d 100644 +index 0bedcaa..bae5580 100644 --- a/node_modules/expo-secure-store/src/SecureStore.ts +++ b/node_modules/expo-secure-store/src/SecureStore.ts -@@ -212,6 +212,19 @@ export type SecureStoreOptions = { - * @platform android - */ - failOnUpdate?: boolean; +@@ -211,6 +211,19 @@ export type SecureStoreOptions = { + * @platform android + */ + failOnUpdate?: boolean; + -+ /** -+ * The LocalAuthentication behaves slightly differently on iOS simulators. -+ * In numerous cases, the authentication prompts are skipped on simulators (as opposed to real devices). -+ * Setting this flag to true forces the prompt to appear on simulators when a value with the `requireAuthentication` flag set to true is read. -+ * This is purely for testing the app on simulators, in cases where the prompt does not appear when the value is read. -+ * This has no effect on real devices. -+ * -+ * @warning: This flag only works for the asynchronous version of the SecureStore read method. -+ * @default false -+ * @platform ios -+ */ -+ forceReadAuthenticationOnSimulators?: boolean; ++ /** ++ * The LocalAuthentication behaves slightly differently on iOS simulators. ++ * In numerous cases, the authentication prompts are skipped on simulators (as opposed to real devices). ++ * Setting this flag to true forces the prompt to appear on simulators when a value with the `requireAuthentication` flag set to true is read. ++ * This is purely for testing the app on simulators, in cases where the prompt does not appear when the value is read. ++ * This has no effect on real devices. ++ * ++ * @warning: This flag only works for the asynchronous version of the SecureStore read method. ++ * @default false ++ * @platform ios ++ */ ++ forceReadAuthenticationOnSimulators?: boolean; }; // @needsAudit -@@ -352,6 +365,7 @@ export function canUseBiometricAuthentication(): boolean { - export function canUseDeviceCredentialsAuthentication(): boolean { - return ExpoSecureStore.canUseDeviceCredentialsAuthentication(); - } -+ - function ensureValidKey(key: string) { - if (!isValidKey(key)) { - throw new Error( From 1109576284c75cc0f40e7c1729c45c4c9d56806e Mon Sep 17 00:00:00 2001 From: Jakub Korytko Date: Tue, 2 Dec 2025 13:40:28 +0100 Subject: [PATCH 4/7] Update details.md --- patches/expo-secure-store/details.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/patches/expo-secure-store/details.md b/patches/expo-secure-store/details.md index aaf1bb1ab02f..6bc1a8c084b1 100644 --- a/patches/expo-secure-store/details.md +++ b/patches/expo-secure-store/details.md @@ -19,7 +19,11 @@ - Reason: ``` - This patch makes the read and write methods return the authentication type used to access the store. + This patch adds the `returnUsedAuthenticationType` flag. + When this flag is set to true, the get methods of SecureStore will return a two-element array. + The first value will be the original value returned when this flag is set to false. + The second value is the authentication type used to read the value from the AUTH_TYPE object. + As for the set function, the returned value will simply be AUTH_TYPE. It uses a pre-defined constant that mimics an enum and can also be imported directly from the app. ``` From cc20346fb1fc970cf9dcd10d3393aecb1ee4968f Mon Sep 17 00:00:00 2001 From: Jakub Korytko Date: Tue, 2 Dec 2025 19:13:32 +0100 Subject: [PATCH 5/7] Fix Android expo-secure-store patch --- ...re-store+14.2.4+002+return-auth-type.patch | 82 +++++++++---------- 1 file changed, 41 insertions(+), 41 deletions(-) diff --git a/patches/expo-secure-store/expo-secure-store+14.2.4+002+return-auth-type.patch b/patches/expo-secure-store/expo-secure-store+14.2.4+002+return-auth-type.patch index 74b39840a335..c8cdfcf5ac02 100644 --- a/patches/expo-secure-store/expo-secure-store+14.2.4+002+return-auth-type.patch +++ b/patches/expo-secure-store/expo-secure-store+14.2.4+002+return-auth-type.patch @@ -5,7 +5,7 @@ index 4a1a009..980ef0a 100644 @@ -20,12 +20,12 @@ class AuthenticationHelper( ) { private var isAuthenticating = false - + - suspend fun authenticateCipher(cipher: Cipher, requiresAuthentication: Boolean, title: String, enableDeviceFallback: Boolean): Cipher { - if (requiresAuthentication) { - return openAuthenticationPrompt(cipher, title, enableDeviceFallback).cryptoObject?.cipher @@ -18,25 +18,25 @@ index 4a1a009..980ef0a 100644 - return cipher + return promptResult } - + private suspend fun openAuthenticationPrompt( diff --git a/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/SecureStoreModule.kt b/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/SecureStoreModule.kt index 3f21552..d209efc 100644 --- a/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/SecureStoreModule.kt +++ b/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/SecureStoreModule.kt @@ -36,23 +36,23 @@ open class SecureStoreModule : Module() { - + AsyncFunction("setValueWithKeyAsync") Coroutine { value: String?, key: String?, options: SecureStoreOptions -> key ?: throw NullKeyException() - return@Coroutine setItemImpl(key, value, options, false) -+ return@Coroutine narrowSecureStoreFeedback(SecureStoreFeedbackAction.SET, setItemImpl(key, value, options, false), options) ++ return@Coroutine narrowSecureStoreFeedback(SecureStoreFeedbackAction.SET, setItemImpl(key, value, options, false), options).value } - + AsyncFunction("getValueWithKeyAsync") Coroutine { key: String, options: SecureStoreOptions -> - return@Coroutine getItemImpl(key, options) -+ return@Coroutine narrowSecureStoreFeedback(SecureStoreFeedbackAction.GET,getItemImpl(key, options), options) ++ return@Coroutine narrowSecureStoreFeedback(SecureStoreFeedbackAction.GET,getItemImpl(key, options), options).value } - + Function("setValueWithKeySync") { value: String?, key: String?, options: SecureStoreOptions -> key ?: throw NullKeyException() return@Function runBlocking { @@ -44,18 +44,18 @@ index 3f21552..d209efc 100644 + return@runBlocking narrowSecureStoreFeedback(SecureStoreFeedbackAction.SET, setItemImpl(key, value, options, keyIsInvalidated = false), options).value } } - + Function("getValueWithKeySync") { key: String, options: SecureStoreOptions -> return@Function runBlocking { - return@runBlocking getItemImpl(key, options) + return@runBlocking narrowSecureStoreFeedback(SecureStoreFeedbackAction.GET, getItemImpl(key, options), options).value } } - + @@ -94,7 +94,23 @@ open class SecureStoreModule : Module() { } } - + - private suspend fun getItemImpl(key: String, options: SecureStoreOptions): String? { + private suspend fun narrowSecureStoreFeedback(action: String, feedback: SecureStoreOriginalFeedback, options: SecureStoreOptions): SecureStoreNarrowedFeedback { + if (!options.returnUsedAuthenticationType) { @@ -84,19 +84,19 @@ index 3f21552..d209efc 100644 - return null + return SecureStoreOriginalFeedback(null) } - + - private suspend fun readJSONEncodedItem(key: String, prefs: SharedPreferences, options: SecureStoreOptions): String? { + private suspend fun readJSONEncodedItem(key: String, prefs: SharedPreferences, options: SecureStoreOptions): SecureStoreOriginalFeedback { val keychainAwareKey = createKeychainAwareKey(key, options.keychainService) - + val legacyEncryptedItemString = prefs.getString(key, null) @@ -126,7 +142,7 @@ open class SecureStoreModule : Module() { "" } - + - encryptedItemString ?: return null + encryptedItemString ?: return SecureStoreOriginalFeedback(null) - + val encryptedItem: JSONObject = try { JSONObject(encryptedItemString) @@ -149,13 +165,13 @@ open class SecureStoreModule : Module() { @@ -136,12 +136,12 @@ index 3f21552..d209efc 100644 @@ -184,7 +200,7 @@ open class SecureStoreModule : Module() { } } - + - private suspend fun setItemImpl(key: String, value: String?, options: SecureStoreOptions, keyIsInvalidated: Boolean) { + private suspend fun setItemImpl(key: String, value: String?, options: SecureStoreOptions, keyIsInvalidated: Boolean): SecureStoreOriginalFeedback { val keychainAwareKey = createKeychainAwareKey(key, options.keychainService) val prefs: SharedPreferences = getSharedPreferences() - + @@ -193,7 +209,7 @@ open class SecureStoreModule : Module() { if (!success) { throw WriteException("Could not write a null value to SecureStore", key, options.keychainService) @@ -149,7 +149,7 @@ index 3f21552..d209efc 100644 - return + return SecureStoreOriginalFeedback(null) } - + try { @@ -210,7 +226,8 @@ open class SecureStoreModule : Module() { back a value. @@ -160,7 +160,7 @@ index 3f21552..d209efc 100644 + val encryptedItem = encryptResult.value encryptedItem.put(SCHEME_PROPERTY, AESEncryptor.NAME) saveEncryptedItem(encryptedItem, prefs, keychainAwareKey, options.requireAuthentication, options.keychainService) - + @@ -218,6 +235,9 @@ open class SecureStoreModule : Module() { if (prefs.contains(key)) { prefs.edit().remove(key).apply() @@ -177,7 +177,7 @@ index 0455fd6..4e30440 100644 +++ b/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/SecureStoreOptions.kt @@ -1,5 +1,6 @@ package expo.modules.securestore - + +import androidx.biometric.BiometricPrompt import expo.modules.kotlin.records.Field import expo.modules.kotlin.records.Record @@ -268,7 +268,7 @@ index 1cd187f..d927ff1 100644 val secretKey = keyStoreEntry.secretKey val cipher = Cipher.getInstance(AES_CIPHER) cipher.init(Cipher.ENCRYPT_MODE, secretKey) - + val gcmSpec = cipher.parameters.getParameterSpec(GCMParameterSpec::class.java) - val authenticatedCipher = authenticationHelper.authenticateCipher(cipher, requireAuthentication, authenticationPrompt, enableDeviceFallback) + var promptResult: BiometricPrompt.AuthenticationResult? = null @@ -280,11 +280,11 @@ index 1cd187f..d927ff1 100644 + } else { + authenticatedCipher = cipher + } - + - return createEncryptedItemWithCipher(plaintextValue, authenticatedCipher, gcmSpec) + return SecureStoreOriginalFeedback(createEncryptedItemWithCipher(plaintextValue, authenticatedCipher, gcmSpec), promptResult) } - + internal fun createEncryptedItemWithCipher( @@ -121,7 +131,7 @@ class AESEncryptor : KeyBasedEncryptor { keyStoreEntry: KeyStore.SecretKeyEntry, @@ -313,7 +313,7 @@ index 1cd187f..d927ff1 100644 + + return SecureStoreOriginalFeedback(String(unlockedCipher.doFinal(ciphertextBytes), StandardCharsets.UTF_8), promptResult) } - + companion object { diff --git a/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/encryptors/HybridAESEncryptor.kt b/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/encryptors/HybridAESEncryptor.kt index 5f8bbfd..7527001 100644 @@ -350,7 +350,7 @@ index 39459ff..a531aff 100644 --- a/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/encryptors/KeyBasedEncryptor.kt +++ b/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/encryptors/KeyBasedEncryptor.kt @@ -2,6 +2,7 @@ package expo.modules.securestore.encryptors - + import expo.modules.securestore.AuthenticationHelper import expo.modules.securestore.SecureStoreOptions +import expo.modules.securestore.SecureStoreOriginalFeedback @@ -363,7 +363,7 @@ index 39459ff..a531aff 100644 enableDeviceFallback: Boolean, - ): JSONObject + ): SecureStoreOriginalFeedback - + @Throws(GeneralSecurityException::class, JSONException::class) suspend fun decryptItem( @@ -36,5 +37,5 @@ interface KeyBasedEncryptor { @@ -564,7 +564,7 @@ index 1268979..7764c8e 100644 @@ -18,28 +18,36 @@ public final class SecureStoreModule: Module { "WHEN_UNLOCKED_THIS_DEVICE_ONLY": SecureStoreAccessible.whenUnlockedThisDeviceOnly.rawValue ]) - + - AsyncFunction("getValueWithKeyAsync") { (key: String, options: SecureStoreOptions) -> String? in - return try get(with: key, options: options) + AsyncFunction("getValueWithKeyAsync") { (key: String, options: SecureStoreOptions) in @@ -572,7 +572,7 @@ index 1268979..7764c8e 100644 + + return wrapResultWithFeedback(action: .get, result: result, options: options).value } - + - Function("getValueWithKeySync") { (key: String, options: SecureStoreOptions) -> String? in - return try get(with: key, options: options) + Function("getValueWithKeySync") { (key: String, options: SecureStoreOptions) in @@ -580,35 +580,35 @@ index 1268979..7764c8e 100644 + + return wrapResultWithFeedback(action: .get, result: result, options: options).value } - + - AsyncFunction("setValueWithKeyAsync") { (value: String, key: String, options: SecureStoreOptions) -> Bool in + AsyncFunction("setValueWithKeyAsync") { (value: String, key: String, options: SecureStoreOptions) in guard let key = validate(for: key) else { throw InvalidKeyException() } - + - return try set(value: value, with: key, options: options) + let result = try set(value: value, with: key, options: options) + + return wrapResultWithFeedback(action: .set, result: result, options: options).value } - + - Function("setValueWithKeySync") {(value: String, key: String, options: SecureStoreOptions) -> Bool in + Function("setValueWithKeySync") {(value: String, key: String, options: SecureStoreOptions) in guard let key = validate(for: key) else { throw InvalidKeyException() } - + - return try set(value: value, with: key, options: options) + let result = try set(value: value, with: key, options: options) + + return wrapResultWithFeedback(action: .set, result: result, options: options).value } - + AsyncFunction("deleteValueWithKeyAsync") { (key: String, options: SecureStoreOptions) in @@ -53,18 +61,7 @@ public final class SecureStoreModule: Module { } - + Function("canUseBiometricAuthentication") {() -> Bool in - #if os(tvOS) - return false @@ -624,12 +624,12 @@ index 1268979..7764c8e 100644 - #endif + return areBiometricsEnabled() } - + Function("canUseDeviceCredentialsAuthentication") { () -> Bool in @@ -76,6 +73,41 @@ public final class SecureStoreModule: Module { return LAContext().canEvaluatePolicy(LAPolicy.deviceOwnerAuthentication, error: nil) } - + + private func getAuthType() -> AuthType { + if !areBiometricsEnabled() {return AuthType.credentials} + let biometryType = LAContext().biometryType @@ -673,7 +673,7 @@ index c9e843b..b95eca7 100644 --- a/node_modules/expo-secure-store/ios/SecureStoreOptions.swift +++ b/node_modules/expo-secure-store/ios/SecureStoreOptions.swift @@ -18,4 +18,71 @@ internal struct SecureStoreOptions: Record { - + @Field var enableDeviceFallback: Bool = false + @@ -751,7 +751,7 @@ index 4f253a1..d44c0bb 100644 @@ -1,8 +1,63 @@ import ExpoSecureStore from './ExpoSecureStore'; import { byteCountOverLimit, VALUE_BYTES_LIMIT } from './byteCounter'; - + +type EmptyObject = Record; +type SecureStoreSetFeedback = + R['returnUsedAuthenticationType'] extends true ? AuthType : void; @@ -760,7 +760,7 @@ index 4f253a1..d44c0bb 100644 + R extends SecureStoreOptions, +> = R['returnUsedAuthenticationType'] extends true ? [T | null, AuthType] : T | null; export type KeychainAccessibilityConstant = number; - + +/** + * Authentication type returned by the SecureStore after reading item or saving it to the store. + */ @@ -834,7 +834,7 @@ index 4f253a1..d44c0bb 100644 + */ + returnUsedAuthenticationType?: boolean; }; - + // @needsAudit @@ -159,10 +231,10 @@ export async function deleteItemAsync( * > After a key has been invalidated, it becomes impossible to read its value. @@ -868,11 +868,11 @@ index 4f253a1..d44c0bb 100644 @@ -189,7 +261,7 @@ export async function setItemAsync( ); } - + - await ExpoSecureStore.setValueWithKeyAsync(value, key, options); + return await ExpoSecureStore.setValueWithKeyAsync(value, key, options); } - + /** @@ -201,7 +273,11 @@ export async function setItemAsync( * @param options An [`SecureStoreOptions`](#securestoreoptions) object. From f6f4721f07c1520873ee5b9fba90c005370279d4 Mon Sep 17 00:00:00 2001 From: Jakub Korytko Date: Thu, 4 Dec 2025 12:41:47 +0100 Subject: [PATCH 6/7] Update enable device fallback patch --- ...re+14.2.4+001+enable-device-fallback.patch | 125 ++++++++++++------ 1 file changed, 86 insertions(+), 39 deletions(-) diff --git a/patches/expo-secure-store/expo-secure-store+14.2.4+001+enable-device-fallback.patch b/patches/expo-secure-store/expo-secure-store+14.2.4+001+enable-device-fallback.patch index 9a0b432e66d9..d5d5edad1a25 100644 --- a/patches/expo-secure-store/expo-secure-store+14.2.4+001+enable-device-fallback.patch +++ b/patches/expo-secure-store/expo-secure-store+14.2.4+001+enable-device-fallback.patch @@ -3,7 +3,7 @@ index a281b88..4a1a009 100644 --- a/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/AuthenticationHelper.kt +++ b/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/AuthenticationHelper.kt @@ -2,6 +2,7 @@ package expo.modules.securestore - + import android.annotation.SuppressLint import android.app.Activity +import android.app.KeyguardManager @@ -13,7 +13,7 @@ index a281b88..4a1a009 100644 @@ -19,9 +20,9 @@ class AuthenticationHelper( ) { private var isAuthenticating = false - + - suspend fun authenticateCipher(cipher: Cipher, requiresAuthentication: Boolean, title: String): Cipher { + suspend fun authenticateCipher(cipher: Cipher, requiresAuthentication: Boolean, title: String, enableDeviceFallback: Boolean): Cipher { if (requiresAuthentication) { @@ -23,7 +23,7 @@ index a281b88..4a1a009 100644 } return cipher @@ -29,7 +30,8 @@ class AuthenticationHelper( - + private suspend fun openAuthenticationPrompt( cipher: Cipher, - title: String @@ -34,7 +34,7 @@ index a281b88..4a1a009 100644 throw AuthenticationException("Biometric authentication requires Android API 23") @@ -41,11 +43,16 @@ class AuthenticationHelper( isAuthenticating = true - + try { - assertBiometricsSupport() + if (enableDeviceFallback) { @@ -45,16 +45,16 @@ index a281b88..4a1a009 100644 + val fragmentActivity = getCurrentActivity() as? FragmentActivity ?: throw AuthenticationException("Cannot display biometric prompt when the app is not in the foreground") - + - val authenticationPrompt = AuthenticationPrompt(fragmentActivity, context, title) + val authenticationPrompt = AuthenticationPrompt(fragmentActivity, context, title, enableDeviceFallback) - + return withContext(Dispatchers.Main.immediate) { return@withContext authenticationPrompt.authenticate(cipher) @@ -56,6 +63,14 @@ class AuthenticationHelper( } } - + + fun assertDeviceSecurity() { + val manager = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager + val isSecure = manager.isDeviceSecure @@ -67,40 +67,54 @@ index a281b88..4a1a009 100644 val biometricManager = BiometricManager.from(context) @SuppressLint("SwitchIntDef") // BiometricManager.BIOMETRIC_SUCCESS shouldn't do anything diff --git a/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/AuthenticationPrompt.kt b/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/AuthenticationPrompt.kt -index e5729cc..2543b0c 100644 +index e5729cc..ce448d6 100644 --- a/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/AuthenticationPrompt.kt +++ b/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/AuthenticationPrompt.kt @@ -1,5 +1,7 @@ package expo.modules.securestore - + +import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG +import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL import android.content.Context import androidx.biometric.BiometricPrompt import androidx.biometric.BiometricPrompt.PromptInfo -@@ -11,11 +13,12 @@ import kotlin.coroutines.resume +@@ -11,12 +13,23 @@ import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException import kotlin.coroutines.suspendCoroutine - + -class AuthenticationPrompt(private val currentActivity: FragmentActivity, context: Context, title: String) { +class AuthenticationPrompt(private val currentActivity: FragmentActivity, context: Context, title: String, enableDeviceFallback: Boolean) { + private var authType: Int = if (enableDeviceFallback) BIOMETRIC_STRONG or DEVICE_CREDENTIAL else BIOMETRIC_STRONG private var executor: Executor = ContextCompat.getMainExecutor(context) - private var promptInfo = PromptInfo.Builder() - .setTitle(title) +- private var promptInfo = PromptInfo.Builder() +- .setTitle(title) - .setNegativeButtonText(context.getString(android.R.string.cancel)) -+ .setAllowedAuthenticators(authType) - .build() - +- .build() ++ private var promptInfo = buildPromptInfo(context, title, enableDeviceFallback) ++ ++ private fun buildPromptInfo(context: Context, title: String, enableDeviceFallback: Boolean): PromptInfo { ++ var prompt = PromptInfo.Builder() ++ .setTitle(title) ++ .setAllowedAuthenticators(authType) ++ ++ if (!enableDeviceFallback) { ++ prompt = prompt. ++ setNegativeButtonText(context.getString(android.R.string.cancel)) ++ } ++ ++ return prompt.build() ++ } + suspend fun authenticate(cipher: Cipher): BiometricPrompt.AuthenticationResult? = + suspendCoroutine { continuation -> diff --git a/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/SecureStoreModule.kt b/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/SecureStoreModule.kt -index 0fef884..3f21552 100644 +index 0fef884..bf9689b 100644 --- a/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/SecureStoreModule.kt +++ b/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/SecureStoreModule.kt @@ -75,6 +75,15 @@ open class SecureStoreModule : Module() { } } - + + Function("canUseDeviceCredentialsAuthentication") { + return@Function try { + authenticationHelper.assertDeviceSecurity() @@ -121,7 +135,7 @@ index 0fef884..3f21552 100644 + val encryptedItem = mAESEncryptor.createEncryptedItem(value, secretKeyEntry, options.requireAuthentication, options.authenticationPrompt, authenticationHelper, options.enableDeviceFallback) encryptedItem.put(SCHEME_PROPERTY, AESEncryptor.NAME) saveEncryptedItem(encryptedItem, prefs, keychainAwareKey, options.requireAuthentication, options.keychainService) - + @@ -343,7 +352,11 @@ open class SecureStoreModule : Module() { return getKeyEntry(keyStoreEntryClass, encryptor, options, requireAuthentication) ?: run { // Android won't allow us to generate the keys if the device doesn't support biometrics or no biometrics are enrolled @@ -135,6 +149,14 @@ index 0fef884..3f21552 100644 } encryptor.initializeKeyStoreEntry(keyStore, options) } +@@ -383,6 +396,7 @@ open class SecureStoreModule : Module() { + private const val KEYSTORE_ALIAS_PROPERTY = "keystoreAlias" + const val USES_KEYSTORE_SUFFIX_PROPERTY = "usesKeystoreSuffix" + const val DEFAULT_KEYSTORE_ALIAS = "key_v1" ++ const val DEFAULT_FALLBACK_KEYSTORE_ALIAS = "fallback_key_v1" + const val AUTHENTICATED_KEYSTORE_SUFFIX = "keystoreAuthenticated" + const val UNAUTHENTICATED_KEYSTORE_SUFFIX = "keystoreUnauthenticated" + } diff --git a/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/SecureStoreOptions.kt b/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/SecureStoreOptions.kt index 79a600f..0455fd6 100644 --- a/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/SecureStoreOptions.kt @@ -148,12 +170,12 @@ index 79a600f..0455fd6 100644 + @Field var enableDeviceFallback: Boolean = false ) : Record, Serializable diff --git a/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/encryptors/AESEncryptor.kt b/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/encryptors/AESEncryptor.kt -index 3a12dc9..1cd187f 100644 +index 3a12dc9..1cec6c0 100644 --- a/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/encryptors/AESEncryptor.kt +++ b/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/encryptors/AESEncryptor.kt @@ -1,9 +1,10 @@ package expo.modules.securestore.encryptors - + -import android.annotation.TargetApi +import android.os.Build import android.security.keystore.KeyGenParameterSpec @@ -163,17 +185,29 @@ index 3a12dc9..1cd187f 100644 import expo.modules.securestore.AuthenticationHelper import expo.modules.securestore.DecryptException import expo.modules.securestore.SecureStoreModule -@@ -50,17 +51,22 @@ class AESEncryptor : KeyBasedEncryptor { +@@ -33,6 +34,11 @@ import javax.crypto.spec.GCMParameterSpec + class AESEncryptor : KeyBasedEncryptor { + override fun getKeyStoreAlias(options: SecureStoreOptions): String { + val baseAlias = options.keychainService ++ ++ if (baseAlias == SecureStoreModule.DEFAULT_KEYSTORE_ALIAS && options.enableDeviceFallback) { ++ return "$AES_CIPHER:${SecureStoreModule.DEFAULT_FALLBACK_KEYSTORE_ALIAS}" ++ } ++ + return "$AES_CIPHER:$baseAlias" + } + +@@ -50,17 +56,22 @@ class AESEncryptor : KeyBasedEncryptor { return "${getKeyStoreAlias(options)}:$suffix" } - + - @TargetApi(23) + @RequiresApi(Build.VERSION_CODES.R) @Throws(GeneralSecurityException::class) override fun initializeKeyStoreEntry(keyStore: KeyStore, options: SecureStoreOptions): KeyStore.SecretKeyEntry { val extendedKeystoreAlias = getExtendedKeyStoreAlias(options, options.requireAuthentication) val keyPurposes = KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT - + + val authType = + if (options.enableDeviceFallback) KeyProperties.AUTH_BIOMETRIC_STRONG or KeyProperties.AUTH_DEVICE_CREDENTIAL + else KeyProperties.AUTH_BIOMETRIC_STRONG @@ -185,9 +219,9 @@ index 3a12dc9..1cd187f 100644 .setUserAuthenticationRequired(options.requireAuthentication) + .setUserAuthenticationParameters(0, authType) .build() - + val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, keyStore.provider) -@@ -78,14 +84,15 @@ class AESEncryptor : KeyBasedEncryptor { +@@ -78,14 +89,15 @@ class AESEncryptor : KeyBasedEncryptor { keyStoreEntry: KeyStore.SecretKeyEntry, requireAuthentication: Boolean, authenticationPrompt: String, @@ -198,14 +232,14 @@ index 3a12dc9..1cd187f 100644 val secretKey = keyStoreEntry.secretKey val cipher = Cipher.getInstance(AES_CIPHER) cipher.init(Cipher.ENCRYPT_MODE, secretKey) - + val gcmSpec = cipher.parameters.getParameterSpec(GCMParameterSpec::class.java) - val authenticatedCipher = authenticationHelper.authenticateCipher(cipher, requireAuthentication, authenticationPrompt) + val authenticatedCipher = authenticationHelper.authenticateCipher(cipher, requireAuthentication, authenticationPrompt, enableDeviceFallback) - + return createEncryptedItemWithCipher(plaintextValue, authenticatedCipher, gcmSpec) } -@@ -128,7 +135,7 @@ class AESEncryptor : KeyBasedEncryptor { +@@ -128,7 +140,7 @@ class AESEncryptor : KeyBasedEncryptor { throw DecryptException("Authentication tag length must be at least $MIN_GCM_AUTHENTICATION_TAG_LENGTH bits long", key, options.keychainService) } cipher.init(Cipher.DECRYPT_MODE, keyStoreEntry.secretKey, gcmSpec) @@ -213,12 +247,24 @@ index 3a12dc9..1cd187f 100644 + val unlockedCipher = authenticationHelper.authenticateCipher(cipher, requiresAuthentication, options.authenticationPrompt, options.enableDeviceFallback) return String(unlockedCipher.doFinal(ciphertextBytes), StandardCharsets.UTF_8) } - + diff --git a/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/encryptors/HybridAESEncryptor.kt b/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/encryptors/HybridAESEncryptor.kt -index fb42599..5f8bbfd 100644 +index fb42599..e996b39 100644 --- a/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/encryptors/HybridAESEncryptor.kt +++ b/node_modules/expo-secure-store/android/src/main/java/expo/modules/securestore/encryptors/HybridAESEncryptor.kt -@@ -69,7 +69,8 @@ class HybridAESEncryptor(private var mContext: Context, private val mAESEncrypto +@@ -51,6 +51,11 @@ class HybridAESEncryptor(private var mContext: Context, private val mAESEncrypto + + override fun getKeyStoreAlias(options: SecureStoreOptions): String { + val baseAlias = options.keychainService ++ ++ if (baseAlias == SecureStoreModule.DEFAULT_KEYSTORE_ALIAS && options.enableDeviceFallback) { ++ return "$RSA_CIPHER:${SecureStoreModule.DEFAULT_FALLBACK_KEYSTORE_ALIAS}" ++ } ++ + return "$RSA_CIPHER:$baseAlias" + } + +@@ -69,7 +74,8 @@ class HybridAESEncryptor(private var mContext: Context, private val mAESEncrypto keyStoreEntry: KeyStore.PrivateKeyEntry, requireAuthentication: Boolean, authenticationPrompt: String, @@ -240,10 +286,10 @@ index e493467..39459ff 100644 + authenticationHelper: AuthenticationHelper, + enableDeviceFallback: Boolean, ): JSONObject - + @Throws(GeneralSecurityException::class, JSONException::class) diff --git a/node_modules/expo-secure-store/build/SecureStore.d.ts b/node_modules/expo-secure-store/build/SecureStore.d.ts -index d5cd157..be9acfe 100644 +index d5cd157..835a4d4 100644 --- a/node_modules/expo-secure-store/build/SecureStore.d.ts +++ b/node_modules/expo-secure-store/build/SecureStore.d.ts @@ -78,6 +78,16 @@ export type SecureStoreOptions = { @@ -275,6 +321,7 @@ index d5cd157..be9acfe 100644 + */ +export declare function canUseDeviceCredentialsAuthentication(): boolean; //# sourceMappingURL=SecureStore.d.ts.map +\ No newline at end of file diff --git a/node_modules/expo-secure-store/build/SecureStore.js b/node_modules/expo-secure-store/build/SecureStore.js index 4d87b38..e11bd94 100644 --- a/node_modules/expo-secure-store/build/SecureStore.js @@ -312,7 +359,7 @@ index 439b08d..1268979 100644 + private func areDeviceCredentialsEnabled() -> Bool { + return LAContext().canEvaluatePolicy(LAPolicy.deviceOwnerAuthentication, error: nil) } - + private func get(with key: String, options: SecureStoreOptions) throws -> String? { @@ -99,12 +107,17 @@ public final class SecureStoreModule: Module { if !options.requireAuthentication { @@ -325,7 +372,7 @@ index 439b08d..1268979 100644 + throw MissingPlistKeyException() + } } - + var error: Unmanaged? = nil - guard let accessOptions = SecAccessControlCreateWithFlags(kCFAllocatorDefault, accessibility, .biometryCurrentSet, &error) else { + @@ -340,7 +387,7 @@ index 7e3fa4d..c9e843b 100644 --- a/node_modules/expo-secure-store/ios/SecureStoreOptions.swift +++ b/node_modules/expo-secure-store/ios/SecureStoreOptions.swift @@ -15,4 +15,7 @@ internal struct SecureStoreOptions: Record { - + @Field var accessGroup: String? + @@ -367,12 +414,12 @@ index ee43e04..4f253a1 100644 + */ + enableDeviceFallback?: boolean; }; - + // @needsAudit @@ -226,6 +237,16 @@ export function canUseBiometricAuthentication(): boolean { return ExpoSecureStore.canUseBiometricAuthentication(); } - + +/** + * Checks whether any device credentials are configured on the device. + * @return `true` if the device has device credentials configured. Otherwise, returns `false`. From b07d42ea7640d543a41fe37ffb0f11a41b5bacdc Mon Sep 17 00:00:00 2001 From: Jakub Korytko Date: Thu, 4 Dec 2025 13:49:04 +0100 Subject: [PATCH 7/7] Update expo-secure-store patches details --- patches/expo-secure-store/details.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/patches/expo-secure-store/details.md b/patches/expo-secure-store/details.md index 6bc1a8c084b1..d6d176d012a1 100644 --- a/patches/expo-secure-store/details.md +++ b/patches/expo-secure-store/details.md @@ -10,7 +10,7 @@ Additionally, support for screen locks can be checked using the new 'canUseDeviceCredentialsAuthentication' method. ``` -- Upstream PR/issue: TBA +- Upstream PR/issue: https://github.com/expo/expo/pull/41409 - E/App issue: https://github.com/Expensify/App/issues/75225 - PR introducing patch: https://github.com/Expensify/App/pull/76288