diff --git a/CHANGELOG.md b/CHANGELOG.md index cd5495c94..c011ea6af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Current Master +- Added seed word recovery to config screen. + ## 2.2.0 2016-06-02 - Redesigned init, vault create, vault restore and seed confirmation screens. diff --git a/app/scripts/background.js b/app/scripts/background.js index bfd1fc92b..432040c53 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -145,7 +145,7 @@ function setupPublicConfig(stream){ } function setupProviderConnection(stream, originDomain){ - + stream.on('data', function onRpcRequest(payload){ // Append origin to rpc payload payload.origin = originDomain @@ -195,6 +195,8 @@ function setupControllerConnection(stream){ exportAccount: idStore.exportAccount.bind(idStore), revealAccount: idStore.revealAccount.bind(idStore), saveAccountLabel: idStore.saveAccountLabel.bind(idStore), + tryPassword: idStore.tryPassword.bind(idStore), + recoverSeed: idStore.recoverSeed.bind(idStore), }) stream.pipe(dnode).pipe(stream) dnode.on('remote', function(remote){ @@ -246,7 +248,7 @@ function newUnsignedTransaction(txParams, cb){ }) var txId = idStore.addUnconfirmedTransaction(txParams, cb) } else { - addUnconfirmedTx(txParams, cb) + addUnconfirmedTx(txParams, cb) } } @@ -258,7 +260,7 @@ function newUnsignedMessage(msgParams, cb){ }) var msgId = idStore.addUnconfirmedMessage(msgParams, cb) } else { - addUnconfirmedMsg(msgParams, cb) + addUnconfirmedMsg(msgParams, cb) } } diff --git a/app/scripts/lib/idStore.js b/app/scripts/lib/idStore.js index 4ce4fd6f2..e9fc10cea 100644 --- a/app/scripts/lib/idStore.js +++ b/app/scripts/lib/idStore.js @@ -59,6 +59,13 @@ IdentityStore.prototype.createNewVault = function(password, entropy, cb){ }) } +IdentityStore.prototype.recoverSeed = function(cb){ + configManager.setShowSeedWords(true) + if (!this._idmgmt) return cb(new Error('Unauthenticated. Please sign in.')) + var seedWords = this._idmgmt.getSeed() + cb(null, seedWords) +} + IdentityStore.prototype.recoverFromSeed = function(password, seed, cb){ this._createIdmgmt(password, seed, null, (err) => { if (err) return cb(err) @@ -150,7 +157,7 @@ IdentityStore.prototype.setLocked = function(cb){ } IdentityStore.prototype.submitPassword = function(password, cb){ - this._tryPassword(password, (err) => { + this.tryPassword(password, (err) => { if (err) return cb(err) // load identities before returning... this._loadIdentities() @@ -366,7 +373,7 @@ IdentityStore.prototype._mayBeFauceting = function(i) { // keyStore managment - unlocking + deserialization // -IdentityStore.prototype._tryPassword = function(password, cb){ +IdentityStore.prototype.tryPassword = function(password, cb){ this._createIdmgmt(password, null, null, cb) } diff --git a/ui/app/actions.js b/ui/app/actions.js index ae6125b20..684d33f3d 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -29,6 +29,10 @@ var actions = { createNewVaultInProgress: createNewVaultInProgress, showNewVaultSeed: showNewVaultSeed, showInfoPage: showInfoPage, + // seed recovery actions + REVEAL_SEED_CONFIRMATION: 'REVEAL_SEED_CONFIRMATION', + revealSeedConfirmation: revealSeedConfirmation, + requestRevealSeed: requestRevealSeed, // unlock screen UNLOCK_IN_PROGRESS: 'UNLOCK_IN_PROGRESS', UNLOCK_FAILED: 'UNLOCK_FAILED', @@ -155,6 +159,26 @@ function createNewVault(password, entropy) { } } +function revealSeedConfirmation() { + return { + type: this.REVEAL_SEED_CONFIRMATION, + } +} + +function requestRevealSeed(password) { + return (dispatch) => { + dispatch(actions.showLoadingIndication()) + _accountManager.tryPassword(password, (err, seed) => { + dispatch(actions.hideLoadingIndication()) + if (err) return dispatch(actions.displayWarning(err.message)) + _accountManager.recoverSeed((err, seed) => { + if (err) return dispatch(actions.displayWarning(err.message)) + dispatch(actions.showNewVaultSeed(seed)) + }) + }) + } +} + function recoverFromSeed(password, seed) { return (dispatch) => { // dispatch(actions.createNewVaultInProgress()) @@ -402,9 +426,10 @@ function previousTx() { } } -function showConfigPage() { +function showConfigPage(transitionForward = true) { return { type: actions.SHOW_CONFIG_PAGE, + value: transitionForward, } } diff --git a/ui/app/app.js b/ui/app/app.js index 7e7ca24ad..094cae76c 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -21,6 +21,7 @@ const SendTransactionScreen = require('./send') const ConfirmTxScreen = require('./conf-tx') // other views const ConfigScreen = require('./config') +const RevealSeedConfirmation = require('./recover-seed/confirmation') const InfoScreen = require('./info') const LoadingIndicator = require('./loading') const txHelper = require('../lib/tx-helper') @@ -232,6 +233,9 @@ App.prototype.renderPrimary = function(){ case 'config': return h(ConfigScreen, {key: 'config'}) + case 'reveal-seed-conf': + return h(RevealSeedConfirmation, {key: 'reveal-seed-conf'}) + case 'info': return h(InfoScreen, {key: 'info'}) diff --git a/ui/app/config.js b/ui/app/config.js index ddf158325..c4d473b10 100644 --- a/ui/app/config.js +++ b/ui/app/config.js @@ -78,7 +78,7 @@ ConfigScreen.prototype.render = function() { ]), h('div', [ - h('button', { + h('button.spaced', { style: { alignSelf: 'center', }, @@ -86,11 +86,11 @@ ConfigScreen.prototype.render = function() { event.preventDefault() state.dispatch(actions.setProviderType('mainnet')) } - }, 'Use Main Network') + }, 'Use Main Network'), ]), h('div', [ - h('button', { + h('button.spaced', { style: { alignSelf: 'center', }, @@ -98,11 +98,11 @@ ConfigScreen.prototype.render = function() { event.preventDefault() state.dispatch(actions.setProviderType('testnet')) } - }, 'Use Morden Test Network') + }, 'Use Morden Test Network'), ]), h('div', [ - h('button', { + h('button.spaced', { style: { alignSelf: 'center', }, @@ -110,7 +110,25 @@ ConfigScreen.prototype.render = function() { event.preventDefault() state.dispatch(actions.setRpcTarget('http://localhost:8545/')) } - }, 'Use http://localhost:8545') + }, 'Use http://localhost:8545'), + ]), + + h('hr.horizontal-line'), + + h('div', { + style: { + marginTop: '20px', + } + }, [ + h('button', { + style: { + alignSelf: 'center', + }, + onClick(event) { + event.preventDefault() + state.dispatch(actions.revealSeedConfirmation()) + } + }, 'Reveal Seed Words') ]), ]), diff --git a/ui/app/css/index.css b/ui/app/css/index.css index f56e9fbc4..4fd51ac95 100644 --- a/ui/app/css/index.css +++ b/ui/app/css/index.css @@ -45,6 +45,10 @@ button { transition: transform 50ms ease-in; } +button.spaced { + margin: 2px; +} + button:hover { transform: scale(1.1); } diff --git a/ui/app/first-time/create-vault-complete.js b/ui/app/first-time/create-vault-complete.js index 7d2db8003..9eceb4421 100644 --- a/ui/app/first-time/create-vault-complete.js +++ b/ui/app/first-time/create-vault-complete.js @@ -14,7 +14,7 @@ function CreateVaultCompleteScreen() { function mapStateToProps(state) { return { - seed: state.appState.currentView.context, + seed: state.appState.currentView.seedWords, cachedSeed: state.metamask.seedWords, } } diff --git a/ui/app/recover-seed/confirmation.js b/ui/app/recover-seed/confirmation.js new file mode 100644 index 000000000..0276d547d --- /dev/null +++ b/ui/app/recover-seed/confirmation.js @@ -0,0 +1,149 @@ +const inherits = require('util').inherits + +const Component = require('react').Component +const connect = require('react-redux').connect +const h = require('react-hyperscript') +const actions = require('../actions') + +module.exports = connect(mapStateToProps)(RevealSeedConfirmatoin) + + +inherits(RevealSeedConfirmatoin, Component) +function RevealSeedConfirmatoin() { + Component.call(this) +} + +function mapStateToProps(state) { + return { + warning: state.appState.warning, + } +} + +RevealSeedConfirmatoin.prototype.confirmationPhrase = 'I understand' + +RevealSeedConfirmatoin.prototype.render = function() { + const props = this.props + const state = this.state + + return ( + + h('.initialize-screen.flex-column.flex-center.flex-grow', [ + + h('h3.flex-center.text-transform-uppercase', { + style: { + background: '#EBEBEB', + color: '#AEAEAE', + marginBottom: 24, + width: '100%', + fontSize: '20px', + padding: 6, + }, + }, [ + 'Reveal Seed Words', + ]), + + h('.div', { + style: { + display: 'flex', + flexDirection: 'column', + padding: '20px', + justifyContent: 'center', + } + }, [ + + h('h4', 'Do not recover your seed words in a public place! These words can be used to steal all your accounts.'), + + // confirmation + h('input.large-input.letter-spacey', { + type: 'password', + id: 'password-box', + placeholder: 'Enter your password to confirm', + onKeyPress: this.checkConfirmation.bind(this), + style: { + width: 260, + marginTop: '12px', + }, + }), + + h(`h4${state && state.confirmationWrong ? '.error' : ''}`, { + style: { + marginTop: '12px', + } + }, `Enter the phrase "I understand" to proceed.`), + + // confirm confirmation + h('input.large-input.letter-spacey', { + type: 'text', + id: 'confirm-box', + placeholder: this.confirmationPhrase, + onKeyPress: this.checkConfirmation.bind(this), + style: { + width: 260, + marginTop: 16, + }, + }), + + h('.flex-row.flex-space-between', { + style: { + marginTop: 30, + width: '50%', + }, + }, [ +// cancel + h('button.primary', { + onClick: this.goHome.bind(this), + }, 'CANCEL'), + + // submit + h('button.primary', { + onClick: this.revealSeedWords.bind(this), + }, 'OK'), + + ]), + + (props.warning) && ( + h('span.error', { + style: { + margin: '20px', + } + }, props.warning.split('-')) + ), + + props.inProgress && ( + h('span.in-progress-notification', 'Generating Seed...') + ), + ]), + ]) + ) +} + +RevealSeedConfirmatoin.prototype.componentDidMount = function(){ + document.getElementById('password-box').focus() +} + +RevealSeedConfirmatoin.prototype.goHome = function() { + this.props.dispatch(actions.showConfigPage(false)) +} + +// create vault + +RevealSeedConfirmatoin.prototype.checkConfirmation = function(event) { + if (event.key === 'Enter') { + event.preventDefault() + this.revealSeedWords() + } +} + +RevealSeedConfirmatoin.prototype.revealSeedWords = function(){ + this.setState({ confirmationWrong: false }) + + const confirmBox = document.getElementById('confirm-box') + const confirmation = confirmBox.value + if (confirmation !== this.confirmationPhrase) { + confirmBox.value = '' + return this.setState({ confirmationWrong: true }) + } + + var password = document.getElementById('password-box').value + this.props.dispatch(actions.requestRevealSeed(password)) +} diff --git a/ui/app/reducers/app.js b/ui/app/reducers/app.js index 08c2268c1..3ee9a61fe 100644 --- a/ui/app/reducers/app.js +++ b/ui/app/reducers/app.js @@ -25,10 +25,11 @@ function reduceApp(state, action) { } // confirm seed words + var seedWords = state.metamask.seedWords var seedConfView = { name: 'createVaultComplete', + seedWords, } - var seedWords = state.metamask.seedWords var appState = extend({ menuOpen: false, @@ -85,7 +86,7 @@ function reduceApp(state, action) { name: 'config', context: appState.currentView.context, }, - transForward: true, + transForward: action.value, }) case actions.SHOW_INFO_PAGE: @@ -111,7 +112,7 @@ function reduceApp(state, action) { return extend(appState, { currentView: { name: 'createVaultComplete', - context: action.value, + seedWords: action.value, }, transForward: true, isLoading: false, @@ -144,6 +145,18 @@ function reduceApp(state, action) { warning: null, }) + // reveal seed words + + case actions.REVEAL_SEED_CONFIRMATION: + return extend(appState, { + currentView: { + name: 'reveal-seed-conf', + }, + transForward: true, + warning: null, + }) + + // accounts case actions.SET_SELECTED_ACCOUNT: @@ -198,6 +211,7 @@ function reduceApp(state, action) { return extend(appState, { currentView: { name: seedWords ? 'createVaultComplete' : 'accounts', + seedWords, }, transForward: true, isLoading: false,