Require phrase confirmation. (#5731)

* Require phrase confirmation.

* Linting issues.

* Fix dialog title.

* Confirm a backup phrase.

* Confirm recovery phrase on fist run as well.
This commit is contained in:
Tomasz Drwięga 2017-06-09 12:25:14 +02:00 committed by Gav Wood
parent 06eb561af5
commit 5c3ea4ec29
7 changed files with 300 additions and 46 deletions

View File

@ -25,9 +25,16 @@ import styles from '../createAccount.css';
@observer
export default class AccountDetails extends Component {
static propTypes = {
isConfirming: PropTypes.bool,
withRequiredBackup: PropTypes.bool,
createStore: PropTypes.object.isRequired
}
static defaultPropTypes = {
isConfirming: false,
withRequiredBackup: false
}
render () {
const { address, description, name } = this.props.createStore;
@ -78,31 +85,103 @@ export default class AccountDetails extends Component {
);
}
renderPhrase () {
const { phrase } = this.props.createStore;
renderRequiredBackup () {
const { phraseBackedUp, phraseBackedUpError } = this.props.createStore;
if (!phrase) {
if (!this.props.withRequiredBackup) {
return null;
}
return (
<Input
allowCopy
hint={
<FormattedMessage
id='createAccount.accountDetails.phrase.hint'
defaultMessage='the account recovery phrase'
/>
}
label={
<FormattedMessage
id='createAccount.accountDetails.phrase.label'
defaultMessage='owner recovery phrase (keep private and secure, it allows full and unlimited access to the account)'
/>
}
readOnly
value={ phrase }
/>
<div>
<Input
error={ phraseBackedUpError }
hint={
<FormattedMessage
id='createAccount.accountDetails.phrase.hint'
defaultMessage='the account recovery phrase'
/>
}
label={
<FormattedMessage
id='createAccount.accountDetails.phrase.backedUp'
defaultMessage='Type "I have written down the phrase" below to confirm it is backed up.'
/>
}
onChange={ this.onEditPhraseBackedUp }
value={ phraseBackedUp }
/>
</div>
);
}
renderPhrase () {
const { isConfirming } = this.props;
const { isTest, phrase, backupPhraseError } = this.props.createStore;
const hint = (
<FormattedMessage
id='createAccount.accountDetails.phrase.hint'
defaultMessage='the account recovery phrase'
/>
);
const label = (
<FormattedMessage
id='createAccount.accountDetails.phrase.label'
defaultMessage='owner recovery phrase'
/>
);
if (!isConfirming) {
if (!phrase) {
return null;
}
return (
<div>
<Input
allowCopy
hint={ hint }
label={ label }
readOnly
value={ phrase }
/>
<div className={ styles.backupPhrase }>
<FormattedMessage
id='createAccount.accountDetails.phrase.backup'
defaultMessage='Please back up the recovery phrase now. Make sure to keep it private and secure, it allows full and unlimited access to the account.'
/>
</div>
{ this.renderRequiredBackup() }
</div>
);
}
return (
<div>
<Input
allowPaste={ isTest }
error={ backupPhraseError }
hint={ hint }
label={ label }
onChange={ this.onEditPhrase }
value={ phrase }
/>
<div className={ styles.backupPhrase }>
<FormattedMessage
id='createAccount.accountDetails.phrase.backupConfirm'
defaultMessage='Type your recovery phrase now.'
/>
</div>
</div>
);
}
onEditPhraseBackedUp = (ev) => {
this.props.createStore.setPhraseBackedUp(ev.target.value);
}
onEditPhrase = (ev) => {
this.props.createStore.setPhrase(ev.target.value);
}
}

View File

@ -131,3 +131,8 @@
padding: 0 4em 1.5em 4em;
text-align: center;
}
.backupPhrase {
line-height: 1.618em;
margin-top: 1.5em;
}

View File

@ -37,7 +37,7 @@ import NewImport from './NewImport';
import NewQr from './NewQr';
import RawKey from './RawKey';
import RecoveryPhrase from './RecoveryPhrase';
import Store, { STAGE_CREATE, STAGE_INFO, STAGE_SELECT_TYPE } from './store';
import Store, { STAGE_CREATE, STAGE_INFO, STAGE_SELECT_TYPE, STAGE_CONFIRM_BACKUP } from './store';
import TypeIcon from './TypeIcon';
import print from './print';
import recoveryPage from './recoveryPage.ejs';
@ -61,6 +61,12 @@ const TITLES = {
defaultMessage='account information'
/>
),
backup: (
<FormattedMessage
id='createAccount.title.backupPhrase'
defaultMessage='confirm recovery phrase'
/>
),
import: (
<FormattedMessage
id='createAccount.title.importAccount'
@ -80,7 +86,7 @@ const TITLES = {
/>
)
};
const STAGE_NAMES = [TITLES.type, TITLES.create, TITLES.info];
const STAGE_NAMES = [TITLES.type, TITLES.create, TITLES.info, TITLES.backup];
const STAGE_IMPORT = [TITLES.type, TITLES.import, TITLES.info];
const STAGE_RESTORE = [TITLES.restore, TITLES.info];
const STAGE_QR = [TITLES.type, TITLES.qr, TITLES.info];
@ -213,14 +219,25 @@ class CreateAccount extends Component {
}
return (
<AccountDetails createStore={ this.createStore } />
<AccountDetails
createStore={ this.createStore }
withRequiredBackup={ createType === 'fromNew' }
/>
);
case STAGE_CONFIRM_BACKUP:
return (
<AccountDetails
createStore={ this.createStore }
isConfirming
/>
);
}
}
renderDialogActions () {
const { restore } = this.props;
const { createType, canCreate, isBusy, stage } = this.createStore;
const { createType, canCreate, isBusy, stage, phraseBackedUpError } = this.createStore;
const cancelBtn = (
<Button
@ -281,8 +298,8 @@ class CreateAccount extends Component {
createType === 'fromNew'
? (
<FormattedMessage
id='createAccount.button.create'
defaultMessage='Create'
id='createAccount.button.next'
defaultMessage='Next'
/>
)
: (
@ -292,7 +309,7 @@ class CreateAccount extends Component {
/>
)
}
onClick={ this.onCreate }
onClick={ createType === 'fromNew' ? this.createStore.nextStage : this.onCreate }
/>
];
@ -314,6 +331,7 @@ class CreateAccount extends Component {
)
: null,
<Button
disabled={ createType === 'fromNew' && !!phraseBackedUpError }
icon={ <DoneIcon /> }
key='done'
label={
@ -322,12 +340,55 @@ class CreateAccount extends Component {
defaultMessage='Done'
/>
}
onClick={ this.onClose }
onClick={ createType === 'fromNew' ? this.onConfirmPhraseBackup : this.onClose }
/>
];
case STAGE_CONFIRM_BACKUP:
return [
<Button
icon={ <DoneIcon /> }
key='done'
label={
<FormattedMessage
id='createAccount.button.create'
defaultMessage='Create'
/>
}
onClick={ this.onCreateNew }
/>
];
}
}
onConfirmPhraseBackup = () => {
this.createStore.clearPhrase();
this.createStore.nextStage();
}
onCreateNew = () => {
this.createStore.setBusy(true);
this.createStore.computeBackupPhraseAddress()
.then(err => {
if (err) {
this.createStore.setBusy(false);
return;
}
return this.createStore.createAccount(this.vaultStore)
.then(() => {
this.createStore.clearPhrase();
this.createStore.setBusy(false);
this.props.onUpdate && this.props.onUpdate();
this.onClose();
});
})
.catch((error) => {
this.createStore.setBusy(false);
this.props.newError(error);
});
}
onCreate = () => {
return this.createStore
.createAccount(this.vaultStore)
@ -341,6 +402,7 @@ class CreateAccount extends Component {
}
onClose = () => {
this.createStore.clearPhrase();
this.props.onClose && this.props.onClose();
}

View File

@ -46,6 +46,20 @@ export default {
/>
),
noMatchBackupPhrase: (
<FormattedMessage
id='errors.noMatchBackupPhrase'
defaultMessage='the supplied recovery phrase does not match'
/>
),
noMatchPhraseBackedUp: (
<FormattedMessage
id='errors.noMatchPhraseBackedUp'
defaultMessage='type "I have written down the phrase"'
/>
),
noName: (
<FormattedMessage
id='errors.noName'
@ -59,4 +73,5 @@ export default {
defaultMessage='the raw key needs to be hex, 64 characters in length and contain the prefix "0x"'
/>
)
};

View File

@ -24,6 +24,7 @@ const FAKEPATH = 'C:\\fakepath\\';
const STAGE_SELECT_TYPE = 0;
const STAGE_CREATE = 1;
const STAGE_INFO = 2;
const STAGE_CONFIRM_BACKUP = 3;
export default class Store {
@observable accounts = null;
@ -41,6 +42,8 @@ export default class Store {
@observable passwordHint = '';
@observable passwordRepeat = '';
@observable phrase = '';
@observable backupPhraseAddress = null;
@observable phraseBackedUp = '';
@observable qrAddress = null;
@observable rawKey = '';
@observable rawKeyError = ERRORS.nokey;
@ -104,17 +107,39 @@ export default class Store {
: ERRORS.noMatchPassword;
}
@computed get backupPhraseError () {
return !this.backupPhraseAddress || this.address === this.backupPhraseAddress
? null
: ERRORS.noMatchBackupPhrase;
}
@computed get phraseBackedUpError () {
return this.phraseBackedUp === 'I have written down the phrase'
? null
: ERRORS.noMatchPhraseBackedUp;
}
@computed get qrAddressValid () {
console.log('qrValid', this.qrAddress, this._api.util.isAddressValid(this.qrAddress));
return this._api.util.isAddressValid(this.qrAddress);
}
@action clearPhrase = () => {
transaction(() => {
this.phrase = '';
this.phraseBackedUp = '';
});
}
@action clearErrors = () => {
transaction(() => {
this.address = '';
this.description = '';
this.password = '';
this.passwordRepeat = '';
this.phrase = '';
this.backupPhraseAddress = null;
this.phraseBackedUp = '';
this.name = '';
this.nameError = ERRORS.noName;
this.qrAddress = null;
@ -192,6 +217,26 @@ export default class Store {
});
}
@action setBackupPhraseAddress = (address) => {
this.backupPhraseAddress = address;
}
@action computeBackupPhraseAddress = () => {
return this._api.parity.phraseToAddress(this.phrase)
.then(address => {
this.setBackupPhraseAddress(address);
return address !== this.address;
})
.catch((error) => {
console.error('createAccount', error);
throw error;
});
}
@action setPhraseBackedUp = (backedUp) => {
this.phraseBackedUp = backedUp;
}
@action setPassword = (password) => {
this.password = password;
}
@ -217,6 +262,7 @@ export default class Store {
.filter((part) => part.length);
this.phrase = phraseParts.join(' ');
this.backupPhraseAddress = null;
}
@action setRawKey = (rawKey) => {
@ -460,7 +506,8 @@ export default class Store {
}
export {
STAGE_CREATE,
STAGE_INFO,
STAGE_CONFIRM_BACKUP,
STAGE_CREATE,
STAGE_SELECT_TYPE
};

View File

@ -52,6 +52,10 @@ const STAGE_NAMES = [
id='firstRun.title.recovery'
defaultMessage='recovery'
/>,
<FormattedMessage
id='firstRun.title.confirmation'
defaultMessage='confirmation'
/>,
<FormattedMessage
id='firstRun.title.completed'
defaultMessage='completed'
@ -129,9 +133,19 @@ class FirstRun extends Component {
);
case 3:
return (
<AccountDetails createStore={ this.createStore } />
<AccountDetails
createStore={ this.createStore }
withRequiredBackup
/>
);
case 4:
return (
<AccountDetails
createStore={ this.createStore }
isConfirming
/>
);
case 5:
return (
<Completed />
);
@ -141,7 +155,7 @@ class FirstRun extends Component {
renderDialogActions () {
const { hasAccounts } = this.props;
const { stage, hasAcceptedTnc } = this.state;
const { canCreate } = this.createStore;
const { canCreate, phraseBackedUpError } = this.createStore;
switch (stage) {
case 0:
@ -169,15 +183,10 @@ class FirstRun extends Component {
const buttons = [
<Button
disabled={ !canCreate }
icon={ <CheckIcon /> }
key='create'
label={
<FormattedMessage
id='firstRun.button.create'
defaultMessage='Create'
/>
}
onClick={ this.onCreate }
icon={ <NextIcon /> }
key='next'
label={ BUTTON_LABEL_NEXT }
onClick={ this.onNext }
/>
];
@ -212,14 +221,30 @@ class FirstRun extends Component {
onClick={ this.printPhrase }
/>,
<Button
disabled={ !!phraseBackedUpError }
icon={ <NextIcon /> }
key='next'
label={ BUTTON_LABEL_NEXT }
onClick={ this.onNext }
onClick={ this.onConfirmPhraseBackup }
/>
];
case 4:
return (
<Button
icon={ <CheckIcon /> }
key='create'
label={
<FormattedMessage
id='firstRun.button.create'
defaultMessage='Create'
/>
}
onClick={ this.onCreate }
/>
);
case 5:
return (
<Button
icon={ <DoneIcon /> }
@ -244,6 +269,11 @@ class FirstRun extends Component {
}, onClose);
}
onConfirmPhraseBackup = () => {
this.createStore.clearPhrase();
this.onNext();
}
onNext = () => {
const { stage } = this.state;
@ -261,11 +291,19 @@ class FirstRun extends Component {
onCreate = () => {
this.createStore.setBusy(true);
return this.createStore
.createAccount()
.then(() => {
this.onNext();
this.createStore.setBusy(false);
this.createStore.computeBackupPhraseAddress()
.then(err => {
if (err) {
this.createStore.setBusy(false);
return;
}
return this.createStore.createAccount()
.then(() => {
this.createStore.clearPhrase();
this.createStore.setBusy(false);
this.onNext();
});
})
.catch((error) => {
this.createStore.setBusy(false);

View File

@ -57,6 +57,7 @@ export default class Input extends Component {
PropTypes.string,
PropTypes.bool
]),
allowPaste: PropTypes.bool,
autoFocus: PropTypes.bool,
children: PropTypes.node,
className: PropTypes.string,
@ -97,6 +98,7 @@ export default class Input extends Component {
static defaultProps = {
allowCopy: false,
allowPaste: true,
escape: 'initial',
hideUnderline: false,
onBlur: noop,
@ -221,6 +223,12 @@ export default class Input extends Component {
}
onChange = (event, value) => {
if (!this.props.allowPaste) {
if (value.length - this.state.value.length > 8) {
return;
}
}
event.persist();
this.setValue(value, () => {