diff --git a/apps/extension/src/core/handlers/index.ts b/apps/extension/src/core/handlers/index.ts index 0cb5d3a16..344dc0553 100644 --- a/apps/extension/src/core/handlers/index.ts +++ b/apps/extension/src/core/handlers/index.ts @@ -1,4 +1,4 @@ -import { DEBUG, PORT_EXTENSION } from "@core/constants" +import { PORT_EXTENSION } from "@core/constants" import { AnyEthRequest } from "@core/injectEth/types" import { log } from "@core/log" import { assert } from "@polkadot/util" @@ -12,6 +12,22 @@ import Tabs from "./Tabs" const extension = new Extension(extensionStores) const tabs = new Tabs(tabStores) +// dev mode logs shouldn't log content for these messages +const OBFUSCATE_LOG_MESSAGES: MessageTypes[] = [ + "pri(mnemonic.unlock)", + "pri(app.authenticate)", + "pri(app.checkPassword)", + "pri(app.changePassword)", + "pri(accounts.export)", + "pri(accounts.export.pk)", + "pri(accounts.validateMnemonic)", + "pri(accounts.create.seed)", + "pri(accounts.create.json)", + "pri(accounts.setVerifierCertMnemonic)", + "pri(app.onboard)", +] +const OBFUSCATED_PAYLOAD = "#OBFUSCATED#" + const formatFrom = (source: string) => { if (["extension", ""].includes(source)) return source if (!source) return source @@ -35,9 +51,10 @@ const talismanHandler = ( const source = `${formatFrom(from)}: ${id}: ${ message === "pub(eth.request)" ? `${message} ${(request as AnyEthRequest).method}` : message }` + const shouldLog = !OBFUSCATE_LOG_MESSAGES.includes(message) // eslint-disable-next-line no-console - DEBUG && console.debug(`[${port.name} REQ] ${source}`, { request }) + log.debug(`[${port.name} REQ] ${source}`, { request: shouldLog ? request : OBFUSCATED_PAYLOAD }) // handle the request and get a promise as a response const promise = isExtension @@ -47,7 +64,10 @@ const talismanHandler = ( // resolve the promise and send back the response promise .then((response): void => { - log.debug(`[${port.name} RES] ${source}`, { request, response }) + log.debug(`[${port.name} RES] ${source}`, { + request: shouldLog ? request : OBFUSCATED_PAYLOAD, + response: shouldLog ? response : OBFUSCATED_PAYLOAD, + }) // between the start and the end of the promise, the user may have closed // the tab, in which case port will be undefined @@ -63,9 +83,12 @@ const talismanHandler = ( } throw e } + + // heap cleanup + response = null }) .catch((error) => { - log.debug(`[err] ${source}:: ${error.message}`, { error }) + log.error(`[${port.name} ERR] ${source}:: ${error.message}`, { error }) if ( error instanceof Error && @@ -97,6 +120,10 @@ const talismanHandler = ( } } }) + .finally(() => { + // heap cleanup + data.request = null + }) } export default talismanHandler diff --git a/apps/extension/src/ui/apps/dashboard/routes/Settings/ChangePassword.tsx b/apps/extension/src/ui/apps/dashboard/routes/Settings/ChangePassword.tsx index f4dfe0274..0ad278a80 100644 --- a/apps/extension/src/ui/apps/dashboard/routes/Settings/ChangePassword.tsx +++ b/apps/extension/src/ui/apps/dashboard/routes/Settings/ChangePassword.tsx @@ -7,7 +7,7 @@ import { api } from "@ui/api" import Layout from "@ui/apps/dashboard/layout" import { MnemonicModal } from "@ui/domains/Settings/MnemonicModal" import useMnemonicBackup from "@ui/hooks/useMnemonicBackup" -import { useCallback } from "react" +import { useCallback, useEffect, useMemo } from "react" import { useForm } from "react-hook-form" import { useTranslation } from "react-i18next" import { useNavigate } from "react-router-dom" @@ -26,22 +26,27 @@ const ChangePassword = () => { const { isNotConfirmed } = useMnemonicBackup() const { isOpen, open, close } = useOpenClose() - const schema = yup - .object({ - currentPw: yup.string().required(""), - newPw: yup.string().required("").min(6, t("Password must be at least 6 characters long")), - newPwConfirm: yup - .string() - .required("") - .oneOf([yup.ref("newPw")], t("Passwords must match!")), - }) - .required() + const schema = useMemo( + () => + yup + .object({ + currentPw: yup.string().required(""), + newPw: yup.string().required("").min(6, t("Password must be at least 6 characters long")), + newPwConfirm: yup + .string() + .required("") + .oneOf([yup.ref("newPw")], t("Passwords must match!")), + }) + .required(), + [t] + ) const { register, handleSubmit, formState: { errors, isValid, isSubmitting }, setError, + setValue, } = useForm({ mode: "onChange", resolver: yupResolver(schema), @@ -73,6 +78,14 @@ const ChangePassword = () => { [navigate, setError, t] ) + useEffect(() => { + return () => { + setValue("currentPw", "") + setValue("newPw", "") + setValue("newPwConfirm", "") + } + }, [setValue]) + return ( <> diff --git a/apps/extension/src/ui/apps/onboard/context.tsx b/apps/extension/src/ui/apps/onboard/context.tsx index 785857a02..13d5d5aef 100644 --- a/apps/extension/src/ui/apps/onboard/context.tsx +++ b/apps/extension/src/ui/apps/onboard/context.tsx @@ -62,6 +62,12 @@ const useAppOnboardProvider = ({ isResettingWallet = false }: { isResettingWalle }) }, [data.allowTracking]) + useEffect(() => { + return () => { + setData(DEFAULT_DATA) + } + }, []) + return { onboard, reset, diff --git a/apps/extension/src/ui/apps/onboard/routes/ImportSeed.tsx b/apps/extension/src/ui/apps/onboard/routes/ImportSeed.tsx index 0c09c626f..1aa77e956 100644 --- a/apps/extension/src/ui/apps/onboard/routes/ImportSeed.tsx +++ b/apps/extension/src/ui/apps/onboard/routes/ImportSeed.tsx @@ -2,7 +2,7 @@ import { yupResolver } from "@hookform/resolvers/yup" import { api } from "@ui/api" import { AnalyticsPage, sendAnalyticsEvent } from "@ui/api/analytics" import { useAnalyticsPageView } from "@ui/hooks/useAnalyticsPageView" -import { useCallback, useMemo } from "react" +import { useCallback, useEffect, useMemo } from "react" import { useForm } from "react-hook-form" import { Trans, useTranslation } from "react-i18next" import { useNavigate } from "react-router-dom" @@ -61,6 +61,7 @@ export const ImportSeedPage = () => { const { register, handleSubmit, + setValue, formState: { errors, isValid }, } = useForm({ mode: "onChange", @@ -85,6 +86,12 @@ export const ImportSeedPage = () => { [data.importAccountType, data.importMethodType, navigate, updateData] ) + useEffect(() => { + return () => { + setValue("mnemonic", "") + } + }, [setValue]) + return (
diff --git a/apps/extension/src/ui/apps/onboard/routes/Password.tsx b/apps/extension/src/ui/apps/onboard/routes/Password.tsx index 86318c6ab..2e239cce1 100644 --- a/apps/extension/src/ui/apps/onboard/routes/Password.tsx +++ b/apps/extension/src/ui/apps/onboard/routes/Password.tsx @@ -53,6 +53,7 @@ export const PasswordPage = () => { handleSubmit, watch, trigger, + setValue, formState: { errors, isValid, isSubmitting }, } = useForm({ mode: "all", @@ -99,6 +100,13 @@ export const PasswordPage = () => { ] }, [data, t]) + useEffect(() => { + return () => { + setValue("password", "") + setValue("passwordConfirm", "") + } + }, [setValue]) + return (
diff --git a/apps/extension/src/ui/apps/popup/pages/Login.tsx b/apps/extension/src/ui/apps/popup/pages/Login.tsx index ac5b6b5d4..99e804b4e 100644 --- a/apps/extension/src/ui/apps/popup/pages/Login.tsx +++ b/apps/extension/src/ui/apps/popup/pages/Login.tsx @@ -137,6 +137,12 @@ const Login = ({ setShowResetWallet }: { setShowResetWallet: () => void }) => { } }, [handleSubmit, setValue, submit]) + useEffect(() => { + return () => { + setValue("password", "") + } + }, [setValue]) + return ( }> diff --git a/apps/extension/src/ui/domains/Account/AccountExportModal.tsx b/apps/extension/src/ui/domains/Account/AccountExportModal.tsx index 2fb6b040b..588e8972c 100644 --- a/apps/extension/src/ui/domains/Account/AccountExportModal.tsx +++ b/apps/extension/src/ui/domains/Account/AccountExportModal.tsx @@ -77,6 +77,7 @@ const ExportAccountForm = ({ onSuccess }: { onSuccess?: () => void }) => { formState: { errors, isValid, isSubmitting }, watch, setError, + setValue, } = useForm({ mode: "onChange", resolver: yupResolver(schema), @@ -99,6 +100,13 @@ const ExportAccountForm = ({ onSuccess }: { onSuccess?: () => void }) => { [exportAccount, setError, onSuccess, password] ) + useEffect(() => { + return () => { + setValue("newPw", "") + setValue("newPwConfirm", "") + } + }, [setValue]) + if (!canExportAccount || !password) return null return (
diff --git a/apps/extension/src/ui/domains/Account/AccountExportPrivateKeyModal.tsx b/apps/extension/src/ui/domains/Account/AccountExportPrivateKeyModal.tsx index 2526c597d..04f5fc17d 100644 --- a/apps/extension/src/ui/domains/Account/AccountExportPrivateKeyModal.tsx +++ b/apps/extension/src/ui/domains/Account/AccountExportPrivateKeyModal.tsx @@ -4,9 +4,9 @@ import { notify } from "@talisman/components/Notifications" import { useOpenClose } from "@talisman/hooks/useOpenClose" import { CopyIcon, LoaderIcon } from "@talisman/theme/icons" import { provideContext } from "@talisman/util/provideContext" -import { useQuery } from "@tanstack/react-query" import { api } from "@ui/api" -import { useCallback, useEffect, useMemo } from "react" +import { useSensitiveState } from "@ui/hooks/useSensitiveState" +import { useCallback, useEffect, useMemo, useState } from "react" import { useTranslation } from "react-i18next" import { Button } from "talisman-ui" @@ -28,7 +28,7 @@ const useAccountExportPrivateKeyModalProvider = () => { ) const exportAccount = useCallback( - (password: string) => { + async (password: string) => { if (!account) return return api.accountExportPrivateKey(account.address, password) }, @@ -46,19 +46,10 @@ const ExportPrivateKeyResult = ({ onClose }: { onClose?: () => void }) => { const { account, exportAccount } = useAccountExportPrivateKeyModal() const { password } = usePasswordUnlock() - // force password check each time this component is rendered - const { - error, - data: privateKey, - isLoading, - } = useQuery({ - queryKey: ["accountExportPrivateKey", !!password], - queryFn: () => (password ? exportAccount(password) : null), - refetchInterval: false, - refetchOnWindowFocus: false, - refetchOnReconnect: false, - refetchOnMount: true, - }) + // don't use react-query here as we don't want this to be cached + const [privateKey, setPrivateKey] = useSensitiveState() + const [isLoading, setIsLoading] = useState(false) + const [error, setError] = useState() const copyToClipboard = useCallback(async () => { if (!privateKey) return @@ -88,6 +79,24 @@ const ExportPrivateKeyResult = ({ onClose }: { onClose?: () => void }) => { } }, [privateKey, t]) + useEffect(() => { + if (password) { + setError(undefined) + setIsLoading(true) + exportAccount(password) + .then(setPrivateKey) + .catch(setError) + .finally(() => setIsLoading(false)) + } + }, [exportAccount, password, setPrivateKey]) + + useEffect(() => { + return () => { + setError(undefined) + setIsLoading(false) + } + }, []) + if (!account) return null return ( diff --git a/apps/extension/src/ui/domains/Account/MnemonicForm.tsx b/apps/extension/src/ui/domains/Account/MnemonicForm.tsx index 1b1ba66a6..11605e131 100644 --- a/apps/extension/src/ui/domains/Account/MnemonicForm.tsx +++ b/apps/extension/src/ui/domains/Account/MnemonicForm.tsx @@ -1,7 +1,8 @@ import { classNames } from "@talismn/util" import { api } from "@ui/api" import useMnemonicBackup from "@ui/hooks/useMnemonicBackup" -import { useEffect, useState } from "react" +import { useSensitiveState } from "@ui/hooks/useSensitiveState" +import { useEffect } from "react" import { Trans, useTranslation } from "react-i18next" import styled from "styled-components" import { Toggle } from "talisman-ui" @@ -41,13 +42,13 @@ type MnemonicFormProps = { const MnemonicForm = ({ className }: MnemonicFormProps) => { const { t } = useTranslation() const { isConfirmed, toggleConfirmed } = useMnemonicBackup() - const [mnemonic, setMnemonic] = useState() + const [mnemonic, setMnemonic] = useSensitiveState() const { password } = usePasswordUnlock() useEffect(() => { if (!password) return - api.mnemonicUnlock(password).then((result) => setMnemonic(result)) - }, [password]) + api.mnemonicUnlock(password).then(setMnemonic) + }, [password, setMnemonic]) return (
diff --git a/apps/extension/src/ui/domains/Account/PasswordUnlock.tsx b/apps/extension/src/ui/domains/Account/PasswordUnlock.tsx index 4b4ed5fb9..a5def5e01 100644 --- a/apps/extension/src/ui/domains/Account/PasswordUnlock.tsx +++ b/apps/extension/src/ui/domains/Account/PasswordUnlock.tsx @@ -2,7 +2,8 @@ import { yupResolver } from "@hookform/resolvers/yup" import { KeyIcon } from "@talisman/theme/icons" import { provideContext } from "@talisman/util/provideContext" import { api } from "@ui/api" -import { ReactNode, useCallback, useState } from "react" +import { useSensitiveState } from "@ui/hooks/useSensitiveState" +import { ReactNode, useCallback, useEffect } from "react" import { useForm } from "react-hook-form" import { useTranslation } from "react-i18next" import { Button, FormFieldContainer, FormFieldInputText } from "talisman-ui" @@ -31,7 +32,7 @@ type PasswordUnlockContext = { } function usePasswordUnlockContext(): PasswordUnlockContext { - const [password, setPassword] = useState() + const [password, setPassword] = useSensitiveState() const checkPassword = useCallback( async (password: string) => { @@ -56,6 +57,8 @@ const BasePasswordUnlock = ({ className, children, buttonText, title }: Password register, handleSubmit, setError, + setValue, + setFocus, formState: { errors, isValid, isSubmitting }, } = useForm({ mode: "onChange", @@ -77,6 +80,16 @@ const BasePasswordUnlock = ({ className, children, buttonText, title }: Password [checkPassword, setError] ) + useEffect(() => { + if (!password) setFocus("password") + }, [password, setFocus]) + + useEffect(() => { + return () => { + setValue("password", "") + } + }, [setValue]) + return password ? (
{children}
) : ( @@ -92,8 +105,6 @@ const BasePasswordUnlock = ({ className, children, buttonText, title }: Password placeholder={t("Enter password")} spellCheck={false} data-lpignore - // eslint-disable-next-line jsx-a11y/no-autofocus - autoFocus />
diff --git a/apps/extension/src/ui/domains/Settings/MigratePassword/context.ts b/apps/extension/src/ui/domains/Settings/MigratePassword/context.ts index 96abbfa2b..81876694f 100644 --- a/apps/extension/src/ui/domains/Settings/MigratePassword/context.ts +++ b/apps/extension/src/ui/domains/Settings/MigratePassword/context.ts @@ -4,14 +4,15 @@ import useStatus, { statusOptions } from "@talisman/hooks/useStatus" import { provideContext } from "@talisman/util/provideContext" import { api } from "@ui/api" import useMnemonicBackup from "@ui/hooks/useMnemonicBackup" +import { useSensitiveState } from "@ui/hooks/useSensitiveState" import { useSetting } from "@ui/hooks/useSettings" import { useCallback, useEffect, useState } from "react" const useMigratePasswordProvider = ({ onComplete }: { onComplete: () => void }) => { - const [password, setPassword] = useState() - const [newPassword, setNewPassword] = useState() + const [password, setPassword] = useSensitiveState() + const [newPassword, setNewPassword] = useSensitiveState() + const [mnemonic, setMnemonic] = useSensitiveState() const [passwordTrimmed, setPasswordTrimmed] = useState() - const [mnemonic, setMnemonic] = useState() const [hasBackedUpMnemonic, setHasBackedUpMnemonic] = useState(false) const [error, setError] = useState() const [useErrorTracking] = useSetting("useErrorTracking") diff --git a/apps/extension/src/ui/hooks/useSensitiveState.ts b/apps/extension/src/ui/hooks/useSensitiveState.ts new file mode 100644 index 000000000..d912b8665 --- /dev/null +++ b/apps/extension/src/ui/hooks/useSensitiveState.ts @@ -0,0 +1,23 @@ +import { useEffect, useState } from "react" + +function useSensitiveState(): [ + T | undefined, + React.Dispatch> +] +function useSensitiveState(initialValue: T): [T, React.Dispatch>] +function useSensitiveState( + initialValue?: T +): [T | undefined, React.Dispatch>] { + const [value, setValue] = useState(initialValue) + + useEffect(() => { + return () => { + // Clear the sensitive state value on unmount + setValue(undefined) + } + }, []) + + return [value, setValue] +} + +export { useSensitiveState }