From 621a147897763957d0ec0c7708cf53620af034ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bohdan=20Ju=C5=99=C3=AD=C4=8Dek?= <36101761+juriczech@users.noreply.github.com> Date: Wed, 24 Apr 2024 07:43:36 +0100 Subject: [PATCH] feat(suite-native): connect & authorize passphrased device (#12036) --- .../message-system/src/messageSystemThunks.ts | 8 +- .../wallet-core/src/device/deviceReducer.ts | 22 ++++-- .../wallet-core/src/device/deviceThunks.ts | 1 + suite-native/app/src/App.tsx | 37 ---------- suite-native/discovery/src/discoveryThunks.ts | 73 ++++++++++++++----- suite-native/discovery/src/utils.ts | 23 ------ 6 files changed, 74 insertions(+), 90 deletions(-) delete mode 100644 suite-native/discovery/src/utils.ts diff --git a/suite-common/message-system/src/messageSystemThunks.ts b/suite-common/message-system/src/messageSystemThunks.ts index 7fdc0a60c78..d8e3a20146d 100644 --- a/suite-common/message-system/src/messageSystemThunks.ts +++ b/suite-common/message-system/src/messageSystemThunks.ts @@ -1,6 +1,6 @@ import { decode, verify } from 'jws'; -import { getEnvironment, getJWSPublicKey, isCodesignBuild } from '@trezor/env-utils'; +import { getJWSPublicKey, isCodesignBuild, isNative } from '@trezor/env-utils'; import { scheduleAction } from '@trezor/utils'; import { createThunk } from '@suite-common/redux-utils'; import { MessageSystem } from '@suite-common/suite-types'; @@ -22,8 +22,6 @@ import { } from './messageSystemSelectors'; import { jws as configJwsLocal } from '../files/config.v1'; -const isMobile = () => getEnvironment() === 'mobile'; - const getConfigJws = async () => { const remoteConfigUrl = isCodesignBuild() ? CONFIG_URL_REMOTE.stable @@ -62,7 +60,7 @@ export const fetchConfigThunk = createThunk( if ( Date.now() >= - timestamp + (isMobile() ? FETCH_INTERVAL_IN_MS_MOBILE : FETCH_INTERVAL_IN_MS) + timestamp + (isNative() ? FETCH_INTERVAL_IN_MS_MOBILE : FETCH_INTERVAL_IN_MS) ) { try { const { configJws, isRemote } = await getConfigJws(); @@ -134,7 +132,7 @@ export const initMessageSystemThunk = createThunk( () => { checkConfig(); }, - isMobile() ? FETCH_CHECK_INTERVAL_IN_MS_MOBILE : FETCH_CHECK_INTERVAL_IN_MS, + isNative() ? FETCH_CHECK_INTERVAL_IN_MS_MOBILE : FETCH_CHECK_INTERVAL_IN_MS, ); }; diff --git a/suite-common/wallet-core/src/device/deviceReducer.ts b/suite-common/wallet-core/src/device/deviceReducer.ts index f98c7e69ec9..ff8859a6067 100644 --- a/suite-common/wallet-core/src/device/deviceReducer.ts +++ b/suite-common/wallet-core/src/device/deviceReducer.ts @@ -12,6 +12,7 @@ import { deviceAuthenticityActions, StoredAuthenticateDeviceResult, } from '@suite-common/device-authenticity'; +import { isNative } from '@trezor/env-utils'; import { deviceActions } from './deviceActions'; import { authorizeDevice } from './deviceThunks'; @@ -67,6 +68,16 @@ const merge = (device: AcquiredDevice, upcoming: Partial): Trezo }, }); +const getShouldUseEmptyPassphrase = (device: Device, deviceInstance?: number): boolean => { + if (!device.features) return false; + if (isNative() && typeof deviceInstance === 'number' && deviceInstance === 1) { + // On mobile, if device has instance === 1, we always want to use empty passphrase since we + // connect & authorize standard wallet by default. Other instances will have `usePassphraseProtection` set same way as web/desktop app. + return true; + } else { + return isUnlocked(device.features) && !device.features.passphrase_protection; + } +}; /** * Action handler: DEVICE.CONNECT + DEVICE.CONNECT_UNACQUIRED * @param {State} draft @@ -115,17 +126,18 @@ const connectDevice = (draft: State, device: Device) => { // fill draft with not affected devices otherDevices.forEach(d => draft.devices.push(d)); - // prepare new device + const deviceInstance = features.passphrase_protection + ? deviceUtils.getNewInstanceNumber(draft.devices, device) || 1 + : undefined; + const newDevice: TrezorDevice = { ...device, - useEmptyPassphrase: isUnlocked(device.features) && !features.passphrase_protection, + useEmptyPassphrase: getShouldUseEmptyPassphrase(device, deviceInstance), remember: false, connected: true, available: true, authConfirm: false, - instance: features.passphrase_protection - ? deviceUtils.getNewInstanceNumber(draft.devices, device) || 1 - : undefined, + instance: deviceInstance, buttonRequests: [], metadata: {}, ts: new Date().getTime(), diff --git a/suite-common/wallet-core/src/device/deviceThunks.ts b/suite-common/wallet-core/src/device/deviceThunks.ts index 12e131fde1a..4cedacc3c2d 100644 --- a/suite-common/wallet-core/src/device/deviceThunks.ts +++ b/suite-common/wallet-core/src/device/deviceThunks.ts @@ -325,6 +325,7 @@ export const authorizeDevice = createThunk( ); // get fresh data from reducer, `useEmptyPassphrase` might be changed after TrezorConnect call const freshDeviceData = getSelectedDevice(device, devices); + if (duplicate) { if (freshDeviceData!.useEmptyPassphrase) { // if currently selected device uses empty passphrase diff --git a/suite-native/app/src/App.tsx b/suite-native/app/src/App.tsx index 44ff30c6d0d..69c1dd11732 100644 --- a/suite-native/app/src/App.tsx +++ b/suite-native/app/src/App.tsx @@ -6,7 +6,6 @@ import { useDispatch, useSelector } from 'react-redux'; import * as SplashScreen from 'expo-splash-screen'; import * as Sentry from '@sentry/react-native'; -import TrezorConnect from '@trezor/connect'; import { selectIsAppReady, selectIsConnectInitialized, StoreProvider } from '@suite-native/state'; import { FormatterProvider } from '@suite-common/formatters'; import { NavigationContainerWithAnalytics } from '@suite-native/navigation'; @@ -35,42 +34,6 @@ const APP_STARTED_TIMESTAMP = Date.now(); // Keep the splash screen visible while we fetch resources SplashScreen.preventAutoHideAsync(); -// NOTE: This is a workaround wrapper for connect methods to prevent sending useEmptyPassphrase as undefined until we will implement passphrase behavior in mobile. -type ConnectKey = keyof typeof TrezorConnect; -const wrappedMethods = [ - 'getAccountInfo', - 'blockchainEstimateFee', - 'blockchainSetCustomBackend', - 'blockchainSubscribeFiatRates', - 'blockchainGetCurrentFiatRates', - 'blockchainSubscribe', - 'blockchainUnsubscribe', - 'cardanoGetPublicKey', - 'getDeviceState', - 'cardanoGetAddress', - 'getAddress', - 'rippleGetAddress', - 'ethereumGetAddress', - 'solanaGetAddress', - 'blockchainGetFiatRatesForTimestamps', - 'getAccountDescriptor', - 'blockchainGetAccountBalanceHistory', - 'blockchainUnsubscribeFiatRates', -]; - -wrappedMethods.forEach(key => { - const original: any = TrezorConnect[key as ConnectKey]; - if (!original) return; - (TrezorConnect[key as ConnectKey] as any) = async (params: any) => { - const result = await original({ - ...params, - useEmptyPassphrase: true, - }); - - return result; - }; -}); - const AppComponent = () => { const dispatch = useDispatch(); const formattersConfig = useFormattersConfig(); diff --git a/suite-native/discovery/src/discoveryThunks.ts b/suite-native/discovery/src/discoveryThunks.ts index 549c1ea3063..bef1d5759e9 100644 --- a/suite-native/discovery/src/discoveryThunks.ts +++ b/suite-native/discovery/src/discoveryThunks.ts @@ -1,4 +1,4 @@ -import { A } from '@mobily/ts-belt'; +import { A, G, pipe } from '@mobily/ts-belt'; import { getWeakRandomId } from '@trezor/utils'; import { createThunk } from '@suite-common/redux-utils'; @@ -26,7 +26,6 @@ import { requestDeviceAccess } from '@suite-native/device-mutex'; import { analytics, EventType } from '@suite-native/analytics'; import { isDebugEnv } from '@suite-native/config'; -import { fetchBundleDescriptors } from './utils'; import { selectDisabledDiscoveryNetworkSymbolsForDevelopment, selectDiscoveryStartTimeStamp, @@ -60,6 +59,30 @@ const getBatchSizeByCoin = (coin: NetworkSymbol): number => { type DiscoveryDescriptorItem = DiscoveryItem & { descriptor: string }; +const fetchBundleDescriptorsThunk = createThunk( + `${DISCOVERY_MODULE_PREFIX}/fetchBundleDescriptorsThunk`, + async (bundle: DiscoveryItem[], { getState }) => { + const device = selectDevice(getState()); + + const { success, payload } = await TrezorConnect.getAccountDescriptor({ + bundle, + skipFinalReload: true, + device, + useEmptyPassphrase: device?.useEmptyPassphrase, + }); + + if (success && payload) + return pipe( + payload, + A.filter(G.isNotNullable), + A.map(bundleItem => bundleItem.descriptor), + A.zipWith(bundle, (descriptor, bundleItem) => ({ ...bundleItem, descriptor })), + ) as DiscoveryDescriptorItem[]; + + return []; + }, +); + const finishNetworkTypeDiscoveryThunk = createThunk( `${DISCOVERY_MODULE_PREFIX}/finishNetworkTypeDiscoveryThunk`, (_, { dispatch, getState }) => { @@ -163,13 +186,15 @@ const addAccountByDescriptorThunk = createThunk( bundleItem: DiscoveryDescriptorItem; identity?: string; }, - { dispatch }, + { dispatch, getState }, ) => { + const device = selectDevice(getState()); const { success, payload: accountInfo } = await TrezorConnect.getAccountInfo({ coin: bundleItem.coin, identity, + device, descriptor: bundleItem.descriptor, - useEmptyPassphrase: true, + useEmptyPassphrase: device?.useEmptyPassphrase, skipFinalReload: true, ...getAccountInfoDetailsLevel(bundleItem.coin), }); @@ -207,7 +232,7 @@ const discoverAccountsByDescriptorThunk = createThunk( deviceState: string; identity?: string; }, - { dispatch }, + { dispatch, getState }, ) => { let isFinalRound = false; @@ -215,12 +240,14 @@ const discoverAccountsByDescriptorThunk = createThunk( isFinalRound = true; } + const device = selectDevice(getState()); for (const bundleItem of descriptorsBundle) { const { success, payload: accountInfo } = await TrezorConnect.getAccountInfo({ coin: bundleItem.coin, identity, descriptor: bundleItem.descriptor, - useEmptyPassphrase: true, + device, + useEmptyPassphrase: device?.useEmptyPassphrase, skipFinalReload: true, ...getAccountInfoDetailsLevel(bundleItem.coin), }); @@ -273,19 +300,23 @@ export const addAndDiscoverNetworkAccountThunk = createThunk( const accountPath = network.bip43Path.replace('i', index.toString()); - // Take exclusive access to the device and hold it until is the fetching of the descriptors done. - const deviceAccessResponse = await requestDeviceAccess(fetchBundleDescriptors, [ - { - path: accountPath, - coin: network.symbol, - index, - accountType, - networkType: network.networkType, - derivationType: getDerivationType(accountType), - suppressBackupWarning: true, - skipFinalReload: true, - }, - ]); + // Take exclusive access to the device and hold it until fetching of the descriptors is done. + const deviceAccessResponse = await requestDeviceAccess(() => + dispatch( + fetchBundleDescriptorsThunk([ + { + path: accountPath, + coin: network.symbol, + index, + accountType, + networkType: network.networkType, + derivationType: getDerivationType(accountType), + suppressBackupWarning: true, + skipFinalReload: true, + }, + ]), + ).unwrap(), + ); if (!deviceAccessResponse.success) { return undefined; @@ -393,7 +424,9 @@ const discoverNetworkBatchThunk = createThunk( } // Take exclusive access to the device and hold it until is the fetching of the descriptors done. - const deviceAccessResponse = await requestDeviceAccess(fetchBundleDescriptors, chunkBundle); + const deviceAccessResponse = await requestDeviceAccess(() => + dispatch(fetchBundleDescriptorsThunk(chunkBundle)).unwrap(), + ); if (!deviceAccessResponse.success) { return; diff --git a/suite-native/discovery/src/utils.ts b/suite-native/discovery/src/utils.ts deleted file mode 100644 index 73f219d3fa5..00000000000 --- a/suite-native/discovery/src/utils.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { A, G, pipe } from '@mobily/ts-belt'; - -import { DiscoveryItem } from '@suite-common/wallet-types'; -import TrezorConnect from '@trezor/connect'; - -import { DiscoveryDescriptorItem } from './types'; - -export const fetchBundleDescriptors = async (bundle: DiscoveryItem[]) => { - const { success, payload } = await TrezorConnect.getAccountDescriptor({ - bundle, - skipFinalReload: true, - }); - - if (success && payload) - return pipe( - payload, - A.filter(G.isNotNullable), - A.map(bundleItem => bundleItem.descriptor), - A.zipWith(bundle, (descriptor, bundleItem) => ({ ...bundleItem, descriptor })), - ) as DiscoveryDescriptorItem[]; - - return []; -};