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:
parent
848ab62431
commit
a6a42fefed
@ -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
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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> {
|
||||
|
||||
@ -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;
|
||||
`);
|
||||
|
||||
@ -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> {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user