Perf and fixes

This commit is contained in:
maciejhirsz 2017-04-03 18:50:11 +02:00
parent ee4f9da385
commit 50e0221dd1
12 changed files with 324 additions and 129 deletions

View File

@ -176,7 +176,7 @@
"geopattern": "1.2.3",
"isomorphic-fetch": "2.2.1",
"js-sha3": "0.5.5",
"keythereum": "0.4.3",
"keythereum": "0.4.6",
"lodash": "4.17.2",
"loglevel": "1.4.1",
"marked": "0.3.6",

View File

@ -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 { keythereum } from '../ethkey';
import { createKeyObject, decryptPrivateKey } from '../ethkey';
export default class Account {
constructor (persist, data) {
@ -31,12 +31,14 @@ export default class Account {
}
isValidPassword (password) {
try {
keythereum.recover(Buffer.from(password), this._keyObject);
return true;
} catch (e) {
return false;
}
return decryptPrivateKey(this._keyObject, password)
.then((privateKey) => {
if (!privateKey) {
return false;
}
return true;
});
}
get address () {
@ -68,21 +70,23 @@ export default class Account {
}
decryptPrivateKey (password) {
return keythereum.recover(Buffer.from(password), this._keyObject);
return decryptPrivateKey(this._keyObject, password);
}
changePassword (key, password) {
return createKeyObject(key, password).then((keyObject) => {
this._keyObject = keyObject;
this._persist();
});
}
static fromPrivateKey (persist, key, password) {
const iv = keythereum.crypto.randomBytes(16);
const salt = keythereum.crypto.randomBytes(32);
return createKeyObject(key, password).then((keyObject) => {
const account = new Account(persist, { keyObject });
// Keythereum will fail if `password` is an empty string
password = Buffer.from(password);
const keyObject = keythereum.dump(password, key, salt, iv);
const account = new Account(persist, { keyObject });
return account;
return account;
});
}
toJSON () {

View File

@ -38,14 +38,22 @@ export default class Accounts {
create (secret, password) {
const privateKey = Buffer.from(secret.slice(2), 'hex');
const account = Account.fromPrivateKey(this.persist, privateKey, password);
this._store.push(account);
this.lastAddress = account.address;
return Account.fromPrivateKey(this.persist, privateKey, password)
.then((account) => {
const { address } = account;
this.persist();
if (this._store.find((account) => account.address === address)) {
throw new Error(`Account ${address} already exists!`);
}
return account.address;
this._store.push(account);
this.lastAddress = address;
this.persist();
return account.address;
});
}
set lastAddress (value) {
@ -73,28 +81,40 @@ export default class Accounts {
remove (address, password) {
address = address.toLowerCase();
const account = this.get(address);
if (!account) {
return false;
}
return account.isValidPassword(password)
.then((isValid) => {
if (!isValid) {
return false;
}
if (address === this.lastAddress) {
this.lastAddress = NULL_ADDRESS;
}
this.removeUnsafe(address);
return true;
});
}
removeUnsafe (address) {
address = address.toLowerCase();
const index = this._store.findIndex((account) => account.address === address);
if (index === -1) {
return false;
}
const account = this._store[index];
if (!account.isValidPassword(password)) {
console.log('invalid password');
return false;
}
if (address === this.lastAddress) {
this.lastAddress = NULL_ADDRESS;
return;
}
this._store.splice(index, 1);
this.persist();
return true;
}
mapArray (mapper) {

View File

@ -14,31 +14,34 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
// Allow a web worker in the browser, with a fallback for Node.js
const hasWebWorkers = typeof Worker !== 'undefined';
const KeyWorker = hasWebWorkers ? require('worker-loader!./worker')
: require('./worker').KeyWorker;
import workerPool from './workerPool';
// Local accounts should never be used outside of the browser
export let keythereum = null;
export function createKeyObject (key, password) {
return workerPool.getWorker().action('createKeyObject', { key, password })
.then((obj) => JSON.parse(obj));
}
if (hasWebWorkers) {
require('keythereum/dist/keythereum');
export function decryptPrivateKey (keyObject, password) {
return workerPool.getWorker()
.action('decryptPrivateKey', { keyObject, password })
.then((privateKey) => {
if (privateKey) {
return Buffer.from(privateKey);
}
keythereum = window.keythereum;
return null;
});
}
export function phraseToAddress (phrase) {
return phraseToWallet(phrase).then((wallet) => wallet.address);
return phraseToWallet(phrase)
.then((wallet) => wallet.address);
}
export function phraseToWallet (phrase) {
return new Promise((resolve, reject) => {
const worker = new KeyWorker();
worker.postMessage(phrase);
worker.onmessage = ({ data }) => {
resolve(data);
};
});
return workerPool.getWorker().action('phraseToWallet', phrase);
}
export function verifySecret (secret) {
return workerPool.getWorker().action('verifySecret', secret);
}

View File

@ -14,58 +14,107 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { keccak_256 as keccak256 } from 'js-sha3';
import secp256k1 from 'secp256k1/js';
import { keccak_256 as keccak256 } from 'js-sha3';
const isWorker = typeof self !== 'undefined';
// Stay compatible between environments
if (typeof self !== 'object') {
if (!isWorker) {
const scope = typeof global === 'undefined' ? window : global;
scope.self = scope;
}
// keythereum should never be used outside of the browser
let keythereum = null;
if (isWorker) {
require('keythereum/dist/keythereum');
keythereum = self.keythereum;
}
function route ({ action, payload }) {
if (action in actions) {
return actions[action](payload);
}
return null;
}
const actions = {
phraseToWallet (phrase) {
let secret = keccak256.array(phrase);
for (let i = 0; i < 16384; i++) {
secret = keccak256.array(secret);
}
while (true) {
secret = keccak256.array(secret);
const secretBuf = Buffer.from(secret);
if (secp256k1.privateKeyVerify(secretBuf)) {
// No compression, slice out last 64 bytes
const publicBuf = secp256k1.publicKeyCreate(secretBuf, false).slice(-64);
const address = keccak256.array(publicBuf).slice(12);
if (address[0] !== 0) {
continue;
}
const wallet = {
secret: bytesToHex(secretBuf),
public: bytesToHex(publicBuf),
address: bytesToHex(address)
};
return wallet;
}
}
},
verifySecret (secret) {
const key = Buffer.from(secret.slice(2), 'hex');
return secp256k1.privateKeyVerify(key);
},
createKeyObject ({ key, password }) {
key = Buffer.from(key);
password = Buffer.from(password);
const iv = keythereum.crypto.randomBytes(16);
const salt = keythereum.crypto.randomBytes(32);
const keyObject = keythereum.dump(password, key, salt, iv);
return JSON.stringify(keyObject);
},
decryptPrivateKey ({ keyObject, password }) {
password = Buffer.from(password);
try {
const key = keythereum.recover(password, keyObject);
// Convert to array to safely send from the worker
return Array.from(key);
} catch (e) {
return null;
}
}
};
function bytesToHex (bytes) {
return '0x' + Array.from(bytes).map(n => ('0' + n.toString(16)).slice(-2)).join('');
}
// Logic ported from /ethkey/src/brain.rs
function phraseToWallet (phrase) {
let secret = keccak256.array(phrase);
for (let i = 0; i < 16384; i++) {
secret = keccak256.array(secret);
}
while (true) {
secret = keccak256.array(secret);
const secretBuf = Buffer.from(secret);
if (secp256k1.privateKeyVerify(secretBuf)) {
// No compression, slice out last 64 bytes
const publicBuf = secp256k1.publicKeyCreate(secretBuf, false).slice(-64);
const address = keccak256.array(publicBuf).slice(12);
if (address[0] !== 0) {
continue;
}
const wallet = {
secret: bytesToHex(secretBuf),
public: bytesToHex(publicBuf),
address: bytesToHex(address)
};
return wallet;
}
}
}
self.onmessage = function ({ data }) {
const wallet = phraseToWallet(data);
const result = route(data);
postMessage(wallet);
close();
postMessage(result);
};
// Emulate a web worker in Node.js
@ -73,9 +122,9 @@ class KeyWorker {
postMessage (data) {
// Force async
setTimeout(() => {
const wallet = phraseToWallet(data);
const result = route(data);
this.onmessage({ data: wallet });
this.onmessage({ data: result });
}, 0);
}

View File

@ -0,0 +1,61 @@
// Copyright 2015-2017 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/>.
// Allow a web worker in the browser, with a fallback for Node.js
const hasWebWorkers = typeof Worker !== 'undefined';
const KeyWorker = hasWebWorkers ? require('worker-loader!./worker')
: require('./worker').KeyWorker;
class WorkerContainer {
busy = false;
_worker = new KeyWorker();
action (action, payload) {
if (this.busy) {
throw new Error('Cannot issue an action on a busy worker!');
}
this.busy = true;
return new Promise((resolve, reject) => {
this._worker.postMessage({ action, payload });
this._worker.onmessage = ({ data }) => {
this.busy = false;
resolve(data);
};
});
}
}
class WorkerPool {
pool = [];
getWorker () {
let container = this.pool.find((container) => !container.busy);
if (container) {
return container;
}
container = new WorkerContainer();
this.pool.push(container);
return container;
}
}
export default new WorkerPool();

View File

@ -19,7 +19,7 @@ import accounts from './accounts';
import transactions from './transactions';
import { Middleware } from '../transport';
import { inNumber16 } from '../format/input';
import { phraseToWallet, phraseToAddress } from './ethkey';
import { phraseToWallet, phraseToAddress, verifySecret } from './ethkey';
import { randomPhrase } from '@parity/wordlist';
export default class LocalAccountsMiddleware extends Middleware {
@ -57,6 +57,21 @@ export default class LocalAccountsMiddleware extends Middleware {
});
});
register('parity_changePassword', ([address, oldPassword, newPassword]) => {
const account = accounts.get(address);
return account.decryptPrivateKey(oldPassword)
.then((privateKey) => {
if (!privateKey) {
return false;
}
account.changePassword(privateKey, newPassword);
return true;
});
});
register('parity_checkRequest', ([id]) => {
return transactions.hash(id) || Promise.resolve(null);
});
@ -84,6 +99,17 @@ export default class LocalAccountsMiddleware extends Middleware {
});
});
register('parity_newAccountFromSecret', ([secret, password]) => {
return verifySecret(secret)
.then((isValid) => {
if (!isValid) {
throw new Error('Invalid secret key');
}
return accounts.create(secret, password);
});
});
register('parity_setAccountMeta', ([address, meta]) => {
accounts.get(address).meta = meta;
@ -127,6 +153,12 @@ export default class LocalAccountsMiddleware extends Middleware {
return accounts.remove(address, password);
});
register('parity_testPassword', ([address, password]) => {
const account = accounts.get(address);
return account.isValidPassword(password);
});
register('signer_confirmRequest', ([id, modify, password]) => {
const {
gasPrice,
@ -137,30 +169,33 @@ export default class LocalAccountsMiddleware extends Middleware {
data
} = Object.assign(transactions.get(id), modify);
return this
.rpcRequest('parity_nextNonce', [from])
.then((nonce) => {
const tx = new EthereumTx({
nonce,
to,
data,
gasLimit: inNumber16(gasLimit),
gasPrice: inNumber16(gasPrice),
value: inNumber16(value)
});
const account = accounts.get(from);
const account = accounts.get(from);
tx.sign(account.decryptPrivateKey(password));
const serializedTx = `0x${tx.serialize().toString('hex')}`;
return this.rpcRequest('eth_sendRawTransaction', [serializedTx]);
})
.then((hash) => {
transactions.confirm(id, hash);
return {};
return Promise.all([
this.rpcRequest('parity_nextNonce', [from]),
account.decryptPrivateKey(password)
])
.then(([nonce, privateKey]) => {
const tx = new EthereumTx({
nonce,
to,
data,
gasLimit: inNumber16(gasLimit),
gasPrice: inNumber16(gasPrice),
value: inNumber16(value)
});
tx.sign(privateKey);
const serializedTx = `0x${tx.serialize().toString('hex')}`;
return this.rpcRequest('eth_sendRawTransaction', [serializedTx]);
})
.then((hash) => {
transactions.confirm(id, hash);
return {};
});
});
register('signer_rejectRequest', ([id]) => {

View File

@ -80,12 +80,16 @@ export default class JsonRpcBase extends EventEmitter {
const res = middleware.handle(method, params);
if (res != null) {
const result = this._wrapSuccessResult(res);
const json = this.encode(method, params);
// If `res` isn't a promise, we need to wrap it
return Promise.resolve(res)
.then((res) => {
const result = this._wrapSuccessResult(res);
const json = this.encode(method, params);
Logging.send(method, params, { json, result });
Logging.send(method, params, { json, result });
return res;
return res;
});
}
}

View File

@ -23,6 +23,7 @@ import { RadioButton, RadioButtonGroup } from 'material-ui/RadioButton';
import { Form, Input, IdentityIcon } from '~/ui';
import PasswordStrength from '~/ui/Form/PasswordStrength';
import { RefreshIcon } from '~/ui/Icons';
import Loading from '~/ui/Loading';
import ChangeVault from '../ChangeVault';
import styles from '../createAccount.css';
@ -170,7 +171,9 @@ export default class CreateAccount extends Component {
const { accounts } = this.state;
if (!accounts) {
return null;
return (
<Loading className={ styles.selector } size={ 1 } />
);
}
const identities = Object
@ -205,6 +208,14 @@ export default class CreateAccount extends Component {
createIdentities = () => {
const { createStore } = this.props;
this.setState({
accounts: null,
selectedAddress: ''
});
createStore.setAddress('');
createStore.setPhrase('');
return createStore
.createIdentities()
.then((accounts) => {

View File

@ -58,12 +58,13 @@ describe('modals/CreateAccount/NewAccount', () => {
return instance.componentWillMount();
});
it('creates initial accounts', () => {
expect(Object.keys(instance.state.accounts).length).to.equal(7);
it('resets the accounts', () => {
expect(instance.state.accounts).to.be.null;
// expect(Object.keys(instance.state.accounts).length).to.equal(7);
});
it('sets the initial selected value', () => {
expect(instance.state.selectedAddress).to.equal(Object.keys(instance.state.accounts)[0]);
it('resets the initial selected value', () => {
expect(instance.state.selectedAddress).to.equal('');
});
});
});

View File

@ -69,7 +69,7 @@ export default class Store {
return !(this.nameError || this.walletFileError);
case 'fromNew':
return !(this.nameError || this.passwordRepeatError);
return !(this.nameError || this.passwordRepeatError) && this.hasAddress;
case 'fromPhrase':
return !(this.nameError || this.passwordRepeatError);
@ -85,6 +85,10 @@ export default class Store {
}
}
@computed get hasAddress () {
return !!(this.address);
}
@computed get passwordRepeatError () {
return this.password === this.passwordRepeat
? null

View File

@ -329,6 +329,7 @@ describe('modals/CreateAccount/Store', () => {
describe('createType === fromNew', () => {
beforeEach(() => {
store.setCreateType('fromNew');
store.setAddress('0x0000000000000000000000000000000000000000');
});
it('returns true on no errors', () => {
@ -337,11 +338,13 @@ describe('modals/CreateAccount/Store', () => {
it('returns false on nameError', () => {
store.setName('');
expect(store.canCreate).to.be.false;
});
it('returns false on passwordRepeatError', () => {
store.setPassword('testing');
expect(store.canCreate).to.be.false;
});
});