Don't ask for the password for 15 minutes (#421)

* Don't ask for the password for 15 minutes

* lock-check logic

* only save expiry

* Adjust margin for unlock

* lock/password check

* CHANGELOG
This commit is contained in:
Jaco Greeff 2020-09-03 15:32:51 +02:00 committed by GitHub
parent 848ab62431
commit a6a42fefed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 142 additions and 56 deletions

View File

@ -10,6 +10,8 @@ Contributed:
Changes:
- Add option to not ask password for 15 minutes (when signing transactions)
- Derived accounts uses the parent genesisHash by default (attaches to same chain)
- Make import from seed, QR & JSON options available on first-start
- Latest upstream polkadot-js dependencies

View File

@ -4,7 +4,7 @@
import { MetadataDef } from '@polkadot/extension-inject/types';
import { SubjectInfo } from '@polkadot/ui-keyring/observable/types';
import { AccountJson, AuthorizeRequest, MessageTypes, MetadataRequest, RequestAccountCreateExternal, RequestAccountCreateSuri, RequestAccountEdit, RequestAccountExport, RequestAccountShow, RequestAccountTie, RequestAccountValidate, RequestAuthorizeApprove, RequestAuthorizeReject, RequestDeriveCreate, ResponseDeriveValidate, RequestMetadataApprove, RequestMetadataReject, RequestSigningApprovePassword, RequestSigningApproveSignature, RequestSigningCancel, RequestSeedCreate, RequestTypes, ResponseAccountExport, RequestAccountForget, ResponseSeedCreate, RequestSeedValidate, RequestDeriveValidate, ResponseSeedValidate, ResponseType, SigningRequest, RequestJsonRestore, ResponseJsonRestore } from '../types';
import { AccountJson, AuthorizeRequest, MessageTypes, MetadataRequest, RequestAccountCreateExternal, RequestAccountCreateSuri, RequestAccountEdit, RequestAccountExport, RequestAccountShow, RequestAccountTie, RequestAccountValidate, RequestAuthorizeApprove, RequestAuthorizeReject, RequestDeriveCreate, ResponseDeriveValidate, RequestMetadataApprove, RequestMetadataReject, RequestSigningApprovePassword, RequestSigningApproveSignature, RequestSigningCancel, RequestSigningIsLocked, RequestSeedCreate, RequestTypes, ResponseAccountExport, RequestAccountForget, ResponseSeedCreate, RequestSeedValidate, RequestDeriveValidate, ResponseSeedValidate, ResponseType, SigningRequest, RequestJsonRestore, ResponseJsonRestore } from '../types';
import chrome from '@polkadot/extension-inject/chrome';
import keyring from '@polkadot/ui-keyring';
@ -17,8 +17,11 @@ import { keyExtractSuri, mnemonicGenerate, mnemonicValidate } from '@polkadot/ut
import State from './State';
import { createSubscription, unsubscribe } from './subscriptions';
type CachedUnlocks = Record<string, number>;
const SEED_DEFAULT_LENGTH = 12;
const SEED_LENGTHS = [12, 15, 18, 21, 24];
const PASSWORD_EXPIRY = 15 * 60 * 1000;
// a global registry to use internally
const registry = new TypeRegistry();
@ -31,9 +34,12 @@ function transformAccounts (accounts: SubjectInfo): AccountJson[] {
}
export default class Extension {
readonly #cachedUnlocks: CachedUnlocks;
readonly #state: State;
constructor (state: State) {
this.#cachedUnlocks = {};
this.#state = state;
}
@ -262,7 +268,7 @@ export default class Extension {
};
}
private signingApprovePassword ({ id, password }: RequestSigningApprovePassword): boolean {
private signingApprovePassword ({ id, isSavedPass, password }: RequestSigningApprovePassword): boolean {
const queued = this.#state.getSignRequest(id);
assert(queued, 'Unable to find request');
@ -276,11 +282,21 @@ export default class Extension {
return false;
}
const now = Date.now();
if (pair.isLocked || password) {
pair.decodePkcs8(password);
}
const result = request.sign(registry, pair);
const savedExpiry = this.#cachedUnlocks[request.payload.address] || 0;
if (isSavedPass) {
this.#cachedUnlocks[request.payload.address] = now + PASSWORD_EXPIRY;
} else if (savedExpiry < now) {
this.#cachedUnlocks[request.payload.address] = 0;
pair.lock();
}
resolve({
id,
@ -314,6 +330,19 @@ export default class Extension {
return true;
}
private signingIsLocked ({ id }: RequestSigningIsLocked): boolean {
const queued = this.#state.getSignRequest(id);
assert(queued, 'Unable to find request');
const { request } = queued;
const pair = keyring.getPair(request.payload.address);
assert(pair, 'Unable to find pair');
return pair.isLocked || ((this.#cachedUnlocks[request.payload.address] || 0) < Date.now());
}
// FIXME This looks very much like what we have in authorization
private signingSubscribe (id: string, port: chrome.runtime.Port): boolean {
const cb = createSubscription<'pri(signing.requests)'>(id, port);
@ -462,6 +491,9 @@ export default class Extension {
case 'pri(signing.cancel)':
return this.signingCancel(request as RequestSigningCancel);
case 'pri(signing.isLocked)':
return this.signingIsLocked(request as RequestSigningIsLocked);
case 'pri(signing.requests)':
return this.signingSubscribe(id, port);

View File

@ -95,6 +95,7 @@ export interface RequestSignatures {
'pri(signing.approve.password)': [RequestSigningApprovePassword, boolean];
'pri(signing.approve.signature)': [RequestSigningApproveSignature, boolean];
'pri(signing.cancel)': [RequestSigningCancel, boolean];
'pri(signing.isLocked)': [RequestSigningIsLocked, boolean];
'pri(signing.requests)': [RequestSigningSubscribe, boolean, SigningRequest[]];
'pri(window.open)': [null, boolean];
'pri(window.open.json)': [null, boolean];
@ -235,6 +236,7 @@ export interface RequestRpcUnsubscribe {
export interface RequestSigningApprovePassword {
id: string;
isSavedPass: boolean;
password: string;
}
@ -247,6 +249,10 @@ export interface RequestSigningCancel {
id: string;
}
export interface RequestSigningIsLocked {
id: string;
}
export type RequestSigningSubscribe = null;
export interface RequestSeedCreate {

View File

@ -6,11 +6,11 @@ import { ExtrinsicPayload } from '@polkadot/types/interfaces';
import { AccountJson, RequestSign } from '@polkadot/extension-base/background/types';
import { SignerPayloadJSON, SignerPayloadRaw } from '@polkadot/types/types';
import React, { useContext, useState, useEffect } from 'react';
import React, { useCallback, useContext, useState, useEffect } from 'react';
import { TypeRegistry } from '@polkadot/types';
import { ActionBar, ActionContext, Address, ButtonArea, Link, VerticalSpace } from '../../components';
import { approveSignPassword, approveSignSignature, cancelSignRequest } from '../../messaging';
import { ActionBar, ActionContext, Address, Button, ButtonArea, Checkbox, Link, VerticalSpace } from '../../components';
import { approveSignPassword, approveSignSignature, cancelSignRequest, isSignLocked } from '../../messaging';
import Bytes from './Bytes';
import Extrinsic from './Extrinsic';
import Qr from './Qr';
@ -42,6 +42,16 @@ export default function Request ({ account: { isExternal }, buttonText, isFirst,
const onAction = useContext(ActionContext);
const [{ hexBytes, payload }, setData] = useState<Data>({ hexBytes: null, payload: null });
const [error, setError] = useState<string | null>(null);
const [isBusy, setIsBusy] = useState(false);
const [isLocked, setIsLocked] = useState<boolean | null>(null);
const [isSavedPass, setIsSavedPass] = useState(false);
useEffect((): void => {
setIsLocked(null);
!isExternal && isSignLocked(signId)
.then((isLocked) => setIsLocked(isLocked))
.catch((error: Error) => console.error(error));
}, [isExternal, signId]);
useEffect((): void => {
const payload = request.payload;
@ -61,24 +71,70 @@ export default function Request ({ account: { isExternal }, buttonText, isFirst,
}
}, [request]);
const _onCancel = (): Promise<void> =>
cancelSignRequest(signId)
.then(() => onAction())
.catch((error: Error) => console.error(error));
const _onSign = (password: string): Promise<void> =>
approveSignPassword(signId, password)
const _onCancel = useCallback(
(): Promise<void> => cancelSignRequest(signId)
.then(() => onAction())
.catch((error: Error) => console.error(error)),
[onAction, signId]
);
const _onSign = useCallback(
(password: string): Promise<void> => {
setIsBusy(true);
return approveSignPassword(signId, password, !!(password && isSavedPass))
.then((): void => {
setIsBusy(false);
onAction();
})
.catch((error: Error): void => {
setIsBusy(false);
setError(error.message);
console.error(error);
});
const _onSignature = ({ signature }: { signature: string }): Promise<void> =>
},
[onAction, isSavedPass, signId]
);
const _onSignQuick = useCallback(
() => _onSign(''),
[_onSign]
);
const _onSignature = useCallback(
({ signature }: { signature: string }): Promise<void> =>
approveSignSignature(signId, signature)
.then(() => onAction())
.catch((error: Error): void => {
setError(error.message);
console.error(error);
});
}),
[onAction, signId]
);
const signButton = isLocked
? (
<Unlock
buttonText={buttonText}
error={error}
isBusy={isBusy}
onSign={_onSign}
>
<Checkbox
checked={!!isSavedPass}
label="Don't ask me again for the next 15 minutes"
onChange={setIsSavedPass}
/>
</Unlock>
)
: (
<Button
isBusy={isBusy}
onClick={_onSignQuick}
>
Sign the transaction
</Button>
);
if (payload !== null) {
const json = request.payload as SignerPayloadJSON;
@ -108,13 +164,7 @@ export default function Request ({ account: { isExternal }, buttonText, isFirst,
)
}
<SignArea>
{isFirst && !isExternal && (
<Unlock
buttonText={buttonText}
error={error}
onSign={_onSign}
/>
)}
{(isLocked !== null) && isFirst && !isExternal && signButton}
<CancelButton>
<Link
isDanger
@ -140,12 +190,7 @@ export default function Request ({ account: { isExternal }, buttonText, isFirst,
/>
<VerticalSpace />
<SignArea>
{!isExternal && (
<Unlock
buttonText={buttonText}
onSign={_onSign}
/>
)}
{!isExternal && signButton}
<CancelButton>
<Link
isDanger

View File

@ -7,14 +7,15 @@ import React, { useCallback, useEffect, useState } from 'react';
import { Button, InputWithLabel } from '../../components';
interface Props {
children?: React.ReactNode;
className?: string;
error?: string | null;
isBusy: boolean;
onSign: (password: string) => Promise<void>;
buttonText?: string;
}
function Unlock ({ buttonText = 'Sign the transaction', className, error, onSign }: Props): React.ReactElement<Props> {
const [isBusy, setIsBusy] = useState(false);
function Unlock ({ buttonText = 'Sign the transaction', children, className, error, isBusy, onSign }: Props): React.ReactElement<Props> {
const [ownError, setError] = useState<string | null>();
const [password, setPassword] = useState('');
@ -29,16 +30,9 @@ function Unlock ({ buttonText = 'Sign the transaction', className, error, onSign
},
[]
);
const _onClick = useCallback(
(): void => {
setIsBusy(true);
onSign(password)
.then(() => setIsBusy(false))
.catch((error: Error): void => {
setIsBusy(false);
setError(error.message);
});
},
() => onSign(password),
[onSign, password]
);
@ -52,7 +46,9 @@ function Unlock ({ buttonText = 'Sign the transaction', className, error, onSign
onChange={_onChangePassword}
onEnter={_onClick}
type='password'
withoutMargin={!!children}
/>
{children}
<Button
isBusy={isBusy}
onClick={_onClick}

View File

@ -11,10 +11,10 @@ import Checkmark from '../assets/checkmark.svg';
interface Props {
checked: boolean;
className?: string;
label: string;
onChange?: (checked: boolean) => void;
onClick?: () => void;
label: string;
className?: string;
}
function Checkbox ({ checked, className, label, onChange, onClick }: Props): React.ReactElement<Props> {

View File

@ -19,9 +19,10 @@ interface Props {
onBlur?: () => void;
onChange?: (value: string) => void;
onEnter?: () => void;
placeholder?: string;
type?: 'text' | 'password';
value?: string;
placeholder?: string;
withoutMargin?: boolean;
}
function InputWithLabel ({ className, defaultValue, disabled, isError, isFocused, isReadOnly, label, onBlur, onChange, onEnter, placeholder, type = 'text', value }: Props): React.ReactElement<Props> {
@ -64,6 +65,6 @@ function InputWithLabel ({ className, defaultValue, disabled, isError, isFocused
);
}
export default styled(InputWithLabel)`
margin-bottom: 16px;
`;
export default styled(InputWithLabel)(({ withoutMargin }: Props) => `
margin-bottom: ${withoutMargin ? 0 : 16}px;
`);

View File

@ -102,8 +102,12 @@ export async function cancelSignRequest (id: string): Promise<boolean> {
return sendMessage('pri(signing.cancel)', { id });
}
export async function approveSignPassword (id: string, password: string): Promise<boolean> {
return sendMessage('pri(signing.approve.password)', { id, password });
export async function isSignLocked (id: string): Promise<boolean> {
return sendMessage('pri(signing.isLocked)', { id });
}
export async function approveSignPassword (id: string, password: string, isSavedPass: boolean): Promise<boolean> {
return sendMessage('pri(signing.approve.password)', { id, isSavedPass, password });
}
export async function approveSignSignature (id: string, signature: string): Promise<boolean> {