From 0b1360f61d16fbaadc326e5fd947e53ab20cd8a3 Mon Sep 17 00:00:00 2001 From: "Justin R. Evans" <330911+jurevans@users.noreply.github.com> Date: Fri, 14 Oct 2022 11:34:56 -0400 Subject: [PATCH] feat/94 - Extension: Storage updates (#97) * Begin setting up KeyRing & accounts storage * Adding a simple typed state class to keyring * Address helper to obtain ImplicitAddress, cargo update * Continue hooking up KeyRing to services * Fix broken imports, confirm message works in popup * Adding msg type for fetch generated mnemonic, update tests * Beginning components package; get styled-components with theme working * Disable devtool sourcemapping, add plugin for extension reloading (all browsers) * Properly include svg assets, port additional components into shared package * Fix issue on reloader plugin * fix module resolution, clean up imports * Remove unnecessary assignment * Consolidate types from Keplr into our own * Validate mnemonic phrase before storing, better error handling in wasm * Minor clean up, better type State class so as not to instantiate directly * Adding mnemonic/password creation screens, added README for types * Split set-up flow into new tab for initial account * Begin wiring up account derivation in service, generate implicit address to store and return * Add account derivation to completion process, load and display accounts * Better error-handling, add user feedback on account creation * Adding AccountListing components * Clean up configs, layout, fixed bug in key storage * Improved naming conventions, add implementation for scrypt, tests * Implement optional custom Scrypt params * Updated documentation * Add aes dependency, being basic implementation * KeyRing state is no longer duplicated, storage is only source of truth * Add icon for copy to clipboard, additional styling, update styled config * description -> alias to match cli, clean up * Adding UI for adding a new derived account with basic validation * Fix bug where alias is not being saved * Add "alias" as a field during setup * Updating for consistency, rough pass at styling derivation form * Move path items to number, validate inputs, set primes appropriately * Begin implementing kdf derived key as bytes * Fix naming on file extension, begin components for password auth * Add basic login/logout functionality, hook up to service backend * Clean up effects, check keyring status on auth * Fix minor bug when locking/unlocking wallet. Switch to numeric input * Fix sourcemap warning, reuse lock button wrapper, minor style * Update kdf libs for password hashing, tests, serializing params to JsValue * Add serializable struct and associated tests (params + bytes) * Fix naming convention, create storage type containing params * Moving storage updates to separate PR * Adding support for argon2 and scrypt pbkdf hashing * Cover Scrypt in jest tests * Clean up * Remove unused dependencies in this branch * 2 more dependencies that are unneeded * Adding dependencies * Clean up * Add serialize to key+params (argon2), add tests * Fix for Cargo * Improve custom params, add salt as an option * Initial implementation of AES+Argon2 with related tests * Clarify naming in jest test * Update scrypt to accept salt, optional params, similar tests with AES * Clean up * Add missing test for scrypt with provided salt, clean up * Clean up Rust lib, begin to implement encrypt/decrypt in KeyRing * Add simple Rng mod for generating random bytes * Touch up TS types * clean up tests * Connect KeyRing instance to new storage types, update UI * Minor styling, match account hierarchy to other wallets * Improve naming conventions, comments, updated for consistency * Couple minor fixes that slipped through the cracks * Minor updates per feedback * Minor updates per PR feedback --- .../App/Accounts/AccountListing.components.ts | 22 +- .../Accounts/AccountListing.components.tsx | 52 ---- .../src/App/Accounts/AccountListing.tsx | 41 ++- .../src/App/Accounts/Accounts.components.ts | 30 +-- apps/extension/src/App/Accounts/Accounts.tsx | 18 +- .../extension/src/App/Accounts/AddAccount.tsx | 76 +++--- apps/extension/src/App/Accounts/index.tsx | 3 - apps/extension/src/App/App.tsx | 7 + apps/extension/src/App/Login/Login.tsx | 2 +- .../Steps/Completion/Completion.tsx | 38 +-- .../src/background/keyring/crypto.ts | 65 +++++ .../extension/src/background/keyring/index.ts | 1 + .../src/background/keyring/keyring.ts | 117 +++++---- .../src/background/keyring/messages.ts | 16 +- .../extension/src/background/keyring/types.ts | 56 ++++- apps/extension/src/background/types/Store.ts | 2 +- packages/crypto/lib/Cargo.lock | 213 +++++++++++++++- packages/crypto/lib/Cargo.toml | 6 + packages/crypto/lib/src/crypto/aes.rs | 80 ++++++ packages/crypto/lib/src/crypto/argon2.rs | 230 +++++++++++++++++ packages/crypto/lib/src/crypto/bip32.rs | 23 +- packages/crypto/lib/src/crypto/bip39.rs | 20 +- packages/crypto/lib/src/crypto/mod.rs | 6 + packages/crypto/lib/src/crypto/rng.rs | 56 +++++ packages/crypto/lib/src/crypto/salt.rs | 62 +++++ packages/crypto/lib/src/crypto/scrypt.rs | 234 ++++++++++++++++++ packages/crypto/src/__tests__/crypto.test.ts | 128 +++++++++- 27 files changed, 1330 insertions(+), 274 deletions(-) delete mode 100644 apps/extension/src/App/Accounts/AccountListing.components.tsx delete mode 100644 apps/extension/src/App/Accounts/index.tsx create mode 100644 apps/extension/src/background/keyring/crypto.ts create mode 100644 packages/crypto/lib/src/crypto/aes.rs create mode 100644 packages/crypto/lib/src/crypto/argon2.rs create mode 100644 packages/crypto/lib/src/crypto/rng.rs create mode 100644 packages/crypto/lib/src/crypto/salt.rs create mode 100644 packages/crypto/lib/src/crypto/scrypt.rs diff --git a/apps/extension/src/App/Accounts/AccountListing.components.ts b/apps/extension/src/App/Accounts/AccountListing.components.ts index de97ac5ab12..2c1671445d3 100644 --- a/apps/extension/src/App/Accounts/AccountListing.components.ts +++ b/apps/extension/src/App/Accounts/AccountListing.components.ts @@ -4,13 +4,14 @@ export const AccountListingContainer = styled.div` display: flex; flex-direction: row; width: 100%; + font-family: "Space Grotesk", sans-serif; background-color: ${(props) => props.theme.colors.utility1.main}; + font-size: 11px; color: ${(props) => props.theme.colors.utility1.main40}; box-sizing: border-box; border: 1px solid ${(props) => props.theme.colors.utility1.main}; border-radius: 8px; box-sizing: border-box; - font-size: 10px; padding: 4px 8px; `; @@ -21,20 +22,25 @@ export const Details = styled.div` flex: 3; `; +export const DerivationPath = styled.div``; + +export const Address = styled.div``; + +export const Alias = styled.div` + color: ${(props) => props.theme.colors.utility1.main20}; + font-weight: 500; + padding: 4px 0; +`; + export const Buttons = styled.div` display: flex; flex-direction: row; justify-content: flex-end; + align-items: center; flex: 1; `; -export const DerivationPath = styled.div``; - -export const Address = styled.div``; - -export const Alias = styled.div``; - -export const CopyToClipboard = styled.a` +export const Button = styled.a` text-decoration: underline; padding: 5px; transition: "1 sec"; diff --git a/apps/extension/src/App/Accounts/AccountListing.components.tsx b/apps/extension/src/App/Accounts/AccountListing.components.tsx deleted file mode 100644 index de97ac5ab12..00000000000 --- a/apps/extension/src/App/Accounts/AccountListing.components.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import styled from "styled-components"; - -export const AccountListingContainer = styled.div` - display: flex; - flex-direction: row; - width: 100%; - background-color: ${(props) => props.theme.colors.utility1.main}; - color: ${(props) => props.theme.colors.utility1.main40}; - box-sizing: border-box; - border: 1px solid ${(props) => props.theme.colors.utility1.main}; - border-radius: 8px; - box-sizing: border-box; - font-size: 10px; - padding: 4px 8px; -`; - -export const Details = styled.div` - display: flex; - flex-direction: column; - justify-content: flex-start; - flex: 3; -`; - -export const Buttons = styled.div` - display: flex; - flex-direction: row; - justify-content: flex-end; - flex: 1; -`; - -export const DerivationPath = styled.div``; - -export const Address = styled.div``; - -export const Alias = styled.div``; - -export const CopyToClipboard = styled.a` - text-decoration: underline; - padding: 5px; - transition: "1 sec"; - border-radius: 4px; - - & > div > svg > path { - fill: ${(props) => props.theme.colors.utility1.main40}; - stroke: ${(props) => props.theme.colors.utility1.main40}; - } - - &:active { - border: 1px solid ${(props) => props.theme.colors.primary.main}; - background-color: ${(props) => props.theme.colors.primary.main}; - } -`; diff --git a/apps/extension/src/App/Accounts/AccountListing.tsx b/apps/extension/src/App/Accounts/AccountListing.tsx index e02aff86adb..4105dc4acaa 100644 --- a/apps/extension/src/App/Accounts/AccountListing.tsx +++ b/apps/extension/src/App/Accounts/AccountListing.tsx @@ -1,3 +1,4 @@ +import { useNavigate } from "react-router-dom"; import { Icon, IconName } from "@anoma/components"; import { AccountListingContainer, @@ -6,43 +7,63 @@ import { Details, Alias, DerivationPath, - CopyToClipboard, + Button, } from "./AccountListing.components"; -import { DerivedAccount } from "background/keyring"; +import { Bip44Path, DerivedAccount, KeyStoreType } from "background/keyring"; +import { TopLevelRoute } from "App/types"; type Props = { + // The parent Bip44 "account" + parent?: number; + // The child account account: DerivedAccount; + parentAlias?: string; }; const textToClipboard = (content: string): void => { navigator.clipboard.writeText(content); }; -const AccountListing = ({ account }: Props): JSX.Element => { - const { address, alias, bip44Path, establishedAddress } = account; - const coinType = "0'"; +const formatDerivationPath = ( + isChildAccount: boolean, + { account, change, index = 0 }: Bip44Path +): string => (isChildAccount ? `/${account}'/${change}/${index}` : ""); + +const AccountListing = ({ account, parentAlias }: Props): JSX.Element => { + const { address, alias, path, establishedAddress, type } = account; + const navigate = useNavigate(); + const isChildAccount = type !== KeyStoreType.Mnemonic; return (
- {alias && {alias}} - m/44'/{coinType}/{`${bip44Path.account}'`}/{bip44Path.change}/ - {bip44Path.index} + {isChildAccount && parentAlias}{" "} + {formatDerivationPath(isChildAccount, path)} + {alias && {alias}}
{address}
{establishedAddress &&
{establishedAddress}
}
- { + navigate(TopLevelRoute.AddAccount); + }} + > + + + )} +
); diff --git a/apps/extension/src/App/Accounts/Accounts.components.ts b/apps/extension/src/App/Accounts/Accounts.components.ts index e7ae321a5bd..2f68f6d7fba 100644 --- a/apps/extension/src/App/Accounts/Accounts.components.ts +++ b/apps/extension/src/App/Accounts/Accounts.components.ts @@ -4,7 +4,7 @@ export const AccountsContainer = styled.div` display: flex; flex-direction: column; height: 100%; - max-height: 400px; + max-height: 420px; width: 100%; box-sizing: border-box; padding: 0 8px; @@ -24,34 +24,6 @@ export const AccountsListItem = styled.li` color: ${(props) => props.theme.colors.utility1.main60}; `; -export const ButtonContainer = styled.div` - display: flex; - flex-direction: row; - justify-content: flex-end; - box-sizing: border-box; -`; - -export const Button = styled.button` - display: flex; - flex-direction: row; - justify-contents: start; - align-items: center; - cursor: pointer; - color: ${(props) => props.theme.colors.utility1.main40}; - background-color: ${(props) => props.theme.colors.utility1.main}; - border: 1px solid ${(props) => props.theme.colors.utility1.main}; - border-radius: 8px; - - & > div > svg > path { - fill: ${(props) => props.theme.colors.utility1.main40}; - stroke: ${(props) => props.theme.colors.utility1.main40}; - } -`; - -export const ButtonText = styled.span` - padding: 8px; -`; - export const ThemedScrollbarContainer = styled.div` overflow-y: auto; diff --git a/apps/extension/src/App/Accounts/Accounts.tsx b/apps/extension/src/App/Accounts/Accounts.tsx index 060f902bf59..4e3e9ded0fa 100644 --- a/apps/extension/src/App/Accounts/Accounts.tsx +++ b/apps/extension/src/App/Accounts/Accounts.tsx @@ -1,15 +1,8 @@ -import { useNavigate } from "react-router-dom"; -import { Icon, IconName } from "@anoma/components"; - -import { TopLevelRoute } from "App/types"; import { DerivedAccount } from "background/keyring"; import { AccountsContainer, AccountsList, AccountsListItem, - ButtonContainer, - Button, - ButtonText, ThemedScrollbarContainer, } from "./Accounts.components"; import { AccountListing } from "App/Accounts"; @@ -19,7 +12,8 @@ type Props = { }; const Accounts = ({ accounts }: Props): JSX.Element => { - const navigate = useNavigate(); + const parentAccount = accounts[0]; + const alias = parentAccount?.alias; return ( @@ -27,16 +21,10 @@ const Accounts = ({ accounts }: Props): JSX.Element => { {accounts.map((account) => ( - + ))} - - - ); diff --git a/apps/extension/src/App/Accounts/AddAccount.tsx b/apps/extension/src/App/Accounts/AddAccount.tsx index 8dfe398334a..8de976406b7 100644 --- a/apps/extension/src/App/Accounts/AddAccount.tsx +++ b/apps/extension/src/App/Accounts/AddAccount.tsx @@ -4,7 +4,11 @@ import { Button, ButtonVariant, Input, InputVariant } from "@anoma/components"; import { ExtensionRequester } from "extension"; import { Ports } from "router"; -import { DerivedAccount, DeriveAccountMsg } from "background/keyring"; +import { + DerivedAccount, + DeriveAccountMsg, + KeyStoreType, +} from "background/keyring"; import { AddAccountContainer, AddAccountForm, @@ -21,24 +25,24 @@ import { } from "./AddAccount.components"; type Props = { + // The parent Bip44 "account" + parentAccount: number; accounts: DerivedAccount[]; requester: ExtensionRequester; setAccounts: (accounts: DerivedAccount[]) => void; }; const validateAccount = ( - newAccount: { account: number; change: number; index: number }, + account: number, + newAccount: { change: number; index: number }, accounts: DerivedAccount[] ): boolean => { - const newPath = [ - newAccount.account, - newAccount.change, - newAccount.index, - ].join("/"); + const newPath = [account, newAccount.change, newAccount.index].join("/"); let isValid = true; - accounts.forEach((a: DerivedAccount) => { - const { bip44Path } = a; - const { account, change, index } = bip44Path; + accounts.forEach((derivedAccount: DerivedAccount) => { + const { + path: { account, change, index }, + } = derivedAccount; const existingPath = [account, change, index].join("/"); if (newPath === existingPath) { @@ -51,14 +55,15 @@ const validateAccount = ( const findNextIndex = (accounts: DerivedAccount[]): number => { let maxIndex = 0; - accounts.forEach((account) => { - const { index } = account.bip44Path; - if (index > maxIndex) { - maxIndex = index; - } - }); - return maxIndex + 1; + accounts + .filter((account) => account.type !== KeyStoreType.Mnemonic) + .forEach((account) => { + const { index = 0 } = account.path; + maxIndex = index + 1; + }); + + return maxIndex; }; enum Status { @@ -67,10 +72,14 @@ enum Status { Failed, } -const AddAccount: React.FC = ({ accounts, requester, setAccounts }) => { +const AddAccount: React.FC = ({ + parentAccount, + accounts, + requester, + setAccounts, +}) => { const navigate = useNavigate(); const [alias, setAlias] = useState(""); - const [account, setAccount] = useState(0); const [change, setChange] = useState(0); const [index, setIndex] = useState(findNextIndex(accounts)); const [bip44Error, setBip44Error] = useState(""); @@ -78,11 +87,11 @@ const AddAccount: React.FC = ({ accounts, requester, setAccounts }) => { const [formError, setFormError] = useState(""); const [formStatus, setFormStatus] = useState(Status.Idle); - const bip44Prefix = "m/44'"; - const coinType = "0'"; + const bip44Prefix = "m/44"; + const coinType = 0; useEffect(() => { - const isValid = validateAccount({ account, change, index }, accounts); + const isValid = validateAccount(parentAccount, { change, index }, accounts); if (!isValid) { setBip44Error("Invalid path! This path is already in use."); setIsFormValid(false); @@ -90,7 +99,7 @@ const AddAccount: React.FC = ({ accounts, requester, setAccounts }) => { setBip44Error(""); setIsFormValid(true); } - }, [account, change, index]); + }, [parentAccount, change, index]); const handleAccountAdd = async (): Promise => { setFormStatus(Status.Pending); @@ -98,7 +107,7 @@ const AddAccount: React.FC = ({ accounts, requester, setAccounts }) => { const derivedAccount: DerivedAccount = await requester.sendMessage( Ports.Background, - new DeriveAccountMsg({ account, change, index }, alias) + new DeriveAccountMsg({ account: parentAccount, change, index }, alias) ); setAccounts([...accounts, derivedAccount]); navigate(-1); @@ -120,6 +129,8 @@ const AddAccount: React.FC = ({ accounts, requester, setAccounts }) => { const handleFocus = (e: React.ChangeEvent): void => e.target.select(); + const parentDerivationPath = `${bip44Prefix}'/${coinType}'/${parentAccount}'/`; + return ( @@ -136,20 +147,11 @@ const AddAccount: React.FC = ({ accounts, requester, setAccounts }) => { diff --git a/apps/extension/src/App/Accounts/index.tsx b/apps/extension/src/App/Accounts/index.tsx deleted file mode 100644 index b51a51d24c7..00000000000 --- a/apps/extension/src/App/Accounts/index.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export { default as Accounts } from "./Accounts"; -export { default as AccountListing } from "./AccountListing"; -export { default as AddAccount } from "./AddAccount"; diff --git a/apps/extension/src/App/App.tsx b/apps/extension/src/App/App.tsx index 517e90a6b8d..3df9b385d4f 100644 --- a/apps/extension/src/App/App.tsx +++ b/apps/extension/src/App/App.tsx @@ -8,6 +8,7 @@ import { Ports } from "router"; import { CheckIsLockedMsg, DerivedAccount, + KeyStoreType, QueryAccountsMsg, } from "background/keyring"; @@ -83,6 +84,11 @@ export const App: React.FC = () => { } }, [status, accounts]); + const parent = accounts.find( + (account) => account.type === KeyStoreType.Mnemonic + ); + const parentAccount = parent?.path?.account ?? 0; + return ( @@ -111,6 +117,7 @@ export const App: React.FC = () => { element={ = ({ requester }) => { const [password, setPassword] = useState(""); const [status, setStatus] = useState(); - const handleSubmit = async () => { + const handleSubmit = async (): Promise => { setStatus(Status.Pending); try { const { status: lockStatus } = await requester.sendMessage( diff --git a/apps/extension/src/Setup/AccountCreation/Steps/Completion/Completion.tsx b/apps/extension/src/Setup/AccountCreation/Steps/Completion/Completion.tsx index 1482a7bc99e..5d13d8e0b7b 100644 --- a/apps/extension/src/Setup/AccountCreation/Steps/Completion/Completion.tsx +++ b/apps/extension/src/Setup/AccountCreation/Steps/Completion/Completion.tsx @@ -3,7 +3,7 @@ import { ExtensionRequester } from "extension"; import browser from "webextension-polyfill"; import { Button, ButtonVariant } from "@anoma/components"; -import { DeriveAccountMsg, SaveMnemonicMsg } from "background/keyring"; +import { SaveMnemonicMsg } from "background/keyring"; import { Ports } from "router"; import { @@ -30,7 +30,6 @@ enum Status { const Completion: React.FC = (props) => { const { alias, mnemonic, password, requester } = props; const [mnemonicStatus, setMnemonicStatus] = useState(Status.Pending); - const [accountStatus, setAccountStatus] = useState(Status.Pending); const [isComplete, setIsComplete] = useState(false); useEffect(() => { @@ -39,7 +38,7 @@ const Completion: React.FC = (props) => { try { await requester.sendMessage( Ports.Background, - new SaveMnemonicMsg(mnemonic, password) + new SaveMnemonicMsg(mnemonic, password, alias) ); setMnemonicStatus(Status.Completed); } catch (e) { @@ -47,17 +46,6 @@ const Completion: React.FC = (props) => { setMnemonicStatus(Status.Failed); } - try { - await requester.sendMessage( - Ports.Background, - new DeriveAccountMsg({ account: 0, change: 0, index: 0 }, alias) - ); - setAccountStatus(Status.Completed); - } catch (e) { - console.error(e); - setAccountStatus(Status.Failed); - } - setIsComplete(true); })(); } @@ -73,25 +61,15 @@ const Completion: React.FC = (props) => { popup to view your accounts. )} - {!isComplete && ( One moment while your wallet is being created... )} - -
    -
  • - Encrypting and storing mnemonic:{" "} - {mnemonicStatus === Status.Completed && Complete!} - {mnemonicStatus === Status.Pending && Pending...} - {mnemonicStatus === Status.Failed && Failed} -
  • -
  • - Deriving and storing initial account:{" "} - {accountStatus === Status.Completed && Complete!} - {accountStatus === Status.Pending && Pending...} - {accountStatus === Status.Failed && Failed} -
  • -
+ + Encrypting and storing mnemonic:{" "} + {mnemonicStatus === Status.Completed && Complete!} + {mnemonicStatus === Status.Pending && Pending...} + {mnemonicStatus === Status.Failed && Failed} +