Skip to content

Commit

Permalink
feat(suite-native): connect & authorize passphrased device (#12036)
Browse files Browse the repository at this point in the history
  • Loading branch information
juriczech authored Apr 24, 2024
1 parent d374579 commit 621a147
Show file tree
Hide file tree
Showing 6 changed files with 74 additions and 90 deletions.
8 changes: 3 additions & 5 deletions suite-common/message-system/src/messageSystemThunks.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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,
);
};

Expand Down
22 changes: 17 additions & 5 deletions suite-common/wallet-core/src/device/deviceReducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -67,6 +68,16 @@ const merge = (device: AcquiredDevice, upcoming: Partial<AcquiredDevice>): 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
Expand Down Expand Up @@ -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(),
Expand Down
1 change: 1 addition & 0 deletions suite-common/wallet-core/src/device/deviceThunks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
37 changes: 0 additions & 37 deletions suite-native/app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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();
Expand Down
73 changes: 53 additions & 20 deletions suite-native/discovery/src/discoveryThunks.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 }) => {
Expand Down Expand Up @@ -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),
});
Expand Down Expand Up @@ -207,20 +232,22 @@ const discoverAccountsByDescriptorThunk = createThunk(
deviceState: string;
identity?: string;
},
{ dispatch },
{ dispatch, getState },
) => {
let isFinalRound = false;

if (A.isEmpty(descriptorsBundle)) {
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),
});
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
23 changes: 0 additions & 23 deletions suite-native/discovery/src/utils.ts

This file was deleted.

0 comments on commit 621a147

Please sign in to comment.