Skip to content

Commit

Permalink
feat(suite-native): add DeviceCompromisedModal for FW revision check
Browse files Browse the repository at this point in the history
  • Loading branch information
Lemonexe committed Feb 6, 2025
1 parent c582d26 commit 0a1177d
Show file tree
Hide file tree
Showing 13 changed files with 201 additions and 3 deletions.
3 changes: 2 additions & 1 deletion suite-native/app/e2e/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,8 @@ export const prepareTrezorEmulator = async (
// Prepare Trezor device for test scenario
await TrezorUserEnvLink.disconnect();
await TrezorUserEnvLink.connect();
await TrezorUserEnvLink.startEmu({ model: 'T3T1', wipe: true });
// start with latest officially released firmware (necessary to pass the firmware checks)
await TrezorUserEnvLink.startEmu({ model: 'T3T1', version: '2-latest', wipe: true });
await TrezorUserEnvLink.setupEmu({
label: TREZOR_DEVICE_LABEL,
mnemonic: seed,
Expand Down
1 change: 1 addition & 0 deletions suite-native/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
"@suite-native/module-accounts-import": "workspace:*",
"@suite-native/module-accounts-management": "workspace:*",
"@suite-native/module-add-accounts": "workspace:*",
"@suite-native/module-authenticity-checks": "workspace:*",
"@suite-native/module-authorize-device": "workspace:*",
"@suite-native/module-connect-popup": "workspace:*",
"@suite-native/module-dev-utils": "workspace:*",
Expand Down
5 changes: 5 additions & 0 deletions suite-native/app/src/navigation/RootStackNavigator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
AccountSettingsScreen,
} from '@suite-native/module-accounts-management';
import { AddCoinAccountStackNavigator } from '@suite-native/module-add-accounts';
import { DeviceCompromisedModalScreen } from '@suite-native/module-authenticity-checks';
import { AuthorizeDeviceStackNavigator } from '@suite-native/module-authorize-device';
import { ConnectPopupScreen } from '@suite-native/module-connect-popup';
import { DevUtilsStackNavigator } from '@suite-native/module-dev-utils';
Expand Down Expand Up @@ -80,6 +81,10 @@ export const RootStackNavigator = () => {
name={RootStackRoutes.SettingsScreenStack}
component={SettingsStackNavigator}
/>
<RootStack.Screen
name={RootStackRoutes.DeviceCompromisedModalScreen}
component={DeviceCompromisedModalScreen}
/>
{/* Navigation flows that start by push from bottom animation on the first screen of its stack. */}
<RootStack.Group screenOptions={{ animation: 'slide_from_bottom' }}>
<RootStack.Screen
Expand Down
3 changes: 3 additions & 0 deletions suite-native/app/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@
"path": "../module-accounts-management"
},
{ "path": "../module-add-accounts" },
{
"path": "../module-authenticity-checks"
},
{ "path": "../module-authorize-device" },
{ "path": "../module-connect-popup" },
{ "path": "../module-dev-utils" },
Expand Down
33 changes: 31 additions & 2 deletions suite-native/device/src/hooks/useHandleDeviceConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
selectIsDeviceInitialized,
selectIsDeviceRemembered,
selectIsDeviceUsingPassphrase,
selectIsFirmwareAuthenticityCheckDismissed,
selectIsNoPhysicalDeviceConnected,
selectIsPortfolioTrackerDevice,
} from '@suite-common/wallet-core';
Expand All @@ -29,7 +30,10 @@ import {
} from '@suite-native/navigation';
import { selectIsOnboardingFinished } from '@suite-native/settings';

import { selectIsDeviceSetupSupported } from '../selectors';
import {
selectHasFirmwareAuthenticityCheckHardFailed,
selectIsDeviceSetupSupported,
} from '../selectors';

type NavigationProp = StackToStackCompositeNavigationProps<
AuthorizeDeviceStackParamList | RootStackParamList,
Expand All @@ -51,13 +55,25 @@ export const useHandleDeviceConnection = () => {
const isDeviceSetupSupported = useSelector(selectIsDeviceSetupSupported);

const { isBiometricsOverlayVisible } = useIsBiometricsOverlayVisible();

const hasFirmwareAuthenticityCheckHardFailed = useSelector(
selectHasFirmwareAuthenticityCheckHardFailed,
);
const isFirmwareAuthenticityCheckDismissed = useSelector(
selectIsFirmwareAuthenticityCheckDismissed,
);
const shouldNavigateToDeviceCompromisedModal =
hasFirmwareAuthenticityCheckHardFailed && !isFirmwareAuthenticityCheckDismissed;

const navigation = useNavigation<NavigationProp>();
const dispatch = useDispatch();

const lastRoute = navigation.getState()?.routes.at(-1)?.name;
const isDeviceSettingsStackFocused = lastRoute === RootStackRoutes.DeviceSettingsStack;
const isSendStackFocused = lastRoute === RootStackRoutes.SendStack;
const shouldBlockSendReviewRedirect = isDeviceRemembered && isSendStackFocused;
const isDeviceCompromisedModalFocused =
lastRoute === RootStackRoutes.DeviceCompromisedModalScreen;

// When is an uninitialized device model that supports device setup, navigate to device onboarding.
useEffect(() => {
Expand Down Expand Up @@ -103,7 +119,8 @@ export const useHandleDeviceConnection = () => {
isOnboardingFinished &&
!isPortfolioTrackerDevice &&
!isDeviceConnectedAndAuthorized &&
!isBiometricsOverlayVisible
!isBiometricsOverlayVisible &&
!shouldNavigateToDeviceCompromisedModal
) {
requestPrioritizedDeviceAccess({
deviceCallback: () => dispatch(authorizeDeviceThunk()),
Expand All @@ -117,6 +134,9 @@ export const useHandleDeviceConnection = () => {
});
}
}
if (shouldNavigateToDeviceCompromisedModal) {
navigation.navigate(RootStackRoutes.DeviceCompromisedModalScreen);
}
}, [
dispatch,
isDeviceConnected,
Expand All @@ -130,6 +150,7 @@ export const useHandleDeviceConnection = () => {
shouldBlockSendReviewRedirect,
isFirmwareInstallationRunning,
isDeviceInitialized,
shouldNavigateToDeviceCompromisedModal,
]);

// In case that the physical device is disconnected, redirect to the home screen and
Expand All @@ -141,6 +162,13 @@ export const useHandleDeviceConnection = () => {
if (shouldBlockSendReviewRedirect) {
return;
}
// DeviceCompromisedModal is persistent, so postpone navigating to away until it's dismissed
// TODO: this hook is getting very complex, and it's hard to understand the logic when it navigates there and back again.
// Ideally there'd be a single source of truth, a function returning "where we should be as per current state"
// rather than multiple useEffects with imperative instructions "go there when X changes"
if (isDeviceCompromisedModalFocused) {
return;
}
navigation.navigate(RootStackRoutes.AppTabs, {
screen: AppTabsRoutes.HomeStack,
params: {
Expand All @@ -154,6 +182,7 @@ export const useHandleDeviceConnection = () => {
navigation,
shouldBlockSendReviewRedirect,
isFirmwareInstallationRunning,
isDeviceCompromisedModalFocused,
]);

// When trezor gets locked, it is necessary to display a PIN matrix for T1 so that it can be unlocked
Expand Down
13 changes: 13 additions & 0 deletions suite-native/intl/src/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1225,6 +1225,19 @@ export const en = {
},
},
},
moduleAuthenticityChecks: {
deviceCompromised: {
title: 'Your device may have been compromised',
subtitle:
'Contact our support to learn what’s going on with your device and what to do next.',
steps: {
disconnectDevice: 'Disconnect your device from your phone.',
avoidUsingDevice: 'Avoid using this device or sending any funds to it.',
contactSupport: 'Continue to Trezor support and use the Chat option.',
},
buttonContactSupport: 'Contact Trezor Support',
},
},
staking: {
stakingDetailScreen: {
title: 'Staking',
Expand Down
22 changes: 22 additions & 0 deletions suite-native/module-authenticity-checks/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"name": "@suite-native/module-authenticity-checks",
"version": "1.0.0",
"private": true,
"license": "See LICENSE.md in repo root",
"sideEffects": false,
"main": "src/index",
"scripts": {
"depcheck": "yarn g:depcheck",
"lint:js": "yarn g:eslint '**/*.{ts,tsx,js}'",
"type-check": "yarn g:tsc --build"
},
"dependencies": {
"@react-navigation/native": "6.1.18",
"@suite-common/wallet-core": "workspace:*",
"@suite-native/atoms": "workspace:*",
"@suite-native/intl": "workspace:*",
"@suite-native/link": "workspace:*",
"@suite-native/navigation": "workspace:*",
"react-redux": "8.0.7"
}
}
1 change: 1 addition & 0 deletions suite-native/module-authenticity-checks/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './screens/DeviceCompromisedModalScreen';
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { useDispatch, useSelector } from 'react-redux';

import { useNavigation } from '@react-navigation/native';

import { deviceActions, selectSelectedDevice } from '@suite-common/wallet-core';
import { Button, IconListTextItem, TitleHeader, VStack } from '@suite-native/atoms';
import { Translation } from '@suite-native/intl';
import { useOpenLink } from '@suite-native/link';
import {
AppTabsRoutes,
HomeStackRoutes,
RootStackParamList,
RootStackRoutes,
Screen,
ScreenHeader,
StackToStackCompositeNavigationProps,
} from '@suite-native/navigation';

// TODO this page is for desktop; await creation of new page tailored to the suite-native UX
const TREZOR_SUPPORT_FW_REVISION_CHECK_FAILED_URL =
'https://trezor.io/support/a/trezor-fw-revision-check-failed';
const chatUrl = `${TREZOR_SUPPORT_FW_REVISION_CHECK_FAILED_URL}#open-chat`;

const InformativeList = () => (
<VStack spacing="sp24">
<IconListTextItem icon="plugs" variant="red">
<Translation id="moduleAuthenticityChecks.deviceCompromised.steps.disconnectDevice" />
</IconListTextItem>

<IconListTextItem icon="handPalm" variant="red">
<Translation id="moduleAuthenticityChecks.deviceCompromised.steps.avoidUsingDevice" />
</IconListTextItem>

<IconListTextItem icon="chatCircle" variant="red">
<Translation id="moduleAuthenticityChecks.deviceCompromised.steps.contactSupport" />
</IconListTextItem>
</VStack>
);

type NavigationProp = StackToStackCompositeNavigationProps<
RootStackParamList,
RootStackRoutes.AppTabs,
RootStackParamList
>;

export const DeviceCompromisedModalScreen = () => {
const device = useSelector(selectSelectedDevice);
const dispatch = useDispatch();
const openLink = useOpenLink();
const navigation = useNavigation<NavigationProp>();

const dismissCheck = () => {
if (device?.id) {
dispatch(deviceActions.dismissFirmwareAuthenticityCheck(device.id));
}
};

// After dismissCheck, an effect could fire in useHandleDeviceConnection to navigate away, but it's not guaranteed!
// To be sure we don't lock user on on this screen, we navigate home.
const handleClose = () => {
navigation.navigate(RootStackRoutes.AppTabs, {
screen: AppTabsRoutes.HomeStack,
params: { screen: HomeStackRoutes.Home },
});
dismissCheck();
};

const handleContactSupportClick = () => openLink(chatUrl);

return (
<Screen header={<ScreenHeader closeActionType="close" closeAction={handleClose} />}>
<VStack spacing="sp32" flex={1}>
<TitleHeader
titleVariant="titleMedium"
titleSpacing="sp12"
title={<Translation id="moduleAuthenticityChecks.deviceCompromised.title" />}
subtitle={
<Translation id="moduleAuthenticityChecks.deviceCompromised.subtitle" />
}
/>
<InformativeList />
</VStack>
<VStack spacing="sp12">
<Button colorScheme="redBold" onPress={handleContactSupportClick}>
<Translation id="moduleAuthenticityChecks.deviceCompromised.buttonContactSupport" />
</Button>
<Button colorScheme="redElevation0" onPress={handleClose}>
<Translation id="generic.buttons.close" />
</Button>
</VStack>
</Screen>
);
};
13 changes: 13 additions & 0 deletions suite-native/module-authenticity-checks/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": { "outDir": "libDev" },
"references": [
{
"path": "../../suite-common/wallet-core"
},
{ "path": "../atoms" },
{ "path": "../intl" },
{ "path": "../link" },
{ "path": "../navigation" }
]
}
1 change: 1 addition & 0 deletions suite-native/navigation/src/navigators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ export type RootStackParamList = {
parsedUrl: ParsedURL;
};
[RootStackRoutes.SettingsScreenStack]: NavigatorScreenParams<SettingsStackParamList>;
[RootStackRoutes.DeviceCompromisedModalScreen]: undefined;
};

export type TradingStackParamList = {
Expand Down
1 change: 1 addition & 0 deletions suite-native/navigation/src/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export enum RootStackRoutes {
CoinEnablingInit = 'CoinEnablingInit',
ConnectPopup = 'ConnectPopup',
SettingsScreenStack = 'SettingsScreenStack',
DeviceCompromisedModalScreen = 'DeviceCompromisedModalScreen',
}

export enum AppTabsRoutes {
Expand Down
15 changes: 15 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -10000,6 +10000,7 @@ __metadata:
"@suite-native/module-accounts-import": "workspace:*"
"@suite-native/module-accounts-management": "workspace:*"
"@suite-native/module-add-accounts": "workspace:*"
"@suite-native/module-authenticity-checks": "workspace:*"
"@suite-native/module-authorize-device": "workspace:*"
"@suite-native/module-connect-popup": "workspace:*"
"@suite-native/module-dev-utils": "workspace:*"
Expand Down Expand Up @@ -10648,6 +10649,20 @@ __metadata:
languageName: unknown
linkType: soft

"@suite-native/module-authenticity-checks@workspace:*, @suite-native/module-authenticity-checks@workspace:suite-native/module-authenticity-checks":
version: 0.0.0-use.local
resolution: "@suite-native/module-authenticity-checks@workspace:suite-native/module-authenticity-checks"
dependencies:
"@react-navigation/native": "npm:6.1.18"
"@suite-common/wallet-core": "workspace:*"
"@suite-native/atoms": "workspace:*"
"@suite-native/intl": "workspace:*"
"@suite-native/link": "workspace:*"
"@suite-native/navigation": "workspace:*"
react-redux: "npm:8.0.7"
languageName: unknown
linkType: soft

"@suite-native/module-authorize-device@workspace:*, @suite-native/module-authorize-device@workspace:suite-native/module-authorize-device":
version: 0.0.0-use.local
resolution: "@suite-native/module-authorize-device@workspace:suite-native/module-authorize-device"
Expand Down

0 comments on commit 0a1177d

Please sign in to comment.