From ca54b8e493ba2d2771acf3845f30c49984811757 Mon Sep 17 00:00:00 2001 From: Gav Wood Date: Thu, 11 Aug 2016 18:31:28 +0200 Subject: [PATCH] RPC for importing geth keys (#1916) * Address book for local addresses. * Simple disk-backed address book. * Add test and keep file with keys. * View and import Geth keys. * Fix test. * Fix up author info. [ci:skip] --- ethcore/src/account_provider.rs | 135 ++++++++++++++++++++++++++------ ethstore/src/dir/disk.rs | 4 +- ethstore/src/dir/mod.rs | 2 + ethstore/src/ethstore.rs | 13 +++ ethstore/src/import.rs | 38 ++++++++- ethstore/src/lib.rs | 3 +- ethstore/src/secret_store.rs | 6 ++ json/src/hash.rs | 17 +++- json/src/lib.rs.in | 1 + json/src/misc/account_meta.rs | 58 ++++++++++++++ json/src/misc/mod.rs | 21 +++++ parity/cli.rs | 6 +- parity/configuration.rs | 2 +- parity/params.rs | 2 +- rpc/src/v1/impls/personal.rs | 30 +++++-- rpc/src/v1/traits/personal.rs | 8 ++ 16 files changed, 307 insertions(+), 39 deletions(-) create mode 100644 json/src/misc/account_meta.rs create mode 100644 json/src/misc/mod.rs diff --git a/ethcore/src/account_provider.rs b/ethcore/src/account_provider.rs index 6a6ece81d..14d28543b 100644 --- a/ethcore/src/account_provider.rs +++ b/ethcore/src/account_provider.rs @@ -16,15 +16,16 @@ //! Account management. -use std::fmt; +use std::{fs, fmt}; use std::collections::HashMap; use std::time::{Instant, Duration}; use util::{Address as H160, H256, H520, Mutex, RwLock}; +use std::path::PathBuf; +use ethjson::misc::AccountMeta; use ethstore::{SecretStore, Error as SSError, SafeAccount, EthStore}; use ethstore::dir::{KeyDirectory}; use ethstore::ethkey::{Address as SSAddress, Message as SSMessage, Secret as SSSecret, Random, Generator}; - /// Type of unlock. #[derive(Clone)] enum Unlock { @@ -131,32 +132,74 @@ impl KeyDirectory for NullDir { } } +/// Disk-backed map from Address to String. Uses JSON. +struct AddressBook { + path: PathBuf, + cache: HashMap, +} + +impl AddressBook { + pub fn new(path: String) -> Self { + trace!(target: "addressbook", "new({})", path); + let mut path: PathBuf = path.into(); + path.push("address_book.json"); + trace!(target: "addressbook", "path={:?}", path); + let mut r = AddressBook { + path: path, + cache: HashMap::new(), + }; + r.revert(); + r + } + + pub fn get(&self) -> HashMap { + self.cache.clone() + } + + pub fn set_name(&mut self, a: H160, name: String) { + let mut x = self.cache.get(&a) + .map(|a| a.clone()) + .unwrap_or(AccountMeta{name: Default::default(), meta: "{}".to_owned(), uuid: None}); + x.name = name; + self.cache.insert(a, x); + self.save(); + } + + pub fn set_meta(&mut self, a: H160, meta: String) { + let mut x = self.cache.get(&a) + .map(|a| a.clone()) + .unwrap_or(AccountMeta{name: "Anonymous".to_owned(), meta: Default::default(), uuid: None}); + x.meta = meta; + self.cache.insert(a, x); + self.save(); + } + + fn revert(&mut self) { + trace!(target: "addressbook", "revert"); + let _ = fs::File::open(self.path.clone()) + .map_err(|e| trace!(target: "addressbook", "Couldn't open address book: {}", e)) + .and_then(|f| AccountMeta::read_address_map(&f) + .map_err(|e| warn!(target: "addressbook", "Couldn't read address book: {}", e)) + .and_then(|m| { self.cache = m; Ok(()) }) + ); + } + + fn save(&mut self) { + trace!(target: "addressbook", "save"); + let _ = fs::File::create(self.path.clone()) + .map_err(|e| warn!(target: "addressbook", "Couldn't open address book for writing: {}", e)) + .and_then(|mut f| AccountMeta::write_address_map(&self.cache, &mut f) + .map_err(|e| warn!(target: "addressbook", "Couldn't write to address book: {}", e)) + ); + } +} + /// Account management. /// Responsible for unlocking accounts. pub struct AccountProvider { unlocked: Mutex>, sstore: Box, -} - -/// Collected account metadata -#[derive(Clone, Debug, PartialEq)] -pub struct AccountMeta { - /// The name of the account. - pub name: String, - /// The rest of the metadata of the account. - pub meta: String, - /// The 128-bit UUID of the account, if it has one (brain-wallets don't). - pub uuid: Option, -} - -impl Default for AccountMeta { - fn default() -> Self { - AccountMeta { - name: String::new(), - meta: "{}".to_owned(), - uuid: None, - } - } + address_book: Mutex, } impl AccountProvider { @@ -164,6 +207,7 @@ impl AccountProvider { pub fn new(sstore: Box) -> Self { AccountProvider { unlocked: Mutex::new(HashMap::new()), + address_book: Mutex::new(AddressBook::new(sstore.local_path().into())), sstore: sstore, } } @@ -172,6 +216,7 @@ impl AccountProvider { pub fn transient_provider() -> Self { AccountProvider { unlocked: Mutex::new(HashMap::new()), + address_book: Mutex::new(AddressBook::new(Default::default())), sstore: Box::new(EthStore::open(Box::new(NullDir::default())).unwrap()) } } @@ -209,6 +254,23 @@ impl AccountProvider { Ok(accounts) } + /// Returns each address along with metadata. + pub fn addresses_info(&self) -> Result, Error> { + Ok(self.address_book.lock().get()) + } + + /// Returns each address along with metadata. + pub fn set_address_name(&self, account: A, name: String) -> Result<(), Error> where Address: From { + let account = Address::from(account).into(); + Ok(self.address_book.lock().set_name(account, name)) + } + + /// Returns each address along with metadata. + pub fn set_address_meta(&self, account: A, meta: String) -> Result<(), Error> where Address: From { + let account = Address::from(account).into(); + Ok(self.address_book.lock().set_meta(account, meta)) + } + /// Returns each account along with name and meta. pub fn accounts_info(&self) -> Result, Error> { let r: HashMap = try!(self.sstore.accounts()) @@ -320,13 +382,38 @@ impl AccountProvider { let signature = try!(self.sstore.sign(&account, &password, &message)); Ok(H520(signature.into())) } + + /// Returns the underlying `SecretStore` reference if one exists. + pub fn list_geth_accounts(&self, testnet: bool) -> Vec { + self.sstore.list_geth_accounts(testnet).into_iter().map(|a| Address::from(a).into()).collect() + } + + /// Returns the underlying `SecretStore` reference if one exists. + pub fn import_geth_accounts(&self, desired: Vec, testnet: bool) -> Result, Error> { + let desired = desired.into_iter().map(|a| Address::from(a).into()).collect(); + Ok(try!(self.sstore.import_geth_accounts(desired, testnet)).into_iter().map(|a| Address::from(a).into()).collect()) + } } #[cfg(test)] mod tests { - use super::AccountProvider; + use super::{AccountProvider, AddressBook}; + use std::collections::HashMap; + use ethjson::misc::AccountMeta; use ethstore::ethkey::{Generator, Random}; use std::time::Duration; + use devtools::RandomTempPath; + + #[test] + fn should_save_and_reload_address_book() { + let temp = RandomTempPath::create_dir(); + let path = temp.as_str().to_owned(); + let mut b = AddressBook::new(path.clone()); + b.set_name(1.into(), "One".to_owned()); + b.set_meta(1.into(), "{1:1}".to_owned()); + let b = AddressBook::new(path); + assert_eq!(b.get(), hash_map![1.into() => AccountMeta{name: "One".to_owned(), meta: "{1:1}".to_owned(), uuid: None}]); + } #[test] fn unlock_account_temp() { diff --git a/ethstore/src/dir/disk.rs b/ethstore/src/dir/disk.rs index 1351ae217..3016412eb 100644 --- a/ethstore/src/dir/disk.rs +++ b/ethstore/src/dir/disk.rs @@ -22,7 +22,7 @@ use ethkey::Address; use {json, SafeAccount, Error}; use super::KeyDirectory; -const IGNORED_FILES: &'static [&'static str] = &["thumbs.db"]; +const IGNORED_FILES: &'static [&'static str] = &["thumbs.db", "address_book.json"]; #[cfg(not(windows))] fn restrict_permissions_to_owner(file_path: &Path) -> Result<(), i32> { @@ -149,6 +149,8 @@ impl KeyDirectory for DiskDirectory { Some((path, _)) => fs::remove_file(path).map_err(From::from) } } + + fn path(&self) -> Option<&PathBuf> { Some(&self.path) } } diff --git a/ethstore/src/dir/mod.rs b/ethstore/src/dir/mod.rs index 6f2b59194..e29bd1ec4 100644 --- a/ethstore/src/dir/mod.rs +++ b/ethstore/src/dir/mod.rs @@ -15,6 +15,7 @@ // along with Parity. If not, see . use ethkey::Address; +use std::path::{PathBuf}; use {SafeAccount, Error}; mod disk; @@ -30,6 +31,7 @@ pub trait KeyDirectory: Send + Sync { fn load(&self) -> Result, Error>; fn insert(&self, account: SafeAccount) -> Result; fn remove(&self, address: &Address) -> Result<(), Error>; + fn path(&self) -> Option<&PathBuf> { None } } pub use self::disk::DiskDirectory; diff --git a/ethstore/src/ethstore.rs b/ethstore/src/ethstore.rs index f2a3a2b78..29f4c757c 100644 --- a/ethstore/src/ethstore.rs +++ b/ethstore/src/ethstore.rs @@ -27,6 +27,7 @@ use {Error, SecretStore}; use json; use json::UUID; use presale::PresaleWallet; +use import; pub struct EthStore { dir: Box, @@ -173,4 +174,16 @@ impl SecretStore for EthStore { // save to file self.save(account) } + + fn local_path(&self) -> String { + self.dir.path().map(|p| p.to_string_lossy().into_owned()).unwrap_or_else(|| String::new()) + } + + fn list_geth_accounts(&self, testnet: bool) -> Vec
{ + import::read_geth_accounts(testnet) + } + + fn import_geth_accounts(&self, desired: Vec
, testnet: bool) -> Result, Error> { + import::import_geth_accounts(&*self.dir, desired.into_iter().collect(), testnet) + } } diff --git a/ethstore/src/import.rs b/ethstore/src/import.rs index b3f23107a..63cc93111 100644 --- a/ethstore/src/import.rs +++ b/ethstore/src/import.rs @@ -16,7 +16,7 @@ use std::collections::HashSet; use ethkey::Address; -use dir::KeyDirectory; +use dir::{GethDirectory, KeyDirectory, DirectoryType}; use Error; pub fn import_accounts(src: &KeyDirectory, dst: &KeyDirectory) -> Result, Error> { @@ -31,3 +31,39 @@ pub fn import_accounts(src: &KeyDirectory, dst: &KeyDirectory) -> Result Vec
{ + let t = if testnet { + DirectoryType::Testnet + } else { + DirectoryType::Main + }; + + GethDirectory::open(t) + .load() + .map(|d| d.into_iter().map(|a| a.address).collect()) + .unwrap_or_else(|_| Vec::new()) +} + +/// Import specific `desired` accounts from the Geth keystore into `dst`. +pub fn import_geth_accounts(dst: &KeyDirectory, desired: HashSet
, testnet: bool) -> Result, Error> { + let t = if testnet { + DirectoryType::Testnet + } else { + DirectoryType::Main + }; + + let src = GethDirectory::open(t); + let accounts = try!(src.load()); + let existing_accounts = try!(dst.load()).into_iter().map(|a| a.address).collect::>(); + + accounts.into_iter() + .filter(|a| !existing_accounts.contains(&a.address)) + .filter(|a| desired.contains(&a.address)) + .map(|a| { + let address = a.address.clone(); + try!(dst.insert(a)); + Ok(address) + }).collect() +} diff --git a/ethstore/src/lib.rs b/ethstore/src/lib.rs index a1131073e..982a47c5a 100644 --- a/ethstore/src/lib.rs +++ b/ethstore/src/lib.rs @@ -48,8 +48,7 @@ mod secret_store; pub use self::account::SafeAccount; pub use self::error::Error; pub use self::ethstore::EthStore; -pub use self::import::import_accounts; +pub use self::import::{import_accounts, read_geth_accounts}; pub use self::presale::PresaleWallet; pub use self::secret_store::SecretStore; pub use self::random::random_phrase; - diff --git a/ethstore/src/secret_store.rs b/ethstore/src/secret_store.rs index 6d5649b37..90ed79fb5 100644 --- a/ethstore/src/secret_store.rs +++ b/ethstore/src/secret_store.rs @@ -42,5 +42,11 @@ pub trait SecretStore: Send + Sync { fn set_name(&self, address: &Address, name: String) -> Result<(), Error>; fn set_meta(&self, address: &Address, meta: String) -> Result<(), Error>; + + fn local_path(&self) -> String; + + fn list_geth_accounts(&self, testnet: bool) -> Vec
; + + fn import_geth_accounts(&self, desired: Vec
, testnet: bool) -> Result, Error>; } diff --git a/json/src/hash.rs b/json/src/hash.rs index 16d8ee9c0..40b0050a8 100644 --- a/json/src/hash.rs +++ b/json/src/hash.rs @@ -17,8 +17,9 @@ //! Lenient hash json deserialization for test json files. use std::str::FromStr; -use serde::{Deserialize, Deserializer, Error}; +use serde::{Deserialize, Deserializer, Serialize, Serializer, Error}; use serde::de::Visitor; +use rustc_serialize::hex::ToHex; use util::hash::{H64 as Hash64, Address as Hash160, H256 as Hash256, H2048 as Hash2048}; @@ -34,6 +35,12 @@ macro_rules! impl_hash { } } + impl From<$inner> for $name { + fn from(i: $inner) -> Self { + $name(i) + } + } + impl Deserialize for $name { fn deserialize(deserializer: &mut D) -> Result where D: Deserializer { @@ -66,6 +73,14 @@ macro_rules! impl_hash { deserializer.deserialize(HashVisitor) } } + + impl Serialize for $name { + fn serialize(&self, serializer: &mut S) -> Result<(), S::Error> where S: Serializer { + let mut hex = "0x".to_owned(); + hex.push_str(&self.0.to_hex()); + serializer.serialize_str(&hex) + } + } } } diff --git a/json/src/lib.rs.in b/json/src/lib.rs.in index 541629305..4754eeee9 100644 --- a/json/src/lib.rs.in +++ b/json/src/lib.rs.in @@ -29,3 +29,4 @@ pub mod vm; pub mod maybe; pub mod state; pub mod transaction; +pub mod misc; diff --git a/json/src/misc/account_meta.rs b/json/src/misc/account_meta.rs new file mode 100644 index 000000000..242b58a01 --- /dev/null +++ b/json/src/misc/account_meta.rs @@ -0,0 +1,58 @@ +// 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 . + +//! Misc deserialization. + +use std::io::{Read, Write}; +use std::collections::HashMap; +use serde_json; +use util; +use hash; + +/// Collected account metadata +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct AccountMeta { + /// The name of the account. + pub name: String, + /// The rest of the metadata of the account. + pub meta: String, + /// The 128-bit UUID of the account, if it has one (brain-wallets don't). + pub uuid: Option, +} + +impl Default for AccountMeta { + fn default() -> Self { + AccountMeta { + name: String::new(), + meta: "{}".to_owned(), + uuid: None, + } + } +} + +impl AccountMeta { + /// Read a hash map of Address -> AccountMeta. + pub fn read_address_map(reader: R) -> Result, serde_json::Error> where R: Read { + serde_json::from_reader(reader).map(|ok: HashMap| + ok.into_iter().map(|(a, m)| (a.into(), m)).collect() + ) + } + + /// Write a hash map of Address -> AccountMeta. + pub fn write_address_map(m: &HashMap, writer: &mut W) -> Result<(), serde_json::Error> where W: Write { + serde_json::to_writer(writer, &m.iter().map(|(a, m)| (a.clone().into(), m)).collect::>()) + } +} diff --git a/json/src/misc/mod.rs b/json/src/misc/mod.rs new file mode 100644 index 000000000..5db868d03 --- /dev/null +++ b/json/src/misc/mod.rs @@ -0,0 +1,21 @@ +// 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 . + +//! Misc deserialization. + +mod account_meta; + +pub use self::account_meta::AccountMeta; diff --git a/parity/cli.rs b/parity/cli.rs index f01436a96..cb354aff5 100644 --- a/parity/cli.rs +++ b/parity/cli.rs @@ -19,7 +19,7 @@ use docopt::Docopt; pub const USAGE: &'static str = r#" Parity. Ethereum Client. - By Wood/Paronyan/Kotewicz/Drwięga/Volf. + By Wood/Paronyan/Kotewicz/Drwięga/Volf et al. Copyright 2015, 2016 Ethcore (UK) Limited Usage: @@ -67,7 +67,6 @@ Account Options: --keys-iterations NUM Specify the number of iterations to use when deriving key from the password (bigger is more secure) [default: 10240]. - --no-import-keys Do not import keys from legacy clients. --force-signer Enable Trusted Signer WebSocket endpoint used by Signer UIs, even when --unlock is in use. --no-signer Disable Trusted Signer WebSocket endpoint used by @@ -248,6 +247,7 @@ Legacy Options: --testnet Geth-compatible testnet mode. Equivalent to --chain testnet --keys-path $HOME/parity/testnet-keys. Overrides the --keys-path option. + --import-geth-keys Attempt to import keys from Geth client. --datadir PATH Equivalent to --db-path PATH. --networkid INDEX Equivalent to --network-id INDEX. --peers NUM Equivalent to --min-peers NUM. @@ -310,7 +310,7 @@ pub struct Args { pub flag_password: Vec, pub flag_keys_path: String, pub flag_keys_iterations: u32, - pub flag_no_import_keys: bool, + pub flag_import_geth_keys: bool, pub flag_bootnodes: Option, pub flag_network_id: Option, pub flag_pruning: String, diff --git a/parity/configuration.rs b/parity/configuration.rs index ef846ca80..ccf1386f3 100644 --- a/parity/configuration.rs +++ b/parity/configuration.rs @@ -310,7 +310,7 @@ impl Configuration { fn accounts_config(&self) -> Result { let cfg = AccountsConfig { iterations: self.args.flag_keys_iterations, - import_keys: !self.args.flag_no_import_keys, + import_keys: self.args.flag_import_geth_keys, testnet: self.args.flag_testnet, password_files: self.args.flag_password.clone(), unlocked_accounts: try!(to_addresses(&self.args.flag_unlock)), diff --git a/parity/params.rs b/parity/params.rs index 6e105f524..e6b195c0b 100644 --- a/parity/params.rs +++ b/parity/params.rs @@ -167,7 +167,7 @@ impl Default for AccountsConfig { fn default() -> Self { AccountsConfig { iterations: 10240, - import_keys: true, + import_keys: false, testnet: false, password_files: Vec::new(), unlocked_accounts: Vec::new(), diff --git a/rpc/src/v1/impls/personal.rs b/rpc/src/v1/impls/personal.rs index 56e5587d7..c0a44e437 100644 --- a/rpc/src/v1/impls/personal.rs +++ b/rpc/src/v1/impls/personal.rs @@ -148,18 +148,20 @@ impl Personal for PersonalClient where C: MiningBl fn set_account_name(&self, params: Params) -> Result { try!(self.active()); let store = take_weak!(self.accounts); - from_params::<(RpcH160, _)>(params).and_then(|(addr, name)| { + from_params::<(RpcH160, String)>(params).and_then(|(addr, name)| { let addr: Address = addr.into(); - store.set_account_name(addr, name).map_err(|e| errors::account("Could not set account name.", e)).map(|_| Value::Null) + store.set_account_name(addr.clone(), name.clone()).or_else(|_| store.set_address_name(addr, name)).expect("set_address_name always returns Ok; qed"); + Ok(Value::Null) }) } fn set_account_meta(&self, params: Params) -> Result { try!(self.active()); let store = take_weak!(self.accounts); - from_params::<(RpcH160, _)>(params).and_then(|(addr, meta)| { + from_params::<(RpcH160, String)>(params).and_then(|(addr, meta)| { let addr: Address = addr.into(); - store.set_account_meta(addr, meta).map_err(|e| errors::account("Could not set account meta.", e)).map(|_| Value::Null) + store.set_account_meta(addr.clone(), meta.clone()).or_else(|_| store.set_address_meta(addr, meta)).expect("set_address_meta always returns Ok; qed"); + Ok(Value::Null) }) } @@ -168,7 +170,8 @@ impl Personal for PersonalClient where C: MiningBl try!(expect_no_params(params)); let store = take_weak!(self.accounts); let info = try!(store.accounts_info().map_err(|e| errors::account("Could not fetch account info.", e))); - Ok(Value::Object(info.into_iter().map(|(a, v)| { + let other = store.addresses_info().expect("addresses_info always returns Ok; qed"); + Ok(Value::Object(info.into_iter().chain(other.into_iter()).map(|(a, v)| { let m = map![ "name".to_owned() => to_value(&v.name).unwrap(), "meta".to_owned() => to_value(&v.meta).unwrap(), @@ -181,4 +184,21 @@ impl Personal for PersonalClient where C: MiningBl (format!("0x{}", a.hex()), Value::Object(m)) }).collect::>())) } + + fn geth_accounts(&self, params: Params) -> Result { + try!(self.active()); + try!(expect_no_params(params)); + let store = take_weak!(self.accounts); + to_value(&store.list_geth_accounts(false).into_iter().map(Into::into).collect::>()) + } + + fn import_geth_accounts(&self, params: Params) -> Result { + from_params::<(Vec,)>(params).and_then(|(addresses,)| { + let store = take_weak!(self.accounts); + to_value(&try!(store + .import_geth_accounts(addresses.into_iter().map(Into::into).collect(), false) + .map_err(|e| errors::account("Couldn't import Geth accounts", e)) + ).into_iter().map(Into::into).collect::>()) + }) + } } diff --git a/rpc/src/v1/traits/personal.rs b/rpc/src/v1/traits/personal.rs index 82ce70dbc..89d63c863 100644 --- a/rpc/src/v1/traits/personal.rs +++ b/rpc/src/v1/traits/personal.rs @@ -54,6 +54,12 @@ pub trait Personal: Sized + Send + Sync + 'static { /// Returns accounts information. fn accounts_info(&self, _: Params) -> Result; + /// Returns the accounts available for importing from Geth. + fn geth_accounts(&self, _: Params) -> Result; + + /// Imports a number of Geth accounts, with the list provided as the argument. + fn import_geth_accounts(&self, _: Params) -> Result; + /// Should be used to convert object to io delegate. fn to_delegate(self) -> IoDelegate { let mut delegate = IoDelegate::new(Arc::new(self)); @@ -67,6 +73,8 @@ pub trait Personal: Sized + Send + Sync + 'static { delegate.add_method("personal_setAccountName", Personal::set_account_name); delegate.add_method("personal_setAccountMeta", Personal::set_account_meta); delegate.add_method("personal_accountsInfo", Personal::accounts_info); + delegate.add_method("personal_listGethAccounts", Personal::geth_accounts); + delegate.add_method("personal_importGethAccounts", Personal::import_geth_accounts); delegate }