From 64fcb792c2f1a561e8efd199fc75cded0e774bc1 Mon Sep 17 00:00:00 2001 From: Trent Nelson Date: Tue, 20 Apr 2021 16:35:28 -0600 Subject: [PATCH] remote-wallet: Add helpers for locating remote wallets --- Cargo.lock | 12 +- programs/bpf/Cargo.lock | 12 +- remote-wallet/Cargo.toml | 3 +- remote-wallet/src/remote_wallet.rs | 616 ++++++++++++++++++++++++++++- 4 files changed, 638 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0724a55fe2..6b7e9490bf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3002,6 +3002,15 @@ dependencies = [ "prost", ] +[[package]] +name = "qstring" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d464fae65fff2680baf48019211ce37aaec0c78e9264c84a3e484717f965104e" +dependencies = [ + "percent-encoding 2.1.0", +] + [[package]] name = "quick-error" version = "1.2.3" @@ -5002,10 +5011,11 @@ dependencies = [ "num-derive", "num-traits", "parking_lot 0.10.2", + "qstring", "semver 0.9.0", "solana-sdk", "thiserror", - "url 2.2.0", + "uriparse", ] [[package]] diff --git a/programs/bpf/Cargo.lock b/programs/bpf/Cargo.lock index 86bcdb84c5..6a9fc26c5b 100644 --- a/programs/bpf/Cargo.lock +++ b/programs/bpf/Cargo.lock @@ -2076,6 +2076,15 @@ dependencies = [ "unicode-xid 0.2.0", ] +[[package]] +name = "qstring" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d464fae65fff2680baf48019211ce37aaec0c78e9264c84a3e484717f965104e" +dependencies = [ + "percent-encoding", +] + [[package]] name = "quote" version = "0.6.13" @@ -3372,10 +3381,11 @@ dependencies = [ "num-derive 0.3.0", "num-traits", "parking_lot 0.10.2", + "qstring", "semver 0.9.0", "solana-sdk", "thiserror", - "url", + "uriparse", ] [[package]] diff --git a/remote-wallet/Cargo.toml b/remote-wallet/Cargo.toml index 6a6debf29d..81c0c34e51 100644 --- a/remote-wallet/Cargo.toml +++ b/remote-wallet/Cargo.toml @@ -18,10 +18,11 @@ log = "0.4.11" num-derive = { version = "0.3" } num-traits = { version = "0.2" } parking_lot = "0.10" +qstring = "0.7.2" semver = "0.9" solana-sdk = { path = "../sdk", version = "=1.7.0" } thiserror = "1.0" -url = "2.1.1" +uriparse = "0.6.3" [features] default = ["linux-static-hidraw"] diff --git a/remote-wallet/src/remote_wallet.rs b/remote-wallet/src/remote_wallet.rs index 75bcc02482..ba81c515a8 100644 --- a/remote-wallet/src/remote_wallet.rs +++ b/remote-wallet/src/remote_wallet.rs @@ -7,16 +7,17 @@ use { parking_lot::{Mutex, RwLock}, solana_sdk::{ derivation_path::{DerivationPath, DerivationPathError}, - pubkey::Pubkey, + pubkey::{ParsePubkeyError, Pubkey}, signature::{Signature, SignerError}, }, std::{ + convert::{Infallible, TryFrom, TryInto}, str::FromStr, sync::Arc, time::{Duration, Instant}, }, thiserror::Error, - url::Url, + uriparse::{URIReference, URIReferenceBuilder, URIReferenceError}, }; const HID_GLOBAL_USAGE_PAGE: u16 = 0xFF00; @@ -250,6 +251,197 @@ pub struct RemoteWalletInfo { pub error: Option, } +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum Manufacturer { + Unknown, + Ledger, +} + +impl Default for Manufacturer { + fn default() -> Self { + Self::Unknown + } +} + +const MFR_UNKNOWN: &str = "unknown"; +const MFR_LEDGER: &str = "ledger"; + +#[derive(Clone, Debug, Error, PartialEq)] +#[error("not a manufacturer")] +pub struct ManufacturerError; + +impl From for ManufacturerError { + fn from(_: Infallible) -> Self { + ManufacturerError + } +} + +impl FromStr for Manufacturer { + type Err = ManufacturerError; + fn from_str(s: &str) -> Result { + let s = s.to_ascii_lowercase(); + match s.as_str() { + MFR_LEDGER => Ok(Self::Ledger), + _ => Err(ManufacturerError), + } + } +} + +impl TryFrom<&str> for Manufacturer { + type Error = ManufacturerError; + fn try_from(s: &str) -> Result { + Manufacturer::from_str(s) + } +} + +impl AsRef for Manufacturer { + fn as_ref(&self) -> &str { + match self { + Self::Unknown => MFR_UNKNOWN, + Self::Ledger => MFR_LEDGER, + } + } +} + +impl std::fmt::Display for Manufacturer { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + let s: &str = self.as_ref(); + write!(f, "{}", s) + } +} + +#[derive(Clone, Debug, Error, PartialEq)] +pub enum LocatorError { + #[error(transparent)] + ManufacturerError(#[from] ManufacturerError), + #[error(transparent)] + PubkeyError(#[from] ParsePubkeyError), + #[error(transparent)] + DerivationPathError(#[from] DerivationPathError), + #[error(transparent)] + UriReferenceError(#[from] URIReferenceError), + #[error("unimplemented scheme")] + UnimplementedScheme, + #[error("infallible")] + Infallible, +} + +impl From for LocatorError { + fn from(_: Infallible) -> Self { + Self::Infallible + } +} + +#[derive(Debug, PartialEq)] +pub struct Locator { + manufacturer: Manufacturer, + pubkey: Option, + derivation_path: Option, +} + +impl std::fmt::Display for Locator { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + let maybe_path = self.pubkey.map(|p| p.to_string()); + let path = maybe_path.as_deref().unwrap_or("/"); + let maybe_query = self.derivation_path.as_ref().map(|d| d.get_query()); + let maybe_query2 = maybe_query.as_ref().map(|q| &q[1..]); + + let mut builder = URIReferenceBuilder::new(); + builder + .try_scheme(Some("usb")) + .unwrap() + .try_authority(Some(self.manufacturer.as_ref())) + .unwrap() + .try_path(path) + .unwrap() + .try_query(maybe_query2) + .unwrap(); + + let uri = builder.build().unwrap(); + write!(f, "{}", uri) + } +} + +impl Locator { + pub fn new_from_path>(path: P) -> Result { + let path = path.as_ref(); + let uri = URIReference::try_from(path)?; + Self::new_from_uri(&uri) + } + + pub fn new_from_uri(uri: &URIReference<'_>) -> Result { + let scheme = uri.scheme().map(|s| s.as_str().to_ascii_lowercase()); + let host = uri.host().map(|h| h.to_string()); + match (scheme, host) { + (Some(scheme), Some(host)) if scheme == "usb" => { + let path = uri.path().segments().get(0).and_then(|s| { + if !s.is_empty() { + Some(s.as_str()) + } else { + None + } + }); + let key = if let Some(query) = uri.query() { + let query_str = query.as_str(); + let query = qstring::QString::from(query_str); + if query.len() > 1 { + return Err(DerivationPathError::InvalidDerivationPath( + "invalid query string, extra fields not supported".to_string(), + ) + .into()); + } + let key = query.get("key"); + if key.is_none() { + return Err(DerivationPathError::InvalidDerivationPath(format!( + "invalid query string `{}`, only `key` supported", + query_str, + )) + .into()); + } + key.map(|v| v.to_string()) + } else { + None + }; + Self::new_from_parts(host.as_str(), path, key.as_deref()) + } + (Some(_scheme), Some(_host)) => Err(LocatorError::UnimplementedScheme), + (None, Some(_host)) => Err(LocatorError::UnimplementedScheme), + (_, None) => Err(LocatorError::ManufacturerError(ManufacturerError)), + } + } + + pub fn new_from_parts( + manufacturer: V, + pubkey: Option

