In-browser signing support (#3231)
* Signer RAW confirmations * Returning address book as eth_accounts * UI support for in-browser signing * Post review fixes * Adding new methods to jsonrpc * Fixing eth_accounts * Deterministic accounts ordering
This commit is contained in:
@@ -118,6 +118,7 @@
|
||||
"bytes": "^2.4.0",
|
||||
"chart.js": "^2.3.0",
|
||||
"es6-promise": "^3.2.1",
|
||||
"ethereumjs-tx": "^1.1.2",
|
||||
"file-saver": "^1.3.3",
|
||||
"format-json": "^1.0.3",
|
||||
"format-number": "^2.0.1",
|
||||
@@ -147,6 +148,7 @@
|
||||
"redux-actions": "^0.10.1",
|
||||
"redux-thunk": "^2.1.0",
|
||||
"rlp": "^2.0.0",
|
||||
"scryptsy": "^2.0.0",
|
||||
"store": "^1.3.20",
|
||||
"utf8": "^2.1.1",
|
||||
"validator": "^5.7.0",
|
||||
|
||||
@@ -93,6 +93,10 @@ export function inFilter (options) {
|
||||
}
|
||||
|
||||
export function inHex (str) {
|
||||
if (str && str.toString) {
|
||||
str = str.toString(16);
|
||||
}
|
||||
|
||||
if (str && str.substr(0, 2) === '0x') {
|
||||
return str.toLowerCase();
|
||||
}
|
||||
|
||||
@@ -181,6 +181,12 @@ export default class Parity {
|
||||
.then(outAddress);
|
||||
}
|
||||
|
||||
nextNonce (account) {
|
||||
return this._transport
|
||||
.execute('parity_nextNonce', inAddress(account))
|
||||
.then(outNumber);
|
||||
}
|
||||
|
||||
nodeName () {
|
||||
return this._transport
|
||||
.execute('parity_nodeName');
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { inNumber16 } from '../../format/input';
|
||||
import { inNumber16, inData } from '../../format/input';
|
||||
import { outSignerRequest } from '../../format/output';
|
||||
|
||||
export default class Signer {
|
||||
@@ -27,6 +27,11 @@ export default class Signer {
|
||||
.execute('signer_confirmRequest', inNumber16(requestId), options, password);
|
||||
}
|
||||
|
||||
confirmRequestRaw (requestId, data) {
|
||||
return this._transport
|
||||
.execute('signer_confirmRequestRaw', inNumber16(requestId), inData(data));
|
||||
}
|
||||
|
||||
generateAuthorizationToken () {
|
||||
return this._transport
|
||||
.execute('signer_generateAuthorizationToken');
|
||||
|
||||
@@ -356,6 +356,20 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
nextNonce: {
|
||||
desc: 'Returns next available nonce for transaction from given account. Includes pending block and transaction queue.',
|
||||
params: [
|
||||
{
|
||||
type: Address,
|
||||
desc: 'Account'
|
||||
}
|
||||
],
|
||||
returns: {
|
||||
type: Quantity,
|
||||
desc: 'Next valid nonce'
|
||||
}
|
||||
},
|
||||
|
||||
nodeName: {
|
||||
desc: 'Returns node name (identity)',
|
||||
params: [],
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { Quantity } from '../types';
|
||||
import { Quantity, Data } from '../types';
|
||||
|
||||
export default {
|
||||
generateAuthorizationToken: {
|
||||
@@ -57,6 +57,24 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
confirmRequestRaw: {
|
||||
desc: 'Confirm a request in the signer queue providing signed request.',
|
||||
params: [
|
||||
{
|
||||
type: Quantity,
|
||||
desc: 'The request id'
|
||||
},
|
||||
{
|
||||
type: Data,
|
||||
desc: 'Signed request (transaction RLP)'
|
||||
}
|
||||
],
|
||||
returns: {
|
||||
type: Boolean,
|
||||
desc: 'The status of the confirmation'
|
||||
}
|
||||
},
|
||||
|
||||
rejectRequest: {
|
||||
desc: 'Rejects a request in the signer queue',
|
||||
params: [
|
||||
|
||||
@@ -16,6 +16,9 @@
|
||||
|
||||
import * as actions from './signerActions';
|
||||
|
||||
import { inHex } from '../../api/format/input';
|
||||
import { Wallet } from '../../util/wallet';
|
||||
|
||||
export default class SignerMiddleware {
|
||||
constructor (api) {
|
||||
this._api = api;
|
||||
@@ -49,23 +52,58 @@ export default class SignerMiddleware {
|
||||
}
|
||||
|
||||
onConfirmStart = (store, action) => {
|
||||
const { id, password } = action.payload;
|
||||
const { id, password, wallet, payload } = action.payload;
|
||||
|
||||
this._api.signer
|
||||
.confirmRequest(id, {}, password)
|
||||
.then((txHash) => {
|
||||
console.log('confirmRequest', id, txHash);
|
||||
if (!txHash) {
|
||||
store.dispatch(actions.errorConfirmRequest({ id, err: 'Unable to confirm.' }));
|
||||
return;
|
||||
const handlePromise = promise => {
|
||||
promise
|
||||
.then((txHash) => {
|
||||
console.log('confirmRequest', id, txHash);
|
||||
if (!txHash) {
|
||||
store.dispatch(actions.errorConfirmRequest({ id, err: 'Unable to confirm.' }));
|
||||
return;
|
||||
}
|
||||
|
||||
store.dispatch(actions.successConfirmRequest({ id, txHash }));
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('confirmRequest', id, error);
|
||||
store.dispatch(actions.errorConfirmRequest({ id, err: error.message }));
|
||||
});
|
||||
};
|
||||
|
||||
// Sign request in-browser
|
||||
if (wallet && payload.transaction) {
|
||||
const { transaction } = payload;
|
||||
|
||||
(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)
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
handlePromise(this._api.signer.confirmRequestRaw(id, rawTx));
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
store.dispatch(actions.errorConfirmRequest({ id, err: error.message }));
|
||||
}
|
||||
|
||||
store.dispatch(actions.successConfirmRequest({ id, txHash }));
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('confirmRequest', id, error);
|
||||
store.dispatch(actions.errorConfirmRequest({ id, err: error.message }));
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
handlePromise(this._api.signer.confirmRequest(id, {}, password));
|
||||
}
|
||||
|
||||
onRejectStart = (store, action) => {
|
||||
|
||||
82
js/src/util/wallet.js
Normal file
82
js/src/util/wallet.js
Normal file
@@ -0,0 +1,82 @@
|
||||
// Copyright 2015, 2016 Ethcore (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 scrypt from 'scryptsy';
|
||||
import Transaction from 'ethereumjs-tx';
|
||||
import { pbkdf2Sync } from 'crypto';
|
||||
import { createDecipheriv } from 'browserify-aes';
|
||||
|
||||
import { inHex } from '../api/format/input';
|
||||
import { sha3 } from '../api/util/sha3';
|
||||
|
||||
// Adapted from https://github.com/kvhnuke/etherwallet/blob/mercury/app/scripts/myetherwallet.js
|
||||
|
||||
export class Wallet {
|
||||
|
||||
static fromJson (json, password) {
|
||||
if (json.version !== 3) {
|
||||
throw new Error('Only V3 wallets are supported');
|
||||
}
|
||||
|
||||
const { kdf } = json.crypto;
|
||||
const kdfparams = json.crypto.kdfparams || {};
|
||||
const pwd = Buffer.from(password);
|
||||
const salt = Buffer.from(kdfparams.salt, 'hex');
|
||||
let derivedKey;
|
||||
|
||||
if (kdf === 'scrypt') {
|
||||
derivedKey = scrypt(pwd, salt, kdfparams.n, kdfparams.r, kdfparams.p, kdfparams.dklen);
|
||||
} else if (kdf === 'pbkdf2') {
|
||||
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]));
|
||||
if (mac !== inHex(json.crypto.mac)) {
|
||||
throw new Error('Key derivation failed - possibly wrong passphrase');
|
||||
}
|
||||
|
||||
const decipher = createDecipheriv(
|
||||
json.crypto.cipher,
|
||||
derivedKey.slice(0, 16),
|
||||
Buffer.from(json.crypto.cipherparams.iv, 'hex')
|
||||
);
|
||||
let seed = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
||||
|
||||
while (seed.length < 32) {
|
||||
const nullBuff = Buffer.from([0x00]);
|
||||
seed = Buffer.concat([nullBuff, seed]);
|
||||
}
|
||||
|
||||
return new Wallet(seed);
|
||||
}
|
||||
|
||||
constructor (seed) {
|
||||
this.seed = seed;
|
||||
}
|
||||
|
||||
signTransaction (transaction) {
|
||||
const tx = new Transaction(transaction);
|
||||
tx.sign(this.seed);
|
||||
return inHex(tx.serialize().toString('hex'));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -33,15 +33,22 @@ export default class RequestPendingWeb3 extends Component {
|
||||
className: PropTypes.string
|
||||
};
|
||||
|
||||
onConfirm = data => {
|
||||
const { onConfirm, payload } = this.props;
|
||||
|
||||
data.payload = payload;
|
||||
onConfirm(data);
|
||||
};
|
||||
|
||||
render () {
|
||||
const { payload, id, className, isSending, date, onConfirm, onReject } = this.props;
|
||||
const { payload, id, className, isSending, date, onReject } = this.props;
|
||||
|
||||
if (payload.sign) {
|
||||
const { sign } = payload;
|
||||
return (
|
||||
<SignRequest
|
||||
className={ className }
|
||||
onConfirm={ onConfirm }
|
||||
onConfirm={ this.onConfirm }
|
||||
onReject={ onReject }
|
||||
isSending={ isSending }
|
||||
isFinished={ false }
|
||||
@@ -57,7 +64,7 @@ export default class RequestPendingWeb3 extends Component {
|
||||
return (
|
||||
<TransactionPending
|
||||
className={ className }
|
||||
onConfirm={ onConfirm }
|
||||
onConfirm={ this.onConfirm }
|
||||
onReject={ onReject }
|
||||
isSending={ isSending }
|
||||
id={ id }
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
|
||||
.mainContainer > * {
|
||||
vertical-align: middle;
|
||||
min-height: 120px;
|
||||
min-height: 190px;
|
||||
}
|
||||
|
||||
.inputs {
|
||||
|
||||
@@ -116,9 +116,11 @@ export default class TransactionPending extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
onConfirm = password => {
|
||||
onConfirm = data => {
|
||||
const { id, gasPrice } = this.props;
|
||||
this.props.onConfirm({ id, password, gasPrice });
|
||||
const { password, wallet } = data;
|
||||
|
||||
this.props.onConfirm({ id, password, wallet, gasPrice });
|
||||
}
|
||||
|
||||
onReject = () => {
|
||||
|
||||
@@ -40,3 +40,7 @@
|
||||
.passwordHint span {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.fileInput input {
|
||||
top: 22px;
|
||||
}
|
||||
|
||||
@@ -35,26 +35,33 @@ class TransactionPendingFormConfirm extends Component {
|
||||
id = Math.random(); // for tooltip
|
||||
|
||||
state = {
|
||||
walletError: null,
|
||||
wallet: null,
|
||||
password: ''
|
||||
}
|
||||
|
||||
render () {
|
||||
const { accounts, address, isSending } = this.props;
|
||||
const { password } = this.state;
|
||||
const { password, walletError, wallet } = this.state;
|
||||
const account = accounts[address] || {};
|
||||
const isExternal = !account.uuid;
|
||||
|
||||
const passwordHint = account.meta && account.meta.passwordHint
|
||||
? (<div><span>(hint) </span>{ account.meta.passwordHint }</div>)
|
||||
: null;
|
||||
|
||||
const isWalletOk = !isExternal || (walletError === null && wallet !== null);
|
||||
const keyInput = isExternal ? this.renderKeyInput() : null;
|
||||
|
||||
return (
|
||||
<div className={ styles.confirmForm }>
|
||||
<Form>
|
||||
{ keyInput }
|
||||
<Input
|
||||
onChange={ this.onModifyPassword }
|
||||
onKeyDown={ this.onKeyDown }
|
||||
label='Account Password'
|
||||
hint='unlock the account'
|
||||
label={ isExternal ? 'Key Password' : 'Account Password' }
|
||||
hint={ isExternal ? 'decrypt the key' : 'unlock the account' }
|
||||
type='password'
|
||||
value={ password } />
|
||||
<div className={ styles.passwordHint }>
|
||||
@@ -71,7 +78,7 @@ class TransactionPendingFormConfirm extends Component {
|
||||
className={ styles.confirmButton }
|
||||
fullWidth
|
||||
primary
|
||||
disabled={ isSending }
|
||||
disabled={ isSending || !isWalletOk }
|
||||
icon={ <IdentityIcon address={ address } button className={ styles.signerIcon } /> }
|
||||
label={ isSending ? 'Confirming...' : 'Confirm Transaction' }
|
||||
/>
|
||||
@@ -82,6 +89,20 @@ class TransactionPendingFormConfirm extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
renderKeyInput () {
|
||||
const { walletError } = this.state;
|
||||
|
||||
return (
|
||||
<Input
|
||||
className={ styles.fileInput }
|
||||
onChange={ this.onKeySelect }
|
||||
error={ walletError }
|
||||
label='Select Local Key'
|
||||
type='file'
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderTooltip () {
|
||||
if (this.state.password.length) {
|
||||
return;
|
||||
@@ -94,6 +115,26 @@ class TransactionPendingFormConfirm extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
onKeySelect = evt => {
|
||||
const fileReader = new FileReader();
|
||||
fileReader.onload = e => {
|
||||
try {
|
||||
const wallet = JSON.parse(e.target.result);
|
||||
this.setState({
|
||||
walletError: null,
|
||||
wallet: wallet
|
||||
});
|
||||
} catch (e) {
|
||||
this.setState({
|
||||
walletError: 'Given wallet file is invalid.',
|
||||
wallet: null
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
fileReader.readAsText(evt.target.files[0]);
|
||||
}
|
||||
|
||||
onModifyPassword = evt => {
|
||||
const password = evt.target.value;
|
||||
this.setState({
|
||||
@@ -102,8 +143,11 @@ class TransactionPendingFormConfirm extends Component {
|
||||
}
|
||||
|
||||
onConfirm = () => {
|
||||
const { password } = this.state;
|
||||
this.props.onConfirm(password);
|
||||
const { password, wallet } = this.state;
|
||||
|
||||
this.props.onConfirm({
|
||||
password, wallet
|
||||
});
|
||||
}
|
||||
|
||||
onKeyDown = evt => {
|
||||
|
||||
@@ -22,6 +22,7 @@ const DEST = process.env.BUILD_DEST || '.build';
|
||||
|
||||
let modules = [
|
||||
'babel-polyfill',
|
||||
'browserify-aes', 'ethereumjs-tx', 'scryptsy',
|
||||
'react', 'react-dom', 'react-redux', 'react-router',
|
||||
'redux', 'redux-thunk', 'react-router-redux',
|
||||
'lodash', 'material-ui', 'moment', 'blockies'
|
||||
|
||||
Reference in New Issue
Block a user