Trezor Support (#6403)

* Copy modal from keepkey branch and generalize

The keepkey PinMatrix modal needs to be the same for Trezor, but we
should probably try to keep it general since it can be used for both.

* Add trezor communication code

This is a result of much trial-and-error and a couple of dead-ends in
how to communicate and wire everything up.

Code here is still a bit WIP with lots of debug prints and stuff.

The test works though, it is possible to sign a transaction.

* Extend the basic lib to allow Trezor

This is kind of ugly and needs some cleanup and generalization. I’ve
just copy-pasted some things to bring in the trezor wallets. I’ve also
had to add a lock to the USB API so that only one thing talks to the
USB at once.

* Add RPC plumbing needed

We need to be able to get “locked” devices from the frontend to figure
out if we’re going to display the PinMatrix or not. Then we need to be
able to send a pin to a device.

* Add logic to query backend for Trezor and display PinMatrix

There’s a bug somewhere here because signing a transaction fails if you
take too long to press the confirm button on the device.

* Change back to paritytech branch

As my fork has been merged in.

* Converting spaces to tabs, as it should be

* Incorporate correct handling of EIP-155

Turns out the Trezor was adjusting the v part of the signature, and
we’re already doing that so it was done twice.

* Some circular logic here that was incorrect

BE-encoded U256 is almost the same as RLP encoded without the
size-byte, except for <u8 sized values. What’s really done is
BE-encoded U256 and then left-trimmed to the smallest size. Kind of
obvious in hindsight.

* Resolve issue where not clicking fast enough fails

The device will not repeat a ButtonRequest when you read from it, so
you need to have a blocking `read` for whatever amount of time that you
want to give the user to click. You could also have a shorter timeout
but keep retrying for some amount of time, but it would amount to the
same thing.

* Scan after pin entry to make accepting it faster

* Remove ability to cancel pin request

* Some slight cleanup

* Probe for the correct HID Version to determine padding

* Move the PinMatrix from Accounts to Application

* Removing unused dependencies

* Mistake in copying over stuff from keepkey branch

* Simplify FormattedMessage

* Move generated code to external crate

* Remove ethcore-util dependency

* Fix broken import in test

This test is useless without a connected Trezor, not sure how to make
it useful without one.

* Merge branch 'master' into fh-4500-trezor-support

# Conflicts:
#	rpc/src/v1/helpers/dispatch.rs

* Ignore test that can't be run without trezor device

* Fixing grumbles

* Avoiding owning data in RPC method
* Checking for overflow in v part of signature
* s/network_id/chain_id
* Propagating an error from the HID Api
* Condensing code a little bit

* Fixing UI.

* Debugging trezor.

* Minor styling tweak

* Make message type into an actual type

This makes the message type that the RPC message accepts into an actual
type as opposed to just a string, based on feedback. Although I’m not
100% sure this has actually improved the situation.

Overall I think the hardware wallet interface needs some refactoring
love.

* Split the trezor RPC endpoint

It’s split into two more generic endpoints that should be suitable for
any hardware wallets with the same behavior to sit behind.

* Reflect RPC method split in javascript

* Fix bug with pin entry

* Fix deadlock for Ledger

* Avoid having a USB lock in just listing locked wallets

* Fix javascript issue (see #6509)

* Replace Mutex with RwLock

* Update Ledger test

* Fix typo causing faulty signatures (sometimes)

* *Actually* fix tests

* Update git submodule

Needed to make tests pass

* Swap line orders to prevent possible deadlock

* Make setPinMatrixRequest an @action
This commit is contained in:
Fredrik Harrysson 2017-09-14 19:28:43 +02:00 committed by Gav Wood
parent e9abcb2f6d
commit 75b6a31e87
23 changed files with 2957 additions and 989 deletions

1817
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -31,7 +31,8 @@ use ethstore::{
use ethstore::dir::MemoryDirectory;
use ethstore::ethkey::{Address, Message, Public, Secret, Random, Generator};
use ethjson::misc::AccountMeta;
use hardware_wallet::{Error as HardwareError, HardwareWalletManager, KeyPath};
use hardware_wallet::{Error as HardwareError, HardwareWalletManager, KeyPath, TransactionInfo};
use super::transaction::{Action, Transaction};
pub use ethstore::ethkey::Signature;
pub use ethstore::{Derivation, IndexDerivation, KeyFile};
@ -288,6 +289,24 @@ impl AccountProvider {
Ok(accounts.into_iter().map(|a| a.address).collect())
}
/// Get a list of paths to locked hardware wallets
pub fn locked_hardware_accounts(&self) -> Result<Vec<String>, SignError> {
match self.hardware_store.as_ref().map(|h| h.list_locked_wallets()) {
None => Err(SignError::NotFound),
Some(Err(e)) => Err(SignError::Hardware(e)),
Some(Ok(s)) => Ok(s),
}
}
/// Provide a pin to a locked hardware wallet on USB path to unlock it
pub fn hardware_pin_matrix_ack(&self, path: &str, pin: &str) -> Result<bool, SignError> {
match self.hardware_store.as_ref().map(|h| h.pin_matrix_ack(path, pin)) {
None => Err(SignError::NotFound),
Some(Err(e)) => Err(SignError::Hardware(e)),
Some(Ok(s)) => Ok(s),
}
}
/// Sets addresses of accounts exposed for unknown dapps.
/// `None` means that all accounts will be visible.
/// If not `None` or empty it will also override default account.
@ -779,8 +798,20 @@ impl AccountProvider {
}
/// 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)) {
pub fn sign_with_hardware(&self, address: Address, transaction: &Transaction, chain_id: Option<u64>, rlp_encoded_transaction: &[u8]) -> Result<Signature, SignError> {
let t_info = TransactionInfo {
nonce: transaction.nonce,
gas_price: transaction.gas_price,
gas_limit: transaction.gas,
to: match transaction.action {
Action::Create => None,
Action::Call(ref to) => Some(to.clone()),
},
value: transaction.value,
data: transaction.data.to_vec(),
chain_id: chain_id,
};
match self.hardware_store.as_ref().map(|s| s.sign_transaction(&address, &t_info, rlp_encoded_transaction)) {
None | Some(Err(HardwareError::KeyNotFound)) => Err(SignError::NotFound),
Some(Err(e)) => Err(From::from(e)),
Some(Ok(s)) => Ok(s),

View File

@ -9,8 +9,10 @@ authors = ["Parity Technologies <admin@parity.io>"]
[dependencies]
log = "0.3"
parking_lot = "0.4"
protobuf = "1.4"
hidapi = { git = "https://github.com/paritytech/hidapi-rs" }
libusb = { git = "https://github.com/paritytech/libusb-rs" }
trezor-sys = { git = "https://github.com/paritytech/trezor-sys" }
ethkey = { path = "../ethkey" }
ethcore-bigint = { path = "../util/bigint" }

View File

@ -17,19 +17,23 @@
//! Ledger hardware wallet module. Supports Ledger Blue and Nano S.
/// See https://github.com/LedgerHQ/blue-app-eth/blob/master/doc/ethapp.asc for protocol details.
use hidapi;
use std::fmt;
use std::cmp::min;
use std::str::FromStr;
use std::time::Duration;
use super::WalletInfo;
use ethkey::{Address, Signature};
use super::{WalletInfo, KeyPath};
use bigint::hash::H256;
use ethkey::{Address, Signature};
use hidapi;
use parking_lot::{Mutex, RwLock};
use std::cmp::min;
use std::fmt;
use std::str::FromStr;
use std::sync::Arc;
use std::time::Duration;
const LEDGER_VID: u16 = 0x2c97;
const LEDGER_PIDS: [u16; 2] = [0x0000, 0x0001]; // Nano S and Blue
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 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 APDU_TAG: u8 = 0x05;
const APDU_CLA: u8 = 0xe0;
@ -43,16 +47,7 @@ mod commands {
pub const SIGN_ETH_TRANSACTION: u8 = 0x04;
}
/// Key derivation paths used on ledger wallets.
#[derive(Debug, Clone, Copy)]
pub enum KeyPath {
/// Ethereum.
Ethereum,
/// Ethereum classic.
EthereumClassic,
}
/// Hardware waller error.
/// Hardware wallet error.
#[derive(Debug)]
pub enum Error {
/// Ethereum wallet protocol error.
@ -84,9 +79,9 @@ impl From<hidapi::HidError> for Error {
/// Ledger device manager.
pub struct Manager {
usb: hidapi::HidApi,
devices: Vec<Device>,
key_path: KeyPath,
usb: Arc<Mutex<hidapi::HidApi>>,
devices: RwLock<Vec<Device>>,
key_path: RwLock<KeyPath>,
}
#[derive(Debug)]
@ -97,19 +92,19 @@ struct Device {
impl Manager {
/// Create a new instance.
pub fn new() -> Result<Manager, Error> {
let manager = Manager {
usb: hidapi::HidApi::new()?,
devices: Vec::new(),
key_path: KeyPath::Ethereum,
};
Ok(manager)
pub fn new(hidapi: Arc<Mutex<hidapi::HidApi>>) -> Manager {
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(&mut self) -> Result<usize, Error> {
self.usb.refresh_devices();
let devices = self.usb.devices();
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 {
@ -117,30 +112,30 @@ impl Manager {
if device.vendor_id != LEDGER_VID || !LEDGER_PIDS.contains(&device.product_id) {
continue;
}
match self.read_device_info(&device) {
match self.read_device_info(&usb, &device) {
Ok(info) => {
debug!("Found device: {:?}", info);
if !self.devices.iter().any(|d| d.path == info.path) {
if !self.devices.read().iter().any(|d| d.path == info.path) {
num_new_devices += 1;
}
new_devices.push(info);
},
}
Err(e) => debug!("Error reading device info: {}", e),
};
}
self.devices = new_devices;
*self.devices.write() = new_devices;
Ok(num_new_devices)
}
/// Select key derivation path for a known chain.
pub fn set_key_path(&mut self, key_path: KeyPath) {
self.key_path = key_path;
pub fn set_key_path(&self, key_path: KeyPath) {
*self.key_path.write() = key_path;
}
fn read_device_info(&self, dev_info: &hidapi::HidDeviceInfo) -> Result<Device, Error> {
let mut handle = self.open_path(&dev_info.path)?;
let address = Self::read_wallet_address(&mut handle, self.key_path)?;
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());
@ -187,24 +182,24 @@ impl Manager {
/// List connected wallets. This only returns wallets that are ready to be used.
pub fn list_devices(&self) -> Vec<WalletInfo> {
self.devices.iter().map(|d| d.info.clone()).collect()
self.devices.read().iter().map(|d| d.info.clone()).collect()
}
/// Get wallet info.
pub fn device_info(&self, address: &Address) -> Option<WalletInfo> {
self.devices.iter().find(|d| &d.info.address == address).map(|d| d.info.clone())
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 device = self.devices.iter().find(|d| &d.info.address == address)
.ok_or(Error::KeyNotFound)?;
let handle = self.open_path(&device.path)?;
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 {
let derivation_path = match *self.key_path.read() {
KeyPath::Ethereum => eth_path,
KeyPath::EthereumClassic => etc_path,
};
@ -218,7 +213,7 @@ impl Manager {
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]);
&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;
@ -236,11 +231,13 @@ impl Manager {
Ok(Signature::from_rsv(&r, &s, v))
}
fn open_path(&self, path: &str) -> Result<hidapi::HidDevice, Error> {
fn open_path<R, F>(&self, f: F) -> Result<R, Error>
where F: Fn() -> Result<R, &'static str>
{
let mut err = Error::KeyNotFound;
/// Try to open device a few times.
for _ in 0..10 {
match self.usb.open_path(&path) {
match f() {
Ok(handle) => return Ok(handle),
Err(e) => err = From::from(e),
}
@ -253,33 +250,33 @@ impl Manager {
const HID_PACKET_SIZE: usize = 64 + HID_PREFIX_ZERO;
let mut offset = 0;
let mut chunk_index = 0;
loop {
let mut hid_chunk: [u8; HID_PACKET_SIZE] = [0; HID_PACKET_SIZE];
let mut chunk_size = if chunk_index == 0 { 12 } else { 5 };
let size = min(64 - chunk_size, data.len() - offset);
{
let mut chunk = &mut hid_chunk[HID_PREFIX_ZERO..];
&mut chunk[0..5].copy_from_slice(&[0x01, 0x01, APDU_TAG, (chunk_index >> 8) as u8, (chunk_index & 0xff) as u8 ]);
loop {
let mut hid_chunk: [u8; HID_PACKET_SIZE] = [0; HID_PACKET_SIZE];
let mut chunk_size = if chunk_index == 0 { 12 } else { 5 };
let size = min(64 - chunk_size, data.len() - offset);
{
let mut chunk = &mut hid_chunk[HID_PREFIX_ZERO..];
&mut chunk[0..5].copy_from_slice(&[0x01, 0x01, APDU_TAG, (chunk_index >> 8) as u8, (chunk_index & 0xff) as u8 ]);
if chunk_index == 0 {
let data_len = data.len() + 5;
&mut chunk[5..12].copy_from_slice(&[ (data_len >> 8) as u8, (data_len & 0xff) as u8, APDU_CLA, command, p1, p2, data.len() as u8 ]);
}
if chunk_index == 0 {
let data_len = data.len() + 5;
&mut chunk[5..12].copy_from_slice(&[ (data_len >> 8) as u8, (data_len & 0xff) as u8, APDU_CLA, command, p1, p2, data.len() as u8 ]);
}
&mut chunk[chunk_size..chunk_size + size].copy_from_slice(&data[offset..offset + size]);
offset += size;
chunk_size += size;
}
trace!("writing {:?}", &hid_chunk[..]);
let n = handle.write(&hid_chunk[..])?;
if n < chunk_size {
return Err(Error::Protocol("Write data size mismatch"));
}
if offset == data.len() {
break;
}
chunk_index += 1;
&mut chunk[chunk_size..chunk_size + size].copy_from_slice(&data[offset..offset + size]);
offset += size;
chunk_size += size;
}
trace!("writing {:?}", &hid_chunk[..]);
let n = handle.write(&hid_chunk[..])?;
if n < chunk_size {
return Err(Error::Protocol("Write data size mismatch"));
}
if offset == data.len() {
break;
}
chunk_index += 1;
}
// read response
chunk_index = 0;
@ -303,7 +300,7 @@ impl Manager {
if chunk_size < 7 {
return Err(Error::Protocol("Unexpected chunk header"));
}
message_size = (chunk[5] as usize) << 8 | (chunk[6] as usize);
message_size = (chunk[5] as usize) << 8 | (chunk[6] as usize);
offset += 2;
}
message.extend_from_slice(&chunk[offset..chunk_size]);
@ -311,12 +308,12 @@ impl Manager {
if message.len() == message_size {
break;
}
chunk_index +=1;
chunk_index += 1;
}
if message.len() < 2 {
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);
match status {
0x6700 => Err(Error::Protocol("Incorrect length")),
@ -341,9 +338,10 @@ impl Manager {
#[test]
fn smoke() {
use rustc_hex::FromHex;
let mut manager = Manager::new().unwrap();
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 {
for d in &*manager.devices.read() {
println!("Device: {:?}", d);
}

View File

@ -16,39 +16,59 @@
//! Hardware wallet management.
extern crate parking_lot;
extern crate ethcore_bigint as bigint;
extern crate ethkey;
extern crate hidapi;
extern crate libusb;
extern crate ethkey;
extern crate ethcore_bigint as bigint;
extern crate parking_lot;
extern crate protobuf;
extern crate trezor_sys;
#[macro_use] extern crate log;
#[cfg(test)] extern crate rustc_hex;
mod ledger;
mod trezor;
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;
use parking_lot::Mutex;
use std::fmt;
use std::sync::{Arc, Weak};
use std::sync::atomic;
use std::sync::atomic::AtomicBool;
use std::thread;
use std::time::Duration;
use bigint::prelude::uint::U256;
/// Hardware waller error.
/// Hardware wallet error.
#[derive(Debug)]
pub enum Error {
/// Ledger device error.
LedgerDevice(ledger::Error),
/// Trezor device error
TrezorDevice(trezor::Error),
/// USB error.
Usb(libusb::Error),
/// HID error
Hid(String),
/// Hardware wallet not found for specified key.
KeyNotFound,
}
/// Hardware waller information.
/// This is the transaction info we need to supply to Trezor message. It's more
/// or less a duplicate of ethcore::transaction::Transaction, but we can't
/// import ethcore here as that would be a circular dependency.
pub struct TransactionInfo {
pub nonce: U256,
pub gas_price: U256,
pub gas_limit: U256,
pub to: Option<Address>,
pub value: U256,
pub data: Vec<u8>,
pub chain_id: Option<u64>,
}
/// Hardware wallet information.
#[derive(Debug, Clone)]
pub struct WalletInfo {
/// Wallet device name.
@ -61,12 +81,23 @@ pub struct WalletInfo {
pub address: Address,
}
/// Key derivation paths used on hardware wallets.
#[derive(Debug, Clone, Copy)]
pub enum KeyPath {
/// Ethereum.
Ethereum,
/// Ethereum classic.
EthereumClassic,
}
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::TrezorDevice(ref e) => write!(f, "{}", e),
Error::Usb(ref e) => write!(f, "{}", e),
Error::Hid(ref e) => write!(f, "{}", e),
}
}
}
@ -80,6 +111,15 @@ impl From<ledger::Error> for Error {
}
}
impl From<trezor::Error> for Error {
fn from(err: trezor::Error) -> Error {
match err {
trezor::Error::KeyNotFound => Error::KeyNotFound,
_ => Error::TrezorDevice(err),
}
}
}
impl From<libusb::Error> for Error {
fn from(err: libusb::Error) -> Error {
Error::Usb(err)
@ -90,23 +130,29 @@ impl From<libusb::Error> for Error {
pub struct HardwareWalletManager {
update_thread: Option<thread::JoinHandle<()>>,
exiting: Arc<AtomicBool>,
ledger: Arc<Mutex<ledger::Manager>>,
ledger: Arc<ledger::Manager>,
trezor: Arc<trezor::Manager>,
}
struct EventHandler {
ledger: Weak<Mutex<ledger::Manager>>,
ledger: Weak<ledger::Manager>,
trezor: Weak<trezor::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() {
if let (Some(l), Some(t)) = (self.ledger.upgrade(), self.trezor.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| {
let l_devices = l.update_devices().unwrap_or_else(|e| {
debug!("Error enumerating Ledger devices: {}", e);
0
}) > 0 {
});
let t_devices = t.update_devices().unwrap_or_else(|e| {
debug!("Error enumerating Trezor devices: {}", e);
0
});
if l_devices + t_devices > 0 {
break;
}
thread::sleep(Duration::from_millis(200));
@ -116,10 +162,9 @@ impl libusb::Hotplug for EventHandler {
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);
}
if let (Some(l), Some(t)) = (self.ledger.upgrade(), self.trezor.upgrade()) {
l.update_devices().unwrap_or_else(|e| {debug!("Error enumerating Ledger devices: {}", e); 0});
t.update_devices().unwrap_or_else(|e| {debug!("Error enumerating Trezor devices: {}", e); 0});
}
}
}
@ -127,47 +172,88 @@ impl libusb::Hotplug for EventHandler {
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 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()));
usb_context.register_callback(
None, None, None,
Box::new(EventHandler {
ledger: Arc::downgrade(&ledger),
trezor: Arc::downgrade(&trezor),
}),
)?;
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;
let t = trezor.clone();
let thread = thread::Builder::new()
.name("hw_wallet".to_string())
.spawn(move || {
if let Err(e) = l.update_devices() {
debug!("Error updating ledger devices: {}", e);
}
}
}).ok();
if let Err(e) = t.update_devices() {
debug!("Error updating trezor 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,
trezor: trezor,
})
}
/// Select key derivation path for a chain.
pub fn set_key_path(&self, key_path: KeyPath) {
self.ledger.lock().set_key_path(key_path);
self.ledger.set_key_path(key_path);
self.trezor.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()
let mut wallets = Vec::new();
wallets.extend(self.ledger.list_devices());
wallets.extend(self.trezor.list_devices());
wallets
}
/// Return a list of paths to locked hardware wallets
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> {
self.ledger.lock().device_info(address)
if let Some(info) = self.ledger.device_info(address) {
Some(info)
} else {
self.trezor.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)?)
pub fn sign_transaction(&self, address: &Address, t_info: &TransactionInfo, encoded_transaction: &[u8]) -> Result<Signature, Error> {
if self.ledger.device_info(address).is_some() {
Ok(self.ledger.sign_transaction(address, encoded_transaction)?)
} else if self.trezor.device_info(address).is_some() {
Ok(self.trezor.sign_transaction(address, t_info)?)
} else {
Err(Error::KeyNotFound)
}
}
/// Send a pin to a device at a certain path to unlock it
pub fn pin_matrix_ack(&self, path: &str, pin: &str) -> Result<bool, Error> {
self.trezor.pin_matrix_ack(path, pin).map_err(Error::TrezorDevice)
}
}

440
hw/src/trezor.rs Normal file
View File

@ -0,0 +1,440 @@
// 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/>.
//! Trezor hardware wallet module. Supports Trezor v1.
//! See http://doc.satoshilabs.com/trezor-tech/api-protobuf.html
//! and https://github.com/trezor/trezor-common/blob/master/protob/protocol.md
//! for protocol details.
use super::{WalletInfo, TransactionInfo, KeyPath};
use bigint::hash::H256;
use ethkey::{Address, Signature};
use hidapi;
use parking_lot::{Mutex, RwLock};
use protobuf;
use protobuf::{Message, ProtobufEnum};
use std::cmp::{min, max};
use std::fmt;
use std::sync::Arc;
use std::time::Duration;
use bigint::prelude::uint::U256;
use trezor_sys::messages::{EthereumAddress, PinMatrixAck, MessageType, EthereumTxRequest, EthereumSignTx, EthereumGetAddress, EthereumTxAck, ButtonAck};
const TREZOR_VID: u16 = 0x534c;
const TREZOR_PIDS: [u16; 1] = [0x0001]; // Trezor v1, keeping this as an array to leave room for Trezor v2 which is in progress
const ETH_DERIVATION_PATH: [u32; 4] = [0x8000002C, 0x8000003C, 0x80000000, 0]; // m/44'/60'/0'/0
const ETC_DERIVATION_PATH: [u32; 4] = [0x8000002C, 0x8000003D, 0x80000000, 0]; // m/44'/61'/0'/0
/// Hardware wallet error.
#[derive(Debug)]
pub enum Error {
/// Ethereum wallet protocol error.
Protocol(&'static str),
/// Hidapi error.
Usb(hidapi::HidError),
/// Device with request key is not available.
KeyNotFound,
/// Signing has been cancelled by user.
UserCancel,
/// The Message Type given in the trezor RPC call is not something we recognize
BadMessageType,
/// Trying to read from a closed device at the given path
ClosedDevice(String),
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
match *self {
Error::Protocol(ref s) => write!(f, "Trezor protocol error: {}", s),
Error::Usb(ref e) => write!(f, "USB communication error: {}", e),
Error::KeyNotFound => write!(f, "Key not found"),
Error::UserCancel => write!(f, "Operation has been cancelled"),
Error::BadMessageType => write!(f, "Bad Message Type in RPC call"),
Error::ClosedDevice(ref s) => write!(f, "Device is closed, needs PIN to perform operations: {}", s),
}
}
}
impl From<hidapi::HidError> for Error {
fn from(err: hidapi::HidError) -> Error {
Error::Usb(err)
}
}
impl From<protobuf::ProtobufError> for Error {
fn from(_: protobuf::ProtobufError) -> Error {
Error::Protocol(&"Could not read response from Trezor Device")
}
}
/// Ledger device manager.
pub struct Manager {
usb: Arc<Mutex<hidapi::HidApi>>,
devices: RwLock<Vec<Device>>,
closed_devices: RwLock<Vec<String>>,
key_path: RwLock<KeyPath>,
}
#[derive(Debug)]
struct Device {
path: String,
info: WalletInfo,
}
/// HID Version used for the Trezor device
enum HidVersion {
V1,
V2,
}
impl Manager {
/// Create a new instance.
pub fn new(hidapi: Arc<Mutex<hidapi::HidApi>>) -> Manager {
Manager {
usb: hidapi,
devices: RwLock::new(Vec::new()),
closed_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 closed_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_info(&usb, &usb_device) {
Ok(device) => new_devices.push(device),
Err(Error::ClosedDevice(path)) => closed_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, closed_devices);
*self.devices.write() = new_devices;
*self.closed_devices.write() = closed_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> {
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::ClosedDevice(dev_info.path.clone())),
Err(e) => Err(e),
}
}
/// 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.closed_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>
{
let mut err = Error::KeyNotFound;
/// Try to open device a few times.
for _ in 0..10 {
match f() {
Ok(handle) => return Ok(handle),
Err(e) => err = From::from(e),
}
::std::thread::sleep(Duration::from_millis(200));
}
Err(err)
}
pub fn pin_matrix_ack(&self, device_path: &str, pin: &str) -> Result<bool, Error> {
let unlocked = {
let usb = self.usb.lock();
let device = self.open_path(|| usb.open_path(&device_path))?;
let t = MessageType::MessageType_PinMatrixAck;
let mut m = PinMatrixAck::new();
m.set_pin(pin.to_string());
self.send_device_message(&device, &t, &m)?;
let (resp_type, _) = self.read_device_response(&device)?;
match resp_type {
// Getting an Address back means it's unlocked, this is undocumented behavior
MessageType::MessageType_EthereumAddress => Ok(true),
// Getting anything else means we didn't unlock it
_ => Ok(false),
}
};
self.update_devices()?;
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];
val.to_big_endian(&mut buf);
buf.iter().skip_while(|x| **x == 0).cloned().collect()
}
fn signing_loop(&self, handle: &hidapi::HidDevice, chain_id: &Option<u64>, data: &[u8]) -> Result<Signature, Error> {
let (resp_type, bytes) = self.read_device_response(&handle)?;
match resp_type {
MessageType::MessageType_Cancel => Err(Error::UserCancel),
MessageType::MessageType_ButtonRequest => {
self.send_device_message(handle, &MessageType::MessageType_ButtonAck, &ButtonAck::new())?;
// Signing loop goes back to the top and reading blocks
// for up to 5 minutes waiting for response from the device
// if the user doesn't click any button within 5 minutes you
// get a signing error and the device sort of locks up on the signing screen
self.signing_loop(handle, chain_id, data)
}
MessageType::MessageType_EthereumTxRequest => {
let resp: EthereumTxRequest = protobuf::core::parse_from_bytes(&bytes)?;
if resp.has_data_length() {
let mut msg = EthereumTxAck::new();
let len = resp.get_data_length() as usize;
msg.set_data_chunk(data[..len].to_vec());
self.send_device_message(handle, &MessageType::MessageType_EthereumTxAck, &msg)?;
self.signing_loop(handle, chain_id, &data[len..])
} else {
let v = resp.get_signature_v();
let r = H256::from_slice(resp.get_signature_r());
let s = H256::from_slice(resp.get_signature_s());
if let Some(c_id) = *chain_id {
// If there is a chain_id supplied, Trezor will return a v
// part of the signature that is already adjusted for EIP-155,
// so v' = v + 2 * chain_id + 35, but code further down the
// pipeline will already do this transformation, so remove it here
let adjustment = 35 + 2 * c_id as u32;
Ok(Signature::from_rsv(&r, &s, (max(v, adjustment) - adjustment) as u8))
} else {
// If there isn't a chain_id, v will be returned as v + 27
let adjusted_v = if v < 27 { v } else { v - 27 };
Ok(Signature::from_rsv(&r, &s, adjusted_v as u8))
}
}
}
MessageType::MessageType_Failure => Err(Error::Protocol("Last message sent to Trezor failed")),
_ => Err(Error::Protocol("Unexpected response from Trezor device.")),
}
}
fn send_device_message(&self, device: &hidapi::HidDevice, msg_type: &MessageType, msg: &Message) -> Result<usize, Error> {
let msg_id = *msg_type as u16;
let mut message = msg.write_to_bytes()?;
let msg_size = message.len();
let mut data = Vec::new();
let hid_version = self.probe_hid_version(device)?;
// Magic constants
data.push('#' as u8);
data.push('#' as u8);
// Convert msg_id to BE and split into bytes
data.push(((msg_id >> 8) & 0xFF) as u8);
data.push((msg_id & 0xFF) as u8);
// Convert msg_size to BE and split into bytes
data.push(((msg_size >> 24) & 0xFF) as u8);
data.push(((msg_size >> 16) & 0xFF) as u8);
data.push(((msg_size >> 8) & 0xFF) as u8);
data.push((msg_size & 0xFF) as u8);
data.append(&mut message);
while data.len() % 63 > 0 {
data.push(0);
}
let mut total_written = 0;
for chunk in data.chunks(63) {
let mut padded_chunk = match hid_version {
HidVersion::V1 => vec!['?' as u8],
HidVersion::V2 => vec![0, '?' as u8],
};
padded_chunk.extend_from_slice(&chunk);
total_written += device.write(&padded_chunk)?;
}
Ok(total_written)
}
fn probe_hid_version(&self, device: &hidapi::HidDevice) -> Result<HidVersion, Error> {
let mut buf2 = [0xFFu8; 65];
buf2[0] = 0;
buf2[1] = 63;
let mut buf1 = [0xFFu8; 64];
buf1[0] = 63;
if device.write(&buf2)? == 65 {
Ok(HidVersion::V2)
} else if device.write(&buf1)? == 64 {
Ok(HidVersion::V1)
} else {
Err(Error::Usb("Unable to determine HID Version"))
}
}
fn read_device_response(&self, device: &hidapi::HidDevice) -> Result<(MessageType, Vec<u8>), Error> {
let protocol_err = Error::Protocol(&"Unexpected wire response from Trezor Device");
let mut buf = vec![0; 64];
let first_chunk = device.read_timeout(&mut buf, 300_000)?;
if first_chunk < 9 || buf[0] != '?' as u8 || buf[1] != '#' as u8 || buf[2] != '#' as u8 {
return Err(protocol_err);
}
let msg_type = MessageType::from_i32(((buf[3] as i32 & 0xFF) << 8) + (buf[4] as i32 & 0xFF)).ok_or(protocol_err)?;
let msg_size = ((buf[5] as u32 & 0xFF) << 24) + ((buf[6] as u32 & 0xFF) << 16) + ((buf[7] as u32 & 0xFF) << 8) + (buf[8] as u32 & 0xFF);
let mut data = Vec::new();
data.extend_from_slice(&buf[9..]);
while data.len() < (msg_size as usize) {
device.read_timeout(&mut buf, 10_000)?;
data.extend_from_slice(&buf[1..]);
}
Ok((msg_type, data[..msg_size as usize].to_vec()))
}
}
#[test]
#[ignore]
/// This test can't be run without an actual trezor device connected
/// (and unlocked) attached to the machine that's running the test
fn test_signature() {
use bigint::prelude::uint::U256;
use bigint::hash::{H160, H256};
let hidapi = Arc::new(Mutex::new(hidapi::HidApi::new().unwrap()));
let manager = Manager::new(hidapi.clone());
let addr: Address = H160::from("some_addr");
manager.update_devices().unwrap();
let t_info = TransactionInfo {
nonce: U256::from(1),
gas_price: U256::from(100),
gas_limit: U256::from(21_000),
to: Some(H160::from("some_other_addr")),
chain_id: Some(17),
value: U256::from(1_000_000),
data: (&[1u8; 3000]).to_vec(),
};
let signature = manager.sign_transaction(&addr, &t_info).unwrap();
let expected = Signature::from_rsv(
&H256::from("device_specific_r"),
&H256::from("device_specific_s"),
0x01
);
assert_eq!(signature, expected)
}

916
js/package-lock.json generated
View File

@ -418,7 +418,7 @@
"glob": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
"integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==",
"integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=",
"dev": true,
"requires": {
"fs.realpath": "1.0.0",
@ -706,7 +706,7 @@
"glob": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
"integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==",
"integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=",
"dev": true,
"requires": {
"fs.realpath": "1.0.0",
@ -2176,6 +2176,7 @@
"requires": {
"anymatch": "1.3.2",
"async-each": "1.0.1",
"fsevents": "1.1.2",
"glob-parent": "2.0.0",
"inherits": "2.0.3",
"is-binary-path": "1.0.1",
@ -4130,7 +4131,7 @@
"glob": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
"integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==",
"integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=",
"dev": true,
"requires": {
"fs.realpath": "1.0.0",
@ -4968,6 +4969,905 @@
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
},
"fsevents": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.1.2.tgz",
"integrity": "sha512-Sn44E5wQW4bTHXvQmvSHwqbuiXtduD6Rrjm2ZtUEGbyrig+nUH3t/QD4M4/ZXViY556TBpRgZkHLDx3JxPwxiw==",
"dev": true,
"optional": true,
"requires": {
"nan": "2.6.2",
"node-pre-gyp": "0.6.36"
},
"dependencies": {
"abbrev": {
"version": "1.1.0",
"bundled": true,
"dev": true,
"optional": true
},
"ajv": {
"version": "4.11.8",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"co": "4.6.0",
"json-stable-stringify": "1.0.1"
}
},
"ansi-regex": {
"version": "2.1.1",
"bundled": true,
"dev": true
},
"aproba": {
"version": "1.1.1",
"bundled": true,
"dev": true,
"optional": true
},
"are-we-there-yet": {
"version": "1.1.4",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"delegates": "1.0.0",
"readable-stream": "2.2.9"
}
},
"asn1": {
"version": "0.2.3",
"bundled": true,
"dev": true,
"optional": true
},
"assert-plus": {
"version": "0.2.0",
"bundled": true,
"dev": true,
"optional": true
},
"asynckit": {
"version": "0.4.0",
"bundled": true,
"dev": true,
"optional": true
},
"aws-sign2": {
"version": "0.6.0",
"bundled": true,
"dev": true,
"optional": true
},
"aws4": {
"version": "1.6.0",
"bundled": true,
"dev": true,
"optional": true
},
"balanced-match": {
"version": "0.4.2",
"bundled": true,
"dev": true
},
"bcrypt-pbkdf": {
"version": "1.0.1",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"tweetnacl": "0.14.5"
}
},
"block-stream": {
"version": "0.0.9",
"bundled": true,
"dev": true,
"requires": {
"inherits": "2.0.3"
}
},
"boom": {
"version": "2.10.1",
"bundled": true,
"dev": true,
"requires": {
"hoek": "2.16.3"
}
},
"brace-expansion": {
"version": "1.1.7",
"bundled": true,
"dev": true,
"requires": {
"balanced-match": "0.4.2",
"concat-map": "0.0.1"
}
},
"buffer-shims": {
"version": "1.0.0",
"bundled": true,
"dev": true
},
"caseless": {
"version": "0.12.0",
"bundled": true,
"dev": true,
"optional": true
},
"co": {
"version": "4.6.0",
"bundled": true,
"dev": true,
"optional": true
},
"code-point-at": {
"version": "1.1.0",
"bundled": true,
"dev": true
},
"combined-stream": {
"version": "1.0.5",
"bundled": true,
"dev": true,
"requires": {
"delayed-stream": "1.0.0"
}
},
"concat-map": {
"version": "0.0.1",
"bundled": true,
"dev": true
},
"console-control-strings": {
"version": "1.1.0",
"bundled": true,
"dev": true
},
"core-util-is": {
"version": "1.0.2",
"bundled": true,
"dev": true
},
"cryptiles": {
"version": "2.0.5",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"boom": "2.10.1"
}
},
"dashdash": {
"version": "1.14.1",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"assert-plus": "1.0.0"
},
"dependencies": {
"assert-plus": {
"version": "1.0.0",
"bundled": true,
"dev": true,
"optional": true
}
}
},
"debug": {
"version": "2.6.8",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"ms": "2.0.0"
}
},
"deep-extend": {
"version": "0.4.2",
"bundled": true,
"dev": true,
"optional": true
},
"delayed-stream": {
"version": "1.0.0",
"bundled": true,
"dev": true
},
"delegates": {
"version": "1.0.0",
"bundled": true,
"dev": true,
"optional": true
},
"ecc-jsbn": {
"version": "0.1.1",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"jsbn": "0.1.1"
}
},
"extend": {
"version": "3.0.1",
"bundled": true,
"dev": true,
"optional": true
},
"extsprintf": {
"version": "1.0.2",
"bundled": true,
"dev": true
},
"forever-agent": {
"version": "0.6.1",
"bundled": true,
"dev": true,
"optional": true
},
"form-data": {
"version": "2.1.4",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"asynckit": "0.4.0",
"combined-stream": "1.0.5",
"mime-types": "2.1.15"
}
},
"fs.realpath": {
"version": "1.0.0",
"bundled": true,
"dev": true
},
"fstream": {
"version": "1.0.11",
"bundled": true,
"dev": true,
"requires": {
"graceful-fs": "4.1.11",
"inherits": "2.0.3",
"mkdirp": "0.5.1",
"rimraf": "2.6.1"
}
},
"fstream-ignore": {
"version": "1.0.5",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"fstream": "1.0.11",
"inherits": "2.0.3",
"minimatch": "3.0.4"
}
},
"gauge": {
"version": "2.7.4",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"aproba": "1.1.1",
"console-control-strings": "1.1.0",
"has-unicode": "2.0.1",
"object-assign": "4.1.1",
"signal-exit": "3.0.2",
"string-width": "1.0.2",
"strip-ansi": "3.0.1",
"wide-align": "1.1.2"
}
},
"getpass": {
"version": "0.1.7",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"assert-plus": "1.0.0"
},
"dependencies": {
"assert-plus": {
"version": "1.0.0",
"bundled": true,
"dev": true,
"optional": true
}
}
},
"glob": {
"version": "7.1.2",
"bundled": true,
"dev": true,
"requires": {
"fs.realpath": "1.0.0",
"inflight": "1.0.6",
"inherits": "2.0.3",
"minimatch": "3.0.4",
"once": "1.4.0",
"path-is-absolute": "1.0.1"
}
},
"graceful-fs": {
"version": "4.1.11",
"bundled": true,
"dev": true
},
"har-schema": {
"version": "1.0.5",
"bundled": true,
"dev": true,
"optional": true
},
"har-validator": {
"version": "4.2.1",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"ajv": "4.11.8",
"har-schema": "1.0.5"
}
},
"has-unicode": {
"version": "2.0.1",
"bundled": true,
"dev": true,
"optional": true
},
"hawk": {
"version": "3.1.3",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"boom": "2.10.1",
"cryptiles": "2.0.5",
"hoek": "2.16.3",
"sntp": "1.0.9"
}
},
"hoek": {
"version": "2.16.3",
"bundled": true,
"dev": true
},
"http-signature": {
"version": "1.1.1",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"assert-plus": "0.2.0",
"jsprim": "1.4.0",
"sshpk": "1.13.0"
}
},
"inflight": {
"version": "1.0.6",
"bundled": true,
"dev": true,
"requires": {
"once": "1.4.0",
"wrappy": "1.0.2"
}
},
"inherits": {
"version": "2.0.3",
"bundled": true,
"dev": true
},
"ini": {
"version": "1.3.4",
"bundled": true,
"dev": true,
"optional": true
},
"is-fullwidth-code-point": {
"version": "1.0.0",
"bundled": true,
"dev": true,
"requires": {
"number-is-nan": "1.0.1"
}
},
"is-typedarray": {
"version": "1.0.0",
"bundled": true,
"dev": true,
"optional": true
},
"isarray": {
"version": "1.0.0",
"bundled": true,
"dev": true
},
"isstream": {
"version": "0.1.2",
"bundled": true,
"dev": true,
"optional": true
},
"jodid25519": {
"version": "1.0.2",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"jsbn": "0.1.1"
}
},
"jsbn": {
"version": "0.1.1",
"bundled": true,
"dev": true,
"optional": true
},
"json-schema": {
"version": "0.2.3",
"bundled": true,
"dev": true,
"optional": true
},
"json-stable-stringify": {
"version": "1.0.1",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"jsonify": "0.0.0"
}
},
"json-stringify-safe": {
"version": "5.0.1",
"bundled": true,
"dev": true,
"optional": true
},
"jsonify": {
"version": "0.0.0",
"bundled": true,
"dev": true,
"optional": true
},
"jsprim": {
"version": "1.4.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"assert-plus": "1.0.0",
"extsprintf": "1.0.2",
"json-schema": "0.2.3",
"verror": "1.3.6"
},
"dependencies": {
"assert-plus": {
"version": "1.0.0",
"bundled": true,
"dev": true,
"optional": true
}
}
},
"mime-db": {
"version": "1.27.0",
"bundled": true,
"dev": true
},
"mime-types": {
"version": "2.1.15",
"bundled": true,
"dev": true,
"requires": {
"mime-db": "1.27.0"
}
},
"minimatch": {
"version": "3.0.4",
"bundled": true,
"dev": true,
"requires": {
"brace-expansion": "1.1.7"
}
},
"minimist": {
"version": "0.0.8",
"bundled": true,
"dev": true
},
"mkdirp": {
"version": "0.5.1",
"bundled": true,
"dev": true,
"requires": {
"minimist": "0.0.8"
}
},
"ms": {
"version": "2.0.0",
"bundled": true,
"dev": true,
"optional": true
},
"node-pre-gyp": {
"version": "0.6.36",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"mkdirp": "0.5.1",
"nopt": "4.0.1",
"npmlog": "4.1.0",
"rc": "1.2.1",
"request": "2.81.0",
"rimraf": "2.6.1",
"semver": "5.3.0",
"tar": "2.2.1",
"tar-pack": "3.4.0"
}
},
"nopt": {
"version": "4.0.1",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"abbrev": "1.1.0",
"osenv": "0.1.4"
}
},
"npmlog": {
"version": "4.1.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"are-we-there-yet": "1.1.4",
"console-control-strings": "1.1.0",
"gauge": "2.7.4",
"set-blocking": "2.0.0"
}
},
"number-is-nan": {
"version": "1.0.1",
"bundled": true,
"dev": true
},
"oauth-sign": {
"version": "0.8.2",
"bundled": true,
"dev": true,
"optional": true
},
"object-assign": {
"version": "4.1.1",
"bundled": true,
"dev": true,
"optional": true
},
"once": {
"version": "1.4.0",
"bundled": true,
"dev": true,
"requires": {
"wrappy": "1.0.2"
}
},
"os-homedir": {
"version": "1.0.2",
"bundled": true,
"dev": true,
"optional": true
},
"os-tmpdir": {
"version": "1.0.2",
"bundled": true,
"dev": true,
"optional": true
},
"osenv": {
"version": "0.1.4",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"os-homedir": "1.0.2",
"os-tmpdir": "1.0.2"
}
},
"path-is-absolute": {
"version": "1.0.1",
"bundled": true,
"dev": true
},
"performance-now": {
"version": "0.2.0",
"bundled": true,
"dev": true,
"optional": true
},
"process-nextick-args": {
"version": "1.0.7",
"bundled": true,
"dev": true
},
"punycode": {
"version": "1.4.1",
"bundled": true,
"dev": true,
"optional": true
},
"qs": {
"version": "6.4.0",
"bundled": true,
"dev": true,
"optional": true
},
"rc": {
"version": "1.2.1",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"deep-extend": "0.4.2",
"ini": "1.3.4",
"minimist": "1.2.0",
"strip-json-comments": "2.0.1"
},
"dependencies": {
"minimist": {
"version": "1.2.0",
"bundled": true,
"dev": true,
"optional": true
}
}
},
"readable-stream": {
"version": "2.2.9",
"bundled": true,
"dev": true,
"requires": {
"buffer-shims": "1.0.0",
"core-util-is": "1.0.2",
"inherits": "2.0.3",
"isarray": "1.0.0",
"process-nextick-args": "1.0.7",
"string_decoder": "1.0.1",
"util-deprecate": "1.0.2"
}
},
"request": {
"version": "2.81.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"aws-sign2": "0.6.0",
"aws4": "1.6.0",
"caseless": "0.12.0",
"combined-stream": "1.0.5",
"extend": "3.0.1",
"forever-agent": "0.6.1",
"form-data": "2.1.4",
"har-validator": "4.2.1",
"hawk": "3.1.3",
"http-signature": "1.1.1",
"is-typedarray": "1.0.0",
"isstream": "0.1.2",
"json-stringify-safe": "5.0.1",
"mime-types": "2.1.15",
"oauth-sign": "0.8.2",
"performance-now": "0.2.0",
"qs": "6.4.0",
"safe-buffer": "5.0.1",
"stringstream": "0.0.5",
"tough-cookie": "2.3.2",
"tunnel-agent": "0.6.0",
"uuid": "3.0.1"
}
},
"rimraf": {
"version": "2.6.1",
"bundled": true,
"dev": true,
"requires": {
"glob": "7.1.2"
}
},
"safe-buffer": {
"version": "5.0.1",
"bundled": true,
"dev": true
},
"semver": {
"version": "5.3.0",
"bundled": true,
"dev": true,
"optional": true
},
"set-blocking": {
"version": "2.0.0",
"bundled": true,
"dev": true,
"optional": true
},
"signal-exit": {
"version": "3.0.2",
"bundled": true,
"dev": true,
"optional": true
},
"sntp": {
"version": "1.0.9",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"hoek": "2.16.3"
}
},
"sshpk": {
"version": "1.13.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"asn1": "0.2.3",
"assert-plus": "1.0.0",
"bcrypt-pbkdf": "1.0.1",
"dashdash": "1.14.1",
"ecc-jsbn": "0.1.1",
"getpass": "0.1.7",
"jodid25519": "1.0.2",
"jsbn": "0.1.1",
"tweetnacl": "0.14.5"
},
"dependencies": {
"assert-plus": {
"version": "1.0.0",
"bundled": true,
"dev": true,
"optional": true
}
}
},
"string_decoder": {
"version": "1.0.1",
"bundled": true,
"dev": true,
"requires": {
"safe-buffer": "5.0.1"
}
},
"string-width": {
"version": "1.0.2",
"bundled": true,
"dev": true,
"requires": {
"code-point-at": "1.1.0",
"is-fullwidth-code-point": "1.0.0",
"strip-ansi": "3.0.1"
}
},
"stringstream": {
"version": "0.0.5",
"bundled": true,
"dev": true,
"optional": true
},
"strip-ansi": {
"version": "3.0.1",
"bundled": true,
"dev": true,
"requires": {
"ansi-regex": "2.1.1"
}
},
"strip-json-comments": {
"version": "2.0.1",
"bundled": true,
"dev": true,
"optional": true
},
"tar": {
"version": "2.2.1",
"bundled": true,
"dev": true,
"requires": {
"block-stream": "0.0.9",
"fstream": "1.0.11",
"inherits": "2.0.3"
}
},
"tar-pack": {
"version": "3.4.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"debug": "2.6.8",
"fstream": "1.0.11",
"fstream-ignore": "1.0.5",
"once": "1.4.0",
"readable-stream": "2.2.9",
"rimraf": "2.6.1",
"tar": "2.2.1",
"uid-number": "0.0.6"
}
},
"tough-cookie": {
"version": "2.3.2",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"punycode": "1.4.1"
}
},
"tunnel-agent": {
"version": "0.6.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"safe-buffer": "5.0.1"
}
},
"tweetnacl": {
"version": "0.14.5",
"bundled": true,
"dev": true,
"optional": true
},
"uid-number": {
"version": "0.0.6",
"bundled": true,
"dev": true,
"optional": true
},
"util-deprecate": {
"version": "1.0.2",
"bundled": true,
"dev": true
},
"uuid": {
"version": "3.0.1",
"bundled": true,
"dev": true,
"optional": true
},
"verror": {
"version": "1.3.6",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"extsprintf": "1.0.2"
}
},
"wide-align": {
"version": "1.1.2",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"string-width": "1.0.2"
}
},
"wrappy": {
"version": "1.0.2",
"bundled": true,
"dev": true
}
}
},
"fstream": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz",
@ -7722,7 +8622,7 @@
"minimatch": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=",
"requires": {
"brace-expansion": "1.1.8"
}
@ -10081,7 +10981,7 @@
"react-qr-reader": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/react-qr-reader/-/react-qr-reader-1.1.3.tgz",
"integrity": "sha512-ruBF8KaSwUW9nbzjO4rA7/HOCGYZuNUz9od7uBRy8SRBi24nwxWWmwa2z8R6vPGDRglA0y2Qk1aVBuC1olTnHw==",
"integrity": "sha1-dDmnZvyZPLj17u/HLCnblh1AswI=",
"requires": {
"jsqr": "git+https://github.com/JodusNodus/jsQR.git#5ba1acefa1cbb9b2bc92b49f503f2674e2ec212b",
"prop-types": "15.5.10",
@ -11654,7 +12554,7 @@
"string-width": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
"integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==",
"integrity": "sha1-q5Pyeo3BPSjKyBXEYhQ6bZASrp4=",
"dev": true,
"requires": {
"is-fullwidth-code-point": "2.0.0",
@ -12611,7 +13511,7 @@
"async": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/async/-/async-2.5.0.tgz",
"integrity": "sha512-e+lJAJeNWuPCNyxZKOBdaJGyLGHugXVQtrAwtuAe2vhxTYxFTKE73p8JuTmdH0qdQZtDvI4dhJwjZc5zsfIsYw==",
"integrity": "sha1-hDGQ/WtzV6C54clW7d3V7IRitU0=",
"dev": true,
"requires": {
"lodash": "4.17.2"
@ -13016,7 +13916,7 @@
"commander": {
"version": "2.11.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz",
"integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ=="
"integrity": "sha1-FXFS/R56bI2YpbcVzzdt+SgARWM="
},
"detect-indent": {
"version": "5.0.0",

View File

@ -252,6 +252,16 @@ export default class Parity {
.then(outHwAccountInfo);
}
lockedHardwareAccountsInfo () {
return this._transport
.execute('parity_lockedHardwareAccountsInfo');
}
hardwarePinMatrixAck (path, pin) {
return this._transport
.execute('parity_hardwarePinMatrixAck', path, pin);
}
hashContent (url) {
return this._transport
.execute('parity_hashContent', url);

View File

@ -360,6 +360,37 @@ export default {
}
},
lockedHardwareAccountsInfo: {
desc: 'Provides a list of paths to locked hardware wallets',
params: [],
returns: {
type: Array,
desc: 'Paths of all locked hardware wallets',
example: "['/dev/hidraw0']"
}
},
hardwarePinMatrixAck: {
desc: 'Send a pin to a hardware wallet at a specific path to unlock it',
params: [
{
type: String,
desc: 'path to the device',
example: 'USB_2b24_0001_14100000'
},
{
type: String,
desc: 'the pin as recieved from the pin matrix',
example: '1234'
}
],
returns: {
type: Boolean,
desc: 'Whether or not the pin entry successfully unlocked the device',
example: true
}
},
listOpenedVaults: {
desc: 'Returns a list of all opened vaults',
params: [],

View File

@ -24,11 +24,14 @@ let instance = null;
export default class HardwareStore {
@observable isScanning = false;
@observable wallets = {};
@observable pinMatrixRequest = [];
constructor (api) {
this._api = api;
this._ledger = Ledger.create(api);
this._pollId = null;
this.hwAccounts = {};
this.ledgerAccounts = {};
this._pollScan();
this._subscribeParity();
@ -49,12 +52,31 @@ export default class HardwareStore {
this.wallets = wallets;
}
@action setPinMatrixRequest = (requests) => {
this.pinMatrixRequest = requests;
}
_pollScan = () => {
this._pollId = setTimeout(() => {
this.scan().then(this._pollScan);
}, HW_SCAN_INTERVAL);
}
scanTrezor () {
return this._api.parity
.lockedHardwareAccountsInfo()
.then((paths) => {
this.setPinMatrixRequest(paths.map((path) => {
return { path: path, manufacturer: 'Trezor' };
}));
return {};
})
.catch((err) => {
console.warn('HardwareStore::scanTrezor', err);
return {};
});
}
scanLedger () {
if (!this._ledger.isSupported) {
return Promise.resolve({});
@ -101,7 +123,8 @@ export default class HardwareStore {
info.address = address;
info.via = 'parity';
});
this.setWallets(hwInfo);
this.hwAccounts = hwInfo;
this.updateWallets();
return hwInfo;
},
onError
@ -110,6 +133,9 @@ export default class HardwareStore {
scan () {
this.setScanning(true);
// This only scans for locked devices and does not return open devices,
// so no need to actually wait for any results here.
this.scanTrezor();
// NOTE: Depending on how the hardware is configured and how the local env setup
// is done, different results will be retrieved via Parity vs. the browser APIs
@ -117,13 +143,18 @@ export default class HardwareStore {
// not intended as a network call, i.e. hw wallet is with the user)
return this.scanLedger()
.then((ledgerAccounts) => {
this.ledgerAccounts = ledgerAccounts;
transaction(() => {
this.setWallets(Object.assign({}, ledgerAccounts));
this.updateWallets();
this.setScanning(false);
});
});
}
updateWallets () {
this.setWallets(Object.assign({}, this.hwAccounts, this.ledgerAccounts));
}
createAccountInfo (entry, original = {}) {
const { address, manufacturer, name } = entry;
@ -151,6 +182,15 @@ export default class HardwareStore {
return this._ledger.signTransaction(transaction);
}
pinMatrixAck (device, passcode) {
return this._api.parity
.hardwarePinMatrixAck(device.path, passcode)
.then((success) => {
this.scan();
return success;
});
}
static get (api) {
if (!instance) {
instance = new HardwareStore(api);

View File

@ -0,0 +1,17 @@
// 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/>.
export default from './pinMatrix';

View File

@ -0,0 +1,142 @@
/* 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/>.
*/
.overlay {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
background: rgba(255, 255, 255, 0.75);
z-index: 20000;
}
.body {
margin: 0 auto;
padding: 2em 4em;
text-align: center;
max-width: 30em;
min-height: 200px;
background: rgba(25, 25, 25, 0.75);
color: rgb(208, 208, 208);
box-shadow: rgba(0, 0, 0, 0.25) 0px 14px 45px, rgba(0, 0, 0, 0.22) 0px 10px 18px;
}
.passcodeBoxes {
display: flex;
flex-flow: row wrap;
justify-content: center;
width: 100px;
padding: 20px 0;
margin: auto;
}
.passcodeBox {
position: relative;
width: 25px;
height: 25px;
line-height: 25px;
margin: 2px;
background-color: rgba(0, 0, 0, 0.5);
transition: background-color 400ms;
cursor: pointer;
border: none;
outline: none;
&:hover {
background-color: rgba(0, 0, 0, 0.25);
}
}
.passcodeBall {
position: absolute;
width: 3px;
height: 3px;
border-radius: 100%;
background-color: rgba(208, 208, 208, 0.5);
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
.pin {
margin-left: 2px;
min-height: 20px;
position: relative;
letter-spacing: 2px;
}
.clearThik {
color: rgba(0, 0, 0, 0.5);
font: 14px/100% arial, sans-serif;
position: absolute;
right: 150px;
text-decoration: none;
top: -5px;
border: solid 3px rgba(0, 0, 0, 0.5);
width: 18px;
height: 18px;
border-bottom-right-radius: 3px;
border-top-right-radius: 3px;
position: absolute;
line-height: 20px;
cursor: pointer;
&:after {
right: 100%;
top: 50%;
border: solid transparent;
content: " ";
height: 0;
width: 0;
position: absolute;
pointer-events: none;
border-right-color: rgba(0, 0, 0, 0.5);
border-width: 12px;
margin-top: -12px;
margin-right: 3px;
}
&:before {
content: '\2716';
padding-left: 2px;
}
}
.button {
padding: 7px;
background-color: rgba(0, 0, 0, 0.5);
border-radius: 3px;
cursor: pointer;
&:hover {
background-color: rgba(0, 0, 0, 0.25);
}
}
.cancel {
float: left;
}
.submit {
float: right;
}
.error {
color: rgba(218, 39, 39, 0.85);
padding-top: 7px;
}

View File

@ -0,0 +1,132 @@
// 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/>.
import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import styles from './pinMatrix.css';
export default class PinMatrix extends Component {
static propTypes = {
store: PropTypes.object.isRequired,
device: PropTypes.object.isRequired
}
state = {
passcode: '',
failureMessage: ''
}
pinMatrix = [7, 8, 9, 4, 5, 6, 1, 2, 3]
render () {
const { passcode, failureMessage } = this.state;
const { device } = this.props;
return (
<div className={ styles.overlay }>
<div className={ styles.body }>
<FormattedMessage
id='pinMatrix.enterPin'
defaultMessage='Please enter the pin for your {manufacturer} hardware wallet'
values={ {
manufacturer: device.manufacturer
} }
/>
<div className={ styles.passcodeBoxes }>
{this.renderPasscodeBox()}
</div>
<div className={ styles.pin }>
{passcode.replace(/./g, '*')}
{
passcode.length
? <div className={ styles.clearThik } onClick={ this.handleRemoveDigit } />
: null
}
</div>
<span
className={ `${styles.button} ${styles.submit}` }
onClick={ this.handleSubmit }
>
Submit
</span>
<div className={ styles.error }>
{ failureMessage }
</div>
</div>
</div>
);
}
handleAddDigit = (ev) => {
const index = ev.currentTarget.getAttribute('data-index');
const digit = this.pinMatrix[index];
const { passcode } = this.state;
if (passcode.length > 8) {
return;
}
this.setState({
passcode: passcode + digit
});
}
renderPasscodeBox () {
return Array.apply(null, Array(9)).map((box, index) => {
return (
<button
className={ styles.passcodeBox }
onClick={ this.handleAddDigit }
data-index={ index }
key={ index }
>
<div className={ styles.passcodeBall } />
</button>
);
});
}
handleRemoveDigit = () => {
this.setState({
passcode: this.state.passcode.slice(0, -1)
});
}
handleSubmit = () => {
const { device, store } = this.props;
const { passcode } = this.state;
store.pinMatrixAck(device, passcode)
.then((status) => {
const passcode = '';
const failureMessage = status ? '' : (
<FormattedMessage
id='pinMatrix.label.failureMessage'
defaultMessage='Wrong pin, try again.'
/>
);
this.setState({ passcode, failureMessage });
})
.catch(err => {
this.setState({
failureMessage: err.toString()
});
});
}
}

View File

@ -29,6 +29,7 @@ export Faucet from './Faucet';
export FirstRun from './FirstRun';
export LoadContract from './LoadContract';
export PasswordManager from './PasswordManager';
export PinMatrix from './PinMatrix';
export SaveContract from './SaveContract';
export Shapeshift from './Shapeshift';
export Transfer from './Transfer';

View File

@ -19,6 +19,8 @@ import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import UpgradeStore from '~/modals/UpgradeParity/store';
import { PinMatrix } from '~/modals';
import HardwareStore from '~/mobx/hardwareStore';
import Connection from '../Connection';
import ParityBar from '../ParityBar';
@ -52,12 +54,15 @@ class Application extends Component {
pending: PropTypes.array
}
hwstore = HardwareStore.get(this.context.api);
store = new Store(this.context.api);
upgradeStore = UpgradeStore.get(this.context.api);
render () {
const [root] = (window.location.hash || '').replace('#/', '').split('/');
const isMinimized = root === 'app' || root === 'web';
const { pinMatrixRequest } = this.hwstore;
if (process.env.NODE_ENV !== 'production' && root === 'playground') {
return (
@ -86,6 +91,7 @@ class Application extends Component {
: null
}
<Connection />
{ (pinMatrixRequest.length > 0) ? <PinMatrix device={ pinMatrixRequest[0] } store={ this.hwstore } /> : null }
<Requests />
<ParityBar dapp={ isMinimized } />
</div>

View File

@ -58,6 +58,7 @@ rlp = { path = "../util/rlp" }
stats = { path = "../util/stats" }
vm = { path = "../ethcore/vm" }
hash = { path = "../util/hash" }
hardware-wallet = { path = "../hw" }
clippy = { version = "0.0.103", optional = true}
pretty_assertions = "0.1"

View File

@ -64,6 +64,7 @@ extern crate parity_updater as updater;
extern crate rlp;
extern crate stats;
extern crate hash;
extern crate hardware_wallet;
#[macro_use]
extern crate log;

View File

@ -562,7 +562,7 @@ fn hardware_signature(accounts: &AccountProvider, address: Address, t: Transacti
let mut stream = rlp::RlpStream::new();
t.rlp_append_unsigned_transaction(&mut stream, chain_id);
let signature = accounts.sign_with_hardware(address, &stream.as_raw())
let signature = accounts.sign_with_hardware(address, &t, chain_id, &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)

View File

@ -132,6 +132,11 @@ impl Parity for ParityClient {
)
}
fn locked_hardware_accounts_info(&self) -> Result<Vec<String>, Error> {
let store = &self.accounts;
Ok(store.locked_hardware_accounts().map_err(|e| errors::account("Error communicating with hardware wallet.", e))?)
}
fn default_account(&self, meta: Self::Metadata) -> BoxFuture<H160, Error> {
let dapp_id = meta.dapp_id();
future::ok(self.accounts

View File

@ -151,6 +151,11 @@ impl<C, M, U> Parity for ParityClient<C, M, U> where
)
}
fn locked_hardware_accounts_info(&self) -> Result<Vec<String>, Error> {
let store = self.account_provider()?;
Ok(store.locked_hardware_accounts().map_err(|e| errors::account("Error communicating with hardware wallet.", e))?)
}
fn default_account(&self, meta: Self::Metadata) -> BoxFuture<H160, Error> {
let dapp_id = meta.dapp_id();
future::ok(

View File

@ -355,6 +355,11 @@ impl ParityAccounts for ParityAccountsClient {
.map(Into::into)
.map_err(|e| errors::account("Could not sign message.", e))
}
fn hardware_pin_matrix_ack(&self, path: String, pin: String) -> Result<bool, Error> {
let store = self.account_provider()?;
Ok(store.hardware_pin_matrix_ack(&path, &pin).map_err(|e| errors::account("Error communicating with hardware wallet.", e))?)
}
}
fn into_vec<A, B>(a: Vec<A>) -> Vec<B> where

View File

@ -45,6 +45,10 @@ build_rpc_trait! {
#[rpc(name = "parity_hardwareAccountsInfo")]
fn hardware_accounts_info(&self) -> Result<BTreeMap<H160, HwAccountInfo>, Error>;
/// Get a list of paths to locked hardware wallets
#[rpc(name = "parity_lockedHardwareAccountsInfo")]
fn locked_hardware_accounts_info(&self) -> Result<Vec<String>, Error>;
/// Returns default account for dapp.
#[rpc(meta, name = "parity_defaultAccount")]
fn default_account(&self, Self::Metadata) -> BoxFuture<H160, Error>;

View File

@ -184,5 +184,9 @@ build_rpc_trait! {
/// Sign raw hash with the key corresponding to address and password.
#[rpc(name = "parity_signMessage")]
fn sign_message(&self, H160, String, H256) -> Result<H520, Error>;
/// Send a PinMatrixAck to a hardware wallet, unlocking it
#[rpc(name = "parity_hardwarePinMatrixAck")]
fn hardware_pin_matrix_ack(&self, String, String) -> Result<bool, Error>;
}
}