, + derivation_path: Option, + ) -> Result + where + VE: Into, + V: TryInto, + PE: Into, + P: TryInto, + DE: Into, + D: TryInto, + { + let manufacturer = manufacturer.try_into().map_err(|e| e.into())?; + let pubkey = if let Some(pubkeyable) = pubkey { + Some(pubkeyable.try_into().map_err(|e| e.into())?) + } else { + None + }; + let derivation_path = if let Some(derivation_path) = derivation_path { + Some(derivation_path.try_into().map_err(|e| e.into())?) + } else { + None + }; + Ok(Self { + manufacturer, + pubkey, + derivation_path, + }) + } +} + impl RemoteWalletInfo { pub fn parse_path(path: String) -> Result<(Self, DerivationPath), RemoteWalletError> { let wallet_path = Url::parse(&path).map_err(|e| { @@ -496,4 +688,424 @@ mod tests { format!("usb://ledger/{}", pubkey_str) ); } + + #[test] + fn test_manufacturer() { + assert_eq!(MFR_LEDGER.try_into(), Ok(Manufacturer::Ledger)); + assert!(matches!(Manufacturer::from_str(MFR_LEDGER), Ok(v) if v == Manufacturer::Ledger)); + assert_eq!(Manufacturer::Ledger.as_ref(), MFR_LEDGER); + + assert!( + matches!(Manufacturer::from_str("bad-manufacturer"), Err(e) if e == ManufacturerError) + ); + } + + #[test] + fn test_locator_new_from_parts() { + let manufacturer = Manufacturer::Ledger; + let manufacturer_str = "ledger"; + let pubkey = Pubkey::new_unique(); + let pubkey_str = pubkey.to_string(); + let derivation_path = DerivationPath::new_bip44(Some(0), Some(0)); + let derivation_path_str = "0/0"; + + let expect = Locator { + manufacturer, + pubkey: None, + derivation_path: None, + }; + assert!(matches!( + Locator::new_from_parts(manufacturer, None::, None::), + Ok(e) if e == expect, + )); + assert!(matches!( + Locator::new_from_parts(manufacturer_str, None::, None::), + Ok(e) if e == expect, + )); + + let expect = Locator { + manufacturer, + pubkey: Some(pubkey), + derivation_path: None, + }; + assert!(matches!( + Locator::new_from_parts(manufacturer, Some(pubkey), None::), + Ok(e) if e == expect, + )); + assert!(matches!( + Locator::new_from_parts(manufacturer_str, Some(pubkey_str.as_str()), None::), + Ok(e) if e == expect, + )); + + let expect = Locator { + manufacturer, + pubkey: None, + derivation_path: Some(derivation_path.clone()), + }; + assert!(matches!( + Locator::new_from_parts(manufacturer, None::, Some(derivation_path)), + Ok(e) if e == expect, + )); + assert!(matches!( + Locator::new_from_parts(manufacturer, None::, Some(derivation_path_str)), + Ok(e) if e == expect, + )); + + assert!(matches!( + Locator::new_from_parts("bad-manufacturer", None::, None::), + Err(LocatorError::ManufacturerError(e)) if e == ManufacturerError, + )); + assert!(matches!( + Locator::new_from_parts(manufacturer, Some("bad-pubkey"), None::), + Err(LocatorError::PubkeyError(e)) if e == ParsePubkeyError::Invalid, + )); + let bad_path = "bad-derivation-path".to_string(); + assert!(matches!( + Locator::new_from_parts(manufacturer, None::, Some(bad_path.as_str())), + Err(LocatorError::DerivationPathError( + DerivationPathError::InvalidDerivationPath(_) + )), + )); + } + + #[test] + fn test_locator_new_from_uri() { + let derivation_path = DerivationPath::new_bip44(Some(0), Some(0)); + let manufacturer = Manufacturer::Ledger; + let pubkey = Pubkey::new_unique(); + let pubkey_str = pubkey.to_string(); + + // usb://ledger/{PUBKEY}?key=0'/0' + let mut builder = URIReferenceBuilder::new(); + builder + .try_scheme(Some("usb")) + .unwrap() + .try_authority(Some(Manufacturer::Ledger.as_ref())) + .unwrap() + .try_path(pubkey_str.as_str()) + .unwrap() + .try_query(Some("key=0/0")) + .unwrap(); + let uri = builder.build().unwrap(); + let expect = Locator { + manufacturer, + pubkey: Some(pubkey), + derivation_path: Some(derivation_path.clone()), + }; + assert_eq!(Locator::new_from_uri(&uri), Ok(expect)); + + // usb://ledger/{PUBKEY} + let mut builder = URIReferenceBuilder::new(); + builder + .try_scheme(Some("usb")) + .unwrap() + .try_authority(Some(Manufacturer::Ledger.as_ref())) + .unwrap() + .try_path(pubkey_str.as_str()) + .unwrap(); + let uri = builder.build().unwrap(); + let expect = Locator { + manufacturer, + pubkey: Some(pubkey), + derivation_path: None, + }; + assert_eq!(Locator::new_from_uri(&uri), Ok(expect)); + + // usb://ledger + let mut builder = URIReferenceBuilder::new(); + builder + .try_scheme(Some("usb")) + .unwrap() + .try_authority(Some(Manufacturer::Ledger.as_ref())) + .unwrap() + .try_path("") + .unwrap(); + let uri = builder.build().unwrap(); + let expect = Locator { + manufacturer, + pubkey: None, + derivation_path: None, + }; + assert_eq!(Locator::new_from_uri(&uri), Ok(expect)); + + // usb://ledger/ + let mut builder = URIReferenceBuilder::new(); + builder + .try_scheme(Some("usb")) + .unwrap() + .try_authority(Some(Manufacturer::Ledger.as_ref())) + .unwrap() + .try_path("/") + .unwrap(); + let uri = builder.build().unwrap(); + let expect = Locator { + manufacturer, + pubkey: None, + derivation_path: None, + }; + assert_eq!(Locator::new_from_uri(&uri), Ok(expect)); + + // usb://ledger?key=0'/0' + let mut builder = URIReferenceBuilder::new(); + builder + .try_scheme(Some("usb")) + .unwrap() + .try_authority(Some(Manufacturer::Ledger.as_ref())) + .unwrap() + .try_path("") + .unwrap() + .try_query(Some("key=0/0")) + .unwrap(); + let uri = builder.build().unwrap(); + let expect = Locator { + manufacturer, + pubkey: None, + derivation_path: Some(derivation_path.clone()), + }; + assert_eq!(Locator::new_from_uri(&uri), Ok(expect)); + + // usb://ledger/?key=0'/0' + let mut builder = URIReferenceBuilder::new(); + builder + .try_scheme(Some("usb")) + .unwrap() + .try_authority(Some(Manufacturer::Ledger.as_ref())) + .unwrap() + .try_path("/") + .unwrap() + .try_query(Some("key=0/0")) + .unwrap(); + let uri = builder.build().unwrap(); + let expect = Locator { + manufacturer, + pubkey: None, + derivation_path: Some(derivation_path), + }; + assert_eq!(Locator::new_from_uri(&uri), Ok(expect)); + + // bad-scheme://ledger + let mut builder = URIReferenceBuilder::new(); + builder + .try_scheme(Some("bad-scheme")) + .unwrap() + .try_authority(Some(Manufacturer::Ledger.as_ref())) + .unwrap() + .try_path("") + .unwrap(); + let uri = builder.build().unwrap(); + assert_eq!( + Locator::new_from_uri(&uri), + Err(LocatorError::UnimplementedScheme) + ); + + // usb://bad-manufacturer + let mut builder = URIReferenceBuilder::new(); + builder + .try_scheme(Some("usb")) + .unwrap() + .try_authority(Some("bad-manufacturer")) + .unwrap() + .try_path("") + .unwrap(); + let uri = builder.build().unwrap(); + assert_eq!( + Locator::new_from_uri(&uri), + Err(LocatorError::ManufacturerError(ManufacturerError)) + ); + + // usb://ledger/bad-pubkey + let mut builder = URIReferenceBuilder::new(); + builder + .try_scheme(Some("usb")) + .unwrap() + .try_authority(Some(Manufacturer::Ledger.as_ref())) + .unwrap() + .try_path("bad-pubkey") + .unwrap(); + let uri = builder.build().unwrap(); + assert_eq!( + Locator::new_from_uri(&uri), + Err(LocatorError::PubkeyError(ParsePubkeyError::Invalid)) + ); + + // usb://ledger?bad-key=0/0 + let mut builder = URIReferenceBuilder::new(); + builder + .try_scheme(Some("usb")) + .unwrap() + .try_authority(Some(Manufacturer::Ledger.as_ref())) + .unwrap() + .try_path("") + .unwrap() + .try_query(Some("bad-key=0/0")) + .unwrap(); + let uri = builder.build().unwrap(); + assert!(matches!( + Locator::new_from_uri(&uri), + Err(LocatorError::DerivationPathError(_)) + )); + + // usb://ledger?key=bad-value + let mut builder = URIReferenceBuilder::new(); + builder + .try_scheme(Some("usb")) + .unwrap() + .try_authority(Some(Manufacturer::Ledger.as_ref())) + .unwrap() + .try_path("") + .unwrap() + .try_query(Some("key=bad-value")) + .unwrap(); + let uri = builder.build().unwrap(); + assert!(matches!( + Locator::new_from_uri(&uri), + Err(LocatorError::DerivationPathError(_)) + )); + + // usb://ledger?key= + let mut builder = URIReferenceBuilder::new(); + builder + .try_scheme(Some("usb")) + .unwrap() + .try_authority(Some(Manufacturer::Ledger.as_ref())) + .unwrap() + .try_path("") + .unwrap() + .try_query(Some("key=")) + .unwrap(); + let uri = builder.build().unwrap(); + assert!(matches!( + Locator::new_from_uri(&uri), + Err(LocatorError::DerivationPathError(_)) + )); + + // usb://ledger?key + let mut builder = URIReferenceBuilder::new(); + builder + .try_scheme(Some("usb")) + .unwrap() + .try_authority(Some(Manufacturer::Ledger.as_ref())) + .unwrap() + .try_path("") + .unwrap() + .try_query(Some("key")) + .unwrap(); + let uri = builder.build().unwrap(); + assert!(matches!( + Locator::new_from_uri(&uri), + Err(LocatorError::DerivationPathError(_)) + )); + } + + #[test] + fn test_locator_new_from_path() { + let derivation_path = DerivationPath::new_bip44(Some(0), Some(0)); + let manufacturer = Manufacturer::Ledger; + let pubkey = Pubkey::new_unique(); + let path = format!("usb://ledger/{}?key=0/0", pubkey); + Locator::new_from_path(path).unwrap(); + + // usb://ledger/{PUBKEY}?key=0'/0' + let path = format!("usb://ledger/{}?key=0'/0'", pubkey); + let expect = Locator { + manufacturer, + pubkey: Some(pubkey), + derivation_path: Some(derivation_path.clone()), + }; + assert_eq!(Locator::new_from_path(path), Ok(expect)); + + // usb://ledger/{PUBKEY} + let path = format!("usb://ledger/{}", pubkey); + let expect = Locator { + manufacturer, + pubkey: Some(pubkey), + derivation_path: None, + }; + assert_eq!(Locator::new_from_path(path), Ok(expect)); + + // usb://ledger + let path = "usb://ledger"; + let expect = Locator { + manufacturer, + pubkey: None, + derivation_path: None, + }; + assert_eq!(Locator::new_from_path(path), Ok(expect)); + + // usb://ledger/ + let path = "usb://ledger/"; + let expect = Locator { + manufacturer, + pubkey: None, + derivation_path: None, + }; + assert_eq!(Locator::new_from_path(path), Ok(expect)); + + // usb://ledger?key=0'/0' + let path = "usb://ledger?key=0'/0'"; + let expect = Locator { + manufacturer, + pubkey: None, + derivation_path: Some(derivation_path.clone()), + }; + assert_eq!(Locator::new_from_path(path), Ok(expect)); + + // usb://ledger/?key=0'/0' + let path = "usb://ledger?key=0'/0'"; + let expect = Locator { + manufacturer, + pubkey: None, + derivation_path: Some(derivation_path), + }; + assert_eq!(Locator::new_from_path(path), Ok(expect)); + + // bad-scheme://ledger + let path = "bad-scheme://ledger"; + assert_eq!( + Locator::new_from_path(path), + Err(LocatorError::UnimplementedScheme) + ); + + // usb://bad-manufacturer + let path = "usb://bad-manufacturer"; + assert_eq!( + Locator::new_from_path(path), + Err(LocatorError::ManufacturerError(ManufacturerError)) + ); + + // usb://ledger/bad-pubkey + let path = "usb://ledger/bad-pubkey"; + assert_eq!( + Locator::new_from_path(path), + Err(LocatorError::PubkeyError(ParsePubkeyError::Invalid)) + ); + + // usb://ledger?bad-key=0/0 + let path = "usb://ledger?bad-key=0'/0'"; + assert!(matches!( + Locator::new_from_path(path), + Err(LocatorError::DerivationPathError(_)) + )); + + // usb://ledger?key=bad-value + let path = format!("usb://ledger/{}?key=bad-value", pubkey); + assert!(matches!( + Locator::new_from_path(path), + Err(LocatorError::DerivationPathError(_)) + )); + + // usb://ledger?key= + let path = format!("usb://ledger/{}?key=", pubkey); + assert!(matches!( + Locator::new_from_path(path), + Err(LocatorError::DerivationPathError(_)) + )); + + // usb://ledger?key + let path = format!("usb://ledger/{}?key", pubkey); + assert!(matches!( + Locator::new_from_path(path), + Err(LocatorError::DerivationPathError(_)) + )); + } }