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:
parent
7a76916143
commit
849f5d9a44
359
hw/src/ledger.rs
359
hw/src/ledger.rs
@ -20,8 +20,11 @@
|
|||||||
use std::cmp::min;
|
use std::cmp::min;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
use std::sync::atomic;
|
||||||
|
use std::sync::atomic::AtomicBool;
|
||||||
use std::sync::{Arc, Weak};
|
use std::sync::{Arc, Weak};
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
use std::thread;
|
||||||
|
|
||||||
use ethereum_types::{H256, Address};
|
use ethereum_types::{H256, Address};
|
||||||
use ethkey::Signature;
|
use ethkey::Signature;
|
||||||
@ -29,12 +32,12 @@ use hidapi;
|
|||||||
use libusb;
|
use libusb;
|
||||||
use parking_lot::{Mutex, RwLock};
|
use parking_lot::{Mutex, RwLock};
|
||||||
|
|
||||||
use super::{WalletInfo, KeyPath};
|
use super::{WalletInfo, KeyPath, Device, Wallet, USB_DEVICE_CLASS_DEVICE};
|
||||||
|
|
||||||
/// Ledger vendor ID
|
/// Ledger vendor ID
|
||||||
pub const LEDGER_VID: u16 = 0x2c97;
|
const LEDGER_VID: u16 = 0x2c97;
|
||||||
/// Legder product IDs: [Nano S and Blue]
|
/// Ledger product IDs: [Nano S and Blue]
|
||||||
pub const LEDGER_PIDS: [u16; 2] = [0x0000, 0x0001];
|
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 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
|
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,
|
KeyNotFound,
|
||||||
/// Signing has been cancelled by user.
|
/// Signing has been cancelled by user.
|
||||||
UserCancel,
|
UserCancel,
|
||||||
/// Invalid Device
|
/// Invalid device
|
||||||
InvalidDevice,
|
InvalidDevice,
|
||||||
|
/// Impossible error
|
||||||
|
ImpossibleError,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for Error {
|
impl fmt::Display for Error {
|
||||||
@ -77,6 +82,7 @@ impl fmt::Display for Error {
|
|||||||
Error::KeyNotFound => write!(f, "Key not found"),
|
Error::KeyNotFound => write!(f, "Key not found"),
|
||||||
Error::UserCancel => write!(f, "Operation has been cancelled"),
|
Error::UserCancel => write!(f, "Operation has been cancelled"),
|
||||||
Error::InvalidDevice => write!(f, "Unsupported product was entered"),
|
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>,
|
key_path: RwLock<KeyPath>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
struct Device {
|
|
||||||
path: String,
|
|
||||||
info: WalletInfo,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Manager {
|
impl Manager {
|
||||||
/// Create a new instance.
|
/// Create a new instance.
|
||||||
pub fn new(hidapi: Arc<Mutex<hidapi::HidApi>>) -> Manager {
|
pub fn new(hidapi: Arc<Mutex<hidapi::HidApi>>, exiting: Arc<AtomicBool>) -> Result<Arc<Manager>, libusb::Error> {
|
||||||
Manager {
|
let manager = Arc::new(Manager {
|
||||||
usb: hidapi,
|
usb: hidapi,
|
||||||
devices: RwLock::new(Vec::new()),
|
devices: RwLock::new(Vec::new()),
|
||||||
key_path: RwLock::new(KeyPath::Ethereum),
|
key_path: RwLock::new(KeyPath::Ethereum),
|
||||||
}
|
});
|
||||||
}
|
|
||||||
|
|
||||||
/// Re-populate device list. Only those devices that have Ethereum app open will be added.
|
let usb_context = Arc::new(libusb::Context::new()?);
|
||||||
pub fn update_devices(&self) -> Result<usize, Error> {
|
let m = manager.clone();
|
||||||
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);
|
|
||||||
|
|
||||||
}
|
// Subscribe to all Ledger devices
|
||||||
Err(e) => debug!("Error reading device info: {}", e),
|
// 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>
|
||||||
*self.devices.write() = new_devices;
|
usb_context.register_callback(
|
||||||
Ok(num_new_devices)
|
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.
|
// Ledger event handler thread
|
||||||
pub fn set_key_path(&self, key_path: KeyPath) {
|
thread::Builder::new()
|
||||||
*self.key_path.write() = key_path;
|
.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 = Ð_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 = Ð_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 {
|
loop {
|
||||||
let p1 = if data_pos == 0 { 0x00 } else { 0x80 };
|
usb_context.handle_events(Some(Duration::from_millis(500)))
|
||||||
let dest_left = MAX_CHUNK_SIZE - dest_offset;
|
.unwrap_or_else(|e| debug!(target: "hw", "Ledger event handler error: {}", e));
|
||||||
let chunk_data_size = min(dest_left, data.len() - data_pos);
|
if exiting.load(atomic::Ordering::Acquire) {
|
||||||
&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() {
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
|
||||||
if result.len() != 65 {
|
Ok(manager)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn send_apdu(handle: &hidapi::HidDevice, command: u8, p1: u8, p2: u8, data: &[u8]) -> Result<Vec<u8>, Error> {
|
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;
|
chunk_index += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// read response
|
// Read response
|
||||||
chunk_index = 0;
|
chunk_index = 0;
|
||||||
let mut message_size = 0;
|
let mut message_size = 0;
|
||||||
let mut message = Vec::new();
|
let mut message = Vec::new();
|
||||||
@ -303,7 +195,7 @@ impl Manager {
|
|||||||
|
|
||||||
let mut offset = 5;
|
let mut offset = 5;
|
||||||
if seq == 0 {
|
if seq == 0 {
|
||||||
// read message size and status word.
|
// Read message size and status word.
|
||||||
if chunk_size < 7 {
|
if chunk_size < 7 {
|
||||||
return Err(Error::Protocol("Unexpected chunk header"));
|
return Err(Error::Protocol("Unexpected chunk header"));
|
||||||
}
|
}
|
||||||
@ -321,7 +213,7 @@ impl Manager {
|
|||||||
return Err(Error::Protocol("No status word"));
|
return Err(Error::Protocol("No status word"));
|
||||||
}
|
}
|
||||||
let status = (message[message.len() - 2] as usize) << 8 | (message[message.len() - 1] as usize);
|
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 {
|
match status {
|
||||||
0x6700 => Err(Error::Protocol("Incorrect length")),
|
0x6700 => Err(Error::Protocol("Incorrect length")),
|
||||||
0x6982 => Err(Error::Protocol("Security status not satisfied (Canceled by user)")),
|
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
|
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 = Ð_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 = Ð_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
|
/// 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
|
/// 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
|
/// therefore starve other events for being process with a spinlock or similar
|
||||||
pub struct EventHandler {
|
struct EventHandler {
|
||||||
ledger: Weak<Manager>,
|
ledger: Weak<Manager>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EventHandler {
|
impl EventHandler {
|
||||||
/// Ledger event handler constructor
|
/// Ledger event handler constructor
|
||||||
pub fn new(ledger: Weak<Manager>) -> Self {
|
fn new(ledger: Weak<Manager>) -> Self {
|
||||||
Self { ledger: ledger }
|
Self { ledger: ledger }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -402,16 +442,24 @@ impl libusb::Hotplug for EventHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
#[ignore]
|
||||||
|
/// This test can't be run without an actual ledger device connected
|
||||||
fn smoke() {
|
fn smoke() {
|
||||||
use rustc_hex::FromHex;
|
use rustc_hex::FromHex;
|
||||||
let hidapi = Arc::new(Mutex::new(hidapi::HidApi::new().unwrap()));
|
let hidapi = Arc::new(Mutex::new(hidapi::HidApi::new().expect("HidApi")));
|
||||||
let manager = Manager::new(hidapi.clone());
|
let manager = Manager::new(hidapi.clone(), Arc::new(AtomicBool::new(false))).expect("Ledger Manager");
|
||||||
manager.update_devices().unwrap();
|
|
||||||
for d in &*manager.devices.read() {
|
// Update device list
|
||||||
println!("Device: {:?}", d);
|
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 tx = FromHex::from_hex("eb018504a817c80082520894a6ca2e6707f2cc189794a9dd459d5b05ed1bcd1c8703f26fcfb7a22480018080").unwrap();
|
||||||
let signature = manager.sign_transaction(&address, &tx);
|
let signature = manager.sign_transaction(&address, &tx);
|
||||||
println!("Got {:?}", signature);
|
println!("Got {:?}", signature);
|
||||||
@ -424,5 +472,4 @@ fn smoke() {
|
|||||||
let signature = manager.sign_transaction(&address, &huge_tx);
|
let signature = manager.sign_transaction(&address, &huge_tx);
|
||||||
println!("Got {:?}", signature);
|
println!("Got {:?}", signature);
|
||||||
assert!(signature.is_ok());
|
assert!(signature.is_ok());
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
132
hw/src/lib.rs
132
hw/src/lib.rs
@ -16,7 +16,8 @@
|
|||||||
|
|
||||||
//! Hardware wallet management.
|
//! Hardware wallet management.
|
||||||
|
|
||||||
#![warn(missing_docs)]
|
#[warn(missing_docs)]
|
||||||
|
#[warn(warnings)]
|
||||||
|
|
||||||
extern crate ethereum_types;
|
extern crate ethereum_types;
|
||||||
extern crate ethkey;
|
extern crate ethkey;
|
||||||
@ -38,12 +39,60 @@ use std::fmt;
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::sync::atomic;
|
use std::sync::atomic;
|
||||||
use std::sync::atomic::AtomicBool;
|
use std::sync::atomic::AtomicBool;
|
||||||
use std::thread;
|
|
||||||
use std::time::Duration;
|
|
||||||
use ethereum_types::U256;
|
use ethereum_types::U256;
|
||||||
|
|
||||||
const USB_DEVICE_CLASS_DEVICE: u8 = 0;
|
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.
|
/// Hardware wallet error.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
@ -144,70 +193,13 @@ pub struct HardwareWalletManager {
|
|||||||
trezor: Arc<trezor::Manager>,
|
trezor: Arc<trezor::Manager>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
impl HardwareWalletManager {
|
impl HardwareWalletManager {
|
||||||
/// Hardware wallet constructor
|
/// Hardware wallet constructor
|
||||||
pub fn new() -> Result<HardwareWalletManager, Error> {
|
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 exiting = Arc::new(AtomicBool::new(false));
|
||||||
let thread_exiting_ledger = exiting.clone();
|
let hidapi = Arc::new(Mutex::new(hidapi::HidApi::new().map_err(|e| Error::Hid(e.to_string().clone()))?));
|
||||||
let thread_exiting_trezor = exiting.clone();
|
let ledger = ledger::Manager::new(hidapi.clone(), exiting.clone())?;
|
||||||
let l = ledger.clone();
|
let trezor = trezor::Manager::new(hidapi.clone(), exiting.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();
|
|
||||||
|
|
||||||
Ok(HardwareWalletManager {
|
Ok(HardwareWalletManager {
|
||||||
exiting: exiting,
|
exiting: exiting,
|
||||||
@ -217,6 +209,8 @@ impl HardwareWalletManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Select key derivation path for a chain.
|
/// 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) {
|
pub fn set_key_path(&self, key_path: KeyPath) {
|
||||||
self.ledger.set_key_path(key_path);
|
self.ledger.set_key_path(key_path);
|
||||||
self.trezor.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
|
/// 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> {
|
pub fn list_locked_wallets(&self) -> Result<Vec<String>, Error> {
|
||||||
Ok(self.trezor.list_locked_devices())
|
Ok(self.trezor.list_locked_devices())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get connected wallet info.
|
/// Get connected wallet info.
|
||||||
pub fn wallet_info(&self, address: &Address) -> Option<WalletInfo> {
|
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)
|
Some(info)
|
||||||
} else {
|
} else {
|
||||||
self.trezor.device_info(address)
|
self.trezor.get_wallet(address)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sign transaction data with wallet managing `address`.
|
/// Sign transaction data with wallet managing `address`.
|
||||||
pub fn sign_transaction(&self, address: &Address, t_info: &TransactionInfo, encoded_transaction: &[u8]) -> Result<Signature, Error> {
|
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)?)
|
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)?)
|
Ok(self.trezor.sign_transaction(address, t_info)?)
|
||||||
} else {
|
} else {
|
||||||
Err(Error::KeyNotFound)
|
Err(Error::KeyNotFound)
|
||||||
@ -256,6 +252,8 @@ impl HardwareWalletManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Send a pin to a device at a certain path to unlock it
|
/// 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> {
|
pub fn pin_matrix_ack(&self, path: &str, pin: &str) -> Result<bool, Error> {
|
||||||
self.trezor.pin_matrix_ack(path, pin).map_err(Error::TrezorDevice)
|
self.trezor.pin_matrix_ack(path, pin).map_err(Error::TrezorDevice)
|
||||||
}
|
}
|
||||||
|
338
hw/src/trezor.rs
338
hw/src/trezor.rs
@ -19,12 +19,15 @@
|
|||||||
//! and https://github.com/trezor/trezor-common/blob/master/protob/protocol.md
|
//! and https://github.com/trezor/trezor-common/blob/master/protob/protocol.md
|
||||||
//! for protocol details.
|
//! 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::cmp::{min, max};
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::sync::{Arc, Weak};
|
use std::sync::{Arc, Weak};
|
||||||
|
use std::sync::atomic;
|
||||||
|
use std::sync::atomic::AtomicBool;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
use std::thread;
|
||||||
|
|
||||||
use ethereum_types::{U256, H256, Address};
|
use ethereum_types::{U256, H256, Address};
|
||||||
use ethkey::Signature;
|
use ethkey::Signature;
|
||||||
@ -37,9 +40,9 @@ use protobuf::{Message, ProtobufEnum};
|
|||||||
use trezor_sys::messages::{EthereumAddress, PinMatrixAck, MessageType, EthereumTxRequest, EthereumSignTx, EthereumGetAddress, EthereumTxAck, ButtonAck};
|
use trezor_sys::messages::{EthereumAddress, PinMatrixAck, MessageType, EthereumTxRequest, EthereumSignTx, EthereumGetAddress, EthereumTxAck, ButtonAck};
|
||||||
|
|
||||||
/// Trezor v1 vendor ID
|
/// Trezor v1 vendor ID
|
||||||
pub const TREZOR_VID: u16 = 0x534c;
|
const TREZOR_VID: u16 = 0x534c;
|
||||||
/// Trezor product IDs
|
/// 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 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
|
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>,
|
key_path: RwLock<KeyPath>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
struct Device {
|
|
||||||
path: String,
|
|
||||||
info: WalletInfo,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// HID Version used for the Trezor device
|
/// HID Version used for the Trezor device
|
||||||
enum HidVersion {
|
enum HidVersion {
|
||||||
V1,
|
V1,
|
||||||
@ -108,102 +105,42 @@ enum HidVersion {
|
|||||||
|
|
||||||
impl Manager {
|
impl Manager {
|
||||||
/// Create a new instance.
|
/// Create a new instance.
|
||||||
pub fn new(hidapi: Arc<Mutex<hidapi::HidApi>>) -> Manager {
|
pub fn new(hidapi: Arc<Mutex<hidapi::HidApi>>, exiting: Arc<AtomicBool>) -> Result<Arc<Manager>, libusb::Error> {
|
||||||
Manager {
|
let manager = Arc::new(Manager {
|
||||||
usb: hidapi,
|
usb: hidapi,
|
||||||
devices: RwLock::new(Vec::new()),
|
devices: RwLock::new(Vec::new()),
|
||||||
locked_devices: RwLock::new(Vec::new()),
|
locked_devices: RwLock::new(Vec::new()),
|
||||||
key_path: RwLock::new(KeyPath::Ethereum),
|
key_path: RwLock::new(KeyPath::Ethereum),
|
||||||
}
|
});
|
||||||
}
|
|
||||||
|
|
||||||
/// Re-populate device list
|
let usb_context = Arc::new(libusb::Context::new()?);
|
||||||
pub fn update_devices(&self) -> Result<usize, Error> {
|
let m = manager.clone();
|
||||||
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!(
|
// Subscribe to TREZOR V1
|
||||||
"Checking device: {:?}, trezor: {:?}, prod: {:?}, valid: {:?}",
|
// Note, this support only TREZOR V1 because TREZOR V2 has a different vendorID for some reason
|
||||||
usb_device,
|
// Also, we now only support one product as the second argument specifies
|
||||||
is_trezor,
|
usb_context.register_callback(
|
||||||
is_supported_product,
|
Some(TREZOR_VID), Some(TREZOR_PIDS[0]), Some(USB_DEVICE_CLASS_DEVICE),
|
||||||
is_valid,
|
Box::new(EventHandler::new(Arc::downgrade(&manager))))?;
|
||||||
);
|
|
||||||
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),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_device_info(&self, usb: &hidapi::HidApi, dev_info: &hidapi::HidDeviceInfo) -> Result<Device, Error> {
|
// Trezor event thread
|
||||||
let handle = self.open_path(|| usb.open_path(&dev_info.path))?;
|
thread::Builder::new()
|
||||||
let manufacturer = dev_info.manufacturer_string.clone().unwrap_or("Unknown".to_owned());
|
.name("hw_wallet_trezor".to_string())
|
||||||
let name = dev_info.product_string.clone().unwrap_or("Unknown".to_owned());
|
.spawn(move || {
|
||||||
let serial = dev_info.serial_number.clone().unwrap_or("Unknown".to_owned());
|
if let Err(e) = m.update_devices() {
|
||||||
match self.get_address(&handle) {
|
debug!(target: "hw", "Trezor couldn't connect at startup, error: {}", e);
|
||||||
Ok(Some(addr)) => {
|
}
|
||||||
Ok(Device {
|
loop {
|
||||||
path: dev_info.path.clone(),
|
usb_context.handle_events(Some(Duration::from_millis(500)))
|
||||||
info: WalletInfo {
|
.unwrap_or_else(|e| debug!(target: "hw", "Trezor event handler error: {}", e));
|
||||||
name: name,
|
if exiting.load(atomic::Ordering::Acquire) {
|
||||||
manufacturer: manufacturer,
|
break;
|
||||||
serial: serial,
|
}
|
||||||
address: addr,
|
}
|
||||||
},
|
|
||||||
})
|
})
|
||||||
}
|
.ok();
|
||||||
Ok(None) => Err(Error::LockedDevice(dev_info.path.clone())),
|
|
||||||
Err(e) => Err(e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Select key derivation path for a known chain.
|
Ok(manager)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn pin_matrix_ack(&self, device_path: &str, pin: &str) -> Result<bool, Error> {
|
pub fn pin_matrix_ack(&self, device_path: &str, pin: &str) -> Result<bool, Error> {
|
||||||
@ -227,61 +164,6 @@ impl Manager {
|
|||||||
unlocked
|
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> {
|
fn u256_to_be_vec(&self, val: &U256) -> Vec<u8> {
|
||||||
let mut buf = [0u8; 32];
|
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`
|
// 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 {
|
fn try_connect_polling(trezor: Arc<Manager>, duration: Duration) -> bool {
|
||||||
let start_time = Instant::now();
|
let start_time = Instant::now();
|
||||||
@ -412,11 +440,11 @@ fn try_connect_polling(trezor: Arc<Manager>, duration: Duration) -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Trezor event handler
|
/// 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
|
/// 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
|
/// therefore starve other events for being process with a spinlock or similar
|
||||||
pub struct EventHandler {
|
struct EventHandler {
|
||||||
trezor: Weak<Manager>,
|
trezor: Weak<Manager>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -455,10 +483,10 @@ fn test_signature() {
|
|||||||
use ethereum_types::{H160, H256, U256};
|
use ethereum_types::{H160, H256, U256};
|
||||||
|
|
||||||
let hidapi = Arc::new(Mutex::new(hidapi::HidApi::new().unwrap()));
|
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");
|
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 {
|
let t_info = TransactionInfo {
|
||||||
nonce: U256::from(1),
|
nonce: U256::from(1),
|
||||||
|
Loading…
Reference in New Issue
Block a user