diff --git a/Cargo.lock b/Cargo.lock index 6c979c602c876a..26d239bcd07b49 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3686,6 +3686,7 @@ dependencies = [ "clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)", "rpassword 4.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "semver 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", + "solana-hardware-wallet 0.24.0", "solana-sdk 0.24.0", "tiny-bip39 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", "url 2.1.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -4009,6 +4010,7 @@ dependencies = [ "num_cpus 1.12.0 (registry+https://github.com/rust-lang/crates.io-index)", "rpassword 4.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "solana-clap-utils 0.24.0", + "solana-hardware-wallet 0.24.0", "solana-sdk 0.24.0", "tiny-bip39 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", ] diff --git a/clap-utils/Cargo.toml b/clap-utils/Cargo.toml index e75d7a8c188927..b1343ab3859233 100644 --- a/clap-utils/Cargo.toml +++ b/clap-utils/Cargo.toml @@ -12,6 +12,7 @@ edition = "2018" clap = "2.33.0" rpassword = "4.0" semver = "0.9.0" +solana-hardware-wallet = { path = "../hardware-wallet", version = "0.24.0" } solana-sdk = { path = "../sdk", version = "0.24.0" } tiny-bip39 = "0.7.0" url = "2.1.0" diff --git a/clap-utils/src/input_parsers.rs b/clap-utils/src/input_parsers.rs index cf12d5c7ec019c..0675a585c55348 100644 --- a/clap-utils/src/input_parsers.rs +++ b/clap-utils/src/input_parsers.rs @@ -1,6 +1,7 @@ use crate::keypair::{keypair_from_seed_phrase, ASK_KEYWORD, SKIP_SEED_PHRASE_VALIDATION_ARG}; use chrono::DateTime; use clap::ArgMatches; +use solana_hardware_wallet::hardware_wallet::DerivationPath; use solana_sdk::{ clock::UnixTimestamp, native_token::sol_to_lamports, @@ -100,6 +101,16 @@ pub fn amount_of(matches: &ArgMatches<'_>, name: &str, unit: &str) -> Option, name: &str) -> Option { + matches.value_of(name).map(|derivation_str| { + let derivation_str = derivation_str.replace("'", ""); + let mut parts = derivation_str.split("/"); + let account = parts.next().unwrap().parse::().unwrap(); + let change = parts.next().map(|change| change.parse::().unwrap()); + DerivationPath { account, change } + }) +} + #[cfg(test)] mod tests { use super::*; @@ -277,4 +288,40 @@ mod tests { .get_matches_from(vec!["test", "--single", "1.5", "--unit", "lamports"]); assert_eq!(amount_of(&matches, "single", "unit"), None); } + + #[test] + fn test_derivation_of() { + let matches = app() + .clone() + .get_matches_from(vec!["test", "--single", "2/3"]); + assert_eq!( + derivation_of(&matches, "single"), + Some(DerivationPath { + account: 2, + change: Some(3) + }) + ); + assert_eq!(derivation_of(&matches, "another"), None); + let matches = app() + .clone() + .get_matches_from(vec!["test", "--single", "2"]); + assert_eq!( + derivation_of(&matches, "single"), + Some(DerivationPath { + account: 2, + change: None + }) + ); + assert_eq!(derivation_of(&matches, "another"), None); + let matches = app() + .clone() + .get_matches_from(vec!["test", "--single", "2'/3'"]); + assert_eq!( + derivation_of(&matches, "single"), + Some(DerivationPath { + account: 2, + change: Some(3) + }) + ); + } } diff --git a/clap-utils/src/input_validators.rs b/clap-utils/src/input_validators.rs index 948765b2dda403..43c085c2605260 100644 --- a/clap-utils/src/input_validators.rs +++ b/clap-utils/src/input_validators.rs @@ -141,3 +141,45 @@ pub fn is_rfc3339_datetime(value: String) -> Result<(), String> { .map(|_| ()) .map_err(|e| format!("{:?}", e)) } + +pub fn is_derivation(value: String) -> Result<(), String> { + let value = value.replace("'", ""); + let mut parts = value.split("/"); + let account = parts.next().unwrap(); + account + .parse::() + .map_err(|e| { + format!( + "Unable to parse derivation, provided: {}, err: {:?}", + account, e + ) + }) + .and_then(|_| { + if let Some(change) = parts.next() { + change.parse::().map_err(|e| { + format!( + "Unable to parse derivation, provided: {}, err: {:?}", + change, e + ) + }) + } else { + Ok(0) + } + }) + .map(|_| ()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_is_derivation() { + assert_eq!(is_derivation("2".to_string()), Ok(())); + assert_eq!(is_derivation("0".to_string()), Ok(())); + assert_eq!(is_derivation("0/2".to_string()), Ok(())); + assert_eq!(is_derivation("0'/2'".to_string()), Ok(())); + assert!(is_derivation("a".to_string()).is_err()); + assert!(is_derivation("a/b".to_string()).is_err()); + } +} diff --git a/hardware-wallet/src/hardware_wallet.rs b/hardware-wallet/src/hardware_wallet.rs index c7dc3275beb8d2..51315df902bce2 100644 --- a/hardware-wallet/src/hardware_wallet.rs +++ b/hardware-wallet/src/hardware_wallet.rs @@ -3,6 +3,7 @@ use log::*; use parking_lot::{Mutex, RwLock}; use solana_sdk::{pubkey::Pubkey, signature::Signature, transaction::Transaction}; use std::{ + fmt, sync::Arc, time::{Duration, Instant}, }; @@ -145,13 +146,12 @@ pub trait HardwareWallet { ) -> Result; /// Get solana pubkey from a HardwareWallet - fn get_pubkey(&self, account: u16, change: Option) -> Result; + fn get_pubkey(&self, derivation: DerivationPath) -> Result; /// Sign transaction data with wallet managing pubkey at derivation path m/44'/501'/'/'. fn sign_transaction( &self, - account: u16, - change: Option, + derivation: DerivationPath, transaction: Transaction, ) -> Result; } @@ -183,7 +183,30 @@ pub struct HardwareWalletInfo { pub pubkey: Pubkey, } +#[derive(Default, PartialEq)] +pub struct DerivationPath { + pub account: u16, + pub change: Option, +} + +impl fmt::Debug for DerivationPath { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let change = if let Some(change) = self.change { + format!("/{:?}'", change) + } else { + "".to_string() + }; + write!(f, "m/44'/501'/{:?}'{}", self.account, change) + } +} + /// Helper to determine if a device is a valid HID pub fn is_valid_hid_device(usage_page: u16, interface_number: i32) -> bool { usage_page == HID_GLOBAL_USAGE_PAGE || interface_number == HID_USB_DEVICE_CLASS as i32 } + +/// Helper to initialize hidapi and HardwareWalletManager +pub fn initialize_wallet_manager() -> Arc { + let hidapi = Arc::new(Mutex::new(hidapi::HidApi::new().unwrap())); + HardwareWalletManager::new(hidapi) +} diff --git a/hardware-wallet/src/ledger.rs b/hardware-wallet/src/ledger.rs index e398e118464fb1..bfed080cf9766b 100644 --- a/hardware-wallet/src/ledger.rs +++ b/hardware-wallet/src/ledger.rs @@ -1,4 +1,6 @@ -use crate::hardware_wallet::{HardwareWallet, HardwareWalletError, HardwareWalletInfo}; +use crate::hardware_wallet::{ + DerivationPath, HardwareWallet, HardwareWalletError, HardwareWalletInfo, +}; use log::*; use semver::Version as FirmwareVersion; use solana_sdk::{pubkey::Pubkey, signature::Signature, transaction::Transaction}; @@ -225,13 +227,13 @@ impl LedgerWallet { )) } - fn get_derivation_path(&self, account: u16, change: Option) -> Vec { - let byte = if change.is_some() { 4 } else { 3 }; + fn get_derivation_path(&self, derivation: DerivationPath) -> Vec { + let byte = if derivation.change.is_some() { 4 } else { 3 }; let mut concat_derivation = vec![byte]; concat_derivation.extend_from_slice(&SOL_DERIVATION_PATH_BE); concat_derivation.extend_from_slice(&[0x80, 0]); - concat_derivation.extend_from_slice(&account.to_be_bytes()); - if let Some(change) = change { + concat_derivation.extend_from_slice(&derivation.account.to_be_bytes()); + if let Some(change) = derivation.change { concat_derivation.extend_from_slice(&[0x80, 0]); concat_derivation.extend_from_slice(&change.to_be_bytes()); } @@ -256,15 +258,16 @@ impl HardwareWallet for LedgerWallet { .serial_number .clone() .unwrap_or_else(|| "Unknown".to_owned()); - self.get_pubkey(0, None).map(|pubkey| HardwareWalletInfo { - name, - manufacturer, - serial, - pubkey, - }) + self.get_pubkey(DerivationPath::default()) + .map(|pubkey| HardwareWalletInfo { + name, + manufacturer, + serial, + pubkey, + }) } - fn get_pubkey(&self, account: u16, change: Option) -> Result { + fn get_pubkey(&self, derivation: DerivationPath) -> Result { // let ledger_version = self.get_firmware_version()?; // if ledger_version < FirmwareVersion::new(1, 0, 3) { // return Err(HardwareWalletError::Protocol( @@ -272,7 +275,7 @@ impl HardwareWallet for LedgerWallet { // )); // } - let derivation_path = self.get_derivation_path(account, change); + let derivation_path = self.get_derivation_path(derivation); let key = self.send_apdu(commands::GET_SOL_PUBKEY, 0, 0, &derivation_path)?; if key.len() != 32 { @@ -283,8 +286,7 @@ impl HardwareWallet for LedgerWallet { fn sign_transaction( &self, - account: u16, - change: Option, + derivation: DerivationPath, transaction: Transaction, ) -> Result { // TODO: see if need version check here @@ -296,7 +298,7 @@ impl HardwareWallet for LedgerWallet { // } let mut chunk = [0_u8; MAX_CHUNK_SIZE]; - let derivation_path = self.get_derivation_path(account, change); + let derivation_path = self.get_derivation_path(derivation); let data = transaction.message_data(); // Copy the address of the key (only done once) @@ -346,15 +348,14 @@ pub fn is_valid_ledger(vendor_id: u16, product_id: u16) -> bool { #[cfg(test)] mod tests { use super::*; - use crate::hardware_wallet::HardwareWalletManager; + use crate::hardware_wallet::{initialize_wallet_manager, HardwareWalletManager}; use parking_lot::Mutex; use std::sync::Arc; /// This test can't be run without an actual ledger device connected with the `Ledger Wallet Solana application` running #[test] fn ledger_test() { - let hidapi = Arc::new(Mutex::new(hidapi::HidApi::new().unwrap())); - let wallet_manager = HardwareWalletManager::new(hidapi); + let wallet_manager = initialize_wallet_manager(); // Update device list wallet_manager.update_devices().expect("No Ledger found, make sure you have a unlocked Ledger connected with the Ledger Wallet Solana running"); diff --git a/keygen/Cargo.toml b/keygen/Cargo.toml index 944cb77b9206a0..8ac34d200e2351 100644 --- a/keygen/Cargo.toml +++ b/keygen/Cargo.toml @@ -15,6 +15,7 @@ dirs = "2.0.2" num_cpus = "1.12.0" rpassword = "4.0" solana-clap-utils = { path = "../clap-utils", version = "0.24.0" } +solana-hardware-wallet = { path = "../hardware-wallet", version = "0.24.0" } solana-sdk = { path = "../sdk", version = "0.24.0" } tiny-bip39 = "0.7.0" diff --git a/keygen/src/keygen.rs b/keygen/src/keygen.rs index 2826d31ba70ef9..4f1df236cbea0d 100644 --- a/keygen/src/keygen.rs +++ b/keygen/src/keygen.rs @@ -5,8 +5,15 @@ use clap::{ SubCommand, }; use num_cpus; -use solana_clap_utils::keypair::{ - keypair_from_seed_phrase, prompt_passphrase, ASK_KEYWORD, SKIP_SEED_PHRASE_VALIDATION_ARG, +use solana_clap_utils::{ + input_parsers::{derivation_of, pubkey_of}, + input_validators::is_derivation, + keypair::{ + keypair_from_seed_phrase, prompt_passphrase, ASK_KEYWORD, SKIP_SEED_PHRASE_VALIDATION_ARG, + }, +}; +use solana_hardware_wallet::hardware_wallet::{ + initialize_wallet_manager, DerivationPath, HardwareWallet, }; use solana_sdk::{ pubkey::write_pubkey_file, @@ -300,6 +307,27 @@ fn main() -> Result<(), Box> { SubCommand::with_name("pubkey") .about("Display the pubkey from a keypair file") .setting(AppSettings::DisableVersion) + .subcommand( + SubCommand::with_name("hw") + .about("Use hardware wallet for key retrieval") + .setting(AppSettings::DisableVersion) + .arg( + Arg::with_name("derivation") + .index(1) + .value_name("ACCOUNT or ACCOUNT/CHANGE") + .takes_value(true) + .validator(is_derivation) + .help("Which derivation path to use: m/44'/501'/ACCOUNT'/CHANGE'; default key is device base pubkey: m/44'/501'/0'") + ) + .arg( + Arg::with_name("wallet_base_pubkey") + .long("base-pubkey") + .value_name("PUBKEY") + .takes_value(true) + .help("Specify which hardware wallet to use") + + ) + ) .arg( Arg::with_name("infile") .index(1) @@ -356,14 +384,53 @@ fn main() -> Result<(), Box> { match matches.subcommand() { ("pubkey", Some(matches)) => { - let keypair = get_keypair_from_matches(matches)?; + let pubkey = if let Some(matches) = matches.subcommand_matches("hw") { + // TODO: move all of this into helper(s) so it can be used in CLI as well + let wallet_manager = initialize_wallet_manager(); + let device_count = wallet_manager.update_devices()?; + if device_count == 0 { + println!( + "No hardware wallets found. Make sure the Solana Wallet app is running." + ); + exit(1); + } + let wallet_base_pubkey = + pubkey_of(matches, "wallet_base_pubkey").unwrap_or_else(|| { + if device_count > 1 { + // TODO: Prompt for choice + println!("Choosing between multiple wallets currently not supported"); + exit(1); + } + wallet_manager.list_devices()[0].pubkey + }); + + let ledger = wallet_manager.get_ledger(&wallet_base_pubkey).unwrap_or_else(|_| { + println!("Wallet {:?} not found. Make sure the Solana Wallet app is still running.", wallet_base_pubkey); + exit(1); + }); + println!( + "Getting key from wallet: {:?}", + wallet_manager.get_wallet_info(&wallet_base_pubkey).unwrap() + ); + + let derivation = derivation_of(matches, "derivation") + .unwrap_or_else(|| DerivationPath::default()); + println!("Derivation path: {:?}", derivation); + ledger.get_pubkey(derivation).unwrap_or_else(|e| { + println!("Wallet did not return pubkey: {:?}", e); + exit(1); + }) + } else { + let keypair = get_keypair_from_matches(matches)?; + keypair.pubkey() + }; if matches.is_present("outfile") { let outfile = matches.value_of("outfile").unwrap(); check_for_overwrite(&outfile, &matches); - write_pubkey_file(outfile, keypair.pubkey())?; + write_pubkey_file(outfile, pubkey)?; } else { - println!("{}", keypair.pubkey()); + println!("{}", pubkey); } } ("new", Some(matches)) => {