diff --git a/ios/Podfile.lock b/ios/Podfile.lock index b4a40339469e4..201f203ac6504 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -176,8 +176,6 @@ PODS: - hermes-engine - React-Core - ReactCommon - - ExpoSecureStore (14.2.4): - - ExpoModulesCore - ExpoStoreReview (9.0.9): - ExpoModulesCore - ExpoTaskManager (55.0.2): @@ -490,7 +488,6 @@ PODS: - React-jsinspectorcdp - React-jsitooling - React-perflogger - - React-rendererconsistency - React-runtimeexecutor - React-runtimescheduler - React-utils @@ -516,7 +513,6 @@ PODS: - React-jsinspectorcdp - React-jsitooling - React-perflogger - - React-rendererconsistency - React-runtimeexecutor - React-runtimescheduler - React-utils @@ -541,7 +537,6 @@ PODS: - React-jsinspectorcdp - React-jsitooling - React-perflogger - - React-rendererconsistency - React-runtimeexecutor - React-runtimescheduler - React-utils @@ -568,7 +563,6 @@ PODS: - React-jsinspectorcdp - React-jsitooling - React-perflogger - - React-rendererconsistency - React-runtimeexecutor - React-runtimescheduler - React-utils @@ -594,7 +588,6 @@ PODS: - React-jsinspectorcdp - React-jsitooling - React-perflogger - - React-rendererconsistency - React-runtimeexecutor - React-runtimescheduler - React-utils @@ -620,7 +613,6 @@ PODS: - React-jsinspectorcdp - React-jsitooling - React-perflogger - - React-rendererconsistency - React-runtimeexecutor - React-runtimescheduler - React-utils @@ -646,7 +638,6 @@ PODS: - React-jsinspectorcdp - React-jsitooling - React-perflogger - - React-rendererconsistency - React-runtimeexecutor - React-runtimescheduler - React-utils @@ -672,7 +663,6 @@ PODS: - React-jsinspectorcdp - React-jsitooling - React-perflogger - - React-rendererconsistency - React-runtimeexecutor - React-runtimescheduler - React-utils @@ -698,7 +688,6 @@ PODS: - React-jsinspectorcdp - React-jsitooling - React-perflogger - - React-rendererconsistency - React-runtimeexecutor - React-runtimescheduler - React-utils @@ -724,7 +713,6 @@ PODS: - React-jsinspectorcdp - React-jsitooling - React-perflogger - - React-rendererconsistency - React-runtimeexecutor - React-runtimescheduler - React-utils @@ -750,7 +738,6 @@ PODS: - React-jsinspectorcdp - React-jsitooling - React-perflogger - - React-rendererconsistency - React-runtimeexecutor - React-runtimescheduler - React-utils @@ -776,7 +763,6 @@ PODS: - React-jsinspectorcdp - React-jsitooling - React-perflogger - - React-rendererconsistency - React-runtimeexecutor - React-runtimescheduler - React-utils @@ -802,7 +788,6 @@ PODS: - React-jsinspectorcdp - React-jsitooling - React-perflogger - - React-rendererconsistency - React-runtimeexecutor - React-runtimescheduler - React-utils @@ -828,7 +813,6 @@ PODS: - React-jsinspectorcdp - React-jsitooling - React-perflogger - - React-rendererconsistency - React-runtimeexecutor - React-runtimescheduler - React-utils @@ -4275,7 +4259,6 @@ DEPENDENCIES: - "ExpoLogBox (from `../node_modules/@expo/log-box`)" - ExpoModulesCore (from `../node_modules/expo-modules-core`) - ExpoModulesJSI (from `../node_modules/expo-modules-core`) - - ExpoSecureStore (from `../node_modules/expo-secure-store/ios`) - ExpoStoreReview (from `../node_modules/expo-store-review/ios`) - ExpoTaskManager (from `../node_modules/expo-task-manager/ios`) - ExpoVideo (from `../node_modules/expo-video/ios`) @@ -4283,7 +4266,7 @@ DEPENDENCIES: - fast_float (from `../node_modules/react-native/third-party-podspecs/fast_float.podspec`) - FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`) - fmt (from `../node_modules/react-native/third-party-podspecs/fmt.podspec`) - - "FullStory (from `{:http=>\"https://ios-releases.fullstory.com/fullstory-1.68.3-xcframework.tar.gz\"}`)" + - "FullStory (from `{http: \"https://ios-releases.fullstory.com/fullstory-1.68.3-xcframework.tar.gz\"}`)" - "fullstory_react-native (from `../node_modules/@fullstory/react-native`)" - glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`) - group-ib-fp (from `../node_modules/group-ib-fp`) @@ -4490,8 +4473,6 @@ EXTERNAL SOURCES: :path: "../node_modules/expo-modules-core" ExpoModulesJSI: :path: "../node_modules/expo-modules-core" - ExpoSecureStore: - :path: "../node_modules/expo-secure-store/ios" ExpoStoreReview: :path: "../node_modules/expo-store-review/ios" ExpoTaskManager: @@ -4768,7 +4749,7 @@ SPEC CHECKSUMS: AirshipServiceExtension: 50d11b2f62c4a490d4e81a1c36f70e2ecb70a27e AppAuth: d4f13a8fe0baf391b2108511793e4b479691fb73 AppLogs: 3bc4e9b141dbf265b9464409caaa40416a9ee0e0 - boost: 659a89341ea4ab3df8259733813b52f26d8be9a5 + boost: 7e761d76ca2ce687f7cc98e698152abd03a18f90 DoubleConversion: cb417026b2400c8f53ae97020b2be961b59470cb EXConstants: b3c63be5f8648e4ab8e6ff5099b62f629247f969 expensify-react-native-background-task: 03c640e1f5649692d058cba48c0a138f024a6dd3 @@ -4784,7 +4765,6 @@ SPEC CHECKSUMS: ExpoLogBox: 89b634d5a8a64c4a6a7caad8f9985a28463c7002 ExpoModulesCore: 1158e7941ddcff1677846dfdec27630e036e3904 ExpoModulesJSI: 455acaa72cb963ceb247df889c8e8cce3e6bbfe6 - ExpoSecureStore: 3f1b632d6d40bcc62b4983ef9199cd079592a50a ExpoStoreReview: 32bb43b6fae9c8db3e33cad69996dff3785eef5f ExpoTaskManager: 23d8ea66d21da98ddcef977f258d1fd62359c2db ExpoVideo: 17748c0ee95e746b6ec75d8522a714cb3e614fa1 @@ -4808,7 +4788,7 @@ SPEC CHECKSUMS: GTMAppAuth: f69bd07d68cd3b766125f7e072c45d7340dea0de GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6 GzipSwift: 893f3e48e597a1a4f62fafcb6514220fcf8287fa - hermes-engine: 6bc8fb50f6b2eb191c5be0f853dc1b2f63dbce95 + hermes-engine: 0711ccb14bd615969ef611bc6c2483ea2ed3b09e libavif: 84bbb62fb232c3018d6f1bab79beea87e35de7b7 libdav1d: 23581a4d8ec811ff171ed5e2e05cd27bad64c39f libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8 @@ -4836,7 +4816,7 @@ SPEC CHECKSUMS: React: 4bc1f928568ad4bcfd147260f907b4ea5873a03b React-callinvoker: 87f8728235a0dc62e9dc19b3851c829d9347d015 React-Codegen: 4b8b4817cea7a54b83851d4c1f91f79aa73de30a - React-Core: 176a81025968530159589ec4a67fe2962909fce9 + React-Core: 76bed73b02821e5630e7f2cb2e82432ee964695d React-CoreModules: 752dbfdaeb096658aa0adc4a03ba6214815a08df React-cxxreact: b6798528aa601c6db66e6adc7e2da2b059c8be74 React-debug: b2c9f60a9b7a81cefd737cb61e31c2bc39fdfe17 @@ -4888,7 +4868,7 @@ SPEC CHECKSUMS: React-NativeModulesApple: e554252d69442010807867cc7d70c0008048ad20 React-networking: 669cb54cc7e5b65d7dafeeb36970a1421adc8bb3 React-oscompat: 80166b66da22e7af7fad94474e9997bd52d4c8c6 - React-perflogger: decbf4d10c1f77d687af1d83a6ba9dc1b23715d6 + React-perflogger: d6797918d2b1031e91a9d8f5e7fdd2c8728fb390 React-performancecdpmetrics: 7706707d5dd49d708518a91abe456dcb585a5865 React-performancetimeline: c9807b559901c4298a92f6bcb069f49f518b7020 React-RCTActionSheet: 3bd5f5db9f983cf38d51bb9a7a198e2ebea94821 @@ -4904,7 +4884,7 @@ SPEC CHECKSUMS: React-RCTSettings: 2c45623d6c0f30851a123f621eb9d32298bcbb0c React-RCTText: 0ee70f5dc18004b4d81b2c214267c6cbec058587 React-RCTVibration: 88557e21e7cc3fe76b5b174cba28ff45c6def997 - React-rendererconsistency: 5236fe878921773ef9274c44e189e057672ef5bc + React-rendererconsistency: ac8a9e9ee3eb299458cc848944133ff4be46cc41 React-renderercss: f04cbe3b06ee071c6ca724f41a3c3aa31332601e React-rendererdebug: 2a2e4f7d42abcbec2047e989a1afda5d62905679 React-RuntimeApple: ce2ae0ea88316a7c708be1e6601e4ec5f6febdce diff --git a/jest.config.js b/jest.config.js index c397bbc89b91d..1e13b56d427d8 100644 --- a/jest.config.js +++ b/jest.config.js @@ -14,7 +14,7 @@ module.exports = { '^.+\\.svg?$': 'jest-transformer-svg', }, transformIgnorePatterns: [ - '/node_modules/(?!.*(react-native|expo|@noble|react-navigation|uuid|@shopify\/flash-list).*/)', + '/node_modules/(?!.*(react-native|expo|react-navigation|uuid|@shopify\/flash-list).*/)', // Prevent Babel from transforming worklets in this file so they are treated as normal functions, otherwise FormatSelectionUtilsTest won't run. '/node_modules/@expensify/react-native-live-markdown/lib/commonjs/parseExpensiMark.js', ], @@ -35,8 +35,6 @@ module.exports = { moduleNameMapper: { '\\.(lottie)$': '/__mocks__/fileMock.ts', '^group-ib-fp$': '/__mocks__/group-ib-fp.ts', - '@noble/ed25519': '/node_modules/@noble/ed25519/index.ts', - '@noble/hashes/(.*)': '/node_modules/@noble/hashes/src/$1.ts', '^parse-imports-exports$': '/node_modules/parse-imports-exports/index.cjs', }, }; diff --git a/jest/setup.ts b/jest/setup.ts index dae0ebb6f0ae2..3625e9138a501 100644 --- a/jest/setup.ts +++ b/jest/setup.ts @@ -12,7 +12,6 @@ import mockStorage from 'react-native-onyx/dist/storage/__mocks__'; import type Animated from 'react-native-reanimated'; import 'setimmediate'; import {TextDecoder, TextEncoder} from 'util'; -import * as MockedSecureStore from '@src/libs/MultifactorAuthentication/NativeBiometrics/SecureStore/index.web'; import '@src/polyfills/PromiseWithResolvers'; import mockFSLibrary from './setupMockFullstoryLib'; import setupMockImages from './setupMockImages'; @@ -125,9 +124,6 @@ jest.mock('react-native-share', () => ({ default: jest.fn(), })); -// Jest has no access to the native secure store module, so we mock it with the web implementation. -jest.mock('@src/libs/MultifactorAuthentication/NativeBiometrics/SecureStore', () => MockedSecureStore); - jest.mock('react-native-reanimated', () => ({ ...jest.requireActual('react-native-reanimated/mock'), createAnimatedPropAdapter: jest.fn, diff --git a/package-lock.json b/package-lock.json index 6d6a8ce4307c2..ba95e316d3bf9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,8 +29,6 @@ "@fullstory/react-native": "^1.9.0", "@gorhom/portal": "^1.0.14", "@invertase/react-native-apple-authentication": "^2.5.0", - "@noble/ed25519": "^3.0.0", - "@noble/hashes": "^2.0.0", "@onfido/react-native-sdk": "15.1.0", "@pusher/pusher-websocket-react-native": "^1.3.1", "@react-native-camera-roll/camera-roll": "7.4.0", @@ -69,7 +67,6 @@ "expo-image-manipulator": "55.0.2", "expo-location": "55.0.3", "expo-modules-core": "55.0.4", - "expo-secure-store": "~14.2.4", "expo-store-review": "~9.0.8", "expo-task-manager": "55.0.2", "expo-video": "55.0.3", @@ -10571,27 +10568,6 @@ "eslint-scope": "5.1.1" } }, - "node_modules/@noble/ed25519": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@noble/ed25519/-/ed25519-3.0.0.tgz", - "integrity": "sha512-QyteqMNm0GLqfa5SoYbSC3+Pvykwpn95Zgth4MFVSMKBB75ELl9tX1LAVsN4c3HXOrakHsF2gL4zWDAYCcsnzg==", - "license": "MIT", - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@noble/hashes": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.0.tgz", - "integrity": "sha512-h8VUBlE8R42+XIDO229cgisD287im3kdY6nbNZJFjc6ZvKIXPYXe6Vc/t+kyjFdMFyt5JpapzTsEg8n63w5/lw==", - "license": "MIT", - "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "devOptional": true, @@ -23963,15 +23939,6 @@ "react-native": "*" } }, - "node_modules/expo-secure-store": { - "version": "14.2.4", - "resolved": "https://registry.npmjs.org/expo-secure-store/-/expo-secure-store-14.2.4.tgz", - "integrity": "sha512-ePaz4fnTitJJZjAiybaVYGfLWWyaEtepZC+vs9ZBMhQMfG5HUotIcVsDaSo3FnwpHmgwsLVPY2qFeryI6AtULw==", - "license": "MIT", - "peerDependencies": { - "expo": "*" - } - }, "node_modules/expo-server": { "version": "55.0.6", "resolved": "https://registry.npmjs.org/expo-server/-/expo-server-55.0.6.tgz", diff --git a/package.json b/package.json index 62a35365ed60a..a36581621849a 100644 --- a/package.json +++ b/package.json @@ -92,8 +92,6 @@ "@fullstory/react-native": "^1.9.0", "@gorhom/portal": "^1.0.14", "@invertase/react-native-apple-authentication": "^2.5.0", - "@noble/ed25519": "^3.0.0", - "@noble/hashes": "^2.0.0", "@onfido/react-native-sdk": "15.1.0", "@pusher/pusher-websocket-react-native": "^1.3.1", "@react-native-camera-roll/camera-roll": "7.4.0", @@ -132,7 +130,6 @@ "expo-image-manipulator": "55.0.2", "expo-location": "55.0.3", "expo-modules-core": "55.0.4", - "expo-secure-store": "~14.2.4", "expo-store-review": "~9.0.8", "expo-task-manager": "55.0.2", "expo-video": "55.0.3", @@ -421,9 +418,7 @@ "expo-keep-awake" ], "android": { - "buildFromSource": [ - "expo-secure-store" - ] + "buildFromSource": [] } } }, diff --git a/patches/expo-secure-store/details.md b/patches/expo-secure-store/details.md deleted file mode 100644 index d6d176d012a17..0000000000000 --- a/patches/expo-secure-store/details.md +++ /dev/null @@ -1,76 +0,0 @@ -# `expo-secure-store` patches - -### [expo-secure-store+14.2.4+001+enable-device-fallback.patch](expo-secure-store+14.2.4+001+enable-device-fallback.patch) - -- Reason: - - ``` - 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: 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 - -### [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 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. - ``` - -- 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. - ``` - -- 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: - - ``` - 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 - 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 deleted file mode 100644 index d5d5edad1a25a..0000000000000 --- a/patches/expo-secure-store/expo-secure-store+14.2.4+001+enable-device-fallback.patch +++ /dev/null @@ -1,435 +0,0 @@ -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..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,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) -- .setNegativeButtonText(context.getString(android.R.string.cancel)) -- .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..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() -+ 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) - } -@@ -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 -+++ 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..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 - 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 -@@ -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 -+ - 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 +89,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 +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) -- 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..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 -@@ -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, -- 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..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 = { - * @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 -\ 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 -+++ 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+002+return-auth-type.patch b/patches/expo-secure-store/expo-secure-store+14.2.4+002+return-auth-type.patch deleted file mode 100644 index c8cdfcf5ac020..0000000000000 --- a/patches/expo-secure-store/expo-secure-store+14.2.4+002+return-auth-type.patch +++ /dev/null @@ -1,901 +0,0 @@ -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 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 -@@ -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 -- ?: throw AuthenticationException("Couldn't get cipher from authentication result") -+ 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") - } -- 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).value - } - - AsyncFunction("getValueWithKeyAsync") Coroutine { key: String, options: SecureStoreOptions -> -- return@Coroutine getItemImpl(key, 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 { -- return@runBlocking setItemImpl(key, value, options, keyIsInvalidated = false) -+ 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) { -+ 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 +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 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() { - "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 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 SecureStoreOriginalFeedback(null) - return hybridAESEncryptor.decryptItem(key, encryptedItem, privateKeyEntry, options, authenticationHelper) - } - else -> { -@@ -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 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 +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 SecureStoreOriginalFeedback(null) - } catch (e: GeneralSecurityException) { - throw (DecryptException(e.message, key, options.keychainService, e)) - } catch (e: CodedException) { -@@ -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) - } -- return -+ return SecureStoreOriginalFeedback(null) - } - - try { -@@ -210,7 +226,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, 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) - -@@ -218,6 +235,9 @@ open class SecureStoreModule : Module() { - if (prefs.contains(key)) { - prefs.edit().remove(key).apply() - } -+ -+ 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..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 -@@ -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 returnUsedAuthenticationType: Boolean = false - ) : Record, Serializable -+ -+enum class SecureStoreAuthType(index: Int) { -+ UNKNOWN(BiometricPrompt.AUTHENTICATION_RESULT_TYPE_UNKNOWN), -+ CREDENTIAL(BiometricPrompt.AUTHENTICATION_RESULT_TYPE_DEVICE_CREDENTIAL), -+ BIOMETRIC(BiometricPrompt.AUTHENTICATION_RESULT_TYPE_BIOMETRIC), -+ -+ /** Prompt failed, no authentication was used at all */ -+ NONE(0) -+} -+ -+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 -> { -+ SecureStoreAuthType.UNKNOWN -+ } -+ BiometricPrompt.AUTHENTICATION_RESULT_TYPE_DEVICE_CREDENTIAL -> { -+ SecureStoreAuthType.CREDENTIAL -+ } -+ BiometricPrompt.AUTHENTICATION_RESULT_TYPE_BIOMETRIC -> { -+ SecureStoreAuthType.BIOMETRIC -+ } -+ else -> { -+ 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 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..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,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.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 { -+ ): 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 -+ -+ if (requireAuthentication) { -+ promptResult = authenticationHelper.authenticateCipher(cipher, authenticationPrompt, enableDeviceFallback) -+ authenticatedCipher = promptResult.cryptoObject?.cipher ?: cipher -+ } 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, - options: SecureStoreOptions, - authenticationHelper: AuthenticationHelper -- ): String { -+ ): 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,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 -+ -+ if (requiresAuthentication) { -+ promptResult = authenticationHelper.authenticateCipher(cipher, options.authenticationPrompt, options.enableDeviceFallback) -+ unlockedCipher = promptResult.cryptoObject?.cipher ?: cipher -+ } else { -+ unlockedCipher = cipher -+ } -+ -+ 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 ---- 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 -@@ -9,6 +9,7 @@ import expo.modules.securestore.EncryptException - import expo.modules.securestore.KeyStoreException - 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 { -+ ): 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. " + -@@ -88,7 +89,7 @@ class HybridAESEncryptor(private var mContext: Context, private val mAESEncrypto - keyStoreEntry: KeyStore.PrivateKeyEntry, - options: SecureStoreOptions, - authenticationHelper: AuthenticationHelper -- ): String { -+ ): 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..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 - 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 -+ ): SecureStoreOriginalFeedback - - @Throws(GeneralSecurityException::class, JSONException::class) - suspend fun decryptItem( -@@ -36,5 +37,5 @@ interface KeyBasedEncryptor { - keyStoreEntry: E, - options: SecureStoreOptions, - authenticationHelper: AuthenticationHelper -- ): String -+ ): 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..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 @@ -+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: { -+ /** -+ * 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 -+ */ -+ readonly UNKNOWN: -1; -+ /** -+ * Returned when the authentication fails -+ * @platform android -+ * @platform ios -+ */ -+ readonly NONE: 0; -+ /** -+ * Generic type, not specified whether it was a passcode or pattern. -+ * @platform android -+ * @platform ios -+ */ -+ readonly CREDENTIALS: 1; -+ /** -+ * Generic type, not specified whether it was a face scan or a fingerprint -+ * @platform android -+ */ -+ readonly BIOMETRICS: 2; -+ /** -+ * FaceID was used to authenticate -+ * @platform ios -+ */ -+ readonly FACE_ID: 3; -+ /** -+ * TouchID was used to authenticate -+ * @platform ios -+ */ -+ readonly TOUCH_ID: 4; -+ /** -+ * OpticID was used to authenticate (reserved by apple, used on Apple Vision Pro, not iOS) -+ */ -+ 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 -@@ -88,6 +137,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,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`. - */ --export declare function getItemAsync(key: string, options?: SecureStoreOptions): Promise; -+export declare function getItemAsync(key: string, options?: R | EmptyObject): Promise>; - /** - * Stores a key–value pair. - * -@@ -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. - */ --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 +204,7 @@ export declare function setItemAsync(key: string, value: string, options?: Secur - * @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?: 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 +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. - */ --export declare function getItem(key: string, options?: SecureStoreOptions): string | null; -+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. -diff --git a/node_modules/expo-secure-store/build/SecureStore.js b/node_modules/expo-secure-store/build/SecureStore.js -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 @@ - import ExpoSecureStore from './ExpoSecureStore'; - import { byteCountOverLimit, VALUE_BYTES_LIMIT } from './byteCounter'; - // @needsAudit -+ -+/** -+ * 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, -+}; -+// @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 +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.`); - } -- await ExpoSecureStore.setValueWithKeyAsync(value, key, options); -+ return await ExpoSecureStore.setValueWithKeyAsync(value, key, options); - } - /** - * 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 1268979..7764c8e 100644 ---- a/node_modules/expo-secure-store/ios/SecureStoreModule.swift -+++ b/node_modules/expo-secure-store/ios/SecureStoreModule.swift -@@ -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 -+ 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 -+ 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) 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 -- #else -- let context = LAContext() -- var error: NSError? -- let isBiometricsSupported: Bool = context.canEvaluatePolicy(LAPolicy.deviceOwnerAuthenticationWithBiometrics, error: &error) -- -- if error != nil { -- return false -- } -- return isBiometricsSupported -- #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 -+ -+ 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 -+ } -+ } -+ -+ 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 { -+ #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() -diff --git a/node_modules/expo-secure-store/ios/SecureStoreOptions.swift b/node_modules/expo-secure-store/ios/SecureStoreOptions.swift -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 -+ -+ @Field -+ var returnUsedAuthenticationType: Bool = false -+} -+ -+@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 -+} -+ -+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 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..d44c0bb 100644 ---- a/node_modules/expo-secure-store/src/SecureStore.ts -+++ b/node_modules/expo-secure-store/src/SecureStore.ts -@@ -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, -+} as const; -+ -+type AuthType = (typeof AUTH_TYPE)[keyof typeof AUTH_TYPE]; -+ - // @needsAudit - /** - * The data in the keychain item cannot be accessed after a restart until the device has been -@@ -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 -@@ -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( -+export async function getItemAsync( - key: string, -- options: SecureStoreOptions = {} --): Promise { -+ options: R | EmptyObject = {} -+): Promise> { - ensureValidKey(key); - return await ExpoSecureStore.getValueWithKeyAsync(key, options); - } -@@ -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( -+export async function setItemAsync( - key: string, - value: string, -- options: SecureStoreOptions = {} --): Promise { -+ options: R | EmptyObject = {} -+): Promise> { - 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); - } - - /** -@@ -201,7 +273,11 @@ export async function setItemAsync( - * @param options An [`SecureStoreOptions`](#securestoreoptions) object. - * - */ --export function setItem(key: string, value: string, options: SecureStoreOptions = {}): void { -+export function setItem( -+ key: string, -+ value: string, -+ options: R | EmptyObject = {} -+): SecureStoreSetFeedback { - 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 { -+export function getItem( -+ key: string, -+ options: R | EmptyObject = {} -+): SecureStoreGetFeedback { - ensureValidKey(key); - return ExpoSecureStore.getValueWithKeySync(key, options); - } 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 deleted file mode 100644 index 59e74054b1629..0000000000000 --- a/patches/expo-secure-store/expo-secure-store+14.2.4+003+force-authentication-on-save.patch +++ /dev/null @@ -1,124 +0,0 @@ -diff --git a/node_modules/expo-secure-store/build/SecureStore.d.ts b/node_modules/expo-secure-store/build/SecureStore.d.ts -index 7097a8a..975d75b 100644 ---- a/node_modules/expo-secure-store/build/SecureStore.d.ts -+++ b/node_modules/expo-secure-store/build/SecureStore.d.ts -@@ -153,6 +153,20 @@ 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; - }; - /** - * 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 7764c8e..02f837c 100644 ---- a/node_modules/expo-secure-store/ios/SecureStoreModule.swift -+++ b/node_modules/expo-secure-store/ios/SecureStoreModule.swift -@@ -35,6 +35,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) - - return wrapResultWithFeedback(action: .set, result: result, options: options).value -@@ -190,6 +194,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 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 b95eca7..321c7e5 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 returnUsedAuthenticationType: Bool = false -+ -+ @Field -+ var forceAuthenticationOnSave: Bool = false - } - - @available(iOS 11.2, *) -@@ -86,3 +89,10 @@ struct SecureStoreSetFeedback: SecureStoreFeedback { - get { return 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 d44c0bb..c4b95ae 100644 ---- a/node_modules/expo-secure-store/src/SecureStore.ts -+++ b/node_modules/expo-secure-store/src/SecureStore.ts -@@ -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; - }; - - // @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 deleted file mode 100644 index c3acba54c6167..0000000000000 --- a/patches/expo-secure-store/expo-secure-store+14.2.4+004+fail-on-update.patch +++ /dev/null @@ -1,100 +0,0 @@ -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 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 -@@ -212,6 +212,10 @@ open class SecureStoreModule : Module() { - return SecureStoreOriginalFeedback(null) - } - -+ 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 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 -@@ -11,7 +11,8 @@ class SecureStoreOptions( - @Field var keychainService: String = SecureStoreModule.DEFAULT_KEYSTORE_ALIAS, - @Field var requireAuthentication: 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 f1e3610..8954c59 100644 ---- a/node_modules/expo-secure-store/build/SecureStore.d.ts -+++ b/node_modules/expo-secure-store/build/SecureStore.d.ts -@@ -167,6 +167,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. -+ * 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 02f837c..47282db 100644 ---- a/node_modules/expo-secure-store/ios/SecureStoreModule.swift -+++ b/node_modules/expo-secure-store/ios/SecureStoreModule.swift -@@ -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 { -+ 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 321c7e5..6a787b0 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 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 c4b95ae..0bedcaa 100644 ---- a/node_modules/expo-secure-store/src/SecureStore.ts -+++ b/node_modules/expo-secure-store/src/SecureStore.ts -@@ -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; - }; - - // @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 deleted file mode 100644 index f65e0a31540a9..0000000000000 --- a/patches/expo-secure-store/expo-secure-store+14.2.4+005+force-read-authentication-on-simulators.patch +++ /dev/null @@ -1,77 +0,0 @@ -diff --git a/node_modules/expo-secure-store/build/SecureStore.d.ts b/node_modules/expo-secure-store/build/SecureStore.d.ts -index 836958c..29531c5 100644 ---- a/node_modules/expo-secure-store/build/SecureStore.d.ts -+++ b/node_modules/expo-secure-store/build/SecureStore.d.ts -@@ -177,6 +177,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). -+ * 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 47282db..dcbe242 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 - 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 6a787b0..8f110dd 100644 ---- a/node_modules/expo-secure-store/ios/SecureStoreOptions.swift -+++ b/node_modules/expo-secure-store/ios/SecureStoreOptions.swift -@@ -27,6 +27,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 0bedcaa..bae5580 100644 ---- a/node_modules/expo-secure-store/src/SecureStore.ts -+++ b/node_modules/expo-secure-store/src/SecureStore.ts -@@ -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; - }; - - // @needsAudit diff --git a/src/components/MultifactorAuthentication/Context/usePromptContent.ts b/src/components/MultifactorAuthentication/Context/usePromptContent.ts index cd6c734633ce2..4d327719a8ae1 100644 --- a/src/components/MultifactorAuthentication/Context/usePromptContent.ts +++ b/src/components/MultifactorAuthentication/Context/usePromptContent.ts @@ -47,7 +47,7 @@ function usePromptContent(promptType: MultifactorAuthenticationPromptType): Prom } checkCredentials(); return () => { - // Guard against race condition in case where multifactorAuthenticationPublicKeyIDs gets updated in onyx while a KeyStore.get call is in-flight + // Guard against race condition in case where multifactorAuthenticationPublicKeyIDs gets updated in onyx while the call to retrieve the key from device is in-flight ignore = true; }; }, [areLocalCredentialsKnownToServer]); diff --git a/src/components/MultifactorAuthentication/biometrics/useNativeBiometrics.ts b/src/components/MultifactorAuthentication/biometrics/useNativeBiometrics.ts deleted file mode 100644 index ed1f78fd9a793..0000000000000 --- a/src/components/MultifactorAuthentication/biometrics/useNativeBiometrics.ts +++ /dev/null @@ -1,202 +0,0 @@ -import {useCallback} from 'react'; -import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; -import useLocalize from '@hooks/useLocalize'; -import {generateKeyPair, signToken as signTokenED25519} from '@libs/MultifactorAuthentication/NativeBiometrics/ED25519'; -import {PrivateKeyStore, PublicKeyStore} from '@libs/MultifactorAuthentication/NativeBiometrics/KeyStore'; -import {SECURE_STORE_VALUES} from '@libs/MultifactorAuthentication/NativeBiometrics/SecureStore'; -import type {NativeBiometricsKeyInfo} from '@libs/MultifactorAuthentication/NativeBiometrics/types'; -import type {RegistrationChallenge} from '@libs/MultifactorAuthentication/shared/challengeTypes'; -import type {MultifactorAuthenticationReason} from '@libs/MultifactorAuthentication/shared/types'; -import VALUES from '@libs/MultifactorAuthentication/VALUES'; -import CONST from '@src/CONST'; -import Base64URL from '@src/utils/Base64URL'; -import type {AuthorizeParams, AuthorizeResult, RegisterResult, UseBiometricsReturn} from './shared/types'; -import useServerCredentials from './shared/useServerCredentials'; - -const EXPO_TO_GENERIC_REASON: Partial> = { - [VALUES.REASON.EXPO.NO_METHOD_AVAILABLE]: VALUES.REASON.GENERIC.NO_AUTHENTICATION_METHODS_ENROLLED, - [VALUES.REASON.EXPO.NOT_SUPPORTED]: VALUES.REASON.GENERIC.NO_AUTHENTICATION_METHODS_ENROLLED, -}; - -function normalizeExpoReason(reason: MultifactorAuthenticationReason): MultifactorAuthenticationReason { - return EXPO_TO_GENERIC_REASON[reason] ?? reason; -} - -/** - * Clears local credentials to allow re-registration. - * Should only be used in response to server indicating credentials were removed. - */ -async function resetKeys(accountID: number) { - await Promise.all([PrivateKeyStore.delete(accountID), PublicKeyStore.delete(accountID)]); -} - -function useNativeBiometrics(): UseBiometricsReturn { - const {accountID} = useCurrentUserPersonalDetails(); - const {translate} = useLocalize(); - const {serverKnownCredentialIDs, haveCredentialsEverBeenConfigured} = useServerCredentials(); - - /** - * Checks if the device supports biometric authentication methods. - * Verifies both biometrics and credentials authentication capabilities. - * @returns True if biometrics or credentials authentication is supported on the device. - */ - const doesDeviceSupportAuthenticationMethod = useCallback(async () => { - const {biometrics, credentials} = PublicKeyStore.supportedAuthentication; - return biometrics || credentials; - }, []); - - // Only the credential ID is checked here because reading the private key - // requires biometric authentication. If the private key is missing, it - // will be detected during authorize() and trigger re-registration. - const getLocalCredentialID = useCallback(async () => { - const {value} = await PublicKeyStore.get(accountID); - return value ?? undefined; - }, [accountID]); - - const areLocalCredentialsKnownToServer = useCallback(async () => { - const key = await getLocalCredentialID(); - return !!key && serverKnownCredentialIDs.includes(key); - }, [getLocalCredentialID, serverKnownCredentialIDs]); - - const deleteLocalKeysForAccount = useCallback(async () => { - await resetKeys(accountID); - }, [accountID]); - - const register = async (onResult: (result: RegisterResult) => Promise | void, registrationChallenge: RegistrationChallenge) => { - // Generate key pair - const {privateKey, publicKey} = generateKeyPair(); - - // Delete existing keys before storing new ones to avoid "key already exists" errors - await Promise.all([PrivateKeyStore.delete(accountID), PublicKeyStore.delete(accountID)]); - - // Store private key - const privateKeyResult = await PrivateKeyStore.set(accountID, privateKey, {nativePromptTitle: translate('multifactorAuthentication.letsVerifyItsYou')}); - const authTypeEntry = Object.values(SECURE_STORE_VALUES.AUTH_TYPE).find(({CODE}) => CODE === privateKeyResult.type); - - const authType = authTypeEntry - ? { - code: authTypeEntry.CODE, - name: authTypeEntry.NAME, - marqetaValue: authTypeEntry.MARQETA_VALUE, - } - : undefined; - - if (!privateKeyResult.value || authType === undefined) { - onResult({ - success: false, - reason: normalizeExpoReason(privateKeyResult.reason), - }); - return; - } - - // Store public key - const publicKeyResult = await PublicKeyStore.set(accountID, publicKey); - if (!publicKeyResult.value) { - // Delete the private key if public key storage fails to maintain a consistent key pair state. - // If only the private key exists without a matching public key, the device will be unable to - // complete authorization later (public key mismatch with server). Clean up to force re-registration - // on the next attempt when both keys can be successfully stored. - await PrivateKeyStore.delete(accountID); - onResult({ - success: false, - reason: normalizeExpoReason(publicKeyResult.reason), - }); - return; - } - - const clientDataJSON = JSON.stringify({challenge: registrationChallenge.challenge}); - const keyInfo: NativeBiometricsKeyInfo = { - rawId: publicKey, - type: CONST.MULTIFACTOR_AUTHENTICATION.ED25519_TYPE, - response: { - clientDataJSON: Base64URL.encode(clientDataJSON), - biometric: { - publicKey, - algorithm: CONST.COSE_ALGORITHM.EDDSA, - }, - }, - }; - - await onResult({ - success: true, - reason: CONST.MULTIFACTOR_AUTHENTICATION.REASON.GENERIC.LOCAL_REGISTRATION_COMPLETE, - keyInfo, - }); - }; - - const authorize = async (params: AuthorizeParams, onResult: (result: AuthorizeResult) => Promise | void) => { - const {challenge} = params; - - // Extract public keys from challenge.allowCredentials - const allowedCredentialIDs = challenge.allowCredentials?.map((cred: {id: string; type: string}) => cred.id) ?? []; - - // Get private key from SecureStore - const privateKeyData = await PrivateKeyStore.get(accountID, {nativePromptTitle: translate('multifactorAuthentication.letsVerifyItsYou')}); - - if (!privateKeyData.value) { - onResult({ - success: false, - reason: normalizeExpoReason(privateKeyData.reason || VALUES.REASON.KEYSTORE.KEY_MISSING), - }); - return; - } - - const credentialID = await getLocalCredentialID(); - - if (!credentialID || !allowedCredentialIDs.includes(credentialID)) { - await resetKeys(accountID); - onResult({ - success: false, - reason: VALUES.REASON.KEYSTORE.REGISTRATION_REQUIRED, - }); - return; - } - - // Sign the challenge - const signedChallenge = signTokenED25519(challenge, privateKeyData.value, credentialID); - const authenticationMethodCode = privateKeyData.type; - const authTypeEntry = Object.values(SECURE_STORE_VALUES.AUTH_TYPE).find(({CODE}) => CODE === authenticationMethodCode); - - const authType = authTypeEntry - ? { - code: authTypeEntry.CODE, - name: authTypeEntry.NAME, - marqetaValue: authTypeEntry.MARQETA_VALUE, - } - : undefined; - - if (!authType) { - onResult({ - success: false, - reason: VALUES.REASON.GENERIC.BAD_REQUEST, - }); - return; - } - - // Return signed challenge - let callback handle backend authorization - await onResult({ - success: true, - reason: VALUES.REASON.CHALLENGE.CHALLENGE_SIGNED, - signedChallenge, - authenticationMethod: authType, - }); - }; - - const hasLocalCredentials = async () => !!(await getLocalCredentialID()); - - return { - deviceVerificationType: CONST.MULTIFACTOR_AUTHENTICATION.TYPE.BIOMETRICS, - serverKnownCredentialIDs, - haveCredentialsEverBeenConfigured, - getLocalCredentialID, - doesDeviceSupportAuthenticationMethod, - deviceCheckFailureReason: VALUES.REASON.GENERIC.NO_AUTHENTICATION_METHODS_ENROLLED, - hasLocalCredentials, - areLocalCredentialsKnownToServer, - register, - authorize, - deleteLocalKeysForAccount, - }; -} - -export default useNativeBiometrics; diff --git a/src/components/MultifactorAuthentication/config/scenarios/names.ts b/src/components/MultifactorAuthentication/config/scenarios/names.ts index 326a0f7d9baea..e128e6b8c4c3e 100644 --- a/src/components/MultifactorAuthentication/config/scenarios/names.ts +++ b/src/components/MultifactorAuthentication/config/scenarios/names.ts @@ -19,7 +19,6 @@ const SCENARIO_NAMES = { */ const PROMPT_NAMES = { BIOMETRICS_HSM: 'biometrics', - BIOMETRICS: 'biometrics', PASSKEYS: 'passkeys', } as const; diff --git a/src/libs/MultifactorAuthentication/NativeBiometrics/ED25519/index.ts b/src/libs/MultifactorAuthentication/NativeBiometrics/ED25519/index.ts deleted file mode 100644 index b5c81f4eac81c..0000000000000 --- a/src/libs/MultifactorAuthentication/NativeBiometrics/ED25519/index.ts +++ /dev/null @@ -1,157 +0,0 @@ -import {etc, hashes, keygen, sign, verify} from '@noble/ed25519'; -import type {Bytes} from '@noble/ed25519'; -import {sha256, sha512} from '@noble/hashes/sha2'; -import {utf8ToBytes} from '@noble/hashes/utils'; -import 'react-native-get-random-values'; -import type {ChallengeFlags, MultifactorAuthenticationChallengeObject, SignedChallenge} from '@libs/MultifactorAuthentication/shared/challengeTypes'; -import VALUES from '@libs/MultifactorAuthentication/VALUES'; -import Base64URL from '@src/utils/Base64URL'; -import type {Base64URLString} from '@src/utils/Base64URL'; - -/** - * ED25519 helpers used to construct and sign multifactor authentication challenges. - * Wraps `@noble/ed25519` to produce WebAuthn-like payloads that the server can verify. - */ - -/** - * Required polyfills for React Native to support ED25519 cryptographic operations. - * Provides implementations for getRandomValues and SHA-512 hashing. - * It is required for internal operations, even if it is not explicitly used in the app code. - * @see https://github.com/paulmillr/noble-ed25519?tab=readme-ov-file#react-native-polyfill-getrandomvalues-and-sha512 - */ -hashes.sha512 = sha512; -hashes.sha512Async = (m: Uint8Array) => Promise.resolve(sha512(m)); - -const {hexToBytes, concatBytes, bytesToHex, randomBytes} = etc; - -/** - * Generates a new ED25519 key pair encoded as hex strings. - */ -function generateKeyPair() { - const {secretKey, publicKey} = keygen(); - - return { - /** - * The public key is stored as url-encoded base64 because that is what the server expects. - * The encoding is arbitrary, and base64 was chosen because it is shorter than hex, so less - * overall data is stored and shipped across the wire. The private key is never exchanged with - * the server, so compatibility with the encoding format is not important. Hex is used instead - * because the @noble library ships with some convenience methods for converting between - * Hex<->Bytes, and the documented examples use those methods. - */ - privateKey: bytesToHex(secretKey), - - publicKey: Base64URL.encode(publicKey), - }; -} - -/* eslint-disable no-bitwise */ -/** - * Builds the challenge flag bitmask describing user presence and verification. - */ -function createFlag(up: boolean, uv: boolean): ChallengeFlags { - let flag = 0; - - // Set bit 0 - // (User Presence, user touched the security key or interacted with the authenticator) - if (up) { - flag |= 0x01; - } - - // Set bit 2 - // (User Verified, user has successfully authenticated with the authenticator) - if (uv) { - flag |= 0x04; - } - - return flag; -} -/* eslint-enable no-bitwise */ - -/** - * Creates the binary authenticator data buffer for a Relying Party Identifier. - * The result is in the form of concatBytes of the RPID, FLAGS, and SIGN_COUNT. - * - * RPID - Relying Party Identifier Hash (SHA-256 hash of the rpId) - * FLAGS - Bitmask describing user presence and verification state - * - * SIGN_COUNT - This exists to support hardware authenticator devices. - * It contains a monotonically increasing integer. The API currently will not validate this field. - * - * @see https://www.w3.org/TR/webauthn-2/#sctn-authenticator-data - */ -function createAuthenticatorData(rpId: string): Bytes { - const rpIdBytes = utf8ToBytes(rpId); - - // Per WebAuthn spec, RPID is hashed to guarantee a consistent length - const hashedRpId = sha256(rpIdBytes); - - // User Presence (UP) is true, User Verification (UV) is true - // This is because we only create challenges after successful biometric verification - const flagsArray = new Uint8Array([createFlag(true, true)]); - - const signCount = 0; - - // Creating a 4-byte buffer for the signCount - const buffer = new ArrayBuffer(4); - - const view = new DataView(buffer); - - // Writing the signCount as a big-endian 32-bit integer because WebAuthn expects it in this format - view.setUint32(0, signCount, false); - - // Creating a Uint8Array from the buffer to get the byte representation - const signCountArray = new Uint8Array(buffer); - - // Concatenate hashedRpId, flagsArray, and signCountArray to form the final binary data - return concatBytes(hashedRpId, flagsArray, signCountArray); -} - -/** - * Signs a multifactor authentication challenge for the given account identifier and key. - * Returns a WebAuthn-compatible signed challenge structure using ED25519. - * - * @param credentialRequestOptions Challenge object from server (must be AuthenticationChallenge format) - * @param privateKey ED25519 private key in hex format - * @param credentialID ED25519 public key in base64url format (used as rawId) - * @returns SignedChallenge with ED25519 signature - */ -function signToken(credentialRequestOptions: MultifactorAuthenticationChallengeObject, privateKey: string, credentialID: Base64URLString): SignedChallenge { - // rawId should be the base64url-encoded public key, serving as credential identifier - const rawId: Base64URLString = credentialID; - const type = VALUES.ED25519_TYPE; // "biometric" - - // Extract rpId from challenge - handle both authentication and registration formats - const rpId = 'rpId' in credentialRequestOptions ? credentialRequestOptions.rpId : credentialRequestOptions.rp.id; - - const authenticatorDataBytes = createAuthenticatorData(rpId); - const authenticatorData: Base64URLString = Base64URL.encode(authenticatorDataBytes); - - const clientDataJSON = JSON.stringify({challenge: credentialRequestOptions.challenge}); - const clientDataBytes = utf8ToBytes(clientDataJSON); - - /** - * Since the token can be of variable length, it is hashed to guarantee that binaryData is always a fixed length. - * This comes from the WebAuthN spec, with which we maintain compatibility here for easier interoperability on the backend. - */ - const clientDataHash = sha256(clientDataBytes); - - // WebAuthn signature format: sign(authenticatorData || SHA-256(clientDataJSON)) - const dataToSign = concatBytes(authenticatorDataBytes, clientDataHash); - const privateKeyBytes = hexToBytes(privateKey); - - const signatureBytes = sign(dataToSign, privateKeyBytes); - const signature: Base64URLString = Base64URL.encode(signatureBytes); - - return { - rawId, - type, - response: { - authenticatorData, - clientDataJSON: Base64URL.encode(clientDataJSON), - signature, - }, - }; -} - -export {generateKeyPair, signToken, createAuthenticatorData, concatBytes, sha256, utf8ToBytes, verify, randomBytes}; diff --git a/src/libs/MultifactorAuthentication/NativeBiometrics/KeyStore.ts b/src/libs/MultifactorAuthentication/NativeBiometrics/KeyStore.ts deleted file mode 100644 index 0b6de5b4214e7..0000000000000 --- a/src/libs/MultifactorAuthentication/NativeBiometrics/KeyStore.ts +++ /dev/null @@ -1,122 +0,0 @@ -/** - * Manages secure storage and retrieval of cryptographic keys for multifactor authentication. - */ -import VALUES from '@libs/MultifactorAuthentication/VALUES'; -import decodeExpoMessage from './helpers'; -import {SECURE_STORE_METHODS, SECURE_STORE_VALUES} from './SecureStore'; -import type {SecureStoreOptions} from './SecureStore'; -import type {MultifactorAuthenticationKeyStoreStatus, MultifactorAuthenticationKeyType, MultifactorKeyStoreOptions} from './types'; - -/** - * Static options for secure store operations. - */ -const STATIC_OPTIONS = { - keychainService: VALUES.KEYCHAIN_SERVICE, - keychainAccessible: SECURE_STORE_VALUES.WHEN_PASSCODE_SET_THIS_DEVICE_ONLY, - enableDeviceFallback: true, - returnUsedAuthenticationType: true, -} as const; - -/** - * Configures secure store options based on the key type and provided options. - * Private keys require additional authentication and are protected from updates. - */ -const secureStoreOptions = (key: T, KSOptions: MultifactorKeyStoreOptions): SecureStoreOptions => { - const isPrivateKey = key === VALUES.KEY_ALIASES.PRIVATE_KEY; - - return { - failOnUpdate: isPrivateKey, - requireAuthentication: isPrivateKey, - forceAuthenticationOnSave: isPrivateKey, - forceReadAuthenticationOnSimulators: isPrivateKey, - authenticationPrompt: KSOptions?.nativePromptTitle, - }; -}; - -/** - * Manages storage and retrieval of a specific key type (public or private) in secure storage. - * Handles encryption, authentication, and error management for cryptographic keys. - */ -class MultifactorAuthenticationKeyStore { - constructor(private readonly key: T) {} - - /** - * Saves a key to secure storage for the given account. - */ - public async set(accountID: number, value: string, KSOptions: MultifactorKeyStoreOptions): Promise> { - try { - const alias = `${accountID}_${this.key}`; - const type = await SECURE_STORE_METHODS.setItemAsync(alias, value, {...secureStoreOptions(this.key, KSOptions), ...STATIC_OPTIONS}); - return { - value: true, - reason: VALUES.REASON.KEYSTORE.KEY_SAVED, - type, - }; - } catch (error) { - return { - value: false, - reason: decodeExpoMessage(error, VALUES.REASON.KEYSTORE.UNABLE_TO_SAVE_KEY), - }; - } - } - - /** - * Deletes a key from secure storage for the given account. - */ - public async delete(accountID: number): Promise> { - try { - const alias = `${accountID}_${this.key}`; - await SECURE_STORE_METHODS.deleteItemAsync(alias, { - keychainService: VALUES.KEYCHAIN_SERVICE, - }); - return { - value: true, - reason: VALUES.REASON.KEYSTORE.KEY_DELETED, - }; - } catch (error) { - return { - value: false, - reason: decodeExpoMessage(error, VALUES.REASON.KEYSTORE.UNABLE_TO_DELETE_KEY), - }; - } - } - - /** - * Retrieves a key from secure storage for the given account with optional authentication. - */ - public async get(accountID: number, KSOptions: MultifactorKeyStoreOptions): Promise> { - try { - const alias = `${accountID}_${this.key}`; - const [key, type] = await SECURE_STORE_METHODS.getItemAsync(alias, {...secureStoreOptions(this.key, KSOptions), ...STATIC_OPTIONS}); - return { - value: key, - reason: key ? VALUES.REASON.KEYSTORE.KEY_RETRIEVED : VALUES.REASON.KEYSTORE.KEY_NOT_FOUND, - type, - }; - } catch (error) { - return { - value: null, - reason: decodeExpoMessage(error, VALUES.REASON.KEYSTORE.UNABLE_TO_RETRIEVE_KEY), - }; - } - } - - /** - * Checks what authentication methods are supported on this device. - */ - get supportedAuthentication() { - return {biometrics: SECURE_STORE_METHODS.canUseBiometricAuthentication(), credentials: SECURE_STORE_METHODS.canUseDeviceCredentialsAuthentication()}; - } -} - -/** - * Store instance for managing private keys. - */ -const MultifactorAuthenticationPrivateKeyStore = new MultifactorAuthenticationKeyStore(VALUES.KEY_ALIASES.PRIVATE_KEY); - -/** - * Store instance for managing public keys. - */ -const MultifactorAuthenticationPublicKeyStore = new MultifactorAuthenticationKeyStore(VALUES.KEY_ALIASES.PUBLIC_KEY); - -export {MultifactorAuthenticationPrivateKeyStore as PrivateKeyStore, MultifactorAuthenticationPublicKeyStore as PublicKeyStore}; diff --git a/src/libs/MultifactorAuthentication/NativeBiometrics/SecureStore/index.ts b/src/libs/MultifactorAuthentication/NativeBiometrics/SecureStore/index.ts deleted file mode 100644 index 8418c508de333..0000000000000 --- a/src/libs/MultifactorAuthentication/NativeBiometrics/SecureStore/index.ts +++ /dev/null @@ -1,75 +0,0 @@ -import * as SecureStore from 'expo-secure-store'; -import MARQETA_VALUES from '@libs/MultifactorAuthentication/shared/MarqetaValues'; -import type {SecureStoreMethods, SecureStoreValues} from './types'; - -/** - * Platform SecureStore constants used by the multifactor authentication biometrics flow. - * Normalizes supported auth types and storage policies exposed by `expo-secure-store`. - * - * @see https://docs.expo.dev/versions/latest/sdk/securestore/ - */ -const SECURE_STORE_VALUES = { - AUTH_TYPE: { - UNKNOWN: { - CODE: SecureStore.AUTH_TYPE.UNKNOWN, - NAME: 'Unknown', - MARQETA_VALUE: MARQETA_VALUES.AUTHENTICATION_METHOD.KNOWLEDGE_BASED, - }, - NONE: { - CODE: SecureStore.AUTH_TYPE.NONE, - NAME: 'None', - MARQETA_VALUE: MARQETA_VALUES.AUTHENTICATION_METHOD.NONE, - }, - CREDENTIALS: { - CODE: SecureStore.AUTH_TYPE.CREDENTIALS, - NAME: 'Credentials', - MARQETA_VALUE: MARQETA_VALUES.AUTHENTICATION_METHOD.KNOWLEDGE_BASED, - }, - BIOMETRICS: { - CODE: SecureStore.AUTH_TYPE.BIOMETRICS, - NAME: 'Biometrics', - MARQETA_VALUE: MARQETA_VALUES.AUTHENTICATION_METHOD.BIOMETRIC_FINGERPRINT, - }, - FACE_ID: { - CODE: SecureStore.AUTH_TYPE.FACE_ID, - NAME: 'Face ID', - MARQETA_VALUE: MARQETA_VALUES.AUTHENTICATION_METHOD.BIOMETRIC_FACE, - }, - TOUCH_ID: { - CODE: SecureStore.AUTH_TYPE.TOUCH_ID, - NAME: 'Touch ID', - MARQETA_VALUE: MARQETA_VALUES.AUTHENTICATION_METHOD.BIOMETRIC_FINGERPRINT, - }, - /** - * OpticID is reserved by apple, used on Apple Vision Pro and not iOS. - * It is declared here for completeness but is not currently supported. - */ - OPTIC_ID: { - CODE: SecureStore.AUTH_TYPE.OPTIC_ID, - NAME: 'Optic ID', - MARQETA_VALUE: MARQETA_VALUES.AUTHENTICATION_METHOD.BIOMETRIC_FACE, - }, - }, - /** - * A flag that ensures data is stored securely and is only accessible - * when the device has at least passcode set and is currently unlocked. - */ - WHEN_PASSCODE_SET_THIS_DEVICE_ONLY: SecureStore.WHEN_PASSCODE_SET_THIS_DEVICE_ONLY, -} as const satisfies SecureStoreValues; - -/** - * Thin wrapper around SecureStore methods used by multifactor authentication. - * Centralizes calls so they can be mocked or swapped on non-native platforms. - */ -const SECURE_STORE_METHODS = { - canUseBiometricAuthentication: SecureStore.canUseBiometricAuthentication, - canUseDeviceCredentialsAuthentication: SecureStore.canUseDeviceCredentialsAuthentication, - getItemAsync: SecureStore.getItemAsync, - setItemAsync: SecureStore.setItemAsync, - deleteItemAsync: SecureStore.deleteItemAsync, -} satisfies SecureStoreMethods; - -type SecureStoreOptions = SecureStore.SecureStoreOptions; - -export {SECURE_STORE_METHODS, SECURE_STORE_VALUES}; -export type {SecureStoreOptions}; diff --git a/src/libs/MultifactorAuthentication/NativeBiometrics/SecureStore/index.web.ts b/src/libs/MultifactorAuthentication/NativeBiometrics/SecureStore/index.web.ts deleted file mode 100644 index e3ba8f25efc2f..0000000000000 --- a/src/libs/MultifactorAuthentication/NativeBiometrics/SecureStore/index.web.ts +++ /dev/null @@ -1,64 +0,0 @@ -import MARQETA_VALUES from '@libs/MultifactorAuthentication/shared/MarqetaValues'; -import type {SecureStoreMethods, SecureStoreValues} from './types'; - -/** - * Web polyfill values mirroring the native SecureStore API for multifactor authentication. - * Provides stable auth type codes and configuration flags for non-native environments. - */ -const SECURE_STORE_VALUES = { - AUTH_TYPE: { - UNKNOWN: { - CODE: -1, - NAME: 'Unknown', - MARQETA_VALUE: MARQETA_VALUES.AUTHENTICATION_METHOD.OTHER, - }, - NONE: { - CODE: 0, - NAME: 'None', - MARQETA_VALUE: MARQETA_VALUES.AUTHENTICATION_METHOD.OTHER, - }, - CREDENTIALS: { - CODE: 1, - NAME: 'Credentials', - MARQETA_VALUE: MARQETA_VALUES.AUTHENTICATION_METHOD.KNOWLEDGE_BASED, - }, - BIOMETRICS: { - CODE: 2, - NAME: 'Biometrics', - MARQETA_VALUE: MARQETA_VALUES.AUTHENTICATION_METHOD.BIOMETRIC_FINGERPRINT, - }, - FACE_ID: { - CODE: 3, - NAME: 'Face ID', - MARQETA_VALUE: MARQETA_VALUES.AUTHENTICATION_METHOD.BIOMETRIC_FACE, - }, - TOUCH_ID: { - CODE: 4, - NAME: 'Touch ID', - MARQETA_VALUE: MARQETA_VALUES.AUTHENTICATION_METHOD.BIOMETRIC_FINGERPRINT, - }, - OPTIC_ID: { - CODE: 5, - NAME: 'Optic ID', - MARQETA_VALUE: MARQETA_VALUES.AUTHENTICATION_METHOD.OTHER, - }, - }, - WHEN_PASSCODE_SET_THIS_DEVICE_ONLY: -1, -} as const satisfies SecureStoreValues; - -/** - * Web-safe polyfill implementations of SecureStore methods used by multifactor authentication. - * Always report that secure auth is unavailable and operate on no-op async calls. - */ -const SECURE_STORE_METHODS = { - canUseBiometricAuthentication: () => false, - canUseDeviceCredentialsAuthentication: () => false, - getItemAsync: async () => [null, 0], - setItemAsync: async () => 0, - deleteItemAsync: async () => {}, -} as const satisfies SecureStoreMethods; - -type SecureStoreOptions = Record; - -export {SECURE_STORE_METHODS, SECURE_STORE_VALUES}; -export type {SecureStoreOptions}; diff --git a/src/libs/MultifactorAuthentication/NativeBiometrics/SecureStore/types.ts b/src/libs/MultifactorAuthentication/NativeBiometrics/SecureStore/types.ts deleted file mode 100644 index c042ac5aa6f09..0000000000000 --- a/src/libs/MultifactorAuthentication/NativeBiometrics/SecureStore/types.ts +++ /dev/null @@ -1,62 +0,0 @@ -/** - * Common type definitions for SecureStore implementations used by multifactor authentication. - * Shared between native and web polyfill implementations. - */ - -/** - * Authentication type information containing both the numeric code and human-readable name. - */ -type AuthTypeInfo = { - CODE: number; - NAME: string; - MARQETA_VALUE: string; -}; - -/** - * Mapping of authentication type names to their corresponding codes and names. - */ -type AuthTypeMap = { - UNKNOWN: AuthTypeInfo; - NONE: AuthTypeInfo; - CREDENTIALS: AuthTypeInfo; - BIOMETRICS: AuthTypeInfo; - FACE_ID: AuthTypeInfo; - TOUCH_ID: AuthTypeInfo; - OPTIC_ID: AuthTypeInfo; -}; - -/** - * Exposed flags for the secure store. - */ -type SecureStoreFlags = { - WHEN_PASSCODE_SET_THIS_DEVICE_ONLY: number; -}; - -/** - * Object containing both the authentication type map and the flags for the secure store. - */ -type SecureStoreValues = { - AUTH_TYPE: AuthTypeMap; -} & SecureStoreFlags; - -/** - * SecureStore configuration options. - * On native platforms, this maps to expo-secure-store's SecureStoreOptions. - * On web, this is a generic record for polyfill compatibility. - */ -type SecureStoreOptions = Record; - -/** - * Methods for the secure store. - * This type is used to satisfy both native and web polyfill implementations. - * For details on the methods, see the expo-secure-store documentation. - */ -type SecureStoreMethods = { - canUseBiometricAuthentication: () => boolean; - canUseDeviceCredentialsAuthentication: () => boolean; - getItemAsync: (key: string, options: SecureStoreOptions) => Promise<[string | null, number] | (string | null)>; - setItemAsync: (key: string, value: string, options: SecureStoreOptions) => Promise; - deleteItemAsync: (key: string, options: SecureStoreOptions) => Promise; -}; - -export type {SecureStoreValues, SecureStoreMethods}; diff --git a/src/libs/MultifactorAuthentication/NativeBiometrics/VALUES.ts b/src/libs/MultifactorAuthentication/NativeBiometrics/VALUES.ts deleted file mode 100644 index 5825c90ba9bb1..0000000000000 --- a/src/libs/MultifactorAuthentication/NativeBiometrics/VALUES.ts +++ /dev/null @@ -1,56 +0,0 @@ -/** - * Constants specific to native biometrics (ED25519 / SecureStore / Expo). - */ -import SHARED_VALUES from '@libs/MultifactorAuthentication/shared/VALUES'; - -const {REASON} = SHARED_VALUES; - -/** - * Expo error message search strings and separator. - */ -const EXPO_ERRORS = { - SEPARATOR: 'Caused by:', - SEARCH_STRING: { - NOT_IN_FOREGROUND: 'not in the foreground', - IN_PROGRESS: 'in progress', - CANCELED: 'canceled', - EXISTS: 'already exists', - NO_AUTHENTICATION: 'No authentication method available', - OLD_ANDROID: 'NoSuchMethodError', - }, -} as const; - -const NATIVE_BIOMETRICS_VALUES = { - /** - * Keychain service name for secure key storage. - */ - KEYCHAIN_SERVICE: 'Expensify', - - /** - * EdDSA key type identifier referred to as EdDSA in the Auth. - */ - ED25519_TYPE: 'biometric', - - /** - * Key alias identifiers for secure storage. - */ - KEY_ALIASES: { - PUBLIC_KEY: '3DS_SCA_KEY_PUBLIC', - PRIVATE_KEY: '3DS_SCA_KEY_PRIVATE', - }, - EXPO_ERRORS, - - /** - * Maps authentication Expo errors to appropriate reason messages. - */ - EXPO_ERROR_MAPPINGS: { - [EXPO_ERRORS.SEARCH_STRING.CANCELED]: REASON.EXPO.CANCELED, - [EXPO_ERRORS.SEARCH_STRING.IN_PROGRESS]: REASON.EXPO.IN_PROGRESS, - [EXPO_ERRORS.SEARCH_STRING.NOT_IN_FOREGROUND]: REASON.EXPO.NOT_IN_FOREGROUND, - [EXPO_ERRORS.SEARCH_STRING.EXISTS]: REASON.EXPO.KEY_EXISTS, - [EXPO_ERRORS.SEARCH_STRING.NO_AUTHENTICATION]: REASON.EXPO.NO_METHOD_AVAILABLE, - [EXPO_ERRORS.SEARCH_STRING.OLD_ANDROID]: REASON.EXPO.NOT_SUPPORTED, - }, -} as const; - -export default NATIVE_BIOMETRICS_VALUES; diff --git a/src/libs/MultifactorAuthentication/NativeBiometrics/helpers.ts b/src/libs/MultifactorAuthentication/NativeBiometrics/helpers.ts deleted file mode 100644 index 7d352e6d9fc38..0000000000000 --- a/src/libs/MultifactorAuthentication/NativeBiometrics/helpers.ts +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Helper utilities for native biometrics Expo error decoding. - */ -import type {MultifactorAuthenticationReason} from '@libs/MultifactorAuthentication/shared/types'; -import VALUES from '@libs/MultifactorAuthentication/VALUES'; - -/** - * Decodes Expo error messages and maps them to authentication error reasons. - */ -function decodeExpoMessage(error: unknown): MultifactorAuthenticationReason { - const errorString = String(error); - const parts = errorString.split(VALUES.EXPO_ERRORS.SEPARATOR); - const searchString = parts.length > 1 ? parts.slice(1).join(';').trim() : errorString; - - for (const [searchKey, errorValue] of Object.entries(VALUES.EXPO_ERROR_MAPPINGS)) { - if (searchString.includes(searchKey)) { - return errorValue; - } - } - - return VALUES.REASON.EXPO.GENERIC; -} - -/** - * Decodes an Expo error message with optional fallback for generic errors. - */ -const decodeMultifactorAuthenticationExpoMessage = (message: unknown, fallback?: MultifactorAuthenticationReason): MultifactorAuthenticationReason => { - const decodedMessage = decodeExpoMessage(message); - return decodedMessage === VALUES.REASON.EXPO.GENERIC && fallback ? fallback : decodedMessage; -}; - -export default decodeMultifactorAuthenticationExpoMessage; diff --git a/src/libs/MultifactorAuthentication/NativeBiometrics/types.ts b/src/libs/MultifactorAuthentication/NativeBiometrics/types.ts deleted file mode 100644 index 33f6948de7573..0000000000000 --- a/src/libs/MultifactorAuthentication/NativeBiometrics/types.ts +++ /dev/null @@ -1,49 +0,0 @@ -/** - * Type definitions specific to native biometrics (ED25519 / KeyStore). - */ -import type {ValueOf} from 'type-fest'; -import type {MultifactorAuthenticationReason} from '@libs/MultifactorAuthentication/shared/types'; -import type CONST from '@src/CONST'; -import type {Base64URLString} from '@src/utils/Base64URL'; -import type {SECURE_STORE_VALUES} from './SecureStore'; -import type VALUES from './VALUES'; - -/** - * Represents a status result of multifactor authentication keystore operation. - * Contains the operation result value, reason message and auth type code. - */ -type MultifactorAuthenticationKeyStoreStatus = { - value: T; - - reason: MultifactorAuthenticationReason; - - type?: ValueOf['CODE']; -}; - -/** - * Identifier for different types of cryptographic keys. - */ -type MultifactorAuthenticationKeyType = ValueOf; - -/** - * Configuration options for multifactor key store operations. - */ -type MultifactorKeyStoreOptions = T extends typeof VALUES.KEY_ALIASES.PRIVATE_KEY - ? { - nativePromptTitle: string; - } - : void; - -type NativeBiometricsKeyInfo = { - rawId: Base64URLString; - type: typeof VALUES.ED25519_TYPE; - response: { - clientDataJSON: Base64URLString; - biometric: { - publicKey: Base64URLString; - algorithm: typeof CONST.COSE_ALGORITHM.EDDSA; - }; - }; -}; - -export type {MultifactorAuthenticationKeyStoreStatus, MultifactorAuthenticationKeyType, MultifactorKeyStoreOptions, NativeBiometricsKeyInfo}; diff --git a/src/libs/MultifactorAuthentication/Passkeys/WebAuthn.ts b/src/libs/MultifactorAuthentication/Passkeys/WebAuthn.ts index 6d9a8db757d6b..7efeaa49a96d3 100644 --- a/src/libs/MultifactorAuthentication/Passkeys/WebAuthn.ts +++ b/src/libs/MultifactorAuthentication/Passkeys/WebAuthn.ts @@ -10,7 +10,6 @@ import Base64URL from '@src/utils/Base64URL'; /** * Passkey authentication type metadata. - * Not part of SecureStore — passkeys bypass the native secure store entirely. */ const PASSKEY_AUTH_TYPE = { NAME: 'Passkey', diff --git a/src/libs/MultifactorAuthentication/VALUES.ts b/src/libs/MultifactorAuthentication/VALUES.ts index 5a8e72a0b1434..27f5aa2a9712d 100644 --- a/src/libs/MultifactorAuthentication/VALUES.ts +++ b/src/libs/MultifactorAuthentication/VALUES.ts @@ -1,16 +1,14 @@ /** - * Barrel file that merges shared, NativeBiometrics, and Passkeys VALUES + * Barrel file that merges shared, NativeBiometricsHSM, and Passkeys VALUES * into a single object matching the original Biometrics/VALUES shape. * This ensures CONST.MULTIFACTOR_AUTHENTICATION continues working everywhere. */ -import NATIVE_BIOMETRICS_VALUES from './NativeBiometrics/VALUES'; import NATIVE_BIOMETRICS_HSM_VALUES from './NativeBiometricsHSM/VALUES'; import PASSKEY_VALUES from './Passkeys/VALUES'; import SHARED_VALUES from './shared/VALUES'; const MULTIFACTOR_AUTHENTICATION_VALUES = { ...SHARED_VALUES, - ...NATIVE_BIOMETRICS_VALUES, ...NATIVE_BIOMETRICS_HSM_VALUES, ...PASSKEY_VALUES, } as const; diff --git a/src/libs/MultifactorAuthentication/shared/VALUES.ts b/src/libs/MultifactorAuthentication/shared/VALUES.ts index cecc0ffc30190..f3417a37dc1f0 100644 --- a/src/libs/MultifactorAuthentication/shared/VALUES.ts +++ b/src/libs/MultifactorAuthentication/shared/VALUES.ts @@ -62,15 +62,6 @@ const REASON = { CHALLENGE_MISSING: 'Challenge is missing', CHALLENGE_SIGNED: 'Challenge signed successfully', }, - EXPO: { - CANCELED: 'Authentication canceled by user', - IN_PROGRESS: 'Authentication already in progress', - NOT_IN_FOREGROUND: 'Application must be in the foreground', - KEY_EXISTS: 'This key already exists', - NO_METHOD_AVAILABLE: 'No authentication methods available', - NOT_SUPPORTED: 'This feature is not supported on the device', - GENERIC: 'An error occurred', - }, GENERIC: { SIGNATURE_MISSING: 'Signature is missing', /** The device type is correct for this scenario but no authentication methods are enrolled (e.g. no fingerprint/face/passcode set up in device settings). */ @@ -84,17 +75,6 @@ const REASON = { UNKNOWN_RESPONSE: 'Unknown response', CANCELED: 'Flow canceled by user', }, - KEYSTORE: { - KEY_DELETED: 'Key successfully deleted from SecureStore', - REGISTRATION_REQUIRED: 'Key is stored locally but not found on server', - KEY_MISSING: 'Key is missing', - KEY_SAVED: 'Key successfully saved in SecureStore', - UNABLE_TO_SAVE_KEY: 'Failed to save key in SecureStore', - UNABLE_TO_DELETE_KEY: 'Failed to delete key from SecureStore', - KEY_RETRIEVED: 'Key successfully retrieved from SecureStore', - KEY_NOT_FOUND: 'Key not found in SecureStore', - UNABLE_TO_RETRIEVE_KEY: 'Failed to retrieve key from SecureStore', - }, WEBAUTHN: { NOT_ALLOWED: 'NotAllowedError', INVALID_STATE: 'InvalidStateError', @@ -218,9 +198,6 @@ type ReasonValue = ValueOf<{ /** Known errors the user is likely to encounter (cancellations, expired transactions, unsupported devices, etc.). Logged at 'info' level. */ const ROUTINE_FAILURES = new Set([ - REASON.EXPO.CANCELED, - REASON.EXPO.NO_METHOD_AVAILABLE, - REASON.EXPO.NOT_SUPPORTED, REASON.GENERIC.CANCELED, REASON.GENERIC.NO_AUTHENTICATION_METHODS_ENROLLED, REASON.GENERIC.AUTHENTICATION_TYPE_NOT_SUPPORTED, @@ -253,17 +230,8 @@ const ANOMALOUS_FAILURES = new Set([ REASON.BACKEND.INVALID_KEY, REASON.BACKEND.AUTHENTICATION_REQUIRED, REASON.BACKEND.UNAUTHORIZED, - REASON.KEYSTORE.KEY_MISSING, - REASON.KEYSTORE.KEY_NOT_FOUND, - REASON.KEYSTORE.REGISTRATION_REQUIRED, - REASON.KEYSTORE.UNABLE_TO_SAVE_KEY, - REASON.KEYSTORE.UNABLE_TO_DELETE_KEY, - REASON.KEYSTORE.UNABLE_TO_RETRIEVE_KEY, REASON.GENERIC.BAD_REQUEST, REASON.GENERIC.UNKNOWN_RESPONSE, - REASON.EXPO.IN_PROGRESS, - REASON.EXPO.NOT_IN_FOREGROUND, - REASON.EXPO.GENERIC, REASON.WEBAUTHN.INVALID_STATE, REASON.WEBAUTHN.SECURITY_ERROR, REASON.WEBAUTHN.CONSTRAINT_ERROR, @@ -294,7 +262,6 @@ const SHARED_VALUES = { */ PROMPT_TYPE_MAP: { BIOMETRICS_HSM: PROMPT_NAMES.BIOMETRICS_HSM, - BIOMETRICS: PROMPT_NAMES.BIOMETRICS, PASSKEYS: PROMPT_NAMES.PASSKEYS, }, @@ -303,7 +270,6 @@ const SHARED_VALUES = { */ TYPE: { BIOMETRICS_HSM: 'BIOMETRICS_HSM', - BIOMETRICS: 'BIOMETRICS', PASSKEYS: 'PASSKEYS', }, CHALLENGE_TYPE: { diff --git a/src/libs/MultifactorAuthentication/shared/types.ts b/src/libs/MultifactorAuthentication/shared/types.ts index 98cd57160c22d..ae30a27c7aab7 100644 --- a/src/libs/MultifactorAuthentication/shared/types.ts +++ b/src/libs/MultifactorAuthentication/shared/types.ts @@ -1,10 +1,9 @@ /** * Shared type definitions for multifactor authentication operations. - * Technology-agnostic types used across NativeBiometrics and Passkeys. + * Technology-agnostic types used across NativeBiometricsHSM and Passkeys. */ import type {ValueOf} from 'type-fest'; import type {MultifactorAuthenticationScenario, MultifactorAuthenticationScenarioAdditionalParams} from '@components/MultifactorAuthentication/config/types'; -import type {NativeBiometricsKeyInfo} from '@libs/MultifactorAuthentication/NativeBiometrics/types'; import type NativeBiometricsHSMKeyInfo from '@libs/MultifactorAuthentication/NativeBiometricsHSM/types'; import type NATIVE_BIOMETRICS_HSM_VALUES from '@libs/MultifactorAuthentication/NativeBiometricsHSM/VALUES'; import type {PasskeyRegistrationKeyInfo} from '@libs/MultifactorAuthentication/Passkeys/types'; @@ -53,7 +52,7 @@ type MultifactorAuthenticationResponseMap = typeof VALUES.API_RESPONSE_MAP; type MultifactorAuthenticationActionParams, R extends keyof AllMultifactorAuthenticationBaseParameters> = T & Pick & {authenticationMethod: MarqetaAuthTypeName}; -type RegistrationKeyInfo = NativeBiometricsKeyInfo | NativeBiometricsHSMKeyInfo | PasskeyRegistrationKeyInfo; +type RegistrationKeyInfo = NativeBiometricsHSMKeyInfo | PasskeyRegistrationKeyInfo; type ChallengeType = ValueOf; diff --git a/src/pages/MultifactorAuthentication/OutcomePage.tsx b/src/pages/MultifactorAuthentication/OutcomePage.tsx index 0b141b260fd8e..3810745282ac9 100644 --- a/src/pages/MultifactorAuthentication/OutcomePage.tsx +++ b/src/pages/MultifactorAuthentication/OutcomePage.tsx @@ -10,7 +10,7 @@ import CONST from '@src/CONST'; * TODO: This is a temporary solution until proper error handling is implemented (https://github.com/Expensify/App/issues/83036). */ function isServerError(error: ErrorState): boolean { - const routineDeviceFailures: MultifactorAuthenticationReason[] = [CONST.MULTIFACTOR_AUTHENTICATION.REASON.EXPO.CANCELED, CONST.MULTIFACTOR_AUTHENTICATION.REASON.GENERIC.CANCELED]; + const routineDeviceFailures: MultifactorAuthenticationReason[] = [CONST.MULTIFACTOR_AUTHENTICATION.REASON.HSM.CANCELED, CONST.MULTIFACTOR_AUTHENTICATION.REASON.GENERIC.CANCELED]; if (routineDeviceFailures.includes(error.reason)) { return false; } diff --git a/tests/unit/MultifactorAuthentication/ED25519.test.ts b/tests/unit/MultifactorAuthentication/ED25519.test.ts deleted file mode 100644 index de20a4aabbb37..0000000000000 --- a/tests/unit/MultifactorAuthentication/ED25519.test.ts +++ /dev/null @@ -1,75 +0,0 @@ -import {TextEncoder} from 'util'; -import {concatBytes, createAuthenticatorData, generateKeyPair, randomBytes, sha256, signToken, utf8ToBytes} from '@libs/MultifactorAuthentication/NativeBiometrics/ED25519'; -import type {MultifactorAuthenticationChallengeObject} from '@libs/MultifactorAuthentication/shared/challengeTypes'; -import VALUES from '@libs/MultifactorAuthentication/VALUES'; - -global.TextEncoder = TextEncoder as typeof global.TextEncoder; - -describe('MultifactorAuthentication Biometrics ED25519 helpers', () => { - it('generates a valid hex-encoded key pair', () => { - const {privateKey, publicKey} = generateKeyPair(); - - expect(typeof privateKey).toBe('string'); - expect(typeof publicKey).toBe('string'); - expect(privateKey).not.toHaveLength(0); - expect(publicKey).not.toHaveLength(0); - - expect(() => privateKey).not.toThrow(); - expect(() => publicKey).not.toThrow(); - }); - - it('creates deterministic binary data for a given rpId', () => { - const rpId = 'example.com'; - - const first = createAuthenticatorData(rpId); - const second = createAuthenticatorData(rpId); - - expect(first).toBeInstanceOf(Uint8Array); - expect(second).toBeInstanceOf(Uint8Array); - expect(first).toHaveLength(second.length); - expect(first).toStrictEqual(second); - }); - - it('produces a signed challenge with expected shape', () => { - const {privateKey, publicKey} = generateKeyPair(); - - const challengeObject: MultifactorAuthenticationChallengeObject = { - challenge: 'test-challenge', - rpId: 'example.com', - allowCredentials: [ - { - type: 'public-key', - id: publicKey, - }, - ], - userVerification: 'required', - timeout: 60000, - }; - - const result = signToken(challengeObject, privateKey, publicKey); - - expect(result.type).toBe(VALUES.ED25519_TYPE); - - // Verify rawId matches the public key - expect(result.rawId).toBe(publicKey); - expect(result.response.authenticatorData).toEqual(expect.any(String)); - expect(result.response.clientDataJSON).toEqual(expect.any(String)); - expect(result.response.signature).toEqual(expect.any(String)); - }); - - it('matches sha256 over concatBytes with utf8ToBytes', () => { - const left = randomBytes(16); - const right = randomBytes(16); - - const concatenated = concatBytes(left, right); - const message = 'test-message'; - const messageBytes = utf8ToBytes(message); - - const hashOfConcat = sha256(concatenated); - const hashOfConcatWithMessage = sha256(concatBytes(concatenated, messageBytes)); - - expect(hashOfConcat).toBeInstanceOf(Uint8Array); - expect(hashOfConcatWithMessage).toBeInstanceOf(Uint8Array); - expect(hashOfConcatWithMessage).toHaveLength(hashOfConcat.length); - }); -}); diff --git a/tests/unit/MultifactorAuthentication/SecureStore.test.ts b/tests/unit/MultifactorAuthentication/SecureStore.test.ts deleted file mode 100644 index 5083b90b93735..0000000000000 --- a/tests/unit/MultifactorAuthentication/SecureStore.test.ts +++ /dev/null @@ -1,21 +0,0 @@ -import {SECURE_STORE_METHODS, SECURE_STORE_VALUES} from '@libs/MultifactorAuthentication/NativeBiometrics/SecureStore'; - -describe('MultifactorAuthentication Biometrics SecureStore (native)', () => { - it('exposes stable AUTH_TYPE mapping', () => { - expect(SECURE_STORE_VALUES.AUTH_TYPE.UNKNOWN.NAME).toBe('Unknown'); - expect(SECURE_STORE_VALUES.AUTH_TYPE.NONE.NAME).toBe('None'); - expect(SECURE_STORE_VALUES.AUTH_TYPE.CREDENTIALS.NAME).toBe('Credentials'); - expect(SECURE_STORE_VALUES.AUTH_TYPE.BIOMETRICS.NAME).toBe('Biometrics'); - expect(SECURE_STORE_VALUES.AUTH_TYPE.FACE_ID.NAME).toBe('Face ID'); - expect(SECURE_STORE_VALUES.AUTH_TYPE.TOUCH_ID.NAME).toBe('Touch ID'); - expect(SECURE_STORE_VALUES.AUTH_TYPE.OPTIC_ID.NAME).toBe('Optic ID'); - }); - - it('exposes wrapper methods as functions', () => { - expect(typeof SECURE_STORE_METHODS.canUseBiometricAuthentication).toBe('function'); - expect(typeof SECURE_STORE_METHODS.canUseDeviceCredentialsAuthentication).toBe('function'); - expect(typeof SECURE_STORE_METHODS.getItemAsync).toBe('function'); - expect(typeof SECURE_STORE_METHODS.setItemAsync).toBe('function'); - expect(typeof SECURE_STORE_METHODS.deleteItemAsync).toBe('function'); - }); -}); diff --git a/tests/unit/components/MultifactorAuthentication/processing.test.ts b/tests/unit/components/MultifactorAuthentication/processing.test.ts index 6e6f24e4a8ef1..b7c34fb92da06 100644 --- a/tests/unit/components/MultifactorAuthentication/processing.test.ts +++ b/tests/unit/components/MultifactorAuthentication/processing.test.ts @@ -17,16 +17,16 @@ describe('MultifactorAuthentication processing', () => { }); }); - // Given a keyInfo object with biometric type (NativeBiometrics) + // Given a keyInfo object with biometric type (HSM) // When processRegistration is called // Then it should forward keyInfo to registerAuthenticationKey it('should call registerAuthenticationKey with the provided keyInfo', async () => { const keyInfo = { rawId: 'public-key-123', - type: 'biometric' as const, + type: 'biometric-hsm' as const, response: { clientDataJSON: 'encoded-client-data', - biometric: {publicKey: 'public-key-123', algorithm: CONST.COSE_ALGORITHM.EDDSA}, + biometric: {publicKey: 'public-key-123', algorithm: CONST.COSE_ALGORITHM.ES256}, }, }; @@ -71,7 +71,7 @@ describe('MultifactorAuthentication processing', () => { }); const result = await processRegistration({ - keyInfo: {rawId: 'key', type: 'biometric' as const, response: {clientDataJSON: 'cdj', biometric: {publicKey: 'key', algorithm: CONST.COSE_ALGORITHM.EDDSA}}}, + keyInfo: {rawId: 'key', type: 'biometric-hsm' as const, response: {clientDataJSON: 'cdj', biometric: {publicKey: 'key', algorithm: CONST.COSE_ALGORITHM.ES256}}}, }); expect(result.success).toBe(true); @@ -87,7 +87,7 @@ describe('MultifactorAuthentication processing', () => { }); const result = await processRegistration({ - keyInfo: {rawId: 'key', type: 'biometric' as const, response: {clientDataJSON: 'cdj', biometric: {publicKey: 'key', algorithm: CONST.COSE_ALGORITHM.EDDSA}}}, + keyInfo: {rawId: 'key', type: 'biometric-hsm' as const, response: {clientDataJSON: 'cdj', biometric: {publicKey: 'key', algorithm: CONST.COSE_ALGORITHM.ES256}}}, }); expect(result.success).toBe(false); diff --git a/tests/unit/components/MultifactorAuthentication/useNativeBiometrics.test.ts b/tests/unit/components/MultifactorAuthentication/useNativeBiometrics.test.ts deleted file mode 100644 index 61c8e5eb70b79..0000000000000 --- a/tests/unit/components/MultifactorAuthentication/useNativeBiometrics.test.ts +++ /dev/null @@ -1,428 +0,0 @@ -import {act, renderHook} from '@testing-library/react-native'; -import useNativeBiometrics from '@components/MultifactorAuthentication/biometrics/useNativeBiometrics'; -import {generateKeyPair, signToken as signTokenED25519} from '@libs/MultifactorAuthentication/NativeBiometrics/ED25519'; -import {PrivateKeyStore, PublicKeyStore} from '@libs/MultifactorAuthentication/NativeBiometrics/KeyStore'; -import type {AuthenticationChallenge} from '@libs/MultifactorAuthentication/shared/challengeTypes'; -import VALUES from '@libs/MultifactorAuthentication/VALUES'; -import CONST from '@src/CONST'; - -jest.mock('@hooks/useCurrentUserPersonalDetails', () => ({ - // eslint-disable-next-line @typescript-eslint/naming-convention - __esModule: true, - default: () => ({ - accountID: 12345, - }), -})); - -jest.mock('@hooks/useLocalize', () => ({ - // eslint-disable-next-line @typescript-eslint/naming-convention - __esModule: true, - default: () => ({ - translate: (key: string) => `translated_${key}`, - }), -})); - -let mockMultifactorAuthenticationPublicKeyIDs: string[] | undefined = []; - -jest.mock('@hooks/useOnyx', () => ({ - // eslint-disable-next-line @typescript-eslint/naming-convention - __esModule: true, - default: () => [mockMultifactorAuthenticationPublicKeyIDs], -})); - -jest.mock('@userActions/MultifactorAuthentication'); -jest.mock('@libs/MultifactorAuthentication/NativeBiometrics/ED25519'); -jest.mock('@libs/MultifactorAuthentication/NativeBiometrics/KeyStore', () => ({ - PublicKeyStore: { - supportedAuthentication: {biometrics: true, deviceCredentials: true}, - set: jest.fn(), - get: jest.fn(), - delete: jest.fn(), - }, - PrivateKeyStore: { - supportedAuthentication: {biometrics: true, deviceCredentials: true}, - set: jest.fn(), - get: jest.fn(), - delete: jest.fn(), - }, -})); - -jest.mock('@components/MultifactorAuthentication/config', () => ({ - MULTIFACTOR_AUTHENTICATION_SCENARIO_CONFIG: new Proxy( - {}, - { - get: () => ({ - nativePromptTitle: 'multifactorAuthentication.biometricsTest.promptTitle', - }), - }, - ), -})); -jest.mock('@userActions/MultifactorAuthentication/processing'); - -describe('useNativeBiometrics hook', () => { - // eslint-disable-next-line @typescript-eslint/unbound-method - const {delete: privateKeyStoreDelete} = jest.mocked(PrivateKeyStore); - // eslint-disable-next-line @typescript-eslint/unbound-method - const {delete: publicKeyStoreDelete} = jest.mocked(PublicKeyStore); - - beforeEach(() => { - jest.clearAllMocks(); - // Reset the Onyx mock - mockMultifactorAuthenticationPublicKeyIDs = []; - // Reset PublicKeyStore.supportedAuthentication to default - Object.defineProperty(PublicKeyStore, 'supportedAuthentication', { - value: {biometrics: true, deviceCredentials: true}, - writable: true, - configurable: true, - }); - // Setup default mock for PublicKeyStore - (PublicKeyStore.get as jest.Mock).mockResolvedValue({ - value: null, - reason: CONST.MULTIFACTOR_AUTHENTICATION.REASON.KEYSTORE.KEY_RETRIEVED, - }); - }); - - describe('Hook initialization', () => { - it('should return hook with required properties', () => { - const {result} = renderHook(() => useNativeBiometrics()); - - expect(result.current).toHaveProperty('serverKnownCredentialIDs'); - expect(result.current).toHaveProperty('doesDeviceSupportAuthenticationMethod'); - expect(result.current).toHaveProperty('getLocalCredentialID'); - expect(result.current).toHaveProperty('areLocalCredentialsKnownToServer'); - expect(result.current).toHaveProperty('register'); - expect(result.current).toHaveProperty('authorize'); - expect(result.current).toHaveProperty('deleteLocalKeysForAccount'); - }); - - it('should initialize info with biometrics status', async () => { - const {result} = renderHook(() => useNativeBiometrics()); - - await expect(result.current.doesDeviceSupportAuthenticationMethod()).resolves.toBe(true); - await expect(result.current.getLocalCredentialID()).resolves.toBeUndefined(); - await expect(result.current.areLocalCredentialsKnownToServer()).resolves.toBe(false); - }); - }); - - describe('doesDeviceSupportAuthenticationMethod', () => { - it('should return true when device supports biometrics', async () => { - const {result} = renderHook(() => useNativeBiometrics()); - - await expect(result.current.doesDeviceSupportAuthenticationMethod()).resolves.toBe(true); - }); - - it('should return boolean based on supportedAuthentication', async () => { - const {result} = renderHook(() => useNativeBiometrics()); - - const support = await result.current.doesDeviceSupportAuthenticationMethod(); - const {biometrics, credentials} = PublicKeyStore.supportedAuthentication; - const expectedValue = biometrics || credentials; - - expect(support).toBe(expectedValue); - }); - }); - - describe('getLocalCredentialID', () => { - it('should return undefined when no local key exists', async () => { - const {result} = renderHook(() => useNativeBiometrics()); - - const key = await result.current.getLocalCredentialID(); - expect(key).toBeUndefined(); - }); - - it('should return the key string when a local key exists', async () => { - (PublicKeyStore.get as jest.Mock).mockResolvedValue({ - value: 'public-key-123', - reason: CONST.MULTIFACTOR_AUTHENTICATION.REASON.KEYSTORE.KEY_RETRIEVED, - }); - - const {result} = renderHook(() => useNativeBiometrics()); - - const key = await result.current.getLocalCredentialID(); - expect(key).toBe('public-key-123'); - }); - }); - - describe('areLocalCredentialsKnownToServer', () => { - it('should return false when no local credential exists', async () => { - const {result} = renderHook(() => useNativeBiometrics()); - - const isKnown = await result.current.areLocalCredentialsKnownToServer(); - expect(isKnown).toBe(false); - }); - - it('should return true when local credential is known to server', async () => { - mockMultifactorAuthenticationPublicKeyIDs = ['public-key-123']; - (PublicKeyStore.get as jest.Mock).mockResolvedValue({ - value: 'public-key-123', - reason: CONST.MULTIFACTOR_AUTHENTICATION.REASON.KEYSTORE.KEY_RETRIEVED, - }); - - const {result} = renderHook(() => useNativeBiometrics()); - - const isKnown = await result.current.areLocalCredentialsKnownToServer(); - expect(isKnown).toBe(true); - }); - }); - - describe('serverKnownCredentialIDs', () => { - it('should expose credential IDs from Onyx state', () => { - mockMultifactorAuthenticationPublicKeyIDs = ['key-1', 'key-2']; - const {result} = renderHook(() => useNativeBiometrics()); - - expect(result.current.serverKnownCredentialIDs).toEqual(['key-1', 'key-2']); - }); - - it('should return empty array when Onyx state is empty', () => { - mockMultifactorAuthenticationPublicKeyIDs = []; - const {result} = renderHook(() => useNativeBiometrics()); - - expect(result.current.serverKnownCredentialIDs).toEqual([]); - }); - }); - - describe('haveCredentialsEverBeenConfigured', () => { - it('should return false when Onyx state is undefined', () => { - mockMultifactorAuthenticationPublicKeyIDs = undefined; - const {result} = renderHook(() => useNativeBiometrics()); - - expect(result.current.haveCredentialsEverBeenConfigured).toBe(false); - }); - - it('should return true when Onyx state is an empty array', () => { - mockMultifactorAuthenticationPublicKeyIDs = []; - const {result} = renderHook(() => useNativeBiometrics()); - - expect(result.current.haveCredentialsEverBeenConfigured).toBe(true); - }); - - it('should return true when Onyx state has credential IDs', () => { - mockMultifactorAuthenticationPublicKeyIDs = ['key-1']; - const {result} = renderHook(() => useNativeBiometrics()); - - expect(result.current.haveCredentialsEverBeenConfigured).toBe(true); - }); - }); - - describe('register', () => { - const mockRegistrationChallenge = { - challenge: 'test-challenge-string', - rp: {id: 'expensify.com'}, - user: {id: 'user-123', displayName: 'Test User'}, - pubKeyCredParams: [{type: 'public-key' as const, alg: -8}], - timeout: 60000, - }; - - beforeEach(() => { - (generateKeyPair as jest.Mock).mockReturnValue({ - publicKey: 'public-key-123', - privateKey: 'private-key-123', - }); - (PrivateKeyStore.set as jest.Mock).mockResolvedValue({ - value: true, - reason: CONST.MULTIFACTOR_AUTHENTICATION.REASON.KEYSTORE.KEY_SAVED, - type: 0, - }); - (PublicKeyStore.set as jest.Mock).mockResolvedValue({ - value: true, - reason: CONST.MULTIFACTOR_AUTHENTICATION.REASON.KEYSTORE.KEY_SAVED, - }); - (PublicKeyStore.get as jest.Mock).mockResolvedValue({ - value: 'public-key-123', - reason: CONST.MULTIFACTOR_AUTHENTICATION.REASON.KEYSTORE.KEY_RETRIEVED, - }); - }); - - it('should generate key pair', async () => { - const {result} = renderHook(() => useNativeBiometrics()); - const onResult = jest.fn(); - - await act(async () => { - await result.current.register(onResult, mockRegistrationChallenge); - }); - - expect(generateKeyPair).toHaveBeenCalled(); - }); - - it('should store private key with prompt title', async () => { - const {result} = renderHook(() => useNativeBiometrics()); - const onResult = jest.fn(); - - await act(async () => { - await result.current.register(onResult, mockRegistrationChallenge); - }); - - // eslint-disable-next-line @typescript-eslint/unbound-method - expect(PrivateKeyStore.set).toHaveBeenCalledWith(12345, 'private-key-123', {nativePromptTitle: 'translated_multifactorAuthentication.letsVerifyItsYou'}); - }); - - it('should store public key', async () => { - const {result} = renderHook(() => useNativeBiometrics()); - const onResult = jest.fn(); - - await act(async () => { - await result.current.register(onResult, mockRegistrationChallenge); - }); - - // eslint-disable-next-line @typescript-eslint/unbound-method - expect(PrivateKeyStore.set).toHaveBeenCalled(); - // eslint-disable-next-line @typescript-eslint/unbound-method - expect(PublicKeyStore.set).toHaveBeenCalled(); - }); - - it('should handle successful registration flow and return keyInfo', async () => { - const {result} = renderHook(() => useNativeBiometrics()); - const onResult = jest.fn(); - - await act(async () => { - await result.current.register(onResult, mockRegistrationChallenge); - }); - - // eslint-disable-next-line @typescript-eslint/unbound-method - expect(generateKeyPair).toHaveBeenCalled(); - // eslint-disable-next-line @typescript-eslint/unbound-method - expect(PrivateKeyStore.set).toHaveBeenCalled(); - // eslint-disable-next-line @typescript-eslint/unbound-method - expect(PublicKeyStore.set).toHaveBeenCalled(); - expect(onResult).toHaveBeenCalledWith( - expect.objectContaining({ - success: true, - keyInfo: expect.objectContaining({ - rawId: 'public-key-123', - type: 'biometric', - }), - }), - ); - }); - }); - - describe('authorize', () => { - beforeEach(() => { - (PrivateKeyStore.get as jest.Mock).mockResolvedValue({ - value: 'private-key-123', - reason: CONST.MULTIFACTOR_AUTHENTICATION.REASON.KEYSTORE.KEY_RETRIEVED, - type: 0, - }); - (PublicKeyStore.get as jest.Mock).mockResolvedValue({ - value: 'public-key-123', - reason: CONST.MULTIFACTOR_AUTHENTICATION.REASON.KEYSTORE.KEY_RETRIEVED, - }); - (signTokenED25519 as jest.Mock).mockReturnValue({ - clientDataJSON: 'client-data', - authenticatorData: 'auth-data', - signature: 'signature', - }); - }); - - // Note: Challenge fetching is now done in Main.tsx, not in useNativeBiometrics - // These tests verify the authorize function with challenge passed as a parameter - - const mockChallenge: AuthenticationChallenge = { - allowCredentials: [{id: 'public-key-123', type: 'public-key'}], - rpId: 'expensify.com', - challenge: 'test-challenge', - userVerification: 'required', - timeout: 60000, - }; - - it('should get private key from secure store', async () => { - const {result} = renderHook(() => useNativeBiometrics()); - const onResult = jest.fn(); - - await act(async () => { - await result.current.authorize({challenge: mockChallenge}, onResult); - }); - - // eslint-disable-next-line @typescript-eslint/unbound-method - expect(PrivateKeyStore.get).toHaveBeenCalledWith(12345, expect.any(Object)); - }); - - it('should handle missing private key', async () => { - (PrivateKeyStore.get as jest.Mock).mockResolvedValue({ - value: null, - reason: VALUES.REASON.KEYSTORE.KEY_MISSING, - }); - - const {result} = renderHook(() => useNativeBiometrics()); - const onResult = jest.fn(); - - await act(async () => { - await result.current.authorize({challenge: mockChallenge}, onResult); - }); - - expect(onResult).toHaveBeenCalledWith({ - success: false, - reason: VALUES.REASON.KEYSTORE.KEY_MISSING, - }); - }); - - it('should verify public key is in allowCredentials', async () => { - const challengeWithOtherKey: AuthenticationChallenge = { - allowCredentials: [{id: 'other-public-key', type: 'public-key'}], - rpId: 'expensify.com', - challenge: 'test-challenge', - userVerification: 'required', - timeout: 60000, - }; - - const {result} = renderHook(() => useNativeBiometrics()); - const onResult = jest.fn(); - - await act(async () => { - await result.current.authorize({challenge: challengeWithOtherKey}, onResult); - }); - - expect(publicKeyStoreDelete).toHaveBeenCalledWith(12345); - expect(privateKeyStoreDelete).toHaveBeenCalledWith(12345); - expect(onResult).toHaveBeenCalledWith({ - success: false, - reason: VALUES.REASON.KEYSTORE.REGISTRATION_REQUIRED, - }); - }); - - it('should sign challenge with private key', async () => { - const {result} = renderHook(() => useNativeBiometrics()); - const onResult = jest.fn(); - - await act(async () => { - await result.current.authorize({challenge: mockChallenge}, onResult); - }); - - expect(signTokenED25519).toHaveBeenCalledWith(expect.any(Object), expect.any(String), 'public-key-123'); - }); - - it('should return success with signed challenge', async () => { - const {result} = renderHook(() => useNativeBiometrics()); - const onResult = jest.fn(); - - await act(async () => { - await result.current.authorize({challenge: mockChallenge}, onResult); - }); - - expect(onResult).toHaveBeenCalledWith( - expect.objectContaining({ - success: true, - reason: VALUES.REASON.CHALLENGE.CHALLENGE_SIGNED, - signedChallenge: { - clientDataJSON: 'client-data', - authenticatorData: 'auth-data', - signature: 'signature', - }, - }), - ); - }); - }); - - describe('deleteLocalKeysForAccount', () => { - it('should delete keys', async () => { - const {result} = renderHook(() => useNativeBiometrics()); - - await act(async () => { - await result.current.deleteLocalKeysForAccount(); - }); - - expect(publicKeyStoreDelete).toHaveBeenCalledWith(12345); - expect(privateKeyStoreDelete).toHaveBeenCalledWith(12345); - }); - }); -}); diff --git a/tests/unit/libs/MultifactorAuthentication/NativeBiometrics/KeyStore.test.ts b/tests/unit/libs/MultifactorAuthentication/NativeBiometrics/KeyStore.test.ts deleted file mode 100644 index a0733a05f33d0..0000000000000 --- a/tests/unit/libs/MultifactorAuthentication/NativeBiometrics/KeyStore.test.ts +++ /dev/null @@ -1,162 +0,0 @@ -import {PrivateKeyStore, PublicKeyStore} from '@libs/MultifactorAuthentication/NativeBiometrics/KeyStore'; -import {SECURE_STORE_METHODS} from '@libs/MultifactorAuthentication/NativeBiometrics/SecureStore'; -import VALUES from '@libs/MultifactorAuthentication/VALUES'; - -jest.mock('@libs/MultifactorAuthentication/NativeBiometrics/SecureStore'); - -const mockedSecureStoreMethods = jest.mocked(SECURE_STORE_METHODS); - -// Mock the SECURE_STORE_METHODS -jest.mock('@libs/MultifactorAuthentication/NativeBiometrics/SecureStore', () => ({ - SECURE_STORE_METHODS: { - getItemAsync: jest.fn(), - setItemAsync: jest.fn(), - deleteItemAsync: jest.fn(), - canUseBiometricAuthentication: jest.fn(() => true), - canUseDeviceCredentialsAuthentication: jest.fn(() => true), - }, - SECURE_STORE_VALUES: { - WHEN_PASSCODE_SET_THIS_DEVICE_ONLY: 'WHEN_PASSCODE_SET_THIS_DEVICE_ONLY', - AUTH_TYPE: { - BIOMETRIC: {CODE: 'biometric', NAME: 'Biometric'}, - DEVICE_CREDENTIAL: {CODE: 'device_credential', NAME: 'Device Credential'}, - }, - }, -})); - -jest.mock('@libs/MultifactorAuthentication/NativeBiometrics/helpers', () => jest.fn(() => 'decoded-error-reason')); - -describe('MultifactorAuthentication KeyStore', () => { - const mockAccountID = 12345; - const mockKey = 'test-key-value'; - const mockOptions = {nativePromptTitle: 'Test Prompt'}; - - beforeEach(() => { - jest.clearAllMocks(); - }); - - describe('PrivateKeyStore', () => { - describe('set method', () => { - it('should save a private key successfully', async () => { - mockedSecureStoreMethods.setItemAsync.mockResolvedValueOnce('biometric'); - - const result = await PrivateKeyStore.set(mockAccountID, mockKey, {nativePromptTitle: 'Test'}); - - expect(result.value).toBe(true); - expect(result.reason).toBe(VALUES.REASON.KEYSTORE.KEY_SAVED); - expect(result.type).toBe('biometric'); - expect(mockedSecureStoreMethods.setItemAsync).toHaveBeenCalled(); - }); - - it('should handle save errors', async () => { - mockedSecureStoreMethods.setItemAsync.mockRejectedValueOnce(new Error('Save failed')); - - const result = await PrivateKeyStore.set(mockAccountID, mockKey, {nativePromptTitle: 'Test'}); - - expect(result.value).toBe(false); - }); - - it('should pass options to secure store', async () => { - mockedSecureStoreMethods.setItemAsync.mockResolvedValueOnce('biometric'); - - await PrivateKeyStore.set(mockAccountID, mockKey, mockOptions); - - expect(mockedSecureStoreMethods.setItemAsync).toHaveBeenCalled(); - }); - }); - - describe('get method', () => { - it('should retrieve a stored private key', async () => { - mockedSecureStoreMethods.getItemAsync.mockResolvedValueOnce([mockKey, 'biometric']); - - const result = await PrivateKeyStore.get(mockAccountID, {nativePromptTitle: 'Test'}); - - expect(result.value).toBe(mockKey); - expect(result.reason).toBe(VALUES.REASON.KEYSTORE.KEY_RETRIEVED); - expect(result.type).toBe('biometric'); - }); - - it('should return KEY_NOT_FOUND when key is null', async () => { - mockedSecureStoreMethods.getItemAsync.mockResolvedValueOnce([null, undefined]); - - const result = await PrivateKeyStore.get(mockAccountID, {nativePromptTitle: 'Test'}); - - expect(result.value).toBeNull(); - expect(result.reason).toBe(VALUES.REASON.KEYSTORE.KEY_NOT_FOUND); - }); - - it('should handle retrieval errors', async () => { - mockedSecureStoreMethods.getItemAsync.mockRejectedValueOnce(new Error('Retrieval failed')); - - const result = await PrivateKeyStore.get(mockAccountID, {nativePromptTitle: 'Test'}); - - expect(result.value).toBeNull(); - }); - }); - - describe('delete method', () => { - it('should delete a private key successfully', async () => { - mockedSecureStoreMethods.deleteItemAsync.mockResolvedValueOnce(undefined); - - const result = await PrivateKeyStore.delete(mockAccountID); - - expect(result.value).toBe(true); - expect(result.reason).toBe(VALUES.REASON.KEYSTORE.KEY_DELETED); - }); - - it('should handle deletion errors', async () => { - mockedSecureStoreMethods.deleteItemAsync.mockRejectedValueOnce(new Error('Deletion failed')); - - const result = await PrivateKeyStore.delete(mockAccountID); - - expect(result.value).toBe(false); - }); - }); - - describe('supportedAuthentication property', () => { - it('should return available authentication methods', () => { - const supported = PrivateKeyStore.supportedAuthentication; - - expect(supported).toEqual({ - biometrics: true, - credentials: true, - }); - }); - }); - }); - - describe('PublicKeyStore', () => { - describe('set method', () => { - it('should save a public key successfully', async () => { - mockedSecureStoreMethods.setItemAsync.mockResolvedValueOnce('device_credential'); - - const result = await PublicKeyStore.set(mockAccountID, mockKey); - - expect(result.value).toBe(true); - expect(result.reason).toBe(VALUES.REASON.KEYSTORE.KEY_SAVED); - }); - }); - - describe('get method', () => { - it('should retrieve a stored public key', async () => { - mockedSecureStoreMethods.getItemAsync.mockResolvedValueOnce([mockKey, 'device_credential']); - - const result = await PublicKeyStore.get(mockAccountID); - - expect(result.value).toBe(mockKey); - expect(result.reason).toBe(VALUES.REASON.KEYSTORE.KEY_RETRIEVED); - }); - }); - - describe('delete method', () => { - it('should delete a public key successfully', async () => { - mockedSecureStoreMethods.deleteItemAsync.mockResolvedValueOnce(undefined); - - const result = await PublicKeyStore.delete(mockAccountID); - - expect(result.value).toBe(true); - expect(result.reason).toBe(VALUES.REASON.KEYSTORE.KEY_DELETED); - }); - }); - }); -}); diff --git a/tests/unit/libs/MultifactorAuthentication/NativeBiometrics/helpers.test.ts b/tests/unit/libs/MultifactorAuthentication/NativeBiometrics/helpers.test.ts deleted file mode 100644 index db26f70c06acf..0000000000000 --- a/tests/unit/libs/MultifactorAuthentication/NativeBiometrics/helpers.test.ts +++ /dev/null @@ -1,61 +0,0 @@ -import decodeExpoMessage from '@libs/MultifactorAuthentication/NativeBiometrics/helpers'; -import VALUES from '@libs/MultifactorAuthentication/VALUES'; - -describe('NativeBiometrics helpers', () => { - describe('decodeExpoMessage', () => { - it('should decode user canceled error', () => { - const result = decodeExpoMessage('User canceled the action. Caused by: canceled'); - - expect(result).toBe(VALUES.REASON.EXPO.CANCELED); - }); - - it('should decode authentication in progress error', () => { - const result = decodeExpoMessage('Authentication already in progress. Caused by: in progress'); - - expect(result).toBe(VALUES.REASON.EXPO.IN_PROGRESS); - }); - - it('should decode not in foreground error', () => { - const result = decodeExpoMessage('App not in foreground. Caused by: not in the foreground'); - - expect(result).toBe(VALUES.REASON.EXPO.NOT_IN_FOREGROUND); - }); - - it('should decode key exists error', () => { - const result = decodeExpoMessage('This key already exists. Caused by: already exists'); - - expect(result).toBe(VALUES.REASON.EXPO.KEY_EXISTS); - }); - - it('should decode no authentication method error', () => { - const result = decodeExpoMessage('No authentication method available'); - - expect(result).toBe(VALUES.REASON.EXPO.NO_METHOD_AVAILABLE); - }); - - it('should decode old android error', () => { - const result = decodeExpoMessage('NoSuchMethodError: Cannot find method'); - - expect(result).toBe(VALUES.REASON.EXPO.NOT_SUPPORTED); - }); - - it('should return generic error for unknown error', () => { - const result = decodeExpoMessage('Unknown error message'); - - expect(result).toBe(VALUES.REASON.EXPO.GENERIC); - }); - - it('should handle error object', () => { - const errorObj = new Error('canceled'); - const result = decodeExpoMessage(errorObj); - - expect(result).toBe(VALUES.REASON.EXPO.CANCELED); - }); - - it('should use fallback when error is generic and fallback is provided', () => { - const result = decodeExpoMessage('Unknown error', VALUES.REASON.BACKEND.REGISTRATION_REQUIRED); - - expect(result).toBe(VALUES.REASON.BACKEND.REGISTRATION_REQUIRED); - }); - }); -});