Wallet rpcs (#1898)
* Add wallet RPCs. * Add wordlist file. * Add standard brain wallet tests. * Allow import of JSON wallets. * Address grumble.
This commit is contained in:
parent
c32244ea4a
commit
286b67d54b
3
Cargo.lock
generated
3
Cargo.lock
generated
@ -416,6 +416,7 @@ dependencies = [
|
||||
"ethcore-ipc 1.4.0",
|
||||
"ethcore-util 1.4.0",
|
||||
"ethjson 0.1.0",
|
||||
"ethstore 0.1.0",
|
||||
"ethsync 1.4.0",
|
||||
"json-ipc-server 0.2.4 (git+https://github.com/ethcore/json-ipc-server.git)",
|
||||
"jsonrpc-core 2.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
@ -505,6 +506,8 @@ name = "ethstore"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"ethkey 0.2.0",
|
||||
"itertools 0.4.13 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"lazy_static 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"rust-crypto 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
|
@ -191,6 +191,18 @@ impl AccountProvider {
|
||||
Ok(Address::from(address).into())
|
||||
}
|
||||
|
||||
/// Import a new presale wallet.
|
||||
pub fn import_presale(&self, presale_json: &[u8], password: &str) -> Result<H160, Error> {
|
||||
let address = try!(self.sstore.import_presale(presale_json, password));
|
||||
Ok(Address::from(address).into())
|
||||
}
|
||||
|
||||
/// Import a new presale wallet.
|
||||
pub fn import_wallet(&self, json: &[u8], password: &str) -> Result<H160, Error> {
|
||||
let address = try!(self.sstore.import_wallet(json, password));
|
||||
Ok(Address::from(address).into())
|
||||
}
|
||||
|
||||
/// Returns addresses of all accounts.
|
||||
pub fn accounts(&self) -> Result<Vec<H160>, Error> {
|
||||
let accounts = try!(self.sstore.accounts()).into_iter().map(|a| H160(a.into())).collect();
|
||||
|
@ -5,7 +5,7 @@ authors = ["debris <marek.kotewicz@gmail.com>"]
|
||||
|
||||
[dependencies]
|
||||
rand = "0.3.14"
|
||||
lazy_static = "0.2.1"
|
||||
lazy_static = "0.2"
|
||||
tiny-keccak = "1.0"
|
||||
eth-secp256k1 = { git = "https://github.com/ethcore/rust-secp256k1" }
|
||||
rustc-serialize = "0.3"
|
||||
|
@ -16,6 +16,8 @@ rust-crypto = "0.2.36"
|
||||
tiny-keccak = "1.0"
|
||||
docopt = { version = "0.6", optional = true }
|
||||
time = "0.1.34"
|
||||
lazy_static = "0.2"
|
||||
itertools = "0.4"
|
||||
|
||||
[build-dependencies]
|
||||
serde_codegen = { version = "0.7", optional = true }
|
||||
|
7530
ethstore/res/wordlist.txt
Normal file
7530
ethstore/res/wordlist.txt
Normal file
File diff suppressed because it is too large
Load Diff
@ -66,7 +66,7 @@ impl Into<json::KeyFile> for SafeAccount {
|
||||
json::KeyFile {
|
||||
id: From::from(self.id),
|
||||
version: self.version.into(),
|
||||
address: self.address.into(), //From::from(self.address),
|
||||
address: self.address.into(),
|
||||
crypto: self.crypto.into(),
|
||||
name: Some(self.name.into()),
|
||||
meta: Some(self.meta.into()),
|
||||
@ -150,13 +150,16 @@ impl SafeAccount {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_file(json: json::KeyFile, filename: String) -> Self {
|
||||
/// Create a new `SafeAccount` from the given `json`; if it was read from a
|
||||
/// file, the `filename` should be `Some` name. If it is as yet anonymous, then it
|
||||
/// can be left `None`.
|
||||
pub fn from_file(json: json::KeyFile, filename: Option<String>) -> Self {
|
||||
SafeAccount {
|
||||
id: json.id.into(),
|
||||
version: json.version.into(),
|
||||
address: json.address.into(),
|
||||
crypto: json.crypto.into(),
|
||||
filename: Some(filename),
|
||||
filename: filename,
|
||||
name: json.name.unwrap_or(String::new()),
|
||||
meta: json.meta.unwrap_or("{}".to_owned()),
|
||||
}
|
||||
|
@ -87,7 +87,7 @@ impl DiskDirectory {
|
||||
.zip(paths.into_iter())
|
||||
.map(|(file, path)| match file {
|
||||
Ok(file) => Ok((path.clone(), SafeAccount::from_file(
|
||||
file, path.file_name().and_then(|n| n.to_str()).expect("Keys have valid UTF8 names only.").to_owned()
|
||||
file, Some(path.file_name().and_then(|n| n.to_str()).expect("Keys have valid UTF8 names only.").to_owned())
|
||||
))),
|
||||
Err(err) => Err(Error::InvalidKeyFile(format!("{:?}: {}", path, err))),
|
||||
})
|
||||
|
@ -24,7 +24,9 @@ use ethkey::{Signature, Address, Message, Secret};
|
||||
use dir::KeyDirectory;
|
||||
use account::SafeAccount;
|
||||
use {Error, SecretStore};
|
||||
use json;
|
||||
use json::UUID;
|
||||
use presale::PresaleWallet;
|
||||
|
||||
pub struct EthStore {
|
||||
dir: Box<KeyDirectory>,
|
||||
@ -89,6 +91,23 @@ impl SecretStore for EthStore {
|
||||
Ok(address)
|
||||
}
|
||||
|
||||
fn import_presale(&self, json: &[u8], password: &str) -> Result<Address, Error> {
|
||||
let json_wallet = try!(json::PresaleWallet::load(json).map_err(|_| Error::InvalidKeyFile("Invalid JSON format".to_owned())));
|
||||
let wallet = PresaleWallet::from(json_wallet);
|
||||
let keypair = try!(wallet.decrypt(password).map_err(|_| Error::InvalidPassword));
|
||||
self.insert_account(keypair.secret().clone(), password)
|
||||
}
|
||||
|
||||
fn import_wallet(&self, json: &[u8], password: &str) -> Result<Address, Error> {
|
||||
let json_keyfile = try!(json::KeyFile::load(json).map_err(|_| Error::InvalidKeyFile("Invalid JSON format".to_owned())));
|
||||
let mut safe_account = SafeAccount::from_file(json_keyfile, None);
|
||||
let secret = try!(safe_account.crypto.secret(password).map_err(|_| Error::InvalidPassword));
|
||||
safe_account.address = try!(KeyPair::from_secret(secret)).address();
|
||||
let address = safe_account.address.clone();
|
||||
try!(self.save(safe_account));
|
||||
Ok(address)
|
||||
}
|
||||
|
||||
fn accounts(&self) -> Result<Vec<Address>, Error> {
|
||||
try!(self.reload_accounts());
|
||||
Ok(self.cache.read().unwrap().keys().cloned().collect())
|
||||
|
@ -18,6 +18,7 @@
|
||||
#![cfg_attr(feature="nightly", plugin(serde_macros))]
|
||||
|
||||
extern crate libc;
|
||||
extern crate itertools;
|
||||
extern crate rand;
|
||||
extern crate time;
|
||||
extern crate serde;
|
||||
@ -25,6 +26,8 @@ extern crate serde_json;
|
||||
extern crate rustc_serialize;
|
||||
extern crate crypto as rcrypto;
|
||||
extern crate tiny_keccak;
|
||||
#[macro_use]
|
||||
extern crate lazy_static;
|
||||
// reexport it nicely
|
||||
extern crate ethkey as _ethkey;
|
||||
|
||||
@ -48,4 +51,5 @@ pub use self::ethstore::EthStore;
|
||||
pub use self::import::import_accounts;
|
||||
pub use self::presale::PresaleWallet;
|
||||
pub use self::secret_store::SecretStore;
|
||||
pub use self::random::random_phrase;
|
||||
|
||||
|
@ -15,6 +15,7 @@
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
use rand::{Rng, OsRng};
|
||||
use itertools::Itertools;
|
||||
|
||||
pub trait Random {
|
||||
fn random() -> Self where Self: Sized;
|
||||
@ -37,3 +38,25 @@ impl Random for [u8; 32] {
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a string which is a random phrase of a number of lowercase words.
|
||||
///
|
||||
/// `words` is the number of words, chosen from a dictionary of 7,530. An value of
|
||||
/// 12 gives 155 bits of entropy (almost saturating address space); 20 gives 258 bits
|
||||
/// which is enough to saturate 32-byte key space
|
||||
pub fn random_phrase(words: usize) -> String {
|
||||
lazy_static! {
|
||||
static ref WORDS: Vec<String> = String::from_utf8_lossy(include_bytes!("../res/wordlist.txt"))
|
||||
.split("\n")
|
||||
.map(|s| s.to_owned())
|
||||
.collect();
|
||||
}
|
||||
let mut rng = OsRng::new().unwrap();
|
||||
(0..words).map(|_| rng.choose(&WORDS).unwrap()).join(" ")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_produce_right_number_of_words() {
|
||||
let p = random_phrase(10);
|
||||
assert_eq!(p.split(" ").count(), 10);
|
||||
}
|
@ -21,6 +21,10 @@ use json::UUID;
|
||||
pub trait SecretStore: Send + Sync {
|
||||
fn insert_account(&self, secret: Secret, password: &str) -> Result<Address, Error>;
|
||||
|
||||
fn import_presale(&self, json: &[u8], password: &str) -> Result<Address, Error>;
|
||||
|
||||
fn import_wallet(&self, json: &[u8], password: &str) -> Result<Address, Error>;
|
||||
|
||||
fn accounts(&self) -> Result<Vec<Address>, Error>;
|
||||
|
||||
fn change_password(&self, account: &Address, old_password: &str, new_password: &str) -> Result<(), Error>;
|
||||
|
@ -17,6 +17,7 @@ jsonrpc-http-server = { git = "https://github.com/ethcore/jsonrpc-http-server.gi
|
||||
ethcore-io = { path = "../util/io" }
|
||||
ethcore-util = { path = "../util" }
|
||||
ethcore = { path = "../ethcore" }
|
||||
ethstore = { path = "../ethstore" }
|
||||
ethash = { path = "../ethash" }
|
||||
ethsync = { path = "../sync" }
|
||||
ethjson = { path = "../json" }
|
||||
|
@ -30,6 +30,7 @@ extern crate jsonrpc_http_server;
|
||||
extern crate ethcore_util as util;
|
||||
extern crate ethcore_io as io;
|
||||
extern crate ethcore;
|
||||
extern crate ethstore;
|
||||
extern crate ethsync;
|
||||
extern crate transient_hashmap;
|
||||
extern crate json_ipc_server as ipc;
|
||||
|
@ -19,6 +19,7 @@ use util::{RotatingLogger};
|
||||
use util::misc::version_data;
|
||||
use std::sync::{Arc, Weak};
|
||||
use std::collections::{BTreeMap};
|
||||
use ethstore::random_phrase;
|
||||
use ethcore::client::{MiningBlockChainClient};
|
||||
use jsonrpc_core::*;
|
||||
use ethcore::miner::MinerService;
|
||||
@ -165,4 +166,11 @@ impl<C, M> Ethcore for EthcoreClient<C, M> where M: MinerService + 'static, C: M
|
||||
Some(ref queue) => to_value(&queue.len()),
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_secret_phrase(&self, params: Params) -> Result<Value, Error> {
|
||||
try!(self.active());
|
||||
try!(expect_no_params(params));
|
||||
|
||||
to_value(&random_phrase(12))
|
||||
}
|
||||
}
|
||||
|
@ -24,7 +24,7 @@ use v1::helpers::{errors, TransactionRequest as TRequest};
|
||||
use v1::helpers::params::expect_no_params;
|
||||
use v1::helpers::dispatch::unlock_sign_and_dispatch;
|
||||
use ethcore::account_provider::AccountProvider;
|
||||
use util::Address;
|
||||
use util::{Address, KeyPair};
|
||||
use ethcore::client::MiningBlockChainClient;
|
||||
use ethcore::miner::MinerService;
|
||||
|
||||
@ -89,6 +89,32 @@ impl<C: 'static, M: 'static> Personal for PersonalClient<C, M> where C: MiningBl
|
||||
)
|
||||
}
|
||||
|
||||
fn new_account_from_phrase(&self, params: Params) -> Result<Value, Error> {
|
||||
try!(self.active());
|
||||
from_params::<(String, String, )>(params).and_then(
|
||||
|(phrase, pass, )| {
|
||||
let store = take_weak!(self.accounts);
|
||||
match store.insert_account(*KeyPair::from_phrase(&phrase).secret(), &pass) {
|
||||
Ok(address) => to_value(&RpcH160::from(address)),
|
||||
Err(e) => Err(errors::account("Could not create account.", e)),
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fn new_account_from_wallet(&self, params: Params) -> Result<Value, Error> {
|
||||
try!(self.active());
|
||||
from_params::<(String, String, )>(params).and_then(
|
||||
|(json, pass, )| {
|
||||
let store = take_weak!(self.accounts);
|
||||
match store.import_presale(json.as_bytes(), &pass).or_else(|_| store.import_wallet(json.as_bytes(), &pass)) {
|
||||
Ok(address) => to_value(&RpcH160::from(address)),
|
||||
Err(e) => Err(errors::account("Could not create account.", e)),
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fn unlock_account(&self, params: Params) -> Result<Value, Error> {
|
||||
try!(self.active());
|
||||
from_params::<(RpcH160, String, Option<u64>)>(params).and_then(
|
||||
|
@ -67,6 +67,9 @@ pub trait Ethcore: Sized + Send + Sync + 'static {
|
||||
/// Returns error when signer is disabled
|
||||
fn unsigned_transactions_count(&self, _: Params) -> Result<Value, Error>;
|
||||
|
||||
/// Returns a cryptographically random phrase sufficient for securely seeding a secret key.
|
||||
fn generate_secret_phrase(&self, _: Params) -> Result<Value, Error>;
|
||||
|
||||
/// Should be used to convert object to io delegate.
|
||||
fn to_delegate(self) -> IoDelegate<Self> {
|
||||
let mut delegate = IoDelegate::new(Arc::new(self));
|
||||
@ -86,6 +89,7 @@ pub trait Ethcore: Sized + Send + Sync + 'static {
|
||||
delegate.add_method("ethcore_defaultExtraData", Ethcore::default_extra_data);
|
||||
delegate.add_method("ethcore_gasPriceStatistics", Ethcore::gas_price_statistics);
|
||||
delegate.add_method("ethcore_unsignedTransactionsCount", Ethcore::unsigned_transactions_count);
|
||||
delegate.add_method("ethcore_generateSecretPhrase", Ethcore::generate_secret_phrase);
|
||||
|
||||
delegate
|
||||
}
|
||||
|
@ -25,8 +25,17 @@ pub trait Personal: Sized + Send + Sync + 'static {
|
||||
fn accounts(&self, _: Params) -> Result<Value, Error>;
|
||||
|
||||
/// Creates new account (it becomes new current unlocked account)
|
||||
/// Param is the password for the account.
|
||||
fn new_account(&self, _: Params) -> Result<Value, Error>;
|
||||
|
||||
/// Creates new account from the given phrase using standard brainwallet mechanism.
|
||||
/// Second parameter is password for the new account.
|
||||
fn new_account_from_phrase(&self, _: Params) -> Result<Value, Error>;
|
||||
|
||||
/// Creates new account from the given JSON wallet.
|
||||
/// Second parameter is password for the wallet and the new account.
|
||||
fn new_account_from_wallet(&self, params: Params) -> Result<Value, Error>;
|
||||
|
||||
/// Unlocks specified account for use (can only be one unlocked account at one moment)
|
||||
fn unlock_account(&self, _: Params) -> Result<Value, Error>;
|
||||
|
||||
@ -51,6 +60,8 @@ pub trait Personal: Sized + Send + Sync + 'static {
|
||||
delegate.add_method("personal_signerEnabled", Personal::signer_enabled);
|
||||
delegate.add_method("personal_listAccounts", Personal::accounts);
|
||||
delegate.add_method("personal_newAccount", Personal::new_account);
|
||||
delegate.add_method("personal_newAccountFromPhrase", Personal::new_account_from_phrase);
|
||||
delegate.add_method("personal_newAccountFromWallet", Personal::new_account_from_wallet);
|
||||
delegate.add_method("personal_unlockAccount", Personal::unlock_account);
|
||||
delegate.add_method("personal_signAndSendTransaction", Personal::sign_and_send_transaction);
|
||||
delegate.add_method("personal_setAccountName", Personal::set_account_name);
|
||||
|
@ -134,6 +134,29 @@ impl KeyPair {
|
||||
public: p,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// TODO: move to ethstore/secret.rs once @debris has refactored necessary dependencies into own crate
|
||||
/// Convert the given phrase into a secret as per brain-wallet spec.
|
||||
/// Taken from https://github.com/ethereum/wiki/wiki/Brain-Wallet
|
||||
/// Note particularly secure for low-entropy keys.
|
||||
pub fn from_phrase(phrase: &str) -> KeyPair {
|
||||
let mut h = phrase.as_bytes().sha3();
|
||||
for _ in 0..16384 {
|
||||
h = h.sha3();
|
||||
}
|
||||
loop {
|
||||
let r = KeyPair::from_secret(h);
|
||||
if r.is_ok() {
|
||||
let r = r.unwrap();
|
||||
if r.address()[0] == 0 {
|
||||
return r;
|
||||
}
|
||||
}
|
||||
h = h.sha3();
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new random key pair
|
||||
pub fn create() -> Result<KeyPair, CryptoError> {
|
||||
let context = &SECP256K1;
|
||||
@ -443,6 +466,11 @@ mod tests {
|
||||
assert_eq!(pair.public().hex(), "101b3ef5a4ea7a1c7928e24c4c75fd053c235d7b80c22ae5c03d145d0ac7396e2a4ffff9adee3133a7b05044a5cee08115fd65145e5165d646bde371010d803c");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_key_from_phrase() {
|
||||
assert_eq!(KeyPair::from_phrase("correct horse battery staple").address(), "0021f80b7f29b9c84e8099c2c6c74a46ed2268c4".into());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ecies_shared() {
|
||||
let kp = KeyPair::create().unwrap();
|
||||
|
Loading…
Reference in New Issue
Block a user