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
This commit is contained in:
Nicolas Gotchac 2017-01-09 11:14:36 +01:00 committed by Gav Wood
parent ec4b4cfbf2
commit 40f0ee004f
16 changed files with 221 additions and 122 deletions

View File

@ -133,7 +133,7 @@ export default class Store {
} }
testPassword = (password) => { testPassword = (password) => {
this.setBusy(false); this.setBusy(true);
return this._api.parity return this._api.parity
.testPassword(this.address, password || this.validatePassword) .testPassword(this.address, password || this.validatePassword)

View File

@ -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 <http://www.gnu.org/licenses/>.
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));
});
};
}

View File

@ -22,7 +22,7 @@ export Status from './status';
export apiReducer from './apiReducer'; export apiReducer from './apiReducer';
export balancesReducer from './balancesReducer'; export balancesReducer from './balancesReducer';
export blockchainReducer from './blockchainReducer'; export blockchainReducer from './blockchainReducer';
export compilerReducer from './compilerReducer'; export workerReducer from './workerReducer';
export imagesReducer from './imagesReducer'; export imagesReducer from './imagesReducer';
export personalReducer from './personalReducer'; export personalReducer from './personalReducer';
export signerReducer from './signerReducer'; export signerReducer from './signerReducer';

View File

@ -17,7 +17,7 @@
import * as actions from './signerActions'; import * as actions from './signerActions';
import { inHex } from '~/api/format/input'; import { inHex } from '~/api/format/input';
import { Wallet } from '../../util/wallet'; import { Signer } from '../../util/signer';
export default class SignerMiddleware { export default class SignerMiddleware {
constructor (api) { constructor (api) {
@ -58,6 +58,7 @@ export default class SignerMiddleware {
promise promise
.then((txHash) => { .then((txHash) => {
console.log('confirmRequest', id, txHash); console.log('confirmRequest', id, txHash);
if (!txHash) { if (!txHash) {
store.dispatch(actions.errorConfirmRequest({ id, err: 'Unable to confirm.' })); store.dispatch(actions.errorConfirmRequest({ id, err: 'Unable to confirm.' }));
return; return;
@ -73,33 +74,49 @@ export default class SignerMiddleware {
// Sign request in-browser // Sign request in-browser
const transaction = payload.sendTransaction || payload.signTransaction; const transaction = payload.sendTransaction || payload.signTransaction;
if (wallet && transaction) { if (wallet && transaction) {
(transaction.nonce.isZero() const noncePromise = transaction.nonce.isZero()
? this._api.parity.nextNonce(transaction.from) ? this._api.parity.nextNonce(transaction.from)
: Promise.resolve(transaction.nonce) : 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)
};
try { const { worker } = store.getState().worker;
// 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);
handlePromise(this._api.signer.confirmRequestRaw(id, rawTx)); const signerPromise = worker && worker._worker.state === 'activated'
} catch (error) { ? worker
console.error(error); .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 })); store.dispatch(actions.errorConfirmRequest({ id, err: error.message }));
} });
});
return;
} }
handlePromise(this._api.signer.confirmRequest(id, { gas, gasPrice }, password)); handlePromise(this._api.signer.confirmRequest(id, { gas, gasPrice }, password));

View File

@ -125,12 +125,13 @@ export default class Status {
this._store.dispatch(statusCollection(status)); this._store.dispatch(statusCollection(status));
this._status = status; this._status = status;
} }
nextTimeout();
}) })
.catch((error) => { .catch((error) => {
console.error('_pollStatus', error); console.error('_pollStatus', error);
nextTimeout();
}); });
nextTimeout();
} }
/** /**

View File

@ -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 <http://www.gnu.org/licenses/>.
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));
});
};

View File

@ -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 <http://www.gnu.org/licenses/>.
export function setWorker (worker) {
return {
type: 'setWorker',
worker
};
}
export function setError (error) {
return {
type: 'setError',
error
};
}

View File

@ -24,7 +24,7 @@ const initialState = {
export default handleActions({ export default handleActions({
setWorker (state, action) { setWorker (state, action) {
const { worker } = action; const { worker } = action;
return Object.assign({}, state, { worker }); return Object.assign({}, state, { worker: worker || null });
}, },
setError (state, action) { setError (state, action) {

View File

@ -19,7 +19,7 @@ import { routerReducer } from 'react-router-redux';
import { import {
apiReducer, balancesReducer, blockchainReducer, apiReducer, balancesReducer, blockchainReducer,
compilerReducer, imagesReducer, personalReducer, workerReducer, imagesReducer, personalReducer,
signerReducer, statusReducer as nodeStatusReducer, signerReducer, statusReducer as nodeStatusReducer,
snackbarReducer, walletReducer snackbarReducer, walletReducer
} from './providers'; } from './providers';
@ -40,12 +40,12 @@ export default function () {
balances: balancesReducer, balances: balancesReducer,
certifications: certificationsReducer, certifications: certificationsReducer,
blockchain: blockchainReducer, blockchain: blockchainReducer,
compiler: compilerReducer,
images: imagesReducer, images: imagesReducer,
nodeStatus: nodeStatusReducer, nodeStatus: nodeStatusReducer,
personal: personalReducer, personal: personalReducer,
signer: signerReducer, signer: signerReducer,
snackbar: snackbarReducer, snackbar: snackbarReducer,
wallet: walletReducer wallet: walletReducer,
worker: workerReducer
}); });
} }

View File

@ -20,6 +20,7 @@ import initMiddleware from './middleware';
import initReducers from './reducers'; import initReducers from './reducers';
import { load as loadWallet } from './providers/walletActions'; import { load as loadWallet } from './providers/walletActions';
import { setupWorker } from './providers/worker';
import { import {
Balances as BalancesProvider, Balances as BalancesProvider,
@ -43,6 +44,7 @@ export default function (api, browserHistory) {
new StatusProvider(store, api).start(); new StatusProvider(store, api).start();
store.dispatch(loadWallet(api)); store.dispatch(loadWallet(api));
setupWorker(store);
return store; return store;
} }

View File

@ -15,6 +15,7 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import registerPromiseWorker from 'promise-worker/register'; import registerPromiseWorker from 'promise-worker/register';
import { Signer } from '~/util/signer';
import SolidityUtils from '~/util/solidity'; import SolidityUtils from '~/util/solidity';
const CACHE_NAME = 'parity-cache-v1'; const CACHE_NAME = 'parity-cache-v1';
@ -93,12 +94,21 @@ function handleMessage (message) {
case 'setFiles': case 'setFiles':
return setFiles(message.data); return setFiles(message.data);
case 'getSignerSeed':
return getSignerSeed(message.data);
default: default:
console.warn(`unknown action "${message.action}"`); console.warn(`unknown action "${message.action}"`);
return null; return null;
} }
} }
function getSignerSeed (data) {
console.log('deriving seed from service-worker');
const { wallet, password } = data;
return Signer.getSeed(wallet, password);
}
function compile (data) { function compile (data) {
const { build } = data; const { build } = data;

View File

@ -38,6 +38,10 @@
justify-content: center; justify-content: center;
} }
.details {
line-height: 1.75em;
}
.details, .details,
.gasDetails { .gasDetails {
color: #aaa; color: #aaa;

View File

@ -196,7 +196,7 @@ class MethodDecoding extends Component {
: text.slice(0, 50) + '...'; : text.slice(0, 50) + '...';
return ( return (
<div> <div className={ styles.details }>
<span>with the </span> <span>with the </span>
<span <span
onClick={ this.toggleInputType } onClick={ this.toggleInputType }

View File

@ -24,9 +24,26 @@ import { sha3 } from '~/api/util/sha3';
// Adapted from https://github.com/kvhnuke/etherwallet/blob/mercury/app/scripts/myetherwallet.js // Adapted from https://github.com/kvhnuke/etherwallet/blob/mercury/app/scripts/myetherwallet.js
export class Wallet { export class Signer {
static fromJson (json, password) { static fromJson (json, password) {
return Signer
.getSeed(json, password)
.then((seed) => {
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) { if (json.version !== 3) {
throw new Error('Only V3 wallets are supported'); throw new Error('Only V3 wallets are supported');
} }
@ -43,15 +60,17 @@ export class Wallet {
if (kdfparams.prf !== 'hmac-sha256') { if (kdfparams.prf !== 'hmac-sha256') {
throw new Error('Unsupported parameters to PBKDF2'); throw new Error('Unsupported parameters to PBKDF2');
} }
derivedKey = pbkdf2Sync(pwd, salt, kdfparams.c, kdfparams.dklen, 'sha256'); derivedKey = pbkdf2Sync(pwd, salt, kdfparams.c, kdfparams.dklen, 'sha256');
} else { } else {
throw new Error('Unsupported key derivation scheme'); throw new Error('Unsupported key derivation scheme');
} }
const ciphertext = Buffer.from(json.crypto.ciphertext, 'hex'); 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)) { 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( const decipher = createDecipheriv(
@ -59,6 +78,7 @@ export class Wallet {
derivedKey.slice(0, 16), derivedKey.slice(0, 16),
Buffer.from(json.crypto.cipherparams.iv, 'hex') Buffer.from(json.crypto.cipherparams.iv, 'hex')
); );
let seed = Buffer.concat([decipher.update(ciphertext), decipher.final()]); let seed = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
while (seed.length < 32) { while (seed.length < 32) {
@ -66,7 +86,7 @@ export class Wallet {
seed = Buffer.concat([nullBuff, seed]); seed = Buffer.concat([nullBuff, seed]);
} }
return new Wallet(seed); return seed;
} }
constructor (seed) { constructor (seed) {

View File

@ -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 () { render () {
const { account, address, isSending } = this.props; const { account, address, isSending } = this.props;
const { password, wallet, walletError } = this.state; const { password, wallet, walletError } = this.state;
const isExternal = !account.uuid; const isExternal = !account.uuid;
const passwordHint = account.meta && account.meta.passwordHint const passwordHintText = this.getPasswordHint();
? (<div><span>(hint) </span>{ account.meta.passwordHint }</div>) const passwordHint = passwordHintText
? (<div><span>(hint) </span>{ passwordHintText }</div>)
: null; : null;
const isWalletOk = !isExternal || (walletError === null && wallet !== null); const isWalletOk = !isExternal || (walletError === null && wallet !== null);
@ -170,12 +185,26 @@ class TransactionPendingFormConfirm extends Component {
} }
onKeySelect = (event) => { 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(); const fileReader = new FileReader();
fileReader.onload = (e) => { fileReader.onload = (e) => {
try { try {
const wallet = JSON.parse(e.target.result); const wallet = JSON.parse(e.target.result);
try {
if (wallet && typeof wallet.meta === 'string') {
wallet.meta = JSON.parse(wallet.meta);
}
} catch (e) {}
this.setState({ this.setState({
wallet, wallet,
walletError: null walletError: null

View File

@ -18,7 +18,6 @@ import React, { PropTypes, Component } from 'react';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import { MenuItem, Toggle } from 'material-ui'; import { MenuItem, Toggle } from 'material-ui';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import CircularProgress from 'material-ui/CircularProgress'; import CircularProgress from 'material-ui/CircularProgress';
import moment from 'moment'; import moment from 'moment';
import { throttle } from 'lodash'; 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 { Actionbar, ActionbarExport, ActionbarImport, Button, Editor, Page, Select, Input } from '~/ui';
import { DeployContract, SaveContract, LoadContract } from '~/modals'; import { DeployContract, SaveContract, LoadContract } from '~/modals';
import { setupWorker } from '~/redux/providers/compilerActions';
import WriteContractStore from './writeContractStore'; import WriteContractStore from './writeContractStore';
import styles from './writeContract.css'; import styles from './writeContract.css';
@ -42,7 +39,6 @@ class WriteContract extends Component {
static propTypes = { static propTypes = {
accounts: PropTypes.object.isRequired, accounts: PropTypes.object.isRequired,
setupWorker: PropTypes.func.isRequired,
worker: PropTypes.object, worker: PropTypes.object,
workerError: PropTypes.any workerError: PropTypes.any
}; };
@ -55,8 +51,7 @@ class WriteContract extends Component {
}; };
componentWillMount () { componentWillMount () {
const { setupWorker, worker } = this.props; const { worker } = this.props;
setupWorker();
if (worker !== undefined) { if (worker !== undefined) {
this.store.setWorker(worker); this.store.setWorker(worker);
@ -575,17 +570,10 @@ class WriteContract extends Component {
function mapStateToProps (state) { function mapStateToProps (state) {
const { accounts } = state.personal; const { accounts } = state.personal;
const { worker, error } = state.compiler; const { worker, error } = state.worker;
return { accounts, worker, workerError: error }; return { accounts, worker, workerError: error };
} }
function mapDispatchToProps (dispatch) {
return bindActionCreators({
setupWorker
}, dispatch);
}
export default connect( export default connect(
mapStateToProps, mapStateToProps
mapDispatchToProps
)(WriteContract); )(WriteContract);