Skip to content

Commit ae27a98

Browse files
authored
Merge pull request #143 from Cardinal-Cryptography/A0-3946-remember-password-not-working
A0-3946: Fix password saving function not working properly
2 parents 59bacbb + 9944dd2 commit ae27a98

File tree

6 files changed

+91
-36
lines changed

6 files changed

+91
-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

+28-23
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,26 @@ 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 };
154156
}
155157

156158
private accountsShow ({ address, isShowing }: RequestAccountShow): boolean {
@@ -334,7 +336,7 @@ export default class Extension {
334336
};
335337
}
336338

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

340342
assert(queued, 'Unable to find request');
@@ -351,10 +353,12 @@ export default class Extension {
351353
throw error;
352354
}
353355

354-
this.refreshAccountPasswordCache(pair);
356+
const { cachedPassword, remainingTime } = await this.refreshAccountPasswordCache(pair);
357+
358+
const password = cachedPassword ?? inputPassword;
355359

356360
// if the keyring pair is locked, the password is needed
357-
if (pair.isLocked && !password) {
361+
if (!password) {
358362
const error = new Error('Password needed to unlock the account');
359363

360364
await this.#state.removeSignRequest(id);
@@ -408,13 +412,14 @@ export default class Extension {
408412
.createType('ExtrinsicPayload', payload, { version: payload.version })
409413
.sign(pair);
410414

411-
if (savePass) {
415+
if (savePass && remainingTime <= 0) {
412416
// unlike queued.account.address the following
413417
// address is encoded with the default prefix
414418
// which what is used for password caching mapping
415-
this.#cachedUnlocks[pair.address] = Date.now() + PASSWORD_EXPIRY_MS;
416-
} else {
417-
pair.lock();
419+
420+
await chromeStorage.setItem('password-cache', (savedPasswords) => (
421+
{ ...savedPasswords, [pair.address]: { password, expiresAt: Date.now() + PASSWORD_EXPIRY_MS } }
422+
));
418423
}
419424

420425
await this.#state.removeSignRequest(id);
@@ -449,11 +454,11 @@ export default class Extension {
449454

450455
assert(pair, 'Unable to find pair');
451456

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

454459
return {
455-
isLocked: pair.isLocked,
456-
remainingTime
460+
remainingTime,
461+
isLocked: remainingTime <= 0
457462
};
458463
}
459464

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

+8-11
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,9 @@ function SignArea({ buttonText, className, error, isExternal, isFirst, isLast, s
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,15 @@ 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+
disabled={!isLocked}
103+
label={t<string>('Remember password for {{expiration}} minutes', {
104+
replace: { expiration: PASSWORD_EXPIRY_MIN }
105+
})}
109106
onChange={setSavePass}
110107
/>
111108
);

packages/extension-ui/src/components/Checkbox.tsx

+17-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import Subtract from '../assets/subtract.svg';
99

1010
interface Props {
1111
checked: boolean;
12+
disabled?: boolean;
1213
indeterminate?: boolean;
1314
className?: string;
1415
label: ReactNode;
@@ -20,6 +21,7 @@ interface Props {
2021
function Checkbox({
2122
checked,
2223
className,
24+
disabled,
2325
indeterminate,
2426
label,
2527
onChange,
@@ -49,6 +51,7 @@ function Checkbox({
4951
{label}
5052
<input
5153
checked={checked && !indeterminate}
54+
disabled={disabled}
5255
onBlur={() => setIsFocusVisible(false)}
5356
onChange={_onChange}
5457
onClick={_onClick}
@@ -102,7 +105,7 @@ const Label = styled.label<{ isOutlined: boolean; variant: NonNullable<Props['va
102105
cursor: pointer;
103106
user-select: none;
104107
padding-left: ${({ variant }) => variantToStyles[variant].paddingLeft};;
105-
padding-top: 1px;
108+
padding-top: 2px;
106109
color: ${({ theme }) => theme.subTextColor};
107110
font-size: ${({ theme }) => theme.fontSize};
108111
line-height: ${({ theme }) => theme.lineHeight};
@@ -115,6 +118,10 @@ const Label = styled.label<{ isOutlined: boolean; variant: NonNullable<Props['va
115118
outline-style: auto;
116119
}
117120
121+
:has(input:disabled) {
122+
color: ${({ theme }) => theme.disabledTextColor}
123+
}
124+
118125
/* :has selector is the pure css solution to this problem, but doesn't have (so far) enough support ;( */
119126
${({ isOutlined }) => (isOutlined ? 'outline-style: auto;' : '')}
120127
`;
@@ -188,6 +195,15 @@ export default styled(Checkbox)(
188195
mask-size: cover;
189196
}
190197
}
198+
199+
input:disabled ~ .checkbox-ui {
200+
background: ${theme.disabledTextColor};
201+
box-shadow: 0 0 0 1px ${theme.disabledTextColor};
202+
203+
&:hover {
204+
box-shadow: 0 0 0 1px ${theme.disabledTextColor};
205+
}
206+
}
191207
}
192208
193209
&:hover {

0 commit comments

Comments
 (0)