Skip to content

Commit

Permalink
Ledger Sync - Display error when scanning invalid QR Code (#7800)
Browse files Browse the repository at this point in the history
fix(llm): ledger sync display error when scanning invalid qr code
  • Loading branch information
cgrellard-ledger committed Sep 13, 2024
1 parent a333038 commit 52ae4d3
Show file tree
Hide file tree
Showing 13 changed files with 188 additions and 3 deletions.
6 changes: 6 additions & 0 deletions .changeset/loud-donkeys-glow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"live-mobile": patch
"@ledgerhq/trustchain": patch
---

Ledger Sync - Display relevant error when scanning old accounts export qr code or an invalid one
14 changes: 14 additions & 0 deletions apps/ledger-live-mobile/src/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -977,6 +977,10 @@
},
"DeleteAppDataError": {
"title": "Error deleting app data"
},
"ScannedNewImportQrCode": {
"title": "Update required",
"description": "To sync your apps, please make sure both are updated to the latest version."
}
},
"crash": {
Expand Down Expand Up @@ -6875,6 +6879,16 @@
"tryAgain": "Try again"
}
},
"scannedInvalidQrCode": {
"title": "Invalid QR Code",
"desc": "It looks like the QR code you scanned isn't valid. Please try again with a Ledger Sync valid QR code.",
"tryAgain": "Try again"
},
"scannedOldQrCode": {
"title": "Update required",
"desc": "To sync your apps, please make sure both are updated to the latest version.",
"tryAgain": "Try again"
},
"unbacked": {
"title": "You need to create your encryption key first",
"description": "Please make sure you’ve created an encryption key on one of your Ledger Live apps before continuing your synchronization.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import { useSyncWithQrCode } from "LLM/features/WalletSync/hooks/useSyncWithQrCo
import { SpecificError } from "LLM/features/WalletSync/components/Error/SpecificError";
import { ErrorReason } from "LLM/features/WalletSync/hooks/useSpecificError";
import { useCurrentStep } from "LLM/features/WalletSync/hooks/useCurrentStep";
import ScannedInvalidQrCode from "~/newArch/features/WalletSync/screens/Synchronize/ScannedInvalidQrCode";
import ScannedOldImportQrCode from "~/newArch/features/WalletSync/screens/Synchronize/ScannedOldImportQrCode";

type Props = {
currency?: CryptoCurrency | TokenCurrency | null;
Expand Down Expand Up @@ -97,6 +99,12 @@ const StepFlow = ({
case Steps.SyncError:
return <SyncError tryAgain={navigateToQrCodeMethod} />;

case Steps.ScannedInvalidQrCode:
return <ScannedInvalidQrCode tryAgain={navigateToQrCodeMethod} />;

case Steps.ScannedOldImportQrCode:
return <ScannedOldImportQrCode tryAgain={navigateToQrCodeMethod} />;

case Steps.UnbackedError:
return <SpecificError primaryAction={onCreateKey} error={ErrorReason.NO_BACKUP} />;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import { AnalyticsPage } from "../../hooks/useLedgerSyncAnalytics";
import PinCodeDisplay from "../../screens/Synchronize/PinCodeDisplay";
import PinCodeInput from "../../screens/Synchronize/PinCodeInput";
import SyncError from "../../screens/Synchronize/SyncError";
import ScannedInvalidQrCode from "../../screens/Synchronize/ScannedInvalidQrCode";
import ScannedOldImportQrCode from "../../screens/Synchronize/ScannedOldImportQrCode";
import { useInitMemberCredentials } from "../../hooks/useInitMemberCredentials";
import { useSyncWithQrCode } from "../../hooks/useSyncWithQrCode";
import { SpecificError } from "../Error/SpecificError";
Expand Down Expand Up @@ -102,6 +104,12 @@ const ActivationFlow = ({
case Steps.SyncError:
return <SyncError tryAgain={navigateToQrCodeMethod} />;

case Steps.ScannedInvalidQrCode:
return <ScannedInvalidQrCode tryAgain={navigateToQrCodeMethod} />;

case Steps.ScannedOldImportQrCode:
return <ScannedOldImportQrCode tryAgain={navigateToQrCodeMethod} />;

case Steps.UnbackedError:
if (!hasCompletedOnboarding) {
return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ export enum AnalyticsPage {
SyncWithQrCode = "Sync with QR code",
PinCode = "Pin code",
PinCodesDoNotMatch = "Pin codes don't match",
ScannedInvalidQrCode = "Scanned invalid QR code",
ScannedIncompatibleApps = "Scans incompatible apps",
Loading = "Loading",
SettingsGeneral = "Settings General",
LedgerSyncSettings = "Ledger Sync Settings",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { useCallback, useState } from "react";
import { MemberCredentials, TrustchainMember } from "@ledgerhq/trustchain/types";
import { createQRCodeCandidateInstance } from "@ledgerhq/trustchain/qrcode/index";
import {
ScannedOldImportQrCode,
ScannedInvalidQrCode,
InvalidDigitsError,
NoTrustchainInitialized,
TrustchainAlreadyInitialized,
Expand Down Expand Up @@ -72,7 +74,11 @@ export const useSyncWithQrCode = () => {
onSyncFinished();
return true;
} catch (e) {
if (e instanceof InvalidDigitsError) {
if (e instanceof ScannedOldImportQrCode) {
setCurrentStep(Steps.ScannedOldImportQrCode);
} else if (e instanceof ScannedInvalidQrCode) {
setCurrentStep(Steps.ScannedInvalidQrCode);
} else if (e instanceof InvalidDigitsError) {
setCurrentStep(Steps.SyncError);
return;
} else if (e instanceof NoTrustchainInitialized) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { ErrorComponent } from "../../components/Error/Simple";
import { AnalyticsButton, AnalyticsPage } from "../../hooks/useLedgerSyncAnalytics";
import { track } from "~/analytics";

interface Props {
tryAgain: () => void;
}

export default function ScannedInvalidQrCode({ tryAgain }: Props) {
const { t } = useTranslation();

const onTryAgain = () => {
tryAgain();
track("button_clicked", {
button: AnalyticsButton.TryAgain,
page: AnalyticsPage.ScannedInvalidQrCode,
});
};

return (
<ErrorComponent
title={t("walletSync.synchronize.qrCode.scannedInvalidQrCode.title")}
desc={t("walletSync.synchronize.qrCode.scannedInvalidQrCode.desc")}
mainButton={{
label: t("walletSync.synchronize.qrCode.scannedInvalidQrCode.tryAgain"),
onPress: onTryAgain,
outline: false,
}}
analyticsPage={AnalyticsPage.ScannedInvalidQrCode}
/>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { ErrorComponent } from "../../components/Error/Simple";
import { AnalyticsButton, AnalyticsPage } from "../../hooks/useLedgerSyncAnalytics";
import { track } from "~/analytics";

interface Props {
tryAgain: () => void;
}

export default function ScannedOldImportQrCode({ tryAgain }: Props) {
const { t } = useTranslation();

const onTryAgain = () => {
tryAgain();
track("button_clicked", {
button: AnalyticsButton.TryAgain,
page: AnalyticsPage.ScannedIncompatibleApps,
});
};

return (
<ErrorComponent
title={t("walletSync.synchronize.qrCode.scannedOldQrCode.title")}
desc={t("walletSync.synchronize.qrCode.scannedOldQrCode.desc")}
mainButton={{
label: t("walletSync.synchronize.qrCode.scannedOldQrCode.tryAgain"),
onPress: onTryAgain,
outline: false,
}}
analyticsPage={AnalyticsPage.ScannedIncompatibleApps}
/>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ export enum Steps {
PinDisplay = "PinDisplay",
PinInput = "PinInput",
SyncError = "SyncError",
ScannedInvalidQrCode = "ScannedInvalidQrCode",
ScannedOldImportQrCode = "ScannedOldImportQrCode",
UnbackedError = "UnbackedError",
AlreadyBacked = "AlreadyBacked",
BackedWithDifferentSeeds = "BackedWithDifferentSeeds",
Expand Down
11 changes: 10 additions & 1 deletion apps/ledger-live-mobile/src/screens/ImportAccounts/Scan.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React, { PureComponent } from "react";
import { StyleSheet, View } from "react-native";
import { parseFramesReducer, framesToData, areFramesComplete, progressOfFrames } from "qrloop";
import { Result as ImportAccountsResult, decode } from "@ledgerhq/live-wallet/liveqr/cross";
import { TrackScreen } from "~/analytics";
import { screen, TrackScreen } from "~/analytics";
import { ScreenName } from "~/const";
import Scanner from "~/components/Scanner";
import GenericErrorBottomModal from "~/components/GenericErrorBottomModal";
Expand All @@ -11,6 +11,8 @@ import { withTheme } from "../../colors";
import type { Theme } from "../../colors";
import type { ImportAccountsNavigatorParamList } from "~/components/RootNavigator/types/ImportAccountsNavigator";
import type { StackNavigatorProps } from "~/components/RootNavigator/types/helpers";
import { ScannedNewImportQrCode } from "@ledgerhq/trustchain/errors";
import { AnalyticsPage } from "~/newArch/features/WalletSync/hooks/useLedgerSyncAnalytics";

type NavigationProps = StackNavigatorProps<
ImportAccountsNavigatorParamList,
Expand Down Expand Up @@ -75,6 +77,13 @@ class Scan extends PureComponent<
}
}
} catch (e) {
if (data.match(/host=([0-9A-Fa-f]+)/)) {
this.setState({
error: new ScannedNewImportQrCode(),
progress: 0,
});
screen("", AnalyticsPage.ScannedIncompatibleApps, { source: "Account Import Sync" });
}
console.warn(e);
}
}
Expand Down
3 changes: 3 additions & 0 deletions libs/trustchain/src/errors.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { createCustomErrorClass } from "@ledgerhq/errors";

export const ScannedOldImportQrCode = createCustomErrorClass("ScannedOldImportQrCode");
export const ScannedNewImportQrCode = createCustomErrorClass("ScannedNewImportQrCode");
export const ScannedInvalidQrCode = createCustomErrorClass("ScannedInvalidQrCode");
export const InvalidDigitsError = createCustomErrorClass("InvalidDigitsError");
export const InvalidEncryptionKeyError = createCustomErrorClass("InvalidEncryptionKeyError");
export const TrustchainEjected = createCustomErrorClass("TrustchainEjected");
Expand Down
52 changes: 52 additions & 0 deletions libs/trustchain/src/qrcode/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { createQRCodeHostInstance, createQRCodeCandidateInstance } from ".";
import WebSocket from "ws";
import { convertKeyPairToLiveCredentials } from "../sdk";
import { crypto } from "@ledgerhq/hw-trustchain";
import { ScannedInvalidQrCode, ScannedOldImportQrCode } from "../errors";

describe("Trustchain QR Code", () => {
let server;
Expand Down Expand Up @@ -83,4 +84,55 @@ describe("Trustchain QR Code", () => {
);
expect(res).toEqual(trustchain);
});
test("invalid qr code scanned", async () => {
const trustchain = {
rootId: "test-root-id",
walletSyncEncryptionKey: "test-wallet-sync-encryption-key",
applicationPath: "m/0'/16'/0'",
};
const addMember = jest.fn(() => Promise.resolve(trustchain));
const memberCredentials = convertKeyPairToLiveCredentials(await crypto.randomKeypair());
const memberName = "foo";

const onRequestQRCodeInput = jest.fn();

const scannedUrl = "https://example.com";

const candidateP = createQRCodeCandidateInstance({
memberCredentials,
memberName,
initialTrustchainId: undefined,
addMember,
scannedUrl,
onRequestQRCodeInput,
});

await expect(candidateP).rejects.toThrow(new ScannedInvalidQrCode());
});
test("old accounts export qr code scanned", async () => {
const trustchain = {
rootId: "test-root-id",
walletSyncEncryptionKey: "test-wallet-sync-encryption-key",
applicationPath: "m/0'/16'/0'",
};
const addMember = jest.fn(() => Promise.resolve(trustchain));
const memberCredentials = convertKeyPairToLiveCredentials(await crypto.randomKeypair());
const memberName = "foo";

const onRequestQRCodeInput = jest.fn();

const scannedUrl =
"ZAADAAIAAAAEd2JXMpuoYdzvkNzFTlmQLPcGf2LSjDOgqaB3nQoZqlimcCX6HNkescWKyT1DCGuwO7IesD7oYg+fdZPkiIfFL3V9swfZRePkaNN09IjXsWLsim9hK/qi/RC1/ofX3hYNKUxUAgYVVG82WKXIk47siWfUlRZsCYSAARQ6ASpUgidPjMHaOMK6w53wTZplwo7Zjv1HrIyKwr3Ci8OmrFye5g==";

const candidateP = createQRCodeCandidateInstance({
memberCredentials,
memberName,
initialTrustchainId: undefined,
addMember,
scannedUrl,
onRequestQRCodeInput,
});

await expect(candidateP).rejects.toThrow(new ScannedOldImportQrCode());
});
});
9 changes: 8 additions & 1 deletion libs/trustchain/src/qrcode/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
InvalidDigitsError,
NoTrustchainInitialized,
QRCodeWSClosed,
ScannedInvalidQrCode,
ScannedOldImportQrCode,
TrustchainAlreadyInitialized,
} from "../errors";
import { log } from "@ledgerhq/logs";
Expand Down Expand Up @@ -267,7 +269,8 @@ export async function createQRCodeCandidateInstance({
}): Promise<Trustchain | void> {
const m = scannedUrl.match(/host=([0-9A-Fa-f]+)/);
if (!m) {
throw new Error("invalid scannedUrl");
if (isFromOldAccountsImport(scannedUrl)) throw new ScannedOldImportQrCode();
throw new ScannedInvalidQrCode();
}
const hostPublicKey = crypto.from_hex(m[1]);
const ephemeralKey = await crypto.randomKeypair();
Expand Down Expand Up @@ -386,3 +389,7 @@ function fromErrorMessage(payload: { message: string; type: string }): Error {
error.name = "TrustchainQRCode-" + payload.type;
return error;
}

function isFromOldAccountsImport(scannedUrl: string): boolean {
return !!scannedUrl.match(/^[A-Za-z0-9+/=]*$/);
}

0 comments on commit 52ae4d3

Please sign in to comment.