Ledger wallet support (#4486)

* Ledger devices support

* structs for RPC types
This commit is contained in:
Arkadiy Paronyan 2017-02-10 01:07:06 +01:00 committed by Gav Wood
parent 395a44e4a8
commit a7e6d8727a
21 changed files with 810 additions and 34 deletions

53
Cargo.lock generated
View File

@ -122,6 +122,14 @@ dependencies = [
"rustc_version 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", "rustc_version 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
] ]
[[package]]
name = "bit-set"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"bit-vec 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]] [[package]]
name = "bit-set" name = "bit-set"
version = "0.4.0" version = "0.4.0"
@ -381,6 +389,7 @@ dependencies = [
"ethkey 0.2.0", "ethkey 0.2.0",
"ethstore 0.1.0", "ethstore 0.1.0",
"evmjit 1.6.0", "evmjit 1.6.0",
"hardware-wallet 1.6.0",
"heapsize 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", "heapsize 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
"hyper 0.10.0-a.0 (git+https://github.com/ethcore/hyper)", "hyper 0.10.0-a.0 (git+https://github.com/ethcore/hyper)",
"lazy_static 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "lazy_static 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
@ -862,6 +871,18 @@ name = "hamming"
version = "0.1.3" version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "hardware-wallet"
version = "1.6.0"
dependencies = [
"ethcore-bigint 0.1.2",
"ethkey 0.2.0",
"hidapi 0.3.1 (git+https://github.com/ethcore/hidapi-rs)",
"libusb 0.3.0 (git+https://github.com/ethcore/libusb-rs)",
"log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
"parking_lot 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]] [[package]]
name = "heapsize" name = "heapsize"
version = "0.3.6" version = "0.3.6"
@ -870,6 +891,15 @@ dependencies = [
"kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
] ]
[[package]]
name = "hidapi"
version = "0.3.1"
source = "git+https://github.com/ethcore/hidapi-rs#9a127c1dca7e327e4fdd428406a76c9f5ef48563"
dependencies = [
"gcc 0.3.35 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.16 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]] [[package]]
name = "hpack" name = "hpack"
version = "0.2.0" version = "0.2.0"
@ -1107,6 +1137,25 @@ name = "libc"
version = "0.2.16" version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "libusb"
version = "0.3.0"
source = "git+https://github.com/ethcore/libusb-rs#32bacf61abd981d5cbd4a8fecca5a2dc0b762a96"
dependencies = [
"bit-set 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.16 (registry+https://github.com/rust-lang/crates.io-index)",
"libusb-sys 0.2.3 (git+https://github.com/ethcore/libusb-sys)",
]
[[package]]
name = "libusb-sys"
version = "0.2.3"
source = "git+https://github.com/ethcore/libusb-sys#3c56a34bd040b0e60758824d839d9b700cfad2e0"
dependencies = [
"gcc 0.3.35 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.16 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]] [[package]]
name = "linked-hash-map" name = "linked-hash-map"
version = "0.2.1" version = "0.2.1"
@ -2486,6 +2535,7 @@ dependencies = [
"checksum aster 0.17.0 (registry+https://github.com/rust-lang/crates.io-index)" = "07d344974f0a155f091948aa389fb1b912d3a58414fbdb9c8d446d193ee3496a" "checksum aster 0.17.0 (registry+https://github.com/rust-lang/crates.io-index)" = "07d344974f0a155f091948aa389fb1b912d3a58414fbdb9c8d446d193ee3496a"
"checksum base32 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "1b9605ba46d61df0410d8ac686b0007add8172eba90e8e909c347856fe794d8c" "checksum base32 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "1b9605ba46d61df0410d8ac686b0007add8172eba90e8e909c347856fe794d8c"
"checksum bigint 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2311bcd71b281e142a095311c22509f0d6bcd87b3000d7dbaa810929b9d6f6ae" "checksum bigint 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2311bcd71b281e142a095311c22509f0d6bcd87b3000d7dbaa810929b9d6f6ae"
"checksum bit-set 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e6e1e6fb1c9e3d6fcdec57216a74eaa03e41f52a22f13a16438251d8e88b89da"
"checksum bit-set 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d9bf6104718e80d7b26a68fdbacff3481cfc05df670821affc7e9cbc1884400c" "checksum bit-set 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d9bf6104718e80d7b26a68fdbacff3481cfc05df670821affc7e9cbc1884400c"
"checksum bit-vec 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "5b97c2c8e8bbb4251754f559df8af22fb264853c7d009084a576cdf12565089d" "checksum bit-vec 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "5b97c2c8e8bbb4251754f559df8af22fb264853c7d009084a576cdf12565089d"
"checksum bitflags 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8dead7461c1127cf637931a1e50934eb6eee8bff2f74433ac7909e9afcee04a3" "checksum bitflags 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8dead7461c1127cf637931a1e50934eb6eee8bff2f74433ac7909e9afcee04a3"
@ -2525,6 +2575,7 @@ dependencies = [
"checksum glob 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "8be18de09a56b60ed0edf84bc9df007e30040691af7acd1c41874faac5895bfb" "checksum glob 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "8be18de09a56b60ed0edf84bc9df007e30040691af7acd1c41874faac5895bfb"
"checksum hamming 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "65043da274378d68241eb9a8f8f8aa54e349136f7b8e12f63e3ef44043cc30e1" "checksum hamming 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "65043da274378d68241eb9a8f8f8aa54e349136f7b8e12f63e3ef44043cc30e1"
"checksum heapsize 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "abb306abb8d398e053cfb1b3e7b72c2f580be048b85745c52652954f8ad1439c" "checksum heapsize 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "abb306abb8d398e053cfb1b3e7b72c2f580be048b85745c52652954f8ad1439c"
"checksum hidapi 0.3.1 (git+https://github.com/ethcore/hidapi-rs)" = "<none>"
"checksum hpack 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3d2da7d3a34cf6406d9d700111b8eafafe9a251de41ae71d8052748259343b58" "checksum hpack 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3d2da7d3a34cf6406d9d700111b8eafafe9a251de41ae71d8052748259343b58"
"checksum httparse 1.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "46534074dbb80b070d60a5cb8ecadd8963a00a438ae1a95268850a7ef73b67ae" "checksum httparse 1.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "46534074dbb80b070d60a5cb8ecadd8963a00a438ae1a95268850a7ef73b67ae"
"checksum hyper 0.10.0-a.0 (git+https://github.com/ethcore/hyper)" = "<none>" "checksum hyper 0.10.0-a.0 (git+https://github.com/ethcore/hyper)" = "<none>"
@ -2547,6 +2598,8 @@ dependencies = [
"checksum lazy_static 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "49247ec2a285bb3dcb23cbd9c35193c025e7251bfce77c1d5da97e6362dffe7f" "checksum lazy_static 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "49247ec2a285bb3dcb23cbd9c35193c025e7251bfce77c1d5da97e6362dffe7f"
"checksum lazycell 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ce12306c4739d86ee97c23139f3a34ddf0387bbf181bc7929d287025a8c3ef6b" "checksum lazycell 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ce12306c4739d86ee97c23139f3a34ddf0387bbf181bc7929d287025a8c3ef6b"
"checksum libc 0.2.16 (registry+https://github.com/rust-lang/crates.io-index)" = "408014cace30ee0f767b1c4517980646a573ec61a57957aeeabcac8ac0a02e8d" "checksum libc 0.2.16 (registry+https://github.com/rust-lang/crates.io-index)" = "408014cace30ee0f767b1c4517980646a573ec61a57957aeeabcac8ac0a02e8d"
"checksum libusb 0.3.0 (git+https://github.com/ethcore/libusb-rs)" = "<none>"
"checksum libusb-sys 0.2.3 (git+https://github.com/ethcore/libusb-sys)" = "<none>"
"checksum linked-hash-map 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "bda158e0dabeb97ee8a401f4d17e479d6b891a14de0bba79d5cc2d4d325b5e48" "checksum linked-hash-map 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "bda158e0dabeb97ee8a401f4d17e479d6b891a14de0bba79d5cc2d4d325b5e48"
"checksum linked-hash-map 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "6d262045c5b87c0861b3f004610afd0e2c851e2908d08b6c870cbb9d5f494ecd" "checksum linked-hash-map 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "6d262045c5b87c0861b3f004610afd0e2c851e2908d08b6c870cbb9d5f494ecd"
"checksum log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "ab83497bf8bf4ed2a74259c1c802351fcd67a65baa86394b6ba73c36f4838054" "checksum log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "ab83497bf8bf4ed2a74259c1c802351fcd67a65baa86394b6ba73c36f4838054"

View File

@ -42,6 +42,7 @@ rlp = { path = "../util/rlp" }
ethcore-stratum = { path = "../stratum" } ethcore-stratum = { path = "../stratum" }
lru-cache = "0.1.0" lru-cache = "0.1.0"
ethcore-bloom-journal = { path = "../util/bloom" } ethcore-bloom-journal = { path = "../util/bloom" }
hardware-wallet = { path = "../hw" }
ethabi = "0.2.2" ethabi = "0.2.2"
[dependencies.hyper] [dependencies.hyper]

View File

@ -29,6 +29,7 @@ use ethstore::{SimpleSecretStore, SecretStore, Error as SSError, EthStore, EthMu
use ethstore::dir::MemoryDirectory; use ethstore::dir::MemoryDirectory;
use ethstore::ethkey::{Address, Message, Public, Secret, Random, Generator}; use ethstore::ethkey::{Address, Message, Public, Secret, Random, Generator};
use ethjson::misc::AccountMeta; use ethjson::misc::AccountMeta;
use hardware_wallet::{Error as HardwareError, HardwareWalletManager, KeyPath};
pub use ethstore::ethkey::Signature; pub use ethstore::ethkey::Signature;
/// Type of unlock. /// Type of unlock.
@ -55,6 +56,10 @@ struct AccountData {
pub enum SignError { pub enum SignError {
/// Account is not unlocked /// Account is not unlocked
NotUnlocked, NotUnlocked,
/// Account does not exist.
NotFound,
/// Low-level hardware device error.
Hardware(HardwareError),
/// Low-level error from store /// Low-level error from store
SStore(SSError) SStore(SSError)
} }
@ -63,11 +68,19 @@ impl fmt::Display for SignError {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
match *self { match *self {
SignError::NotUnlocked => write!(f, "Account is locked"), SignError::NotUnlocked => write!(f, "Account is locked"),
SignError::NotFound => write!(f, "Account does not exist"),
SignError::Hardware(ref e) => write!(f, "{}", e),
SignError::SStore(ref e) => write!(f, "{}", e), SignError::SStore(ref e) => write!(f, "{}", e),
} }
} }
} }
impl From<HardwareError> for SignError {
fn from(e: HardwareError) -> Self {
SignError::Hardware(e)
}
}
impl From<SSError> for SignError { impl From<SSError> for SignError {
fn from(e: SSError) -> Self { fn from(e: SSError) -> Self {
SignError::SStore(e) SignError::SStore(e)
@ -107,17 +120,47 @@ pub struct AccountProvider {
sstore: Box<SecretStore>, sstore: Box<SecretStore>,
/// Accounts unlocked with rolling tokens /// Accounts unlocked with rolling tokens
transient_sstore: EthMultiStore, transient_sstore: EthMultiStore,
/// Accounts in hardware wallets.
hardware_store: Option<HardwareWalletManager>,
}
/// Account management settings.
pub struct AccountProviderSettings {
/// Enable hardware wallet support.
pub enable_hardware_wallets: bool,
/// Use the classic chain key on the hardware wallet.
pub hardware_wallet_classic_key: bool,
}
impl Default for AccountProviderSettings {
fn default() -> Self {
AccountProviderSettings {
enable_hardware_wallets: false,
hardware_wallet_classic_key: false,
}
}
} }
impl AccountProvider { impl AccountProvider {
/// Creates new account provider. /// Creates new account provider.
pub fn new(sstore: Box<SecretStore>) -> Self { pub fn new(sstore: Box<SecretStore>, settings: AccountProviderSettings) -> Self {
let mut hardware_store = None;
if settings.enable_hardware_wallets {
match HardwareWalletManager::new() {
Ok(manager) => {
manager.set_key_path(if settings.hardware_wallet_classic_key { KeyPath::EthereumClassic } else { KeyPath::Ethereum });
hardware_store = Some(manager)
},
Err(e) => warn!("Error initializing hardware wallets: {}", e),
}
}
AccountProvider { AccountProvider {
unlocked: RwLock::new(HashMap::new()), unlocked: RwLock::new(HashMap::new()),
address_book: RwLock::new(AddressBook::new(&sstore.local_path())), address_book: RwLock::new(AddressBook::new(&sstore.local_path())),
dapps_settings: RwLock::new(DappsSettingsStore::new(&sstore.local_path())), dapps_settings: RwLock::new(DappsSettingsStore::new(&sstore.local_path())),
sstore: sstore, sstore: sstore,
transient_sstore: transient_sstore(), transient_sstore: transient_sstore(),
hardware_store: hardware_store,
} }
} }
@ -129,6 +172,7 @@ impl AccountProvider {
dapps_settings: RwLock::new(DappsSettingsStore::transient()), dapps_settings: RwLock::new(DappsSettingsStore::transient()),
sstore: Box::new(EthStore::open(Box::new(MemoryDirectory::default())).expect("MemoryDirectory load always succeeds; qed")), sstore: Box::new(EthStore::open(Box::new(MemoryDirectory::default())).expect("MemoryDirectory load always succeeds; qed")),
transient_sstore: transient_sstore(), transient_sstore: transient_sstore(),
hardware_store: None,
} }
} }
@ -176,6 +220,12 @@ impl AccountProvider {
Ok(accounts.into_iter().map(|a| a.address).collect()) Ok(accounts.into_iter().map(|a| a.address).collect())
} }
/// Returns addresses of hardware accounts.
pub fn hardware_accounts(&self) -> Result<Vec<Address>, Error> {
let accounts = self.hardware_store.as_ref().map_or(Vec::new(), |h| h.list_wallets());
Ok(accounts.into_iter().map(|a| a.address).collect())
}
/// Sets a whitelist of accounts exposed for unknown dapps. /// Sets a whitelist of accounts exposed for unknown dapps.
/// `None` means that all accounts will be visible. /// `None` means that all accounts will be visible.
pub fn set_new_dapps_whitelist(&self, accounts: Option<Vec<Address>>) -> Result<(), Error> { pub fn set_new_dapps_whitelist(&self, accounts: Option<Vec<Address>>) -> Result<(), Error> {
@ -271,15 +321,36 @@ impl AccountProvider {
/// Returns each account along with name and meta. /// Returns each account along with name and meta.
pub fn accounts_info(&self) -> Result<HashMap<Address, AccountMeta>, Error> { pub fn accounts_info(&self) -> Result<HashMap<Address, AccountMeta>, Error> {
let r: HashMap<Address, AccountMeta> = self.sstore.accounts()? let r = self.sstore.accounts()?
.into_iter() .into_iter()
.map(|a| (a.address.clone(), self.account_meta(a.address).ok().unwrap_or_default())) .map(|a| (a.address.clone(), self.account_meta(a.address).ok().unwrap_or_default()))
.collect(); .collect();
Ok(r) Ok(r)
} }
/// Returns each hardware account along with name and meta.
pub fn hardware_accounts_info(&self) -> Result<HashMap<Address, AccountMeta>, Error> {
let r = self.hardware_accounts()?
.into_iter()
.map(|address| (address.clone(), self.account_meta(address).ok().unwrap_or_default()))
.collect();
Ok(r)
}
/// Returns each hardware account along with name and meta.
pub fn is_hardware_address(&self, address: Address) -> bool {
self.hardware_store.as_ref().and_then(|s| s.wallet_info(&address)).is_some()
}
/// Returns each account along with name and meta. /// Returns each account along with name and meta.
pub fn account_meta(&self, address: Address) -> Result<AccountMeta, Error> { pub fn account_meta(&self, address: Address) -> Result<AccountMeta, Error> {
if let Some(info) = self.hardware_store.as_ref().and_then(|s| s.wallet_info(&address)) {
Ok(AccountMeta {
name: info.name,
meta: info.manufacturer,
uuid: None,
})
} else {
let account = self.sstore.account_ref(&address)?; let account = self.sstore.account_ref(&address)?;
Ok(AccountMeta { Ok(AccountMeta {
name: self.sstore.name(&account)?, name: self.sstore.name(&account)?,
@ -287,6 +358,7 @@ impl AccountProvider {
uuid: self.sstore.uuid(&account).ok().map(Into::into), // allowed to not have a Uuid uuid: self.sstore.uuid(&account).ok().map(Into::into), // allowed to not have a Uuid
}) })
} }
}
/// Returns each account along with name and meta. /// Returns each account along with name and meta.
pub fn set_account_name(&self, address: Address, name: String) -> Result<(), Error> { pub fn set_account_name(&self, address: Address, name: String) -> Result<(), Error> {
@ -505,6 +577,15 @@ impl AccountProvider {
self.sstore.set_vault_meta(name, meta) self.sstore.set_vault_meta(name, meta)
.map_err(Into::into) .map_err(Into::into)
} }
/// Sign transaction with hardware wallet.
pub fn sign_with_hardware(&self, address: Address, transaction: &[u8]) -> Result<Signature, SignError> {
match self.hardware_store.as_ref().map(|s| s.sign_transaction(&address, transaction)) {
None | Some(Err(HardwareError::KeyNotFound)) => Err(SignError::NotFound),
Some(Err(e)) => Err(From::from(e)),
Some(Ok(s)) => Ok(s),
}
}
} }
#[cfg(test)] #[cfg(test)]

View File

@ -105,6 +105,7 @@ extern crate linked_hash_map;
extern crate lru_cache; extern crate lru_cache;
extern crate ethcore_stratum; extern crate ethcore_stratum;
extern crate ethabi; extern crate ethabi;
extern crate hardware_wallet;
#[macro_use] #[macro_use]
extern crate log; extern crate log;

View File

@ -154,7 +154,7 @@ impl Transaction {
pub fn hash(&self, network_id: Option<u64>) -> H256 { pub fn hash(&self, network_id: Option<u64>) -> H256 {
let mut stream = RlpStream::new(); let mut stream = RlpStream::new();
self.rlp_append_unsigned_transaction(&mut stream, network_id); self.rlp_append_unsigned_transaction(&mut stream, network_id);
stream.out().sha3() stream.as_raw().sha3()
} }
/// Signs the transaction as coming from `sender`. /// Signs the transaction as coming from `sender`.

18
hw/Cargo.toml Normal file
View File

@ -0,0 +1,18 @@
[package]
description = "Hardware wallet support."
homepage = "http://parity.io"
license = "GPL-3.0"
name = "hardware-wallet"
version = "1.6.0"
authors = ["Parity Technologies <admin@parity.io>"]
[dependencies]
log = "0.3"
parking_lot = "0.3"
hidapi = { git = "https://github.com/ethcore/hidapi-rs" }
libusb = { git = "https://github.com/ethcore/libusb-rs" }
ethkey = { path = "../ethkey" }
ethcore-bigint = { path = "../util/bigint" }
[dev-dependencies]
rustc-serialize = "0.3"

364
hw/src/ledger.rs Normal file

File diff suppressed because one or more lines are too long

183
hw/src/lib.rs Normal file
View File

@ -0,0 +1,183 @@
// 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/>.
//! Hardware wallet management.
extern crate parking_lot;
extern crate hidapi;
extern crate libusb;
extern crate ethkey;
extern crate ethcore_bigint;
#[macro_use] extern crate log;
#[cfg(test)] extern crate rustc_serialize;
mod ledger;
use std::fmt;
use std::thread;
use std::sync::atomic;
use std::sync::{Arc, Weak};
use std::sync::atomic::AtomicBool;
use std::time::Duration;
use parking_lot::Mutex;
use ethkey::{Address, Signature};
pub use ledger::KeyPath;
/// Hardware waller error.
#[derive(Debug)]
pub enum Error {
/// Ledger device error.
LedgerDevice(ledger::Error),
/// USB error.
Usb(libusb::Error),
/// Hardware wallet not found for specified key.
KeyNotFound,
}
/// Hardware waller information.
#[derive(Debug, Clone)]
pub struct WalletInfo {
/// Wallet device name.
pub name: String,
/// Wallet device manufacturer.
pub manufacturer: String,
/// Wallet device serial number.
pub serial: String,
/// Ethereum address.
pub address: Address,
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
match *self {
Error::KeyNotFound => write!(f, "Key not found for given address."),
Error::LedgerDevice(ref e) => write!(f, "{}", e),
Error::Usb(ref e) => write!(f, "{}", e),
}
}
}
impl From<ledger::Error> for Error {
fn from(err: ledger::Error) -> Error {
match err {
ledger::Error::KeyNotFound => Error::KeyNotFound,
_ => Error::LedgerDevice(err),
}
}
}
impl From<libusb::Error> for Error {
fn from(err: libusb::Error) -> Error {
Error::Usb(err)
}
}
/// Hardware wallet management interface.
pub struct HardwareWalletManager {
update_thread: Option<thread::JoinHandle<()>>,
exiting: Arc<AtomicBool>,
ledger: Arc<Mutex<ledger::Manager>>,
}
struct EventHandler {
ledger: Weak<Mutex<ledger::Manager>>,
}
impl libusb::Hotplug for EventHandler {
fn device_arrived(&mut self, _device: libusb::Device) {
debug!("USB Device arrived");
if let Some(l) = self.ledger.upgrade() {
for _ in 0..10 {
// The device might not be visible right away. Try a few times.
if l.lock().update_devices().unwrap_or_else(|e| {
debug!("Error enumerating Ledger devices: {}", e);
0
}) > 0 {
break;
}
thread::sleep(Duration::from_millis(200));
}
}
}
fn device_left(&mut self, _device: libusb::Device) {
debug!("USB Device lost");
if let Some(l) = self.ledger.upgrade() {
if let Err(e) = l.lock().update_devices() {
debug!("Error enumerating Ledger devices: {}", e);
}
}
}
}
impl HardwareWalletManager {
pub fn new() -> Result<HardwareWalletManager, Error> {
let usb_context = Arc::new(libusb::Context::new()?);
let ledger = Arc::new(Mutex::new(ledger::Manager::new()?));
usb_context.register_callback(None, None, None, Box::new(EventHandler { ledger: Arc::downgrade(&ledger) }))?;
let exiting = Arc::new(AtomicBool::new(false));
let thread_exiting = exiting.clone();
let l = ledger.clone();
let thread = thread::Builder::new().name("hw_wallet".to_string()).spawn(move || {
if let Err(e) = l.lock().update_devices() {
debug!("Error updating ledger devices: {}", e);
}
loop {
usb_context.handle_events(Some(Duration::from_millis(500))).unwrap_or_else(|e| debug!("Error processing USB events: {}", e));
if thread_exiting.load(atomic::Ordering::Acquire) {
break;
}
}
}).ok();
Ok(HardwareWalletManager {
update_thread: thread,
exiting: exiting,
ledger: ledger,
})
}
/// Select key derivation path for a chain.
pub fn set_key_path(&self, key_path: KeyPath) {
self.ledger.lock().set_key_path(key_path);
}
/// List connected wallets. This only returns wallets that are ready to be used.
pub fn list_wallets(&self) -> Vec<WalletInfo> {
self.ledger.lock().list_devices()
}
/// Get connected wallet info.
pub fn wallet_info(&self, address: &Address) -> Option<WalletInfo> {
self.ledger.lock().device_info(address)
}
/// Sign transaction data with wallet managing `address`.
pub fn sign_transaction(&self, address: &Address, data: &[u8]) -> Result<Signature, Error> {
Ok(self.ledger.lock().sign_transaction(address, data)?)
}
}
impl Drop for HardwareWalletManager {
fn drop(&mut self) {
self.exiting.store(true, atomic::Ordering::Release);
if let Some(thread) = self.update_thread.take() {
thread.thread().unpark();
thread.join().ok();
}
}
}

View File

@ -18,7 +18,7 @@ use std::path::PathBuf;
use ethcore::ethstore::{EthStore, SecretStore, import_accounts, read_geth_accounts}; use ethcore::ethstore::{EthStore, SecretStore, import_accounts, read_geth_accounts};
use ethcore::ethstore::dir::RootDiskDirectory; use ethcore::ethstore::dir::RootDiskDirectory;
use ethcore::ethstore::SecretVaultRef; use ethcore::ethstore::SecretVaultRef;
use ethcore::account_provider::AccountProvider; use ethcore::account_provider::{AccountProvider, AccountProviderSettings};
use helpers::{password_prompt, password_from_file}; use helpers::{password_prompt, password_from_file};
use params::SpecType; use params::SpecType;
@ -92,7 +92,7 @@ fn new(n: NewAccount) -> Result<String, String> {
let dir = Box::new(keys_dir(n.path, n.spec)?); let dir = Box::new(keys_dir(n.path, n.spec)?);
let secret_store = Box::new(secret_store(dir, Some(n.iterations))?); let secret_store = Box::new(secret_store(dir, Some(n.iterations))?);
let acc_provider = AccountProvider::new(secret_store); let acc_provider = AccountProvider::new(secret_store, AccountProviderSettings::default());
let new_account = acc_provider.new_account(&password).map_err(|e| format!("Could not create new account: {}", e))?; let new_account = acc_provider.new_account(&password).map_err(|e| format!("Could not create new account: {}", e))?;
Ok(format!("{:?}", new_account)) Ok(format!("{:?}", new_account))
} }
@ -100,7 +100,7 @@ fn new(n: NewAccount) -> Result<String, String> {
fn list(list_cmd: ListAccounts) -> Result<String, String> { fn list(list_cmd: ListAccounts) -> Result<String, String> {
let dir = Box::new(keys_dir(list_cmd.path, list_cmd.spec)?); let dir = Box::new(keys_dir(list_cmd.path, list_cmd.spec)?);
let secret_store = Box::new(secret_store(dir, None)?); let secret_store = Box::new(secret_store(dir, None)?);
let acc_provider = AccountProvider::new(secret_store); let acc_provider = AccountProvider::new(secret_store, AccountProviderSettings::default());
let accounts = acc_provider.accounts(); let accounts = acc_provider.accounts();
let result = accounts.into_iter() let result = accounts.into_iter()
.map(|a| format!("{:?}", a)) .map(|a| format!("{:?}", a))

View File

@ -101,6 +101,9 @@ usage! {
or |c: &Config| otry!(c.account).password.clone(), or |c: &Config| otry!(c.account).password.clone(),
flag_keys_iterations: u32 = 10240u32, flag_keys_iterations: u32 = 10240u32,
or |c: &Config| otry!(c.account).keys_iterations.clone(), or |c: &Config| otry!(c.account).keys_iterations.clone(),
flag_no_hardware_wallets: bool = false,
or |c: &Config| otry!(c.account).disable_hardware.clone(),
flag_force_ui: bool = false, flag_force_ui: bool = false,
or |c: &Config| otry!(c.ui).force.clone(), or |c: &Config| otry!(c.ui).force.clone(),
@ -347,6 +350,7 @@ struct Account {
unlock: Option<Vec<String>>, unlock: Option<Vec<String>>,
password: Option<Vec<String>>, password: Option<Vec<String>>,
keys_iterations: Option<u32>, keys_iterations: Option<u32>,
disable_hardware: Option<bool>,
} }
#[derive(Default, Debug, PartialEq, RustcDecodable)] #[derive(Default, Debug, PartialEq, RustcDecodable)]
@ -583,6 +587,7 @@ mod tests {
flag_unlock: Some("0xdeadbeefcafe0000000000000000000000000000".into()), flag_unlock: Some("0xdeadbeefcafe0000000000000000000000000000".into()),
flag_password: vec!["~/.safe/password.file".into()], flag_password: vec!["~/.safe/password.file".into()],
flag_keys_iterations: 10240u32, flag_keys_iterations: 10240u32,
flag_no_hardware_wallets: false,
flag_force_ui: false, flag_force_ui: false,
flag_no_ui: false, flag_no_ui: false,
@ -769,6 +774,7 @@ mod tests {
unlock: Some(vec!["0x1".into(), "0x2".into(), "0x3".into()]), unlock: Some(vec!["0x1".into(), "0x2".into(), "0x3".into()]),
password: Some(vec!["passwdfile path".into()]), password: Some(vec!["passwdfile path".into()]),
keys_iterations: None, keys_iterations: None,
disable_hardware: None,
}), }),
ui: Some(Ui { ui: Some(Ui {
force: None, force: None,

View File

@ -78,6 +78,7 @@ Account Options:
--keys-iterations NUM Specify the number of iterations to use when --keys-iterations NUM Specify the number of iterations to use when
deriving key from the password (bigger is more deriving key from the password (bigger is more
secure) (default: {flag_keys_iterations}). secure) (default: {flag_keys_iterations}).
--no-hardware-wallets Disables hardware wallet support. (default: {flag_no_hardware_wallets})
UI Options: UI Options:
--force-ui Enable Trusted UI WebSocket endpoint, --force-ui Enable Trusted UI WebSocket endpoint,

View File

@ -461,6 +461,7 @@ impl Configuration {
testnet: self.args.flag_testnet, testnet: self.args.flag_testnet,
password_files: self.args.flag_password.clone(), password_files: self.args.flag_password.clone(),
unlocked_accounts: to_addresses(&self.args.flag_unlock)?, unlocked_accounts: to_addresses(&self.args.flag_unlock)?,
enable_hardware_wallets: !self.args.flag_no_hardware_wallets,
}; };
Ok(cfg) Ok(cfg)

View File

@ -175,6 +175,7 @@ pub struct AccountsConfig {
pub testnet: bool, pub testnet: bool,
pub password_files: Vec<String>, pub password_files: Vec<String>,
pub unlocked_accounts: Vec<Address>, pub unlocked_accounts: Vec<Address>,
pub enable_hardware_wallets: bool,
} }
impl Default for AccountsConfig { impl Default for AccountsConfig {
@ -184,6 +185,7 @@ impl Default for AccountsConfig {
testnet: false, testnet: false,
password_files: Vec::new(), password_files: Vec::new(),
unlocked_accounts: Vec::new(), unlocked_accounts: Vec::new(),
enable_hardware_wallets: true,
} }
} }
} }

View File

@ -16,7 +16,7 @@
use ethcore::ethstore::{PresaleWallet, EthStore}; use ethcore::ethstore::{PresaleWallet, EthStore};
use ethcore::ethstore::dir::RootDiskDirectory; use ethcore::ethstore::dir::RootDiskDirectory;
use ethcore::account_provider::AccountProvider; use ethcore::account_provider::{AccountProvider, AccountProviderSettings};
use helpers::{password_prompt, password_from_file}; use helpers::{password_prompt, password_from_file};
use params::SpecType; use params::SpecType;
@ -37,7 +37,7 @@ pub fn execute(cmd: ImportWallet) -> Result<String, String> {
let dir = Box::new(RootDiskDirectory::create(cmd.path).unwrap()); let dir = Box::new(RootDiskDirectory::create(cmd.path).unwrap());
let secret_store = Box::new(EthStore::open_with_iterations(dir, cmd.iterations).unwrap()); let secret_store = Box::new(EthStore::open_with_iterations(dir, cmd.iterations).unwrap());
let acc_provider = AccountProvider::new(secret_store); let acc_provider = AccountProvider::new(secret_store, AccountProviderSettings::default());
let wallet = PresaleWallet::open(cmd.wallet_path).map_err(|_| "Unable to open presale wallet.")?; let wallet = PresaleWallet::open(cmd.wallet_path).map_err(|_| "Unable to open presale wallet.")?;
let kp = wallet.decrypt(&password).map_err(|_| "Invalid password.")?; let kp = wallet.decrypt(&password).map_err(|_| "Invalid password.")?;
let address = acc_provider.insert_account(kp.secret().clone(), &password).unwrap(); let address = acc_provider.insert_account(kp.secret().clone(), &password).unwrap();

View File

@ -26,7 +26,7 @@ use ethcore_logger::{Config as LogConfig};
use ethcore::miner::{StratumOptions, Stratum}; use ethcore::miner::{StratumOptions, Stratum};
use ethcore::client::{Mode, DatabaseCompactionProfile, VMType, BlockChainClient}; use ethcore::client::{Mode, DatabaseCompactionProfile, VMType, BlockChainClient};
use ethcore::service::ClientService; use ethcore::service::ClientService;
use ethcore::account_provider::AccountProvider; use ethcore::account_provider::{AccountProvider, AccountProviderSettings};
use ethcore::miner::{Miner, MinerService, ExternalMiner, MinerOptions}; use ethcore::miner::{Miner, MinerService, ExternalMiner, MinerOptions};
use ethcore::snapshot; use ethcore::snapshot;
use ethcore::verification::queue::VerifierSettings; use ethcore::verification::queue::VerifierSettings;
@ -516,9 +516,13 @@ fn prepare_account_provider(spec: &SpecType, dirs: &Directories, data_dir: &str,
let path = dirs.keys_path(data_dir); let path = dirs.keys_path(data_dir);
upgrade_key_location(&dirs.legacy_keys_path(cfg.testnet), &path); upgrade_key_location(&dirs.legacy_keys_path(cfg.testnet), &path);
let dir = Box::new(RootDiskDirectory::create(&path).map_err(|e| format!("Could not open keys directory: {}", e))?); let dir = Box::new(RootDiskDirectory::create(&path).map_err(|e| format!("Could not open keys directory: {}", e))?);
let account_provider = AccountProvider::new(Box::new( let account_settings = AccountProviderSettings {
EthStore::open_with_iterations(dir, cfg.iterations).map_err(|e| format!("Could not open keys directory: {}", e))? enable_hardware_wallets: cfg.enable_hardware_wallets,
)); hardware_wallet_classic_key: spec == &SpecType::Classic,
};
let account_provider = AccountProvider::new(
Box::new(EthStore::open_with_iterations(dir, cfg.iterations).map_err(|e| format!("Could not open keys directory: {}", e))?),
account_settings);
for a in cfg.unlocked_accounts { for a in cfg.unlocked_accounts {
// Check if the account exists // Check if the account exists

View File

@ -16,7 +16,7 @@
use std::fmt::Debug; use std::fmt::Debug;
use std::ops::Deref; use std::ops::Deref;
use rlp; use rlp::{self, Stream};
use util::{Address, H520, H256, U256, Uint, Bytes}; use util::{Address, H520, H256, U256, Uint, Bytes};
use util::bytes::ToPretty; use util::bytes::ToPretty;
use util::sha3::Hashable; use util::sha3::Hashable;
@ -190,11 +190,26 @@ pub fn sign_no_dispatch<C, M>(client: &C, miner: &M, accounts: &AccountProvider,
}; };
let hash = t.hash(network_id); let hash = t.hash(network_id);
if accounts.is_hardware_address(address) {
let mut stream = rlp::RlpStream::new();
t.rlp_append_unsigned_transaction(&mut stream, network_id);
let signature = accounts.sign_with_hardware(address, &stream.as_raw())
.map_err(|e| {
debug!(target: "miner", "Error signing transaction with hardware wallet: {}", e);
errors::account("Error signing transaction with hardware wallet", e)
})?;
WithToken::No(SignedTransaction::new(t.with_signature(signature, network_id))
.map_err(|e| {
debug!(target: "miner", "Hardware wallet has produced invalid signature: {}", e);
errors::account("Invalid signature generated", e)
})?)
} else {
let signature = signature(accounts, address, hash, password)?; let signature = signature(accounts, address, hash, password)?;
signature.map(|sig| { signature.map(|sig| {
SignedTransaction::new(t.with_signature(sig, network_id)) SignedTransaction::new(t.with_signature(sig, network_id))
.expect("Transaction was signed by AccountsProvider; it never produces invalid signatures; qed") .expect("Transaction was signed by AccountsProvider; it never produces invalid signatures; qed")
}) })
}
}; };
Ok(signed_transaction) Ok(signed_transaction)
} }

View File

@ -45,6 +45,7 @@ use v1::types::{
TransactionStats, LocalTransactionStatus, TransactionStats, LocalTransactionStatus,
BlockNumber, ConsensusCapability, VersionInfo, BlockNumber, ConsensusCapability, VersionInfo,
OperationsInfo, DappId, ChainStatus, OperationsInfo, DappId, ChainStatus,
AccountInfo, HwAccountInfo
}; };
/// Parity implementation. /// Parity implementation.
@ -111,7 +112,7 @@ impl<C, M, S: ?Sized, U> Parity for ParityClient<C, M, S, U> where
{ {
type Metadata = Metadata; type Metadata = Metadata;
fn accounts_info(&self, dapp: Trailing<DappId>) -> Result<BTreeMap<String, BTreeMap<String, String>>, Error> { fn accounts_info(&self, dapp: Trailing<DappId>) -> Result<BTreeMap<H160, AccountInfo>, Error> {
let dapp = dapp.0; let dapp = dapp.0;
let store = take_weak!(self.accounts); let store = take_weak!(self.accounts);
@ -128,12 +129,17 @@ impl<C, M, S: ?Sized, U> Parity for ParityClient<C, M, S, U> where
.into_iter() .into_iter()
.chain(other.into_iter()) .chain(other.into_iter())
.filter(|&(ref a, _)| dapp_accounts.contains(a)) .filter(|&(ref a, _)| dapp_accounts.contains(a))
.map(|(a, v)| { .map(|(a, v)| (H160::from(a), AccountInfo { name: v.name }))
let m = map![ .collect()
"name".to_owned() => v.name )
]; }
(format!("0x{}", a.hex()), m)
}) fn hardware_accounts_info(&self) -> Result<BTreeMap<H160, HwAccountInfo>, Error> {
let store = take_weak!(self.accounts);
let info = store.hardware_accounts_info().map_err(|e| errors::account("Could not fetch account info.", e))?;
Ok(info
.into_iter()
.map(|(a, v)| (H160::from(a), HwAccountInfo { name: v.name, manufacturer: v.meta }))
.collect() .collect()
) )
} }

View File

@ -16,7 +16,7 @@
use std::sync::Arc; use std::sync::Arc;
use ethcore::account_provider::AccountProvider; use ethcore::account_provider::{AccountProvider, AccountProviderSettings};
use ethstore::EthStore; use ethstore::EthStore;
use ethstore::dir::RootDiskDirectory; use ethstore::dir::RootDiskDirectory;
use devtools::RandomTempPath; use devtools::RandomTempPath;
@ -36,7 +36,7 @@ fn accounts_provider() -> Arc<AccountProvider> {
fn accounts_provider_with_vaults_support(temp_path: &str) -> Arc<AccountProvider> { fn accounts_provider_with_vaults_support(temp_path: &str) -> Arc<AccountProvider> {
let root_keys_dir = RootDiskDirectory::create(temp_path).unwrap(); let root_keys_dir = RootDiskDirectory::create(temp_path).unwrap();
let secret_store = EthStore::open(Box::new(root_keys_dir)).unwrap(); let secret_store = EthStore::open(Box::new(root_keys_dir)).unwrap();
Arc::new(AccountProvider::new(Box::new(secret_store))) Arc::new(AccountProvider::new(Box::new(secret_store), AccountProviderSettings::default()))
} }
fn setup_with_accounts_provider(accounts_provider: Arc<AccountProvider>) -> ParityAccountsTester { fn setup_with_accounts_provider(accounts_provider: Arc<AccountProvider>) -> ParityAccountsTester {

View File

@ -28,6 +28,7 @@ use v1::types::{
TransactionStats, LocalTransactionStatus, TransactionStats, LocalTransactionStatus,
BlockNumber, ConsensusCapability, VersionInfo, BlockNumber, ConsensusCapability, VersionInfo,
OperationsInfo, DappId, ChainStatus, OperationsInfo, DappId, ChainStatus,
AccountInfo, HwAccountInfo,
}; };
build_rpc_trait! { build_rpc_trait! {
@ -37,7 +38,11 @@ build_rpc_trait! {
/// Returns accounts information. /// Returns accounts information.
#[rpc(name = "parity_accountsInfo")] #[rpc(name = "parity_accountsInfo")]
fn accounts_info(&self, Trailing<DappId>) -> Result<BTreeMap<String, BTreeMap<String, String>>, Error>; fn accounts_info(&self, Trailing<DappId>) -> Result<BTreeMap<H160, AccountInfo>, Error>;
/// Returns hardware accounts information.
#[rpc(name = "parity_hardwareAccountsInfo")]
fn hardware_accounts_info(&self) -> Result<BTreeMap<H160, HwAccountInfo>, Error>;
/// Returns default account for dapp. /// Returns default account for dapp.
#[rpc(meta, name = "parity_defaultAccount")] #[rpc(meta, name = "parity_defaultAccount")]

View File

@ -0,0 +1,33 @@
// 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/>.
use serde::{Serialize};
/// Account information.
#[derive(Debug, Default, Clone, PartialEq, Serialize)]
pub struct AccountInfo {
/// Account name
pub name: String,
}
/// Hardware wallet information.
#[derive(Debug, Default, Clone, PartialEq, Serialize)]
pub struct HwAccountInfo {
/// Device name.
pub name: String,
/// Device manufacturer.
pub manufacturer: String,
}

View File

@ -14,6 +14,7 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
mod account_info;
mod bytes; mod bytes;
mod block; mod block;
mod block_number; mod block_number;
@ -65,3 +66,4 @@ pub use self::uint::{U128, U256};
pub use self::work::Work; pub use self::work::Work;
pub use self::histogram::Histogram; pub use self::histogram::Histogram;
pub use self::consensus_status::*; pub use self::consensus_status::*;
pub use self::account_info::{AccountInfo, HwAccountInfo};