Skip to content

Commit

Permalink
Add password for backups
Browse files Browse the repository at this point in the history
  • Loading branch information
serjonya-trili committed Sep 17, 2024
1 parent de03d6d commit e8dd2cc
Show file tree
Hide file tree
Showing 21 changed files with 442 additions and 84 deletions.
1 change: 1 addition & 0 deletions apps/desktop-e2e/src/features/onboarding.feature
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ Feature: User Onboarding
| |
| TestAccount |

@focus
Scenario: User imports a backup file
Given I am on the welcome page

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { mnemonic1 } from "@umami/test-utils";
import { DEFAULT_DERIVATION_PATH_TEMPLATE } from "@umami/tezos";

import { type AccountGroup, AccountGroupBuilder } from "../../helpers/AccountGroup";
import { type AccountGroup, AccountGroupBuilder } from "./AccountGroup";

export const v1BackedupAccountGroups = async () => {
const expectedGroups: AccountGroup[] = [];
Expand Down
12 changes: 7 additions & 5 deletions apps/desktop-e2e/src/steps/onboarding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,8 @@ import { mnemonic1 as existingSeedphrase } from "@umami/test-utils";
import { DEFAULT_DERIVATION_PATH_TEMPLATE } from "@umami/tezos";

import { type CustomWorld } from "./world";
import {
v1BackedupAccountGroups,
v2BackedupAccountGroups,
} from "../fixtures/backups/backedupAccountGroups";
import { type AccountGroup, AccountGroupBuilder } from "../helpers/AccountGroup";
import { v1BackedupAccountGroups, v2BackedupAccountGroups } from "../helpers/backedupAccountGroups";
import { AccountsPage } from "../pages/AccountsPage";

export const BASE_URL = "http://127.0.0.1:3000";
Expand Down Expand Up @@ -83,7 +80,12 @@ When("I upload {string} backup file", async function (this: CustomWorld, backupF
const fileChooserPromise = this.page.waitForEvent("filechooser");
await this.page.getByTestId("file-input").click();
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles(path.join(__dirname, `../fixtures/backups/${backupFileName}`));
await fileChooser.setFiles(
path.resolve(
__dirname,
`../../../../packages/test-utils/src/fixtures/backups/${backupFileName}`
)
);
});

Then(/I am on an? (\w+) page/, async function (this: CustomWorld, pageName) {
Expand Down
6 changes: 4 additions & 2 deletions apps/desktop/src/components/ErrorPage.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { Box, Button, Center, Heading, Link, VStack } from "@chakra-ui/react";
import { downloadBackupFile } from "@umami/state";

import { useOffboardingModal } from "./Offboarding/useOffboardingModal";
import { ModalContentWrapper } from "./Onboarding/ModalContentWrapper";
import { NoticeIcon, ReloadIcon } from "../assets/icons";
import BackgroundImage from "../assets/onboarding/background_image.png";
import colors from "../style/colors";
import { useSaveBackup } from "../utils/useSaveBackup";

const feedbackEmailBodyTemplate =
"What is it about? (if a bug report please consider including your account address) %0A PLEASE FILL %0A%0A What is the feedback? %0A PLEASE FILL";
Expand All @@ -16,9 +16,11 @@ const refresh = () => {

export const ErrorPage = () => {
const { modalElement: OffboardingModal, onOpen: onOpenOffboardingModal } = useOffboardingModal();
const { content: saveBackupModal, onOpen: saveBackup } = useSaveBackup();

return (
<Center height="100vh" padding="60px" backgroundImage={BackgroundImage} backgroundSize="cover">
{saveBackupModal}
<Box
width="480px"
padding="40px"
Expand All @@ -34,7 +36,7 @@ export const ErrorPage = () => {
title="Oops! Something went wrong!"
>
<VStack width="100%" spacing="16px">
<Button width="100%" borderRadius="4px" onClick={downloadBackupFile} size="lg">
<Button width="100%" borderRadius="4px" onClick={saveBackup} size="lg">
Save Backup
</Button>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@ import { EnterPassword } from "./password/EnterPassword";
export const MasterPassword = ({
account,
onClose,
onVerify,
}: {
account: MasterPasswordStep["account"];
onClose: () => void;
account?: MasterPasswordStep["account"];
onClose?: () => void;
onVerify?: (password: string) => void;
}) => {
const restoreFromMnemonic = useRestoreFromMnemonic();
const restoreFromSecretKey = useRestoreFromSecretKey();
Expand All @@ -25,10 +27,19 @@ export const MasterPassword = ({

const { isLoading, handleAsyncAction } = useAsyncActionHandler();
const toast = useToast();

const handleSubmit = (password: string) =>
handleAsyncAction(async () => {
await checkPassword?.(password);

if (onVerify) {
return onVerify(password);
}

if (!account) {
throw new Error("No account data provided.");
}

switch (account.type) {
case "secret_key":
await restoreFromSecretKey(account.secretKey, password, account.label);
Expand All @@ -41,7 +52,7 @@ export const MasterPassword = ({
});
}
toast({ description: "Account successfully created!", status: "success" });
onClose();
onClose?.();
});

if (passwordHasBeenSet) {
Expand Down
38 changes: 38 additions & 0 deletions apps/desktop/src/utils/useSaveBackup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import {
Modal,
ModalCloseButton,
ModalContent,
ModalHeader,
useDisclosure,
} from "@chakra-ui/react";
import { useDownloadBackupFile } from "@umami/state";

import { MasterPassword } from "../components/Onboarding/masterPassword/MasterPassword";

export const useSaveBackup = () => {
const { isOpen, onClose, onOpen } = useDisclosure();
const downloadBackupFile = useDownloadBackupFile();

return {
content: (
<Modal
autoFocus={false}
closeOnOverlayClick={false}
isCentered
isOpen={isOpen}
onClose={onClose}
>
<ModalContent>
<ModalHeader>
<ModalCloseButton />
</ModalHeader>
<MasterPassword
onClose={onClose}
onVerify={password => downloadBackupFile(password).then(onClose)}
/>
</ModalContent>
</Modal>
),
onOpen,
};
};
45 changes: 18 additions & 27 deletions apps/desktop/src/views/settings/SettingsView.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Box, Button, Flex, Heading } from "@chakra-ui/react";
import { useDynamicModalContext } from "@umami/components";
import { downloadBackupFile } from "@umami/state";
import { type PropsWithChildren } from "react";

import { DAppsDrawerCard } from "./DAppsDrawerCard";
Expand All @@ -11,6 +10,7 @@ import { ChangePasswordForm } from "../../components/ChangePassword/ChangePasswo
import { ClickableCard, SettingsCardWithDrawerIcon } from "../../components/ClickableCard";
import { useOffboardingModal } from "../../components/Offboarding/useOffboardingModal";
import { TopBar } from "../../components/TopBar";
import { useSaveBackup } from "../../utils/useSaveBackup";

export const SettingsView = () => (
<Flex flexDirection="column" height="100%">
Expand All @@ -27,33 +27,28 @@ export const SettingsView = () => (

const GeneralSection = () => (
<SectionContainer title="General">
{/*
TODO: implement this
<SettingsCard left="Theme">
<Flex alignItems="center" justifyContent="space-between">
<Text size="sm">Light</Text>
<Switch marginX={3} isChecked isDisabled />
<Heading size="sm">Dark</Heading>
</Flex>
</SettingsCard>
*/}
<NetworkSettingsDrawerCard />
<ErrorLogsDrawerCard />
</SectionContainer>
);

const BackupSection = () => (
<SectionContainer title="Backup">
<ClickableCard isSelected={false} onClick={downloadBackupFile}>
<Flex alignItems="center" justifyContent="space-between">
<Heading size="sm">Download backup file</Heading>
<Button onClick={downloadBackupFile} variant="unstyled">
<DownloadIcon cursor="pointer" />
</Button>
</Flex>
</ClickableCard>
</SectionContainer>
);
const BackupSection = () => {
const { onOpen, content } = useSaveBackup();

return (
<SectionContainer title="Backup">
{content}
<ClickableCard isSelected={false} onClick={onOpen}>
<Flex alignItems="center" justifyContent="space-between">
<Heading size="sm">Download backup file</Heading>
<Button onClick={onOpen} variant="unstyled">
<DownloadIcon cursor="pointer" />
</Button>
</Flex>
</ClickableCard>
</SectionContainer>
);
};

const AdvancedSection = () => {
const { modalElement: OffboardingModal, onOpen: onOpenOffboardingModal } = useOffboardingModal();
Expand All @@ -62,10 +57,6 @@ const AdvancedSection = () => {
return (
<SectionContainer title="Advanced Settings">
<DAppsDrawerCard />
{/*
TODO: implement this
<SettingsCardWithDrawerIcon left="Reset Settings" onClick={() => {}} />
*/}
<SettingsCardWithDrawerIcon
left="Off-board Wallet"
isSelected={false}
Expand Down
4 changes: 4 additions & 0 deletions apps/web/src/components/Menu/Menu.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,10 @@ describe("<Menu />", () => {

await user.click(screen.getByText("Save Backup"));

await user.type(screen.getByLabelText("Set Password"), "password");
await user.type(screen.getByLabelText("Confirm Password"), "password");
await user.click(screen.getByRole("button", { name: "Save Backup" }));

expect(downloadBackupFile).toHaveBeenCalled();
});

Expand Down
5 changes: 3 additions & 2 deletions apps/web/src/components/Menu/Menu.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { Switch } from "@chakra-ui/react";
import { useColorMode } from "@chakra-ui/system";
import { useDynamicDrawerContext, useDynamicModalContext } from "@umami/components";
import { downloadBackupFile } from "@umami/state";

import { AddressBookMenu } from "./AddressBookMenu/AddressBookMenu";
import { AdvancedMenu } from "./AdvancedMenu/AdvancedMenu";
import { AppsMenu } from "./AppsMenu/AppsMenu";
import { GenericMenu } from "./GenericMenu";
import { LogoutModal } from "./LogoutModal";
import { useSaveBackup } from "./useSaveBackup";
import {
BookIcon,
CodeSandboxIcon,
Expand All @@ -25,6 +25,7 @@ export const Menu = () => {
const { openWith: openDrawer } = useDynamicDrawerContext();
const { colorMode, toggleColorMode } = useColorMode();
const isVerified = useIsAccountVerified();
const saveBackup = useSaveBackup();

const colorModeSwitchLabel = colorMode === "light" ? "Light mode" : "Dark mode";

Expand All @@ -43,7 +44,7 @@ export const Menu = () => {
{
label: "Save Backup",
icon: <DownloadIcon />,
onClick: downloadBackupFile,
onClick: saveBackup,
},
{
label: "Apps",
Expand Down
9 changes: 9 additions & 0 deletions apps/web/src/components/Menu/useSaveBackup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { useDynamicModalContext } from "@umami/components";

import { SetupPassword } from "../Onboarding/SetupPassword";

export const useSaveBackup = () => {
const { openWith } = useDynamicModalContext();

return () => openWith(<SetupPassword mode="save_backup" />);
};
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
useAppDispatch,
useAsyncActionHandler,
useCurrentAccount,
useDownloadBackupFile,
useGetDecryptedMnemonic,
useGetNextAvailableAccountLabels,
useIsPasswordSet,
Expand All @@ -44,7 +45,7 @@ type FormFields = {
curve: Exclude<Curves, "bip25519">;
};

type Mode = "mnemonic" | "secret_key" | "new_mnemonic" | "verification";
type Mode = "mnemonic" | "secret_key" | "new_mnemonic" | "verification" | "save_backup";

type SetupPasswordProps = {
mode: Mode;
Expand All @@ -66,6 +67,12 @@ const getModeConfig = (mode: Mode) => {
buttonLabel: "Create Account",
subtitle: "Set a password to unlock your wallet. Make sure to store your password safely.",
};
case "save_backup":
return {
icon: LockIcon,
title: "Encrypt Backup",
buttonLabel: "Save Backup",
};
case "mnemonic":
case "secret_key":
return {
Expand All @@ -89,6 +96,7 @@ export const SetupPassword = ({ mode }: SetupPasswordProps) => {
const isPasswordSet = useIsPasswordSet();
const getDecryptedMnemonic = useGetDecryptedMnemonic();
const currentAccount = useCurrentAccount();
const downloadBackupFile = useDownloadBackupFile();

const form = useMultiForm<FormFields>({
mode: "onBlur",
Expand Down Expand Up @@ -146,6 +154,9 @@ export const SetupPassword = ({ mode }: SetupPasswordProps) => {

return openWith(<ImportantNoticeModal mnemonic={mnemonic} />, { size: "xl" });
}
case "save_backup": {
await downloadBackupFile(password);
}
}

return onClose();
Expand Down
2 changes: 1 addition & 1 deletion packages/crypto/src/AES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export const encrypt = async (data: string, password: string): Promise<Encrypted
const encrypted = await crypto.subtle.encrypt(
{
name: AES_MODE,
iv: iv,
iv,
},
derivedKey,
Buffer.from(data, "utf-8")
Expand Down
3 changes: 2 additions & 1 deletion packages/state/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"eslint": "^8.57.0",
"jest": "^29.7.0",
"madge": "^8.0.0",
"mockstate": "^0.0.7",
"prettier": "^3.3.2",
"rimraf": "^6.0.1",
"tsup": "^8.2.4",
Expand Down Expand Up @@ -71,6 +72,7 @@
"@tanstack/react-query": "^5.56.2",
"@taquito/signer": "^20.0.1",
"@taquito/utils": "^20.0.1",
"@types/jest": "^29.5.13",
"@umami/core": "workspace:^",
"@umami/crypto": "workspace:^",
"@umami/multisig": "workspace:^",
Expand All @@ -82,7 +84,6 @@
"framer-motion": "^11.5.4",
"immer": "^10.1.1",
"lodash": "^4.17.21",
"@types/jest": "^29.5.13",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-redux": "^9.1.2",
Expand Down
Loading

0 comments on commit e8dd2cc

Please sign in to comment.