diff --git a/packages/extension-base/src/background/handlers/Extension.ts b/packages/extension-base/src/background/handlers/Extension.ts index 0770377d4dd..e2bc5ff75b8 100644 --- a/packages/extension-base/src/background/handlers/Extension.ts +++ b/packages/extension-base/src/background/handlers/Extension.ts @@ -8,7 +8,7 @@ import type { SubjectInfo } from '@polkadot/ui-keyring/observable/types'; import type { KeypairType } from '@polkadot/util-crypto/types'; import type { AccountJson, AllowedPath, MessageTypes, RequestAccountChangePassword, RequestAccountCreateHardware, RequestAccountCreateSuri, RequestAccountEdit, RequestAccountExport, RequestAccountForget, RequestAccountShow, RequestAccountTie, RequestAccountValidate, RequestActiveTabUrlUpdate, RequestAuthorizeApprove, RequestAuthorizeReject, RequestBatchRestore, RequestDeriveCreate, RequestDeriveValidate, RequestJsonRestore, RequestMetadataApprove, RequestMetadataReject, RequestSeedCreate, RequestSeedValidate, RequestSigningApprovePassword, RequestSigningApproveSignature, RequestSigningCancel, RequestSigningIsLocked, RequestTypes, RequestUpdateAuthorizedAccounts, ResponseAccountExport, ResponseAuthorizeList, ResponseDeriveValidate, ResponseJsonGetAccountInfo, ResponseSeedCreate, ResponseSeedValidate, ResponseSigningIsLocked } from '../types'; -import { ALLOWED_PATH, PASSWORD_EXPIRY_MS } from '@polkadot/extension-base/defaults'; +import { ALLOWED_PATH } from '@polkadot/extension-base/defaults'; import { isJsonAuthentic, signJson } from '@polkadot/extension-base/utils/accountJsonIntegrity'; import { metadataExpand } from '@polkadot/extension-chains'; import { wrapBytes } from '@polkadot/extension-dapp'; @@ -18,13 +18,12 @@ import { accounts as accountsObservable } from '@polkadot/ui-keyring/observable/ import { assert, isHex, u8aToHex } from '@polkadot/util'; import { keyExtractSuri, mnemonicGenerate, mnemonicValidate } from '@polkadot/util-crypto'; +import chromeStorage from './chromeStorage'; import { POPUP_CREATE_WINDOW_DATA } from './consts'; import { openCenteredWindow } from './helpers'; import State from './State'; import { createSubscription, unsubscribe } from './subscriptions'; -type CachedUnlocks = Record; - type GetContentPort = (tabId: number) => chrome.runtime.Port const SEED_DEFAULT_LENGTH = 12; @@ -40,12 +39,9 @@ function isJsonPayload (value: SignerPayloadJSON | SignerPayloadRaw): value is S } export default class Extension { - readonly #cachedUnlocks: CachedUnlocks; - readonly #state: State; constructor (state: State) { - this.#cachedUnlocks = {}; this.#state = state; } @@ -137,14 +133,15 @@ export default class Extension { return true; } - private refreshAccountPasswordCache (pair: KeyringPair): number { + private async refreshAccountPasswordCache (pair: KeyringPair): Promise { const { address } = pair; - const savedExpiry = this.#cachedUnlocks[address] || 0; - const remainingTime = savedExpiry - Date.now(); + const savedPassword = await chromeStorage.getPassword(address); + + const remainingTime = savedPassword?.expiry - Date.now(); if (remainingTime < 0) { - this.#cachedUnlocks[address] = 0; + await chromeStorage.removePassword(address); pair.lock(); return 0; @@ -334,13 +331,16 @@ export default class Extension { }; } - private async signingApprovePassword ({ id, password, savePass }: RequestSigningApprovePassword, getContentPort: GetContentPort): Promise { + private async signingApprovePassword ({ id, password: inputPassword, savePass }: RequestSigningApprovePassword, getContentPort: GetContentPort): Promise { const queued = await this.#state.getSignRequest(id); assert(queued, 'Unable to find request'); const { payload } = queued; const pair = keyring.getPair(queued.account.address); + const savedPassword = await chromeStorage.getPassword(pair.address); + + const password = savedPassword?.expiry > 0 ? savedPassword?.password : inputPassword; if (!pair) { const error = new Error('Unable to find pair'); @@ -351,7 +351,7 @@ export default class Extension { throw error; } - this.refreshAccountPasswordCache(pair); + await this.refreshAccountPasswordCache(pair); // if the keyring pair is locked, the password is needed if (pair.isLocked && !password) { @@ -408,13 +408,19 @@ export default class Extension { .createType('ExtrinsicPayload', payload, { version: payload.version }) .sign(pair); - if (savePass) { + if (savePass && password) { // unlike queued.account.address the following // address is encoded with the default prefix // which what is used for password caching mapping - this.#cachedUnlocks[pair.address] = Date.now() + PASSWORD_EXPIRY_MS; + await chromeStorage.setPassword(pair.address, password); } else { - pair.lock(); + const savedExpiry = await chromeStorage.getPassword(pair.address); + + const remainingTime = savedExpiry?.expiry - Date.now(); + + if (remainingTime <= 0) { + pair.lock(); + } } await this.#state.removeSignRequest(id); @@ -449,7 +455,7 @@ export default class Extension { assert(pair, 'Unable to find pair'); - const remainingTime = this.refreshAccountPasswordCache(pair); + const remainingTime = await this.refreshAccountPasswordCache(pair); return { isLocked: pair.isLocked, diff --git a/packages/extension-base/src/background/handlers/chromeStorage.ts b/packages/extension-base/src/background/handlers/chromeStorage.ts new file mode 100644 index 00000000000..d588dc80e03 --- /dev/null +++ b/packages/extension-base/src/background/handlers/chromeStorage.ts @@ -0,0 +1,61 @@ +import { z } from 'zod'; + +import { PASSWORD_EXPIRY_MS } from '../../defaults'; + +const addressSchema = z.string(); +const passwordSchema = z.record(z.string(), z.object({ expiry: z.number(), password: z.string() })); + +type Password = { expiry: number, password?: string } + +async function getPassword (address: string): Promise { + if (addressSchema.safeParse(address).success) { + const { savePass } = await chrome.storage.session.get('savePass'); + + const pass = passwordSchema.safeParse(savePass); + + if (pass.success) { + return pass.data[address]; + } else { + return { expiry: 0 }; + } + } else { + return { expiry: 0 }; + } +} + +async function setPassword (address: string, password: string): Promise { + if (addressSchema.safeParse(address).success) { + const { savePass } = await chrome.storage.session.get('savePass'); + + const savedPasswords = passwordSchema.safeParse(savePass); + + if (savedPasswords.success) { + await chrome.storage.session.set({ savePass: { ...savedPasswords.data, [address]: { password, expiry: Date.now() + PASSWORD_EXPIRY_MS } } }); + } else { + await chrome.storage.session.set({ savePass: { [address]: { expiry: Date.now() + PASSWORD_EXPIRY_MS, password } } }); + } + } else { + console.error('Provided address did not pass validation'); + } +} + +async function removePassword (address: string): Promise { + if (addressSchema.safeParse(address).success) { + const { savePass } = await chrome.storage.session.get('savePass'); + + const pass = passwordSchema.safeParse(savePass); + + if (pass.success) { + delete pass.data[address]; + await chrome.storage.session.set({ savePass: pass.data }); + } + } +} + +const chromeStorage = { + setPassword, + getPassword, + removePassword +}; + +export default chromeStorage; diff --git a/packages/extension-ui/src/Popup/Signing/Request/SignArea.tsx b/packages/extension-ui/src/Popup/Signing/Request/SignArea.tsx index 043e1d4ce7a..5499f223427 100644 --- a/packages/extension-ui/src/Popup/Signing/Request/SignArea.tsx +++ b/packages/extension-ui/src/Popup/Signing/Request/SignArea.tsx @@ -37,15 +37,15 @@ function SignArea({ buttonText, className, error, isExternal, isFirst, isLast, s !isExternal && isSignLocked(signId) - .then(({ isLocked, remainingTime }) => { - setIsLocked(isLocked); + .then(({ remainingTime }) => { + setIsLocked(remainingTime <= 0); timeout = setTimeout(() => { setIsLocked(true); }, remainingTime); // if the account was unlocked check the remember me // automatically to prolong the unlock period - !isLocked && setSavePass(true); + remainingTime > 0 && setSavePass(true); }) .catch((error: Error) => console.error(error));