Skip to content

Commit 8f4a3bf

Browse files
RobertsrAskVAL
Roberts
authored andcommitted
A0-3946: Fix password saving function not working properly
1 parent 59bacbb commit 8f4a3bf

File tree

5 files changed

+89
-36
lines changed

5 files changed

+89
-36
lines changed

packages/extension-base/src/background/handlers/Extension.spec.ts

+5
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ const authUrls = {
3333

3434
chromeStub.windows.getAll.resolves([]);
3535

36+
// @ts-expect-error the "sinon-chrome" mocking library does not provide stubs for session storage, so we have to append them ourselves
37+
chromeStub.storage.session = {
38+
get: () => Promise.resolve({})
39+
};
40+
3641
const stubChromeStorage = (data: Record<string, unknown> = {}) => chromeStub.storage.local.get.resolves({
3742
authUrls,
3843
...data

packages/extension-base/src/background/handlers/Extension.ts

+42-22
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,12 @@ import { accounts as accountsObservable } from '@polkadot/ui-keyring/observable/
1818
import { assert, isHex, u8aToHex } from '@polkadot/util';
1919
import { keyExtractSuri, mnemonicGenerate, mnemonicValidate } from '@polkadot/util-crypto';
2020

21+
import chromeStorage from './chromeStorage';
2122
import { POPUP_CREATE_WINDOW_DATA } from './consts';
2223
import { openCenteredWindow } from './helpers';
2324
import State from './State';
2425
import { createSubscription, unsubscribe } from './subscriptions';
2526

26-
type CachedUnlocks = Record<string, number>;
27-
2827
type GetContentPort = (tabId: number) => chrome.runtime.Port
2928

3029
const SEED_DEFAULT_LENGTH = 12;
@@ -40,12 +39,9 @@ function isJsonPayload (value: SignerPayloadJSON | SignerPayloadRaw): value is S
4039
}
4140

4241
export default class Extension {
43-
readonly #cachedUnlocks: CachedUnlocks;
44-
4542
readonly #state: State;
4643

4744
constructor (state: State) {
48-
this.#cachedUnlocks = {};
4945
this.#state = state;
5046
}
5147

@@ -137,20 +133,39 @@ export default class Extension {
137133
return true;
138134
}
139135

140-
private refreshAccountPasswordCache (pair: KeyringPair): number {
136+
private async refreshAccountPasswordCache (pair: KeyringPair): Promise<{remainingTime: number, cachedPassword?: string}> {
141137
const { address } = pair;
142138

143-
const savedExpiry = this.#cachedUnlocks[address] || 0;
144-
const remainingTime = savedExpiry - Date.now();
139+
const savedPasswords = await chromeStorage.getItem('password-cache') ?? {};
140+
const expiresAt = savedPasswords[address]?.expiresAt ?? 0;
141+
const remainingTime = expiresAt - Date.now();
142+
const cachedPassword = savedPasswords[address]?.password;
145143

146-
if (remainingTime < 0) {
147-
this.#cachedUnlocks[address] = 0;
148-
pair.lock();
144+
if (remainingTime > 0) {
145+
return { remainingTime, cachedPassword };
146+
}
149147

150-
return 0;
148+
if (address in savedPasswords) {
149+
delete savedPasswords[address];
150+
await chromeStorage.setItem('password-cache', () => (savedPasswords));
151151
}
152152

153-
return remainingTime;
153+
pair.lock();
154+
155+
return { remainingTime: 0 };
156+
}
157+
158+
private async clearPasswordCache (pair: KeyringPair): Promise<void> {
159+
const { address } = pair;
160+
161+
const savedPasswords = await chromeStorage.getItem('password-cache') ?? {};
162+
163+
if (address in savedPasswords) {
164+
delete savedPasswords[address];
165+
await chromeStorage.setItem('password-cache', () => (savedPasswords));
166+
}
167+
168+
pair.lock();
154169
}
155170

156171
private accountsShow ({ address, isShowing }: RequestAccountShow): boolean {
@@ -334,7 +349,7 @@ export default class Extension {
334349
};
335350
}
336351

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

340355
assert(queued, 'Unable to find request');
@@ -351,10 +366,12 @@ export default class Extension {
351366
throw error;
352367
}
353368

354-
this.refreshAccountPasswordCache(pair);
369+
const { cachedPassword } = await this.refreshAccountPasswordCache(pair);
370+
371+
const password = cachedPassword ?? inputPassword;
355372

356373
// if the keyring pair is locked, the password is needed
357-
if (pair.isLocked && !password) {
374+
if (!password) {
358375
const error = new Error('Password needed to unlock the account');
359376

360377
await this.#state.removeSignRequest(id);
@@ -408,13 +425,16 @@ export default class Extension {
408425
.createType('ExtrinsicPayload', payload, { version: payload.version })
409426
.sign(pair);
410427

411-
if (savePass) {
428+
if (savePass && password) {
412429
// unlike queued.account.address the following
413430
// address is encoded with the default prefix
414431
// which what is used for password caching mapping
415-
this.#cachedUnlocks[pair.address] = Date.now() + PASSWORD_EXPIRY_MS;
432+
433+
await chromeStorage.setItem('password-cache', (savedPasswords) => (
434+
{ ...savedPasswords, [pair.address]: { password, expiresAt: Date.now() + PASSWORD_EXPIRY_MS } }
435+
));
416436
} else {
417-
pair.lock();
437+
await this.clearPasswordCache(pair);
418438
}
419439

420440
await this.#state.removeSignRequest(id);
@@ -449,11 +469,11 @@ export default class Extension {
449469

450470
assert(pair, 'Unable to find pair');
451471

452-
const remainingTime = this.refreshAccountPasswordCache(pair);
472+
const { remainingTime } = await this.refreshAccountPasswordCache(pair);
453473

454474
return {
455-
isLocked: pair.isLocked,
456-
remainingTime
475+
remainingTime,
476+
isLocked: pair.isLocked
457477
};
458478
}
459479

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { z } from 'zod';
2+
3+
// use namespaces to add typization for your storage data
4+
const namespaces = { 'password-cache': z.record(z.string(), z.object({ password: z.string(), expiresAt: z.number() })) } as const satisfies Record<string, z.ZodSchema>;
5+
6+
export const setItem = async <N extends keyof typeof namespaces>(namespace: N, updater: (currData?: z.TypeOf<typeof namespaces[N]>) => z.TypeOf<typeof namespaces[N]>) => {
7+
const currentData = await getItem(namespace);
8+
9+
await chrome.storage.session.set({ [namespace]: updater(currentData) });
10+
};
11+
12+
export const getItem = async <N extends keyof typeof namespaces>(namespace: N): Promise<z.infer<typeof namespaces[N]> | undefined> => {
13+
const { [namespace]: cachedData } = await chrome.storage.session.get(namespace);
14+
15+
try {
16+
return namespaces[namespace].parse(cachedData);
17+
} catch {
18+
return undefined;
19+
}
20+
};
21+
22+
export const removeItem = async <N extends keyof typeof namespaces>(namespace: N) => {
23+
await chrome.storage.session.remove(namespace);
24+
};
25+
26+
const chromeStorage = {
27+
getItem,
28+
setItem,
29+
removeItem
30+
};
31+
32+
export default chromeStorage;

packages/extension-base/src/background/types.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -306,8 +306,8 @@ export interface RequestSigningIsLocked {
306306
}
307307

308308
export interface ResponseSigningIsLocked {
309-
isLocked: boolean;
310309
remainingTime: number;
310+
isLocked: boolean;
311311
}
312312

313313
export type RequestSigningSubscribe = null;

packages/extension-ui/src/Popup/Signing/Request/SignArea.tsx

+9-13
Original file line numberDiff line numberDiff line change
@@ -37,15 +37,17 @@ function SignArea({ buttonText, className, error, isExternal, isFirst, isLast, s
3737

3838
!isExternal &&
3939
isSignLocked(signId)
40-
.then(({ isLocked, remainingTime }) => {
41-
setIsLocked(isLocked);
40+
.then(({ remainingTime }) => {
41+
setIsLocked(remainingTime <= 0);
4242
timeout = setTimeout(() => {
4343
setIsLocked(true);
4444
}, remainingTime);
4545

4646
// if the account was unlocked check the remember me
4747
// automatically to prolong the unlock period
48-
!isLocked && setSavePass(true);
48+
if(remainingTime > 0) {
49+
setSavePass(true);
50+
}
4951
})
5052
.catch((error: Error) => console.error(error));
5153

@@ -92,20 +94,14 @@ function SignArea({ buttonText, className, error, isExternal, isFirst, isLast, s
9294

9395
const StyledCheckbox = styled(Checkbox)`
9496
margin-left: 8px;
95-
`;
97+
`;
9698

9799
const RememberPasswordCheckbox = () => (
98100
<StyledCheckbox
99101
checked={savePass}
100-
label={
101-
isLocked
102-
? t<string>('Remember password for {{expiration}} minutes', {
103-
replace: { expiration: PASSWORD_EXPIRY_MIN }
104-
})
105-
: t<string>('Extend the period without password by {{expiration}} minutes', {
106-
replace: { expiration: PASSWORD_EXPIRY_MIN }
107-
})
108-
}
102+
label={t<string>('Remember password for {{expiration}} minutes', {
103+
replace: { expiration: PASSWORD_EXPIRY_MIN }
104+
})}
109105
onChange={setSavePass}
110106
/>
111107
);

0 commit comments

Comments
 (0)