diff --git a/packages/extension-base/src/background/handlers/Extension.ts b/packages/extension-base/src/background/handlers/Extension.ts index 0770377d4dd..9bcc752d756 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 savedExpiry = await chromeStorage.getPasswordExpiry(address); + const remainingTime = savedExpiry - Date.now(); if (remainingTime < 0) { - this.#cachedUnlocks[address] = 0; + await chromeStorage.removePassword(address); pair.lock(); return 0; @@ -351,7 +348,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) { @@ -412,9 +409,15 @@ export default class Extension { // 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); } else { - pair.lock(); + const savedExpiry = await chromeStorage.getPasswordExpiry(pair.address); + + const remainingTime = savedExpiry - Date.now(); + + if (remainingTime <= 0) { + pair.lock(); + } } await this.#state.removeSignRequest(id); @@ -449,7 +452,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..1a65687e674 --- /dev/null +++ b/packages/extension-base/src/background/handlers/chromeStorage.ts @@ -0,0 +1,59 @@ +import { z } from 'zod'; + +import { PASSWORD_EXPIRY_MS } from '../../defaults'; + +const addressSchema = z.string(); +const passwordSchema = z.record(z.string(), z.number()); + +async function getPasswordExpiry (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 0; + } + } else { + return 0; + } +} + +async function setPassword (address: 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]: Date.now() + PASSWORD_EXPIRY_MS } }); + } else { + await chrome.storage.session.set({ savePass: { [address]: Date.now() + PASSWORD_EXPIRY_MS } }); + } + } 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, + getPasswordExpiry, + removePassword +}; + +export default chromeStorage;