Skip to content

Commit

Permalink
Single password for the extension (anoma#436)
Browse files Browse the repository at this point in the history
* feat(extension): decoupling vault from keyring in order to have a master password for the extension

* feat(extension, app): adding a few tests for vault

* feat(extension): adding lock extension message to ExtensionBroadcaster

* feat(extension,setup): optional password field, if master password is already set-up

* feat(extension): fixing some bugs on app locking and some refactory

* feat(extension): adding lock functionality + big refactory on App

* feat(components): tweaking spacements and sizing

* feat(components): adding basic button hover

* feat(extension,tests): improving vault tests

* feat(extension, app): change password screen with a temporary location

* feat(extension, app): unifying ledger accounts in the keyring + necessary changes

* feat(tests): updating unit tests to reflect Vault changes
  • Loading branch information
pedrorezende authored Nov 10, 2023
1 parent 2794b44 commit c890c2d
Show file tree
Hide file tree
Showing 96 changed files with 2,332 additions and 2,118 deletions.
7 changes: 4 additions & 3 deletions apps/extension/src/App/Accounts/AddAccount.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ import {
} from "./AddAccount.components";
import { TopLevelRoute } from "App/types";
import { useAuth } from "hooks";
import { AddLedgerAccountMsg, Ledger } from "background/ledger";
import { Ledger } from "background/ledger";
import { AddLedgerAccountMsg } from "background/keyring";
import { isKeyChainLocked, redirectToLogin } from "hooks/useAuth";

type Props = {
Expand Down Expand Up @@ -219,7 +220,7 @@ const AddAccount: React.FC<Props> = ({
}
};

const addLedgerAccount = async (): Promise<DerivedAccount | void> => {
const addLedgerAccount = async (): Promise<DerivedAccount | false | void> => {
setFormStatus(Status.Pending);

const bip44Path = {
Expand Down Expand Up @@ -257,7 +258,7 @@ const AddAccount: React.FC<Props> = ({
// TODO: provide a password for ledger
return await requester.sendMessage(
Ports.Background,
new AddLedgerAccountMsg(alias, address, parentId, publicKey, bip44Path)
new AddLedgerAccountMsg(alias, address, publicKey, bip44Path, parentId)
);
} catch (e) {
setFormError(`${e}`);
Expand Down
73 changes: 31 additions & 42 deletions apps/extension/src/App/Accounts/DeleteAccount/DeleteAccount.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useCallback, useEffect, useState } from "react";
import {
ActionButton,
Alert,
GapPatterns,
Heading,
Input,
InputVariants,
Expand All @@ -11,11 +12,9 @@ import {
Text,
} from "@namada/components";
import { AccountType, DerivedAccount } from "@namada/types";
import { assertNever } from "@namada/utils";
import { TopLevelRoute } from "App/types";
import { DeleteAccountMsg } from "background/keyring";
import { DeleteAccountError } from "background/keyring/types";
import { DeleteLedgerAccountMsg } from "background/ledger";
import { formatRouterPath } from "@namada/utils";
import { AccountManagementRoute, TopLevelRoute } from "App/types";
import { CheckPasswordMsg } from "background/vault";
import { useRequester } from "hooks/useRequester";
import { useLocation, useNavigate, useParams } from "react-router-dom";
import { Ports } from "router";
Expand All @@ -28,14 +27,14 @@ enum Status {
}

export type Props = {
onComplete: () => void;
onDelete: (accountId: string) => void;
};

export type DeleteAccountLocationState = {
account?: DerivedAccount;
};

export const DeleteAccount: React.FC<Props> = ({ onComplete }) => {
export const DeleteAccount: React.FC<Props> = ({ onDelete }) => {
// TODO: When state is not passed, query by accountId
const { state }: { state: DeleteAccountLocationState } = useLocation();
const { accountId = "" } = useParams();
Expand All @@ -56,56 +55,46 @@ export const DeleteAccount: React.FC<Props> = ({ onComplete }) => {
const handleSubmit = useCallback(
async (e: React.FormEvent): Promise<void> => {
e.preventDefault();
setStatus(Status.Pending);
setLoadingState("Deleting Key...");
try {
setStatus(Status.Pending);
setLoadingState("Deleting Key...");

const result =
accountType === AccountType.Ledger
? await requester.sendMessage<DeleteLedgerAccountMsg>(
Ports.Background,
new DeleteLedgerAccountMsg(accountId)
)
: await requester.sendMessage<DeleteAccountMsg>(
Ports.Background,
new DeleteAccountMsg(accountId, password)
);
const verifyPassword = await requester.sendMessage<CheckPasswordMsg>(
Ports.Background,
new CheckPasswordMsg(password)
);

setLoadingState("");
if (result.ok) {
setStatus(Status.Complete);
onComplete();
} else {
setStatus(Status.Failed);
switch (result.error) {
case DeleteAccountError.BadPassword:
setErrorMessage("Password is incorrect");
break;
case DeleteAccountError.KeyStoreError:
setErrorMessage("Unknown error");
break;
default:
assertNever(result.error);
if (!verifyPassword) {
setErrorMessage("Password is incorrect");
setStatus(Status.Failed);
setLoadingState("");
return;
}

await onDelete(accountId);
} catch (error) {
setLoadingState("");
setErrorMessage(`${error}`);
setStatus(Status.Failed);
}
},
[accountId, password]
);

useEffect(() => {
if (status === Status.Complete) {
navigate(TopLevelRoute.Accounts);
}
}, [status]);

useEffect(() => {
if (!accountId || !state.account) {
navigate(TopLevelRoute.Accounts);
navigate(
formatRouterPath([
TopLevelRoute.Accounts,
AccountManagementRoute.ViewAccounts,
])
);
}
}, [accountId, state]);

return (
<>
<Stack as="form" onSubmit={handleSubmit} gap={9}>
<Stack as="form" onSubmit={handleSubmit} gap={GapPatterns.TitleContent}>
<Stack as="header" gap={4}>
<Heading>Delete Keys</Heading>
<Alert type="warning" title="Alert!">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ export const SettingsContainer = styled.section`
`;

export const SettingsHeader = styled.nav`
margin-top: ${spacement(4)};
display: grid;
grid-template-columns: auto ${spacement(30)};
align-items: end;
Expand Down
125 changes: 35 additions & 90 deletions apps/extension/src/App/Accounts/ParentAccounts.tsx
Original file line number Diff line number Diff line change
@@ -1,83 +1,42 @@
import React, { useEffect, useRef, useState } from "react";
import React from "react";
import { useNavigate } from "react-router-dom";
import browser from "webextension-polyfill";

import {
ActionButton,
Alert,
GapPatterns,
Heading,
KeyListItem,
LinkButton,
Stack,
Text,
} from "@namada/components";
import { DerivedAccount } from "@namada/types";

import {
LockKeyRingMsg,
ParentAccount,
QueryParentAccountsMsg,
SetActiveAccountMsg,
} from "background/keyring";

import { formatRouterPath } from "@namada/utils";
import { Ports } from "router";
import { Status } from "../App";
import { ParentAccount } from "background/keyring";
import { AccountManagementRoute, TopLevelRoute } from "../types";
import { SettingsHeader } from "./ParentAccounts.components";
import { useRequester } from "hooks/useRequester";

type ParentAccountsProps = {
activeAccountId: string;
parentAccounts: DerivedAccount[];
onChangeActiveAccount: (
accountId: string,
accountType: ParentAccount
) => void;
onLockApp: () => void;
};

/**
* Represents the extension's settings page.
*/
export const ParentAccounts: React.FC<{
activeAccountId: string;
onSelectAccount: (account: DerivedAccount) => void;
}> = ({ activeAccountId, onSelectAccount }) => {
const [status, setStatus] = useState<Status>(Status.Pending);
const [error, setError] = useState<string>("");
const [parentAccounts, setParentAccounts] = useState<DerivedAccount[]>([]);

const requester = useRequester();
export const ParentAccounts: React.FC<ParentAccountsProps> = ({
activeAccountId,
onLockApp,
parentAccounts,
onChangeActiveAccount,
}) => {
const navigate = useNavigate();

const fetchParentAccounts = async (): Promise<void> => {
setStatus(Status.Pending);
try {
const accounts = await requester.sendMessage(
Ports.Background,
new QueryParentAccountsMsg()
);
setParentAccounts(accounts);
setStatus(Status.Completed);
} catch (e) {
console.error(e);
setError(`An error occurred while loading extension: ${e}`);
setStatus(Status.Failed);
}
};

const handleSelectAccount = async (
account: DerivedAccount
): Promise<void> => {
const { id, type } = account;
try {
await requester.sendMessage(
Ports.Background,
new SetActiveAccountMsg(id, type as ParentAccount)
);

// Lock current wallet keyring:
await requester.sendMessage(Ports.Background, new LockKeyRingMsg());

// Fetch accounts for selected parent account
onSelectAccount(account);
} catch (e) {
console.error(e);
setError(`An error occurred while setting active account: ${e}`);
setStatus(Status.Failed);
}
};

const goToSetupPage = (): void => {
browser.tabs.create({
url: browser.runtime.getURL("setup.html"),
Expand Down Expand Up @@ -110,35 +69,10 @@ export const ParentAccounts: React.FC<{
navigate(formatRouterPath([TopLevelRoute.ConnectedSites]));
};

// we use a ref to make sure the effect does not run on the first
// render, which would be before parent accounts have loaded
const firstRender = useRef(true);
useEffect(() => {
if (firstRender.current) {
firstRender.current = false;
return;
}
(async () => {
if (parentAccounts.length === 0) {
// the last account has been deleted so navigate to setup
navigate(TopLevelRoute.Setup);
} else if (!parentAccounts.some(({ id }) => id === activeAccountId)) {
// the active account was deleted so make the first account active
await handleSelectAccount(parentAccounts[0]);
}
})();
}, [parentAccounts]);

useEffect(() => {
fetchParentAccounts();
}, []);

return (
<>
<Stack gap={GapPatterns.TitleContent}>
<Heading>Namada Keys</Heading>
<Stack gap={4}>
{error && <Alert type="error">{error}</Alert>}
{status === Status.Pending && "loading"}
<SettingsHeader>
<Text>Set default keys</Text>
<ActionButton size="sm" onClick={goToSetupPage}>
Expand All @@ -152,10 +86,15 @@ export const ParentAccounts: React.FC<{
as="li"
alias={account.alias}
isMainKey={activeAccountId === account.id}
onSelectAccount={() => handleSelectAccount(account)}
onRename={() => {}}
onDelete={() => goToDeletePage(account)}
onViewAccount={() => goToViewAccount(account)}
onRename={() => {}}
onSelectAccount={() => {
onChangeActiveAccount(
account.id,
account.type as ParentAccount
);
}}
/>
))}
</Stack>
Expand All @@ -166,7 +105,13 @@ export const ParentAccounts: React.FC<{
>
View Connected Sites
</ActionButton>
<ActionButton onClick={() => navigate("/change-password")} size="sm">
Change password
</ActionButton>
<LinkButton onClick={onLockApp} size="sm">
Lock Wallet
</LinkButton>
</Stack>
</>
</Stack>
);
};
7 changes: 4 additions & 3 deletions apps/extension/src/App/Accounts/ViewAccount/ViewAccount.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
Alert,
GapPatterns,
Heading,
LinkButton,
Loading,
Expand All @@ -22,7 +23,7 @@ type ViewAccountUrlParams = {
};

export const ViewAccount = (): JSX.Element => {
const { accountId = "", type } = useParams<ViewAccountUrlParams>();
const { accountId = "" } = useParams<ViewAccountUrlParams>();
const [loadingStatus, setLoadingStatus] = useState("");
const [accounts, setAccounts] = useState<DerivedAccount[]>([]);
const [parentAccount, setParentAccount] = useState<DerivedAccount>();
Expand All @@ -38,7 +39,7 @@ export const ViewAccount = (): JSX.Element => {
try {
const accounts = await requester.sendMessage(
Ports.Background,
new QueryAccountsMsg({ accountId, type })
new QueryAccountsMsg({ accountId })
);
setAccounts(accounts);

Expand Down Expand Up @@ -93,7 +94,7 @@ export const ViewAccount = (): JSX.Element => {
)}

{!loadingStatus && !error && parentAccount && accounts.length > 0 && (
<Stack gap={6}>
<Stack gap={GapPatterns.TitleContent}>
<Heading>{parentAccount.alias}</Heading>
<ViewKeys
publicKeyAddress={parentAccount.publicKey ?? ""}
Expand Down
Loading

0 comments on commit c890c2d

Please sign in to comment.