Hardware Wallet trait (#8071)

* getting started with replacing HardwareWalletManager

* trezor and ledger impls the new trait with some drawbacks

* Everything move to the new trait

* It required lifetime annotations in the trait because [u8] in unsized
* Lets now start moving entry point from HardwareWalletManager

* rename trait to Wallet

* move thread management to the actual wallets

* Moved thread management to each respective Wallet
* Cleaned up pub items that is needed to be pub
* Wallet trait more or less finished
* Cleaned up docs

* fix tests

* omit removed docs

* fix spelling, naming och remove old comments

* ledger test is broken, add correct logging format

* So locally on my machine Linux Ubuntu 17.10 the test doesn't panic but on the CI server libusb::Context::new()
fails which I don't understand because it has worked before
* Additionally the ledger test is optional so I lean toward ignoring it the CI Server

* ignore hardware tests by default

* more verbose checking in ledger test
This commit is contained in:
Niklas Adolfsson 2018-05-01 15:01:49 +02:00 committed by Afri Schoedon
parent 7a76916143
commit 849f5d9a44
3 changed files with 469 additions and 396 deletions

View File

@ -20,8 +20,11 @@
use std::cmp::min;
use std::fmt;
use std::str::FromStr;
use std::sync::atomic;
use std::sync::atomic::AtomicBool;
use std::sync::{Arc, Weak};
use std::time::{Duration, Instant};
use std::thread;
use ethereum_types::{H256, Address};
use ethkey::Signature;
@ -29,12 +32,12 @@ use hidapi;
use libusb;
use parking_lot::{Mutex, RwLock};
use super::{WalletInfo, KeyPath};
use super::{WalletInfo, KeyPath, Device, Wallet, USB_DEVICE_CLASS_DEVICE};
/// Ledger vendor ID
pub const LEDGER_VID: u16 = 0x2c97;
/// Legder product IDs: [Nano S and Blue]
pub const LEDGER_PIDS: [u16; 2] = [0x0000, 0x0001];
const LEDGER_VID: u16 = 0x2c97;
/// Ledger product IDs: [Nano S and Blue]
const LEDGER_PIDS: [u16; 2] = [0x0000, 0x0001];
const ETH_DERIVATION_PATH_BE: [u8; 17] = [4, 0x80, 0, 0, 44, 0x80, 0, 0, 60, 0x80, 0, 0, 0, 0, 0, 0, 0]; // 44'/60'/0'/0
const ETC_DERIVATION_PATH_BE: [u8; 21] = [5, 0x80, 0, 0, 44, 0x80, 0, 0, 60, 0x80, 0x02, 0x73, 0xd0, 0x80, 0, 0, 0, 0, 0, 0, 0]; // 44'/60'/160720'/0'/0
@ -64,8 +67,10 @@ pub enum Error {
KeyNotFound,
/// Signing has been cancelled by user.
UserCancel,
/// Invalid Device
/// Invalid device
InvalidDevice,
/// Impossible error
ImpossibleError,
}
impl fmt::Display for Error {
@ -77,6 +82,7 @@ impl fmt::Display for Error {
Error::KeyNotFound => write!(f, "Key not found"),
Error::UserCancel => write!(f, "Operation has been cancelled"),
Error::InvalidDevice => write!(f, "Unsupported product was entered"),
Error::ImpossibleError => write!(f, "Placeholder error"),
}
}
}
@ -100,157 +106,43 @@ pub struct Manager {
key_path: RwLock<KeyPath>,
}
#[derive(Debug)]
struct Device {
path: String,
info: WalletInfo,
}
impl Manager {
/// Create a new instance.
pub fn new(hidapi: Arc<Mutex<hidapi::HidApi>>) -> Manager {
Manager {
pub fn new(hidapi: Arc<Mutex<hidapi::HidApi>>, exiting: Arc<AtomicBool>) -> Result<Arc<Manager>, libusb::Error> {
let manager = Arc::new(Manager {
usb: hidapi,
devices: RwLock::new(Vec::new()),
key_path: RwLock::new(KeyPath::Ethereum),
}
}
});
/// Re-populate device list. Only those devices that have Ethereum app open will be added.
pub fn update_devices(&self) -> Result<usize, Error> {
let mut usb = self.usb.lock();
usb.refresh_devices();
let devices = usb.devices();
let mut new_devices = Vec::new();
let mut num_new_devices = 0;
for device in devices {
trace!("Checking device: {:?}", device);
if device.vendor_id != LEDGER_VID || !LEDGER_PIDS.contains(&device.product_id) {
continue;
}
match self.read_device_info(&usb, &device) {
Ok(info) => {
debug!("Found device: {:?}", info);
if !self.devices.read().iter().any(|d| d.path == info.path) {
num_new_devices += 1;
}
new_devices.push(info);
let usb_context = Arc::new(libusb::Context::new()?);
let m = manager.clone();
}
Err(e) => debug!("Error reading device info: {}", e),
};
}
*self.devices.write() = new_devices;
Ok(num_new_devices)
}
// Subscribe to all Ledger devices
// This means that we need to check that the given productID is supported
// None => LIBUSB_HOTPLUG_MATCH_ANY, in other words that all are subscribed to
// More info can be found: <http://libusb.sourceforge.net/api-1.0/group__hotplug.html#gae6c5f1add6cc754005549c7259dc35ea>
usb_context.register_callback(
Some(LEDGER_VID), None, Some(USB_DEVICE_CLASS_DEVICE),
Box::new(EventHandler::new(Arc::downgrade(&manager)))).expect("usb_callback");
/// Select key derivation path for a known chain.
pub fn set_key_path(&self, key_path: KeyPath) {
*self.key_path.write() = key_path;
// Ledger event handler thread
thread::Builder::new()
.spawn(move || {
if let Err(e) = m.update_devices() {
debug!(target: "hw", "Ledger couldn't connect at startup, error: {}", e);
}
fn read_device_info(&self, usb: &hidapi::HidApi, dev_info: &hidapi::HidDeviceInfo) -> Result<Device, Error> {
let mut handle = self.open_path(|| usb.open_path(&dev_info.path))?;
let address = Self::read_wallet_address(&mut handle, *self.key_path.read())?;
let manufacturer = dev_info.manufacturer_string.clone().unwrap_or("Unknown".to_owned());
let name = dev_info.product_string.clone().unwrap_or("Unknown".to_owned());
let serial = dev_info.serial_number.clone().unwrap_or("Unknown".to_owned());
Ok(Device {
path: dev_info.path.clone(),
info: WalletInfo {
name: name,
manufacturer: manufacturer,
serial: serial,
address: address,
},
})
}
fn read_wallet_address(handle: &hidapi::HidDevice, key_path: KeyPath) -> Result<Address, Error> {
let ver = Self::send_apdu(handle, commands::GET_APP_CONFIGURATION, 0, 0, &[])?;
if ver.len() != 4 {
return Err(Error::Protocol("Version packet size mismatch"));
}
let (major, minor, patch) = (ver[1], ver[2], ver[3]);
if major < 1 || (major == 1 && minor == 0 && patch < 3) {
return Err(Error::Protocol("App version 1.0.3 is required."));
}
let eth_path = &ETH_DERIVATION_PATH_BE[..];
let etc_path = &ETC_DERIVATION_PATH_BE[..];
let derivation_path = match key_path {
KeyPath::Ethereum => eth_path,
KeyPath::EthereumClassic => etc_path,
};
let key_and_address = Self::send_apdu(handle, commands::GET_ETH_PUBLIC_ADDRESS, 0, 0, derivation_path)?;
if key_and_address.len() != 107 { // 1 + 65 PK + 1 + 40 Addr (ascii-hex)
return Err(Error::Protocol("Key packet size mismatch"));
}
let address_string = ::std::str::from_utf8(&key_and_address[67..107])
.map_err(|_| Error::Protocol("Invalid address string"))?;
let address = Address::from_str(&address_string)
.map_err(|_| Error::Protocol("Invalid address string"))?;
Ok(address)
}
/// List connected wallets. This only returns wallets that are ready to be used.
pub fn list_devices(&self) -> Vec<WalletInfo> {
self.devices.read().iter().map(|d| d.info.clone()).collect()
}
/// Get wallet info.
pub fn device_info(&self, address: &Address) -> Option<WalletInfo> {
self.devices.read().iter().find(|d| &d.info.address == address).map(|d| d.info.clone())
}
/// Sign transaction data with wallet managing `address`.
pub fn sign_transaction(&self, address: &Address, data: &[u8]) -> Result<Signature, Error> {
let usb = self.usb.lock();
let devices = self.devices.read();
let device = devices.iter().find(|d| &d.info.address == address).ok_or(Error::KeyNotFound)?;
let handle = self.open_path(|| usb.open_path(&device.path))?;
let eth_path = &ETH_DERIVATION_PATH_BE[..];
let etc_path = &ETC_DERIVATION_PATH_BE[..];
let derivation_path = match *self.key_path.read() {
KeyPath::Ethereum => eth_path,
KeyPath::EthereumClassic => etc_path,
};
const MAX_CHUNK_SIZE: usize = 255;
let mut chunk: [u8; MAX_CHUNK_SIZE] = [0; MAX_CHUNK_SIZE];
&mut chunk[0..derivation_path.len()].copy_from_slice(derivation_path);
let mut dest_offset = derivation_path.len();
let mut data_pos = 0;
let mut result;
loop {
let p1 = if data_pos == 0 { 0x00 } else { 0x80 };
let dest_left = MAX_CHUNK_SIZE - dest_offset;
let chunk_data_size = min(dest_left, data.len() - data_pos);
&mut chunk[dest_offset..][0..chunk_data_size].copy_from_slice(&data[data_pos..][0..chunk_data_size]);
result = Self::send_apdu(&handle, commands::SIGN_ETH_TRANSACTION, p1, 0, &chunk[0..(dest_offset + chunk_data_size)])?;
dest_offset = 0;
data_pos += chunk_data_size;
if data_pos == data.len() {
usb_context.handle_events(Some(Duration::from_millis(500)))
.unwrap_or_else(|e| debug!(target: "hw", "Ledger event handler error: {}", e));
if exiting.load(atomic::Ordering::Acquire) {
break;
}
}
})
.ok();
if result.len() != 65 {
return Err(Error::Protocol("Signature packet size mismatch"));
}
let v = (result[0] + 1) % 2;
let r = H256::from_slice(&result[1..33]);
let s = H256::from_slice(&result[33..65]);
Ok(Signature::from_rsv(&r, &s, v))
}
fn open_path<R, F>(&self, f: F) -> Result<R, Error>
where F: Fn() -> Result<R, &'static str>
{
f().map_err(Into::into)
Ok(manager)
}
fn send_apdu(handle: &hidapi::HidDevice, command: u8, p1: u8, p2: u8, data: &[u8]) -> Result<Vec<u8>, Error> {
@ -285,7 +177,7 @@ impl Manager {
chunk_index += 1;
}
// read response
// Read response
chunk_index = 0;
let mut message_size = 0;
let mut message = Vec::new();
@ -303,7 +195,7 @@ impl Manager {
let mut offset = 5;
if seq == 0 {
// read message size and status word.
// Read message size and status word.
if chunk_size < 7 {
return Err(Error::Protocol("Unexpected chunk header"));
}
@ -321,7 +213,7 @@ impl Manager {
return Err(Error::Protocol("No status word"));
}
let status = (message[message.len() - 2] as usize) << 8 | (message[message.len() - 1] as usize);
debug!("Read status {:x}", status);
debug!(target: "hw", "Read status {:x}", status);
match status {
0x6700 => Err(Error::Protocol("Incorrect length")),
0x6982 => Err(Error::Protocol("Security status not satisfied (Canceled by user)")),
@ -365,18 +257,166 @@ fn try_connect_polling(ledger: Arc<Manager>, timeout: Duration) -> bool {
false
}
impl <'a>Wallet<'a> for Manager {
type Error = Error;
type Transaction = &'a [u8];
fn sign_transaction(&self, address: &Address, transaction: Self::Transaction) -> Result<Signature, Self::Error> {
let usb = self.usb.lock();
let devices = self.devices.read();
let device = devices.iter().find(|d| &d.info.address == address).ok_or(Error::KeyNotFound)?;
let handle = self.open_path(|| usb.open_path(&device.path))?;
let eth_path = &ETH_DERIVATION_PATH_BE[..];
let etc_path = &ETC_DERIVATION_PATH_BE[..];
let derivation_path = match *self.key_path.read() {
KeyPath::Ethereum => eth_path,
KeyPath::EthereumClassic => etc_path,
};
const MAX_CHUNK_SIZE: usize = 255;
let mut chunk: [u8; MAX_CHUNK_SIZE] = [0; MAX_CHUNK_SIZE];
&mut chunk[0..derivation_path.len()].copy_from_slice(derivation_path);
let mut dest_offset = derivation_path.len();
let mut data_pos = 0;
let mut result;
loop {
let p1 = if data_pos == 0 { 0x00 } else { 0x80 };
let dest_left = MAX_CHUNK_SIZE - dest_offset;
let chunk_data_size = min(dest_left, transaction.len() - data_pos);
&mut chunk[dest_offset..][0..chunk_data_size].copy_from_slice(&transaction[data_pos..][0..chunk_data_size]);
result = Self::send_apdu(&handle, commands::SIGN_ETH_TRANSACTION, p1, 0, &chunk[0..(dest_offset + chunk_data_size)])?;
dest_offset = 0;
data_pos += chunk_data_size;
if data_pos == transaction.len() {
break;
}
}
if result.len() != 65 {
return Err(Error::Protocol("Signature packet size mismatch"));
}
let v = (result[0] + 1) % 2;
let r = H256::from_slice(&result[1..33]);
let s = H256::from_slice(&result[33..65]);
Ok(Signature::from_rsv(&r, &s, v))
}
fn set_key_path(&self, key_path: KeyPath) {
*self.key_path.write() = key_path;
}
fn update_devices(&self) -> Result<usize, Self::Error> {
let mut usb = self.usb.lock();
usb.refresh_devices();
let devices = usb.devices();
let mut new_devices = Vec::new();
let mut num_new_devices = 0;
for device in devices {
trace!("Checking device: {:?}", device);
if device.vendor_id != LEDGER_VID || !LEDGER_PIDS.contains(&device.product_id) {
continue;
}
match self.read_device(&usb, &device) {
Ok(info) => {
debug!(target: "hw", "Found device: {:?}", info);
if !self.devices.read().iter().any(|d| d.path == info.path) {
num_new_devices += 1;
}
new_devices.push(info);
}
Err(e) => debug!(target: "hw", "Error reading device info: {}", e),
};
}
*self.devices.write() = new_devices;
Ok(num_new_devices)
}
fn read_device(&self, usb: &hidapi::HidApi, dev_info: &hidapi::HidDeviceInfo) -> Result<Device, Self::Error> {
let handle = self.open_path(|| usb.open_path(&dev_info.path))?;
let manufacturer = dev_info.manufacturer_string.clone().unwrap_or("Unknown".to_owned());
let name = dev_info.product_string.clone().unwrap_or("Unknown".to_owned());
let serial = dev_info.serial_number.clone().unwrap_or("Unknown".to_owned());
match self.get_address(&handle) {
Ok(Some(addr)) => {
Ok(Device {
path: dev_info.path.clone(),
info: WalletInfo {
name: name,
manufacturer: manufacturer,
serial: serial,
address: addr,
},
})
}
// This variant is not possible, but the trait forces this return type
Ok(None) => Err(Error::ImpossibleError),
Err(e) => Err(e),
}
}
fn list_devices(&self) -> Vec<WalletInfo> {
self.devices.read().iter().map(|d| d.info.clone()).collect()
}
// Not used because it is not supported by Ledger
fn list_locked_devices(&self) -> Vec<String> {
vec![]
}
fn get_wallet(&self, address: &Address) -> Option<WalletInfo> {
self.devices.read().iter().find(|d| &d.info.address == address).map(|d| d.info.clone())
}
fn get_address(&self, device: &hidapi::HidDevice) -> Result<Option<Address>, Self::Error> {
let ver = Self::send_apdu(device, commands::GET_APP_CONFIGURATION, 0, 0, &[])?;
if ver.len() != 4 {
return Err(Error::Protocol("Version packet size mismatch"));
}
let (major, minor, patch) = (ver[1], ver[2], ver[3]);
if major < 1 || (major == 1 && minor == 0 && patch < 3) {
return Err(Error::Protocol("App version 1.0.3 is required."));
}
let eth_path = &ETH_DERIVATION_PATH_BE[..];
let etc_path = &ETC_DERIVATION_PATH_BE[..];
let derivation_path = match *self.key_path.read() {
KeyPath::Ethereum => eth_path,
KeyPath::EthereumClassic => etc_path,
};
let key_and_address = Self::send_apdu(device, commands::GET_ETH_PUBLIC_ADDRESS, 0, 0, derivation_path)?;
if key_and_address.len() != 107 { // 1 + 65 PK + 1 + 40 Addr (ascii-hex)
return Err(Error::Protocol("Key packet size mismatch"));
}
let address_string = ::std::str::from_utf8(&key_and_address[67..107])
.map_err(|_| Error::Protocol("Invalid address string"))?;
let address = Address::from_str(&address_string)
.map_err(|_| Error::Protocol("Invalid address string"))?;
Ok(Some(address))
}
fn open_path<R, F>(&self, f: F) -> Result<R, Self::Error>
where F: Fn() -> Result<R, &'static str>
{
f().map_err(Into::into)
}
}
/// Ledger event handler
/// A seperate thread is hanedling incoming events
/// A separate thread is handling incoming events
///
/// Note, that this run to completion and race-conditions can't occur but this can
/// therefore starve other events for being process with a spinlock or similar
pub struct EventHandler {
struct EventHandler {
ledger: Weak<Manager>,
}
impl EventHandler {
/// Ledger event handler constructor
pub fn new(ledger: Weak<Manager>) -> Self {
fn new(ledger: Weak<Manager>) -> Self {
Self { ledger: ledger }
}
}
@ -402,16 +442,24 @@ impl libusb::Hotplug for EventHandler {
}
#[test]
#[ignore]
/// This test can't be run without an actual ledger device connected
fn smoke() {
use rustc_hex::FromHex;
let hidapi = Arc::new(Mutex::new(hidapi::HidApi::new().unwrap()));
let manager = Manager::new(hidapi.clone());
manager.update_devices().unwrap();
for d in &*manager.devices.read() {
println!("Device: {:?}", d);
}
let hidapi = Arc::new(Mutex::new(hidapi::HidApi::new().expect("HidApi")));
let manager = Manager::new(hidapi.clone(), Arc::new(AtomicBool::new(false))).expect("Ledger Manager");
// Update device list
assert_eq!(try_connect_polling(manager.clone(), Duration::from_millis(500)), true);
// Fetch the ethereum address of a connected ledger device
let address = manager.list_devices()
.iter()
.filter(|d| d.manufacturer == "Ledger".to_string())
.nth(0)
.map(|d| d.address.clone())
.expect("No ledger device detected");
if let Some(address) = manager.list_devices().first().map(|d| d.address.clone()) {
let tx = FromHex::from_hex("eb018504a817c80082520894a6ca2e6707f2cc189794a9dd459d5b05ed1bcd1c8703f26fcfb7a22480018080").unwrap();
let signature = manager.sign_transaction(&address, &tx);
println!("Got {:?}", signature);
@ -425,4 +473,3 @@ fn smoke() {
println!("Got {:?}", signature);
assert!(signature.is_ok());
}
}

View File

@ -16,7 +16,8 @@
//! Hardware wallet management.
#![warn(missing_docs)]
#[warn(missing_docs)]
#[warn(warnings)]
extern crate ethereum_types;
extern crate ethkey;
@ -38,12 +39,60 @@ use std::fmt;
use std::sync::Arc;
use std::sync::atomic;
use std::sync::atomic::AtomicBool;
use std::thread;
use std::time::Duration;
use ethereum_types::U256;
const USB_DEVICE_CLASS_DEVICE: u8 = 0;
#[derive(Debug)]
pub struct Device {
path: String,
info: WalletInfo,
}
pub trait Wallet<'a> {
/// Error
type Error;
/// Transaction data format
type Transaction;
/// Sign transaction data with wallet managing `address`.
fn sign_transaction(&self, address: &Address, transaction: Self::Transaction) -> Result<Signature, Self::Error>;
/// Set key derivation path for a chain.
fn set_key_path(&self, key_path: KeyPath);
/// Re-populate device list
/// Note, this assumes all devices are iterated over and updated
fn update_devices(&self) -> Result<usize, Self::Error>;
/// Read device info
fn read_device(&self, usb: &hidapi::HidApi, dev_info: &hidapi::HidDeviceInfo) -> Result<Device, Self::Error>;
/// List connected and acknowledged wallets
fn list_devices(&self) -> Vec<WalletInfo>;
/// List locked wallets
/// This may be moved if it is the wrong assumption, for example this is not supported by Ledger
/// Then this method return a empty vector
fn list_locked_devices(&self) -> Vec<String>;
/// Get wallet info.
fn get_wallet(&self, address: &Address) -> Option<WalletInfo>;
/// Generate ethereum address for a Wallet
fn get_address(&self, device: &hidapi::HidDevice) -> Result<Option<Address>, Self::Error>;
/// Open a device using `device path`
/// Note, f - is a closure that borrows HidResult<HidDevice>
/// HidDevice is in turn a type alias for a `c_void function pointer`
/// For further information see:
/// * <https://github.com/paritytech/hidapi-rs>
/// * <https://github.com/rust-lang/libc>
fn open_path<R, F>(&self, f: F) -> Result<R, Self::Error>
where F: Fn() -> Result<R, &'static str>;
}
/// Hardware wallet error.
#[derive(Debug)]
pub enum Error {
@ -144,70 +193,13 @@ pub struct HardwareWalletManager {
trezor: Arc<trezor::Manager>,
}
impl HardwareWalletManager {
/// Hardware wallet constructor
pub fn new() -> Result<HardwareWalletManager, Error> {
let usb_context_trezor = Arc::new(libusb::Context::new()?);
let usb_context_ledger = Arc::new(libusb::Context::new()?);
let hidapi = Arc::new(Mutex::new(hidapi::HidApi::new().map_err(|e| Error::Hid(e.to_string().clone()))?));
let ledger = Arc::new(ledger::Manager::new(hidapi.clone()));
let trezor = Arc::new(trezor::Manager::new(hidapi.clone()));
// Subscribe to TREZOR V1
// Note, this support only TREZOR V1 becasue TREZOR V2 has another vendorID for some reason
// Also, we now only support one product as the second argument specifies
usb_context_trezor.register_callback(
Some(trezor::TREZOR_VID), Some(trezor::TREZOR_PIDS[0]), Some(USB_DEVICE_CLASS_DEVICE),
Box::new(trezor::EventHandler::new(Arc::downgrade(&trezor))))?;
// Subscribe to all Ledger Devices
// This means that we need to check that the given productID is supported
// None => LIBUSB_HOTPLUG_MATCH_ANY, in other words that all are subscribed to
// More info can be found: http://libusb.sourceforge.net/api-1.0/group__hotplug.html#gae6c5f1add6cc754005549c7259dc35ea
usb_context_ledger.register_callback(
Some(ledger::LEDGER_VID), None, Some(USB_DEVICE_CLASS_DEVICE),
Box::new(ledger::EventHandler::new(Arc::downgrade(&ledger))))?;
let exiting = Arc::new(AtomicBool::new(false));
let thread_exiting_ledger = exiting.clone();
let thread_exiting_trezor = exiting.clone();
let l = ledger.clone();
let t = trezor.clone();
// Ledger event thread
thread::Builder::new()
.name("hw_wallet_ledger".to_string())
.spawn(move || {
if let Err(e) = l.update_devices() {
debug!(target: "hw", "Ledger couldn't connect at startup, error: {}", e);
}
loop {
usb_context_ledger.handle_events(Some(Duration::from_millis(500)))
.unwrap_or_else(|e| debug!(target: "hw", "Ledger event handler error: {}", e));
if thread_exiting_ledger.load(atomic::Ordering::Acquire) {
break;
}
}
})
.ok();
// Trezor event thread
thread::Builder::new()
.name("hw_wallet_trezor".to_string())
.spawn(move || {
if let Err(e) = t.update_devices() {
debug!(target: "hw", "Trezor couldn't connect at startup, error: {}", e);
}
loop {
usb_context_trezor.handle_events(Some(Duration::from_millis(500)))
.unwrap_or_else(|e| debug!(target: "hw", "Trezor event handler error: {}", e));
if thread_exiting_trezor.load(atomic::Ordering::Acquire) {
break;
}
}
})
.ok();
let hidapi = Arc::new(Mutex::new(hidapi::HidApi::new().map_err(|e| Error::Hid(e.to_string().clone()))?));
let ledger = ledger::Manager::new(hidapi.clone(), exiting.clone())?;
let trezor = trezor::Manager::new(hidapi.clone(), exiting.clone())?;
Ok(HardwareWalletManager {
exiting: exiting,
@ -217,6 +209,8 @@ impl HardwareWalletManager {
}
/// Select key derivation path for a chain.
/// Currently, only one hard-coded keypath is supported
/// It is managed by `ethcore/account_provider`
pub fn set_key_path(&self, key_path: KeyPath) {
self.ledger.set_key_path(key_path);
self.trezor.set_key_path(key_path);
@ -231,24 +225,26 @@ impl HardwareWalletManager {
}
/// Return a list of paths to locked hardware wallets
/// This is only applicable to Trezor because Ledger only appears as
/// a device when it is unlocked
pub fn list_locked_wallets(&self) -> Result<Vec<String>, Error> {
Ok(self.trezor.list_locked_devices())
}
/// Get connected wallet info.
pub fn wallet_info(&self, address: &Address) -> Option<WalletInfo> {
if let Some(info) = self.ledger.device_info(address) {
if let Some(info) = self.ledger.get_wallet(address) {
Some(info)
} else {
self.trezor.device_info(address)
self.trezor.get_wallet(address)
}
}
/// Sign transaction data with wallet managing `address`.
pub fn sign_transaction(&self, address: &Address, t_info: &TransactionInfo, encoded_transaction: &[u8]) -> Result<Signature, Error> {
if self.ledger.device_info(address).is_some() {
if self.ledger.get_wallet(address).is_some() {
Ok(self.ledger.sign_transaction(address, encoded_transaction)?)
} else if self.trezor.device_info(address).is_some() {
} else if self.trezor.get_wallet(address).is_some() {
Ok(self.trezor.sign_transaction(address, t_info)?)
} else {
Err(Error::KeyNotFound)
@ -256,6 +252,8 @@ impl HardwareWalletManager {
}
/// Send a pin to a device at a certain path to unlock it
/// This is only applicable to Trezor because Ledger only appears as
/// a device when it is unlocked
pub fn pin_matrix_ack(&self, path: &str, pin: &str) -> Result<bool, Error> {
self.trezor.pin_matrix_ack(path, pin).map_err(Error::TrezorDevice)
}

View File

@ -19,12 +19,15 @@
//! and https://github.com/trezor/trezor-common/blob/master/protob/protocol.md
//! for protocol details.
use super::{WalletInfo, TransactionInfo, KeyPath};
use super::{WalletInfo, TransactionInfo, KeyPath, Wallet, Device, USB_DEVICE_CLASS_DEVICE};
use std::cmp::{min, max};
use std::fmt;
use std::sync::{Arc, Weak};
use std::sync::atomic;
use std::sync::atomic::AtomicBool;
use std::time::{Duration, Instant};
use std::thread;
use ethereum_types::{U256, H256, Address};
use ethkey::Signature;
@ -37,9 +40,9 @@ use protobuf::{Message, ProtobufEnum};
use trezor_sys::messages::{EthereumAddress, PinMatrixAck, MessageType, EthereumTxRequest, EthereumSignTx, EthereumGetAddress, EthereumTxAck, ButtonAck};
/// Trezor v1 vendor ID
pub const TREZOR_VID: u16 = 0x534c;
const TREZOR_VID: u16 = 0x534c;
/// Trezor product IDs
pub const TREZOR_PIDS: [u16; 1] = [0x0001];
const TREZOR_PIDS: [u16; 1] = [0x0001];
const ETH_DERIVATION_PATH: [u32; 5] = [0x8000002C, 0x8000003C, 0x80000000, 0, 0]; // m/44'/60'/0'/0/0
const ETC_DERIVATION_PATH: [u32; 5] = [0x8000002C, 0x8000003D, 0x80000000, 0, 0]; // m/44'/61'/0'/0/0
@ -94,12 +97,6 @@ pub struct Manager {
key_path: RwLock<KeyPath>,
}
#[derive(Debug)]
struct Device {
path: String,
info: WalletInfo,
}
/// HID Version used for the Trezor device
enum HidVersion {
V1,
@ -108,102 +105,42 @@ enum HidVersion {
impl Manager {
/// Create a new instance.
pub fn new(hidapi: Arc<Mutex<hidapi::HidApi>>) -> Manager {
Manager {
pub fn new(hidapi: Arc<Mutex<hidapi::HidApi>>, exiting: Arc<AtomicBool>) -> Result<Arc<Manager>, libusb::Error> {
let manager = Arc::new(Manager {
usb: hidapi,
devices: RwLock::new(Vec::new()),
locked_devices: RwLock::new(Vec::new()),
key_path: RwLock::new(KeyPath::Ethereum),
}
}
});
/// Re-populate device list
pub fn update_devices(&self) -> Result<usize, Error> {
let mut usb = self.usb.lock();
usb.refresh_devices();
let devices = usb.devices();
let mut new_devices = Vec::new();
let mut locked_devices = Vec::new();
let mut error = None;
for usb_device in devices {
let is_trezor = usb_device.vendor_id == TREZOR_VID;
let is_supported_product = TREZOR_PIDS.contains(&usb_device.product_id);
let is_valid = usb_device.usage_page == 0xFF00 || usb_device.interface_number == 0;
let usb_context = Arc::new(libusb::Context::new()?);
let m = manager.clone();
trace!(
"Checking device: {:?}, trezor: {:?}, prod: {:?}, valid: {:?}",
usb_device,
is_trezor,
is_supported_product,
is_valid,
);
if !is_trezor || !is_supported_product || !is_valid {
continue;
}
match self.read_device_info(&usb, &usb_device) {
Ok(device) => new_devices.push(device),
Err(Error::LockedDevice(path)) => locked_devices.push(path.to_string()),
Err(e) => {
warn!("Error reading device: {:?}", e);
error = Some(e);
}
}
}
let count = new_devices.len();
trace!("Got devices: {:?}, closed: {:?}", new_devices, locked_devices);
*self.devices.write() = new_devices;
*self.locked_devices.write() = locked_devices;
match error {
Some(e) => Err(e),
None => Ok(count),
}
}
// Subscribe to TREZOR V1
// Note, this support only TREZOR V1 because TREZOR V2 has a different vendorID for some reason
// Also, we now only support one product as the second argument specifies
usb_context.register_callback(
Some(TREZOR_VID), Some(TREZOR_PIDS[0]), Some(USB_DEVICE_CLASS_DEVICE),
Box::new(EventHandler::new(Arc::downgrade(&manager))))?;
fn read_device_info(&self, usb: &hidapi::HidApi, dev_info: &hidapi::HidDeviceInfo) -> Result<Device, Error> {
let handle = self.open_path(|| usb.open_path(&dev_info.path))?;
let manufacturer = dev_info.manufacturer_string.clone().unwrap_or("Unknown".to_owned());
let name = dev_info.product_string.clone().unwrap_or("Unknown".to_owned());
let serial = dev_info.serial_number.clone().unwrap_or("Unknown".to_owned());
match self.get_address(&handle) {
Ok(Some(addr)) => {
Ok(Device {
path: dev_info.path.clone(),
info: WalletInfo {
name: name,
manufacturer: manufacturer,
serial: serial,
address: addr,
},
// Trezor event thread
thread::Builder::new()
.name("hw_wallet_trezor".to_string())
.spawn(move || {
if let Err(e) = m.update_devices() {
debug!(target: "hw", "Trezor couldn't connect at startup, error: {}", e);
}
loop {
usb_context.handle_events(Some(Duration::from_millis(500)))
.unwrap_or_else(|e| debug!(target: "hw", "Trezor event handler error: {}", e));
if exiting.load(atomic::Ordering::Acquire) {
break;
}
}
})
}
Ok(None) => Err(Error::LockedDevice(dev_info.path.clone())),
Err(e) => Err(e),
}
}
.ok();
/// Select key derivation path for a known chain.
pub fn set_key_path(&self, key_path: KeyPath) {
*self.key_path.write() = key_path;
}
/// List connected wallets. This only returns wallets that are ready to be used.
pub fn list_devices(&self) -> Vec<WalletInfo> {
self.devices.read().iter().map(|d| d.info.clone()).collect()
}
pub fn list_locked_devices(&self) -> Vec<String> {
(*self.locked_devices.read()).clone()
}
/// Get wallet info.
pub fn device_info(&self, address: &Address) -> Option<WalletInfo> {
self.devices.read().iter().find(|d| &d.info.address == address).map(|d| d.info.clone())
}
fn open_path<R, F>(&self, f: F) -> Result<R, Error>
where F: Fn() -> Result<R, &'static str>
{
f().map_err(Into::into)
Ok(manager)
}
pub fn pin_matrix_ack(&self, device_path: &str, pin: &str) -> Result<bool, Error> {
@ -227,61 +164,6 @@ impl Manager {
unlocked
}
fn get_address(&self, device: &hidapi::HidDevice) -> Result<Option<Address>, Error> {
let typ = MessageType::MessageType_EthereumGetAddress;
let mut message = EthereumGetAddress::new();
match *self.key_path.read() {
KeyPath::Ethereum => message.set_address_n(ETH_DERIVATION_PATH.to_vec()),
KeyPath::EthereumClassic => message.set_address_n(ETC_DERIVATION_PATH.to_vec()),
}
message.set_show_display(false);
self.send_device_message(&device, &typ, &message)?;
let (resp_type, bytes) = self.read_device_response(&device)?;
match resp_type {
MessageType::MessageType_EthereumAddress => {
let response: EthereumAddress = protobuf::core::parse_from_bytes(&bytes)?;
Ok(Some(From::from(response.get_address())))
}
_ => Ok(None),
}
}
/// Sign transaction data with wallet managing `address`.
pub fn sign_transaction(&self, address: &Address, t_info: &TransactionInfo) -> Result<Signature, Error> {
let usb = self.usb.lock();
let devices = self.devices.read();
let device = devices.iter().find(|d| &d.info.address == address).ok_or(Error::KeyNotFound)?;
let handle = self.open_path(|| usb.open_path(&device.path))?;
let msg_type = MessageType::MessageType_EthereumSignTx;
let mut message = EthereumSignTx::new();
match *self.key_path.read() {
KeyPath::Ethereum => message.set_address_n(ETH_DERIVATION_PATH.to_vec()),
KeyPath::EthereumClassic => message.set_address_n(ETC_DERIVATION_PATH.to_vec()),
}
message.set_nonce(self.u256_to_be_vec(&t_info.nonce));
message.set_gas_limit(self.u256_to_be_vec(&t_info.gas_limit));
message.set_gas_price(self.u256_to_be_vec(&t_info.gas_price));
message.set_value(self.u256_to_be_vec(&t_info.value));
match t_info.to {
Some(addr) => {
message.set_to(addr.to_vec())
}
None => (),
}
let first_chunk_length = min(t_info.data.len(), 1024);
let chunk = &t_info.data[0..first_chunk_length];
message.set_data_initial_chunk(chunk.to_vec());
message.set_data_length(t_info.data.len() as u32);
if let Some(c_id) = t_info.chain_id {
message.set_chain_id(c_id as u32);
}
self.send_device_message(&handle, &msg_type, &message)?;
self.signing_loop(&handle, &t_info.chain_id, &t_info.data[first_chunk_length..])
}
fn u256_to_be_vec(&self, val: &U256) -> Vec<u8> {
let mut buf = [0u8; 32];
@ -400,6 +282,152 @@ impl Manager {
}
}
impl <'a>Wallet<'a> for Manager {
type Error = Error;
type Transaction = &'a TransactionInfo;
fn sign_transaction(&self, address: &Address, t_info: Self::Transaction) ->
Result<Signature, Error> {
let usb = self.usb.lock();
let devices = self.devices.read();
let device = devices.iter().find(|d| &d.info.address == address).ok_or(Error::KeyNotFound)?;
let handle = self.open_path(|| usb.open_path(&device.path))?;
let msg_type = MessageType::MessageType_EthereumSignTx;
let mut message = EthereumSignTx::new();
match *self.key_path.read() {
KeyPath::Ethereum => message.set_address_n(ETH_DERIVATION_PATH.to_vec()),
KeyPath::EthereumClassic => message.set_address_n(ETC_DERIVATION_PATH.to_vec()),
}
message.set_nonce(self.u256_to_be_vec(&t_info.nonce));
message.set_gas_limit(self.u256_to_be_vec(&t_info.gas_limit));
message.set_gas_price(self.u256_to_be_vec(&t_info.gas_price));
message.set_value(self.u256_to_be_vec(&t_info.value));
match t_info.to {
Some(addr) => {
message.set_to(addr.to_vec())
}
None => (),
}
let first_chunk_length = min(t_info.data.len(), 1024);
let chunk = &t_info.data[0..first_chunk_length];
message.set_data_initial_chunk(chunk.to_vec());
message.set_data_length(t_info.data.len() as u32);
if let Some(c_id) = t_info.chain_id {
message.set_chain_id(c_id as u32);
}
self.send_device_message(&handle, &msg_type, &message)?;
self.signing_loop(&handle, &t_info.chain_id, &t_info.data[first_chunk_length..])
}
fn set_key_path(&self, key_path: KeyPath) {
*self.key_path.write() = key_path;
}
fn update_devices(&self) -> Result<usize, Error> {
let mut usb = self.usb.lock();
usb.refresh_devices();
let devices = usb.devices();
let mut new_devices = Vec::new();
let mut locked_devices = Vec::new();
let mut error = None;
for usb_device in devices {
let is_trezor = usb_device.vendor_id == TREZOR_VID;
let is_supported_product = TREZOR_PIDS.contains(&usb_device.product_id);
let is_valid = usb_device.usage_page == 0xFF00 || usb_device.interface_number == 0;
trace!(
"Checking device: {:?}, trezor: {:?}, prod: {:?}, valid: {:?}",
usb_device,
is_trezor,
is_supported_product,
is_valid,
);
if !is_trezor || !is_supported_product || !is_valid {
continue;
}
match self.read_device(&usb, &usb_device) {
Ok(device) => new_devices.push(device),
Err(Error::LockedDevice(path)) => locked_devices.push(path.to_string()),
Err(e) => {
warn!("Error reading device: {:?}", e);
error = Some(e);
}
}
}
let count = new_devices.len();
trace!("Got devices: {:?}, closed: {:?}", new_devices, locked_devices);
*self.devices.write() = new_devices;
*self.locked_devices.write() = locked_devices;
match error {
Some(e) => Err(e),
None => Ok(count),
}
}
fn read_device(&self, usb: &hidapi::HidApi, dev_info: &hidapi::HidDeviceInfo) -> Result<Device, Error> {
let handle = self.open_path(|| usb.open_path(&dev_info.path))?;
let manufacturer = dev_info.manufacturer_string.clone().unwrap_or("Unknown".to_owned());
let name = dev_info.product_string.clone().unwrap_or("Unknown".to_owned());
let serial = dev_info.serial_number.clone().unwrap_or("Unknown".to_owned());
match self.get_address(&handle) {
Ok(Some(addr)) => {
Ok(Device {
path: dev_info.path.clone(),
info: WalletInfo {
name: name,
manufacturer: manufacturer,
serial: serial,
address: addr,
},
})
}
Ok(None) => Err(Error::LockedDevice(dev_info.path.clone())),
Err(e) => Err(e),
}
}
fn list_devices(&self) -> Vec<WalletInfo> {
self.devices.read().iter().map(|d| d.info.clone()).collect()
}
fn list_locked_devices(&self) -> Vec<String> {
(*self.locked_devices.read()).clone()
}
fn get_wallet(&self, address: &Address) -> Option<WalletInfo> {
self.devices.read().iter().find(|d| &d.info.address == address).map(|d| d.info.clone())
}
fn get_address(&self, device: &hidapi::HidDevice) -> Result<Option<Address>, Error> {
let typ = MessageType::MessageType_EthereumGetAddress;
let mut message = EthereumGetAddress::new();
match *self.key_path.read() {
KeyPath::Ethereum => message.set_address_n(ETH_DERIVATION_PATH.to_vec()),
KeyPath::EthereumClassic => message.set_address_n(ETC_DERIVATION_PATH.to_vec()),
}
message.set_show_display(false);
self.send_device_message(&device, &typ, &message)?;
let (resp_type, bytes) = self.read_device_response(&device)?;
match resp_type {
MessageType::MessageType_EthereumAddress => {
let response: EthereumAddress = protobuf::core::parse_from_bytes(&bytes)?;
Ok(Some(From::from(response.get_address())))
}
_ => Ok(None),
}
}
fn open_path<R, F>(&self, f: F) -> Result<R, Error>
where F: Fn() -> Result<R, &'static str>
{
f().map_err(Into::into)
}
}
// Try to connect to the device using polling in at most the time specified by the `timeout`
fn try_connect_polling(trezor: Arc<Manager>, duration: Duration) -> bool {
let start_time = Instant::now();
@ -412,11 +440,11 @@ fn try_connect_polling(trezor: Arc<Manager>, duration: Duration) -> bool {
}
/// Trezor event handler
/// A separate thread is handeling incoming events
/// A separate thread is handling incoming events
///
/// Note, that this run to completion and race-conditions can't occur but this can
/// therefore starve other events for being process with a spinlock or similar
pub struct EventHandler {
struct EventHandler {
trezor: Weak<Manager>,
}
@ -455,10 +483,10 @@ fn test_signature() {
use ethereum_types::{H160, H256, U256};
let hidapi = Arc::new(Mutex::new(hidapi::HidApi::new().unwrap()));
let manager = Manager::new(hidapi.clone());
let manager = Manager::new(hidapi.clone(), Arc::new(AtomicBool::new(false))).unwrap();
let addr: Address = H160::from("some_addr");
manager.update_devices().unwrap();
assert_eq!(try_connect_polling(manager.clone(), Duration::from_millis(500)), true);
let t_info = TransactionInfo {
nonce: U256::from(1),