diff --git a/package.json b/package.json index c044f144..2cf96db1 100644 --- a/package.json +++ b/package.json @@ -144,7 +144,7 @@ "react-confetti": "^6.1.0", "react-dom": "^18.2.0", "react-i18next": "^12.2.0", - "react-icons": "^4.7.1", + "react-icons": "^5.2.1", "react-loader-spinner": "^5.3.4", "react-redux": "^8.0.5", "react-router-dom": "^6.8.2", diff --git a/src/common/types/IAuthenticationExtensionsClientOutputs.ts b/src/common/types/IAuthenticationExtensionsClientOutputs.ts new file mode 100644 index 00000000..6440b296 --- /dev/null +++ b/src/common/types/IAuthenticationExtensionsClientOutputs.ts @@ -0,0 +1,9 @@ +// types +import IPRFExtensionOutput from './IPRFExtensionOutput'; + +interface IAuthenticationExtensionsClientOutputs + extends AuthenticationExtensionsClientOutputs { + prf?: IPRFExtensionOutput; +} + +export default IAuthenticationExtensionsClientOutputs; diff --git a/src/common/types/IPRFExtensionOutput.ts b/src/common/types/IPRFExtensionOutput.ts new file mode 100644 index 00000000..07641a8a --- /dev/null +++ b/src/common/types/IPRFExtensionOutput.ts @@ -0,0 +1,9 @@ +// types +import IPRFExtensionResults from './IPRFExtensionResults'; + +interface IPRFExtensionOutput { + enabled?: boolean; + results?: IPRFExtensionResults; +} + +export default IPRFExtensionOutput; diff --git a/src/common/types/IPRFExtensionResults.ts b/src/common/types/IPRFExtensionResults.ts new file mode 100644 index 00000000..e333be2f --- /dev/null +++ b/src/common/types/IPRFExtensionResults.ts @@ -0,0 +1,5 @@ +interface IPRFExtensionResults { + first: ArrayBuffer; +} + +export default IPRFExtensionResults; diff --git a/src/common/types/index.ts b/src/common/types/index.ts index cc58a252..bea996d2 100644 --- a/src/common/types/index.ts +++ b/src/common/types/index.ts @@ -1,7 +1,10 @@ +export type { default as IAuthenticationExtensionsClientOutputs } from './IAuthenticationExtensionsClientOutputs'; export type { default as IBaseOptions } from './IBaseOptions'; export type { default as IClientInformation } from './IClientInformation'; export type { default as IClientRequestMessage } from './IClientRequestMessage'; export type { default as IClientResponseMessage } from './IClientResponseMessage'; export type { default as ILogger } from './ILogger'; export type { default as ILogLevel } from './ILogLevel'; +export type { default as IPRFExtensionOutput } from './IPRFExtensionOutput'; +export type { default as IPRFExtensionResults } from './IPRFExtensionResults'; export type { default as TProviderMessages } from './TProviderMessages'; diff --git a/src/extension/apps/background/App.tsx b/src/extension/apps/background/App.tsx index eb02383c..67e23f5c 100644 --- a/src/extension/apps/background/App.tsx +++ b/src/extension/apps/background/App.tsx @@ -14,6 +14,7 @@ import { reducer as eventsReducer } from '@extension/features/events'; import { reducer as layoutReducer } from '@extension/features/layout'; import { reducer as messagesReducer } from '@extension/features/messages'; import { reducer as networksReducer } from '@extension/features/networks'; +import { reducer as passkeysReducer } from '@extension/features/passkeys'; import { reducer as passwordLockReducer } from '@extension/features/password-lock'; import { reducer as sessionsReducer } from '@extension/features/sessions'; import { reducer as settingsReducer } from '@extension/features/settings'; @@ -35,6 +36,7 @@ const App: FC = ({ i18next, initialColorMode }: IAppProps) => { layout: layoutReducer, messages: messagesReducer, networks: networksReducer, + passkeys: passkeysReducer, passwordLock: passwordLockReducer, sessions: sessionsReducer, settings: settingsReducer, diff --git a/src/extension/apps/background/Root.tsx b/src/extension/apps/background/Root.tsx index b8fc5b1e..9b1ea55e 100644 --- a/src/extension/apps/background/Root.tsx +++ b/src/extension/apps/background/Root.tsx @@ -8,6 +8,7 @@ import LoadingPage from '@extension/components/LoadingPage'; import { fetchAccountsFromStorageThunk } from '@extension/features/accounts'; import { handleNewEventByIdThunk } from '@extension/features/events'; import { closeCurrentWindowThunk } from '@extension/features/layout'; +import { fetchFromStorageThunk as fetchPasskeyCredentialFromStorageThunk } from '@extension/features/passkeys'; import { fetchSessionsThunk } from '@extension/features/sessions'; import { fetchSettingsFromStorageThunk } from '@extension/features/settings'; import { fetchStandardAssetsFromStorageThunk } from '@extension/features/standard-assets'; @@ -47,6 +48,7 @@ const Root: FC = () => { return; } + dispatch(fetchPasskeyCredentialFromStorageThunk()); dispatch(fetchSystemInfoFromStorageThunk()); dispatch(fetchSettingsFromStorageThunk()); dispatch(fetchSessionsThunk()); diff --git a/src/extension/apps/main/App.tsx b/src/extension/apps/main/App.tsx index e40d8ea7..01467fc4 100644 --- a/src/extension/apps/main/App.tsx +++ b/src/extension/apps/main/App.tsx @@ -38,6 +38,7 @@ import { reducer as messagesReducer } from '@extension/features/messages'; import { reducer as networksReducer } from '@extension/features/networks'; import { reducer as newsReducer } from '@extension/features/news'; import { reducer as notificationsReducer } from '@extension/features/notifications'; +import { reducer as passkeysReducer } from '@extension/features/passkeys'; import { reducer as passwordLockReducer } from '@extension/features/password-lock'; import { reducer as reKeyAccountReducer } from '@extension/features/re-key-account'; import { reducer as removeAssetsReducer } from '@extension/features/remove-assets'; @@ -68,6 +69,7 @@ import type { IAppThunkDispatch, IMainRootState, ISettings, + TEncryptionCredentials, } from '@extension/types'; // utils @@ -143,17 +145,17 @@ const createRouter = ({ dispatch, getState }: Store) => { ], element: , loader: async () => { - let password: string | null; + let credentials: TEncryptionCredentials | null; let settings: ISettings; try { settings = await (dispatch as IAppThunkDispatch)( fetchSettingsFromStorageThunk() ).unwrap(); // fetch the settings from storage - password = getState().passwordLock.password; + credentials = getState().passwordLock.credentials; - // if the password lock is on, we need the password - if (settings.security.enablePasswordLock && !password) { + // if the password lock is on, we need the passkey/password + if (settings.security.enablePasswordLock && !credentials) { return redirect(PASSWORD_LOCK_ROUTE); } } catch (error) { @@ -187,6 +189,7 @@ const App: FC = ({ i18next, initialColorMode }: IAppProps) => { networks: networksReducer, news: newsReducer, notifications: notificationsReducer, + passkeys: passkeysReducer, passwordLock: passwordLockReducer, reKeyAccount: reKeyAccountReducer, removeAssets: removeAssetsReducer, diff --git a/src/extension/apps/main/Root.tsx b/src/extension/apps/main/Root.tsx index 9dedb1c4..f445e6e9 100644 --- a/src/extension/apps/main/Root.tsx +++ b/src/extension/apps/main/Root.tsx @@ -27,6 +27,7 @@ import { } from '@extension/features/networks'; import { fetchFromStorageThunk as fetchNewsFromStorageThunk } from '@extension/features/news'; import { setShowingConfetti } from '@extension/features/notifications'; +import { fetchFromStorageThunk as fetchPasskeyCredentialFromStorageThunk } from '@extension/features/passkeys'; import { reset as resetReKeyAccount } from '@extension/features/re-key-account'; import { reset as resetRemoveAssets } from '@extension/features/remove-assets'; import { reset as resetSendAsset } from '@extension/features/send-assets'; @@ -50,6 +51,7 @@ import useNotifications from '@extension/hooks/useNotifications'; import AddAssetsModal, { AddAssetsForWatchAccountModal, } from '@extension/modals/AddAssetsModal'; +import AddPasskeyModal from '@extension/modals/AddPasskeyModal'; import ARC0300KeyRegistrationTransactionSendEventModal from '@extension/modals/ARC0300KeyRegistrationTransactionSendEventModal'; import ConfirmModal from '@extension/modals/ConfirmModal'; import EnableModal from '@extension/modals/EnableModal'; @@ -65,7 +67,7 @@ import WalletConnectModal from '@extension/modals/WalletConnectModal'; // selectors import { useSelectAccounts, - useSelectPasswordLockPassword, + useSelectPasswordLockCredentials, useSelectNotificationsShowingConfetti, useSelectSelectedNetwork, useSelectSettings, @@ -79,7 +81,7 @@ const Root: FC = () => { const navigate = useNavigate(); // selectors const accounts = useSelectAccounts(); - const passwordLockPassword = useSelectPasswordLockPassword(); + const passwordLockPassword = useSelectPasswordLockCredentials(); const selectedNetwork = useSelectSelectedNetwork(); const settings = useSelectSettings(); const showingConfetti = useSelectNotificationsShowingConfetti(); @@ -98,6 +100,7 @@ const Root: FC = () => { useEffect(() => { dispatch(fetchSystemInfoFromStorageThunk()); dispatch(fetchSettingsFromStorageThunk()); + dispatch(fetchPasskeyCredentialFromStorageThunk()); dispatch(fetchSessionsThunk()); dispatch(fetchStandardAssetsFromStorageThunk()); dispatch(fetchARC0072AssetsFromStorageThunk()); diff --git a/src/extension/components/ARC0300AccountImportWithAddressModalContent/ARC0300AccountImportWithAddressModalContent.tsx b/src/extension/components/ARC0300AccountImportWithAddressModalContent/ARC0300AccountImportWithAddressModalContent.tsx index d928722b..64443cdf 100644 --- a/src/extension/components/ARC0300AccountImportWithAddressModalContent/ARC0300AccountImportWithAddressModalContent.tsx +++ b/src/extension/components/ARC0300AccountImportWithAddressModalContent/ARC0300AccountImportWithAddressModalContent.tsx @@ -22,6 +22,7 @@ import ModalSkeletonItem from '@extension/components/ModalSkeletonItem'; import ModalItem from '@extension/components/ModalItem'; import ModalTextItem from '@extension/components/ModalTextItem'; import ModalSubHeading from '@extension/components/ModalSubHeading'; +import WatchAccountBadge from '@extension/components/WatchAccountBadge'; // constants import { @@ -56,7 +57,7 @@ import { } from '@extension/selectors'; // services -import AccountService from '@extension/services/AccountService'; +import PrivateKeyService from '@extension/services/PrivateKeyService'; import QuestsService from '@extension/services/QuestsService'; // theme @@ -72,8 +73,8 @@ import type { } from '@extension/types'; // utils +import convertPublicKeyToAVMAddress from '@extension/utils/convertPublicKeyToAVMAddress'; import ellipseAddress from '@extension/utils/ellipseAddress'; -import WatchAccountBadge from '@extension/components/WatchAccountBadge'; const ARC0300AccountImportWithAddressModalContent: FC< IARC0300ModalContentProps< @@ -158,8 +159,8 @@ const ARC0300AccountImportWithAddressModalContent: FC< ephemeral: true, description: t('captions.addedAccount', { address: ellipseAddress( - AccountService.convertPublicKeyToAlgorandAddress( - account.publicKey + convertPublicKeyToAVMAddress( + PrivateKeyService.decode(account.publicKey) ) ), }), @@ -174,7 +175,9 @@ const ARC0300AccountImportWithAddressModalContent: FC< // track the action await questsService.importAccountViaQRCodeQuest( - AccountService.convertPublicKeyToAlgorandAddress(account.publicKey) + convertPublicKeyToAVMAddress( + PrivateKeyService.decode(account.publicKey) + ) ); // go to the account and the assets tab diff --git a/src/extension/components/ARC0300AccountImportWithPrivateKeyModalContent/ARC0300AccountImportWithPrivateKeyModalContent.tsx b/src/extension/components/ARC0300AccountImportWithPrivateKeyModalContent/ARC0300AccountImportWithPrivateKeyModalContent.tsx index 900e576b..52f77a27 100644 --- a/src/extension/components/ARC0300AccountImportWithPrivateKeyModalContent/ARC0300AccountImportWithPrivateKeyModalContent.tsx +++ b/src/extension/components/ARC0300AccountImportWithPrivateKeyModalContent/ARC0300AccountImportWithPrivateKeyModalContent.tsx @@ -6,9 +6,10 @@ import { ModalFooter, ModalHeader, Text, + useDisclosure, VStack, } from '@chakra-ui/react'; -import React, { FC, KeyboardEvent, useEffect, useRef, useState } from 'react'; +import React, { FC, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useDispatch } from 'react-redux'; import { useNavigate } from 'react-router-dom'; @@ -22,9 +23,6 @@ import ModalSkeletonItem from '@extension/components/ModalSkeletonItem'; import ModalItem from '@extension/components/ModalItem'; import ModalTextItem from '@extension/components/ModalTextItem'; import ModalSubHeading from '@extension/components/ModalSubHeading'; -import PasswordInput, { - usePassword, -} from '@extension/components/PasswordInput'; // constants import { @@ -40,6 +38,9 @@ import { ErrorCodeEnum, } from '@extension/enums'; +// errors +import { BaseExtensionError } from '@extension/errors'; + // features import { addARC0200AssetHoldingsThunk, @@ -55,17 +56,23 @@ import usePrimaryButtonTextColor from '@extension/hooks/usePrimaryButtonTextColo import useSubTextColor from '@extension/hooks/useSubTextColor'; import useUpdateARC0200Assets from '@extension/hooks/useUpdateARC0200Assets'; +// modals +import AuthenticationModal, { + TOnConfirmResult, +} from '@extension/modals/AuthenticationModal'; + +// models +import Ed21559KeyPair from '@extension/models/Ed21559KeyPair'; + // selectors import { useSelectActiveAccountDetails, useSelectLogger, - useSelectPasswordLockPassword, useSelectSelectedNetwork, - useSelectSettings, } from '@extension/selectors'; // services -import AccountService from '@extension/services/AccountService'; +import PrivateKeyService from '@extension/services/PrivateKeyService'; import QuestsService from '@extension/services/QuestsService'; // theme @@ -81,7 +88,8 @@ import type { } from '@extension/types'; // utils -import convertPrivateKeyToAddress from '@extension/utils/convertPrivateKeyToAddress'; +import convertPrivateKeyToAVMAddress from '@extension/utils/convertPrivateKeyToAVMAddress'; +import convertPublicKeyToAVMAddress from '@extension/utils/convertPublicKeyToAVMAddress'; import ellipseAddress from '@extension/utils/ellipseAddress'; import decodePrivateKeyFromAccountImportSchema from '@extension/utils/decodePrivateKeyFromImportKeySchema'; @@ -93,13 +101,15 @@ const ARC0300AccountImportWithPrivateKeyModalContent: FC< const { t } = useTranslation(); const dispatch = useDispatch(); const navigate = useNavigate(); - const passwordInputRef = useRef(null); + const { + isOpen: isAuthenticationModalOpen, + onClose: onAuthenticationModalClose, + onOpen: onAuthenticationModalOpen, + } = useDisclosure(); // selectors const activeAccountDetails = useSelectActiveAccountDetails(); const logger = useSelectLogger(); const network = useSelectSelectedNetwork(); - const passwordLockPassword = useSelectPasswordLockPassword(); - const settings = useSelectSettings(); // hooks const defaultTextColor = useDefaultTextColor(); const { @@ -107,71 +117,31 @@ const ARC0300AccountImportWithPrivateKeyModalContent: FC< loading, reset: resetUpdateAssets, } = useUpdateARC0200Assets(schema.query[ARC0300QueryEnum.Asset]); - const { - error: passwordError, - onChange: onPasswordChange, - reset: resetPassword, - setError: setPasswordError, - validate: validatePassword, - value: password, - } = usePassword(); const primaryButtonTextColor = usePrimaryButtonTextColor(); const subTextColor = useSubTextColor(); // states const [address, setAddress] = useState(null); const [saving, setSaving] = useState(false); + // misc + const reset = () => { + resetUpdateAssets(); + setSaving(false); + }; // handlers const handleCancelClick = () => { reset(); onCancel(); }; - const handleImportClick = async () => { - const _functionName: string = 'handleImportClick'; - let _password: string | null; + const handleImportClick = () => onAuthenticationModalOpen(); + const handleOnAuthenticationModalConfirm = async ( + result: TOnConfirmResult + ) => { + const _functionName = 'handleOnAuthenticationModalConfirm'; + const privateKey: Uint8Array | null = + decodePrivateKeyFromAccountImportSchema(schema); let account: IAccount | null; let questsService: QuestsService; - let privateKey: Uint8Array | null; - let result: IUpdateAssetHoldingsResult; - - // if there is no password lock - if (!settings.security.enablePasswordLock && !passwordLockPassword) { - // validate the password input - if (validatePassword()) { - logger.debug( - `${ARC0300AccountImportWithPrivateKeyModalContent.name}#${_functionName}: password not valid` - ); - - return; - } - } - - _password = settings.security.enablePasswordLock - ? passwordLockPassword - : password; - - if (!_password) { - logger.debug( - `${ARC0300AccountImportWithPrivateKeyModalContent.name}#${_functionName}: unable to use password from password lock, value is "null"` - ); - - dispatch( - createNotification({ - description: t('errors.descriptions.code', { - context: ErrorCodeEnum.ParsingError, - type: 'password', - }), - ephemeral: true, - title: t('errors.titles.code', { - context: ErrorCodeEnum.ParsingError, - }), - type: 'error', - }) - ); - - return; - } - - privateKey = decodePrivateKeyFromAccountImportSchema(schema); + let updateAssetHoldingsResult: IUpdateAssetHoldingsResult; if (!privateKey) { logger.debug( @@ -200,15 +170,15 @@ const ARC0300AccountImportWithPrivateKeyModalContent: FC< try { account = await dispatch( saveNewAccountThunk({ + keyPair: Ed21559KeyPair.generateFromPrivateKey(privateKey), name: null, - password: _password, - privateKey, + ...result, }) ).unwrap(); // if there are assets, add them to the new account if (assets.length > 0 && network) { - result = await dispatch( + updateAssetHoldingsResult = await dispatch( addARC0200AssetHoldingsThunk({ accountId: account.id, assets, @@ -216,14 +186,10 @@ const ARC0300AccountImportWithPrivateKeyModalContent: FC< }) ).unwrap(); - account = result.account; + account = updateAssetHoldingsResult.account; } } catch (error) { switch (error.code) { - case ErrorCodeEnum.InvalidPasswordError: - setPasswordError(t('errors.inputs.invalidPassword')); - - break; case ErrorCodeEnum.PrivateKeyAlreadyExistsError: logger.debug( `${ARC0300AccountImportWithPrivateKeyModalContent.name}#${_functionName}: account already exists, carry on` @@ -259,8 +225,8 @@ const ARC0300AccountImportWithPrivateKeyModalContent: FC< ephemeral: true, description: t('captions.addedAccount', { address: ellipseAddress( - AccountService.convertPublicKeyToAlgorandAddress( - account.publicKey + convertPublicKeyToAVMAddress( + PrivateKeyService.decode(account.publicKey) ) ), }), @@ -275,7 +241,9 @@ const ARC0300AccountImportWithPrivateKeyModalContent: FC< // track the action await questsService.importAccountViaQRCodeQuest( - AccountService.convertPublicKeyToAlgorandAddress(account.publicKey) + convertPublicKeyToAVMAddress( + PrivateKeyService.decode(account.publicKey) + ) ); // go to the account and the assets tab @@ -291,139 +259,137 @@ const ARC0300AccountImportWithPrivateKeyModalContent: FC< // clean up and close handleOnComplete(); }; - const handleKeyUpPasswordInput = async ( - event: KeyboardEvent - ) => { - if (event.key === 'Enter') { - await handleImportClick(); - } - }; + const handleOnAuthenticationError = (error: BaseExtensionError) => + dispatch( + createNotification({ + description: t('errors.descriptions.code', { + code: error.code, + context: error.code, + }), + ephemeral: true, + title: t('errors.titles.code', { context: error.code }), + type: 'error', + }) + ); const handleOnComplete = () => { reset(); onComplete(); }; - const reset = () => { - resetPassword(); - resetUpdateAssets(); - setSaving(false); - }; - useEffect(() => { - if (passwordInputRef.current) { - passwordInputRef.current.focus(); - } - }, []); useEffect(() => { const privateKey: Uint8Array | null = decodePrivateKeyFromAccountImportSchema(schema); if (privateKey) { - setAddress(convertPrivateKeyToAddress(privateKey)); + setAddress(convertPrivateKeyToAVMAddress(privateKey)); } }, []); return ( - - {/*header*/} - - - {t('headings.importAccount')} - - - - {/*body*/} - - - - {t('captions.importAccount')} - - - - ('labels.account')} /> - - {/*address*/} - {!address ? ( - - ) : ( - ('labels.address')}:`} - tooltipLabel={address} - value={ellipseAddress(address, { - end: 10, - start: 10, - })} - /> - )} - + <> + {/*authentication modal*/} + ('captions.mustEnterPasswordToImportAccount')} + /> + + + {/*header*/} + + + {t('headings.importAccount')} + + + + {/*body*/} + + + + {t('captions.importAccount')} + - {/*assets*/} - {loading && ( - - - - - )} - {assets.length > 0 && !loading && ( - - ('labels.assets')} /> - - {assets.map((value, index) => ( - - {/*icon*/} - - } - size="xs" - /> - - {/*symbol*/} - - {value.symbol} - - - {/*type*/} - - - } + ('labels.account')} /> + + {/*address*/} + {!address ? ( + + ) : ( + ('labels.address')}:`} + tooltipLabel={address} + value={ellipseAddress(address, { + end: 10, + start: 10, + })} /> - ))} + )} - )} - - - - {/*footer*/} - - - {!settings.security.enablePasswordLock && !passwordLockPassword && ( - ('captions.mustEnterPasswordToImportAccount')} - onChange={onPasswordChange} - onKeyUp={handleKeyUpPasswordInput} - inputRef={passwordInputRef} - value={password} - /> - )} + {/*assets*/} + {loading && ( + + + + + + )} + {assets.length > 0 && !loading && ( + + ('labels.assets')} /> + + {assets.map((value, index) => ( + + {/*icon*/} + + } + size="xs" + /> + + {/*symbol*/} + + {value.symbol} + + + {/*type*/} + + + } + /> + ))} + + )} + + + + {/*footer*/} + {/*cancel button*/} - - - + + + ); }; diff --git a/src/extension/components/ARC0300KeyRegistrationTransactionSendModalContent/ARC0300KeyRegistrationTransactionSendModalContent.tsx b/src/extension/components/ARC0300KeyRegistrationTransactionSendModalContent/ARC0300KeyRegistrationTransactionSendModalContent.tsx index 6803d87c..1e5dd13c 100644 --- a/src/extension/components/ARC0300KeyRegistrationTransactionSendModalContent/ARC0300KeyRegistrationTransactionSendModalContent.tsx +++ b/src/extension/components/ARC0300KeyRegistrationTransactionSendModalContent/ARC0300KeyRegistrationTransactionSendModalContent.tsx @@ -14,7 +14,7 @@ import { encode as encodeBase64, } from '@stablelib/base64'; import { Transaction } from 'algosdk'; -import React, { FC, KeyboardEvent, useEffect, useRef, useState } from 'react'; +import React, { FC, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useDispatch } from 'react-redux'; @@ -22,9 +22,6 @@ import { useDispatch } from 'react-redux'; import Button from '@extension/components/Button'; import KeyRegistrationTransactionModalBody from '@extension/components/KeyRegistrationTransactionModalBody'; import ModalSkeletonItem from '@extension/components/ModalSkeletonItem'; -import PasswordInput, { - usePassword, -} from '@extension/components/PasswordInput'; // constants import { BODY_BACKGROUND_COLOR, DEFAULT_GAP } from '@extension/constants'; @@ -32,12 +29,16 @@ import { BODY_BACKGROUND_COLOR, DEFAULT_GAP } from '@extension/constants'; // enums import { ARC0300QueryEnum, + EncryptionMethodEnum, ErrorCodeEnum, TransactionTypeEnum, } from '@extension/enums'; // errors -import { NotEnoughMinimumBalanceError } from '@extension/errors'; +import { + BaseExtensionError, + NotEnoughMinimumBalanceError, +} from '@extension/errors'; // features import { updateAccountsThunk } from '@extension/features/accounts'; @@ -46,13 +47,17 @@ import { create as createNotification } from '@extension/features/notifications' // hooks import useDefaultTextColor from '@extension/hooks/useDefaultTextColor'; +// modals +import AuthenticationModal, { + TOnConfirmResult, +} from '@extension/modals/AuthenticationModal'; + // selectors import { useSelectAccountByAddress, useSelectAccounts, useSelectLogger, useSelectNetworks, - useSelectPasswordLockPassword, useSelectSettings, } from '@extension/selectors'; @@ -82,28 +87,27 @@ const ARC0300KeyRegistrationTransactionSendModalContent: FC< > > = ({ cancelButtonIcon, cancelButtonLabel, onComplete, onCancel, schema }) => { const { t } = useTranslation(); - const passwordInputRef = useRef(null); const dispatch: IAppThunkDispatch = useDispatch(); - const { isOpen, onOpen, onClose } = useDisclosure(); + const { + isOpen: isAuthenticationModalOpen, + onClose: onAuthenticationModalClose, + onOpen: onAuthenticationModalOpen, + } = useDisclosure(); + const { + isOpen: isMoreInformationToggleOpen, + onOpen: onMoreInformationOpen, + onClose: onMoreInformationClose, + } = useDisclosure(); // selectors const account = useSelectAccountByAddress( schema.query[ARC0300QueryEnum.Sender] ); const accounts = useSelectAccounts(); const logger = useSelectLogger(); - const passwordLockPassword = useSelectPasswordLockPassword(); const networks = useSelectNetworks(); const settings = useSelectSettings(); // hooks const defaultTextColor: string = useDefaultTextColor(); - const { - error: passwordError, - onChange: onPasswordChange, - reset: resetPassword, - setError: setPasswordError, - validate: validatePassword, - value: password, - } = usePassword(); // states const [sending, setSending] = useState(false); const [unsignedTransaction, setUnsignedTransaction] = @@ -129,20 +133,12 @@ const ARC0300KeyRegistrationTransactionSendModalContent: FC< : selectNetworkFromSettings(networks, settings) || selectDefaultNetwork(networks); // if we have the genesis hash get the network, otherwise get the selected network const reset = () => { - resetPassword(); setSending(false); setUnsignedTransaction(null); }; // handlers - const handleKeyUpPasswordInput = async ( - event: KeyboardEvent - ) => { - if (event.key === 'Enter') { - await handleSendClick(); - } - }; const handleMoreInformationToggle = (value: boolean) => - value ? onOpen() : onClose(); + value ? onMoreInformationOpen() : onMoreInformationClose(); const handleOnComplete = () => { reset(); onComplete(); @@ -151,9 +147,22 @@ const ARC0300KeyRegistrationTransactionSendModalContent: FC< reset(); onCancel(); }; - const handleSendClick = async () => { - const _functionName: string = 'handleSendClick'; - let _password: string | null; + const handleError = (error: BaseExtensionError) => + dispatch( + createNotification({ + description: t('errors.descriptions.code', { + code: error.code, + context: error.code, + }), + ephemeral: true, + title: t('errors.titles.code', { context: error.code }), + type: 'error', + }) + ); + const handleOnAuthenticationModalConfirm = async ( + result: TOnConfirmResult + ) => { + const _functionName = 'handleOnAuthenticationModalConfirm'; let signedTransaction: Uint8Array; if (!unsignedTransaction) { @@ -184,30 +193,6 @@ const ARC0300KeyRegistrationTransactionSendModalContent: FC< return; } - // if there is no password lock - if (!settings.security.enablePasswordLock && !passwordLockPassword) { - // validate the password input - if (validatePassword()) { - logger.debug( - `${ARC0300KeyRegistrationTransactionSendModalContent.name}#${_functionName}: password not valid` - ); - - return; - } - } - - _password = settings.security.enablePasswordLock - ? passwordLockPassword - : password; - - if (!_password) { - logger.debug( - `${ARC0300KeyRegistrationTransactionSendModalContent.name}#${_functionName}: unable to use password from password lock, value is "null"` - ); - - return; - } - setSending(true); try { @@ -229,8 +214,16 @@ const ARC0300KeyRegistrationTransactionSendModalContent: FC< authAccounts: accounts, logger, networks, - password, unsignedTransaction, + ...(result.type === EncryptionMethodEnum.Password + ? { + password: result.password, + type: EncryptionMethodEnum.Password, + } + : { + inputKeyMaterial: result.inputKeyMaterial, + type: EncryptionMethodEnum.Passkey, + }), }); await sendTransactionsForNetwork({ @@ -269,9 +262,6 @@ const ARC0300KeyRegistrationTransactionSendModalContent: FC< handleOnComplete(); } catch (error) { switch (error.code) { - case ErrorCodeEnum.InvalidPasswordError: - setPasswordError(t('errors.inputs.invalidPassword')); - break; case ErrorCodeEnum.OfflineError: dispatch( createNotification({ @@ -299,12 +289,8 @@ const ARC0300KeyRegistrationTransactionSendModalContent: FC< setSending(false); }; + const handleSendClick = () => onAuthenticationModalOpen(); - useEffect(() => { - if (passwordInputRef.current) { - passwordInputRef.current.focus(); - } - }, []); useEffect(() => { if (account && network) { (async () => @@ -319,68 +305,71 @@ const ARC0300KeyRegistrationTransactionSendModalContent: FC< }, [account, network, schema]); return ( - - {/*header*/} - - - {t( - isOnlineKeyRegistrationTransaction(schema) - ? `headings.transaction_${TransactionTypeEnum.KeyRegistrationOnline}` - : `headings.transaction_${TransactionTypeEnum.KeyRegistrationOffline}` - )} - - - - {/*body*/} - - - - {t('captions.keyRegistrationURI', { - status: isOnlineKeyRegistrationTransaction(schema) - ? 'online' - : 'offline', - })} - - - {!account || !network || !unsignedTransaction ? ( - - - - - - ) : ( - - )} - - - - {/*footer*/} - - - {!settings.security.enablePasswordLock && !passwordLockPassword && ( - ('captions.mustEnterPasswordToSendTransaction')} - onChange={onPasswordChange} - onKeyUp={handleKeyUpPasswordInput} - inputRef={passwordInputRef} - value={password} - /> - )} - + <> + {/*authentication*/} + ('captions.mustEnterPasswordToSendTransaction')} + /> + + + {/*header*/} + + + {t( + isOnlineKeyRegistrationTransaction(schema) + ? `headings.transaction_${TransactionTypeEnum.KeyRegistrationOnline}` + : `headings.transaction_${TransactionTypeEnum.KeyRegistrationOffline}` + )} + + + + {/*body*/} + + + + {t('captions.keyRegistrationURI', { + status: isOnlineKeyRegistrationTransaction(schema) + ? 'online' + : 'offline', + })} + + + {!account || !network || !unsignedTransaction ? ( + + + + + + ) : ( + + )} + + + + {/*footer*/} + {/*cancel button*/} - - - + + + ); }; diff --git a/src/extension/components/AccountItem/AccountItem.tsx b/src/extension/components/AccountItem/AccountItem.tsx index 4aefe3b3..8fe42f55 100644 --- a/src/extension/components/AccountItem/AccountItem.tsx +++ b/src/extension/components/AccountItem/AccountItem.tsx @@ -12,12 +12,13 @@ import useDefaultTextColor from '@extension/hooks/useDefaultTextColor'; import useSubTextColor from '@extension/hooks/useSubTextColor'; // services -import AccountService from '@extension/services/AccountService'; +import PrivateKeyService from '@extension/services/PrivateKeyService'; // types import type { IProps } from './types'; // utils +import convertPublicKeyToAVMAddress from '@extension/utils/convertPublicKeyToAVMAddress'; import ellipseAddress from '@extension/utils/ellipseAddress'; const AccountItem: FC = ({ account, subTextColor, textColor }) => { @@ -25,8 +26,8 @@ const AccountItem: FC = ({ account, subTextColor, textColor }) => { const defaultSubTextColor = useSubTextColor(); const defaultTextColor = useDefaultTextColor(); // misc - const address = AccountService.convertPublicKeyToAlgorandAddress( - account.publicKey + const address = convertPublicKeyToAVMAddress( + PrivateKeyService.decode(account.publicKey) ); return ( diff --git a/src/extension/components/AccountSelect/AccountSelect.tsx b/src/extension/components/AccountSelect/AccountSelect.tsx index 32a5de28..6c1d8731 100644 --- a/src/extension/components/AccountSelect/AccountSelect.tsx +++ b/src/extension/components/AccountSelect/AccountSelect.tsx @@ -13,7 +13,7 @@ import { BODY_BACKGROUND_COLOR, OPTION_HEIGHT } from '@extension/constants'; import useColorModeValue from '@extension/hooks/useColorModeValue'; // services -import AccountService from '@extension/services/AccountService'; +import PrivateKeyService from '@extension/services/PrivateKeyService'; // theme import { theme } from '@extension/theme'; @@ -21,6 +21,9 @@ import { theme } from '@extension/theme'; // types import type { IOption, IProps } from './types'; +// utils +import convertPublicKeyToAVMAddress from '@extension/utils/convertPublicKeyToAVMAddress'; + const AccountSelect: FC = ({ accounts, disabled = false, @@ -51,7 +54,7 @@ const AccountSelect: FC = ({ { data }: FilterOptionOption, inputValue: string ) => - AccountService.convertPublicKeyToAlgorandAddress(data.value.publicKey) + convertPublicKeyToAVMAddress(PrivateKeyService.decode(data.value.publicKey)) .toUpperCase() .includes(inputValue) || (!!data.value.name && data.value.name.toUpperCase().includes(inputValue)); diff --git a/src/extension/components/AddressDisplay/AddressDisplay.tsx b/src/extension/components/AddressDisplay/AddressDisplay.tsx index f15a7966..e1c996af 100644 --- a/src/extension/components/AddressDisplay/AddressDisplay.tsx +++ b/src/extension/components/AddressDisplay/AddressDisplay.tsx @@ -10,13 +10,14 @@ import useColorModeValue from '@extension/hooks/useColorModeValue'; import { useSelectSettingsColorMode } from '@extension/selectors'; // services -import AccountService from '@extension/services/AccountService'; +import PrivateKeyService from '@extension/services/PrivateKeyService'; // types import type { IAccount } from '@extension/types'; import type { IProps } from './types'; // utils +import convertPublicKeyToAVMAddress from '@extension/utils/convertPublicKeyToAVMAddress'; import ellipseAddress from '@extension/utils/ellipseAddress'; const AddressDisplay: FC = ({ @@ -35,8 +36,9 @@ const AddressDisplay: FC = ({ const account: IAccount | null = accounts.find( (value) => - AccountService.convertPublicKeyToAlgorandAddress(value.publicKey) === - address + convertPublicKeyToAVMAddress( + PrivateKeyService.decode(value.publicKey) + ) === address ) || null; if (account) { @@ -54,8 +56,8 @@ const AddressDisplay: FC = ({ ) : ( {ellipseAddress( - AccountService.convertPublicKeyToAlgorandAddress( - account.publicKey + convertPublicKeyToAVMAddress( + PrivateKeyService.decode(account.publicKey) ) )} diff --git a/src/extension/components/AddressInput/AddressInput.tsx b/src/extension/components/AddressInput/AddressInput.tsx index 28816793..3d6e8baa 100644 --- a/src/extension/components/AddressInput/AddressInput.tsx +++ b/src/extension/components/AddressInput/AddressInput.tsx @@ -25,12 +25,15 @@ import useDefaultTextColor from '@extension/hooks/useDefaultTextColor'; import usePrimaryColor from '@extension/hooks/usePrimaryColor'; // services -import AccountService from '@extension/services/AccountService'; +import PrivateKeyService from '@extension/services/PrivateKeyService'; // types import type { IAccountWithExtendedProps } from '@extension/types'; import type { IProps } from './types'; +// utils +import convertPublicKeyToAVMAddress from '@extension/utils/convertPublicKeyToAVMAddress'; + const AddressInput: FC = ({ accounts, disabled, @@ -48,7 +51,7 @@ const AddressInput: FC = ({ // handlers const handleAccountClick = (account: IAccountWithExtendedProps) => () => onChange( - AccountService.convertPublicKeyToAlgorandAddress(account.publicKey) + convertPublicKeyToAVMAddress(PrivateKeyService.decode(account.publicKey)) ); const handleHandleOnChange = (event: ChangeEvent) => onChange(event.target.value); diff --git a/src/extension/components/AnimatedKibisisIcon/AnimatedKibisisIcon.tsx b/src/extension/components/AnimatedKibisisIcon/AnimatedKibisisIcon.tsx new file mode 100644 index 00000000..8c405072 --- /dev/null +++ b/src/extension/components/AnimatedKibisisIcon/AnimatedKibisisIcon.tsx @@ -0,0 +1,24 @@ +import { Icon, IconProps } from '@chakra-ui/react'; +import React, { FC } from 'react'; + +const AnimatedKibisisIcon: FC = (props: IconProps) => ( + + + + + +); + +export default AnimatedKibisisIcon; diff --git a/src/extension/components/AnimatedKibisisIcon/index.ts b/src/extension/components/AnimatedKibisisIcon/index.ts new file mode 100644 index 00000000..7069e771 --- /dev/null +++ b/src/extension/components/AnimatedKibisisIcon/index.ts @@ -0,0 +1 @@ +export { default } from './AnimatedKibisisIcon'; diff --git a/src/extension/components/COSEAlgorithmBadge/COSEAlgorithmBadge.tsx b/src/extension/components/COSEAlgorithmBadge/COSEAlgorithmBadge.tsx new file mode 100644 index 00000000..03add28d --- /dev/null +++ b/src/extension/components/COSEAlgorithmBadge/COSEAlgorithmBadge.tsx @@ -0,0 +1,47 @@ +import { ColorMode, Tag, TagLabel } from '@chakra-ui/react'; +import React, { FC } from 'react'; +import { useTranslation } from 'react-i18next'; + +// selectors +import { useSelectSettingsColorMode } from '@extension/selectors'; + +// types +import type { IProps } from './types'; + +const COSEAlgorithmBadge: FC = ({ algorithm, size = 'sm' }: IProps) => { + const { t } = useTranslation(); + // hooks + const colorMode: ColorMode = useSelectSettingsColorMode(); + // misc + let colorScheme = 'orange'; + let label = t('labels.unknown'); + + switch (algorithm) { + case -7: + colorScheme = 'blue'; + label = 'ES256'; + break; + case -8: + colorScheme = 'blue'; + label = 'Ed25519'; + break; + case -257: + colorScheme = 'blue'; + label = 'RS256'; + break; + default: + break; + } + + return ( + + {label} + + ); +}; + +export default COSEAlgorithmBadge; diff --git a/src/extension/components/COSEAlgorithmBadge/index.ts b/src/extension/components/COSEAlgorithmBadge/index.ts new file mode 100644 index 00000000..d133d34e --- /dev/null +++ b/src/extension/components/COSEAlgorithmBadge/index.ts @@ -0,0 +1 @@ +export { default } from './COSEAlgorithmBadge'; diff --git a/src/extension/components/COSEAlgorithmBadge/types/IProps.ts b/src/extension/components/COSEAlgorithmBadge/types/IProps.ts new file mode 100644 index 00000000..d22a88e6 --- /dev/null +++ b/src/extension/components/COSEAlgorithmBadge/types/IProps.ts @@ -0,0 +1,6 @@ +interface IProps { + algorithm: number; + size?: string; +} + +export default IProps; diff --git a/src/extension/components/COSEAlgorithmBadge/types/index.ts b/src/extension/components/COSEAlgorithmBadge/types/index.ts new file mode 100644 index 00000000..f404deed --- /dev/null +++ b/src/extension/components/COSEAlgorithmBadge/types/index.ts @@ -0,0 +1 @@ +export type { default as IProps } from './IProps'; diff --git a/src/extension/components/CircularProgressWithIcon/CircularProgressWithIcon.tsx b/src/extension/components/CircularProgressWithIcon/CircularProgressWithIcon.tsx new file mode 100644 index 00000000..52262f86 --- /dev/null +++ b/src/extension/components/CircularProgressWithIcon/CircularProgressWithIcon.tsx @@ -0,0 +1,57 @@ +import { + CircularProgress, + CircularProgressLabel, + Icon, +} from '@chakra-ui/react'; +import React, { FC } from 'react'; + +// hooks +import usePrimaryColor from '@extension/hooks/usePrimaryColor'; +import useSubTextColor from '@extension/hooks/useSubTextColor'; + +// types +import type { IProps } from './types'; + +// utils +import calculateIconSize from '@extension/utils/calculateIconSize'; + +const CircularProgressWithIcon: FC = ({ + icon, + iconColor, + progress, + progressColor, +}) => { + // hooks + const primaryColor = usePrimaryColor(); + const subTextColor = useSubTextColor(); + // misc + const iconSize = calculateIconSize('lg'); + + return ( + 0 ? (progress[0] / progress[1]) * 100 : 0, + })} + > + + + + + ); +}; + +export default CircularProgressWithIcon; diff --git a/src/extension/components/CircularProgressWithIcon/index.ts b/src/extension/components/CircularProgressWithIcon/index.ts new file mode 100644 index 00000000..5be2216f --- /dev/null +++ b/src/extension/components/CircularProgressWithIcon/index.ts @@ -0,0 +1,2 @@ +export { default } from './CircularProgressWithIcon'; +export * from './types'; diff --git a/src/extension/components/CircularProgressWithIcon/types/IProps.ts b/src/extension/components/CircularProgressWithIcon/types/IProps.ts new file mode 100644 index 00000000..ebe44dde --- /dev/null +++ b/src/extension/components/CircularProgressWithIcon/types/IProps.ts @@ -0,0 +1,17 @@ +import { IconType } from 'react-icons'; + +/** + * @property {IconType} icon - the icon to use in the centre. + * @property {string} iconColor - [optional] the color of the icon. Defaults to the default text color. + * @property {[number, number]} progress - [optional] a tuple where the first value is the count and the second value + * is the total. If this value is omitted, the progress will be indeterminate. + * @property {string} progressColor - [optional] the color of the progress bar. Defaults to the primary color. + */ +interface IProps { + icon: IconType; + iconColor?: string; + progress?: [number, number]; + progressColor?: string; +} + +export default IProps; diff --git a/src/extension/components/CircularProgressWithIcon/types/index.ts b/src/extension/components/CircularProgressWithIcon/types/index.ts new file mode 100644 index 00000000..f404deed --- /dev/null +++ b/src/extension/components/CircularProgressWithIcon/types/index.ts @@ -0,0 +1 @@ +export type { default as IProps } from './IProps'; diff --git a/src/extension/components/InnerTransactionAccordion/AssetTransferInnerTransactionAccordionItem.tsx b/src/extension/components/InnerTransactionAccordion/AssetTransferInnerTransactionAccordionItem.tsx index fc84176e..e63e783d 100644 --- a/src/extension/components/InnerTransactionAccordion/AssetTransferInnerTransactionAccordionItem.tsx +++ b/src/extension/components/InnerTransactionAccordion/AssetTransferInnerTransactionAccordionItem.tsx @@ -29,7 +29,7 @@ import useSubTextColor from '@extension/hooks/useSubTextColor'; import { useSelectSettingsPreferredBlockExplorer } from '@extension/selectors'; // services -import AccountService from '@extension/services/AccountService'; +import PrivateKeyService from '@extension/services/PrivateKeyService'; // types import type { @@ -39,6 +39,7 @@ import type { import type { IItemProps } from './types'; // utils +import convertPublicKeyToAVMAddress from '@extension/utils/convertPublicKeyToAVMAddress'; import isAccountKnown from '@extension/utils/isAccountKnown'; const AssetTransferInnerTransactionAccordionItem: FC< @@ -61,8 +62,9 @@ const AssetTransferInnerTransactionAccordionItem: FC< const defaultTextColor: string = useDefaultTextColor(); const subTextColor: string = useSubTextColor(); // misc - const accountAddress: string = - AccountService.convertPublicKeyToAlgorandAddress(account.publicKey); + const accountAddress: string = convertPublicKeyToAVMAddress( + PrivateKeyService.decode(account.publicKey) + ); const amount: BigNumber = new BigNumber(String(transaction.amount)); const explorer: IBlockExplorer | null = network.blockExplorers.find( diff --git a/src/extension/components/InnerTransactionAccordion/PaymentInnerTransactionAccordionItem.tsx b/src/extension/components/InnerTransactionAccordion/PaymentInnerTransactionAccordionItem.tsx index d2daab51..bf79e20c 100644 --- a/src/extension/components/InnerTransactionAccordion/PaymentInnerTransactionAccordionItem.tsx +++ b/src/extension/components/InnerTransactionAccordion/PaymentInnerTransactionAccordionItem.tsx @@ -24,14 +24,12 @@ import useDefaultTextColor from '@extension/hooks/useDefaultTextColor'; // selectors import { useSelectSettingsPreferredBlockExplorer } from '@extension/selectors'; -// services -import AccountService from '@extension/services/AccountService'; - // types import type { IBlockExplorer, IPaymentTransaction } from '@extension/types'; import type { IItemProps } from './types'; // utils +import convertPublicKeyToAVMAddress from '@extension/utils/convertPublicKeyToAVMAddress'; import createIconFromDataUri from '@extension/utils/createIconFromDataUri'; import isAccountKnown from '@extension/utils/isAccountKnown'; @@ -53,8 +51,9 @@ const PaymentInnerTransactionAccordionItem: FC< // hooks const defaultTextColor: string = useDefaultTextColor(); // misc - const accountAddress: string = - AccountService.convertPublicKeyToAlgorandAddress(account.publicKey); + const accountAddress: string = convertPublicKeyToAVMAddress( + account.publicKey + ); const amount: BigNumber = new BigNumber(String(transaction.amount)); const explorer: IBlockExplorer | null = network.blockExplorers.find( diff --git a/src/extension/components/ModalItem/ModalItem.tsx b/src/extension/components/ModalItem/ModalItem.tsx index c3567d73..1dbfc614 100644 --- a/src/extension/components/ModalItem/ModalItem.tsx +++ b/src/extension/components/ModalItem/ModalItem.tsx @@ -5,7 +5,7 @@ import React, { FC } from 'react'; import WarningIcon from '@extension/components/WarningIcon'; // constants -import { MODAL_ITEM_HEIGHT } from '@extension/constants'; +import { DEFAULT_GAP, MODAL_ITEM_HEIGHT } from '@extension/constants'; // hooks import useDefaultTextColor from '@extension/hooks/useDefaultTextColor'; @@ -28,16 +28,16 @@ const ModalItem: FC = ({ alignItems="center" justifyContent="space-between" minH={MODAL_ITEM_HEIGHT} - spacing={2} + spacing={DEFAULT_GAP / 3} w="full" {...stackProps} > {/*label*/} - + {label} - + {/*value*/} {tooltipLabel ? ( = ({ account }) => { const { t } = useTranslation(); // selectors @@ -108,7 +110,7 @@ const NFTsTab: FC = ({ account }) => { // if there are new assets acquired, track an action for each new asset newARC0072AssetHoldings.forEach(({ id }) => questsService.acquireARC0072Quest( - AccountService.convertPublicKeyToAlgorandAddress(account.publicKey), + convertPublicKeyToAVMAddress(account.publicKey), { appID: id, genesisHash: selectedNetwork.genesisHash, diff --git a/src/extension/components/PageItem/PageItem.tsx b/src/extension/components/PageItem/PageItem.tsx index 5ed8ffd4..ac3e63ed 100644 --- a/src/extension/components/PageItem/PageItem.tsx +++ b/src/extension/components/PageItem/PageItem.tsx @@ -17,7 +17,7 @@ const PageItem: FC = ({ ...stackProps }) => { // hooks - const defaultTextColor: string = useDefaultTextColor(); + const defaultTextColor = useDefaultTextColor(); return ( = ({ color, fontSize = 'md', text }) => { + // hooks + const subTextColor = useSubTextColor(); + + return ( + + {text} + + ); +}; + +export default PageSubHeading; diff --git a/src/extension/components/PageSubHeading/index.ts b/src/extension/components/PageSubHeading/index.ts new file mode 100644 index 00000000..722dfbb5 --- /dev/null +++ b/src/extension/components/PageSubHeading/index.ts @@ -0,0 +1,2 @@ +export { default } from './PageSubHeading'; +export * from './types'; diff --git a/src/extension/components/PageSubHeading/types/IProps.ts b/src/extension/components/PageSubHeading/types/IProps.ts new file mode 100644 index 00000000..85d12a6d --- /dev/null +++ b/src/extension/components/PageSubHeading/types/IProps.ts @@ -0,0 +1,10 @@ +import { ResponsiveValue } from '@chakra-ui/react'; +import * as CSS from 'csstype'; + +interface IProps { + color?: string; + fontSize?: ResponsiveValue; + text: string; +} + +export default IProps; diff --git a/src/extension/components/PageSubHeading/types/index.ts b/src/extension/components/PageSubHeading/types/index.ts new file mode 100644 index 00000000..f404deed --- /dev/null +++ b/src/extension/components/PageSubHeading/types/index.ts @@ -0,0 +1 @@ +export type { default as IProps } from './IProps'; diff --git a/src/extension/components/PasskeyCapabilities/PasskeyCapabilities.tsx b/src/extension/components/PasskeyCapabilities/PasskeyCapabilities.tsx new file mode 100644 index 00000000..fc8e9282 --- /dev/null +++ b/src/extension/components/PasskeyCapabilities/PasskeyCapabilities.tsx @@ -0,0 +1,65 @@ +import { HStack, Icon, Tooltip } from '@chakra-ui/react'; +import React, { FC } from 'react'; +import type { IconType } from 'react-icons'; +import { BsUsbSymbol } from 'react-icons/bs'; +import { IoBluetoothOutline, IoFingerPrintOutline } from 'react-icons/io5'; +import { LuNfc } from 'react-icons/lu'; + +// constants +import { DEFAULT_GAP } from '@extension/constants'; + +// hooks +import useSubTextColor from '@extension/hooks/useSubTextColor'; + +// types +import type { IProps } from './types'; + +// utils +import calculateIconSize from '@extension/utils/calculateIconSize'; + +const PasskeyCapabilities: FC = ({ capabilities, size = 'sm' }) => { + // hooks + const subTextColor = useSubTextColor(); + // misc + const iconSize = calculateIconSize(size); + + return ( + + {capabilities.map((value, index) => { + let icon: IconType; + let label: string; + + switch (value) { + case 'ble': + icon = IoBluetoothOutline; + label = 'Bluetooth'; + break; + case 'internal': + icon = IoFingerPrintOutline; + label = 'Internal'; + break; + case 'nfc': + icon = LuNfc; + label = 'NFC'; + break; + case 'usb': + icon = BsUsbSymbol; + label = 'USB'; + break; + default: + return null; + } + + return ( + + + + + + ); + })} + + ); +}; + +export default PasskeyCapabilities; diff --git a/src/extension/components/PasskeyCapabilities/index.ts b/src/extension/components/PasskeyCapabilities/index.ts new file mode 100644 index 00000000..380ba92b --- /dev/null +++ b/src/extension/components/PasskeyCapabilities/index.ts @@ -0,0 +1 @@ +export { default } from './PasskeyCapabilities'; diff --git a/src/extension/components/PasskeyCapabilities/types/IProps.ts b/src/extension/components/PasskeyCapabilities/types/IProps.ts new file mode 100644 index 00000000..de4686de --- /dev/null +++ b/src/extension/components/PasskeyCapabilities/types/IProps.ts @@ -0,0 +1,9 @@ +// types +import type { TSizes } from '@extension/types'; + +interface IProps { + capabilities: AuthenticatorTransport[]; + size?: TSizes; +} + +export default IProps; diff --git a/src/extension/components/PasskeyCapabilities/types/index.ts b/src/extension/components/PasskeyCapabilities/types/index.ts new file mode 100644 index 00000000..f404deed --- /dev/null +++ b/src/extension/components/PasskeyCapabilities/types/index.ts @@ -0,0 +1 @@ +export type { default as IProps } from './IProps'; diff --git a/src/extension/components/ReEncryptKeysLoadingContent/ReEncryptKeysLoadingContent.tsx b/src/extension/components/ReEncryptKeysLoadingContent/ReEncryptKeysLoadingContent.tsx new file mode 100644 index 00000000..42648c6b --- /dev/null +++ b/src/extension/components/ReEncryptKeysLoadingContent/ReEncryptKeysLoadingContent.tsx @@ -0,0 +1,66 @@ +import { Text, VStack } from '@chakra-ui/react'; +import React, { FC } from 'react'; +import { useTranslation } from 'react-i18next'; +import { IoLockClosedOutline, IoLockOpenOutline } from 'react-icons/io5'; + +// components +import CircularProgressWithIcon from '@extension/components/CircularProgressWithIcon'; + +// constants +import { DEFAULT_GAP } from '@extension/constants'; + +// hooks +import useDefaultTextColor from '@extension/hooks/useDefaultTextColor'; +import useSubTextColor from '@extension/hooks/useSubTextColor'; + +// types +import type { IProps } from './types'; + +const ReEncryptKeysLoadingContent: FC = ({ + encryptionProgressState, + fontSize = 'sm', +}) => { + const { t } = useTranslation(); + // hooks + const defaultTextColor = useDefaultTextColor(); + const subTextColor = useSubTextColor(); + // misc + const count = encryptionProgressState.filter( + ({ encrypted }) => encrypted + ).length; + const total = encryptionProgressState.length; + const incomplete = count < total || total <= 0; + + return ( + + {/*progress*/} + + + {/*caption*/} + + {t('captions.reEncryptingKeys', { + count, + total, + })} + + + ); +}; + +export default ReEncryptKeysLoadingContent; diff --git a/src/extension/components/ReEncryptKeysLoadingContent/index.ts b/src/extension/components/ReEncryptKeysLoadingContent/index.ts new file mode 100644 index 00000000..dcfa0e87 --- /dev/null +++ b/src/extension/components/ReEncryptKeysLoadingContent/index.ts @@ -0,0 +1,2 @@ +export { default } from './ReEncryptKeysLoadingContent'; +export * from './types'; diff --git a/src/extension/components/ReEncryptKeysLoadingContent/types/IEncryptionState.ts b/src/extension/components/ReEncryptKeysLoadingContent/types/IEncryptionState.ts new file mode 100644 index 00000000..d25023ac --- /dev/null +++ b/src/extension/components/ReEncryptKeysLoadingContent/types/IEncryptionState.ts @@ -0,0 +1,6 @@ +interface IEncryptionState { + id: string; + encrypted: boolean; +} + +export default IEncryptionState; diff --git a/src/extension/components/ReEncryptKeysLoadingContent/types/IProps.ts b/src/extension/components/ReEncryptKeysLoadingContent/types/IProps.ts new file mode 100644 index 00000000..26a253c9 --- /dev/null +++ b/src/extension/components/ReEncryptKeysLoadingContent/types/IProps.ts @@ -0,0 +1,12 @@ +import type { ResponsiveValue } from '@chakra-ui/react'; +import * as CSS from 'csstype'; + +// types +import type IEncryptionState from './IEncryptionState'; + +interface IProps { + encryptionProgressState: IEncryptionState[]; + fontSize?: ResponsiveValue; +} + +export default IProps; diff --git a/src/extension/components/ReEncryptKeysLoadingContent/types/index.ts b/src/extension/components/ReEncryptKeysLoadingContent/types/index.ts new file mode 100644 index 00000000..3abe3389 --- /dev/null +++ b/src/extension/components/ReEncryptKeysLoadingContent/types/index.ts @@ -0,0 +1,2 @@ +export type { default as IEncryptionState } from './IEncryptionState'; +export type { default as IProps } from './IProps'; diff --git a/src/extension/components/SettingsLinkItem/SettingsLinkItem.tsx b/src/extension/components/SettingsLinkItem/SettingsLinkItem.tsx index 1f8f2040..93953afe 100644 --- a/src/extension/components/SettingsLinkItem/SettingsLinkItem.tsx +++ b/src/extension/components/SettingsLinkItem/SettingsLinkItem.tsx @@ -1,8 +1,17 @@ -import { Button, HStack, Icon, Text } from '@chakra-ui/react'; +import { + Button, + HStack, + Icon, + Tag, + TagLabel, + Text, + VStack, +} from '@chakra-ui/react'; +import { encode as encodeHex } from '@stablelib/hex'; import React, { FC } from 'react'; -import { IconType } from 'react-icons'; import { IoChevronForward } from 'react-icons/io5'; import { Link } from 'react-router-dom'; +import { hash } from 'tweetnacl'; // constants import { DEFAULT_GAP, SETTINGS_ITEM_HEIGHT } from '@extension/constants'; @@ -11,15 +20,21 @@ import { DEFAULT_GAP, SETTINGS_ITEM_HEIGHT } from '@extension/constants'; import useButtonHoverBackgroundColor from '@extension/hooks/useButtonHoverBackgroundColor'; import useDefaultTextColor from '@extension/hooks/useDefaultTextColor'; -interface IProps { - icon: IconType; - label: string; - to: string; -} +// selectors +import { useSelectSettingsColorMode } from '@extension/selectors'; -const SettingsLinkItem: FC = ({ icon, label, to }: IProps) => { - const buttonHoverBackgroundColor: string = useButtonHoverBackgroundColor(); - const defaultTextColor: string = useDefaultTextColor(); +// types +import type { IProps } from './types'; + +const SettingsLinkItem: FC = ({ badges, icon, label, to }) => { + // selectors + const colorMode = useSelectSettingsColorMode(); + // hooks + const buttonHoverBackgroundColor = useButtonHoverBackgroundColor(); + const defaultTextColor = useDefaultTextColor(); + // misc + const iconSize = 6; + const labelHash = encodeHex(hash(new TextEncoder().encode(label)), true); return ( ); diff --git a/src/extension/components/SettingsLinkItem/types/IBadgeProps.ts b/src/extension/components/SettingsLinkItem/types/IBadgeProps.ts new file mode 100644 index 00000000..395b861a --- /dev/null +++ b/src/extension/components/SettingsLinkItem/types/IBadgeProps.ts @@ -0,0 +1,6 @@ +interface IBadgeProps { + colorScheme: string; + label: string; +} + +export default IBadgeProps; diff --git a/src/extension/components/SettingsLinkItem/types/IProps.ts b/src/extension/components/SettingsLinkItem/types/IProps.ts new file mode 100644 index 00000000..fd94d74b --- /dev/null +++ b/src/extension/components/SettingsLinkItem/types/IProps.ts @@ -0,0 +1,13 @@ +import type { IconType } from 'react-icons'; + +// types +import type IBadgeProps from './IBadgeProps'; + +interface IProps { + badges?: IBadgeProps[]; + icon: IconType; + label: string; + to: string; +} + +export default IProps; diff --git a/src/extension/components/SettingsLinkItem/types/index.ts b/src/extension/components/SettingsLinkItem/types/index.ts new file mode 100644 index 00000000..cd056309 --- /dev/null +++ b/src/extension/components/SettingsLinkItem/types/index.ts @@ -0,0 +1,2 @@ +export type { default as IProps } from './IProps'; +export type { default as IBadgeProps } from './IBadgeProps'; diff --git a/src/extension/components/SideBar/SideBar.tsx b/src/extension/components/SideBar/SideBar.tsx index 772d312a..709c556f 100644 --- a/src/extension/components/SideBar/SideBar.tsx +++ b/src/extension/components/SideBar/SideBar.tsx @@ -59,15 +59,15 @@ import { useSelectSelectedNetwork, } from '@extension/selectors'; -// services -import AccountService from '@extension/services/AccountService'; - // types import type { IAccountWithExtendedProps, IAppThunkDispatch, } from '@extension/types'; +// utils +import convertPublicKeyToAVMAddress from '@extension/utils/convertPublicKeyToAVMAddress'; + const SideBar: FC = () => { const { t } = useTranslation(); const dispatch = useDispatch(); @@ -139,9 +139,7 @@ const SideBar: FC = () => { dispatch( initializeSendAsset({ - fromAddress: AccountService.convertPublicKeyToAlgorandAddress( - fromAccount.publicKey - ), + fromAddress: convertPublicKeyToAVMAddress(fromAccount.publicKey), selectedAsset: network.nativeCurrency, // use native currency }) ); diff --git a/src/extension/components/SideBar/SideBarAccountItem.tsx b/src/extension/components/SideBar/SideBarAccountItem.tsx index 872fcfc6..850549ba 100644 --- a/src/extension/components/SideBar/SideBarAccountItem.tsx +++ b/src/extension/components/SideBar/SideBarAccountItem.tsx @@ -25,13 +25,11 @@ import useColorModeValue from '@extension/hooks/useColorModeValue'; import useDefaultTextColor from '@extension/hooks/useDefaultTextColor'; import useSubTextColor from '@extension/hooks/useSubTextColor'; -// services -import AccountService from '@extension/services/AccountService'; - // types import type { ISideBarAccountItemProps } from './types'; // utils +import convertPublicKeyToAVMAddress from '@extension/utils/convertPublicKeyToAVMAddress'; import ellipseAddress from '@extension/utils/ellipseAddress'; const SideBarAccountItem: FC = ({ @@ -47,9 +45,7 @@ const SideBarAccountItem: FC = ({ const subTextColor = useSubTextColor(); const activeBackground = useColorModeValue('gray.200', 'whiteAlpha.200'); // misc - const address = AccountService.convertPublicKeyToAlgorandAddress( - account.publicKey - ); + const address = convertPublicKeyToAVMAddress(account.publicKey); const activeProps: Partial = active ? { _hover: { diff --git a/src/extension/components/TransactionItem/ARC0200AssetTransferTransactionItemContent.tsx b/src/extension/components/TransactionItem/ARC0200AssetTransferTransactionItemContent.tsx index dd8115b7..3a968578 100644 --- a/src/extension/components/TransactionItem/ARC0200AssetTransferTransactionItemContent.tsx +++ b/src/extension/components/TransactionItem/ARC0200AssetTransferTransactionItemContent.tsx @@ -21,7 +21,7 @@ import { } from '@extension/selectors'; // services -import AccountService from '@extension/services/AccountService'; +import PrivateKeyService from '@extension/services/PrivateKeyService'; // types import type { @@ -30,6 +30,9 @@ import type { } from '@extension/types'; import type { IProps } from './types'; +// utils +import convertPublicKeyToAVMAddress from '@extension/utils/convertPublicKeyToAVMAddress'; + const ARC0200AssetTransferTransactionItemContent: FC< IProps > = ({ account, accounts, network, transaction }) => { @@ -44,8 +47,8 @@ const ARC0200AssetTransferTransactionItemContent: FC< const [asset, setAsset] = useState(null); // misc const amount = new BigNumber(String(transaction.amount)); - const senderAddress = AccountService.convertPublicKeyToAlgorandAddress( - account.publicKey + const senderAddress = convertPublicKeyToAVMAddress( + PrivateKeyService.decode(account.publicKey) ); useEffect(() => { diff --git a/src/extension/components/TransactionItem/AssetTransferTransactionItemContent.tsx b/src/extension/components/TransactionItem/AssetTransferTransactionItemContent.tsx index d717b7c9..b3a9a663 100644 --- a/src/extension/components/TransactionItem/AssetTransferTransactionItemContent.tsx +++ b/src/extension/components/TransactionItem/AssetTransferTransactionItemContent.tsx @@ -13,12 +13,15 @@ import useStandardAssetById from '@extension/hooks/useStandardAssetById'; import useSubTextColor from '@extension/hooks/useSubTextColor'; // services -import AccountService from '@extension/services/AccountService'; +import PrivateKeyService from '@extension/services/PrivateKeyService'; // types import type { IAssetTransferTransaction } from '@extension/types'; import type { IProps } from './types'; +// utils +import convertPublicKeyToAVMAddress from '@extension/utils/convertPublicKeyToAVMAddress'; + const AssetTransferTransactionItemContent: FC< IProps > = ({ account, accounts, network, transaction }) => { @@ -27,8 +30,9 @@ const AssetTransferTransactionItemContent: FC< const { standardAsset, updating } = useStandardAssetById(transaction.assetId); const defaultTextColor: string = useDefaultTextColor(); const subTextColor: string = useSubTextColor(); - const accountAddress: string = - AccountService.convertPublicKeyToAlgorandAddress(account.publicKey); + const accountAddress: string = convertPublicKeyToAVMAddress( + PrivateKeyService.decode(account.publicKey) + ); const amount: BigNumber = new BigNumber(String(transaction.amount)); return ( diff --git a/src/extension/components/TransactionItem/PaymentTransactionItemContent.tsx b/src/extension/components/TransactionItem/PaymentTransactionItemContent.tsx index 15f6eb70..67894301 100644 --- a/src/extension/components/TransactionItem/PaymentTransactionItemContent.tsx +++ b/src/extension/components/TransactionItem/PaymentTransactionItemContent.tsx @@ -11,15 +11,13 @@ import AssetDisplay from '@extension/components/AssetDisplay'; import useDefaultTextColor from '@extension/hooks/useDefaultTextColor'; import useSubTextColor from '@extension/hooks/useSubTextColor'; -// services -import AccountService from '@extension/services/AccountService'; - // types import type { IPaymentTransaction } from '@extension/types'; import type { IProps } from './types'; // utils import createIconFromDataUri from '@extension/utils/createIconFromDataUri'; +import convertPublicKeyToAVMAddress from '@extension/utils/convertPublicKeyToAVMAddress'; const PaymentTransactionItemContent: FC> = ({ account, @@ -31,8 +29,9 @@ const PaymentTransactionItemContent: FC> = ({ // hooks const defaultTextColor: string = useDefaultTextColor(); const subTextColor: string = useSubTextColor(); - const accountAddress: string = - AccountService.convertPublicKeyToAlgorandAddress(account.publicKey); + const accountAddress: string = convertPublicKeyToAVMAddress( + account.publicKey + ); const amount: BigNumber = new BigNumber(String(transaction.amount)); return ( diff --git a/src/extension/constants/Dimensions.ts b/src/extension/constants/Dimensions.ts index e4ef6599..95f458ef 100644 --- a/src/extension/constants/Dimensions.ts +++ b/src/extension/constants/Dimensions.ts @@ -1,8 +1,8 @@ export const ACCOUNT_PAGE_HEADER_ITEM_HEIGHT = 10; // 2.5rem - 40px export const ACCOUNT_SELECT_ITEM_MINIMUM_HEIGHT = 60; // px export const DEFAULT_GAP = 6; -export const DEFAULT_POPUP_HEIGHT = 740; -export const DEFAULT_POPUP_WIDTH = 400; +export const DEFAULT_POPUP_HEIGHT = 750; +export const DEFAULT_POPUP_WIDTH = 465; export const MODAL_ITEM_HEIGHT = 10; // 10 = 2.5rem = 40px export const OPTION_HEIGHT = '57px'; export const PAGE_ITEM_HEIGHT = 10; // 10 = 2.5rem = 40px diff --git a/src/extension/constants/Keys.ts b/src/extension/constants/Keys.ts index c298d4a1..6a303ea3 100644 --- a/src/extension/constants/Keys.ts +++ b/src/extension/constants/Keys.ts @@ -7,6 +7,7 @@ export const EVENT_QUEUE_ITEM_KEY: string = 'event_queue'; export const NETWORK_TRANSACTION_PARAMS_ITEM_KEY_PREFIX: string = 'network_transaction_params_'; export const NEWS_KEY: string = 'news'; +export const PASSKEY_CREDENTIAL_KEY: string = 'passkey_credential'; export const PASSWORD_LOCK_ITEM_KEY: string = 'password_lock'; export const PASSWORD_TAG_ITEM_KEY: string = 'password_tag'; export const PRIVATE_KEY_ITEM_KEY_PREFIX: string = 'private_key_'; diff --git a/src/extension/constants/Routes.ts b/src/extension/constants/Routes.ts index 283a6427..bef18c59 100644 --- a/src/extension/constants/Routes.ts +++ b/src/extension/constants/Routes.ts @@ -14,6 +14,7 @@ export const GET_STARTED_ROUTE: string = '/get-started'; export const IMPORT_ACCOUNT_VIA_SEED_PHRASE_ROUTE: string = '/import-account-via-seed-phrase'; export const NFTS_ROUTE: string = '/nfts'; +export const PASSKEY_ROUTE: string = '/passkey'; export const PASSWORD_LOCK_ROUTE: string = '/password-lock'; export const PRIVACY_ROUTE: string = '/privacy'; export const SECURITY_ROUTE: string = '/security'; diff --git a/src/extension/enums/EncryptionMethodEnum.ts b/src/extension/enums/EncryptionMethodEnum.ts new file mode 100644 index 00000000..0ba22ac7 --- /dev/null +++ b/src/extension/enums/EncryptionMethodEnum.ts @@ -0,0 +1,6 @@ +enum EncryptionMethodEnum { + Passkey = 'passkey', + Password = 'password', +} + +export default EncryptionMethodEnum; diff --git a/src/extension/enums/ErrorCodeEnum.ts b/src/extension/enums/ErrorCodeEnum.ts index 2c0b2926..568c0f5c 100644 --- a/src/extension/enums/ErrorCodeEnum.ts +++ b/src/extension/enums/ErrorCodeEnum.ts @@ -35,6 +35,11 @@ enum ErrorCodeEnum { ScreenCaptureError = 7000, ScreenCaptureNotAllowedError = 7001, ScreenCaptureNotFoundError = 7002, + + // passkey + PasskeyNotSupportedError = 8000, + PasskeyCreationError = 8001, + UnableToFetchPasskeyError = 8002, } export default ErrorCodeEnum; diff --git a/src/extension/enums/RegisterThunkEnum.ts b/src/extension/enums/RegisterThunkEnum.ts deleted file mode 100644 index 31797db6..00000000 --- a/src/extension/enums/RegisterThunkEnum.ts +++ /dev/null @@ -1,6 +0,0 @@ -enum RegisterThunkEnum { - SaveCredentials = 'register/saveCredentials', - SetMnemonic = 'register/setMnemonic', -} - -export default RegisterThunkEnum; diff --git a/src/extension/enums/StoreNameEnum.ts b/src/extension/enums/StoreNameEnum.ts index a543478d..469b1099 100644 --- a/src/extension/enums/StoreNameEnum.ts +++ b/src/extension/enums/StoreNameEnum.ts @@ -9,6 +9,7 @@ enum StoreNameEnum { Networks = 'networks', News = 'news', Notifications = 'notifications', + Passkeys = 'passkeys', PasswordLock = 'password-lock', Register = 'register', ReKeyAccount = 're-key-account', diff --git a/src/extension/enums/index.ts b/src/extension/enums/index.ts index ac69665b..fdc34b43 100644 --- a/src/extension/enums/index.ts +++ b/src/extension/enums/index.ts @@ -9,6 +9,7 @@ export { default as ARC0300EncodingEnum } from './ARC0300EncodingEnum'; export { default as ARC0300PathEnum } from './ARC0300PathEnum'; export { default as ARC0300QueryEnum } from './ARC0300QueryEnum'; export { default as AssetTypeEnum } from './AssetTypeEnum'; +export { default as EncryptionMethodEnum } from './EncryptionMethodEnum'; export { default as EventTypeEnum } from './EventTypeEnum'; export { default as ErrorCodeEnum } from './ErrorCodeEnum'; export { default as EventsThunkEnum } from './EventsThunkEnum'; @@ -16,7 +17,6 @@ export { default as MessagesThunkEnum } from './MessagesThunkEnum'; export { default as NetworksThunkEnum } from './NetworksThunkEnum'; export { default as NetworkTypeEnum } from './NetworkTypeEnum'; export { default as PasswordLockThunkEnum } from './PasswordLockThunkEnum'; -export { default as RegisterThunkEnum } from './RegisterThunkEnum'; export { default as ScanModeEnum } from './ScanModeEnum'; export { default as SendAssetsThunkEnum } from './SendAssetsThunkEnum'; export { default as SessionsThunkEnum } from './SessionsThunkEnum'; diff --git a/src/extension/errors/PasskeyCreationError.ts b/src/extension/errors/PasskeyCreationError.ts new file mode 100644 index 00000000..acca4b0d --- /dev/null +++ b/src/extension/errors/PasskeyCreationError.ts @@ -0,0 +1,10 @@ +// enums +import { ErrorCodeEnum } from '../enums'; + +// errors +import BaseExtensionError from './BaseExtensionError'; + +export default class PasskeyCreationError extends BaseExtensionError { + public readonly code = ErrorCodeEnum.PasskeyCreationError; + public readonly name = 'PasskeyCreationError'; +} diff --git a/src/extension/errors/PasskeyNotSupportedError.ts b/src/extension/errors/PasskeyNotSupportedError.ts new file mode 100644 index 00000000..56290e34 --- /dev/null +++ b/src/extension/errors/PasskeyNotSupportedError.ts @@ -0,0 +1,10 @@ +// enums +import { ErrorCodeEnum } from '../enums'; + +// errors +import BaseExtensionError from './BaseExtensionError'; + +export default class PasskeyNotSupportedError extends BaseExtensionError { + public readonly code = ErrorCodeEnum.PasskeyNotSupportedError; + public readonly name = 'PasskeyNotSupportedError'; +} diff --git a/src/extension/errors/UnableToFetchPasskeyError.ts b/src/extension/errors/UnableToFetchPasskeyError.ts new file mode 100644 index 00000000..0ae13b77 --- /dev/null +++ b/src/extension/errors/UnableToFetchPasskeyError.ts @@ -0,0 +1,17 @@ +// enums +import { ErrorCodeEnum } from '../enums'; + +// errors +import BaseExtensionError from './BaseExtensionError'; + +export default class UnableToFetchPasskeyError extends BaseExtensionError { + public readonly code = ErrorCodeEnum.UnableToFetchPasskeyError; + public readonly id: string; + public readonly name = 'UnableToFetchPasskeyError'; + + constructor(id: string, message?: string) { + super(message || `unable to fetch passkey "${id}"`); + + this.id = id; + } +} diff --git a/src/extension/errors/index.ts b/src/extension/errors/index.ts index 8225c92b..8466846c 100644 --- a/src/extension/errors/index.ts +++ b/src/extension/errors/index.ts @@ -16,9 +16,12 @@ export { default as NotAZeroBalanceError } from './NotAZeroBalanceError'; export { default as NotEnoughMinimumBalanceError } from './NotEnoughMinimumBalanceError'; export { default as OfflineError } from './OfflineError'; export { default as ParsingError } from './ParsingError'; +export { default as PasskeyCreationError } from './PasskeyCreationError'; +export { default as PasskeyNotSupportedError } from './PasskeyNotSupportedError'; export { default as PrivateKeyAlreadyExistsError } from './PrivateKeyAlreadyExistsError'; export { default as ReadABIContractError } from './ReadABIContractError'; export { default as ScreenCaptureError } from './ScreenCaptureError'; export { default as ScreenCaptureNotAllowedError } from './ScreenCaptureNotAllowedError'; export { default as ScreenCaptureNotFoundError } from './ScreenCaptureNotFoundError'; +export { default as UnableToFetchPasskeyError } from './UnableToFetchPasskeyError'; export { default as UnknownError } from './UnknownError'; diff --git a/src/extension/features/accounts/thunks/addARC0200AssetHoldingsThunk.ts b/src/extension/features/accounts/thunks/addARC0200AssetHoldingsThunk.ts index d68eac92..d00ebc9a 100644 --- a/src/extension/features/accounts/thunks/addARC0200AssetHoldingsThunk.ts +++ b/src/extension/features/accounts/thunks/addARC0200AssetHoldingsThunk.ts @@ -11,6 +11,7 @@ import { MalformedDataError, NetworkNotSelectedError } from '@extension/errors'; // services import AccountService from '@extension/services/AccountService'; +import PrivateKeyService from '@extension/services/PrivateKeyService'; // types import type { @@ -28,6 +29,7 @@ import type { // utils import convertGenesisHashToHex from '@extension/utils/convertGenesisHashToHex'; +import convertPublicKeyToAVMAddress from '@extension/utils/convertPublicKeyToAVMAddress'; import initializeARC0200AssetHoldingFromARC0200Asset from '@extension/utils/initializeARC0200AssetHoldingFromARC0200Asset'; import isWatchAccount from '@extension/utils/isWatchAccount'; import updateAccountInformation from '@extension/utils/updateAccountInformation'; @@ -102,8 +104,8 @@ const addARC0200AssetHoldingsThunk: AsyncThunk< networkInformation: { ...account.networkInformation, [encodedGenesisHash]: await updateAccountInformation({ - address: AccountService.convertPublicKeyToAlgorandAddress( - account.publicKey + address: convertPublicKeyToAVMAddress( + PrivateKeyService.decode(account.publicKey) ), currentAccountInformation: { ...currentAccountInformation, diff --git a/src/extension/features/accounts/thunks/addStandardAssetHoldingsThunk.ts b/src/extension/features/accounts/thunks/addStandardAssetHoldingsThunk.ts index 5c2b5509..bd56095d 100644 --- a/src/extension/features/accounts/thunks/addStandardAssetHoldingsThunk.ts +++ b/src/extension/features/accounts/thunks/addStandardAssetHoldingsThunk.ts @@ -25,6 +25,7 @@ import { // services import AccountService from '@extension/services/AccountService'; +import PrivateKeyService from '@extension/services/PrivateKeyService'; // types import type { @@ -35,13 +36,14 @@ import type { IStandardAsset, } from '@extension/types'; import type { - IUpdateStandardAssetHoldingsPayload, IUpdateStandardAssetHoldingsResult, + TUpdateStandardAssetHoldingsPayload, } from '../types'; // utils import createAlgodClient from '@common/utils/createAlgodClient'; import calculateMinimumBalanceRequirementForStandardAssets from '@extension/utils/calculateMinimumBalanceRequirementForStandardAssets'; +import convertPublicKeyToAVMAddress from '@extension/utils/convertPublicKeyToAVMAddress'; import isWatchAccount from '@extension/utils/isWatchAccount'; import sendTransactionsForNetwork from '@extension/utils/sendTransactionsForNetwork'; import signTransaction from '@extension/utils/signTransaction'; @@ -52,16 +54,16 @@ import { findAccountWithoutExtendedProps } from '../utils'; const addStandardAssetHoldingsThunk: AsyncThunk< IUpdateStandardAssetHoldingsResult, // return - IUpdateStandardAssetHoldingsPayload, // args + TUpdateStandardAssetHoldingsPayload, // args IBaseAsyncThunkConfig > = createAsyncThunk< IUpdateStandardAssetHoldingsResult, - IUpdateStandardAssetHoldingsPayload, + TUpdateStandardAssetHoldingsPayload, IBaseAsyncThunkConfig >( ThunkEnum.AddStandardAssetHoldings, async ( - { accountId, assets, genesisHash, password }, + { accountId, assets, genesisHash, ...encryptionOptions }, { getState, rejectWithValue } ) => { const accounts = getState().accounts.items; @@ -121,8 +123,8 @@ const addStandardAssetHoldingsThunk: AsyncThunk< ); } - address = AccountService.convertPublicKeyToAlgorandAddress( - account.publicKey + address = convertPublicKeyToAVMAddress( + PrivateKeyService.decode(account.publicKey) ); accountInformation = AccountService.extractAccountInformationForNetwork(account, network) || @@ -192,11 +194,11 @@ const addStandardAssetHoldingsThunk: AsyncThunk< signedTransactions = await Promise.all( unsignedTransactions.map((value) => signTransaction({ + ...encryptionOptions, accounts, authAccounts: accounts, logger, networks, - password, unsignedTransaction: value, }) ) diff --git a/src/extension/features/accounts/thunks/fetchAccountsFromStorageThunk.ts b/src/extension/features/accounts/thunks/fetchAccountsFromStorageThunk.ts index c6e8ab11..4afc41dd 100644 --- a/src/extension/features/accounts/thunks/fetchAccountsFromStorageThunk.ts +++ b/src/extension/features/accounts/thunks/fetchAccountsFromStorageThunk.ts @@ -7,6 +7,7 @@ import { ThunkEnum } from '../enums'; // services import AccountService from '@extension/services/AccountService'; +import PrivateKeyService from '@extension/services/PrivateKeyService'; // types import { @@ -22,6 +23,7 @@ import type { // utils import convertGenesisHashToHex from '@extension/utils/convertGenesisHashToHex'; +import convertPublicKeyToAVMAddress from '@extension/utils/convertPublicKeyToAVMAddress'; import isWatchAccount from '@extension/utils/isWatchAccount'; import selectNetworkFromSettings from '@extension/utils/selectNetworkFromSettings'; import updateAccountInformation from '@extension/utils/updateAccountInformation'; @@ -75,8 +77,8 @@ const fetchAccountsFromStorageThunk: AsyncThunk< networkInformation: { ...account.networkInformation, [encodedGenesisHash]: await updateAccountInformation({ - address: AccountService.convertPublicKeyToAlgorandAddress( - account.publicKey + address: convertPublicKeyToAVMAddress( + PrivateKeyService.decode(account.publicKey) ), currentAccountInformation: account.networkInformation[encodedGenesisHash] || @@ -105,8 +107,8 @@ const fetchAccountsFromStorageThunk: AsyncThunk< networkTransactions: { ...account.networkTransactions, [encodedGenesisHash]: await updateAccountTransactions({ - address: AccountService.convertPublicKeyToAlgorandAddress( - account.publicKey + address: convertPublicKeyToAVMAddress( + PrivateKeyService.decode(account.publicKey) ), currentAccountTransactions: account.networkTransactions[encodedGenesisHash] || diff --git a/src/extension/features/accounts/thunks/removeAccountByIdThunk.ts b/src/extension/features/accounts/thunks/removeAccountByIdThunk.ts index 26f48bd2..335488d1 100644 --- a/src/extension/features/accounts/thunks/removeAccountByIdThunk.ts +++ b/src/extension/features/accounts/thunks/removeAccountByIdThunk.ts @@ -42,7 +42,6 @@ const removeAccountByIdThunk: AsyncThunk< privateKeyService = new PrivateKeyService({ logger, - passwordTag: browser.runtime.id, }); logger.debug( @@ -50,7 +49,7 @@ const removeAccountByIdThunk: AsyncThunk< ); // remove the private key - await privateKeyService.removePrivateKeyByPublicKey(account.publicKey); + await privateKeyService.removeFromStorageByPublicKey(account.publicKey); return account.id; } diff --git a/src/extension/features/accounts/thunks/removeStandardAssetHoldingsThunk.ts b/src/extension/features/accounts/thunks/removeStandardAssetHoldingsThunk.ts index be1aeaad..6fe79c59 100644 --- a/src/extension/features/accounts/thunks/removeStandardAssetHoldingsThunk.ts +++ b/src/extension/features/accounts/thunks/removeStandardAssetHoldingsThunk.ts @@ -37,14 +37,15 @@ import type { IStandardAssetHolding, } from '@extension/types'; import type { - IUpdateStandardAssetHoldingsPayload, + TUpdateStandardAssetHoldingsPayload, IUpdateStandardAssetHoldingsResult, } from '../types'; // utils import createAlgodClient from '@common/utils/createAlgodClient'; -import convertGenesisHashToHex from '@extension/utils/convertGenesisHashToHex'; import calculateMinimumBalanceRequirementForStandardAssets from '@extension/utils/calculateMinimumBalanceRequirementForStandardAssets'; +import convertGenesisHashToHex from '@extension/utils/convertGenesisHashToHex'; +import convertPublicKeyToAVMAddress from '@extension/utils/convertPublicKeyToAVMAddress'; import isWatchAccount from '@extension/utils/isWatchAccount'; import sendTransactionsForNetwork from '@extension/utils/sendTransactionsForNetwork'; import signTransaction from '@extension/utils/signTransaction'; @@ -54,16 +55,16 @@ import { findAccountWithoutExtendedProps } from '../utils'; const removeStandardAssetHoldingsThunk: AsyncThunk< IUpdateStandardAssetHoldingsResult, // return - IUpdateStandardAssetHoldingsPayload, // args + TUpdateStandardAssetHoldingsPayload, // args IBaseAsyncThunkConfig > = createAsyncThunk< IUpdateStandardAssetHoldingsResult, - IUpdateStandardAssetHoldingsPayload, + TUpdateStandardAssetHoldingsPayload, IBaseAsyncThunkConfig >( ThunkEnum.RemoveStandardAssetHoldings, async ( - { accountId, assets, genesisHash, password }, + { accountId, assets, genesisHash, ...encryptionOptions }, { getState, rejectWithValue } ) => { const accounts = getState().accounts.items; @@ -126,9 +127,7 @@ const removeStandardAssetHoldingsThunk: AsyncThunk< ); } - address = AccountService.convertPublicKeyToAlgorandAddress( - account.publicKey - ); + address = convertPublicKeyToAVMAddress(account.publicKey); accountInformation = AccountService.extractAccountInformationForNetwork(account, network) || AccountService.initializeDefaultAccountInformation(); @@ -216,8 +215,8 @@ const removeStandardAssetHoldingsThunk: AsyncThunk< authAccounts: accounts, logger, networks, - password, unsignedTransaction: value, + ...encryptionOptions, }) ) ); diff --git a/src/extension/features/accounts/thunks/saveNewAccountThunk.ts b/src/extension/features/accounts/thunks/saveNewAccountThunk.ts index a3d7c1c5..aac05b2c 100644 --- a/src/extension/features/accounts/thunks/saveNewAccountThunk.ts +++ b/src/extension/features/accounts/thunks/saveNewAccountThunk.ts @@ -1,12 +1,10 @@ import { AsyncThunk, createAsyncThunk } from '@reduxjs/toolkit'; -import { encode as encodeHex } from '@stablelib/hex'; -import browser from 'webextension-polyfill'; + +// enums +import { EncryptionMethodEnum } from '@extension/enums'; // errors -import { - MalformedDataError, - PrivateKeyAlreadyExistsError, -} from '@extension/errors'; +import { MalformedDataError } from '@extension/errors'; // enums import { ThunkEnum } from '../enums'; @@ -23,6 +21,10 @@ import type { } from '@extension/types'; import type { ISaveNewAccountPayload } from '../types'; +// utils +import savePrivateKeyItemWithPasskey from '@extension/utils/savePrivateKeyItemWithPasskey'; +import savePrivateKeyItemWithPassword from '@extension/utils/savePrivateKeyItemWithPassword'; + const saveNewAccountThunk: AsyncThunk< IAccountWithExtendedProps, // return ISaveNewAccountPayload, // args @@ -33,62 +35,47 @@ const saveNewAccountThunk: AsyncThunk< IAsyncThunkConfigWithRejectValue >( ThunkEnum.SaveNewAccount, - async ({ name, password, privateKey }, { getState, rejectWithValue }) => { - const encodedPublicKey: string = encodeHex( - PrivateKeyService.extractPublicKeyFromPrivateKey(privateKey) - ).toUpperCase(); + async ( + { name, keyPair, ...encryptionOptions }, + { getState, rejectWithValue } + ) => { + const encodedPublicKey = PrivateKeyService.encode(keyPair.publicKey); const logger = getState().system.logger; + let _error: string; let account: IAccountWithExtendedProps; let accountService: AccountService; - let errorMessage: string; - let privateKeyItem: IPrivateKey | null; - let privateKeyService: PrivateKeyService; - - logger.debug(`${ThunkEnum.SaveNewAccount}: inferring public key`); - - privateKeyService = new PrivateKeyService({ - logger, - passwordTag: browser.runtime.id, - }); - - privateKeyItem = await privateKeyService.getPrivateKeyByPublicKey( - encodedPublicKey - ); - - if (privateKeyItem) { - errorMessage = `private key for "${encodedPublicKey}" already exists`; - - logger.debug(`${ThunkEnum.SaveNewAccount}: ${errorMessage}`); - - return rejectWithValue(new PrivateKeyAlreadyExistsError(errorMessage)); - } - - logger.debug( - `${ThunkEnum.SaveNewAccount}: saving private key "${encodedPublicKey}" to storage` - ); + let privateKeyItem: IPrivateKey | null = null; try { - // add the new private key - privateKeyItem = await privateKeyService.setPrivateKey( - privateKey, - password - ); + if (encryptionOptions.type === EncryptionMethodEnum.Passkey) { + privateKeyItem = await savePrivateKeyItemWithPasskey({ + inputKeyMaterial: encryptionOptions.inputKeyMaterial, + keyPair, + logger, + }); + } + + if (encryptionOptions.type === EncryptionMethodEnum.Password) { + privateKeyItem = await savePrivateKeyItemWithPassword({ + keyPair, + logger, + password: encryptionOptions.password, + }); + } } catch (error) { - logger.error(`${ThunkEnum.SaveNewAccount}: ${error.message}`); - return rejectWithValue(error); } if (!privateKeyItem) { - errorMessage = `failed to save private key "${encodedPublicKey}" to storage`; + _error = `failed to save key "${encodedPublicKey}" (public key) to storage`; - logger.debug(`${ThunkEnum.SaveNewAccount}: ${errorMessage}`); + logger.debug(`${ThunkEnum.SaveNewAccount}: ${_error}`); - return rejectWithValue(new MalformedDataError(errorMessage)); + return rejectWithValue(new MalformedDataError(_error)); } logger.debug( - `${ThunkEnum.SaveNewAccount}: successfully saved private key "${encodedPublicKey}" to storage` + `${ThunkEnum.SaveNewAccount}: successfully saved key "${encodedPublicKey}" (public key) to storage` ); account = { @@ -111,7 +98,7 @@ const saveNewAccountThunk: AsyncThunk< await accountService.saveAccounts([account]); logger.debug( - `${ThunkEnum.SaveNewAccount}: saved account for "${encodedPublicKey}" to storage` + `${ThunkEnum.SaveNewAccount}: saved account for key "${encodedPublicKey}" (public key) to storage` ); return account; diff --git a/src/extension/features/accounts/thunks/saveNewWatchAccountThunk.ts b/src/extension/features/accounts/thunks/saveNewWatchAccountThunk.ts index abe1b463..1bef446b 100644 --- a/src/extension/features/accounts/thunks/saveNewWatchAccountThunk.ts +++ b/src/extension/features/accounts/thunks/saveNewWatchAccountThunk.ts @@ -1,6 +1,5 @@ import { AsyncThunk, createAsyncThunk } from '@reduxjs/toolkit'; import { isValidAddress } from 'algosdk'; -import browser from 'webextension-polyfill'; // errors import { MalformedDataError } from '@extension/errors'; @@ -21,6 +20,9 @@ import type { } from '@extension/types'; import type { ISaveNewWatchAccountPayload } from '../types'; +// utils +import convertPublicKeyToAVMAddress from '@extension/utils/convertPublicKeyToAVMAddress'; + const saveNewWatchAccountThunk: AsyncThunk< IAccountWithExtendedProps, // return ISaveNewWatchAccountPayload, // args @@ -35,9 +37,9 @@ const saveNewWatchAccountThunk: AsyncThunk< const logger = getState().system.logger; const accountService = new AccountService({ logger }); const accounts = await accountService.getAllAccounts(); + let _error: string; let account: IAccount | null; let encodedPublicKey: string; - let errorMessage: string; let privateKeyItem: IPrivateKey | null; let privateKeyService: PrivateKeyService; @@ -46,15 +48,14 @@ const saveNewWatchAccountThunk: AsyncThunk< ); if (!isValidAddress(address)) { - errorMessage = `address "${address}" is not valid`; + _error = `address "${address}" is not valid`; - logger.debug(`${ThunkEnum.SaveNewWatchAccount}: ${errorMessage}`); + logger.debug(`${ThunkEnum.SaveNewWatchAccount}: ${_error}`); - return rejectWithValue(new MalformedDataError(errorMessage)); + return rejectWithValue(new MalformedDataError(_error)); } - encodedPublicKey = - AccountService.convertAlgorandAddressToPublicKey(address); + encodedPublicKey = convertPublicKeyToAVMAddress(address); account = accounts.find((value) => value.publicKey === encodedPublicKey) || null; @@ -66,9 +67,8 @@ const saveNewWatchAccountThunk: AsyncThunk< if (account) { privateKeyService = new PrivateKeyService({ logger, - passwordTag: browser.runtime.id, }); - privateKeyItem = await privateKeyService.getPrivateKeyByPublicKey( + privateKeyItem = await privateKeyService.fetchFromStorageByPublicKey( encodedPublicKey ); diff --git a/src/extension/features/accounts/thunks/updateAccountsThunk.ts b/src/extension/features/accounts/thunks/updateAccountsThunk.ts index 2adbe4aa..e3497dff 100644 --- a/src/extension/features/accounts/thunks/updateAccountsThunk.ts +++ b/src/extension/features/accounts/thunks/updateAccountsThunk.ts @@ -19,6 +19,7 @@ import type { IUpdateAccountsPayload } from '../types'; // utils import convertGenesisHashToHex from '@extension/utils/convertGenesisHashToHex'; +import convertPublicKeyToAVMAddress from '@extension/utils/convertPublicKeyToAVMAddress'; import isWatchAccount from '@extension/utils/isWatchAccount'; import mapAccountWithExtendedPropsToAccount from '@extension/utils/mapAccountWithExtendedPropsToAccount'; import selectNetworkFromSettings from '@extension/utils/selectNetworkFromSettings'; @@ -96,9 +97,7 @@ const updateAccountsThunk: AsyncThunk< networkInformation: { ...account.networkInformation, [encodedGenesisHash]: await updateAccountInformation({ - address: AccountService.convertPublicKeyToAlgorandAddress( - account.publicKey - ), + address: convertPublicKeyToAVMAddress(account.publicKey), currentAccountInformation: account.networkInformation[encodedGenesisHash] || AccountService.initializeDefaultAccountInformation(), @@ -119,9 +118,7 @@ const updateAccountsThunk: AsyncThunk< networkTransactions: { ...account.networkTransactions, [encodedGenesisHash]: await updateAccountTransactions({ - address: AccountService.convertPublicKeyToAlgorandAddress( - account.publicKey - ), + address: convertPublicKeyToAVMAddress(account.publicKey), currentAccountTransactions: account.networkTransactions[encodedGenesisHash] || AccountService.initializeDefaultAccountTransactions(), diff --git a/src/extension/features/accounts/types/ISaveNewAccountPayload.ts b/src/extension/features/accounts/types/ISaveNewAccountPayload.ts index f3a4e262..c608a69c 100644 --- a/src/extension/features/accounts/types/ISaveNewAccountPayload.ts +++ b/src/extension/features/accounts/types/ISaveNewAccountPayload.ts @@ -1,7 +1,15 @@ -interface ISaveNewAccountPayload { +// models +import Ed21559KeyPair from '@extension/models/Ed21559KeyPair'; + +// types +import type { TEncryptionCredentials } from '@extension/types'; + +interface ISaveNewAccountPayloadFragment { + keyPair: Ed21559KeyPair; name: string | null; - privateKey: Uint8Array; - password: string; } -export default ISaveNewAccountPayload; +type TSaveNewAccountPayload = ISaveNewAccountPayloadFragment & + TEncryptionCredentials; + +export default TSaveNewAccountPayload; diff --git a/src/extension/features/accounts/types/IUpdateStandardAssetHoldingsPayload.ts b/src/extension/features/accounts/types/IUpdateStandardAssetHoldingsPayload.ts deleted file mode 100644 index 5b7db7a8..00000000 --- a/src/extension/features/accounts/types/IUpdateStandardAssetHoldingsPayload.ts +++ /dev/null @@ -1,10 +0,0 @@ -// types -import type { IStandardAsset } from '@extension/types'; -import type IUpdateAssetHoldingsPayload from './IUpdateAssetHoldingsPayload'; - -interface IUpdateStandardAssetHoldingsPayload - extends IUpdateAssetHoldingsPayload { - password: string; -} - -export default IUpdateStandardAssetHoldingsPayload; diff --git a/src/extension/features/accounts/types/TUpdateStandardAssetHoldingsPayload.ts b/src/extension/features/accounts/types/TUpdateStandardAssetHoldingsPayload.ts new file mode 100644 index 00000000..d94b03f5 --- /dev/null +++ b/src/extension/features/accounts/types/TUpdateStandardAssetHoldingsPayload.ts @@ -0,0 +1,8 @@ +// types +import type { IStandardAsset, TEncryptionCredentials } from '@extension/types'; +import type IUpdateAssetHoldingsPayload from './IUpdateAssetHoldingsPayload'; + +type TUpdateStandardAssetHoldingsPayload = + IUpdateAssetHoldingsPayload & TEncryptionCredentials; + +export default TUpdateStandardAssetHoldingsPayload; diff --git a/src/extension/features/accounts/types/index.ts b/src/extension/features/accounts/types/index.ts index 1f81705a..a6d50e73 100644 --- a/src/extension/features/accounts/types/index.ts +++ b/src/extension/features/accounts/types/index.ts @@ -8,5 +8,5 @@ export type { default as IState } from './IState'; export type { default as IUpdateAccountsPayload } from './IUpdateAccountsPayload'; export type { default as IUpdateAssetHoldingsPayload } from './IUpdateAssetHoldingsPayload'; export type { default as IUpdateAssetHoldingsResult } from './IUpdateAssetHoldingsResult'; -export type { default as IUpdateStandardAssetHoldingsPayload } from './IUpdateStandardAssetHoldingsPayload'; export type { default as IUpdateStandardAssetHoldingsResult } from './IUpdateStandardAssetHoldingsResult'; +export type { default as TUpdateStandardAssetHoldingsPayload } from './TUpdateStandardAssetHoldingsPayload'; diff --git a/src/extension/features/messages/thunks/sendEnableResponseThunk.ts b/src/extension/features/messages/thunks/sendEnableResponseThunk.ts index 7d0ed7f2..6960bb83 100644 --- a/src/extension/features/messages/thunks/sendEnableResponseThunk.ts +++ b/src/extension/features/messages/thunks/sendEnableResponseThunk.ts @@ -16,9 +16,6 @@ import { removeEventByIdThunk } from '@extension/features/events'; // messages import { ClientResponseMessage } from '@common/messages'; -// services -import AccountService from '@extension/services/AccountService'; - // types import type { IAccount, @@ -27,6 +24,9 @@ import type { } from '@extension/types'; import type { IEnableResponseThunkPayload } from '../types'; +// utils +import convertPublicKeyToAVMAddress from '@extension/utils/convertPublicKeyToAVMAddress'; + const sendEnableResponseThunk: AsyncThunk< void, // return IEnableResponseThunkPayload, // args @@ -77,9 +77,7 @@ const sendEnableResponseThunk: AsyncThunk< const account: IAccount | null = accounts.find( (value) => - AccountService.convertPublicKeyToAlgorandAddress( - value.publicKey - ) === address + convertPublicKeyToAVMAddress(value.publicKey) === address ) || null; return { diff --git a/src/extension/features/passkeys/enums/ThunkEnum.ts b/src/extension/features/passkeys/enums/ThunkEnum.ts new file mode 100644 index 00000000..5d459504 --- /dev/null +++ b/src/extension/features/passkeys/enums/ThunkEnum.ts @@ -0,0 +1,7 @@ +enum ThunkEnum { + FetchFromStorage = 'passkeys/fetchFromStorage', + RemoveFromStorage = 'passkeys/removeFromStorage', + SaveToStorage = 'passkeys/saveToStorage', +} + +export default ThunkEnum; diff --git a/src/extension/features/passkeys/enums/index.ts b/src/extension/features/passkeys/enums/index.ts new file mode 100644 index 00000000..14ab6bbd --- /dev/null +++ b/src/extension/features/passkeys/enums/index.ts @@ -0,0 +1 @@ +export { default as ThunkEnum } from './ThunkEnum'; diff --git a/src/extension/features/passkeys/index.ts b/src/extension/features/passkeys/index.ts new file mode 100644 index 00000000..dd1694da --- /dev/null +++ b/src/extension/features/passkeys/index.ts @@ -0,0 +1,5 @@ +export * from './enums'; +export * from './slice'; +export * from './thunks'; +export * from './types'; +export * from './utils'; diff --git a/src/extension/features/passkeys/slice.ts b/src/extension/features/passkeys/slice.ts new file mode 100644 index 00000000..2ac1a767 --- /dev/null +++ b/src/extension/features/passkeys/slice.ts @@ -0,0 +1,71 @@ +import { createSlice, PayloadAction, Reducer } from '@reduxjs/toolkit'; + +// enums +import { StoreNameEnum } from '@extension/enums'; + +// thunks +import { + fetchFromStorageThunk, + removeFromStorageThunk, + saveToStorageThunk, +} from './thunks'; + +// types +import type { IPasskeyCredential } from '@extension/types'; +import type { IState } from './types'; + +// utils +import { getInitialState } from './utils'; + +const slice = createSlice({ + extraReducers: (builder) => { + /** fetch from storage **/ + builder.addCase( + fetchFromStorageThunk.fulfilled, + (state: IState, action: PayloadAction) => { + state.passkey = action.payload; + state.fetching = false; + } + ); + builder.addCase(fetchFromStorageThunk.pending, (state: IState) => { + state.fetching = true; + }); + builder.addCase(fetchFromStorageThunk.rejected, (state: IState) => { + state.fetching = false; + }); + /** remove from storage **/ + builder.addCase(removeFromStorageThunk.fulfilled, (state: IState) => { + state.passkey = null; + state.saving = false; + }); + builder.addCase(removeFromStorageThunk.pending, (state: IState) => { + state.saving = true; + }); + builder.addCase(removeFromStorageThunk.rejected, (state: IState) => { + state.saving = false; + }); + /** save to storage **/ + builder.addCase( + saveToStorageThunk.fulfilled, + (state: IState, action: PayloadAction) => { + state.passkey = action.payload; + state.saving = false; + } + ); + builder.addCase(saveToStorageThunk.pending, (state: IState) => { + state.saving = true; + }); + builder.addCase(saveToStorageThunk.rejected, (state: IState) => { + state.saving = false; + }); + }, + initialState: getInitialState(), + name: StoreNameEnum.Passkeys, + reducers: { + noop: () => { + return; + }, + }, +}); + +export const reducer: Reducer = slice.reducer; diff --git a/src/extension/features/passkeys/thunks/fetchFromStorageThunk.ts b/src/extension/features/passkeys/thunks/fetchFromStorageThunk.ts new file mode 100644 index 00000000..3f4c9d5b --- /dev/null +++ b/src/extension/features/passkeys/thunks/fetchFromStorageThunk.ts @@ -0,0 +1,28 @@ +import { AsyncThunk, createAsyncThunk } from '@reduxjs/toolkit'; + +// enums +import { ThunkEnum } from '../enums'; + +// services +import PasskeyService from '@extension/services/PasskeyService'; + +// types +import type { + IBaseAsyncThunkConfig, + IPasskeyCredential, +} from '@extension/types'; + +const fetchFromStorageThunk: AsyncThunk< + IPasskeyCredential | null, // return + void, // args + IBaseAsyncThunkConfig +> = createAsyncThunk( + ThunkEnum.FetchFromStorage, + async () => { + const passkeyService = new PasskeyService(); + + return await passkeyService.fetchFromStorage(); + } +); + +export default fetchFromStorageThunk; diff --git a/src/extension/features/passkeys/thunks/index.ts b/src/extension/features/passkeys/thunks/index.ts new file mode 100644 index 00000000..6074f0bb --- /dev/null +++ b/src/extension/features/passkeys/thunks/index.ts @@ -0,0 +1,3 @@ +export { default as fetchFromStorageThunk } from './fetchFromStorageThunk'; +export { default as removeFromStorageThunk } from './removeFromStorageThunk'; +export { default as saveToStorageThunk } from './saveToStorageThunk'; diff --git a/src/extension/features/passkeys/thunks/removeFromStorageThunk.ts b/src/extension/features/passkeys/thunks/removeFromStorageThunk.ts new file mode 100644 index 00000000..a10978cf --- /dev/null +++ b/src/extension/features/passkeys/thunks/removeFromStorageThunk.ts @@ -0,0 +1,25 @@ +import { AsyncThunk, createAsyncThunk } from '@reduxjs/toolkit'; + +// enums +import { ThunkEnum } from '../enums'; + +// services +import PasskeyService from '@extension/services/PasskeyService'; + +// types +import type { IBaseAsyncThunkConfig } from '@extension/types'; + +const removeFromStorageThunk: AsyncThunk< + void, // return + void, // args + IBaseAsyncThunkConfig +> = createAsyncThunk( + ThunkEnum.RemoveFromStorage, + async () => { + const passkeyService = new PasskeyService(); + + return await passkeyService.removeFromStorage(); + } +); + +export default removeFromStorageThunk; diff --git a/src/extension/features/passkeys/thunks/saveToStorageThunk.ts b/src/extension/features/passkeys/thunks/saveToStorageThunk.ts new file mode 100644 index 00000000..a8617671 --- /dev/null +++ b/src/extension/features/passkeys/thunks/saveToStorageThunk.ts @@ -0,0 +1,29 @@ +import { AsyncThunk, createAsyncThunk } from '@reduxjs/toolkit'; + +// enums +import { ThunkEnum } from '../enums'; + +// services +import PasskeyService from '@extension/services/PasskeyService'; + +// types +import type { + IBaseAsyncThunkConfig, + IPasskeyCredential, +} from '@extension/types'; + +const saveToStorageThunk: AsyncThunk< + IPasskeyCredential, // return + IPasskeyCredential, // args + IBaseAsyncThunkConfig +> = createAsyncThunk< + IPasskeyCredential, + IPasskeyCredential, + IBaseAsyncThunkConfig +>(ThunkEnum.SaveToStorage, async (credential) => { + const passkeyService = new PasskeyService(); + + return await passkeyService.saveToStorage(credential); +}); + +export default saveToStorageThunk; diff --git a/src/extension/features/passkeys/types/IState.ts b/src/extension/features/passkeys/types/IState.ts new file mode 100644 index 00000000..4d262770 --- /dev/null +++ b/src/extension/features/passkeys/types/IState.ts @@ -0,0 +1,15 @@ +// types +import type { IPasskeyCredential } from '@extension/types'; + +/** + * @property {boolean} fetching - whether the credential is being fetched. + * @property {IPasskeyCredential | null} passkey - the stored passkey credential. + * @property {boolean} saving - whether the credential is being saved. + */ +interface IState { + fetching: boolean; + passkey: IPasskeyCredential | null; + saving: boolean; +} + +export default IState; diff --git a/src/extension/features/passkeys/types/index.ts b/src/extension/features/passkeys/types/index.ts new file mode 100644 index 00000000..bf812279 --- /dev/null +++ b/src/extension/features/passkeys/types/index.ts @@ -0,0 +1 @@ +export type { default as IState } from './IState'; diff --git a/src/extension/features/passkeys/utils/getInitialState.ts b/src/extension/features/passkeys/utils/getInitialState.ts new file mode 100644 index 00000000..2fc4fd62 --- /dev/null +++ b/src/extension/features/passkeys/utils/getInitialState.ts @@ -0,0 +1,10 @@ +// types +import type { IState } from '../types'; + +export default function getInitialState(): IState { + return { + fetching: false, + passkey: null, + saving: false, + }; +} diff --git a/src/extension/features/passkeys/utils/index.ts b/src/extension/features/passkeys/utils/index.ts new file mode 100644 index 00000000..85e2c689 --- /dev/null +++ b/src/extension/features/passkeys/utils/index.ts @@ -0,0 +1 @@ +export { default as getInitialState } from './getInitialState'; diff --git a/src/extension/features/password-lock/slice.ts b/src/extension/features/password-lock/slice.ts index b8aeb5d8..63dc51a4 100644 --- a/src/extension/features/password-lock/slice.ts +++ b/src/extension/features/password-lock/slice.ts @@ -7,6 +7,7 @@ import { StoreNameEnum } from '@extension/enums'; import { savePasswordLockThunk } from './thunks'; // types +import type { TEncryptionCredentials } from '@extension/types'; import type { IState } from './types'; // utils @@ -14,11 +15,11 @@ import { getInitialState } from './utils'; const slice = createSlice({ extraReducers: (builder) => { - /** Save credentials **/ + /**save password lock**/ builder.addCase( savePasswordLockThunk.fulfilled, - (state: IState, action: PayloadAction) => { - state.password = action.payload; + (state: IState, action: PayloadAction) => { + state.credentials = action.payload; state.saving = false; } ); @@ -32,14 +33,14 @@ const slice = createSlice({ initialState: getInitialState(), name: StoreNameEnum.PasswordLock, reducers: { - setPassword: ( + setCredentials: ( state: Draft, - action: PayloadAction + action: PayloadAction ) => { - state.password = action.payload; + state.credentials = action.payload; }, }, }); export const reducer: Reducer = slice.reducer; -export const { setPassword } = slice.actions; +export const { setCredentials } = slice.actions; diff --git a/src/extension/features/password-lock/thunks/savePasswordLockThunk.ts b/src/extension/features/password-lock/thunks/savePasswordLockThunk.ts index 32068cc9..e4d8b931 100644 --- a/src/extension/features/password-lock/thunks/savePasswordLockThunk.ts +++ b/src/extension/features/password-lock/thunks/savePasswordLockThunk.ts @@ -8,23 +8,27 @@ import { PasswordLockThunkEnum } from '@extension/enums'; import { ProviderPasswordLockClearMessage } from '@common/messages'; // types -import type { IBaseAsyncThunkConfig } from '@extension/types'; +import type { + IBaseAsyncThunkConfig, + TEncryptionCredentials, +} from '@extension/types'; /** * Sends a message to the background service worker to clear the password lock alarm. This is either called when setting - * the password (when the password lock screen is successful), or when the password lock is being disabled. + * the passkey/password (when the password lock screen is successful), or when the password lock is being disabled. */ const savePasswordLockThunk: AsyncThunk< - string | null, // return - string | null, // args + TEncryptionCredentials | null, // return + TEncryptionCredentials | null, // args IBaseAsyncThunkConfig -> = createAsyncThunk( - PasswordLockThunkEnum.SavePasswordLock, - async (password) => { - await browser.runtime.sendMessage(new ProviderPasswordLockClearMessage()); +> = createAsyncThunk< + TEncryptionCredentials | null, + TEncryptionCredentials | null, + IBaseAsyncThunkConfig +>(PasswordLockThunkEnum.SavePasswordLock, async (credentials) => { + await browser.runtime.sendMessage(new ProviderPasswordLockClearMessage()); - return password; - } -); + return credentials; +}); export default savePasswordLockThunk; diff --git a/src/extension/features/password-lock/types/IState.ts b/src/extension/features/password-lock/types/IState.ts index 9fc05610..cb1fd661 100644 --- a/src/extension/features/password-lock/types/IState.ts +++ b/src/extension/features/password-lock/types/IState.ts @@ -1,9 +1,12 @@ +// types +import type { TEncryptionCredentials } from '@extension/types'; + /** - * @property {string | null} password - the password to use to secure accounts. + * @property {TEncryptionCredentials | null} credentials - the password or the passkey used to secure accounts. * @property {saving} saving - whether the password lock is being saved or not. */ interface IState { - password: string | null; + credentials: TEncryptionCredentials | null; saving: boolean; } diff --git a/src/extension/features/password-lock/utils/getInitialState.ts b/src/extension/features/password-lock/utils/getInitialState.ts index ee751ba0..996cd951 100644 --- a/src/extension/features/password-lock/utils/getInitialState.ts +++ b/src/extension/features/password-lock/utils/getInitialState.ts @@ -3,7 +3,7 @@ import type { IState } from '../types'; export default function getInitialState(): IState { return { - password: null, + credentials: null, saving: false, }; } diff --git a/src/extension/features/re-key-account/thunks/reKeyAccountThunk.ts b/src/extension/features/re-key-account/thunks/reKeyAccountThunk.ts index f5584442..299081c8 100644 --- a/src/extension/features/re-key-account/thunks/reKeyAccountThunk.ts +++ b/src/extension/features/re-key-account/thunks/reKeyAccountThunk.ts @@ -26,35 +26,34 @@ import type { IBaseAsyncThunkConfig, IMainRootState, } from '@extension/types'; -import type { IReKeyAccountThunkPayload } from '../types'; +import type { TReKeyAccountThunkPayload } from '../types'; // utils import createAlgodClient from '@common/utils/createAlgodClient'; +import convertPublicKeyToAVMAddress from '@extension/utils/convertPublicKeyToAVMAddress'; import doesAccountFallBelowMinimumBalanceRequirementForTransactions from '@extension/utils/doesAccountFallBelowMinimumBalanceRequirementForTransactions'; import sendTransactionsForNetwork from '@extension/utils/sendTransactionsForNetwork'; import signTransaction from '@extension/utils/signTransaction'; const reKeyAccountThunk: AsyncThunk< string | null, // return - IReKeyAccountThunkPayload, // args + TReKeyAccountThunkPayload, // args IBaseAsyncThunkConfig > = createAsyncThunk< string | null, - IReKeyAccountThunkPayload, + TReKeyAccountThunkPayload, IBaseAsyncThunkConfig >( ThunkEnum.ReKeyAccount, async ( - { authorizedAddress, network, password, reKeyAccount }, + { authorizedAddress, network, reKeyAccount, ...encryptionOptions }, { getState, rejectWithValue } ) => { const accounts = getState().accounts.items; const logger = getState().system.logger; const accountInformation: IAccountInformation | null = AccountService.extractAccountInformationForNetwork(reKeyAccount, network); - const address = AccountService.convertPublicKeyToAlgorandAddress( - reKeyAccount.publicKey - ); + const address = convertPublicKeyToAVMAddress(reKeyAccount.publicKey); const networks = getState().networks.items; let _error: string; let algodClient: Algodv2; @@ -100,11 +99,11 @@ const reKeyAccountThunk: AsyncThunk< try { signedTransaction = await signTransaction({ + ...encryptionOptions, accounts, authAccounts: accounts, logger, networks, - password, unsignedTransaction, }); diff --git a/src/extension/features/re-key-account/thunks/undoReKeyAccountThunk.ts b/src/extension/features/re-key-account/thunks/undoReKeyAccountThunk.ts index 5510e3d9..1af11915 100644 --- a/src/extension/features/re-key-account/thunks/undoReKeyAccountThunk.ts +++ b/src/extension/features/re-key-account/thunks/undoReKeyAccountThunk.ts @@ -26,35 +26,34 @@ import type { IBaseAsyncThunkConfig, IMainRootState, } from '@extension/types'; -import type { IUndoReKeyAccountThunkPayload } from '../types'; +import type { TUndoReKeyAccountThunkPayload } from '../types'; // utils import createAlgodClient from '@common/utils/createAlgodClient'; +import convertPublicKeyToAVMAddress from '@extension/utils/convertPublicKeyToAVMAddress'; import doesAccountFallBelowMinimumBalanceRequirementForTransactions from '@extension/utils/doesAccountFallBelowMinimumBalanceRequirementForTransactions'; import sendTransactionsForNetwork from '@extension/utils/sendTransactionsForNetwork'; import signTransaction from '@extension/utils/signTransaction'; const undoReKeyAccountThunk: AsyncThunk< string | null, // return - IUndoReKeyAccountThunkPayload, // args + TUndoReKeyAccountThunkPayload, // args IBaseAsyncThunkConfig > = createAsyncThunk< string | null, - IUndoReKeyAccountThunkPayload, + TUndoReKeyAccountThunkPayload, IBaseAsyncThunkConfig >( ThunkEnum.UndoReKeyAccount, async ( - { network, password, reKeyAccount }, + { network, reKeyAccount, ...encryptionOptions }, { getState, rejectWithValue } ) => { const accounts = getState().accounts.items; const logger = getState().system.logger; const accountInformation: IAccountInformation | null = AccountService.extractAccountInformationForNetwork(reKeyAccount, network); - const address = AccountService.convertPublicKeyToAlgorandAddress( - reKeyAccount.publicKey - ); + const address = convertPublicKeyToAVMAddress(reKeyAccount.publicKey); const networks = getState().networks.items; let _error: string; let algodClient: Algodv2; @@ -112,8 +111,8 @@ const undoReKeyAccountThunk: AsyncThunk< authAccounts: accounts, logger, networks, - password, unsignedTransaction, + ...encryptionOptions, }); await sendTransactionsForNetwork({ diff --git a/src/extension/features/re-key-account/types/IUndoReKeyAccountThunkPayload.ts b/src/extension/features/re-key-account/types/IUndoReKeyAccountThunkPayload.ts deleted file mode 100644 index bdcdd5cf..00000000 --- a/src/extension/features/re-key-account/types/IUndoReKeyAccountThunkPayload.ts +++ /dev/null @@ -1,13 +0,0 @@ -// types -import type { - IAccountWithExtendedProps, - INetworkWithTransactionParams, -} from '@extension/types'; - -interface IUndoReKeyAccountThunkPayload { - network: INetworkWithTransactionParams; - password: string; - reKeyAccount: IAccountWithExtendedProps; -} - -export default IUndoReKeyAccountThunkPayload; diff --git a/src/extension/features/re-key-account/types/TReKeyAccountThunkPayload.ts b/src/extension/features/re-key-account/types/TReKeyAccountThunkPayload.ts new file mode 100644 index 00000000..45bcb3ad --- /dev/null +++ b/src/extension/features/re-key-account/types/TReKeyAccountThunkPayload.ts @@ -0,0 +1,17 @@ +// types +import type { + IAccountWithExtendedProps, + INetworkWithTransactionParams, + TEncryptionCredentials, +} from '@extension/types'; + +interface IReKeyAccountThunkPayload { + authorizedAddress: string; + network: INetworkWithTransactionParams; + reKeyAccount: IAccountWithExtendedProps; +} + +type TReKeyAccountThunkPayload = IReKeyAccountThunkPayload & + TEncryptionCredentials; + +export default TReKeyAccountThunkPayload; diff --git a/src/extension/features/re-key-account/types/IReKeyAccountThunkPayload.ts b/src/extension/features/re-key-account/types/TUndoReKeyAccountThunkPayload.ts similarity index 59% rename from src/extension/features/re-key-account/types/IReKeyAccountThunkPayload.ts rename to src/extension/features/re-key-account/types/TUndoReKeyAccountThunkPayload.ts index 53533802..bd701f68 100644 --- a/src/extension/features/re-key-account/types/IReKeyAccountThunkPayload.ts +++ b/src/extension/features/re-key-account/types/TUndoReKeyAccountThunkPayload.ts @@ -2,13 +2,15 @@ import type { IAccountWithExtendedProps, INetworkWithTransactionParams, + TEncryptionCredentials, } from '@extension/types'; interface IUndoReKeyAccountThunkPayload { - authorizedAddress: string; network: INetworkWithTransactionParams; - password: string; reKeyAccount: IAccountWithExtendedProps; } -export default IUndoReKeyAccountThunkPayload; +type TUndoReKeyAccountThunkPayload = IUndoReKeyAccountThunkPayload & + TEncryptionCredentials; + +export default TUndoReKeyAccountThunkPayload; diff --git a/src/extension/features/re-key-account/types/index.ts b/src/extension/features/re-key-account/types/index.ts index c9497c9a..1af0c43f 100644 --- a/src/extension/features/re-key-account/types/index.ts +++ b/src/extension/features/re-key-account/types/index.ts @@ -1,5 +1,5 @@ -export type { default as IReKeyAccountThunkPayload } from './IReKeyAccountThunkPayload'; export type { default as ISetAccountAndActionPayload } from './ISetAccountAndActionPayload'; export type { default as IState } from './IState'; -export type { default as IUndoReKeyAccountThunkPayload } from './IUndoReKeyAccountThunkPayload'; +export type { default as TReKeyAccountThunkPayload } from './TReKeyAccountThunkPayload'; export type { default as TReKeyType } from './TReKeyType'; +export type { default as TUndoReKeyAccountThunkPayload } from './TUndoReKeyAccountThunkPayload'; diff --git a/src/extension/features/registration/enums/ThunkEnum.ts b/src/extension/features/registration/enums/ThunkEnum.ts new file mode 100644 index 00000000..e36793bf --- /dev/null +++ b/src/extension/features/registration/enums/ThunkEnum.ts @@ -0,0 +1,5 @@ +enum ThunkEnum { + SaveCredentials = 'register/saveCredentials', +} + +export default ThunkEnum; diff --git a/src/extension/features/registration/enums/index.ts b/src/extension/features/registration/enums/index.ts new file mode 100644 index 00000000..14ab6bbd --- /dev/null +++ b/src/extension/features/registration/enums/index.ts @@ -0,0 +1 @@ +export { default as ThunkEnum } from './ThunkEnum'; diff --git a/src/extension/features/registration/thunks/saveCredentialsThunk.ts b/src/extension/features/registration/thunks/saveCredentialsThunk.ts index 131ccd54..28ec5f06 100644 --- a/src/extension/features/registration/thunks/saveCredentialsThunk.ts +++ b/src/extension/features/registration/thunks/saveCredentialsThunk.ts @@ -1,16 +1,17 @@ import { AsyncThunk, createAsyncThunk } from '@reduxjs/toolkit'; -import { encode as encodeHex } from '@stablelib/hex'; -import { sign } from 'tweetnacl'; +import { encode as encodeUtf8 } from '@stablelib/utf8'; import browser from 'webextension-polyfill'; // enums -import { RegisterThunkEnum } from '@extension/enums'; +import { EncryptionMethodEnum } from '@extension/enums'; +import { ThunkEnum } from '../enums'; // errors import { InvalidPasswordError } from '@extension/errors'; // services import AccountService from '@extension/services/AccountService'; +import PasswordService from '@extension/services/PasswordService'; import PrivateKeyService from '@extension/services/PrivateKeyService'; // types @@ -18,15 +19,16 @@ import type { IAccount, IAsyncThunkConfigWithRejectValue, INetwork, + IPasswordTag, IPrivateKey, IRegistrationRootState, } from '@extension/types'; import type { ISaveCredentialsPayload } from '../types'; // utils -import selectDefaultNetwork from '@extension/utils/selectDefaultNetwork'; import convertGenesisHashToHex from '@extension/utils/convertGenesisHashToHex'; import initializeARC0200AssetHoldingFromARC0200Asset from '@extension/utils/initializeARC0200AssetHoldingFromARC0200Asset'; +import selectDefaultNetwork from '@extension/utils/selectDefaultNetwork'; const saveCredentialsThunk: AsyncThunk< IAccount, // return @@ -37,11 +39,8 @@ const saveCredentialsThunk: AsyncThunk< ISaveCredentialsPayload, IAsyncThunkConfigWithRejectValue >( - RegisterThunkEnum.SaveCredentials, - async ( - { arc0200Assets, name, privateKey }, - { getState, rejectWithValue } - ) => { + ThunkEnum.SaveCredentials, + async ({ arc0200Assets, keyPair, name }, { getState, rejectWithValue }) => { const logger = getState().system.logger; const networks = getState().networks.items; const password = getState().registration.password; @@ -49,57 +48,79 @@ const saveCredentialsThunk: AsyncThunk< let accountService: AccountService; let defaultNetwork: INetwork; let encodedGenesisHash: string; - let encodedPublicKey: string; + let passwordService: PasswordService; + let passwordTagItem: IPasswordTag; let privateKeyItem: IPrivateKey | null; let privateKeyService: PrivateKeyService; if (!password) { - logger.error(`${RegisterThunkEnum.SaveCredentials}: no password found`); + logger.error(`${ThunkEnum.SaveCredentials}: no password found`); return rejectWithValue(new InvalidPasswordError()); } + passwordService = new PasswordService({ + logger, + passwordTag: browser.runtime.id, + }); + privateKeyService = new PrivateKeyService({ + logger, + }); + try { + // reset any previous keys/credentials + await passwordService.removeFromStorage(); + await privateKeyService.removeAllFromStorage(); + logger.debug( - `${RegisterThunkEnum.SaveCredentials}: inferring public key` + `${ThunkEnum.SaveCredentials}: saving password tag to storage` ); - encodedPublicKey = encodeHex( - sign.keyPair.fromSecretKey(privateKey).publicKey + // save the password and the keys + passwordTagItem = await passwordService.saveToStorage( + PasswordService.createPasswordTag({ + encryptedTag: await PasswordService.encryptBytes({ + data: encodeUtf8(passwordService.getPasswordTag()), + logger, + password, + }), + }) ); - privateKeyService = new PrivateKeyService({ - logger, - passwordTag: browser.runtime.id, - }); logger.debug( - `${RegisterThunkEnum.SaveCredentials}: saving private key, with encoded public key "${encodedPublicKey}", to storage` + `${ThunkEnum.SaveCredentials}: saving private key to storage` ); - // reset any previous credentials, set the password and the account - await privateKeyService.reset(); - await privateKeyService.setPassword(password); - - privateKeyItem = await privateKeyService.setPrivateKey( - privateKey, - password + privateKeyItem = await privateKeyService.saveToStorage( + PrivateKeyService.createPrivateKey({ + encryptedPrivateKey: await PasswordService.encryptBytes({ + data: keyPair.privateKey, + logger, + password, + }), + encryptionID: passwordTagItem.id, + encryptionMethod: EncryptionMethodEnum.Password, + publicKey: keyPair.publicKey, + }) ); } catch (error) { - logger.error(`${RegisterThunkEnum.SaveCredentials}: ${error.message}`); + logger.error(`${ThunkEnum.SaveCredentials}:`, error); + + // clean up, we errored + await passwordService.removeFromStorage(); + await privateKeyService.removeAllFromStorage(); return rejectWithValue(error); } logger.debug( - `${RegisterThunkEnum.SaveCredentials}: successfully saved credentials` + `${ThunkEnum.SaveCredentials}: successfully saved credentials` ); defaultNetwork = selectDefaultNetwork(networks); - encodedGenesisHash = convertGenesisHashToHex( - defaultNetwork.genesisHash - ).toUpperCase(); + encodedGenesisHash = convertGenesisHashToHex(defaultNetwork.genesisHash); account = AccountService.initializeDefaultAccount({ - publicKey: encodedPublicKey, + publicKey: privateKeyItem.publicKey, ...(privateKeyItem && { createdAt: privateKeyItem.createdAt, }), @@ -127,7 +148,7 @@ const saveCredentialsThunk: AsyncThunk< await accountService.saveAccounts([account]); logger.debug( - `${RegisterThunkEnum.SaveCredentials}: saved account for "${encodedPublicKey}" to storage` + `${ThunkEnum.SaveCredentials}: saved account for "${privateKeyItem.publicKey}" to storage` ); return account; diff --git a/src/extension/features/registration/types/ISaveCredentialsPayload.ts b/src/extension/features/registration/types/ISaveCredentialsPayload.ts index 5bfdc5db..835588a8 100644 --- a/src/extension/features/registration/types/ISaveCredentialsPayload.ts +++ b/src/extension/features/registration/types/ISaveCredentialsPayload.ts @@ -1,10 +1,13 @@ +// models +import Ed21559KeyPair from '@extension/models/Ed21559KeyPair'; + // types import type { IARC0200Asset } from '@extension/types'; interface ISaveCredentialsPayload { arc0200Assets: IARC0200Asset[]; + keyPair: Ed21559KeyPair; name: string | null; - privateKey: Uint8Array; } export default ISaveCredentialsPayload; diff --git a/src/extension/features/send-assets/thunks/createUnsignedTransactionsThunk.ts b/src/extension/features/send-assets/thunks/createUnsignedTransactionsThunk.ts index 2e7dad05..1c7311de 100644 --- a/src/extension/features/send-assets/thunks/createUnsignedTransactionsThunk.ts +++ b/src/extension/features/send-assets/thunks/createUnsignedTransactionsThunk.ts @@ -15,6 +15,7 @@ import { // services import AccountService from '@extension/services/AccountService'; +import PrivateKeyService from '@extension/services/PrivateKeyService'; // types import type { ILogger } from '@common/types'; @@ -30,10 +31,11 @@ import type { // utils import convertToAtomicUnit from '@common/utils/convertToAtomicUnit'; -import selectNetworkFromSettings from '@extension/utils/selectNetworkFromSettings'; +import convertPublicKeyToAVMAddress from '@extension/utils/convertPublicKeyToAVMAddress'; import createUnsignedARC0200TransferTransactions from '@extension/utils/createUnsignedARC0200TransferTransactions'; import createUnsignedPaymentTransactions from '@extension/utils/createUnsignedPaymentTransactions'; import createUnsignedStandardAssetTransferTransactions from '@extension/utils/createUnsignedStandardAssetTransferTransactions'; +import selectNetworkFromSettings from '@extension/utils/selectNetworkFromSettings'; const createUnsignedTransactionsThunk: AsyncThunk< Transaction[], // return @@ -78,8 +80,9 @@ const createUnsignedTransactionsThunk: AsyncThunk< fromAccount = getState().accounts.items.find( (value) => - AccountService.convertPublicKeyToAlgorandAddress(value.publicKey) === - fromAddress + convertPublicKeyToAVMAddress( + PrivateKeyService.decode(value.publicKey) + ) === fromAddress ) || null; if (!fromAccount) { diff --git a/src/extension/features/send-assets/thunks/submitTransactionThunk.ts b/src/extension/features/send-assets/thunks/submitTransactionThunk.ts index 47c47723..3f7a53c0 100644 --- a/src/extension/features/send-assets/thunks/submitTransactionThunk.ts +++ b/src/extension/features/send-assets/thunks/submitTransactionThunk.ts @@ -13,18 +13,16 @@ import { OfflineError, } from '@extension/errors'; -// services -import AccountService from '@extension/services/AccountService'; - // types import type { IAccount, IAsyncThunkConfigWithRejectValue, IMainRootState, } from '@extension/types'; -import type { ISubmitTransactionsThunkPayload } from '../types'; +import type { TSubmitTransactionsThunkPayload } from '../types'; // utils +import convertPublicKeyToAVMAddress from '@extension/utils/convertPublicKeyToAVMAddress'; import doesAccountFallBelowMinimumBalanceRequirementForTransactions from '@extension/utils/doesAccountFallBelowMinimumBalanceRequirementForTransactions'; import isAccountKnown from '@extension/utils/isAccountKnown'; import sendTransactionsForNetwork from '@extension/utils/sendTransactionsForNetwork'; @@ -33,15 +31,18 @@ import uniqueGenesisHashesFromTransactions from '@extension/utils/uniqueGenesisH const submitTransactionThunk: AsyncThunk< string[], // return - ISubmitTransactionsThunkPayload, // args + TSubmitTransactionsThunkPayload, // args IAsyncThunkConfigWithRejectValue > = createAsyncThunk< string[], - ISubmitTransactionsThunkPayload, + TSubmitTransactionsThunkPayload, IAsyncThunkConfigWithRejectValue >( SendAssetsThunkEnum.SubmitTransaction, - async ({ password, transactions }, { getState, rejectWithValue }) => { + async ( + { transactions, ...encryptionOptions }, + { getState, rejectWithValue } + ) => { const accounts = getState().accounts.items; const fromAddress = getState().sendAssets.fromAddress; const logger = getState().system.logger; @@ -65,9 +66,7 @@ const submitTransactionThunk: AsyncThunk< fromAccount = accounts.find( - (value) => - AccountService.convertPublicKeyToAlgorandAddress(value.publicKey) === - fromAddress + (value) => convertPublicKeyToAVMAddress(value.publicKey) === fromAddress ) || null; if (!fromAccount) { @@ -141,8 +140,8 @@ const submitTransactionThunk: AsyncThunk< authAccounts: accounts, logger, networks, - password, unsignedTransaction: value, + ...encryptionOptions, }) ) ); diff --git a/src/extension/features/send-assets/types/ISubmitTransactionsThunkPayload.ts b/src/extension/features/send-assets/types/ISubmitTransactionsThunkPayload.ts deleted file mode 100644 index c398bc2d..00000000 --- a/src/extension/features/send-assets/types/ISubmitTransactionsThunkPayload.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Transaction } from 'algosdk'; - -interface ISubmitTransactionsThunkPayload { - password: string; - transactions: Transaction[]; -} - -export default ISubmitTransactionsThunkPayload; diff --git a/src/extension/features/send-assets/types/TSubmitTransactionsThunkPayload.ts b/src/extension/features/send-assets/types/TSubmitTransactionsThunkPayload.ts new file mode 100644 index 00000000..5e4f5335 --- /dev/null +++ b/src/extension/features/send-assets/types/TSubmitTransactionsThunkPayload.ts @@ -0,0 +1,13 @@ +import type { Transaction } from 'algosdk'; + +// types +import type { TEncryptionCredentials } from '@extension/types'; + +interface ISubmitTransactionsThunkPayload { + transactions: Transaction[]; +} + +type TSubmitTransactionsThunkPayload = ISubmitTransactionsThunkPayload & + TEncryptionCredentials; + +export default TSubmitTransactionsThunkPayload; diff --git a/src/extension/features/send-assets/types/index.ts b/src/extension/features/send-assets/types/index.ts index 146aa70b..c3934d8c 100644 --- a/src/extension/features/send-assets/types/index.ts +++ b/src/extension/features/send-assets/types/index.ts @@ -1,3 +1,3 @@ export type { default as IInitializeSendAssetPayload } from './IInitializeSendAssetPayload'; export type { default as IState } from './IState'; -export type { default as ISubmitTransactionsThunkPayload } from './ISubmitTransactionsThunkPayload'; +export type { default as TSubmitTransactionsThunkPayload } from './TSubmitTransactionsThunkPayload'; diff --git a/src/extension/hooks/useChangePassword/types/IChangePasswordActionOptions.ts b/src/extension/hooks/useChangePassword/types/IChangePasswordActionOptions.ts new file mode 100644 index 00000000..b4a823cc --- /dev/null +++ b/src/extension/hooks/useChangePassword/types/IChangePasswordActionOptions.ts @@ -0,0 +1,6 @@ +interface IChangePasswordActionOptions { + currentPassword: string; + newPassword: string; +} + +export default IChangePasswordActionOptions; diff --git a/src/extension/hooks/useChangePassword/types/IReEncryptPrivateKeyItemWithDelayOptions.ts b/src/extension/hooks/useChangePassword/types/IReEncryptPrivateKeyItemWithDelayOptions.ts new file mode 100644 index 00000000..1782d3c4 --- /dev/null +++ b/src/extension/hooks/useChangePassword/types/IReEncryptPrivateKeyItemWithDelayOptions.ts @@ -0,0 +1,12 @@ +// types +import type { IBaseOptions } from '@common/types'; +import type { IPrivateKey } from '@extension/types'; + +interface IReEncryptPrivateKeyItemWithDelayOptions extends IBaseOptions { + currentPassword: string; + delay?: number; + newPassword: string; + privateKeyItem: IPrivateKey; +} + +export default IReEncryptPrivateKeyItemWithDelayOptions; diff --git a/src/extension/hooks/useChangePassword/types/IState.ts b/src/extension/hooks/useChangePassword/types/IState.ts new file mode 100644 index 00000000..2314f603 --- /dev/null +++ b/src/extension/hooks/useChangePassword/types/IState.ts @@ -0,0 +1,21 @@ +// errors +import { BaseExtensionError } from '@extension/errors'; + +// types +import type { IEncryptionState } from '@extension/components/ReEncryptKeysLoadingContent'; +import type { IPasswordTag } from '@extension/types'; +import type IChangePasswordActionOptions from './IChangePasswordActionOptions'; + +interface IState { + changePasswordAction: ( + options: IChangePasswordActionOptions + ) => Promise; + encryptionProgressState: IEncryptionState[]; + encrypting: boolean; + error: BaseExtensionError | null; + passwordTag: IPasswordTag | null; + resetAction: () => void; + validating: boolean; +} + +export default IState; diff --git a/src/extension/hooks/useChangePassword/types/IUseChangePasswordState.ts b/src/extension/hooks/useChangePassword/types/IUseChangePasswordState.ts deleted file mode 100644 index 3f9f91ff..00000000 --- a/src/extension/hooks/useChangePassword/types/IUseChangePasswordState.ts +++ /dev/null @@ -1,17 +0,0 @@ -// errors -import { BaseExtensionError } from '@extension/errors'; - -// types -import { IPasswordTag } from '@extension/types'; - -interface IUseChangePasswordState { - changePassword: ( - newPassword: string, - currentPassword: string - ) => Promise; - error: BaseExtensionError | null; - passwordTag: IPasswordTag | null; - saving: boolean; -} - -export default IUseChangePasswordState; diff --git a/src/extension/hooks/useChangePassword/types/index.ts b/src/extension/hooks/useChangePassword/types/index.ts index 6f4ed9fe..e83b4ab6 100644 --- a/src/extension/hooks/useChangePassword/types/index.ts +++ b/src/extension/hooks/useChangePassword/types/index.ts @@ -1 +1,3 @@ -export type { default as IUseChangePasswordState } from './IUseChangePasswordState'; +export type { default as IChangePasswordActionOptions } from './IChangePasswordActionOptions'; +export type { default as IReEncryptPrivateKeyItemWithDelayOptions } from './IReEncryptPrivateKeyItemWithDelayOptions'; +export type { default as IState } from './IState'; diff --git a/src/extension/hooks/useChangePassword/useChangePassword.ts b/src/extension/hooks/useChangePassword/useChangePassword.ts index 89956e66..9efa47fa 100644 --- a/src/extension/hooks/useChangePassword/useChangePassword.ts +++ b/src/extension/hooks/useChangePassword/useChangePassword.ts @@ -1,58 +1,210 @@ +import { encode as encodeUTF8 } from '@stablelib/utf8'; import { useState } from 'react'; import browser from 'webextension-polyfill'; // errors -import { BaseExtensionError } from '@extension/errors'; +import { + BaseExtensionError, + InvalidPasswordError, + MalformedDataError, +} from '@extension/errors'; // selectors -import { useSelectLogger } from '@extension/selectors'; +import { + useSelectLogger, + useSelectPasskeysEnabled, +} from '@extension/selectors'; // services +import PasswordService from '@extension/services/PasswordService'; import PrivateKeyService from '@extension/services/PrivateKeyService'; // types -import { ILogger } from '@common/types'; -import { IPasswordTag } from '@extension/types'; -import { IUseChangePasswordState } from './types'; +import type { IEncryptionState } from '@extension/components/ReEncryptKeysLoadingContent'; +import type { IPasswordTag, IPrivateKey } from '@extension/types'; +import type { IChangePasswordActionOptions, IState } from './types'; -export default function useChangePassword(): IUseChangePasswordState { - const logger: ILogger = useSelectLogger(); +// utils +import { encryptPrivateKeyItemWithDelay } from './utils'; + +export default function useChangePassword(): IState { + const _hookName = 'useChangePassword'; + // selectors + const logger = useSelectLogger(); + const passkeyEnabled = useSelectPasskeysEnabled(); + // states + const [encryptionProgressState, setEncryptionProgressState] = useState< + IEncryptionState[] + >([]); + const [encrypting, setEncrypting] = useState(false); const [error, setError] = useState(null); const [passwordTag, setPasswordTag] = useState(null); - const [saving, setSaving] = useState(false); - const changePassword: ( - newPassword: string, - currentPassword: string - ) => Promise = async (newPassword: string, currentPassword: string) => { - let newPasswordTag: IPasswordTag; + const [validating, setValidating] = useState(false); + // actions + const changePasswordAction = async ({ + currentPassword, + newPassword, + }: IChangePasswordActionOptions): Promise => { + const _functionName = 'changePasswordAction'; + const passwordService = new PasswordService({ + logger, + passwordTag: browser.runtime.id, + }); + let _error: string; + let isPasswordValid: boolean; + let passwordTag = await passwordService.fetchFromStorage(); + let privateKeyItems: IPrivateKey[]; let privateKeyService: PrivateKeyService; + // reset the previous values + resetAction(); + + setValidating(true); + + // if there is no password tag + if (!passwordTag) { + _error = `attempted to change password, but no previous password tag found`; + + logger.debug(`${_hookName}#${_functionName}: ${_error}`); + + setValidating(false); + setError(new MalformedDataError(_error)); + + return false; + } + + if (currentPassword === newPassword) { + logger.debug( + `${_hookName}#${_functionName}: passwords match, ignoring update` + ); + + setPasswordTag(passwordTag); + + return true; + } + + isPasswordValid = await passwordService.verifyPassword(currentPassword); + + if (!isPasswordValid) { + logger?.debug(`${_hookName}#${_functionName}: invalid password`); + + setValidating(false); + setError(new InvalidPasswordError()); + + return false; + } + + setValidating(false); + setEncrypting(true); + + // re-encrypt the password tag with the new password try { - setSaving(true); + passwordTag = { + ...passwordTag, + encryptedTag: PasswordService.encode( + await PasswordService.encryptBytes({ + data: encodeUTF8(passwordService.getPasswordTag()), + logger, + password: newPassword, + }) + ), + }; + } catch (error) { + setEncrypting(false); + setError(error); + + return false; + } + + logger?.debug( + `${_hookName}#${_functionName}: re-encrypted password tag "${passwordTag.id}"` + ); + + // only re-encrypt the keys if the passkey is not enabled + if (!passkeyEnabled) { + logger?.debug( + `${_hookName}#${_functionName}: re-encrypting private keys` + ); privateKeyService = new PrivateKeyService({ logger, - passwordTag: browser.runtime.id, }); + privateKeyItems = await privateKeyService.fetchAllFromStorage(); - newPasswordTag = await privateKeyService.setPassword( - newPassword, - currentPassword + // set the encryption state for each item to false + setEncryptionProgressState( + privateKeyItems.map(({ id }) => ({ + id, + encrypted: false, + })) ); - setError(null); - setPasswordTag(newPasswordTag); - } catch (error) { - setError(error); + // re-encrypt each private key items + try { + privateKeyItems = await Promise.all( + privateKeyItems.map(async (privateKeyItem, index) => { + const item = await encryptPrivateKeyItemWithDelay({ + currentPassword, + delay: (index + 1) * 300, // add a staggered delay for the ui to catch up + logger, + newPassword, + privateKeyItem, + }); + + setEncryptionProgressState((_encryptionProgressState) => + _encryptionProgressState.map((value) => + value.id === privateKeyItem.id + ? { + ...value, + encrypted: true, + } + : value + ) + ); + + return item; + }) + ); + } catch (error) { + setEncrypting(false); + setError(error); + + return false; + } + + // save the new encrypted items to storage + await privateKeyService.saveManyToStorage(privateKeyItems); + + logger?.debug(`${_hookName}#${_functionName}: re-encrypted private keys`); } - setSaving(false); + // save the new password tag to storage + passwordTag = await passwordService.saveToStorage(passwordTag); + + logger?.debug( + `${_hookName}#${_functionName}: successfully changed password` + ); + + setPasswordTag(passwordTag); + setEncrypting(false); + + return true; + }; + const resetAction = () => { + setEncryptionProgressState([]); + setEncrypting(false); + setError(null); + setPasswordTag(null); + setValidating(false); }; return { - changePassword, + changePasswordAction, + encryptionProgressState, + encrypting, error, passwordTag, - saving, + resetAction, + validating, }; } diff --git a/src/extension/hooks/useChangePassword/utils/encryptPrivateKeyItemWithDelay.ts b/src/extension/hooks/useChangePassword/utils/encryptPrivateKeyItemWithDelay.ts new file mode 100644 index 00000000..66d353be --- /dev/null +++ b/src/extension/hooks/useChangePassword/utils/encryptPrivateKeyItemWithDelay.ts @@ -0,0 +1,61 @@ +// services +import PasswordService from '@extension/services/PasswordService'; +import PrivateKeyService from '@extension/services/PrivateKeyService'; + +// types +import type { IPrivateKey } from '@extension/types'; +import type { IReEncryptPrivateKeyItemWithDelayOptions } from '../types'; + +export default async function encryptPrivateKeyItemWithDelay({ + currentPassword, + delay = 0, + logger, + newPassword, + privateKeyItem, +}: IReEncryptPrivateKeyItemWithDelayOptions): Promise { + const _functionName = 'reEncryptPrivateKeyItemWithDelay'; + + return new Promise((resolve, reject) => { + setTimeout(async () => { + let decryptedPrivateKey: Uint8Array; + let reEncryptedPrivateKey: Uint8Array; + let version: number = privateKeyItem.version; + + try { + decryptedPrivateKey = await PasswordService.decryptBytes({ + data: PrivateKeyService.decode(privateKeyItem.encryptedPrivateKey), + logger, + password: currentPassword, + }); // decrypt the private key with the current password + + // if the saved private key is a legacy item, it is using the "secret key" form - the private key concatenated to the public key + if (privateKeyItem.version <= 0) { + logger?.debug( + `${_functionName}: key "${privateKeyItem}" on legacy version "${privateKeyItem.version}", updating` + ); + + decryptedPrivateKey = + PrivateKeyService.extractPrivateKeyFromSecretKey( + decryptedPrivateKey + ); + version = PrivateKeyService.latestVersion; // update to the latest version + } + + reEncryptedPrivateKey = await PasswordService.encryptBytes({ + data: decryptedPrivateKey, + logger, + password: newPassword, + }); // re-encrypt the private key with the new password + } catch (error) { + return reject(error); + } + + return resolve({ + ...privateKeyItem, + encryptedPrivateKey: PrivateKeyService.encode(reEncryptedPrivateKey), + updatedAt: new Date().getTime(), + version, + }); + }, delay); + }); +} diff --git a/src/extension/hooks/useChangePassword/utils/index.ts b/src/extension/hooks/useChangePassword/utils/index.ts new file mode 100644 index 00000000..758b9fa5 --- /dev/null +++ b/src/extension/hooks/useChangePassword/utils/index.ts @@ -0,0 +1 @@ +export { default as encryptPrivateKeyItemWithDelay } from './encryptPrivateKeyItemWithDelay'; diff --git a/src/extension/hooks/useOnMainAppMessage/useOnMainAppMessage.ts b/src/extension/hooks/useOnMainAppMessage/useOnMainAppMessage.ts index 8cf57ff8..8226c7b9 100644 --- a/src/extension/hooks/useOnMainAppMessage/useOnMainAppMessage.ts +++ b/src/extension/hooks/useOnMainAppMessage/useOnMainAppMessage.ts @@ -7,7 +7,7 @@ import { ProviderMessageReferenceEnum } from '@common/enums'; // features import { handleNewEventByIdThunk } from '@extension/features/events'; -import { setPassword } from '@extension/features/password-lock'; +import { setCredentials as setPasswordLockCredentials } from '@extension/features/password-lock'; // messages import { ProviderEventAddedMessage } from '@common/messages'; @@ -37,8 +37,8 @@ export default function useOnMainAppMessage(): void { break; case ProviderMessageReferenceEnum.PasswordLockTimeout: - // remove the password - dispatch(setPassword(null)); + // remove the password lock credentials + dispatch(setPasswordLockCredentials(null)); break; default: diff --git a/src/extension/modals/AddAssetsModal/AddAssetsForWatchAccountModal.tsx b/src/extension/modals/AddAssetsModal/AddAssetsForWatchAccountModal.tsx index ec3af4c5..9b19dd6d 100644 --- a/src/extension/modals/AddAssetsModal/AddAssetsForWatchAccountModal.tsx +++ b/src/extension/modals/AddAssetsModal/AddAssetsForWatchAccountModal.tsx @@ -66,6 +66,7 @@ import { // services import AccountService from '@extension/services/AccountService'; +import PrivateKeyService from '@extension/services/PrivateKeyService'; import QuestsService, { QuestNameEnum, } from '@extension/services/QuestsService'; @@ -85,6 +86,7 @@ import type { // utils import convertGenesisHashToHex from '@extension/utils/convertGenesisHashToHex'; +import convertPublicKeyToAVMAddress from '@extension/utils/convertPublicKeyToAVMAddress'; import isNumericString from '@extension/utils/isNumericString'; import isReKeyedAuthAccountAvailable from '@extension/utils/isReKeyedAuthAccountAvailable'; @@ -176,7 +178,9 @@ const AddAssetsForWatchAccountModal: FC = ({ onClose }) => { // track the action if this is a new asset if (isNewSelectedAsset) { questsSent = await questsService.addARC0200AssetQuest( - AccountService.convertPublicKeyToAlgorandAddress(account.publicKey), + convertPublicKeyToAVMAddress( + PrivateKeyService.decode(account.publicKey) + ), { appID: selectedAsset.id, genesisHash: selectedNetwork.genesisHash, diff --git a/src/extension/modals/AddAssetsModal/AddAssetsModal.tsx b/src/extension/modals/AddAssetsModal/AddAssetsModal.tsx index 06832404..b7a35e98 100644 --- a/src/extension/modals/AddAssetsModal/AddAssetsModal.tsx +++ b/src/extension/modals/AddAssetsModal/AddAssetsModal.tsx @@ -11,14 +11,13 @@ import { ModalHeader, Spinner, Text, + useDisclosure, VStack, } from '@chakra-ui/react'; import React, { ChangeEvent, FC, - KeyboardEvent, ReactNode, - useEffect, useMemo, useRef, useState, @@ -30,9 +29,6 @@ import { useDispatch } from 'react-redux'; // components import Button from '@extension/components/Button'; import IconButton from '@extension/components/IconButton'; -import PasswordInput, { - usePassword, -} from '@extension/components/PasswordInput'; import AddAssetsARC0200AssetItem from './AddAssetsARC0200AssetItem'; import AddAssetsARC0200AssetSummaryModalContent from './AddAssetsARC0200AssetSummaryModalContent'; import AddAssetsConfirmingModalContent from './AddAssetsConfirmingModalContent'; @@ -45,6 +41,9 @@ import { BODY_BACKGROUND_COLOR, DEFAULT_GAP } from '@extension/constants'; // enums import { AssetTypeEnum, ErrorCodeEnum } from '@extension/enums'; +// errors +import { BaseExtensionError } from '@extension/errors'; + // features import { addARC0200AssetHoldingsThunk, @@ -69,6 +68,11 @@ import usePrimaryColor from '@extension/hooks/usePrimaryColor'; import usePrimaryColorScheme from '@extension/hooks/usePrimaryColorScheme'; import useIsNewSelectedAsset from './hooks/useIsNewSelectedAsset'; +// modals +import AuthenticationModal, { + TOnConfirmResult, +} from '@extension/modals/AuthenticationModal'; + // selectors import { useSelectAccounts, @@ -79,14 +83,13 @@ import { useSelectAddAssetsSelectedAsset, useSelectAddAssetsStandardAssets, useSelectLogger, - useSelectPasswordLockPassword, useSelectSettingsPreferredBlockExplorer, useSelectSelectedNetwork, - useSelectSettings, } from '@extension/selectors'; // services import AccountService from '@extension/services/AccountService'; +import PrivateKeyService from '@extension/services/PrivateKeyService'; import QuestsService, { QuestNameEnum, } from '@extension/services/QuestsService'; @@ -107,14 +110,19 @@ import type { // utils import convertGenesisHashToHex from '@extension/utils/convertGenesisHashToHex'; +import convertPublicKeyToAVMAddress from '@extension/utils/convertPublicKeyToAVMAddress'; import isNumericString from '@extension/utils/isNumericString'; import isReKeyedAuthAccountAvailable from '@extension/utils/isReKeyedAuthAccountAvailable'; const AddAssetsModal: FC = ({ onClose }) => { const { t } = useTranslation(); - const passwordInputRef = useRef(null); const dispatch = useDispatch(); const assetContainerRef = useRef(null); + const { + isOpen: isAuthenticationModalOpen, + onClose: onAuthenticationModalClose, + onOpen: onAuthenticationModalOpen, + } = useDisclosure(); // selectors const account = useSelectAddAssetsAccount(); const accounts = useSelectAccounts(); @@ -123,10 +131,8 @@ const AddAssetsModal: FC = ({ onClose }) => { const explorer = useSelectSettingsPreferredBlockExplorer(); const fetching = useSelectAddAssetsFetching(); const logger = useSelectLogger(); - const passwordLockPassword = useSelectPasswordLockPassword(); const selectedNetwork = useSelectSelectedNetwork(); const selectedAsset = useSelectAddAssetsSelectedAsset(); - const settings = useSelectSettings(); const standardAssets = useSelectAddAssetsStandardAssets(); // hooks const defaultTextColor = useDefaultTextColor(); @@ -154,14 +160,6 @@ const AddAssetsModal: FC = ({ onClose }) => { // if it has not been re-keyed, check if it is a watch account return !account.watchAccount; }, [account, accounts, selectedNetwork]); - const { - error: passwordError, - onChange: onPasswordChange, - reset: resetPassword, - setError: setPasswordError, - validate: validatePassword, - value: password, - } = usePassword(); const primaryColor = usePrimaryColor(); const primaryColorScheme = usePrimaryColorScheme(); // state @@ -217,7 +215,9 @@ const AddAssetsModal: FC = ({ onClose }) => { // track the action if this is a new asset if (isNewSelectedAsset) { questsSent = await questsService.addARC0200AssetQuest( - AccountService.convertPublicKeyToAlgorandAddress(account.publicKey), + convertPublicKeyToAVMAddress( + PrivateKeyService.decode(account.publicKey) + ), { appID: selectedAsset.id, genesisHash: selectedNetwork.genesisHash, @@ -275,9 +275,10 @@ const AddAssetsModal: FC = ({ onClose }) => { dispatch(setConfirming(false)); }; - const handleAddStandardAssetClick = async () => { - const _functionName: string = 'handleAddStandardAssetClick'; - let _password: string | null; + const handleAddStandardAssetClick = () => onAuthenticationModalOpen(); + const handleOnAuthenticationModalConfirm = async ( + result: TOnConfirmResult + ) => { let hasQuestBeenCompletedToday: boolean = false; let questsSent: boolean = false; let questsService: QuestsService; @@ -291,30 +292,6 @@ const AddAssetsModal: FC = ({ onClose }) => { return; } - // if there is no password lock - if (!settings.security.enablePasswordLock && !passwordLockPassword) { - // validate the password input - if (validatePassword()) { - logger.debug( - `${AddAssetsModal.name}#${_functionName}: password not valid` - ); - - return; - } - } - - _password = settings.security.enablePasswordLock - ? passwordLockPassword - : password; - - if (!_password) { - logger.debug( - `${AddAssetsModal.name}#${_functionName}: unable to use password from password lock, value is "null"` - ); - - return; - } - dispatch(setConfirming(true)); try { @@ -323,7 +300,7 @@ const AddAssetsModal: FC = ({ onClose }) => { accountId: account.id, assets: [selectedAsset], genesisHash: selectedNetwork.genesisHash, - password: _password, + ...result, }) ).unwrap(); @@ -338,7 +315,9 @@ const AddAssetsModal: FC = ({ onClose }) => { // track the action if this is a new asset if (isNewSelectedAsset) { questsSent = await questsService.addStandardAssetQuest( - AccountService.convertPublicKeyToAlgorandAddress(account.publicKey), + convertPublicKeyToAVMAddress( + PrivateKeyService.decode(account.publicKey) + ), { assetID: selectedAsset.id, genesisHash: selectedNetwork.genesisHash, @@ -370,10 +349,6 @@ const AddAssetsModal: FC = ({ onClose }) => { handleClose(); } catch (error) { switch (error.code) { - case ErrorCodeEnum.InvalidPasswordError: - setPasswordError(t('errors.inputs.invalidPassword')); - - break; case ErrorCodeEnum.OfflineError: dispatch( createNotification({ @@ -407,7 +382,6 @@ const AddAssetsModal: FC = ({ onClose }) => { dispatch(clearAssets()); }; const handleClose = () => { - resetPassword(); setQuery(''); setQueryARC0200AssetDispatch(null); setQueryStandardAssetDispatch(null); @@ -479,13 +453,18 @@ const AddAssetsModal: FC = ({ onClose }) => { ) ); }; - const handleKeyUpPasswordInput = async ( - event: KeyboardEvent - ) => { - if (event.key === 'Enter') { - await handleAddStandardAssetClick(); - } - }; + const handleOnAuthenticationError = (error: BaseExtensionError) => + dispatch( + createNotification({ + description: t('errors.descriptions.code', { + code: error.code, + context: error.code, + }), + ephemeral: true, + title: t('errors.titles.code', { context: error.code }), + type: 'error', + }) + ); const handleOnQueryChange = (event: ChangeEvent) => { setQuery(event.target.value); }; @@ -519,7 +498,6 @@ const AddAssetsModal: FC = ({ onClose }) => { }; const handlePreviousClick = () => { dispatch(setSelectedAsset(null)); - resetPassword(); }; const handleSelectAssetClick = (asset: IAssetTypes) => dispatch(setSelectedAsset(asset)); @@ -671,31 +649,18 @@ const AddAssetsModal: FC = ({ onClose }) => { // for standard assets, we need a password to authorize the opt-in transaction if (selectedAsset.type === AssetTypeEnum.Standard) { return ( - - {!settings.security.enablePasswordLock && !passwordLockPassword && ( - ('captions.mustEnterPasswordToAuthorizeOptIn')} - inputRef={passwordInputRef} - onChange={onPasswordChange} - onKeyUp={handleKeyUpPasswordInput} - value={password} - /> - )} + + {previousButtonNode} - - {previousButtonNode} - - - - + + ); } @@ -722,43 +687,43 @@ const AddAssetsModal: FC = ({ onClose }) => { ); }; - // only standard assets will have the password submit - useEffect(() => { - if ( - selectedAsset && - selectedAsset.type === AssetTypeEnum.Standard && - passwordInputRef.current - ) { - passwordInputRef.current.focus(); - } - }, [selectedAsset]); - return ( - - + {/*authentication modal*/} + ('captions.mustEnterPasswordToAuthorizeOptIn')} + /> + + - - - {t('headings.addAsset')} - - - - - {renderContent()} - - - {renderFooter()} - - + + + + {t('headings.addAsset')} + + + + + {renderContent()} + + + {renderFooter()} + + + ); }; diff --git a/src/extension/modals/AddAssetsModal/AddAssetsStandardAssetSummaryModalContent.tsx b/src/extension/modals/AddAssetsModal/AddAssetsStandardAssetSummaryModalContent.tsx index 86ad3c8a..b5ba1a0b 100644 --- a/src/extension/modals/AddAssetsModal/AddAssetsStandardAssetSummaryModalContent.tsx +++ b/src/extension/modals/AddAssetsModal/AddAssetsStandardAssetSummaryModalContent.tsx @@ -30,7 +30,7 @@ import useSubTextColor from '@extension/hooks/useSubTextColor'; import useAddAssetStandardAssetSummaryContent from './hooks/useAddAssetStandardAssetSummaryContent'; // services -import AccountService from '@extension/services/AccountService'; +import PrivateKeyService from '@extension/services/PrivateKeyService'; // types import type { IAddAssetsModalStandardAssetSummaryContentProps } from './types'; @@ -38,6 +38,7 @@ import type { IAddAssetsModalStandardAssetSummaryContentProps } from './types'; // utils import convertToStandardUnit from '@common/utils/convertToStandardUnit'; import formatCurrencyUnit from '@common/utils/formatCurrencyUnit'; +import convertPublicKeyToAVMAddress from '@extension/utils/convertPublicKeyToAVMAddress'; import createIconFromDataUri from '@extension/utils/createIconFromDataUri'; import isAccountKnown from '@extension/utils/isAccountKnown'; @@ -59,8 +60,9 @@ const AddAssetsStandardAssetSummaryModalContent: FC< const primaryButtonTextColor: string = usePrimaryButtonTextColor(); const subTextColor: string = useSubTextColor(); // misc - const accountAddress: string = - AccountService.convertPublicKeyToAlgorandAddress(account.publicKey); + const accountAddress: string = convertPublicKeyToAVMAddress( + PrivateKeyService.decode(account.publicKey) + ); const totalSupplyInStandardUnits: BigNumber = convertToStandardUnit( new BigNumber(asset.totalSupply), asset.decimals diff --git a/src/extension/modals/AddPasskeyModal/AddPasskeyModal.tsx b/src/extension/modals/AddPasskeyModal/AddPasskeyModal.tsx new file mode 100644 index 00000000..79933fb0 --- /dev/null +++ b/src/extension/modals/AddPasskeyModal/AddPasskeyModal.tsx @@ -0,0 +1,431 @@ +import { + Code, + Heading, + HStack, + Icon, + Modal, + ModalBody, + ModalContent, + ModalFooter, + ModalHeader, + Text, + useDisclosure, + VStack, +} from '@chakra-ui/react'; +import React, { FC, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import { GoShieldLock } from 'react-icons/go'; +import { IoKeyOutline } from 'react-icons/io5'; +import { Radio } from 'react-loader-spinner'; +import { useDispatch } from 'react-redux'; + +// components +import Button from '@extension/components/Button'; +import CopyIconButton from '@extension/components/CopyIconButton'; +import COSEAlgorithmBadge from '@extension/components/COSEAlgorithmBadge'; +import ModalItem from '@extension/components/ModalItem'; +import ModalTextItem from '@extension/components/ModalTextItem'; +import MoreInformationAccordion from '@extension/components/MoreInformationAccordion'; +import PasskeyCapabilities from '@extension/components/PasskeyCapabilities'; +import ReEncryptKeysLoadingContent from '@extension/components/ReEncryptKeysLoadingContent'; + +// constants +import { + BODY_BACKGROUND_COLOR, + DEFAULT_GAP, + MODAL_ITEM_HEIGHT, +} from '@extension/constants'; + +// features +import { create as createNotification } from '@extension/features/notifications'; + +// hooks +import useColorModeValue from '@extension/hooks/useColorModeValue'; +import useDefaultTextColor from '@extension/hooks/useDefaultTextColor'; +import useSubTextColor from '@extension/hooks/useSubTextColor'; +import useAddPasskey from './hooks/useAddPasskey'; + +// modals +import ConfirmPasswordModal from '@extension/modals/ConfirmPasswordModal'; + +// selectors +import { useSelectPasskeysSaving } from '@extension/selectors'; + +// theme +import { theme } from '@extension/theme'; + +// types +import type { IAppThunkDispatch } from '@extension/types'; +import type { IProps } from './types'; + +// utils +import calculateIconSize from '@extension/utils/calculateIconSize'; + +const AddPasskeyModal: FC = ({ addPasskey, onClose }) => { + const { t } = useTranslation(); + const dispatch = useDispatch(); + const { + isOpen: isConfirmPasswordModalOpen, + onClose: onConfirmPasswordModalClose, + onOpen: onConfirmPasswordModalOpen, + } = useDisclosure(); + const { + isOpen: isMoreInformationOpen, + onOpen: onMoreInformationOpen, + onClose: onMoreInformationClose, + } = useDisclosure(); + // selectors + const saving = useSelectPasskeysSaving(); + // hooks + const { + addPasskeyAction, + encrypting, + encryptionProgressState, + error, + passkey, + requesting, + resetAction: resetAddPasskeyAction, + } = useAddPasskey(); + const defaultTextColor = useDefaultTextColor(); + const primaryColorCode = useColorModeValue( + theme.colors.primaryLight['500'], + theme.colors.primaryDark['500'] + ); + const subTextColor = useSubTextColor(); + // misc + const isLoading = encrypting || requesting || saving; + // handlers + const handleCancelClick = async () => handleClose(); + const handleClose = () => { + onClose && onClose(); + + resetAddPasskeyAction(); + }; + const handleEncryptClick = () => onConfirmPasswordModalOpen(); + const handleOnConfirmPasswordModalConfirm = async (password: string) => { + let success: boolean; + + if (!addPasskey) { + return; + } + + success = await addPasskeyAction({ + passkey: addPasskey, + password, + }); + + if (success) { + // display a success notification + dispatch( + createNotification({ + description: t('captions.passkeyAdded', { + name: addPasskey.name, + }), + ephemeral: true, + title: t('headings.passkeyAdded'), + type: 'success', + }) + ); + + // close the modal + handleClose(); + } + }; + const handleMoreInformationToggle = (value: boolean) => + value ? onMoreInformationOpen() : onMoreInformationClose(); + // renders + const renderContent = () => { + const iconSize = calculateIconSize('xl'); + + if (!addPasskey) { + return; + } + + if (encrypting) { + return ( + + {/*loader*/} + + + ); + } + + if (requesting) { + return ( + + {/*loader*/} + + + {/*caption*/} + + {t('captions.requestingPasskeyPermission', { + name: addPasskey.name, + })} + + + ); + } + + return ( + + {/*icon*/} + + + {/*details*/} + + {/*name*/} + ('labels.name')}:`} + tooltipLabel={addPasskey.name} + value={addPasskey.name} + /> + + {/*credential id*/} + ('labels.credentialID')}:`} + value={ + + + {addPasskey.id} + + + {/*copy credential id button*/} + ('labels.copyCredentialID')} + tooltipLabel={t('labels.copyCredentialID')} + value={addPasskey.id} + /> + + } + /> + + {/*user id*/} + ('labels.userID')}:`} + value={ + + + {addPasskey.userID} + + + {/*copy user id button*/} + ('labels.copyUserID')} + tooltipLabel={t('labels.copyUserID')} + value={addPasskey.userID} + /> + + } + /> + + {/*capabilities*/} + ('labels.capabilities')}:`} + value={} + /> + + + + {/*public key*/} + ('labels.publicKey')}:`} + value={ + + + {addPasskey.publicKey || '-'} + + + {/*copy public key button*/} + {addPasskey.publicKey && ( + ('labels.copyPublicKey')} + tooltipLabel={t('labels.copyPublicKey')} + value={addPasskey.publicKey} + /> + )} + + } + /> + + {/*algorithm*/} + ('labels.algorithm')}:`} + value={} + /> + + + + {/*instructions*/} + + + {t('captions.encryptWithPasskeyInstruction1')} + + + + {t('captions.encryptWithPasskeyInstruction2')} + + + + + ); + }; + + // if there is an error from the hook, show a toast + useEffect(() => { + error && + dispatch( + createNotification({ + description: t('errors.descriptions.code', { + code: error.code, + context: error.code, + }), + ephemeral: true, + title: t('errors.titles.code', { context: error.code }), + type: 'error', + }) + ); + }, [error]); + // if we have the updated the passkey close the modal + useEffect(() => { + if (passkey) { + // display a notification + dispatch( + createNotification({ + description: t('captions.passkeyAdded', { + name: passkey.name, + }), + ephemeral: true, + title: t('headings.passkeyAdded'), + type: 'info', + }) + ); + + handleClose(); + } + }, [passkey]); + + return ( + <> + {/*confirm password modal*/} + ('captions.mustEnterPasswordToDecryptPrivateKeys')} + isOpen={isConfirmPasswordModalOpen} + onClose={onConfirmPasswordModalClose} + onConfirm={handleOnConfirmPasswordModalConfirm} + /> + + + + + + {t('headings.addPasskey')} + + + + + {renderContent()} + + + + + {/*cancel*/} + + + {/*encrypt*/} + + + + + + + ); +}; + +export default AddPasskeyModal; diff --git a/src/extension/modals/AddPasskeyModal/hooks/useAddPasskey/index.ts b/src/extension/modals/AddPasskeyModal/hooks/useAddPasskey/index.ts new file mode 100644 index 00000000..0f407556 --- /dev/null +++ b/src/extension/modals/AddPasskeyModal/hooks/useAddPasskey/index.ts @@ -0,0 +1 @@ +export { default } from './useAddPasskey'; diff --git a/src/extension/modals/AddPasskeyModal/hooks/useAddPasskey/types/IAddPasskeyActionOptions.ts b/src/extension/modals/AddPasskeyModal/hooks/useAddPasskey/types/IAddPasskeyActionOptions.ts new file mode 100644 index 00000000..42bb81b8 --- /dev/null +++ b/src/extension/modals/AddPasskeyModal/hooks/useAddPasskey/types/IAddPasskeyActionOptions.ts @@ -0,0 +1,13 @@ +// types +import type { IPasskeyCredential } from '@extension/types'; + +/** + * @property {IPasskeyCredential} passkey - the passkey credential to add. + * @property {string} password - the password used to encrypt the private keys. + */ +interface IAddPasskeyActionOptions { + passkey: IPasskeyCredential; + password: string; +} + +export default IAddPasskeyActionOptions; diff --git a/src/extension/modals/AddPasskeyModal/hooks/useAddPasskey/types/IEncryptPrivateKeyItemWithDelayOptions.ts b/src/extension/modals/AddPasskeyModal/hooks/useAddPasskey/types/IEncryptPrivateKeyItemWithDelayOptions.ts new file mode 100644 index 00000000..c5561958 --- /dev/null +++ b/src/extension/modals/AddPasskeyModal/hooks/useAddPasskey/types/IEncryptPrivateKeyItemWithDelayOptions.ts @@ -0,0 +1,13 @@ +// types +import type { IBaseOptions } from '@common/types'; +import type { IPasskeyCredential, IPrivateKey } from '@extension/types'; + +interface IEncryptPrivateKeyItemWithDelayOptions extends IBaseOptions { + delay?: number; + inputKeyMaterial: Uint8Array; + passkey: IPasskeyCredential; + password: string; + privateKeyItem: IPrivateKey; +} + +export default IEncryptPrivateKeyItemWithDelayOptions; diff --git a/src/extension/modals/AddPasskeyModal/hooks/useAddPasskey/types/IState.ts b/src/extension/modals/AddPasskeyModal/hooks/useAddPasskey/types/IState.ts new file mode 100644 index 00000000..bd0e7af9 --- /dev/null +++ b/src/extension/modals/AddPasskeyModal/hooks/useAddPasskey/types/IState.ts @@ -0,0 +1,19 @@ +// errors +import { BaseExtensionError } from '@extension/errors'; + +// types +import type { IEncryptionState } from '@extension/components/ReEncryptKeysLoadingContent'; +import type { IPasskeyCredential } from '@extension/types'; +import type IAddPasskeyActionOptions from './IAddPasskeyActionOptions'; + +interface IState { + addPasskeyAction: (options: IAddPasskeyActionOptions) => Promise; + encryptionProgressState: IEncryptionState[]; + encrypting: boolean; + error: BaseExtensionError | null; + passkey: IPasskeyCredential | null; + requesting: boolean; + resetAction: () => void; +} + +export default IState; diff --git a/src/extension/modals/AddPasskeyModal/hooks/useAddPasskey/types/index.ts b/src/extension/modals/AddPasskeyModal/hooks/useAddPasskey/types/index.ts new file mode 100644 index 00000000..c9ef3b1e --- /dev/null +++ b/src/extension/modals/AddPasskeyModal/hooks/useAddPasskey/types/index.ts @@ -0,0 +1,3 @@ +export type { default as IAddPasskeyActionOptions } from './IAddPasskeyActionOptions'; +export type { default as IEncryptPrivateKeyItemWithDelayOptions } from './IEncryptPrivateKeyItemWithDelayOptions'; +export type { default as IState } from './IState'; diff --git a/src/extension/modals/AddPasskeyModal/hooks/useAddPasskey/useAddPasskey.ts b/src/extension/modals/AddPasskeyModal/hooks/useAddPasskey/useAddPasskey.ts new file mode 100644 index 00000000..c434fccf --- /dev/null +++ b/src/extension/modals/AddPasskeyModal/hooks/useAddPasskey/useAddPasskey.ts @@ -0,0 +1,179 @@ +import { useState } from 'react'; +import { useDispatch } from 'react-redux'; +import browser from 'webextension-polyfill'; + +// errors +import { BaseExtensionError, InvalidPasswordError } from '@extension/errors'; + +// features +import { saveToStorageThunk as savePasskeyToStorageThunk } from '@extension/features/passkeys'; + +// selectors +import { useSelectLogger } from '@extension/selectors'; + +// services +import PasskeyService from '@extension/services/PasskeyService'; +import PasswordService from '@extension/services/PasswordService'; +import PrivateKeyService from '@extension/services/PrivateKeyService'; + +// types +import type { IEncryptionState } from '@extension/components/ReEncryptKeysLoadingContent'; +import type { + IAppThunkDispatch, + IPasskeyCredential, + IPrivateKey, +} from '@extension/types'; +import type { IAddPasskeyActionOptions, IState } from './types'; + +// utils +import { encryptPrivateKeyItemWithDelay } from './utils'; + +export default function useAddPasskey(): IState { + const _hookName = 'useAddPasskey'; + const dispatch = useDispatch(); + // selectors + const logger = useSelectLogger(); + // states + const [encryptionProgressState, setEncryptionProgressState] = useState< + IEncryptionState[] + >([]); + const [encrypting, setEncrypting] = useState(false); + const [error, setError] = useState(null); + const [passkey, setPasskey] = useState(null); + const [requesting, setRequesting] = useState(false); + // actions + const addPasskeyAction = async ({ + passkey, + password, + }: IAddPasskeyActionOptions): Promise => { + const _functionName = 'addPasskeyAction'; + const passwordService = new PasswordService({ + logger, + passwordTag: browser.runtime.id, + }); + let _passkey: IPasskeyCredential; + let inputKeyMaterial: Uint8Array; + let isPasswordValid: boolean; + let privateKeyItems: IPrivateKey[]; + let privateKeyService: PrivateKeyService; + + // reset the previous values + resetAction(); + + isPasswordValid = await passwordService.verifyPassword(password); + + if (!isPasswordValid) { + logger?.debug(`${_hookName}#${_functionName}: invalid password`); + + setError(new InvalidPasswordError()); + + return false; + } + + setRequesting(true); + + logger.debug( + `${_hookName}#${_functionName}: requesting input key material from passkey "${passkey.id}"` + ); + + try { + // fetch the encryption key material + inputKeyMaterial = await PasskeyService.fetchInputKeyMaterialFromPasskey({ + credential: passkey, + logger, + }); + } catch (error) { + logger?.debug(`${_hookName}#${_functionName}:`, error); + + setRequesting(false); + setError(error); + + return false; + } + + setRequesting(false); + setEncrypting(true); + + privateKeyService = new PrivateKeyService({ + logger, + }); + privateKeyItems = await privateKeyService.fetchAllFromStorage(); + + // set the encryption state for each item to false + setEncryptionProgressState( + privateKeyItems.map(({ id }) => ({ + id, + encrypted: false, + })) + ); + + // re-encrypt each private key items with the passkey + try { + privateKeyItems = await Promise.all( + privateKeyItems.map(async (privateKeyItem, index) => { + const item = await encryptPrivateKeyItemWithDelay({ + delay: (index + 1) * 300, // add a staggered delay for the ui to catch up + inputKeyMaterial, + logger, + passkey, + password, + privateKeyItem, + }); + + // update the encryption state + setEncryptionProgressState((_encryptionProgressState) => + _encryptionProgressState.map((value) => + value.id === privateKeyItem.id + ? { + ...value, + encrypted: true, + } + : value + ) + ); + + return item; + }) + ); + } catch (error) { + logger?.debug(`${_hookName}#${_functionName}:`, error); + + setEncrypting(false); + setError(error); + + return false; + } + + // save the new encrypted items to storage + await privateKeyService.saveManyToStorage(privateKeyItems); + + // save the new passkey to storage + _passkey = await dispatch(savePasskeyToStorageThunk(passkey)).unwrap(); + + logger?.debug( + `${_hookName}#${_functionName}: successfully enabled passkey` + ); + + setPasskey(_passkey); + setEncrypting(false); + + return true; + }; + const resetAction = () => { + setEncryptionProgressState([]); + setEncrypting(false); + setError(null); + setPasskey(null); + setRequesting(false); + }; + + return { + addPasskeyAction, + encryptionProgressState, + encrypting, + error, + passkey, + requesting, + resetAction, + }; +} diff --git a/src/extension/modals/AddPasskeyModal/hooks/useAddPasskey/utils/encryptPrivateKeyItemWithDelay.ts b/src/extension/modals/AddPasskeyModal/hooks/useAddPasskey/utils/encryptPrivateKeyItemWithDelay.ts new file mode 100644 index 00000000..8575eb04 --- /dev/null +++ b/src/extension/modals/AddPasskeyModal/hooks/useAddPasskey/utils/encryptPrivateKeyItemWithDelay.ts @@ -0,0 +1,75 @@ +// enums +import { EncryptionMethodEnum } from '@extension/enums'; + +// services +import PasskeyService from '@extension/services/PasskeyService'; +import PasswordService from '@extension/services/PasswordService'; +import PrivateKeyService from '@extension/services/PrivateKeyService'; + +// types +import type { IPrivateKey } from '@extension/types'; +import type { IEncryptPrivateKeyItemWithDelayOptions } from '../types'; + +/** + * Convenience function that decrypts a private key item with the password and re-encrypts with the passkey. + * @param {IEncryptPrivateKeyItemWithDelayOptions} options - the password, the passkey credential, the input key + * material to derive a passkey encryption key, the private key item to decrypt/encrypt and an optional delay. + * @returns {IPrivateKey} a re-encrypted private key item. + */ +export default async function encryptPrivateKeyItemWithDelay({ + delay = 0, + inputKeyMaterial, + logger, + passkey, + password, + privateKeyItem, +}: IEncryptPrivateKeyItemWithDelayOptions): Promise { + const _functionName = 'encryptPrivateKeyItemWithDelay'; + + return new Promise((resolve, reject) => { + setTimeout(async () => { + let decryptedPrivateKey: Uint8Array; + let reEncryptedPrivateKey: Uint8Array; + let version: number = privateKeyItem.version; + + try { + decryptedPrivateKey = await PasswordService.decryptBytes({ + data: PrivateKeyService.decode(privateKeyItem.encryptedPrivateKey), + logger, + password, + }); // decrypt the private key with the current password + + // if the saved private key is a legacy item, it is using the "secret key" form - the private key concatenated to the public key + if (privateKeyItem.version <= 0) { + logger?.debug( + `${_functionName}: key "${privateKeyItem}" on legacy version "${privateKeyItem.version}", updating` + ); + + decryptedPrivateKey = + PrivateKeyService.extractPrivateKeyFromSecretKey( + decryptedPrivateKey + ); + version = PrivateKeyService.latestVersion; // update to the latest version + } + + reEncryptedPrivateKey = await PasskeyService.encryptBytes({ + bytes: decryptedPrivateKey, + inputKeyMaterial, + passkey, + logger, + }); // re-encrypt the private key with the new password + } catch (error) { + return reject(error); + } + + return resolve({ + ...privateKeyItem, + encryptedPrivateKey: PrivateKeyService.encode(reEncryptedPrivateKey), + encryptionID: passkey.id, + encryptionMethod: EncryptionMethodEnum.Passkey, + updatedAt: new Date().getTime(), + version, + }); + }, delay); + }); +} diff --git a/src/extension/modals/AddPasskeyModal/hooks/useAddPasskey/utils/index.ts b/src/extension/modals/AddPasskeyModal/hooks/useAddPasskey/utils/index.ts new file mode 100644 index 00000000..758b9fa5 --- /dev/null +++ b/src/extension/modals/AddPasskeyModal/hooks/useAddPasskey/utils/index.ts @@ -0,0 +1 @@ +export { default as encryptPrivateKeyItemWithDelay } from './encryptPrivateKeyItemWithDelay'; diff --git a/src/extension/modals/AddPasskeyModal/index.ts b/src/extension/modals/AddPasskeyModal/index.ts new file mode 100644 index 00000000..4a4737bd --- /dev/null +++ b/src/extension/modals/AddPasskeyModal/index.ts @@ -0,0 +1 @@ +export { default } from './AddPasskeyModal'; diff --git a/src/extension/modals/AddPasskeyModal/types/IProps.ts b/src/extension/modals/AddPasskeyModal/types/IProps.ts new file mode 100644 index 00000000..e25289de --- /dev/null +++ b/src/extension/modals/AddPasskeyModal/types/IProps.ts @@ -0,0 +1,8 @@ +// types +import type { IModalProps, IPasskeyCredential } from '@extension/types'; + +interface IProps extends IModalProps { + addPasskey: IPasskeyCredential | null; +} + +export default IProps; diff --git a/src/extension/modals/AddPasskeyModal/types/index.ts b/src/extension/modals/AddPasskeyModal/types/index.ts new file mode 100644 index 00000000..f404deed --- /dev/null +++ b/src/extension/modals/AddPasskeyModal/types/index.ts @@ -0,0 +1 @@ +export type { default as IProps } from './IProps'; diff --git a/src/extension/modals/AuthenticationModal/AuthenticationModal.tsx b/src/extension/modals/AuthenticationModal/AuthenticationModal.tsx new file mode 100644 index 00000000..8d513bb5 --- /dev/null +++ b/src/extension/modals/AuthenticationModal/AuthenticationModal.tsx @@ -0,0 +1,307 @@ +import { + HStack, + Modal, + ModalBody, + ModalContent, + ModalFooter, + ModalOverlay, + Spinner, + Text, + VStack, +} from '@chakra-ui/react'; +import React, { FC, KeyboardEvent, useEffect, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Radio } from 'react-loader-spinner'; +import browser from 'webextension-polyfill'; + +// components +import Button from '@extension/components/Button'; +import PasswordInput, { + usePassword, +} from '@extension/components/PasswordInput'; + +// constants +import { BODY_BACKGROUND_COLOR, DEFAULT_GAP } from '@extension/constants'; + +// enums +import { EncryptionMethodEnum } from '@extension/enums'; + +// errors + +// hooks +import useColorModeValue from '@extension/hooks/useColorModeValue'; +import useSubTextColor from '@extension/hooks/useSubTextColor'; + +// selectors +import { + useSelectLogger, + useSelectPasskeysPasskey, + useSelectPasswordLockCredentials, + useSelectSettings, +} from '@extension/selectors'; + +// services +import PasskeyService from '@extension/services/PasskeyService'; +import PasswordService from '@extension/services/PasswordService'; + +// theme +import { theme } from '@extension/theme'; + +// types +import type { IProps } from './types'; +import useDefaultTextColor from '@extension/hooks/useDefaultTextColor'; + +const AuthenticationModal: FC = ({ + isOpen, + onClose, + onConfirm, + onError, + passwordHint, +}) => { + const { t } = useTranslation(); + const passwordInputRef = useRef(null); + // selectors + const logger = useSelectLogger(); + const passkey = useSelectPasskeysPasskey(); + const passwordLockCredentials = useSelectPasswordLockCredentials(); + const settings = useSelectSettings(); + // hooks + const defaultTextColor = useDefaultTextColor(); + const primaryColorCode = useColorModeValue( + theme.colors.primaryLight['500'], + theme.colors.primaryDark['500'] + ); + const { + error: passwordError, + onChange: onPasswordChange, + reset: resetPassword, + setError: setPasswordError, + validate: validatePassword, + value: password, + } = usePassword(); + const subTextColor = useSubTextColor(); + // states + const [verifying, setVerifying] = useState(false); + // misc + const reset = () => { + resetPassword(); + setVerifying(false); + }; + // handlers + const handleCancelClick = () => handleClose(); + const handleConfirmClick = async () => { + let isValid: boolean; + let passwordService: PasswordService; + + // check if the input is valid + if (validatePassword()) { + return; + } + + passwordService = new PasswordService({ + logger, + passwordTag: browser.runtime.id, + }); + + setVerifying(true); + + isValid = await passwordService.verifyPassword(password); + + setVerifying(false); + + if (!isValid) { + setPasswordError(t('errors.inputs.invalidPassword')); + + return; + } + + onConfirm({ + password, + type: EncryptionMethodEnum.Password, + }); + + handleClose(); + }; + const handleClose = () => { + onClose && onClose(); + + reset(); // clean up + }; + const handleKeyUpPasswordInput = async ( + event: KeyboardEvent + ) => { + if (event.key === 'Enter') { + await handleConfirmClick(); + } + }; + // renders + const renderContent = () => { + // show a loader for passkeys + if (passkey) { + return ( + + {/*passkey loader*/} + + + {/*caption*/} + + {t('captions.requestingPasskeyPermission', { + name: passkey.name, + })} + + + ); + } + + // show a loader if there is a password lock and password + if (settings.security.enablePasswordLock && passwordLockCredentials) { + return ( + + {/*loader*/} + + + {/*caption*/} + + {t('captions.checkingAuthenticationCredentials')} + + + ); + } + + return ( + + ('captions.mustEnterPasswordToConfirm') + } + inputRef={passwordInputRef} + onChange={onPasswordChange} + onKeyUp={handleKeyUpPasswordInput} + value={password || ''} + /> + + ); + }; + + // set focus when opening + useEffect(() => { + if (passwordInputRef.current) { + passwordInputRef.current.focus(); + } + }, []); + useEffect(() => { + (async () => { + let inputKeyMaterial: Uint8Array; + + if (!isOpen) { + return; + } + + // if there is a passkey, attempt to fetch the passkey input key material + if (passkey) { + try { + // fetch the encryption key material + inputKeyMaterial = + await PasskeyService.fetchInputKeyMaterialFromPasskey({ + credential: passkey, + logger, + }); + + onConfirm({ + inputKeyMaterial, + type: EncryptionMethodEnum.Passkey, + }); + + return handleClose(); + } catch (error) { + logger.error(`${AuthenticationModal.name}#useEffect:`, error); + + return onError && onError(error); + } + } + + // otherwise, check if there is a password lock and passkey/password present + if (settings.security.enablePasswordLock && passwordLockCredentials) { + return onConfirm(passwordLockCredentials); + } + })(); + }, [isOpen]); + + return ( + + + + + {/*content*/} + {renderContent()} + + {/*footer*/} + {!passkey && ( + + + + + + + + )} + + + ); +}; + +export default AuthenticationModal; diff --git a/src/extension/modals/AuthenticationModal/index.ts b/src/extension/modals/AuthenticationModal/index.ts new file mode 100644 index 00000000..6aa58ab4 --- /dev/null +++ b/src/extension/modals/AuthenticationModal/index.ts @@ -0,0 +1,2 @@ +export { default } from './AuthenticationModal'; +export * from './types'; diff --git a/src/extension/modals/AuthenticationModal/types/IProps.ts b/src/extension/modals/AuthenticationModal/types/IProps.ts new file mode 100644 index 00000000..a03dc8a7 --- /dev/null +++ b/src/extension/modals/AuthenticationModal/types/IProps.ts @@ -0,0 +1,15 @@ +// errors +import { BaseExtensionError } from '@extension/errors'; + +// types +import type { IModalProps } from '@extension/types'; +import type TOnConfirmResult from './TOnConfirmResult'; + +interface IProps extends IModalProps { + isOpen: boolean; + passwordHint?: string; + onConfirm: (result: TOnConfirmResult) => void; + onError?: (error: BaseExtensionError) => void; +} + +export default IProps; diff --git a/src/extension/modals/AuthenticationModal/types/TOnConfirmResult.ts b/src/extension/modals/AuthenticationModal/types/TOnConfirmResult.ts new file mode 100644 index 00000000..0e08d9a7 --- /dev/null +++ b/src/extension/modals/AuthenticationModal/types/TOnConfirmResult.ts @@ -0,0 +1,14 @@ +// enums +import { EncryptionMethodEnum } from '@extension/enums'; + +type TOnConfirmResult = + | { + password: string; + type: EncryptionMethodEnum.Password; + } + | { + inputKeyMaterial: Uint8Array; + type: EncryptionMethodEnum.Passkey; + }; + +export default TOnConfirmResult; diff --git a/src/extension/modals/AuthenticationModal/types/index.ts b/src/extension/modals/AuthenticationModal/types/index.ts new file mode 100644 index 00000000..84e7f139 --- /dev/null +++ b/src/extension/modals/AuthenticationModal/types/index.ts @@ -0,0 +1,2 @@ +export type { default as IProps } from './IProps'; +export type { default as TOnConfirmResult } from './TOnConfirmResult'; diff --git a/src/extension/modals/ChangePasswordLoadingModal/ChangePasswordLoadingModal.tsx b/src/extension/modals/ChangePasswordLoadingModal/ChangePasswordLoadingModal.tsx new file mode 100644 index 00000000..4f9fa1e9 --- /dev/null +++ b/src/extension/modals/ChangePasswordLoadingModal/ChangePasswordLoadingModal.tsx @@ -0,0 +1,52 @@ +import { Modal, ModalBody, ModalContent, ModalOverlay } from '@chakra-ui/react'; +import React, { FC } from 'react'; + +// components +import ReEncryptKeysLoadingContent from '@extension/components/ReEncryptKeysLoadingContent'; + +// constants +import { BODY_BACKGROUND_COLOR, DEFAULT_GAP } from '@extension/constants'; + +// theme +import { theme } from '@extension/theme'; + +// types +import type { IProps } from './types'; + +const ChangePasswordLoadingModal: FC = ({ + encryptionProgressState, + isOpen, + onClose, +}) => { + // handlers + const handleCLose = () => onClose && onClose(); + + return ( + + + + + + + + + + ); +}; + +export default ChangePasswordLoadingModal; diff --git a/src/extension/modals/ChangePasswordLoadingModal/index.ts b/src/extension/modals/ChangePasswordLoadingModal/index.ts new file mode 100644 index 00000000..54506a8b --- /dev/null +++ b/src/extension/modals/ChangePasswordLoadingModal/index.ts @@ -0,0 +1 @@ +export { default } from './ChangePasswordLoadingModal'; diff --git a/src/extension/modals/ChangePasswordLoadingModal/types/IProps.ts b/src/extension/modals/ChangePasswordLoadingModal/types/IProps.ts new file mode 100644 index 00000000..c79d184f --- /dev/null +++ b/src/extension/modals/ChangePasswordLoadingModal/types/IProps.ts @@ -0,0 +1,10 @@ +// types +import type { IEncryptionState } from '@extension/components/ReEncryptKeysLoadingContent'; +import type { IModalProps } from '@extension/types'; + +interface IProps extends IModalProps { + encryptionProgressState: IEncryptionState[]; + isOpen: boolean; +} + +export default IProps; diff --git a/src/extension/modals/ChangePasswordLoadingModal/types/index.ts b/src/extension/modals/ChangePasswordLoadingModal/types/index.ts new file mode 100644 index 00000000..f404deed --- /dev/null +++ b/src/extension/modals/ChangePasswordLoadingModal/types/index.ts @@ -0,0 +1 @@ +export type { default as IProps } from './IProps'; diff --git a/src/extension/modals/ConfirmPasswordModal/ConfirmPasswordModal.tsx b/src/extension/modals/ConfirmPasswordModal/ConfirmPasswordModal.tsx index 08689dd7..226be946 100644 --- a/src/extension/modals/ConfirmPasswordModal/ConfirmPasswordModal.tsx +++ b/src/extension/modals/ConfirmPasswordModal/ConfirmPasswordModal.tsx @@ -5,16 +5,11 @@ import { ModalContent, ModalFooter, ModalOverlay, + Spinner, + Text, VStack, } from '@chakra-ui/react'; -import React, { - FC, - KeyboardEvent, - MutableRefObject, - useEffect, - useRef, - useState, -} from 'react'; +import React, { FC, KeyboardEvent, useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import browser from 'webextension-polyfill'; @@ -27,35 +22,43 @@ import PasswordInput, { // constants import { BODY_BACKGROUND_COLOR, DEFAULT_GAP } from '@extension/constants'; +// enums +import { EncryptionMethodEnum } from '@extension/enums'; + +// hooks +import useDefaultTextColor from '@extension/hooks/useDefaultTextColor'; +import useSubTextColor from '@extension/hooks/useSubTextColor'; + // selectors -import { useSelectLogger } from '@extension/selectors'; +import { + useSelectLogger, + useSelectPasswordLockCredentials, + useSelectSettings, +} from '@extension/selectors'; // services -import PrivateKeyService from '@extension/services/PrivateKeyService'; +import PasswordService from '@extension/services/PasswordService'; // theme import { theme } from '@extension/theme'; // types -import { ILogger } from '@common/types'; - -interface IProps { - hint?: string; - isOpen: boolean; - onCancel: () => void; - onConfirm: (password: string) => void; -} +import type { IProps } from './types'; const ConfirmPasswordModal: FC = ({ - hint, isOpen, - onCancel, + hint, + onClose, onConfirm, -}: IProps) => { +}) => { const { t } = useTranslation(); - const passwordInputRef: MutableRefObject = - useRef(null); + const passwordInputRef = useRef(null); + // selectors + const logger = useSelectLogger(); + const passwordLockCredentials = useSelectPasswordLockCredentials(); + const settings = useSelectSettings(); // hooks + const defaultTextColor = useDefaultTextColor(); const { error: passwordError, onChange: onPasswordChange, @@ -64,9 +67,8 @@ const ConfirmPasswordModal: FC = ({ validate: validatePassword, value: password, } = usePassword(); - // selectors - const logger: ILogger = useSelectLogger(); - // state + const subTextColor = useSubTextColor(); + // states const [verifying, setVerifying] = useState(false); // misc const reset = () => { @@ -77,21 +79,21 @@ const ConfirmPasswordModal: FC = ({ const handleCancelClick = () => handleClose(); const handleConfirmClick = async () => { let isValid: boolean; - let privateKeyService: PrivateKeyService; + let passwordService: PasswordService; // check if the input is valid if (validatePassword()) { return; } - privateKeyService = new PrivateKeyService({ + passwordService = new PasswordService({ logger, passwordTag: browser.runtime.id, }); setVerifying(true); - isValid = await privateKeyService.verifyPassword(password); + isValid = await passwordService.verifyPassword(password); setVerifying(false); @@ -102,15 +104,12 @@ const ConfirmPasswordModal: FC = ({ } onConfirm(password); - - // clean up - reset(); + handleClose(); }; const handleClose = () => { - onCancel(); + onClose && onClose(); - // clean up - reset(); + reset(); // clean up }; const handleKeyUpPasswordInput = async ( event: KeyboardEvent @@ -119,6 +118,49 @@ const ConfirmPasswordModal: FC = ({ await handleConfirmClick(); } }; + // renders + const renderContent = () => { + // show a loader if there is a password lock and password + if (settings.security.enablePasswordLock && passwordLockCredentials) { + return ( + + {/*loader*/} + + + {/*caption*/} + + {t('captions.checkingAuthenticationCredentials')} + + + ); + } + + return ( + + ('captions.mustEnterPasswordToConfirm')} + inputRef={passwordInputRef} + onChange={onPasswordChange} + onKeyUp={handleKeyUpPasswordInput} + value={password || ''} + /> + + ); + }; // set focus when opening useEffect(() => { @@ -126,9 +168,20 @@ const ConfirmPasswordModal: FC = ({ passwordInputRef.current.focus(); } }, []); + // check if there is a password lock and password lock password present + useEffect(() => { + if ( + settings.security.enablePasswordLock && + passwordLockCredentials?.type === EncryptionMethodEnum.Password + ) { + onConfirm(passwordLockCredentials.password); + handleClose(); + } + }, [isOpen]); return ( = ({ minH={0} > {/*content*/} - - - ('captions.mustEnterPasswordToConfirm')} - inputRef={passwordInputRef} - onChange={onPasswordChange} - onKeyUp={handleKeyUpPasswordInput} - value={password || ''} - /> - - + {renderContent()} {/*footer*/} - + - ) : ( - - )} - - + + {cancelButtonNode} + + + ); } @@ -401,35 +317,50 @@ const ReKeyAccountModal: FC = ({ onClose }) => { }; return ( - - + {/*authentication modal*/} + ( + reKeyType === 'undo' + ? 'captions.mustEnterPasswordToAuthorizeUndoReKey' + : 'captions.mustEnterPasswordToAuthorizeReKey' + )} + /> + + - - - {t( - accountInformation && reKeyType === 'undo' - ? 'headings.undoReKey' - : 'headings.reKeyAccount' - )} - - - - - {renderContent()} - - - {renderFooter()} - - + + + + {t( + accountInformation && reKeyType === 'undo' + ? 'headings.undoReKey' + : 'headings.reKeyAccount' + )} + + + + + {renderContent()} + + + {renderFooter()} + + + ); }; diff --git a/src/extension/modals/ReKeyAccountModal/ReKeyAccountModalContent.tsx b/src/extension/modals/ReKeyAccountModal/ReKeyAccountModalContent.tsx index 048a6d77..87958f51 100644 --- a/src/extension/modals/ReKeyAccountModal/ReKeyAccountModalContent.tsx +++ b/src/extension/modals/ReKeyAccountModal/ReKeyAccountModalContent.tsx @@ -17,13 +17,11 @@ import { DEFAULT_GAP } from '@extension/constants'; import useDefaultTextColor from '@extension/hooks/useDefaultTextColor'; import useSubTextColor from '@extension/hooks/useSubTextColor'; -// services -import AccountService from '@extension/services/AccountService'; - // types import type { IReKeyAccountModalContentProps } from './types'; // utils +import convertPublicKeyToAVMAddress from '@extension/utils/convertPublicKeyToAVMAddress'; import createIconFromDataUri from '@extension/utils/createIconFromDataUri'; const ReKeyAccountConfirmingModalContent: FC< @@ -58,9 +56,7 @@ const ReKeyAccountConfirmingModalContent: FC< value={ = ({ @@ -51,9 +49,7 @@ const UndoReKeyAccountModalContent: FC = ({ value={ Promise; @@ -124,8 +128,8 @@ const ConfirmAccountModalContent: FC = ({ await onComplete({ arc0200Assets: assets, + keyPair: Ed21559KeyPair.generateFromPrivateKey(privateKey), name: null, - privateKey, }); }; @@ -134,7 +138,7 @@ const ConfirmAccountModalContent: FC = ({ decodePrivateKeyFromAccountImportSchema(schema); if (privateKey) { - setAddress(convertPrivateKeyToAddress(privateKey)); + setAddress(convertPrivateKeyToAVMAddress(privateKey)); } }, []); diff --git a/src/extension/modals/RemoveAssetsModal/RemoveAssetsModal.tsx b/src/extension/modals/RemoveAssetsModal/RemoveAssetsModal.tsx index 2b2f47f8..25bb28e4 100644 --- a/src/extension/modals/RemoveAssetsModal/RemoveAssetsModal.tsx +++ b/src/extension/modals/RemoveAssetsModal/RemoveAssetsModal.tsx @@ -7,10 +7,11 @@ import { ModalFooter, ModalHeader, Text, + useDisclosure, VStack, } from '@chakra-ui/react'; import BigNumber from 'bignumber.js'; -import React, { FC, KeyboardEvent, ReactNode, useEffect, useRef } from 'react'; +import React, { FC, ReactNode } from 'react'; import { useTranslation } from 'react-i18next'; import { useDispatch } from 'react-redux'; import { useNavigate } from 'react-router-dom'; @@ -25,9 +26,6 @@ import ModalAssetItem from '@extension/components/ModalAssetItem'; import ModalItem from '@extension/components/ModalItem'; import ModalTextItem from '@extension/components/ModalTextItem'; import OpenTabIconButton from '@extension/components/OpenTabIconButton'; -import PasswordInput, { - usePassword, -} from '@extension/components/PasswordInput'; import RemoveAssetsConfirmingModalContent from './RemoveAssetsConfirmingModalContent'; // constants @@ -40,6 +38,9 @@ import { // enums import { AssetTypeEnum, ErrorCodeEnum } from '@extension/enums'; +// errors +import { BaseExtensionError } from '@extension/errors'; + // features import { removeARC0200AssetHoldingsThunk, @@ -52,22 +53,21 @@ import { setConfirming } from '@extension/features/remove-assets'; import useDefaultTextColor from '@extension/hooks/useDefaultTextColor'; import useSubTextColor from '@extension/hooks/useSubTextColor'; +// modals +import AuthenticationModal, { + TOnConfirmResult, +} from '@extension/modals/AuthenticationModal'; + // selectors import { useSelectAccounts, - useSelectLogger, - useSelectPasswordLockPassword, useSelectRemoveAssetsAccount, useSelectRemoveAssetsConfirming, useSelectRemoveAssetsSelectedAsset, useSelectSelectedNetwork, - useSelectSettings, useSelectSettingsPreferredBlockExplorer, } from '@extension/selectors'; -// services -import AccountService from '@extension/services/AccountService'; - // theme import { theme } from '@extension/theme'; @@ -76,33 +76,27 @@ import type { IAppThunkDispatch } from '@extension/types'; import type { IRemoveAssetsModalProps } from './types'; // utils +import convertPublicKeyToAVMAddress from '@extension/utils/convertPublicKeyToAVMAddress'; import createIconFromDataUri from '@extension/utils/createIconFromDataUri'; const RemoveAssetsModal: FC = ({ onClose }) => { const { t } = useTranslation(); - const passwordInputRef = useRef(null); const dispatch = useDispatch(); const navigate = useNavigate(); + const { + isOpen: isAuthenticationModalOpen, + onClose: onAuthenticationModalClose, + onOpen: onAuthenticationModalOpen, + } = useDisclosure(); // selectors const account = useSelectRemoveAssetsAccount(); const accounts = useSelectAccounts(); const confirming = useSelectRemoveAssetsConfirming(); const explorer = useSelectSettingsPreferredBlockExplorer(); - const logger = useSelectLogger(); - const passwordLockPassword = useSelectPasswordLockPassword(); const selectedNetwork = useSelectSelectedNetwork(); const selectedAsset = useSelectRemoveAssetsSelectedAsset(); - const settings = useSelectSettings(); // hooks const defaultTextColor = useDefaultTextColor(); - const { - error: passwordError, - onChange: onPasswordChange, - reset: resetPassword, - setError: setPasswordError, - validate: validatePassword, - value: password, - } = usePassword(); const subTextColor = useSubTextColor(); // misc const isOpen = !!account && !!selectedAsset; @@ -159,10 +153,11 @@ const RemoveAssetsModal: FC = ({ onClose }) => { dispatch(setConfirming(false)); }; - const handleRemoveStandardAssetClick = async () => { - const _functionName = 'handleAddStandardAssetClick'; - let _password: string | null; - + const handleRemoveStandardAssetClick = async () => + onAuthenticationModalOpen(); + const handleOnAuthenticationModalConfirm = async ( + result: TOnConfirmResult + ) => { if ( !selectedNetwork || !account || @@ -172,30 +167,6 @@ const RemoveAssetsModal: FC = ({ onClose }) => { return; } - // if there is no password lock - if (!settings.security.enablePasswordLock && !passwordLockPassword) { - // validate the password input - if (validatePassword()) { - logger.debug( - `${RemoveAssetsModal.name}#${_functionName}: password not valid` - ); - - return; - } - } - - _password = settings.security.enablePasswordLock - ? passwordLockPassword - : password; - - if (!_password) { - logger.debug( - `${RemoveAssetsModal.name}#${_functionName}: unable to use password from password lock, value is "null"` - ); - - return; - } - dispatch(setConfirming(true)); try { @@ -204,7 +175,7 @@ const RemoveAssetsModal: FC = ({ onClose }) => { accountId: account.id, assets: [selectedAsset], genesisHash: selectedNetwork.genesisHash, - password: _password, + ...result, }) ).unwrap(); @@ -226,10 +197,6 @@ const RemoveAssetsModal: FC = ({ onClose }) => { handleClose(); } catch (error) { switch (error.code) { - case ErrorCodeEnum.InvalidPasswordError: - setPasswordError(t('errors.inputs.invalidPassword')); - - break; case ErrorCodeEnum.OfflineError: dispatch( createNotification({ @@ -258,23 +225,19 @@ const RemoveAssetsModal: FC = ({ onClose }) => { dispatch(setConfirming(false)); }; const handleCancelClick = () => handleClose(); - const handleClose = () => { - resetPassword(); - onClose(); - }; - const handleKeyUpPasswordInput = async ( - event: KeyboardEvent - ) => { - if (event.key === 'Enter') { - if (selectedAsset?.type === AssetTypeEnum.ARC0200) { - return await handleRemoveARC0200AssetClick(); - } - - if (selectedAsset?.type === AssetTypeEnum.Standard) { - return await handleRemoveStandardAssetClick(); - } - } - }; + const handleClose = () => onClose(); + const handleOnAuthenticationError = (error: BaseExtensionError) => + dispatch( + createNotification({ + description: t('errors.descriptions.code', { + code: error.code, + context: error.code, + }), + ephemeral: true, + title: t('errors.titles.code', { context: error.code }), + type: 'error', + }) + ); // renders const renderContent = () => { let address: string; @@ -302,9 +265,7 @@ const RemoveAssetsModal: FC = ({ onClose }) => { break; } - address = AccountService.convertPublicKeyToAlgorandAddress( - account.publicKey - ); + address = convertPublicKeyToAVMAddress(account.publicKey); return ( = ({ onClose }) => { if (selectedAsset.type === AssetTypeEnum.Standard) { return ( - - {!settings.security.enablePasswordLock && !passwordLockPassword && ( - ('captions.mustEnterPasswordToAuthorizeOptOut')} - inputRef={passwordInputRef} - onChange={onPasswordChange} - onKeyUp={handleKeyUpPasswordInput} - value={password} - /> - )} + + {cancelButtonNode} - - {cancelButtonNode} - - - - + + ); } } @@ -545,41 +493,41 @@ const RemoveAssetsModal: FC = ({ onClose }) => { ); }; - // only standard assets will have the password submit - useEffect(() => { - if ( - selectedAsset && - selectedAsset.type === AssetTypeEnum.Standard && - passwordInputRef.current - ) { - passwordInputRef.current.focus(); - } - }, [selectedAsset]); - return ( - - + {/*authentication modal*/} + ('captions.mustEnterPasswordToAuthorizeOptOut')} + /> + + - - {renderHeader()} - - - - {renderContent()} - - - {renderFooter()} - - + + + {renderHeader()} + + + + {renderContent()} + + + {renderFooter()} + + + ); }; diff --git a/src/extension/modals/RemovePasskeyModal/RemovePasskeyModal.tsx b/src/extension/modals/RemovePasskeyModal/RemovePasskeyModal.tsx new file mode 100644 index 00000000..41c8635d --- /dev/null +++ b/src/extension/modals/RemovePasskeyModal/RemovePasskeyModal.tsx @@ -0,0 +1,282 @@ +import { + Heading, + HStack, + Icon, + Modal, + ModalBody, + ModalContent, + ModalFooter, + ModalHeader, + Text, + useDisclosure, + VStack, +} from '@chakra-ui/react'; +import React, { FC, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import { GoShieldSlash } from 'react-icons/go'; +import { Radio } from 'react-loader-spinner'; +import { useDispatch } from 'react-redux'; + +// components +import Button from '@extension/components/Button'; +import ReEncryptKeysLoadingContent from '@extension/components/ReEncryptKeysLoadingContent'; + +// constants +import { BODY_BACKGROUND_COLOR, DEFAULT_GAP } from '@extension/constants'; + +// features +import { create as createNotification } from '@extension/features/notifications'; + +// hooks +import useColorModeValue from '@extension/hooks/useColorModeValue'; +import useDefaultTextColor from '@extension/hooks/useDefaultTextColor'; +import useSubTextColor from '@extension/hooks/useSubTextColor'; +import useRemovePasskey from './hooks/useRemovePasskey'; + +// modals +import ConfirmPasswordModal from '@extension/modals/ConfirmPasswordModal'; + +// selectors +import { useSelectPasskeysSaving } from '@extension/selectors'; + +// theme +import { theme } from '@extension/theme'; + +// types +import type { IAppThunkDispatch } from '@extension/types'; +import type { IProps } from './types'; + +// utils +import calculateIconSize from '@extension/utils/calculateIconSize'; + +const RemovePasskeyModal: FC = ({ onClose, removePasskey }) => { + const { t } = useTranslation(); + const dispatch = useDispatch(); + const { + isOpen: isConfirmPasswordModalOpen, + onClose: onConfirmPasswordModalClose, + onOpen: onConfirmPasswordModalOpen, + } = useDisclosure(); + // selectors + const saving = useSelectPasskeysSaving(); + // hooks + const { + encrypting, + encryptionProgressState, + error, + removePasskeyAction, + requesting, + resetAction: resetRemovePasskeyAction, + } = useRemovePasskey(); + const defaultTextColor = useDefaultTextColor(); + const primaryColorCode = useColorModeValue( + theme.colors.primaryLight['500'], + theme.colors.primaryDark['500'] + ); + const subTextColor = useSubTextColor(); + // misc + const isLoading = encrypting || requesting || saving; + // handlers + const handleCancelClick = async () => handleClose(); + const handleClose = () => { + resetRemovePasskeyAction(); + + onClose && onClose(); + }; + const handleOnConfirmPasswordModalConfirm = async (password: string) => { + let success: boolean; + + if (!removePasskey) { + return; + } + + success = await removePasskeyAction({ + passkey: removePasskey, + password, + }); + + if (success) { + // display a success notification + dispatch( + createNotification({ + description: t('captions.passkeyRemoved', { + name: removePasskey.name, + }), + ephemeral: true, + title: t('headings.passkeyRemoved'), + type: 'info', + }) + ); + + // close the modal + handleClose(); + } + }; + const handleRemoveClick = () => onConfirmPasswordModalOpen(); + // renders + const renderContent = () => { + const iconSize = calculateIconSize('xl'); + + if (!removePasskey) { + return; + } + + if (encrypting) { + return ( + + {/*loader*/} + + + ); + } + + if (requesting) { + return ( + + {/*loader*/} + + + {/*caption*/} + + {t('captions.requestingPasskeyPermission', { + name: removePasskey.name, + })} + + + ); + } + + return ( + + {/*icon*/} + + + {/*description*/} + + {t('captions.removePasskey', { name: removePasskey.name })} + + + {/*instructions*/} + + + {t('captions.removePasskeyInstruction1')} + + + + {t('captions.removePasskeyInstruction2')} + + + + ); + }; + + // if there is an error from the hook, show a toast + useEffect(() => { + error && + dispatch( + createNotification({ + description: t('errors.descriptions.code', { + code: error.code, + context: error.code, + }), + ephemeral: true, + title: t('errors.titles.code', { context: error.code }), + type: 'error', + }) + ); + }, [error]); + + return ( + <> + {/*confirm password modal*/} + ('captions.mustEnterPasswordToReEncryptPrivateKeys')} + isOpen={isConfirmPasswordModalOpen} + onClose={onConfirmPasswordModalClose} + onConfirm={handleOnConfirmPasswordModalConfirm} + /> + + + + + + {t('headings.removePasskey')} + + + + + {renderContent()} + + + + + {/*cancel*/} + + + {/*remove*/} + + + + + + + ); +}; + +export default RemovePasskeyModal; diff --git a/src/extension/modals/RemovePasskeyModal/hooks/useRemovePasskey/index.ts b/src/extension/modals/RemovePasskeyModal/hooks/useRemovePasskey/index.ts new file mode 100644 index 00000000..32844f13 --- /dev/null +++ b/src/extension/modals/RemovePasskeyModal/hooks/useRemovePasskey/index.ts @@ -0,0 +1 @@ +export { default } from './useRemovePasskey'; diff --git a/src/extension/modals/RemovePasskeyModal/hooks/useRemovePasskey/types/IEncryptPrivateKeyItemWithDelayOptions.ts b/src/extension/modals/RemovePasskeyModal/hooks/useRemovePasskey/types/IEncryptPrivateKeyItemWithDelayOptions.ts new file mode 100644 index 00000000..c5561958 --- /dev/null +++ b/src/extension/modals/RemovePasskeyModal/hooks/useRemovePasskey/types/IEncryptPrivateKeyItemWithDelayOptions.ts @@ -0,0 +1,13 @@ +// types +import type { IBaseOptions } from '@common/types'; +import type { IPasskeyCredential, IPrivateKey } from '@extension/types'; + +interface IEncryptPrivateKeyItemWithDelayOptions extends IBaseOptions { + delay?: number; + inputKeyMaterial: Uint8Array; + passkey: IPasskeyCredential; + password: string; + privateKeyItem: IPrivateKey; +} + +export default IEncryptPrivateKeyItemWithDelayOptions; diff --git a/src/extension/modals/RemovePasskeyModal/hooks/useRemovePasskey/types/IRemovePasskeyActionOptions.ts b/src/extension/modals/RemovePasskeyModal/hooks/useRemovePasskey/types/IRemovePasskeyActionOptions.ts new file mode 100644 index 00000000..421f1360 --- /dev/null +++ b/src/extension/modals/RemovePasskeyModal/hooks/useRemovePasskey/types/IRemovePasskeyActionOptions.ts @@ -0,0 +1,13 @@ +// types +import type { IPasskeyCredential } from '@extension/types'; + +/** + * @property {IPasskeyCredential} passkey - the passkey credential to remove. + * @property {string} password - the password used to encrypt the private keys. + */ +interface IRemovePasskeyActionOptions { + passkey: IPasskeyCredential; + password: string; +} + +export default IRemovePasskeyActionOptions; diff --git a/src/extension/modals/RemovePasskeyModal/hooks/useRemovePasskey/types/IState.ts b/src/extension/modals/RemovePasskeyModal/hooks/useRemovePasskey/types/IState.ts new file mode 100644 index 00000000..2bd552c6 --- /dev/null +++ b/src/extension/modals/RemovePasskeyModal/hooks/useRemovePasskey/types/IState.ts @@ -0,0 +1,19 @@ +// errors +import { BaseExtensionError } from '@extension/errors'; + +// types +import type { IEncryptionState } from '@extension/components/ReEncryptKeysLoadingContent'; +import type IRemovePasskeyActionOptions from './IRemovePasskeyActionOptions'; + +interface IState { + removePasskeyAction: ( + options: IRemovePasskeyActionOptions + ) => Promise; + encryptionProgressState: IEncryptionState[]; + encrypting: boolean; + error: BaseExtensionError | null; + requesting: boolean; + resetAction: () => void; +} + +export default IState; diff --git a/src/extension/modals/RemovePasskeyModal/hooks/useRemovePasskey/types/index.ts b/src/extension/modals/RemovePasskeyModal/hooks/useRemovePasskey/types/index.ts new file mode 100644 index 00000000..508c1dec --- /dev/null +++ b/src/extension/modals/RemovePasskeyModal/hooks/useRemovePasskey/types/index.ts @@ -0,0 +1,3 @@ +export type { default as IEncryptPrivateKeyItemWithDelayOptions } from './IEncryptPrivateKeyItemWithDelayOptions'; +export type { default as IRemovePasskeyActionOptions } from './IRemovePasskeyActionOptions'; +export type { default as IState } from './IState'; diff --git a/src/extension/modals/RemovePasskeyModal/hooks/useRemovePasskey/useRemovePasskey.ts b/src/extension/modals/RemovePasskeyModal/hooks/useRemovePasskey/useRemovePasskey.ts new file mode 100644 index 00000000..2809ddaa --- /dev/null +++ b/src/extension/modals/RemovePasskeyModal/hooks/useRemovePasskey/useRemovePasskey.ts @@ -0,0 +1,170 @@ +import { useState } from 'react'; +import { useDispatch } from 'react-redux'; +import browser from 'webextension-polyfill'; + +// errors +import { BaseExtensionError, InvalidPasswordError } from '@extension/errors'; + +// features +import { removeFromStorageThunk as removePasskeyToStorageThunk } from '@extension/features/passkeys'; + +// selectors +import { useSelectLogger } from '@extension/selectors'; + +// services +import PasskeyService from '@extension/services/PasskeyService'; +import PasswordService from '@extension/services/PasswordService'; +import PrivateKeyService from '@extension/services/PrivateKeyService'; + +// types +import type { IEncryptionState } from '@extension/components/ReEncryptKeysLoadingContent'; +import type { IAppThunkDispatch, IPrivateKey } from '@extension/types'; +import type { IRemovePasskeyActionOptions, IState } from './types'; + +// utils +import { encryptPrivateKeyItemAndDelay } from './utils'; + +export default function useRemovePasskey(): IState { + const _hookName = 'useAddPasskey'; + const dispatch = useDispatch(); + // selectors + const logger = useSelectLogger(); + // states + const [encryptionProgressState, setEncryptionProgressState] = useState< + IEncryptionState[] + >([]); + const [encrypting, setEncrypting] = useState(false); + const [error, setError] = useState(null); + const [requesting, setRequesting] = useState(false); + // actions + const removePasskeyAction = async ({ + passkey, + password, + }: IRemovePasskeyActionOptions): Promise => { + const _functionName = 'removePasskeyAction'; + const passwordService = new PasswordService({ + logger, + passwordTag: browser.runtime.id, + }); + let inputKeyMaterial: Uint8Array; + let isPasswordValid: boolean; + let privateKeyItems: IPrivateKey[]; + let privateKeyService: PrivateKeyService; + + // reset the previous values + resetAction(); + + isPasswordValid = await passwordService.verifyPassword(password); + + if (!isPasswordValid) { + logger?.debug(`${_hookName}#${_functionName}: invalid password`); + + setError(new InvalidPasswordError()); + + return false; + } + + setRequesting(true); + + logger.debug( + `${_hookName}#${_functionName}: requesting input key material from passkey "${passkey.id}"` + ); + + try { + // fetch the encryption key material + inputKeyMaterial = await PasskeyService.fetchInputKeyMaterialFromPasskey({ + credential: passkey, + logger, + }); + } catch (error) { + logger?.debug(`${_hookName}#${_functionName}:`, error); + + setRequesting(false); + setError(error); + + return false; + } + + setRequesting(false); + setEncrypting(true); + + privateKeyService = new PrivateKeyService({ + logger, + }); + privateKeyItems = await privateKeyService.fetchAllFromStorage(); + + // set the encryption state for each item to false + setEncryptionProgressState( + privateKeyItems.map(({ id }) => ({ + id, + encrypted: false, + })) + ); + + // re-encrypt each private key items with the password + try { + privateKeyItems = await Promise.all( + privateKeyItems.map(async (privateKeyItem, index) => { + const item = await encryptPrivateKeyItemAndDelay({ + delay: (index + 1) * 300, // add a staggered delay for the ui to catch up + inputKeyMaterial, + logger, + passkey, + password, + privateKeyItem, + }); + + // update the encryption state + setEncryptionProgressState((_encryptionProgressState) => + _encryptionProgressState.map((value) => + value.id === privateKeyItem.id + ? { + ...value, + encrypted: true, + } + : value + ) + ); + + return item; + }) + ); + } catch (error) { + logger?.debug(`${_hookName}#${_functionName}:`, error); + + setEncrypting(false); + setError(error); + + return error; + } + + // save the new encrypted items to storage + await privateKeyService.saveManyToStorage(privateKeyItems); + + // remove the passkey to storage + await dispatch(removePasskeyToStorageThunk()).unwrap(); + + logger?.debug( + `${_hookName}#${_functionName}: successfully removed passkey` + ); + + setEncrypting(false); + + return true; + }; + const resetAction = () => { + setEncryptionProgressState([]); + setEncrypting(false); + setError(null); + setRequesting(false); + }; + + return { + encryptionProgressState, + encrypting, + error, + removePasskeyAction, + requesting, + resetAction, + }; +} diff --git a/src/extension/modals/RemovePasskeyModal/hooks/useRemovePasskey/utils/encryptPrivateKeyItemAndDelay.ts b/src/extension/modals/RemovePasskeyModal/hooks/useRemovePasskey/utils/encryptPrivateKeyItemAndDelay.ts new file mode 100644 index 00000000..91f5a290 --- /dev/null +++ b/src/extension/modals/RemovePasskeyModal/hooks/useRemovePasskey/utils/encryptPrivateKeyItemAndDelay.ts @@ -0,0 +1,97 @@ +import browser from 'webextension-polyfill'; + +// enums +import { EncryptionMethodEnum } from '@extension/enums'; + +// errors +import { MalformedDataError } from '@extension/errors'; + +// services +import PasskeyService from '@extension/services/PasskeyService'; +import PasswordService from '@extension/services/PasswordService'; +import PrivateKeyService from '@extension/services/PrivateKeyService'; + +// types +import type { IPasswordTag, IPrivateKey } from '@extension/types'; +import type { IEncryptPrivateKeyItemWithDelayOptions } from '../types'; + +/** + * Convenience function that decrypts a private key item with the passkey and re-encrypts with the password. + * @param {IEncryptPrivateKeyItemWithDelayOptions} options - the password, the passkey credential, the input key + * material to derive a passkey encryption key, the private key item to decrypt/encrypt and an optional delay. + * @returns {IPrivateKey} a re-encrypted private key item. + */ +export default async function encryptPrivateKeyItemAndDelay({ + delay = 0, + inputKeyMaterial, + logger, + passkey, + password, + privateKeyItem, +}: IEncryptPrivateKeyItemWithDelayOptions): Promise { + const _functionName = 'encryptPrivateKeyItemAndDelay'; + + return new Promise((resolve, reject) => { + setTimeout(async () => { + const passwordService = new PasswordService({ + logger, + passwordTag: browser.runtime.id, + }); + let _error: string; + let decryptedPrivateKey: Uint8Array; + let passwordTagItem: IPasswordTag | null; + let reEncryptedPrivateKey: Uint8Array; + let version: number = privateKeyItem.version; + + try { + decryptedPrivateKey = await PasskeyService.decryptBytes({ + encryptedBytes: PrivateKeyService.decode( + privateKeyItem.encryptedPrivateKey + ), + inputKeyMaterial, + passkey, + logger, + }); // decrypt the private key with the passkey + + // if the saved private key is a legacy item, it is using the "secret key" form - the private key concatenated to the public key + if (privateKeyItem.version <= 0) { + logger?.debug( + `${_functionName}: key "${privateKeyItem}" on legacy version "${privateKeyItem.version}", updating` + ); + + decryptedPrivateKey = + PrivateKeyService.extractPrivateKeyFromSecretKey( + decryptedPrivateKey + ); + version = PrivateKeyService.latestVersion; // update to the latest version + } + + reEncryptedPrivateKey = await PasswordService.encryptBytes({ + data: decryptedPrivateKey, + logger, + password, + }); // re-encrypt the private key with the password + passwordTagItem = await passwordService.fetchFromStorage(); + + if (!passwordTagItem) { + _error = `failed to get password tag from storage, doesn't exist`; + + logger?.error(`${_functionName}: ${_error}`); + + throw new MalformedDataError(_error); + } + } catch (error) { + return reject(error); + } + + return resolve({ + ...privateKeyItem, + encryptedPrivateKey: PrivateKeyService.encode(reEncryptedPrivateKey), + encryptionID: passwordTagItem.id, + encryptionMethod: EncryptionMethodEnum.Password, + updatedAt: new Date().getTime(), + version, + }); + }, delay); + }); +} diff --git a/src/extension/modals/RemovePasskeyModal/hooks/useRemovePasskey/utils/index.ts b/src/extension/modals/RemovePasskeyModal/hooks/useRemovePasskey/utils/index.ts new file mode 100644 index 00000000..4b24fc41 --- /dev/null +++ b/src/extension/modals/RemovePasskeyModal/hooks/useRemovePasskey/utils/index.ts @@ -0,0 +1 @@ +export { default as encryptPrivateKeyItemAndDelay } from './encryptPrivateKeyItemAndDelay'; diff --git a/src/extension/modals/RemovePasskeyModal/index.ts b/src/extension/modals/RemovePasskeyModal/index.ts new file mode 100644 index 00000000..a976166d --- /dev/null +++ b/src/extension/modals/RemovePasskeyModal/index.ts @@ -0,0 +1 @@ +export { default } from './RemovePasskeyModal'; diff --git a/src/extension/modals/RemovePasskeyModal/types/IProps.ts b/src/extension/modals/RemovePasskeyModal/types/IProps.ts new file mode 100644 index 00000000..948e7fb7 --- /dev/null +++ b/src/extension/modals/RemovePasskeyModal/types/IProps.ts @@ -0,0 +1,8 @@ +// types +import type { IModalProps, IPasskeyCredential } from '@extension/types'; + +interface IProps extends IModalProps { + removePasskey: IPasskeyCredential | null; +} + +export default IProps; diff --git a/src/extension/modals/RemovePasskeyModal/types/index.ts b/src/extension/modals/RemovePasskeyModal/types/index.ts new file mode 100644 index 00000000..f404deed --- /dev/null +++ b/src/extension/modals/RemovePasskeyModal/types/index.ts @@ -0,0 +1 @@ +export type { default as IProps } from './IProps'; diff --git a/src/extension/modals/SendAssetModal/SendAssetModal.tsx b/src/extension/modals/SendAssetModal/SendAssetModal.tsx index 4b4f3496..29a96d7e 100644 --- a/src/extension/modals/SendAssetModal/SendAssetModal.tsx +++ b/src/extension/modals/SendAssetModal/SendAssetModal.tsx @@ -8,18 +8,12 @@ import { ModalHeader, Text, Textarea, + useDisclosure, VStack, } from '@chakra-ui/react'; import { Transaction } from 'algosdk'; import BigNumber from 'bignumber.js'; -import React, { - ChangeEvent, - FC, - KeyboardEvent, - useEffect, - useRef, - useState, -} from 'react'; +import React, { ChangeEvent, FC, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { IoArrowBackOutline, IoArrowForwardOutline } from 'react-icons/io5'; import { useDispatch } from 'react-redux'; @@ -31,20 +25,20 @@ import AddressInput, { } from '@extension/components/AddressInput'; import AssetSelect from '@extension/components/AssetSelect'; import Button from '@extension/components/Button'; -import PasswordInput, { - usePassword, -} from '@extension/components/PasswordInput'; import SendAmountInput from './SendAmountInput'; import SendAssetModalConfirmingContent from './SendAssetModalConfirmingContent'; import SendAssetModalContentSkeleton from './SendAssetModalContentSkeleton'; import SendAssetModalSummaryContent from './SendAssetModalSummaryContent'; // constants -import { DEFAULT_GAP } from '@extension/constants'; +import { BODY_BACKGROUND_COLOR, DEFAULT_GAP } from '@extension/constants'; // enums import { AssetTypeEnum, ErrorCodeEnum } from '@extension/enums'; +// errors +import { BaseExtensionError } from '@extension/errors'; + // features import { updateAccountsThunk } from '@extension/features/accounts'; import { create as createNotification } from '@extension/features/notifications'; @@ -63,13 +57,17 @@ import { import useDefaultTextColor from '@extension/hooks/useDefaultTextColor'; import usePrimaryColor from '@extension/hooks/usePrimaryColor'; +// modals +import AuthenticationModal, { + TOnConfirmResult, +} from '@extension/modals/AuthenticationModal'; + // selectors import { useSelectAccounts, useSelectARC0200AssetsBySelectedNetwork, useSelectAvailableAccountsForSelectedNetwork, useSelectLogger, - useSelectPasswordLockPassword, useSelectSelectedNetwork, useSelectSendAssetAmountInStandardUnits, useSelectSendAssetConfirming, @@ -77,12 +75,10 @@ import { useSelectSendAssetFromAccount, useSelectSendAssetNote, useSelectSendAssetSelectedAsset, - useSelectSettings, useSelectStandardAssetsBySelectedNetwork, } from '@extension/selectors'; // services -import AccountService from '@extension/services/AccountService'; import QuestsService, { QuestNameEnum, } from '@extension/services/QuestsService'; @@ -102,11 +98,16 @@ import type { // utils import calculateMaxTransactionAmount from '@extension/utils/calculateMaxTransactionAmount'; +import convertPublicKeyToAVMAddress from '@extension/utils/convertPublicKeyToAVMAddress'; const SendAssetModal: FC = ({ onClose }) => { const { t } = useTranslation(); - const passwordInputRef = useRef(null); const dispatch = useDispatch(); + const { + isOpen: isAuthenticationModalOpen, + onClose: onAuthenticationModalClose, + onOpen: onAuthenticationModalOpen, + } = useDisclosure(); // selectors const accounts = useSelectAccounts(); const amountInStandardUnits = useSelectSendAssetAmountInStandardUnits(); @@ -119,9 +120,7 @@ const SendAssetModal: FC = ({ onClose }) => { const logger = useSelectLogger(); const network = useSelectSelectedNetwork(); const note = useSelectSendAssetNote(); - const passwordLockPassword = useSelectPasswordLockPassword(); const selectedAsset = useSelectSendAssetSelectedAsset(); - const settings = useSelectSettings(); // hooks const { error: toAddressError, @@ -131,16 +130,8 @@ const SendAssetModal: FC = ({ onClose }) => { validate: validateToAddress, value: toAddress, } = useAddressInput(); - const defaultTextColor: string = useDefaultTextColor(); - const { - error: passwordError, - onChange: onPasswordChange, - reset: resetPassword, - setError: setPasswordError, - validate: validatePassword, - value: password, - } = usePassword(); - const primaryColor: string = usePrimaryColor(); + const defaultTextColor = useDefaultTextColor(); + const primaryColor = usePrimaryColor(); // state const [maximumTransactionAmount, setMaximumTransactionAmount] = useState('0'); @@ -170,19 +161,12 @@ const SendAssetModal: FC = ({ onClose }) => { // reset modal input and transactions setTransactions(null); resetToAddress(); - resetPassword(); onClose && onClose(); }; const handleFromAccountChange = (account: IAccountWithExtendedProps) => - dispatch( - setFromAddress( - AccountService.convertPublicKeyToAlgorandAddress( - account.publicKey - ).toUpperCase() - ) - ); + dispatch(setFromAddress(convertPublicKeyToAVMAddress(account.publicKey))); const handleNextClick = async () => { - const _functionName: string = 'handleNextClick'; + const _functionName = 'handleNextClick'; let _transactions: Transaction[]; if (validateToAddress()) { @@ -227,20 +211,23 @@ const SendAssetModal: FC = ({ onClose }) => { dispatch( setNote(event.target.value.length > 0 ? event.target.value : null) ); - const handleKeyUpPasswordInput = async ( - event: KeyboardEvent + const handleOnAuthenticationError = (error: BaseExtensionError) => + dispatch( + createNotification({ + description: t('errors.descriptions.code', { + code: error.code, + context: error.code, + }), + ephemeral: true, + title: t('errors.titles.code', { context: error.code }), + type: 'error', + }) + ); + const handlePreviousClick = () => setTransactions(null); + const handleOnAuthenticationModalConfirm = async ( + result: TOnConfirmResult ) => { - if (event.key === 'Enter') { - await handleSendClick(); - } - }; - const handlePreviousClick = () => { - resetPassword(); - setTransactions(null); - }; - const handleSendClick = async () => { - const _functionName: string = 'handleSendClick'; - let _password: string | null; + const _functionName = 'handleOnAuthenticationModalConfirm'; let fromAddress: string; let hasQuestBeenCompletedToday: boolean = false; let questsService: QuestsService; @@ -252,44 +239,13 @@ const SendAssetModal: FC = ({ onClose }) => { return; } - // if there is no password lock - if (!settings.security.enablePasswordLock && !passwordLockPassword) { - // validate the password input - if (validatePassword()) { - logger.debug( - `${SendAssetModal.name}#${_functionName}: password not valid` - ); - - return; - } - } - - _password = settings.security.enablePasswordLock - ? passwordLockPassword - : password; - - if (!_password) { - logger.debug( - `${SendAssetModal.name}#${_functionName}: unable to use password from password lock, value is "null"` - ); - - return; - } - try { transactionIds = await dispatch( submitTransactionThunk({ - password: _password, transactions, + ...result, }) ).unwrap(); - toAccount = - accounts.find( - (value) => - AccountService.convertPublicKeyToAlgorandAddress( - value.publicKey - ) === toAddress - ) || null; logger.debug( `${ @@ -299,9 +255,11 @@ const SendAssetModal: FC = ({ onClose }) => { .join(',')}] to the network` ); - fromAddress = AccountService.convertPublicKeyToAlgorandAddress( - fromAccount.publicKey - ); + toAccount = + accounts.find( + (value) => convertPublicKeyToAVMAddress(value.publicKey) === toAddress + ) || null; + fromAddress = convertPublicKeyToAVMAddress(fromAccount.publicKey); questsService = new QuestsService({ logger, }); @@ -393,10 +351,6 @@ const SendAssetModal: FC = ({ onClose }) => { handleClose(); } catch (error) { switch (error.code) { - case ErrorCodeEnum.InvalidPasswordError: - setPasswordError(t('errors.inputs.invalidPassword')); - - break; case ErrorCodeEnum.OfflineError: dispatch( createNotification({ @@ -422,20 +376,25 @@ const SendAssetModal: FC = ({ onClose }) => { } } }; + const handleSendClick = () => onAuthenticationModalOpen(); const handleToAddressChange = (value: string) => { dispatch(setToAddress(value.length > 0 ? value : null)); onToAddressChange(value); }; // renders const renderContent = () => { - if (confirming) { - return ; - } - if (!fromAccount || !network || !selectedAsset) { return ; } + if (confirming) { + return ( + + ); + } + if (transactions && transactions.length > 0) { return ( = ({ onClose }) => { if (transactions && transactions.length > 0) { return ( - - {!settings.security.enablePasswordLock && !passwordLockPassword && ( - ('captions.mustEnterPasswordToSendTransaction')} - onChange={onPasswordChange} - onKeyUp={handleKeyUpPasswordInput} - inputRef={passwordInputRef} - value={password} - /> - )} - - - - - - - + + + + + ); } @@ -642,11 +583,6 @@ const SendAssetModal: FC = ({ onClose }) => { } }; - useEffect(() => { - if (transactions && transactions.length > 0 && passwordInputRef.current) { - passwordInputRef.current.focus(); - } - }, [transactions]); useEffect(() => { let newMaximumTransactionAmount: BigNumber; @@ -674,29 +610,40 @@ const SendAssetModal: FC = ({ onClose }) => { }, [fromAccount, network, selectedAsset]); return ( - - + {/*authentication modal*/} + ('captions.mustEnterPasswordToSendTransaction')} + /> + + - - {renderHeader()} - - - - {renderContent()} - - - {renderFooter()} - - + + + {renderHeader()} + + + + {renderContent()} + + + {renderFooter()} + + + ); }; diff --git a/src/extension/modals/SendAssetModal/SendAssetModalConfirmingContent.tsx b/src/extension/modals/SendAssetModal/SendAssetModalConfirmingContent.tsx index 3d2798c6..fa185bae 100644 --- a/src/extension/modals/SendAssetModal/SendAssetModalConfirmingContent.tsx +++ b/src/extension/modals/SendAssetModal/SendAssetModalConfirmingContent.tsx @@ -1,38 +1,45 @@ -import { Spinner, Text, VStack } from '@chakra-ui/react'; +import { Text, VStack } from '@chakra-ui/react'; import React, { FC } from 'react'; import { useTranslation } from 'react-i18next'; +import { IoSwapVerticalOutline } from 'react-icons/io5'; + +// components +import CircularProgressWithIcon from '@extension/components/CircularProgressWithIcon'; // constants import { DEFAULT_GAP } from '@extension/constants'; // hooks import useDefaultTextColor from '@extension/hooks/useDefaultTextColor'; -import usePrimaryColor from '@extension/hooks/usePrimaryColor'; -const SendAssetModalConfirmingContent: FC = () => { +// types +import type { ISendAssetModalConfirmingContentProps } from './types'; + +const SendAssetModalConfirmingContent: FC< + ISendAssetModalConfirmingContentProps +> = ({ numberOfTransactions }) => { const { t } = useTranslation(); // hooks - const defaultTextColor: string = useDefaultTextColor(); - const primaryColor: string = usePrimaryColor(); + const defaultTextColor = useDefaultTextColor(); return ( - + {/*progress*/} + + {/*captions*/} - {t('captions.confirmingTransaction')} + {numberOfTransactions + ? t('captions.confirmingTransactionWithAmountWithAmount', { + number: numberOfTransactions, + }) + : t('captions.confirmingTransactionWithAmount')} ); diff --git a/src/extension/modals/SendAssetModal/SendAssetModalSummaryContent.tsx b/src/extension/modals/SendAssetModal/SendAssetModalSummaryContent.tsx index a0a0615b..ef2cc318 100644 --- a/src/extension/modals/SendAssetModal/SendAssetModalSummaryContent.tsx +++ b/src/extension/modals/SendAssetModal/SendAssetModalSummaryContent.tsx @@ -24,19 +24,17 @@ import usePrimaryButtonTextColor from '@extension/hooks/usePrimaryButtonTextColo import useMinimumBalanceRequirementsForTransactions from '@extension/hooks/useMinimumBalanceRequirementsForTransactions'; import useSubTextColor from '@extension/hooks/useSubTextColor'; -// services -import AccountService from '@extension/services/AccountService'; - // types -import type { SendAssetModalSummaryContentProps } from './types'; +import type { ISendAssetModalSummaryContentProps } from './types'; // utils import convertToAtomicUnit from '@common/utils/convertToAtomicUnit'; import convertToStandardUnit from '@common/utils/convertToStandardUnit'; import formatCurrencyUnit from '@common/utils/formatCurrencyUnit'; +import convertPublicKeyToAVMAddress from '@extension/utils/convertPublicKeyToAVMAddress'; import createIconFromDataUri from '@extension/utils/createIconFromDataUri'; -const SendAssetModalSummaryContent: FC = ({ +const SendAssetModalSummaryContent: FC = ({ accounts, amountInStandardUnits, asset, @@ -241,9 +239,7 @@ const SendAssetModalSummaryContent: FC = ({ item={ = ({ {/*warning*/} {(!signer || decodedJwt.payload.subject !== - AccountService.convertPublicKeyToAlgorandAddress( - signer.publicKey - )) && ( + convertPublicKeyToAVMAddress(signer.publicKey)) && ( ('captions.addressDoesNotMatch')} /> diff --git a/src/extension/modals/SignMessageModal/SignMessageModal.tsx b/src/extension/modals/SignMessageModal/SignMessageModal.tsx index eebf4ed0..c5419ba9 100644 --- a/src/extension/modals/SignMessageModal/SignMessageModal.tsx +++ b/src/extension/modals/SignMessageModal/SignMessageModal.tsx @@ -11,10 +11,11 @@ import { ModalFooter, ModalHeader, Text, + useDisclosure, VStack, } from '@chakra-ui/react'; import { encode as encodeBase64 } from '@stablelib/base64'; -import React, { FC, KeyboardEvent, useEffect, useRef } from 'react'; +import React, { FC } from 'react'; import { useTranslation } from 'react-i18next'; import { useDispatch } from 'react-redux'; @@ -25,16 +26,13 @@ import Button from '@extension/components/Button'; import ClientHeader, { ClientHeaderSkeleton, } from '@extension/components/ClientHeader'; -import PasswordInput, { - usePassword, -} from '@extension/components/PasswordInput'; import SignMessageContentSkeleton from './SignMessageContentSkeleton'; // constants import { BODY_BACKGROUND_COLOR, DEFAULT_GAP } from '@extension/constants'; -// enums -import { ErrorCodeEnum } from '@extension/enums'; +// errors +import { BaseExtensionError } from '@extension/errors'; // features import { removeEventByIdThunk } from '@extension/features/events'; @@ -45,6 +43,11 @@ import { create as createNotification } from '@extension/features/notifications' import useSubTextColor from '@extension/hooks/useSubTextColor'; import useSignMessageModal from './hooks/useSignMessageModal'; +// modals +import AuthenticationModal, { + TOnConfirmResult, +} from '@extension/modals/AuthenticationModal'; + // selectors import { useSelectAccountsFetching, @@ -52,7 +55,7 @@ import { } from '@extension/selectors'; // services -import AccountService from '@extension/services/AccountService'; +import PrivateKeyService from '@extension/services/PrivateKeyService'; import QuestsService from '@extension/services/QuestsService'; // theme @@ -66,24 +69,21 @@ import type { } from '@extension/types'; // utils +import convertPublicKeyToAVMAddress from '@extension/utils/convertPublicKeyToAVMAddress'; import signBytes from '@extension/utils/signBytes'; const SignMessageModal: FC = ({ onClose }) => { const { t } = useTranslation(); - const passwordInputRef = useRef(null); const dispatch = useDispatch(); + const { + isOpen: isAuthenticationModalOpen, + onClose: onAuthenticationModalClose, + onOpen: onAuthenticationModalOpen, + } = useDisclosure(); // selectors const fetching = useSelectAccountsFetching(); const logger = useSelectLogger(); // hooks - const { - error: passwordError, - onChange: onPasswordChange, - reset: resetPassword, - setError: setPasswordError, - validate: validatePassword, - value: password, - } = usePassword(); const { authorizedAccounts, event, @@ -95,6 +95,18 @@ const SignMessageModal: FC = ({ onClose }) => { // handlers const handleAccountSelect = (account: IAccountWithExtendedProps) => setSigner(account); + const handleAuthenticationError = (error: BaseExtensionError) => + dispatch( + createNotification({ + description: t('errors.descriptions.code', { + code: error.code, + context: error.code, + }), + ephemeral: true, + title: t('errors.titles.code', { context: error.code }), + type: 'error', + }) + ); const handleCancelClick = async () => { if (event) { await dispatch( @@ -116,39 +128,24 @@ const SignMessageModal: FC = ({ onClose }) => { handleClose(); }; const handleClose = () => { - resetPassword(); setAuthorizedAccounts(null); setSigner(null); - if (onClose) { - onClose(); - } + onClose && onClose(); }; - const handleKeyUpPasswordInput = async ( - event: KeyboardEvent + const handleOnAuthenticationModalConfirm = async ( + result: TOnConfirmResult ) => { - if (event.key === 'Enter') { - await handleSignClick(); - } - }; - const handleSignClick = async () => { - const _functionName = 'handleSignClick'; + const _functionName = 'handleOnAuthenticationModalConfirm'; let questsService: QuestsService; let signature: Uint8Array; let signerAddress: string; - if ( - validatePassword() || - !event || - !event.payload.message.params || - !signer - ) { + if (!event || !event.payload.message.params || !signer) { return; } - signerAddress = AccountService.convertPublicKeyToAlgorandAddress( - signer.publicKey - ); + signerAddress = convertPublicKeyToAVMAddress(signer.publicKey); logger.debug( `${SignMessageModal.name}#${_functionName}: signing message for signer "${signerAddress}"` @@ -158,8 +155,8 @@ const SignMessageModal: FC = ({ onClose }) => { signature = await signBytes({ bytes: new TextEncoder().encode(event.payload.message.params.message), logger, - password, - publicKey: AccountService.decodePublicKey(signer.publicKey), + publicKey: PrivateKeyService.decode(signer.publicKey), + ...result, }); logger.debug( @@ -188,10 +185,6 @@ const SignMessageModal: FC = ({ onClose }) => { handleClose(); } catch (error) { switch (error.code) { - case ErrorCodeEnum.InvalidPasswordError: - setPasswordError(t('errors.inputs.invalidPassword')); - - break; default: dispatch( createNotification({ @@ -209,6 +202,8 @@ const SignMessageModal: FC = ({ onClose }) => { } } }; + const handleSignClick = () => onAuthenticationModalOpen(); + // renders const renderContent = () => { if ( fetching || @@ -259,61 +254,56 @@ const SignMessageModal: FC = ({ onClose }) => { ); }; - // focus when the modal is opened - useEffect(() => { - if (passwordInputRef.current) { - passwordInputRef.current.focus(); - } - }, []); - return ( - - + {/*authentication modal*/} + ('captions.mustEnterPasswordToSign')} + /> + + - - {event ? ( - - - - {/*caption*/} - - {t('captions.signMessageRequest')} - - - ) : ( - - )} - - - {renderContent()} - - - - ('captions.mustEnterPasswordToSign')} - inputRef={passwordInputRef} - onChange={onPasswordChange} - onKeyUp={handleKeyUpPasswordInput} - value={password} - /> - + + + {event ? ( + + + + {/*caption*/} + + {t('captions.signMessageRequest')} + + + ) : ( + + )} + + + {renderContent()} + + - - - - + + + + ); }; diff --git a/src/extension/modals/SignMessageModal/hooks/useSignMessageModal/useSignMessageModal.ts b/src/extension/modals/SignMessageModal/hooks/useSignMessageModal/useSignMessageModal.ts index 5c42d5f9..17a1feb4 100644 --- a/src/extension/modals/SignMessageModal/hooks/useSignMessageModal/useSignMessageModal.ts +++ b/src/extension/modals/SignMessageModal/hooks/useSignMessageModal/useSignMessageModal.ts @@ -14,9 +14,6 @@ import { useSelectSessions, } from '@extension/selectors'; -// services -import AccountService from '@extension/services/AccountService'; - // types import type { IAccountWithExtendedProps, @@ -26,6 +23,7 @@ import type { IUseSignMessageModalState } from './types'; // utils import authorizedAccountsForHost from '@extension/utils/authorizedAccountsForHost'; +import convertPublicKeyToAVMAddress from '@extension/utils/convertPublicKeyToAVMAddress'; export default function useSignMessageModal(): IUseSignMessageModalState { // selectors @@ -67,9 +65,8 @@ export default function useSignMessageModal(): IUseSignMessageModalState { setSigner( authorizedAccounts.find( (value) => - AccountService.convertPublicKeyToAlgorandAddress( - value.publicKey - ) === event.payload.message.params?.signer + convertPublicKeyToAVMAddress(value.publicKey) === + event.payload.message.params?.signer ) || authorizedAccounts[0] || null diff --git a/src/extension/modals/SignTransactionsModal/AssetFreezeTransactionContent.tsx b/src/extension/modals/SignTransactionsModal/AssetFreezeTransactionContent.tsx index e3f4363a..175ebb5a 100644 --- a/src/extension/modals/SignTransactionsModal/AssetFreezeTransactionContent.tsx +++ b/src/extension/modals/SignTransactionsModal/AssetFreezeTransactionContent.tsx @@ -33,6 +33,7 @@ import { useSelectLogger } from '@extension/selectors'; // services import AccountService from '@extension/services/AccountService'; +import PrivateKeyService from '@extension/services/PrivateKeyService'; // types import type { @@ -43,7 +44,9 @@ import type { import type { IAssetTransactionBodyProps } from './types'; // utils +import convertAVMAddressToPublicKey from '@extension/utils/convertAVMAddressToPublicKey'; import convertGenesisHashToHex from '@extension/utils/convertGenesisHashToHex'; +import convertPublicKeyToAVMAddress from '@extension/utils/convertPublicKeyToAVMAddress'; import createIconFromDataUri from '@extension/utils/createIconFromDataUri'; import parseTransactionType from '@extension/utils/parseTransactionType'; import updateAccountInformation from '@extension/utils/updateAccountInformation'; @@ -172,9 +175,7 @@ const AssetFreezeTransactionContent: FC = ({ account = accounts.find( (value) => - AccountService.convertPublicKeyToAlgorandAddress( - value.publicKey - ) === freezeAddress + convertPublicKeyToAVMAddress(value.publicKey) === freezeAddress ) || null; // if we have this account, just use ut @@ -191,8 +192,9 @@ const AssetFreezeTransactionContent: FC = ({ ).toUpperCase(); account = { ...AccountService.initializeDefaultAccount({ - publicKey: - AccountService.convertAlgorandAddressToPublicKey(freezeAddress), + publicKey: PrivateKeyService.encode( + convertAVMAddressToPublicKey(freezeAddress) + ), }), watchAccount: false, }; diff --git a/src/extension/modals/SignTransactionsModal/AtomicTransactionsContent.tsx b/src/extension/modals/SignTransactionsModal/AtomicTransactionsContent.tsx index bd168a0c..ef01fdd1 100644 --- a/src/extension/modals/SignTransactionsModal/AtomicTransactionsContent.tsx +++ b/src/extension/modals/SignTransactionsModal/AtomicTransactionsContent.tsx @@ -38,6 +38,7 @@ import { // services import AccountService from '@extension/services/AccountService'; +import PrivateKeyService from '@extension/services/PrivateKeyService'; // types import type { @@ -50,6 +51,7 @@ import type { IAtomicTransactionsContentProps } from './types'; // utils import computeGroupId from '@common/utils/computeGroupId'; import convertGenesisHashToHex from '@extension/utils/convertGenesisHashToHex'; +import convertPublicKeyToAVMAddress from '@extension/utils/convertPublicKeyToAVMAddress'; import parseTransactionType from '@extension/utils/parseTransactionType'; import uniqueGenesisHashesFromTransactions from '@extension/utils/uniqueGenesisHashesFromTransactions'; import updateAccountInformation from '@extension/utils/updateAccountInformation'; @@ -280,10 +282,9 @@ const AtomicTransactionsContent: FC = ({ watchAccount: true, }; accountInformation = await updateAccountInformation({ - address: - AccountService.convertPublicKeyToAlgorandAddress( - encodedPublicKey - ), + address: convertPublicKeyToAVMAddress( + PrivateKeyService.decode(encodedPublicKey) + ), currentAccountInformation: account.networkInformation[encodedGenesisHash] || AccountService.initializeDefaultAccountInformation(), diff --git a/src/extension/modals/SignTransactionsModal/SignTransactionsModal.tsx b/src/extension/modals/SignTransactionsModal/SignTransactionsModal.tsx index 31841eac..760bb8a2 100644 --- a/src/extension/modals/SignTransactionsModal/SignTransactionsModal.tsx +++ b/src/extension/modals/SignTransactionsModal/SignTransactionsModal.tsx @@ -11,11 +11,12 @@ import { ModalFooter, ModalHeader, Text, + useDisclosure, VStack, } from '@chakra-ui/react'; import { decode as decodeBase64 } from '@stablelib/base64'; import { Transaction } from 'algosdk'; -import React, { FC, KeyboardEvent, useRef, useState } from 'react'; +import React, { FC, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { IoArrowBackOutline } from 'react-icons/io5'; import { useDispatch } from 'react-redux'; @@ -25,9 +26,6 @@ import Button from '@extension/components/Button'; import ClientHeader, { ClientHeaderSkeleton, } from '@extension/components/ClientHeader'; -import PasswordInput, { - usePassword, -} from '@extension/components/PasswordInput'; import AtomicTransactionsContent from './AtomicTransactionsContent'; import GroupOfTransactionsContent from './GroupOfTransactionsContent'; import SingleTransactionContent from './SingleTransactionContent'; @@ -39,7 +37,10 @@ import { BODY_BACKGROUND_COLOR, DEFAULT_GAP } from '@extension/constants'; import { MultipleTransactionsContext } from './contexts'; // enums -import { ErrorCodeEnum } from '@extension/enums'; +import { EncryptionMethodEnum } from '@extension/enums'; + +// errors +import { BaseExtensionError } from '@extension/errors'; // features import { removeEventByIdThunk } from '@extension/features/events'; @@ -50,6 +51,11 @@ import { create as createNotification } from '@extension/features/notifications' import useSubTextColor from '@extension/hooks/useSubTextColor'; import useSignTransactionsModal from './hooks/useSignTransactionsModal'; +// modals +import AuthenticationModal, { + TOnConfirmResult, +} from '@extension/modals/AuthenticationModal'; + // selectors import { useSelectAccounts, @@ -76,8 +82,12 @@ import signTransactions from './utils/signTransactions'; const SignTransactionsModal: FC = ({ onClose }) => { const { t } = useTranslation(); - const passwordInputRef = useRef(null); const dispatch = useDispatch(); + const { + isOpen: isAuthenticationModalOpen, + onClose: onAuthenticationModalClose, + onOpen: onAuthenticationModalOpen, + } = useDisclosure(); // selectors const accounts = useSelectAccounts(); const logger = useSelectLogger(); @@ -85,14 +95,6 @@ const SignTransactionsModal: FC = ({ onClose }) => { const sessions = useSelectSessions(); // hooks const { event } = useSignTransactionsModal(); - const { - error: passwordError, - onChange: onPasswordChange, - reset: resetPassword, - setError: setPasswordError, - validate: validatePassword, - value: password, - } = usePassword(); const subTextColor = useSubTextColor(); // states const [moreDetailsTransactions, setMoreDetailsTransactions] = useState< @@ -120,23 +122,18 @@ const SignTransactionsModal: FC = ({ onClose }) => { handleClose(); }; const handleClose = () => { - resetPassword(); + setMoreDetailsTransactions(null); + setSigning(false); onClose && onClose(); }; - const handleKeyUpPasswordInput = async ( - event: KeyboardEvent + const handleOnAuthenticationModalConfirm = async ( + result: TOnConfirmResult ) => { - if (event.key === 'Enter') { - await handleSignClick(); - } - }; - const handlePreviousClick = () => setMoreDetailsTransactions(null); - const handleSignClick = async () => { let authorizedAccounts: IAccountWithExtendedProps[]; let stxns: (string | null)[]; - if (validatePassword() || !event || !event.payload.message.params) { + if (!event || !event.payload.message.params) { return; } @@ -156,7 +153,15 @@ const SignTransactionsModal: FC = ({ onClose }) => { authAccounts: accounts, logger, networks, - password, + ...(result.type === EncryptionMethodEnum.Password + ? { + password: result.password, + type: EncryptionMethodEnum.Password, + } + : { + inputKeyMaterial: result.inputKeyMaterial, + type: EncryptionMethodEnum.Passkey, + }), }); // send a response @@ -173,10 +178,6 @@ const SignTransactionsModal: FC = ({ onClose }) => { handleClose(); } catch (error) { switch (error.code) { - case ErrorCodeEnum.InvalidPasswordError: - setPasswordError(t('errors.inputs.invalidPassword')); - - break; case ARC0027ErrorCodeEnum.UnauthorizedSignerError: dispatch( sendSignTransactionsResponseThunk({ @@ -207,6 +208,20 @@ const SignTransactionsModal: FC = ({ onClose }) => { setSigning(false); }; + const handleAuthenticationError = (error: BaseExtensionError) => + dispatch( + createNotification({ + description: t('errors.descriptions.code', { + code: error.code, + context: error.code, + }), + ephemeral: true, + title: t('errors.titles.code', { context: error.code }), + type: 'error', + }) + ); + const handlePreviousClick = () => setMoreDetailsTransactions(null); + const handleSignClick = async () => onAuthenticationModalOpen(); const renderContent = () => { let decodedTransactions: Transaction[]; let groupsOfTransactions: Transaction[][]; @@ -250,68 +265,66 @@ const SignTransactionsModal: FC = ({ onClose }) => { }; return ( - - + {/*authentication*/} + ( + event.payload.message.params.txns.length > 1 + ? 'captions.mustEnterPasswordToSignTransactions' + : 'captions.mustEnterPasswordToSignTransaction' + ), + })} + /> + + - - {event && event.payload.message.params ? ( - - - - {/*caption*/} - - {t( - event.payload.message.params.txns.length > 1 - ? 'captions.signTransactionsRequest' - : 'captions.signTransactionRequest' - )} - - - ) : ( - - )} - - - {renderContent()} - - - - {/*password input*/} - ( - event.payload.message.params.txns.length > 1 - ? 'captions.mustEnterPasswordToSignTransactions' - : 'captions.mustEnterPasswordToSignTransaction' - ) - : null - } - inputRef={passwordInputRef} - onChange={onPasswordChange} - onKeyUp={handleKeyUpPasswordInput} - value={password} - /> - - {/*buttons*/} + + + {event && event.payload.message.params ? ( + + + + {/*caption*/} + + {t( + event.payload.message.params.txns.length > 1 + ? 'captions.signTransactionsRequest' + : 'captions.signTransactionRequest' + )} + + + ) : ( + + )} + + + {renderContent()} + + {moreDetailsTransactions && moreDetailsTransactions.length > 0 ? ( // previous button @@ -347,10 +360,10 @@ const SignTransactionsModal: FC = ({ onClose }) => { {t('buttons.sign')} - - - - + + + + ); }; diff --git a/src/extension/modals/SignTransactionsModal/SingleTransactionContent.tsx b/src/extension/modals/SignTransactionsModal/SingleTransactionContent.tsx index 68e19a0f..811bee0b 100644 --- a/src/extension/modals/SignTransactionsModal/SingleTransactionContent.tsx +++ b/src/extension/modals/SignTransactionsModal/SingleTransactionContent.tsx @@ -22,7 +22,6 @@ import { useSelectNetworkByGenesisHash, useSelectSettingsPreferredBlockExplorer, useSelectStandardAssetsByGenesisHash, - useSelectStandardAssetsUpdating, } from '@extension/selectors'; // services @@ -36,8 +35,9 @@ import type { import type { ISingleTransactionContentProps } from './types'; // utils -import parseTransactionType from '@extension/utils/parseTransactionType'; import convertGenesisHashToHex from '@extension/utils/convertGenesisHashToHex'; +import convertPublicKeyToAVMAddress from '@extension/utils/convertPublicKeyToAVMAddress'; +import parseTransactionType from '@extension/utils/parseTransactionType'; import updateAccountInformation from '@extension/utils/updateAccountInformation'; const SingleTransactionContent: FC = ({ @@ -51,7 +51,6 @@ const SingleTransactionContent: FC = ({ const preferredExplorer = useSelectSettingsPreferredBlockExplorer(); const standardAssets = useSelectStandardAssetsByGenesisHash(encodedGenesisHash); - const updatingStandardAssets = useSelectStandardAssetsUpdating(); // states const [fetchingAccountInformation, setFetchingAccountInformation] = useState(false); @@ -116,8 +115,7 @@ const SingleTransactionContent: FC = ({ network.genesisHash ).toUpperCase(); accountInformation = await updateAccountInformation({ - address: - AccountService.convertPublicKeyToAlgorandAddress(encodedPublicKey), + address: convertPublicKeyToAVMAddress(encodedPublicKey), currentAccountInformation: account.networkInformation[encodedGenesisHash] || AccountService.initializeDefaultAccountInformation(), diff --git a/src/extension/modals/SignTransactionsModal/hooks/useSignTransactionsModal/useSignTransactionsModal.ts b/src/extension/modals/SignTransactionsModal/hooks/useSignTransactionsModal/useSignTransactionsModal.ts index e024a561..d8f77c25 100644 --- a/src/extension/modals/SignTransactionsModal/hooks/useSignTransactionsModal/useSignTransactionsModal.ts +++ b/src/extension/modals/SignTransactionsModal/hooks/useSignTransactionsModal/useSignTransactionsModal.ts @@ -4,7 +4,7 @@ import { ISignTransactionsParams, } from '@agoralabs-sh/avm-web-provider'; import { decode as decodeBase64 } from '@stablelib/base64'; -import type { Transaction } from 'algosdk'; +import { Transaction, TransactionType } from 'algosdk'; import { useEffect, useState } from 'react'; import { useDispatch } from 'react-redux'; @@ -33,9 +33,9 @@ import type { import type { IState } from './types'; // utils +import convertGenesisHashToHex from '@extension/utils/convertGenesisHashToHex'; import decodeUnsignedTransaction from '@extension/utils/decodeUnsignedTransaction'; import uniqueGenesisHashesFromTransactions from '@extension/utils/uniqueGenesisHashesFromTransactions'; -import convertGenesisHashToHex from '@extension/utils/convertGenesisHashToHex'; export default function useSignTransactionsModal(): IState { const _hookName = 'useSignTransactionsModal'; @@ -104,7 +104,7 @@ export default function useSignTransactionsModal(): IState { const encodedGenesisHash: string = convertGenesisHashToHex(genesisHash).toUpperCase(); const unknownAssetIds: string[] = decodedUnsignedTransactions - .filter((value) => value.type === 'axfer') + .filter((value) => value.type === TransactionType.axfer) .filter( (transaction) => !standardAssets[encodedGenesisHash].some( diff --git a/src/extension/modals/SignTransactionsModal/utils/authorizedAccountsForEvent/authorizedAccountsForEvent.ts b/src/extension/modals/SignTransactionsModal/utils/authorizedAccountsForEvent/authorizedAccountsForEvent.ts index 81026132..a6d3dbd4 100644 --- a/src/extension/modals/SignTransactionsModal/utils/authorizedAccountsForEvent/authorizedAccountsForEvent.ts +++ b/src/extension/modals/SignTransactionsModal/utils/authorizedAccountsForEvent/authorizedAccountsForEvent.ts @@ -19,6 +19,7 @@ import type { import type { IOptions } from './types'; // utils +import convertPublicKeyToAVMAddress from '@extension/utils/convertPublicKeyToAVMAddress'; import decodeUnsignedTransaction from '@extension/utils/decodeUnsignedTransaction'; import getAuthorizedAddressesForHost from '@extension/utils/getAuthorizedAddressesForHost'; @@ -64,7 +65,7 @@ export default async function authorizedAccountsForEvent({ accounts.find( (value) => value.publicKey === - AccountService.encodePublicKey(currentValue.from.publicKey) + convertPublicKeyToAVMAddress(currentValue.from.publicKey) ) || null; base64EncodedGenesisHash = encodeBase64(currentValue.genesisHash); authorizedAddresses = getAuthorizedAddressesForHost( @@ -93,10 +94,7 @@ export default async function authorizedAccountsForEvent({ acc.find((value) => value.id === account?.id) || !authorizedAddresses.find( (value) => - value === - AccountService.convertPublicKeyToAlgorandAddress( - account?.publicKey || '' - ) + value === convertPublicKeyToAVMAddress(account?.publicKey || '') ) || (account.watchAccount && !accountInformation.authAddress) ) { diff --git a/src/extension/modals/SignTransactionsModal/utils/signTransactions/signTransactions.ts b/src/extension/modals/SignTransactionsModal/utils/signTransactions/signTransactions.ts index ddfe2c46..9aed3f78 100644 --- a/src/extension/modals/SignTransactionsModal/utils/signTransactions/signTransactions.ts +++ b/src/extension/modals/SignTransactionsModal/utils/signTransactions/signTransactions.ts @@ -3,18 +3,19 @@ import { decode as decodeBase64, encode as encodeBase64, } from '@stablelib/base64'; -import { encodeAddress, Transaction } from 'algosdk'; +import type { Transaction } from 'algosdk'; // errors import { MalformedDataError } from '@extension/errors'; // services -import AccountService from '@extension/services/AccountService'; +import PrivateKeyService from '@extension/services/PrivateKeyService'; // types -import type { IOptions } from './types'; +import type { TOptions } from './types'; // utils +import convertPublicKeyToAVMAddress from '@extension/utils/convertPublicKeyToAVMAddress'; import decodeUnsignedTransaction from '@extension/utils/decodeUnsignedTransaction'; import signTransaction from '@extension/utils/signTransaction'; @@ -32,8 +33,8 @@ export default async function signTransactions({ authAccounts, logger, networks, - password, -}: IOptions): Promise<(string | null)[]> { + ...encryptionOptions +}: TOptions): Promise<(string | null)[]> { const _functionName: string = 'signTransactions'; return await Promise.all( @@ -55,7 +56,9 @@ export default async function signTransactions({ } try { - signerAddress = encodeAddress(unsignedTransaction.from.publicKey); + signerAddress = convertPublicKeyToAVMAddress( + unsignedTransaction.from.publicKey + ); } catch (error) { logger?.error(`${_functionName}: ${error.message}`); @@ -67,7 +70,7 @@ export default async function signTransactions({ !accounts.some( (value) => value.publicKey === - AccountService.encodePublicKey(unsignedTransaction.from.publicKey) + PrivateKeyService.encode(unsignedTransaction.from.publicKey) ) ) { // if there is no signed transaction, we have been instructed to sign, so error @@ -89,11 +92,11 @@ export default async function signTransactions({ try { signedTransaction = await signTransaction({ + ...encryptionOptions, accounts, authAccounts, logger, networks, - password, unsignedTransaction, }); diff --git a/src/extension/modals/SignTransactionsModal/utils/signTransactions/types/IOptions.ts b/src/extension/modals/SignTransactionsModal/utils/signTransactions/types/TOptions.ts similarity index 75% rename from src/extension/modals/SignTransactionsModal/utils/signTransactions/types/IOptions.ts rename to src/extension/modals/SignTransactionsModal/utils/signTransactions/types/TOptions.ts index 195e8129..a69fa995 100644 --- a/src/extension/modals/SignTransactionsModal/utils/signTransactions/types/IOptions.ts +++ b/src/extension/modals/SignTransactionsModal/utils/signTransactions/types/TOptions.ts @@ -2,21 +2,25 @@ import type { IARC0001Transaction } from '@agoralabs-sh/avm-web-provider'; // types import type { IBaseOptions } from '@common/types'; -import type { IAccountWithExtendedProps, INetwork } from '@extension/types'; +import type { + IAccountWithExtendedProps, + INetwork, + TEncryptionCredentials, +} from '@extension/types'; /** * @property {IAccountWithExtendedProps[]} accounts - the authorized accounts. * @property {IAccountWithExtendedProps[]} authAccounts - [optional] a list of auth accounts that can sign the transaction for * re-keyed accounts. * @property {IARC0001Transaction[]} arc0001Transactions - the transactions to be signed. - * @property {string} password - the password that was used to encrypt the private key. */ interface IOptions extends IBaseOptions { accounts: IAccountWithExtendedProps[]; arc0001Transactions: IARC0001Transaction[]; authAccounts: IAccountWithExtendedProps[]; networks: INetwork[]; - password: string; } -export default IOptions; +type TOptions = IOptions & TEncryptionCredentials; + +export default TOptions; diff --git a/src/extension/modals/SignTransactionsModal/utils/signTransactions/types/index.ts b/src/extension/modals/SignTransactionsModal/utils/signTransactions/types/index.ts index 68e70016..b9eb0637 100644 --- a/src/extension/modals/SignTransactionsModal/utils/signTransactions/types/index.ts +++ b/src/extension/modals/SignTransactionsModal/utils/signTransactions/types/index.ts @@ -1 +1 @@ -export type { default as IOptions } from './IOptions'; +export type { default as TOptions } from './TOptions'; diff --git a/src/extension/modals/WalletConnectModal/WalletConnectModal.tsx b/src/extension/modals/WalletConnectModal/WalletConnectModal.tsx index 547dbf59..8b55ba81 100644 --- a/src/extension/modals/WalletConnectModal/WalletConnectModal.tsx +++ b/src/extension/modals/WalletConnectModal/WalletConnectModal.tsx @@ -49,9 +49,6 @@ import { useSelectWalletConnectModalOpen, } from '@extension/selectors'; -// services -import AccountService from '@extension/services/AccountService'; - // theme import { theme } from '@extension/theme'; @@ -59,6 +56,7 @@ import { theme } from '@extension/theme'; import { IAccount, INetwork } from '@extension/types'; // utils +import convertPublicKeyToAVMAddress from '@extension/utils/convertPublicKeyToAVMAddress'; import ellipseAddress from '@extension/utils/ellipseAddress'; interface IProps { @@ -250,8 +248,7 @@ const WalletConnectModal: FC = ({ onClose }: IProps) => { accountNodes = accounts.reduce( (acc, account, currentIndex) => { - const address: string = - AccountService.convertPublicKeyToAlgorandAddress(account.publicKey); + const address: string = convertPublicKeyToAVMAddress(account.publicKey); return [ ...acc, diff --git a/src/extension/models/Ed21559KeyPair/Ed21559KeyPair.ts b/src/extension/models/Ed21559KeyPair/Ed21559KeyPair.ts new file mode 100644 index 00000000..b953e4f8 --- /dev/null +++ b/src/extension/models/Ed21559KeyPair/Ed21559KeyPair.ts @@ -0,0 +1,71 @@ +import { sign, SignKeyPair } from 'tweetnacl'; + +// types +import type { INewOptions } from './types'; + +export default class Ed21559KeyPair { + // public variables + public readonly privateKey: Uint8Array; + public readonly publicKey: Uint8Array; + + constructor({ privateKey, publicKey }: INewOptions) { + this.privateKey = privateKey; + this.publicKey = publicKey; + } + + /** + * public static functions + */ + + /** + * Generates a new Ed21559 key pair. + * @returns {Ed21559KeyPair} a new Ed21559 key pair. + * @public + * @static + */ + public static generate(): Ed21559KeyPair { + const keyPair: SignKeyPair = sign.keyPair(); + + return new Ed21559KeyPair({ + privateKey: keyPair.secretKey.slice(0, sign.seedLength), // the private key or "seed" is the first 32 bytes of the "secret key" which is the private key concentrated to the public key + publicKey: keyPair.publicKey, + }); + } + + /** + * Generates an Ed21559 key pair from a private key. + * @param {Uint8Array} privateKey - a 32-byte private key (seed). + * @returns {Ed21559KeyPair} a new Ed21559 key pair. + * @public + * @static + */ + public static generateFromPrivateKey(privateKey: Uint8Array): Ed21559KeyPair { + const keyPair: SignKeyPair = sign.keyPair.fromSeed(privateKey); + + return new Ed21559KeyPair({ + privateKey: keyPair.secretKey.slice(0, sign.seedLength), // the private key or "seed" is the first 32 bytes of the "secret key" which is the private key concentrated to the public key + publicKey: keyPair.publicKey, + }); + } + + /** + * public functions + */ + + /** + * Gets the secret key for signing. The secret key is a 64 byte concatenation of the private key (32 byte) and the + * public key (32 byte). + * @returns {Uint8Array} the secret key used for signing. + * @public + */ + public getSecretKey(): Uint8Array { + const secretKey = new Uint8Array( + this.privateKey.length + this.publicKey.length + ); + + secretKey.set(this.privateKey); + secretKey.set(this.publicKey, this.privateKey.length); + + return secretKey; + } +} diff --git a/src/extension/models/Ed21559KeyPair/index.ts b/src/extension/models/Ed21559KeyPair/index.ts new file mode 100644 index 00000000..4fa155bb --- /dev/null +++ b/src/extension/models/Ed21559KeyPair/index.ts @@ -0,0 +1 @@ +export { default } from './Ed21559KeyPair'; diff --git a/src/extension/models/Ed21559KeyPair/types/INewOptions.ts b/src/extension/models/Ed21559KeyPair/types/INewOptions.ts new file mode 100644 index 00000000..e99f6897 --- /dev/null +++ b/src/extension/models/Ed21559KeyPair/types/INewOptions.ts @@ -0,0 +1,6 @@ +interface INewOptions { + privateKey: Uint8Array; + publicKey: Uint8Array; +} + +export default INewOptions; diff --git a/src/extension/models/Ed21559KeyPair/types/index.ts b/src/extension/models/Ed21559KeyPair/types/index.ts new file mode 100644 index 00000000..52d8062b --- /dev/null +++ b/src/extension/models/Ed21559KeyPair/types/index.ts @@ -0,0 +1 @@ +export type { default as INewOptions } from './INewOptions'; diff --git a/src/extension/pages/AccountPage/AccountPage.tsx b/src/extension/pages/AccountPage/AccountPage.tsx index b818952f..4aab67d8 100644 --- a/src/extension/pages/AccountPage/AccountPage.tsx +++ b/src/extension/pages/AccountPage/AccountPage.tsx @@ -43,6 +43,8 @@ import OpenTabIconButton from '@extension/components/OpenTabIconButton'; import NativeBalance from '@extension/components/NativeBalance'; import NetworkSelect from '@extension/components/NetworkSelect'; import NFTsTab from '@extension/components/NFTsTab'; +import ReKeyedAccountBadge from '@extension/components/RekeyedAccountBadge'; +import WatchAccountBadge from '@extension/components/WatchAccountBadge'; import AccountPageSkeletonContent from './AccountPageSkeletonContent'; // constants @@ -95,15 +97,14 @@ import { } from '@extension/selectors'; // services -import AccountService from '@extension/services/AccountService'; +import PrivateKeyService from '@extension/services/PrivateKeyService'; // types import type { IAppThunkDispatch, INetwork } from '@extension/types'; // utils +import convertPublicKeyToAVMAddress from '@extension/utils/convertPublicKeyToAVMAddress'; import ellipseAddress from '@extension/utils/ellipseAddress'; -import WatchAccountBadge from '@extension/components/WatchAccountBadge'; -import ReKeyedAccountBadge from '@extension/components/RekeyedAccountBadge'; import isReKeyedAuthAccountAvailable from '@extension/utils/isReKeyedAuthAccountAvailable'; const AccountPage: FC = () => { @@ -196,8 +197,8 @@ const AccountPage: FC = () => { setConfirmModal({ description: t('captions.removeAccount', { address: ellipseAddress( - AccountService.convertPublicKeyToAlgorandAddress( - account.publicKey + convertPublicKeyToAVMAddress( + PrivateKeyService.decode(account.publicKey) ), { end: 10, @@ -251,8 +252,8 @@ const AccountPage: FC = () => { } if (account && accountInformation && selectedNetwork) { - address = AccountService.convertPublicKeyToAlgorandAddress( - account.publicKey + address = convertPublicKeyToAVMAddress( + PrivateKeyService.decode(account.publicKey) ); return ( @@ -572,8 +573,8 @@ const AccountPage: FC = () => { {account && ( <> { dispatch( initializeSendAsset({ - fromAddress: AccountService.convertPublicKeyToAlgorandAddress( - account.publicKey + fromAddress: convertPublicKeyToAVMAddress( + PrivateKeyService.decode(account.publicKey) ), selectedAsset: asset, }) @@ -164,8 +165,8 @@ const AssetPage: FC = () => { return ; } - accountAddress = AccountService.convertPublicKeyToAlgorandAddress( - account.publicKey + accountAddress = convertPublicKeyToAVMAddress( + PrivateKeyService.decode(account.publicKey) ); return ( diff --git a/src/extension/pages/ChangePasswordPage/ChangePasswordPage.tsx b/src/extension/pages/ChangePasswordPage/ChangePasswordPage.tsx index 440e184b..5c82babf 100644 --- a/src/extension/pages/ChangePasswordPage/ChangePasswordPage.tsx +++ b/src/extension/pages/ChangePasswordPage/ChangePasswordPage.tsx @@ -1,7 +1,7 @@ import { Text, VStack, useDisclosure } from '@chakra-ui/react'; import React, { FC, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { NavigateFunction, useNavigate } from 'react-router-dom'; +import { useNavigate } from 'react-router-dom'; import { useDispatch } from 'react-redux'; // components @@ -26,22 +26,37 @@ import useChangePassword from '@extension/hooks/useChangePassword'; import useSubTextColor from '@extension/hooks/useSubTextColor'; // modals +import ChangePasswordLoadingModal from '@extension/modals/ChangePasswordLoadingModal'; import ConfirmPasswordModal from '@extension/modals/ConfirmPasswordModal'; // types -import { IAppThunkDispatch } from '@extension/types'; +import type { IAppThunkDispatch } from '@extension/types'; const ChangePasswordPage: FC = () => { const { t } = useTranslation(); - const dispatch: IAppThunkDispatch = useDispatch(); - const navigate: NavigateFunction = useNavigate(); - const { isOpen, onClose, onOpen } = useDisclosure(); + const dispatch = useDispatch(); + const navigate = useNavigate(); + const { + isOpen: isConfirmPasswordModalOpen, + onClose: onConfirmPasswordModalClose, + onOpen: onConfirmPasswordModalOpen, + } = useDisclosure(); // hooks - const { changePassword, error, passwordTag, saving } = useChangePassword(); - const subTextColor: string = useSubTextColor(); + const { + changePasswordAction, + encryptionProgressState, + encrypting, + error, + passwordTag, + resetAction, + validating, + } = useChangePassword(); + const subTextColor = useSubTextColor(); // state const [newPassword, setNewPassword] = useState(null); const [score, setScore] = useState(-1); + // misc + const isLoading = encrypting || validating; // handlers const handlePasswordChange = (newPassword: string, newScore: number) => { setNewPassword(newPassword); @@ -49,52 +64,84 @@ const ChangePasswordPage: FC = () => { }; const handleChangeClick = () => { if (!validate(newPassword || '', score, t)) { - onOpen(); + onConfirmPasswordModalOpen(); } }; const handleOnConfirmPasswordModalConfirm = async ( currentPassword: string ) => { - onClose(); + let success: boolean; + + if (!newPassword) { + return; + } // save the new password - if (newPassword) { - await changePassword(newPassword, currentPassword); + success = await changePasswordAction({ + currentPassword, + newPassword, + }); + + if (success) { + dispatch( + createNotification({ + ephemeral: true, + title: t('headings.passwordChanged'), + type: 'info', + }) + ); + navigate(`${SETTINGS_ROUTE}${SECURITY_ROUTE}`, { + replace: true, + }); + + // clean up + reset(); } }; + const reset = () => { + setNewPassword(null); + setScore(-1); + resetAction(); + }; // if there is an error from the hook, show a toast useEffect(() => { - if (error) { + error && dispatch( createNotification({ + description: t('errors.descriptions.code', { + code: error.code, + context: error.code, + }), ephemeral: true, - description: error.message, - title: `${error.code}: ${error.name}`, + title: t('errors.titles.code', { context: error.code }), type: 'error', }) ); - } }, [error]); // if we have the updated password tag navigate back useEffect(() => { if (passwordTag) { - setNewPassword(null); - setScore(-1); - navigate(`${SETTINGS_ROUTE}${SECURITY_ROUTE}`, { replace: true, }); + + reset(); } }, [passwordTag]); return ( <> + + ('titles.page', { context: 'changePassword' })} /> @@ -116,7 +163,7 @@ const ChangePasswordPage: FC = () => { ('labels.newPassword')} onChange={handlePasswordChange} score={score} @@ -125,7 +172,7 @@ const ChangePasswordPage: FC = () => { + + ); + } + + return ( + <> + + {/*icon*/} + + + {/*captions*/} + + + {t('captions.addPasskey1')} + + + + {t('captions.addPasskey2')} + + + + {t('captions.addPasskeyInstruction')} + + + + {/*passkey name*/} + + + {`${t('labels.passkeyName')} ${t( + 'labels.optional' + )}`} + + + + ('placeholders.passkeyName')} + type="text" + value={passkeyName || ''} + /> + + + + + + + ); + }; + + return ( + <> + {/*modals*/} + + + + ('titles.page', { context: 'passkey' })} /> + + + {renderContent()} + + + ); +}; + +export default PasskeyPage; diff --git a/src/extension/pages/PasskeyPage/index.ts b/src/extension/pages/PasskeyPage/index.ts new file mode 100644 index 00000000..ac8527dd --- /dev/null +++ b/src/extension/pages/PasskeyPage/index.ts @@ -0,0 +1 @@ +export { default } from './PasskeyPage'; diff --git a/src/extension/pages/PasswordLockPage/PasswordLockPage.tsx b/src/extension/pages/PasswordLockPage/PasswordLockPage.tsx index d80cd0e0..f610d25d 100644 --- a/src/extension/pages/PasswordLockPage/PasswordLockPage.tsx +++ b/src/extension/pages/PasswordLockPage/PasswordLockPage.tsx @@ -1,116 +1,83 @@ -import { Center, Flex, Heading, VStack } from '@chakra-ui/react'; -import React, { - FC, - KeyboardEvent, - MutableRefObject, - useEffect, - useRef, - useState, -} from 'react'; +import { Center, Flex, Heading, useDisclosure, VStack } from '@chakra-ui/react'; +import React, { FC, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useDispatch } from 'react-redux'; -import { NavigateFunction, useNavigate } from 'react-router-dom'; -import browser from 'webextension-polyfill'; +import { useNavigate } from 'react-router-dom'; // components -import KibisisIcon from '@extension/components/KibisisIcon'; import Button from '@extension/components/Button'; -import PasswordInput, { - usePassword, -} from '@extension/components/PasswordInput'; +import KibisisIcon from '@extension/components/KibisisIcon'; // constants import { BODY_BACKGROUND_COLOR, DEFAULT_GAP } from '@extension/constants'; +// errors +import { BaseExtensionError } from '@extension/errors'; + // features +import { create as createNotification } from '@extension/features/notifications'; import { savePasswordLockThunk } from '@extension/features/password-lock'; // hooks import useDefaultTextColor from '@extension/hooks/useDefaultTextColor'; import usePrimaryColor from '@extension/hooks/usePrimaryColor'; +// modals +import AuthenticationModal, { + TOnConfirmResult, +} from '@extension/modals/AuthenticationModal'; + // selectors import { useSelectLogger, - useSelectPasswordLockPassword, + useSelectPasswordLockCredentials, useSelectPasswordLockSaving, } from '@extension/selectors'; -// services -import PrivateKeyService from '@extension/services/PrivateKeyService'; - // types -import type { ILogger } from '@common/types'; import type { IAppThunkDispatch } from '@extension/types'; const PasswordLockPage: FC = () => { const { t } = useTranslation(); - const dispatch: IAppThunkDispatch = useDispatch(); - const navigate: NavigateFunction = useNavigate(); - const passwordInputRef: MutableRefObject = - useRef(null); + const dispatch = useDispatch(); + const navigate = useNavigate(); + const { + isOpen: isAuthenticationModalOpen, + onClose: onAuthenticationModalClose, + onOpen: onAuthenticationModalOpen, + } = useDisclosure(); // selectors - const logger: ILogger = useSelectLogger(); - const passwordLockPassword: string | null = useSelectPasswordLockPassword(); - const saving: boolean = useSelectPasswordLockSaving(); + const logger = useSelectLogger(); + const passwordLockPassword = useSelectPasswordLockCredentials(); + const saving = useSelectPasswordLockSaving(); // hooks - const defaultTextColor: string = useDefaultTextColor(); - const { - error: passwordError, - onChange: onPasswordChange, - setError: setPasswordError, - validate: validatePassword, - value: password, - } = usePassword(); - const primaryColor: string = usePrimaryColor(); + const defaultTextColor = useDefaultTextColor(); + const primaryColor = usePrimaryColor(); // states const [verifying, setVerifying] = useState(false); // misc - const isLoading: boolean = saving || verifying; + const isLoading = saving || verifying; // handlers - const handleKeyUpPasswordInput = async ( - event: KeyboardEvent + const handleOnAuthenticationError = (error: BaseExtensionError) => + dispatch( + createNotification({ + description: t('errors.descriptions.code', { + code: error.code, + context: error.code, + }), + ephemeral: true, + title: t('errors.titles.code', { context: error.code }), + type: 'error', + }) + ); + const handleUnlockClick = () => onAuthenticationModalOpen(); + const handleOnAuthenticationModalConfirm = async ( + result: TOnConfirmResult ) => { - if (event.key === 'Enter') { - await handleConfirmClick(); - } + // save the password lock passkey/password and clear any alarms + dispatch(savePasswordLockThunk(result)); }; - const handleConfirmClick = async () => { - let isValid: boolean; - let privateKeyService: PrivateKeyService; - - // check if the input is valid - if (validatePassword()) { - return; - } - - privateKeyService = new PrivateKeyService({ - logger, - passwordTag: browser.runtime.id, - }); - - setVerifying(true); - - isValid = await privateKeyService.verifyPassword(password); - - setVerifying(false); - if (!isValid) { - setPasswordError(t('errors.inputs.invalidPassword')); - - return; - } - - // save the password lock password and clear any alarms - dispatch(savePasswordLockThunk(password)); - }; - - // focus on password input - useEffect(() => { - if (passwordInputRef.current) { - passwordInputRef.current.focus(); - } - }, []); useEffect(() => { if (passwordLockPassword) { navigate(-1); @@ -118,56 +85,61 @@ const PasswordLockPage: FC = () => { }, [passwordLockPassword]); return ( -
- - + {/*authentication modal*/} + ('captions.mustEnterPasswordToUnlock')} + /> + +
+ - - - {/*icon*/} - - - {/*heading*/} - - {t('headings.passwordLock')} - + + + + {/*icon*/} + + + {/*heading*/} + + {t('headings.passwordLock')} + + - {/*password input*/} - ('captions.mustEnterPasswordToUnlock')} - inputRef={passwordInputRef} - onChange={onPasswordChange} - onKeyUp={handleKeyUpPasswordInput} - value={password || ''} - /> + {/*unlock button*/} + - - {/*confirm button*/} - - - -
+
+
+ ); }; diff --git a/src/extension/pages/SecuritySettingsIndexPage/SecuritySettingsIndexPage.tsx b/src/extension/pages/SecuritySettingsIndexPage/SecuritySettingsIndexPage.tsx index e2a76d49..494199b8 100644 --- a/src/extension/pages/SecuritySettingsIndexPage/SecuritySettingsIndexPage.tsx +++ b/src/extension/pages/SecuritySettingsIndexPage/SecuritySettingsIndexPage.tsx @@ -1,6 +1,7 @@ import { useDisclosure, VStack } from '@chakra-ui/react'; import React, { ChangeEvent, FC } from 'react'; import { useTranslation } from 'react-i18next'; +import { GoShieldLock } from 'react-icons/go'; import { IoKeyOutline, IoLockClosedOutline, @@ -21,6 +22,7 @@ import SettingsSwitchItem from '@extension/components/SettingsSwitchItem'; import { CHANGE_PASSWORD_ROUTE, EXPORT_ACCOUNT_ROUTE, + PASSKEY_ROUTE, PASSWORD_LOCK_DURATION_HIGH, PASSWORD_LOCK_DURATION_HIGHER, PASSWORD_LOCK_DURATION_HIGHEST, @@ -32,31 +34,44 @@ import { VIEW_SEED_PHRASE_ROUTE, } from '@extension/constants'; +// errors +import { BaseExtensionError } from '@extension/errors'; + // features +import { create as createNotification } from '@extension/features/notifications'; import { savePasswordLockThunk } from '@extension/features/password-lock'; import { saveSettingsToStorageThunk } from '@extension/features/settings'; // modals -import ConfirmPasswordModal from '@extension/modals/ConfirmPasswordModal'; +import AuthenticationModal, { + TOnConfirmResult, +} from '@extension/modals/AuthenticationModal'; // selectors -import { useSelectLogger, useSelectSettings } from '@extension/selectors'; +import { + useSelectLogger, + useSelectPasskeysEnabled, + useSelectSettings, +} from '@extension/selectors'; + +// services +import PasskeyService from '@extension/services/PasskeyService'; // types -import type { ILogger } from '@common/types'; -import type { IAppThunkDispatch, ISettings } from '@extension/types'; +import type { IAppThunkDispatch } from '@extension/types'; const SecuritySettingsIndexPage: FC = () => { const { t } = useTranslation(); - const dispatch: IAppThunkDispatch = useDispatch(); + const dispatch = useDispatch(); const { - isOpen: isPasswordConfirmModalOpen, - onClose: onPasswordConfirmModalClose, - onOpen: onPasswordConfirmModalOpen, + isOpen: isAuthenticationModalOpen, + onClose: onAuthenticationModalClose, + onOpen: onAuthenticationModalOpen, } = useDisclosure(); // selectors - const logger: ILogger = useSelectLogger(); - const settings: ISettings = useSelectSettings(); + const logger = useSelectLogger(); + const passkeyEnabled = useSelectPasskeysEnabled(); + const settings = useSelectSettings(); // misc const durationOptions: IOption[] = [ { @@ -104,11 +119,11 @@ const SecuritySettingsIndexPage: FC = () => { const handleEnablePasswordLockSwitchChange = async ( event: ChangeEvent ) => { - const _functionName: string = 'handleEnablePasswordLockSwitchChange'; + const _functionName = 'handleEnablePasswordLockSwitchChange'; // if we are enabling, we need to set the password if (event.target.checked) { - onPasswordConfirmModalOpen(); + onAuthenticationModalOpen(); return; } @@ -133,10 +148,10 @@ const SecuritySettingsIndexPage: FC = () => { ); } }; - const handleOnConfirmPasswordModalConfirm = async (password: string) => { - const _functionName: string = 'handleOnConfirmPasswordModalConfirm'; - - onPasswordConfirmModalClose(); + const handleOnAuthenticationModalConfirm = async ( + result: TOnConfirmResult + ) => { + const _functionName = 'handleOnAuthenticationModalConfirm'; try { // enable the lock and wait for the settings to be updated @@ -151,13 +166,25 @@ const SecuritySettingsIndexPage: FC = () => { ).unwrap(); // then... save the new password to the password lock - dispatch(savePasswordLockThunk(password)); + dispatch(savePasswordLockThunk(result)); } catch (error) { logger.debug( `${SecuritySettingsIndexPage.name}#${_functionName}: failed save settings` ); } }; + const handleOnAuthenticationError = (error: BaseExtensionError) => + dispatch( + createNotification({ + description: t('errors.descriptions.code', { + code: error.code, + context: error.code, + }), + ephemeral: true, + title: t('errors.titles.code', { context: error.code }), + type: 'error', + }) + ); const handlePasswordTimeoutDurationChange = (option: IOption) => { dispatch( saveSettingsToStorageThunk({ @@ -172,10 +199,12 @@ const SecuritySettingsIndexPage: FC = () => { return ( <> - ('titles.page', { context: 'security' })} /> @@ -219,6 +248,42 @@ const SecuritySettingsIndexPage: FC = () => { to={`${SETTINGS_ROUTE}${SECURITY_ROUTE}${CHANGE_PASSWORD_ROUTE}`} /> + {/*passkey*/} + ('labels.enabled'), + } + : { + colorScheme: 'red', + label: t('labels.disabled'), + }), + }, + ] + : [ + { + colorScheme: 'yellow', + label: t('labels.notSupported'), + }, + ]), + { + colorScheme: 'blue', + label: t('labels.experimental'), + }, + ]} + icon={GoShieldLock} + label={t('titles.page', { context: 'passkey' })} + to={`${SETTINGS_ROUTE}${SECURITY_ROUTE}${PASSKEY_ROUTE}`} + /> + + {/*accounts*/} + ('headings.accounts')} /> + {/*view seed phrase*/} diff --git a/src/extension/pages/TransactionPage/AccountReKeyTransactionPage.tsx b/src/extension/pages/TransactionPage/AccountReKeyTransactionPage.tsx index 07c2a51a..30f71e45 100644 --- a/src/extension/pages/TransactionPage/AccountReKeyTransactionPage.tsx +++ b/src/extension/pages/TransactionPage/AccountReKeyTransactionPage.tsx @@ -33,13 +33,14 @@ import { } from '@extension/selectors'; // services -import AccountService from '@extension/services/AccountService'; +import PrivateKeyService from '@extension/services/PrivateKeyService'; // types import type { IAccountReKeyTransaction } from '@extension/types'; import type { IProps } from './types'; // utils +import convertPublicKeyToAVMAddress from '@extension/utils/convertPublicKeyToAVMAddress'; import createIconFromDataUri from '@extension/utils/createIconFromDataUri'; import ellipseAddress from '@extension/utils/ellipseAddress'; @@ -65,20 +66,23 @@ const AccountReKeyTransactionPage: FC> = ({ const isAuthAddrKnown = accounts.findIndex( (value) => - AccountService.convertPublicKeyToAlgorandAddress(value.publicKey) === - transaction.authAddr + convertPublicKeyToAVMAddress( + PrivateKeyService.decode(value.publicKey) + ) === transaction.authAddr ) > -1; const isReKeyedKnown = accounts.findIndex( (value) => - AccountService.convertPublicKeyToAlgorandAddress(value.publicKey) === - transaction.rekeyTo + convertPublicKeyToAVMAddress( + PrivateKeyService.decode(value.publicKey) + ) === transaction.rekeyTo ) > -1; const isSenderKnown = accounts.findIndex( (value) => - AccountService.convertPublicKeyToAlgorandAddress(value.publicKey) === - transaction.sender + convertPublicKeyToAVMAddress( + PrivateKeyService.decode(value.publicKey) + ) === transaction.sender ) > -1; // handlers const handleMoreInformationToggle = (value: boolean) => diff --git a/src/extension/pages/TransactionPage/AccountUndoReKeyTransactionPage.tsx b/src/extension/pages/TransactionPage/AccountUndoReKeyTransactionPage.tsx index 9b392bac..645dd295 100644 --- a/src/extension/pages/TransactionPage/AccountUndoReKeyTransactionPage.tsx +++ b/src/extension/pages/TransactionPage/AccountUndoReKeyTransactionPage.tsx @@ -33,13 +33,14 @@ import { } from '@extension/selectors'; // services -import AccountService from '@extension/services/AccountService'; +import PrivateKeyService from '@extension/services/PrivateKeyService'; // types import type { IAccountUndoReKeyTransaction } from '@extension/types'; import type { IProps } from './types'; // utils +import convertPublicKeyToAVMAddress from '@extension/utils/convertPublicKeyToAVMAddress'; import createIconFromDataUri from '@extension/utils/createIconFromDataUri'; import ellipseAddress from '@extension/utils/ellipseAddress'; @@ -64,14 +65,16 @@ const AccountUndoReKeyTransactionPage: FC< const isAuthAddressKnown = accounts.findIndex( (value) => - AccountService.convertPublicKeyToAlgorandAddress(value.publicKey) === - transaction.authAddr + convertPublicKeyToAVMAddress( + PrivateKeyService.decode(value.publicKey) + ) === transaction.authAddr ) > -1; const isSenderKnown = accounts.findIndex( (value) => - AccountService.convertPublicKeyToAlgorandAddress(value.publicKey) === - transaction.sender + convertPublicKeyToAVMAddress( + PrivateKeyService.decode(value.publicKey) + ) === transaction.sender ) > -1; // handlers const handleMoreInformationToggle = (value: boolean) => diff --git a/src/extension/pages/TransactionPage/AssetTransferTransactionPage.tsx b/src/extension/pages/TransactionPage/AssetTransferTransactionPage.tsx index 16dafc83..a457dc17 100644 --- a/src/extension/pages/TransactionPage/AssetTransferTransactionPage.tsx +++ b/src/extension/pages/TransactionPage/AssetTransferTransactionPage.tsx @@ -39,13 +39,14 @@ import useSubTextColor from '@extension/hooks/useSubTextColor'; import { useSelectSettingsPreferredBlockExplorer } from '@extension/selectors'; // services -import AccountService from '@extension/services/AccountService'; +import PrivateKeyService from '@extension/services/PrivateKeyService'; // types import type { IAssetTransferTransaction } from '@extension/types'; import type { IProps } from './types'; // utils +import convertPublicKeyToAVMAddress from '@extension/utils/convertPublicKeyToAVMAddress'; import createIconFromDataUri from '@extension/utils/createIconFromDataUri'; import ellipseAddress from '@extension/utils/ellipseAddress'; import isAccountKnown from '@extension/utils/isAccountKnown'; @@ -66,8 +67,8 @@ const AssetTransferTransactionPage: FC> = ({ const primaryButtonTextColor = usePrimaryButtonTextColor(); const subTextColor = useSubTextColor(); // misc - const accountAddress = AccountService.convertPublicKeyToAlgorandAddress( - account.publicKey + const accountAddress = convertPublicKeyToAVMAddress( + PrivateKeyService.decode(account.publicKey) ); const amount = new BigNumber(transaction.amount); const explorer = diff --git a/src/extension/pages/TransactionPage/PaymentTransactionPage.tsx b/src/extension/pages/TransactionPage/PaymentTransactionPage.tsx index d6d73055..0367dfce 100644 --- a/src/extension/pages/TransactionPage/PaymentTransactionPage.tsx +++ b/src/extension/pages/TransactionPage/PaymentTransactionPage.tsx @@ -29,14 +29,12 @@ import useSubTextColor from '@extension/hooks/useSubTextColor'; // selectors import { useSelectSettingsPreferredBlockExplorer } from '@extension/selectors'; -// services -import AccountService from '@extension/services/AccountService'; - // types import type { IPaymentTransaction } from '@extension/types'; import type { IProps } from './types'; // utils +import convertPublicKeyToAVMAddress from '@extension/utils/convertPublicKeyToAVMAddress'; import createIconFromDataUri from '@extension/utils/createIconFromDataUri'; import ellipseAddress from '@extension/utils/ellipseAddress'; @@ -54,9 +52,7 @@ const PaymentTransactionPage: FC> = ({ const defaultTextColor = useDefaultTextColor(); const subTextColor = useSubTextColor(); // misc - const accountAddress = AccountService.convertPublicKeyToAlgorandAddress( - account.publicKey - ); + const accountAddress = convertPublicKeyToAVMAddress(account.publicKey); const amount: BigNumber = new BigNumber(transaction.amount); const explorer = network.blockExplorers.find( @@ -67,14 +63,12 @@ const PaymentTransactionPage: FC> = ({ const isReceiverKnown = accounts.findIndex( (value) => - AccountService.convertPublicKeyToAlgorandAddress(value.publicKey) === - transaction.receiver + convertPublicKeyToAVMAddress(value.publicKey) === transaction.receiver ) > -1; const isSenderKnown = accounts.findIndex( (value) => - AccountService.convertPublicKeyToAlgorandAddress(value.publicKey) === - transaction.sender + convertPublicKeyToAVMAddress(value.publicKey) === transaction.sender ) > -1; // handlers const handleMoreInformationToggle = (value: boolean) => diff --git a/src/extension/pages/ViewSeedPhrasePage/ViewSeedPhrasePage.tsx b/src/extension/pages/ViewSeedPhrasePage/ViewSeedPhrasePage.tsx index 816ee8cf..40c53fed 100644 --- a/src/extension/pages/ViewSeedPhrasePage/ViewSeedPhrasePage.tsx +++ b/src/extension/pages/ViewSeedPhrasePage/ViewSeedPhrasePage.tsx @@ -1,11 +1,8 @@ import { SkeletonText, Text, useDisclosure, VStack } from '@chakra-ui/react'; -import { decode as decodeHex } from '@stablelib/hex'; -import { secretKeyToMnemonic } from 'algosdk'; import React, { FC, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { IoEyeOutline } from 'react-icons/io5'; import { useDispatch } from 'react-redux'; -import browser from 'webextension-polyfill'; // components import AccountSelect from '@extension/components/AccountSelect'; @@ -22,8 +19,11 @@ import { DEFAULT_GAP, } from '@extension/constants'; +// enums +import { EncryptionMethodEnum } from '@extension/enums'; + // errors -import { DecryptionError } from '@extension/errors'; +import { BaseExtensionError, DecryptionError } from '@extension/errors'; // features import { create as createNotification } from '@extension/features/notifications'; @@ -33,29 +33,41 @@ import useDefaultTextColor from '@extension/hooks/useDefaultTextColor'; import usePrimaryColorScheme from '@extension/hooks/usePrimaryColorScheme'; // modals -import ConfirmPasswordModal from '@extension/modals/ConfirmPasswordModal'; +import AuthenticationModal, { + TOnConfirmResult, +} from '@extension/modals/AuthenticationModal'; + +// models +import Ed21559KeyPair from '@extension/models/Ed21559KeyPair'; // selectors import { useSelectActiveAccount, - useSelectNonWatchAccounts, useSelectLogger, + useSelectNonWatchAccounts, } from '@extension/selectors'; -// services -import AccountService from '@extension/services/AccountService'; -import PrivateKeyService from '@extension/services/PrivateKeyService'; - // types import type { IAccountWithExtendedProps, IAppThunkDispatch, } from '@extension/types'; +import type { ISeedPhraseInput } from './types'; + +// utils +import convertPrivateKeyToSeedPhrase from '@extension/utils/convertPrivateKeyToSeedPhrase'; +import convertPublicKeyToAVMAddress from '@extension/utils/convertPublicKeyToAVMAddress'; +import fetchDecryptedKeyPairFromStorageWithPasskey from '@extension/utils/fetchDecryptedKeyPairFromStorageWithPasskey'; +import fetchDecryptedKeyPairFromStorageWithPassword from '@extension/utils/fetchDecryptedKeyPairFromStorageWithPassword'; const ViewSeedPhrasePage: FC = () => { const { t } = useTranslation(); const dispatch = useDispatch(); - const { isOpen, onClose, onOpen } = useDisclosure(); + const { + isOpen: isAuthenticationModalOpen, + onClose: onAuthenticationModalClose, + onOpen: onAuthenticationModalOpen, + } = useDisclosure(); // selectors const accounts = useSelectNonWatchAccounts(); const activeAccount = useSelectActiveAccount(); @@ -64,53 +76,74 @@ const ViewSeedPhrasePage: FC = () => { const defaultTextColor = useDefaultTextColor(); const primaryColorScheme = usePrimaryColorScheme(); // state - const [password, setPassword] = useState(null); - const [seedPhrase, setSeedPhrase] = useState( - createMaskedSeedPhrase() - ); + const [decrypting, setDecrypting] = useState(false); + const [seedPhrase, setSeedPhrase] = useState({ + masked: true, + value: createMaskedSeedPhrase(), + }); const [selectedAccount, setSelectedAccount] = useState(null); - // misc - const decryptSeedPhrase = async () => { - const _functionName = 'decryptSeedPhrase'; - const privateKeyService = new PrivateKeyService({ - logger, - passwordTag: browser.runtime.id, - }); - let privateKey: Uint8Array | null; - - if (!password || !selectedAccount) { + // handlers + const handleAccountSelect = (account: IAccountWithExtendedProps) => + setSelectedAccount(account); + const handleOnAuthenticationModalConfirm = async ( + result: TOnConfirmResult + ) => { + const _functionName = 'handleOnAuthenticationModalConfirm'; + let keyPair: Ed21559KeyPair | null = null; + + if (!selectedAccount) { logger?.debug( - `${ViewSeedPhrasePage.name}#${_functionName}: no password or account found` + `${ViewSeedPhrasePage.name}#${_functionName}: no account selected` ); return; } + setDecrypting(true); + // get the private key try { - privateKey = await privateKeyService.getDecryptedPrivateKey( - decodeHex(selectedAccount.publicKey), - password - ); + if (result.type === EncryptionMethodEnum.Passkey) { + keyPair = await fetchDecryptedKeyPairFromStorageWithPasskey({ + inputKeyMaterial: result.inputKeyMaterial, + logger, + publicKey: selectedAccount.publicKey, + }); + } + + if (result.type === EncryptionMethodEnum.Password) { + keyPair = await fetchDecryptedKeyPairFromStorageWithPassword({ + logger, + password: result.password, + publicKey: selectedAccount.publicKey, + }); + } - if (!privateKey) { + if (!keyPair) { throw new DecryptionError( - `failed to get private key for account "${AccountService.convertPublicKeyToAlgorandAddress( + `failed to get private key for account "${convertPublicKeyToAVMAddress( selectedAccount.publicKey )}"` ); } - // convert the secret key to the mnemonic - setSeedPhrase(secretKeyToMnemonic(privateKey)); + // convert the private key to the seed phrase + setSeedPhrase({ + masked: false, + value: convertPrivateKeyToSeedPhrase({ + logger, + privateKey: keyPair.privateKey, + }), + }); } catch (error) { - logger?.error( - `${ViewSeedPhrasePage.name}#${_functionName}: ${error.message}` - ); + logger?.error(`${ViewSeedPhrasePage.name}#${_functionName}:`, error); // reset the seed phrase - setSeedPhrase(createMaskedSeedPhrase()); + setSeedPhrase({ + masked: true, + value: createMaskedSeedPhrase(), + }); dispatch( createNotification({ @@ -123,26 +156,24 @@ const ViewSeedPhrasePage: FC = () => { type: 'error', }) ); - - return; } - }; - // handlers - const handleAccountSelect = (account: IAccountWithExtendedProps) => - setSelectedAccount(account); - const handleOnConfirmPasswordModalConfirm = async (password: string) => { - // close the password modal - onClose(); - setPassword(password); + setDecrypting(false); }; - const handleViewClick = () => onOpen(); + const handleOnAuthenticationError = (error: BaseExtensionError) => + dispatch( + createNotification({ + description: t('errors.descriptions.code', { + code: error.code, + context: error.code, + }), + ephemeral: true, + title: t('errors.titles.code', { context: error.code }), + type: 'error', + }) + ); + const handleViewClick = () => onAuthenticationModalOpen(); - useEffect(() => { - if (password) { - (async () => await decryptSeedPhrase())(); - } - }, [password, selectedAccount]); useEffect(() => { let _selectedAccount: IAccountWithExtendedProps; @@ -160,10 +191,12 @@ const ViewSeedPhrasePage: FC = () => { return ( <> - {/*page title*/} @@ -214,15 +247,15 @@ const ViewSeedPhrasePage: FC = () => { {/*seed phrase*/} - + - {password ? ( + {!seedPhrase.masked ? ( // copy seed phrase button @@ -231,6 +264,7 @@ const ViewSeedPhrasePage: FC = () => { ) : ( // view button