Skip to content

Commit

Permalink
A0-3946: Fix password saving function not working properly
Browse files Browse the repository at this point in the history
  • Loading branch information
Roberts authored and rAskVAL committed Feb 8, 2024
1 parent 59bacbb commit 1e62d49
Show file tree
Hide file tree
Showing 3 changed files with 71 additions and 23 deletions.
52 changes: 33 additions & 19 deletions packages/extension-base/src/background/handlers/Extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, number>;

type GetContentPort = (tabId: number) => chrome.runtime.Port

const SEED_DEFAULT_LENGTH = 12;
Expand All @@ -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;
}

Expand Down Expand Up @@ -137,20 +133,27 @@ export default class Extension {
return true;
}

private refreshAccountPasswordCache (pair: KeyringPair): number {
private async refreshAccountPasswordCache (pair: KeyringPair): Promise<number> {
const { address } = pair;

const savedExpiry = this.#cachedUnlocks[address] || 0;
const remainingTime = savedExpiry - Date.now();
const savedPasswords = await chromeStorage.getItem('password-cache');

if (savedPasswords) {
const remainingTime = savedPasswords[address]?.expiresAt - Date.now();

if (remainingTime < 0) {
delete savedPasswords[address];

if (remainingTime < 0) {
this.#cachedUnlocks[address] = 0;
pair.lock();
await chromeStorage.setItem('password-cache', () => (savedPasswords));
pair.lock();

return 0;
}

return 0;
return remainingTime;
}

return remainingTime;
return 0;
}

private accountsShow ({ address, isShowing }: RequestAccountShow): boolean {
Expand Down Expand Up @@ -334,14 +337,18 @@ export default class Extension {
};
}

private async signingApprovePassword ({ id, password, savePass }: RequestSigningApprovePassword, getContentPort: GetContentPort): Promise<void> {
private async signingApprovePassword ({ id, password: inputPassword, savePass }: RequestSigningApprovePassword, getContentPort: GetContentPort): Promise<void> {
const queued = await this.#state.getSignRequest(id);

assert(queued, 'Unable to find request');

const { payload } = queued;
const pair = keyring.getPair(queued.account.address);

const savedPasswords = await chromeStorage.getItem('password-cache');

const password = savedPasswords && savedPasswords[pair.address]?.expiresAt - Date.now() > 0 ? savedPasswords[pair.address].password : inputPassword;

if (!pair) {
const error = new Error('Unable to find pair');

Expand All @@ -351,7 +358,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) {
Expand Down Expand Up @@ -408,13 +415,20 @@ 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.setItem('password-cache', (savedPasswords) => ({ ...savedPasswords, [pair.address]: { password, expiresAt: Date.now() + PASSWORD_EXPIRY_MS } }));
} else {
pair.lock();
const savedPasswords = await chromeStorage.getItem('password-cache');

const remainingTime = (savedPasswords && savedPasswords[pair.address]?.expiresAt - Date.now()) || 0;

if (remainingTime <= 0) {
pair.lock();
}
}

await this.#state.removeSignRequest(id);
Expand Down Expand Up @@ -449,7 +463,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,
Expand Down
32 changes: 32 additions & 0 deletions packages/extension-base/src/background/handlers/chromeStorage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { z } from 'zod';

// use namespaces to add typization for your storage data
const namespaces = { 'password-cache': z.record(z.string(), z.object({ password: z.string(), expiresAt: z.number() })) } as const satisfies Record<string, z.ZodSchema>;

export const setItem = async <N extends keyof typeof namespaces>(namespace: N, updater: (currData?: z.TypeOf<typeof namespaces[N]>) => z.TypeOf<typeof namespaces[N]>) => {
const currentData = await getItem(namespace);

await chrome.storage.session.set({ [namespace]: updater(currentData) });
};

export const getItem = async <N extends keyof typeof namespaces>(namespace: N): Promise<z.infer<typeof namespaces[N]> | undefined> => {
const { [namespace]: cachedData } = await chrome.storage.session.get(namespace);

try {
return namespaces[namespace].parse(cachedData);
} catch {
return undefined;
}
};

export const removeItem = async <N extends keyof typeof namespaces>(namespace: N) => {
await chrome.storage.session.remove(namespace);
};

const chromeStorage = {
getItem,
setItem,
removeItem
};

export default chromeStorage;
10 changes: 6 additions & 4 deletions packages/extension-ui/src/Popup/Signing/Request/SignArea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,17 @@ 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);
if(remainingTime > 0) {
setSavePass(true);
}
})
.catch((error: Error) => console.error(error));

Expand Down Expand Up @@ -92,7 +94,7 @@ function SignArea({ buttonText, className, error, isExternal, isFirst, isLast, s

const StyledCheckbox = styled(Checkbox)`
margin-left: 8px;
`;
`;

const RememberPasswordCheckbox = () => (
<StyledCheckbox
Expand Down

0 comments on commit 1e62d49

Please sign in to comment.