diff --git a/js/package.json b/js/package.json
index 82807e820..0585a1681 100644
--- a/js/package.json
+++ b/js/package.json
@@ -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",
diff --git a/js/src/api/local/accounts/account.js b/js/src/api/local/accounts/account.js
index da9de1359..94e923f45 100644
--- a/js/src/api/local/accounts/account.js
+++ b/js/src/api/local/accounts/account.js
@@ -14,7 +14,7 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see .
-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 () {
diff --git a/js/src/api/local/accounts/accounts.js b/js/src/api/local/accounts/accounts.js
index 576addcb1..e7e5cc397 100644
--- a/js/src/api/local/accounts/accounts.js
+++ b/js/src/api/local/accounts/accounts.js
@@ -38,14 +38,23 @@ 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 +82,41 @@ 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) {
diff --git a/js/src/api/local/ethkey/index.js b/js/src/api/local/ethkey/index.js
index ac2efa72e..a6967da25 100644
--- a/js/src/api/local/ethkey/index.js
+++ b/js/src/api/local/ethkey/index.js
@@ -14,31 +14,35 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see .
-// 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);
}
diff --git a/js/src/api/local/ethkey/worker.js b/js/src/api/local/ethkey/worker.js
index a472ee29a..00f4a0bed 100644
--- a/js/src/api/local/ethkey/worker.js
+++ b/js/src/api/local/ethkey/worker.js
@@ -14,58 +14,104 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see .
-import { keccak_256 as keccak256 } from 'js-sha3';
import secp256k1 from 'secp256k1/js';
+import { keccak_256 as keccak256 } from 'js-sha3';
+import { bytesToHex } from '~/api/util/format';
+
+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;
}
-function bytesToHex (bytes) {
- return '0x' + Array.from(bytes).map(n => ('0' + n.toString(16)).slice(-2)).join('');
+// keythereum should never be used outside of the browser
+let keythereum = null;
+
+if (isWorker) {
+ require('keythereum/dist/keythereum');
+
+ keythereum = self.keythereum;
}
-// 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);
+function route ({ action, payload }) {
+ if (action in actions) {
+ return actions[action](payload);
}
- while (true) {
- secret = keccak256.array(secret);
+ return null;
+}
- const secretBuf = Buffer.from(secret);
+const actions = {
+ phraseToWallet (phrase) {
+ let secret = keccak256.array(phrase);
- 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);
+ for (let i = 0; i < 16384; i++) {
+ secret = keccak256.array(secret);
+ }
- if (address[0] !== 0) {
- continue;
+ 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;
}
+ }
+ },
- const wallet = {
- secret: bytesToHex(secretBuf),
- public: bytesToHex(publicBuf),
- address: bytesToHex(address)
- };
+ verifySecret (secret) {
+ const key = Buffer.from(secret.slice(2), 'hex');
- return wallet;
+ 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;
}
}
-}
+};
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 +119,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);
}
diff --git a/js/src/api/local/ethkey/workerPool.js b/js/src/api/local/ethkey/workerPool.js
new file mode 100644
index 000000000..ff5315898
--- /dev/null
+++ b/js/src/api/local/ethkey/workerPool.js
@@ -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 .
+
+// 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();
diff --git a/js/src/api/local/middleware.js b/js/src/api/local/middleware.js
index d5997c60a..36a8cd2cf 100644
--- a/js/src/api/local/middleware.js
+++ b/js/src/api/local/middleware.js
@@ -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,22 @@ 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 +100,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 +154,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 +170,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]) => {
diff --git a/js/src/api/transport/jsonRpcBase.js b/js/src/api/transport/jsonRpcBase.js
index 46df718a7..573204c3e 100644
--- a/js/src/api/transport/jsonRpcBase.js
+++ b/js/src/api/transport/jsonRpcBase.js
@@ -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;
+ });
}
}
diff --git a/js/src/api/util/format.js b/js/src/api/util/format.js
index c7594b692..61fc9d32c 100644
--- a/js/src/api/util/format.js
+++ b/js/src/api/util/format.js
@@ -17,7 +17,7 @@
import { range } from 'lodash';
export function bytesToHex (bytes) {
- return '0x' + bytes.map((b) => ('0' + b.toString(16)).slice(-2)).join('');
+ return '0x' + Buffer.from(bytes).toString('hex');
}
export function cleanupValue (value, type) {
diff --git a/js/src/modals/CreateAccount/NewAccount/newAccount.js b/js/src/modals/CreateAccount/NewAccount/newAccount.js
index 04f2f272a..9c6be9f6e 100644
--- a/js/src/modals/CreateAccount/NewAccount/newAccount.js
+++ b/js/src/modals/CreateAccount/NewAccount/newAccount.js
@@ -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 (
+
+ );
}
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) => {
diff --git a/js/src/modals/CreateAccount/NewAccount/newAccount.spec.js b/js/src/modals/CreateAccount/NewAccount/newAccount.spec.js
index 87c7ba3fc..935fe5b80 100644
--- a/js/src/modals/CreateAccount/NewAccount/newAccount.spec.js
+++ b/js/src/modals/CreateAccount/NewAccount/newAccount.spec.js
@@ -58,12 +58,12 @@ 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;
});
- 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('');
});
});
});
diff --git a/js/src/modals/CreateAccount/store.js b/js/src/modals/CreateAccount/store.js
index 52dddac80..9bc60d9af 100644
--- a/js/src/modals/CreateAccount/store.js
+++ b/js/src/modals/CreateAccount/store.js
@@ -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
diff --git a/js/src/modals/CreateAccount/store.spec.js b/js/src/modals/CreateAccount/store.spec.js
index b02f013b6..9d7bc10a2 100644
--- a/js/src/modals/CreateAccount/store.spec.js
+++ b/js/src/modals/CreateAccount/store.spec.js
@@ -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;
});
});