From 40f0ee004f03d0d5adc04313c9f2fe6460505bfb Mon Sep 17 00:00:00 2001 From: Nicolas Gotchac Date: Mon, 9 Jan 2017 11:14:36 +0100 Subject: [PATCH] Key derivation in Worker (#4071) * Add Signer Key Derivation in Service Worker * Several fixes throughout the UI * Hint for external account // working Worker * Add Worker state change * PR Grumbles --- js/src/modals/PasswordManager/store.js | 2 +- js/src/redux/providers/compilerActions.js | 69 ------------------- js/src/redux/providers/index.js | 2 +- js/src/redux/providers/signerMiddleware.js | 63 ++++++++++------- js/src/redux/providers/status.js | 5 +- js/src/redux/providers/worker.js | 68 ++++++++++++++++++ js/src/redux/providers/workerActions.js | 29 ++++++++ .../{compilerReducer.js => workerReducer.js} | 2 +- js/src/redux/reducers.js | 6 +- js/src/redux/store.js | 2 + js/src/serviceWorker.js | 10 +++ js/src/ui/MethodDecoding/methodDecoding.css | 4 ++ js/src/ui/MethodDecoding/methodDecoding.js | 2 +- js/src/util/{wallet.js => signer.js} | 28 ++++++-- .../transactionPendingFormConfirm.js | 33 ++++++++- js/src/views/WriteContract/writeContract.js | 18 +---- 16 files changed, 221 insertions(+), 122 deletions(-) delete mode 100644 js/src/redux/providers/compilerActions.js create mode 100644 js/src/redux/providers/worker.js create mode 100644 js/src/redux/providers/workerActions.js rename js/src/redux/providers/{compilerReducer.js => workerReducer.js} (94%) rename js/src/util/{wallet.js => signer.js} (83%) diff --git a/js/src/modals/PasswordManager/store.js b/js/src/modals/PasswordManager/store.js index c60576e0f..659543c28 100644 --- a/js/src/modals/PasswordManager/store.js +++ b/js/src/modals/PasswordManager/store.js @@ -133,7 +133,7 @@ export default class Store { } testPassword = (password) => { - this.setBusy(false); + this.setBusy(true); return this._api.parity .testPassword(this.address, password || this.validatePassword) diff --git a/js/src/redux/providers/compilerActions.js b/js/src/redux/providers/compilerActions.js deleted file mode 100644 index d638c03a2..000000000 --- a/js/src/redux/providers/compilerActions.js +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright 2015, 2016 Parity Technologies (UK) Ltd. -// This file is part of Parity. - -// Parity is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. - -// Parity is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with Parity. If not, see . - -import PromiseWorker from 'promise-worker'; -import runtime from 'serviceworker-webpack-plugin/lib/runtime'; - -let workerRegistration; - -// Setup the Service Worker -if ('serviceWorker' in navigator) { - workerRegistration = runtime - .register() - .then(() => navigator.serviceWorker.ready) - .then((registration) => { - const _worker = registration.active; - _worker.controller = registration.active; - const worker = new PromiseWorker(_worker); - - return worker; - }); -} else { - workerRegistration = Promise.reject('Service Worker is not available in your browser.'); -} - -export function setWorker (worker) { - return { - type: 'setWorker', - worker - }; -} - -export function setError (error) { - return { - type: 'setError', - error - }; -} - -export function setupWorker () { - return (dispatch, getState) => { - const state = getState(); - - if (state.compiler.worker) { - return; - } - - workerRegistration - .then((worker) => { - dispatch(setWorker(worker)); - }) - .catch((error) => { - console.error('sw', error); - dispatch(setWorker(null)); - }); - }; -} diff --git a/js/src/redux/providers/index.js b/js/src/redux/providers/index.js index 6a000bdac..e1441c479 100644 --- a/js/src/redux/providers/index.js +++ b/js/src/redux/providers/index.js @@ -22,7 +22,7 @@ export Status from './status'; export apiReducer from './apiReducer'; export balancesReducer from './balancesReducer'; export blockchainReducer from './blockchainReducer'; -export compilerReducer from './compilerReducer'; +export workerReducer from './workerReducer'; export imagesReducer from './imagesReducer'; export personalReducer from './personalReducer'; export signerReducer from './signerReducer'; diff --git a/js/src/redux/providers/signerMiddleware.js b/js/src/redux/providers/signerMiddleware.js index 018e01e59..ba51d3426 100644 --- a/js/src/redux/providers/signerMiddleware.js +++ b/js/src/redux/providers/signerMiddleware.js @@ -17,7 +17,7 @@ import * as actions from './signerActions'; import { inHex } from '~/api/format/input'; -import { Wallet } from '../../util/wallet'; +import { Signer } from '../../util/signer'; export default class SignerMiddleware { constructor (api) { @@ -58,6 +58,7 @@ export default class SignerMiddleware { promise .then((txHash) => { console.log('confirmRequest', id, txHash); + if (!txHash) { store.dispatch(actions.errorConfirmRequest({ id, err: 'Unable to confirm.' })); return; @@ -73,33 +74,49 @@ export default class SignerMiddleware { // Sign request in-browser const transaction = payload.sendTransaction || payload.signTransaction; + if (wallet && transaction) { - (transaction.nonce.isZero() + const noncePromise = transaction.nonce.isZero() ? this._api.parity.nextNonce(transaction.from) - : Promise.resolve(transaction.nonce) - ).then(nonce => { - let txData = { - to: inHex(transaction.to), - nonce: inHex(transaction.nonce.isZero() ? nonce : transaction.nonce), - gasPrice: inHex(transaction.gasPrice), - gasLimit: inHex(transaction.gas), - value: inHex(transaction.value), - data: inHex(transaction.data) - }; + : Promise.resolve(transaction.nonce); - try { - // NOTE: Derving the key takes significant amount of time, - // make sure to display some kind of "in-progress" state. - const signer = Wallet.fromJson(wallet, password); - const rawTx = signer.signTransaction(txData); + const { worker } = store.getState().worker; - handlePromise(this._api.signer.confirmRequestRaw(id, rawTx)); - } catch (error) { - console.error(error); + const signerPromise = worker && worker._worker.state === 'activated' + ? worker + .postMessage({ + action: 'getSignerSeed', + data: { wallet, password } + }) + .then((result) => { + const seed = Buffer.from(result.data); + return new Signer(seed); + }) + : Signer.fromJson(wallet, password); + + // NOTE: Derving the key takes significant amount of time, + // make sure to display some kind of "in-progress" state. + return Promise + .all([ signerPromise, noncePromise ]) + .then(([ signer, nonce ]) => { + const txData = { + to: inHex(transaction.to), + nonce: inHex(transaction.nonce.isZero() ? nonce : transaction.nonce), + gasPrice: inHex(transaction.gasPrice), + gasLimit: inHex(transaction.gas), + value: inHex(transaction.value), + data: inHex(transaction.data) + }; + + return signer.signTransaction(txData); + }) + .then((rawTx) => { + return handlePromise(this._api.signer.confirmRequestRaw(id, rawTx)); + }) + .catch((error) => { + console.error(error.message); store.dispatch(actions.errorConfirmRequest({ id, err: error.message })); - } - }); - return; + }); } handlePromise(this._api.signer.confirmRequest(id, { gas, gasPrice }, password)); diff --git a/js/src/redux/providers/status.js b/js/src/redux/providers/status.js index ef4c09224..6d0e24c6b 100644 --- a/js/src/redux/providers/status.js +++ b/js/src/redux/providers/status.js @@ -125,12 +125,13 @@ export default class Status { this._store.dispatch(statusCollection(status)); this._status = status; } + + nextTimeout(); }) .catch((error) => { console.error('_pollStatus', error); + nextTimeout(); }); - - nextTimeout(); } /** diff --git a/js/src/redux/providers/worker.js b/js/src/redux/providers/worker.js new file mode 100644 index 000000000..35ca0f173 --- /dev/null +++ b/js/src/redux/providers/worker.js @@ -0,0 +1,68 @@ +// Copyright 2015, 2016 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +import PromiseWorker from 'promise-worker'; +import runtime from 'serviceworker-webpack-plugin/lib/runtime'; + +import { setWorker } from './workerActions'; + +function getWorker () { + // Setup the Service Worker + if ('serviceWorker' in navigator) { + return runtime + .register() + .then(() => navigator.serviceWorker.ready) + .then((registration) => { + const worker = registration.active; + worker.controller = registration.active; + + return new PromiseWorker(worker); + }); + } + + return Promise.reject('Service Worker is not available in your browser.'); +} + +export const setupWorker = (store) => { + const { dispatch, getState } = store; + + const state = getState(); + const stateWorker = state.worker.worker; + + if (stateWorker !== undefined && !(stateWorker && stateWorker._worker.state === 'redundant')) { + return; + } + + getWorker() + .then((worker) => { + if (worker) { + worker._worker.addEventListener('statechange', (event) => { + console.warn('worker state changed to', worker._worker.state); + + // Re-install the new Worker + if (worker._worker.state === 'redundant') { + setupWorker(store); + } + }); + } + + dispatch(setWorker(worker)); + }) + .catch((error) => { + console.error('sw', error); + dispatch(setWorker(null)); + }); +}; diff --git a/js/src/redux/providers/workerActions.js b/js/src/redux/providers/workerActions.js new file mode 100644 index 000000000..50a877508 --- /dev/null +++ b/js/src/redux/providers/workerActions.js @@ -0,0 +1,29 @@ +// Copyright 2015, 2016 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +export function setWorker (worker) { + return { + type: 'setWorker', + worker + }; +} + +export function setError (error) { + return { + type: 'setError', + error + }; +} diff --git a/js/src/redux/providers/compilerReducer.js b/js/src/redux/providers/workerReducer.js similarity index 94% rename from js/src/redux/providers/compilerReducer.js rename to js/src/redux/providers/workerReducer.js index e23bf3b16..27144e11b 100644 --- a/js/src/redux/providers/compilerReducer.js +++ b/js/src/redux/providers/workerReducer.js @@ -24,7 +24,7 @@ const initialState = { export default handleActions({ setWorker (state, action) { const { worker } = action; - return Object.assign({}, state, { worker }); + return Object.assign({}, state, { worker: worker || null }); }, setError (state, action) { diff --git a/js/src/redux/reducers.js b/js/src/redux/reducers.js index aac1ea779..1156d1836 100644 --- a/js/src/redux/reducers.js +++ b/js/src/redux/reducers.js @@ -19,7 +19,7 @@ import { routerReducer } from 'react-router-redux'; import { apiReducer, balancesReducer, blockchainReducer, - compilerReducer, imagesReducer, personalReducer, + workerReducer, imagesReducer, personalReducer, signerReducer, statusReducer as nodeStatusReducer, snackbarReducer, walletReducer } from './providers'; @@ -40,12 +40,12 @@ export default function () { balances: balancesReducer, certifications: certificationsReducer, blockchain: blockchainReducer, - compiler: compilerReducer, images: imagesReducer, nodeStatus: nodeStatusReducer, personal: personalReducer, signer: signerReducer, snackbar: snackbarReducer, - wallet: walletReducer + wallet: walletReducer, + worker: workerReducer }); } diff --git a/js/src/redux/store.js b/js/src/redux/store.js index dc043e242..9924aa461 100644 --- a/js/src/redux/store.js +++ b/js/src/redux/store.js @@ -20,6 +20,7 @@ import initMiddleware from './middleware'; import initReducers from './reducers'; import { load as loadWallet } from './providers/walletActions'; +import { setupWorker } from './providers/worker'; import { Balances as BalancesProvider, @@ -43,6 +44,7 @@ export default function (api, browserHistory) { new StatusProvider(store, api).start(); store.dispatch(loadWallet(api)); + setupWorker(store); return store; } diff --git a/js/src/serviceWorker.js b/js/src/serviceWorker.js index 136e6a6b7..3fdfc02ac 100644 --- a/js/src/serviceWorker.js +++ b/js/src/serviceWorker.js @@ -15,6 +15,7 @@ // along with Parity. If not, see . import registerPromiseWorker from 'promise-worker/register'; +import { Signer } from '~/util/signer'; import SolidityUtils from '~/util/solidity'; const CACHE_NAME = 'parity-cache-v1'; @@ -93,12 +94,21 @@ function handleMessage (message) { case 'setFiles': return setFiles(message.data); + case 'getSignerSeed': + return getSignerSeed(message.data); + default: console.warn(`unknown action "${message.action}"`); return null; } } +function getSignerSeed (data) { + console.log('deriving seed from service-worker'); + const { wallet, password } = data; + return Signer.getSeed(wallet, password); +} + function compile (data) { const { build } = data; diff --git a/js/src/ui/MethodDecoding/methodDecoding.css b/js/src/ui/MethodDecoding/methodDecoding.css index adb899e1c..c782d6ce7 100644 --- a/js/src/ui/MethodDecoding/methodDecoding.css +++ b/js/src/ui/MethodDecoding/methodDecoding.css @@ -38,6 +38,10 @@ justify-content: center; } +.details { + line-height: 1.75em; +} + .details, .gasDetails { color: #aaa; diff --git a/js/src/ui/MethodDecoding/methodDecoding.js b/js/src/ui/MethodDecoding/methodDecoding.js index 59704a731..693ae60b5 100644 --- a/js/src/ui/MethodDecoding/methodDecoding.js +++ b/js/src/ui/MethodDecoding/methodDecoding.js @@ -196,7 +196,7 @@ class MethodDecoding extends Component { : text.slice(0, 50) + '...'; return ( -
+
with the { + return new Signer(seed); + }); + } + + static getSeed (json, password) { + try { + const seed = Signer.getSyncSeed(json, password); + return Promise.resolve(seed); + } catch (error) { + return Promise.reject(error); + } + } + + static getSyncSeed (json, password) { if (json.version !== 3) { throw new Error('Only V3 wallets are supported'); } @@ -43,15 +60,17 @@ export class Wallet { if (kdfparams.prf !== 'hmac-sha256') { throw new Error('Unsupported parameters to PBKDF2'); } + derivedKey = pbkdf2Sync(pwd, salt, kdfparams.c, kdfparams.dklen, 'sha256'); } else { throw new Error('Unsupported key derivation scheme'); } const ciphertext = Buffer.from(json.crypto.ciphertext, 'hex'); - let mac = sha3(Buffer.concat([derivedKey.slice(16, 32), ciphertext])); + const mac = sha3(Buffer.concat([derivedKey.slice(16, 32), ciphertext])); + if (mac !== inHex(json.crypto.mac)) { - throw new Error('Key derivation failed - possibly wrong passphrase'); + throw new Error('Key derivation failed - possibly wrong password'); } const decipher = createDecipheriv( @@ -59,6 +78,7 @@ export class Wallet { derivedKey.slice(0, 16), Buffer.from(json.crypto.cipherparams.iv, 'hex') ); + let seed = Buffer.concat([decipher.update(ciphertext), decipher.final()]); while (seed.length < 32) { @@ -66,7 +86,7 @@ export class Wallet { seed = Buffer.concat([nullBuff, seed]); } - return new Wallet(seed); + return seed; } constructor (seed) { diff --git a/js/src/views/Signer/components/TransactionPendingForm/TransactionPendingFormConfirm/transactionPendingFormConfirm.js b/js/src/views/Signer/components/TransactionPendingForm/TransactionPendingFormConfirm/transactionPendingFormConfirm.js index 99bd1c5f3..45eb3e5dd 100644 --- a/js/src/views/Signer/components/TransactionPendingForm/TransactionPendingFormConfirm/transactionPendingFormConfirm.js +++ b/js/src/views/Signer/components/TransactionPendingForm/TransactionPendingFormConfirm/transactionPendingFormConfirm.js @@ -77,13 +77,28 @@ class TransactionPendingFormConfirm extends Component { } } + getPasswordHint () { + const { account } = this.props; + const accountHint = account && account.meta && account.meta.passwordHint; + + if (accountHint) { + return accountHint; + } + + const { wallet } = this.state; + const walletHint = wallet && wallet.meta && wallet.meta.passwordHint; + + return walletHint || null; + } + render () { const { account, address, isSending } = this.props; const { password, wallet, walletError } = this.state; const isExternal = !account.uuid; - const passwordHint = account.meta && account.meta.passwordHint - ? (
(hint) { account.meta.passwordHint }
) + const passwordHintText = this.getPasswordHint(); + const passwordHint = passwordHintText + ? (
(hint) { passwordHintText }
) : null; const isWalletOk = !isExternal || (walletError === null && wallet !== null); @@ -170,12 +185,26 @@ class TransactionPendingFormConfirm extends Component { } onKeySelect = (event) => { + // Check that file have been selected + if (event.target.files.length === 0) { + return this.setState({ + wallet: null, + walletError: null + }); + } + const fileReader = new FileReader(); fileReader.onload = (e) => { try { const wallet = JSON.parse(e.target.result); + try { + if (wallet && typeof wallet.meta === 'string') { + wallet.meta = JSON.parse(wallet.meta); + } + } catch (e) {} + this.setState({ wallet, walletError: null diff --git a/js/src/views/WriteContract/writeContract.js b/js/src/views/WriteContract/writeContract.js index c013775a1..8a3ddf3d1 100644 --- a/js/src/views/WriteContract/writeContract.js +++ b/js/src/views/WriteContract/writeContract.js @@ -18,7 +18,6 @@ import React, { PropTypes, Component } from 'react'; import { observer } from 'mobx-react'; import { MenuItem, Toggle } from 'material-ui'; import { connect } from 'react-redux'; -import { bindActionCreators } from 'redux'; import CircularProgress from 'material-ui/CircularProgress'; import moment from 'moment'; import { throttle } from 'lodash'; @@ -32,8 +31,6 @@ import SendIcon from 'material-ui/svg-icons/content/send'; import { Actionbar, ActionbarExport, ActionbarImport, Button, Editor, Page, Select, Input } from '~/ui'; import { DeployContract, SaveContract, LoadContract } from '~/modals'; -import { setupWorker } from '~/redux/providers/compilerActions'; - import WriteContractStore from './writeContractStore'; import styles from './writeContract.css'; @@ -42,7 +39,6 @@ class WriteContract extends Component { static propTypes = { accounts: PropTypes.object.isRequired, - setupWorker: PropTypes.func.isRequired, worker: PropTypes.object, workerError: PropTypes.any }; @@ -55,8 +51,7 @@ class WriteContract extends Component { }; componentWillMount () { - const { setupWorker, worker } = this.props; - setupWorker(); + const { worker } = this.props; if (worker !== undefined) { this.store.setWorker(worker); @@ -575,17 +570,10 @@ class WriteContract extends Component { function mapStateToProps (state) { const { accounts } = state.personal; - const { worker, error } = state.compiler; + const { worker, error } = state.worker; return { accounts, worker, workerError: error }; } -function mapDispatchToProps (dispatch) { - return bindActionCreators({ - setupWorker - }, dispatch); -} - export default connect( - mapStateToProps, - mapDispatchToProps + mapStateToProps )(WriteContract);