From 729982440cb62dac53261b24c16b6541221ad6f6 Mon Sep 17 00:00:00 2001 From: edouardparis Date: Wed, 6 Mar 2024 22:32:11 +0100 Subject: [PATCH] Add jade --- Cargo.toml | 16 +- cli/Cargo.toml | 2 +- cli/src/bin/hwi.rs | 2 +- cli/src/lib.rs | 18 ++ src/bitbox.rs | 2 +- src/jade/api.rs | 142 +++++++++++++++ src/jade/mod.rs | 405 ++++++++++++++++++++++++++++++++++++++++++ src/jade/pinserver.rs | 52 ++++++ src/ledger.rs | 58 +----- src/lib.rs | 5 + src/utils.rs | 44 ++++- 11 files changed, 685 insertions(+), 61 deletions(-) create mode 100644 src/jade/api.rs create mode 100644 src/jade/mod.rs create mode 100644 src/jade/pinserver.rs diff --git a/Cargo.toml b/Cargo.toml index 51d4d1a..b170123 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,10 +19,11 @@ authors.workspace = true repository.workspace = true [features] -default = ["ledger", "specter", "coldcard", "bitbox"] +default = ["ledger", "specter", "coldcard", "bitbox", "jade"] bitbox = ["tokio", "hidapi", "bitbox-api", "regex"] coldcard = ["dep:coldcard", "regex"] specter = ["tokio", "tokio-serial", "serialport"] +jade = ["tokio", "tokio-serial", "serde", "serde_cbor", "serialport", "reqwest"] ledger = ["regex", "tokio", "ledger_bitcoin_client", "ledger-transport-hidapi", "ledger-apdu", "hidapi"] regex = ["dep:regex"] @@ -31,14 +32,19 @@ async-trait = "0.1.52" futures = "0.3" bitcoin = { version = "0.31", default-features = false, features = ["base64", "serde", "std"] } -# specter +# specter & jade tokio-serial = { version = "5.4.1", optional = true } serialport = { version = "4.2", optional = true } -#bitbox +# jade +serde = { version = "1.0", features = ["derive"], optional = true } +serde_cbor = { version = "0.11", optional = true } +reqwest = { version = "0.11", features = ["json"] , optional = true} + +# bitbox bitbox-api = { version = "0.2.3", default-features = false, features = ["usb", "tokio", "multithreaded"], optional = true } -#coldcard +# coldcard coldcard = { version = "0.12.1", optional = true } # ledger @@ -46,7 +52,7 @@ ledger_bitcoin_client = { version = "0.4.0", optional = true } ledger-apdu = { version = "0.10", optional = true } ledger-transport-hidapi = { version = "0.10.0", optional = true } -#bitbox & ledger +# bitbox & ledger hidapi = { version = "2.4.1", features = ["linux-static-hidraw"], default-features = false, optional = true } regex = { version = "1.6.0", optional = true } diff --git a/cli/Cargo.toml b/cli/Cargo.toml index abed163..7653fa3 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -16,5 +16,5 @@ path = "src/bin/hwi.rs" clap = { version = "4.4.7", features = ["derive"] } bitcoin = "0.31" hex = "0.4" -async-hwi = "0.0.14" +async-hwi = { path = ".." } tokio = { version = "1", features = ["macros", "net", "rt", "rt-multi-thread", "io-util", "sync"] } diff --git a/cli/src/bin/hwi.rs b/cli/src/bin/hwi.rs index e29da21..30f4804 100644 --- a/cli/src/bin/hwi.rs +++ b/cli/src/bin/hwi.rs @@ -148,7 +148,7 @@ async fn main() -> Result<(), Box> { } Commands::Device(DeviceCommands::List) => { for device in command::list(args.network, None).await? { - eprint!("{}", device.get_master_fingerprint().await?,); + eprint!("{}", device.get_master_fingerprint().await?); eprint!(" {}", device.device_kind()); if let Ok(version) = device.get_version().await.map(|v| v.to_string()) { eprint!(" {}", version); diff --git a/cli/src/lib.rs b/cli/src/lib.rs index cecc703..971e20d 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -2,6 +2,7 @@ pub mod command { use async_hwi::{ bitbox::{api::runtime, BitBox02, PairingBitbox02WithLocalCache}, coldcard, + jade::{self, Jade}, ledger::{HidApi, Ledger, LedgerSimulator, TransportHID}, specter::{Specter, SpecterSimulator}, HWI, @@ -31,6 +32,23 @@ pub mod command { } } + match Jade::enumerate().await { + Err(e) => println!("{:?}", e), + Ok(devices) => { + for device in devices { + if let Ok(info) = device.get_info().await { + if info.jade_state == jade::api::JadeState::Locked { + if let Err(e) = device.auth().await { + eprintln!("{:?}", e); + continue; + } + } + hws.push(device.into()); + } + } + } + } + if let Ok(device) = LedgerSimulator::try_connect().await { hws.push(device.into()); } diff --git a/src/bitbox.rs b/src/bitbox.rs index 525bd90..93b1e04 100644 --- a/src/bitbox.rs +++ b/src/bitbox.rs @@ -504,7 +504,7 @@ mod tests { use super::*; #[test] - fn test_extract_keys_and_template() { + fn test_extract_script_config_policy() { let policy = extract_script_config_policy("wsh(or_d(pk([f5acc2fd/49'/1'/0']tpubDCbK3Ysvk8HjcF6mPyrgMu3KgLiaaP19RjKpNezd8GrbAbNg6v5BtWLaCt8FNm6QkLseopKLf5MNYQFtochDTKHdfgG6iqJ8cqnLNAwtXuP/**),and_v(v:pkh(tpubDDtb2WPYwEWw2WWDV7reLV348iJHw2HmhzvPysKKrJw3hYmvrd4jasyoioVPdKGQqjyaBMEvTn1HvHWDSVqQ6amyyxRZ5YjpPBBGjJ8yu8S/**),older(100))))").unwrap(); assert_eq!(2, policy.pubkeys.len()); assert_eq!( diff --git a/src/jade/api.rs b/src/jade/api.rs new file mode 100644 index 0000000..18bacbf --- /dev/null +++ b/src/jade/api.rs @@ -0,0 +1,142 @@ +/// See https://github.com/Blockstream/Jade/blob/master/docs/index.rst +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize)] +pub struct Request<'a, T: Serialize> { + pub id: String, + pub method: &'a str, + pub params: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct EmptyRequest; + +#[derive(Debug, Serialize, Deserialize)] +pub struct Response { + pub id: String, + pub result: Option, + pub error: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Error { + pub code: i32, + pub message: Option, + pub data: Option>, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct GetXpubParams<'a> { + pub network: &'a str, + pub path: Vec, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct AuthUserParams<'a> { + pub network: &'a str, + pub epoch: u64, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(untagged)] +pub enum AuthUserResponse { + Authenticated(bool), + PinServerRequired { + http_request: PinServerRequest, + }, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct PinServerRequest { + pub params: PinServerRequestParams, + #[serde(alias = "on-reply")] + pub onreply: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct PinServerRequestParams { + pub urls: PinServerUrls, + pub method: String, + pub accept: String, + pub data: T, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(untagged)] +pub enum PinServerUrls { + Array(Vec), + Object { url: String, onion: String }, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct PinServerEncryptedData { + pub cke: String, + pub encrypted_data: String, + pub hmac_encrypted_data: String, + pub ske: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct HandShakeInitParams { + pub sig: String, + pub ske: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct HandShakeInitResponse { + pub http_request: PinServerRequest, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct HandShakeCompleteParams { + pub encrypted_key: String, + pub hmac: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct GetInfoResponse { + #[serde(alias = "JADE_VERSION")] + pub jade_version: String, + #[serde(alias = "JADE_STATE")] + pub jade_state: JadeState, + #[serde(alias = "JADE_NETWORKS")] + pub jade_networks: JadeNetworks, +} + +#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] +pub enum JadeState { + #[serde(alias = "UNINIT")] + Uninit, + #[serde(alias = "UNSAVED")] + Unsaved, + #[serde(alias = "LOCKED")] + Locked, + #[serde(alias = "READY")] + Ready, + #[serde(alias = "TEMP")] + Temp, +} + +#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] +pub enum JadeNetworks { + #[serde(alias = "MAIN")] + Main, + #[serde(alias = "TEST")] + Test, + #[serde(alias = "ALL")] + All, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct RegisterDescriptorParams<'a> { + pub network: &'a str, + pub descriptor_name: String, + pub descriptor: String, + pub datavalues: Vec, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct DataValue { + pub key: String, + pub value: String, +} diff --git a/src/jade/mod.rs b/src/jade/mod.rs new file mode 100644 index 0000000..bab38ca --- /dev/null +++ b/src/jade/mod.rs @@ -0,0 +1,405 @@ +pub mod api; +pub mod pinserver; + +use std::{ + fmt::Debug, + str::FromStr, + time::{SystemTime, UNIX_EPOCH}, +}; + +use serde::{de::DeserializeOwned, Serialize}; + +use bitcoin::{ + bip32::{DerivationPath, Fingerprint, Xpub}, + psbt::Psbt, +}; + +use serialport::{available_ports, SerialPort, SerialPortType}; +use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; +use tokio_serial::SerialPortBuilderExt; + +pub use tokio_serial::SerialStream; + +use crate::{parse_version, utils}; + +use super::{AddressScript, DeviceKind, Error as HWIError, HWI}; +use async_trait::async_trait; + +#[derive(Debug)] +pub struct Jade { + transport: T, + network: String, + kind: DeviceKind, +} + +impl Jade { + pub fn new(transport: T) -> Self { + Self { + transport, + network: "mainnet".to_string(), + kind: DeviceKind::Jade, + } + } + + pub async fn ping(&self) -> Result<(), JadeError> { + let _res: u64 = self + .transport + .request("ping", Option::::None) + .await?; + Ok(()) + } + + pub async fn get_info(&self) -> Result { + let info: api::GetInfoResponse = self + .transport + .request("get_version_info", Option::::None) + .await?; + Ok(info) + } + + pub async fn auth(&self) -> Result<(), JadeError> { + let res: api::AuthUserResponse = self + .transport + .request( + "auth_user", + Some(api::AuthUserParams { + network: &self.network, + epoch: SystemTime::now() + .duration_since(UNIX_EPOCH) + .ok() + .map(|t| t.as_secs()) + .unwrap_or(0), + }), + ) + .await?; + + if let api::AuthUserResponse::PinServerRequired { http_request } = res { + let client = pinserver::PinServerClient::new(); + let handshake_init_params: api::HandShakeInitParams = + client.request(http_request.params).await?; + let handshake: api::HandShakeInitResponse = self + .transport + .request("handshake_init", Some(handshake_init_params)) + .await?; + let handshake_completed_params: api::HandShakeCompleteParams = + client.request(handshake.http_request.params).await?; + let handshake_completed: bool = self + .transport + .request("handshake_complete", Some(handshake_completed_params)) + .await?; + if !handshake_completed { + return Err(JadeError::HandShakeRefused); + } + } + Ok(()) + } +} + +#[async_trait] +impl HWI for Jade { + fn device_kind(&self) -> DeviceKind { + self.kind + } + + async fn get_version(&self) -> Result { + let info = self.get_info().await?; + parse_version(&info.jade_version) + } + + async fn get_master_fingerprint(&self) -> Result { + let xpub = self.get_extended_pubkey(&DerivationPath::master()).await?; + Ok(xpub.fingerprint()) + } + + async fn get_extended_pubkey(&self, path: &DerivationPath) -> Result { + let s: String = self + .transport + .request( + "get_xpub", + Some(api::GetXpubParams { + network: &self.network, + path: path.to_u32_vec(), + }), + ) + .await?; + let xpub = Xpub::from_str(&s).map_err(|e| HWIError::Device(e.to_string()))?; + Ok(xpub) + } + + async fn display_address(&self, _script: &AddressScript) -> Result<(), HWIError> { + Err(HWIError::UnimplementedMethod) + } + + async fn register_wallet( + &self, + name: &str, + policy: &str, + ) -> Result, HWIError> { + let (descriptor_template, keys) = utils::extract_keys_and_template::(policy)?; + let registered: bool = self + .transport + .request( + "register_descriptor", + Some(api::RegisterDescriptorParams { + network: &self.network, + descriptor_name: name.to_string(), + descriptor: descriptor_template, + datavalues: keys + .into_iter() + .enumerate() + .map(|(i, value)| api::DataValue { + key: format!("@{}", i), + value, + }) + .collect(), + }), + ) + .await?; + if !registered { + Err(HWIError::UserRefused) + } else { + Ok(None) + } + } + + async fn is_wallet_registered(&self, _name: &str, _policy: &str) -> Result { + Err(HWIError::UnimplementedMethod) + } + + async fn sign_tx(&self, _psbt: &mut Psbt) -> Result<(), HWIError> { + Err(HWIError::UnimplementedMethod) + } +} + +impl From> for Box { + fn from(s: Jade) -> Box { + Box::new(s) + } +} + +async fn exchange( + transport: &mut T, + method: &str, + params: Option, +) -> Result +where + T: Unpin + AsyncRead + AsyncWrite, + S: Serialize + Unpin, + D: DeserializeOwned + Unpin, +{ + let (reader, mut writer) = tokio::io::split(transport); + + let id = std::process::id(); + let req = serde_cbor::to_vec(&api::Request { + id: id.to_string(), + method, + params, + }) + .map_err(TransportError::from)?; + + writer.write_all(&req).await.map_err(TransportError::from)?; + + let response = read_stream(reader).await?; + + if response.id != id.to_string() { + return Err(TransportError::NonceMismatch.into()); + } + + if let Some(e) = response.error { + return Err(JadeError::Rpc(e)); + } + + response + .result + .ok_or_else(|| TransportError::NoErrorOrResult.into()) +} + +async fn read_stream( + mut stream: S, +) -> Result, TransportError> { + let mut buf = Vec::::new(); + let mut chunk = [0; 512]; + loop { + match stream.read(&mut chunk).await? { + 0 => break, // End of stream + n => { + buf.extend_from_slice(&chunk[..n]); + if let Ok(response) = serde_cbor::from_slice(&buf) { + return Ok(response); + } + } + } + } + Err(TransportError::NoErrorOrResult) +} + +#[async_trait] +pub trait Transport: Debug { + async fn request( + &self, + method: &str, + params: Option, + ) -> Result; +} + +impl Jade { + pub async fn enumerate() -> Result, JadeError> { + let mut res = Vec::new(); + for port_name in SerialTransport::enumerate_potential_ports()? { + let jade = Jade::::new(SerialTransport { port_name }); + match jade.ping().await { + Err(e) => println!("{:?}", e), + Ok(_) => res.push(jade), + } + } + Ok(res) + } +} + +#[derive(Debug)] +pub struct SerialTransport { + port_name: String, +} + +pub const JADE_DEVICE_IDS: [(u16, u16); 4] = [ + (0x10c4, 0xea60), + (0x1a86, 0x55d4), + (0x0403, 0x6001), + (0x1a86, 0x7523), +]; + +impl SerialTransport { + pub fn enumerate_potential_ports() -> Result, JadeError> { + match available_ports() { + Ok(ports) => Ok(ports + .into_iter() + .filter_map(|p| match p.port_type { + SerialPortType::PciPort => Some(p.port_name), + SerialPortType::UsbPort(info) => { + if JADE_DEVICE_IDS.contains(&(info.vid, info.pid)) { + Some(p.port_name) + } else { + None + } + } + _ => None, + }) + .collect()), + Err(e) => Err(JadeError::Transport(e.into())), + } + } +} + +#[async_trait] +impl Transport for SerialTransport { + async fn request( + &self, + method: &str, + params: Option, + ) -> Result { + let mut transport = tokio_serial::new(self.port_name.clone(), 115200) + .open_native_async() + .map_err(|e| JadeError::Transport(e.into()))?; + // Ensure RTS and DTR are not set (as this can cause the hw to reboot) + // according to https://github.com/Blockstream/Jade/blob/master/jadepy/jade_serial.py#L56 + transport + .write_request_to_send(false) + .map_err(TransportError::from)?; + transport + .write_data_terminal_ready(false) + .map_err(TransportError::from)?; + exchange(&mut transport, method, params).await + } +} + +#[derive(Debug)] +pub enum TransportError { + Serialize(serde_cbor::Error), + NoErrorOrResult, + NonceMismatch, + Io(std::io::Error), + Serial(serialport::Error), +} + +impl From for TransportError { + fn from(e: serde_cbor::Error) -> Self { + Self::Serialize(e) + } +} + +impl From for TransportError { + fn from(e: std::io::Error) -> Self { + Self::Io(e) + } +} + +impl From for TransportError { + fn from(e: serialport::Error) -> Self { + Self::Serial(e) + } +} + +impl std::fmt::Display for TransportError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + Self::Serialize(e) => write!(f, "{}", e), + Self::NoErrorOrResult => write!(f, "No Error or Result"), + Self::NonceMismatch => write!(f, "Nonce mismatched"), + Self::Io(e) => write!(f, "{}", e), + Self::Serial(e) => write!(f, "{}", e), + } + } +} + +#[derive(Debug)] +pub enum JadeError { + DeviceNotFound, + DeviceDidNotSign, + UserCancelled, + Transport(TransportError), + Rpc(api::Error), + PinServer(pinserver::Error), + HandShakeRefused, +} + +impl From for JadeError { + fn from(e: TransportError) -> Self { + Self::Transport(e) + } +} + +impl From for JadeError { + fn from(e: pinserver::Error) -> Self { + Self::PinServer(e) + } +} + +impl std::fmt::Display for JadeError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + Self::DeviceNotFound => write!(f, "Jade not found"), + Self::DeviceDidNotSign => write!(f, "Jade did not sign the psbt"), + Self::Transport(e) => write!(f, "{}", e), + Self::Rpc(e) => write!(f, "{:?}", e), + Self::UserCancelled => write!(f, "User cancelled operation"), + Self::PinServer(e) => write!(f, "{:?}", e), + Self::HandShakeRefused => write!(f, "Handshake with pinserver refused"), + } + } +} + +impl From for HWIError { + fn from(e: JadeError) -> HWIError { + match e { + JadeError::DeviceNotFound => HWIError::DeviceNotFound, + JadeError::DeviceDidNotSign => HWIError::DeviceDidNotSign, + JadeError::Transport(e) => HWIError::Device(e.to_string()), + JadeError::Rpc(e) => HWIError::Device(format!("{:?}", e)), + JadeError::PinServer(e) => HWIError::Device(format!("{:?}", e)), + JadeError::UserCancelled => HWIError::UserRefused, + JadeError::HandShakeRefused => { + HWIError::Device("Handshake with pinserver refused".to_string()) + } + } + } +} diff --git a/src/jade/pinserver.rs b/src/jade/pinserver.rs new file mode 100644 index 0000000..cd5bef8 --- /dev/null +++ b/src/jade/pinserver.rs @@ -0,0 +1,52 @@ +use super::api; +use serde::Serialize; + +pub struct PinServerClient { + pub client: reqwest::Client, +} + +impl Default for PinServerClient { + fn default() -> Self { + Self::new() + } +} + +impl PinServerClient { + pub fn new() -> Self { + Self { + client: reqwest::Client::new(), + } + } + + pub async fn request(&self, req: api::PinServerRequestParams) -> Result + where + T: Serialize, + D: serde::de::DeserializeOwned, + { + let url = match &req.urls { + api::PinServerUrls::Array(urls) => urls.first().ok_or(Error::NoUrlProvided)?, + api::PinServerUrls::Object { url, .. } => url, + }; + + let res = self.client.post(url).json(&req.data).send().await?; + + if res.status().is_success() { + res.json().await.map_err(Error::from) + } else { + Err(Error::Server(format!("{:?}", res))) + } + } +} + +#[derive(Debug)] +pub enum Error { + NoUrlProvided, + Client(reqwest::Error), + Server(String), +} + +impl From for Error { + fn from(e: reqwest::Error) -> Self { + Self::Client(e) + } +} diff --git a/src/ledger.rs b/src/ledger.rs index 92d1b73..00a6f2b 100644 --- a/src/ledger.rs +++ b/src/ledger.rs @@ -2,7 +2,6 @@ use std::convert::TryFrom; use std::default::Default; use std::error::Error; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; -use std::str::FromStr; use async_trait::async_trait; use bitcoin::{ @@ -10,7 +9,6 @@ use bitcoin::{ psbt::Psbt, }; use ledger_bitcoin_client::psbt::PartialSignature; -use regex::Regex; use ledger_apdu::APDUAnswer; use ledger_transport_hidapi::TransportNativeHID; @@ -57,7 +55,7 @@ impl Ledger { policy: &str, hmac: Option<[u8; 32]>, ) -> Result { - let (descriptor_template, keys) = extract_keys_and_template(policy)?; + let (descriptor_template, keys) = utils::extract_keys_and_template::(policy)?; let wallet = WalletPolicy::new(name.into(), WalletVersion::V2, descriptor_template, keys); self.options.wallet = Some((wallet, hmac)); Ok(self) @@ -113,7 +111,8 @@ impl HWI for Ledger { path.to_string().trim_start_matches('m'), xpub ); - let (descriptor_template, keys) = extract_keys_and_template(&policy)?; + let (descriptor_template, keys) = + utils::extract_keys_and_template::(&policy)?; let wallet = WalletPolicy::new("".into(), WalletVersion::V2, descriptor_template, keys); @@ -146,7 +145,7 @@ impl HWI for Ledger { name: &str, policy: &str, ) -> Result, HWIError> { - let (descriptor_template, keys) = extract_keys_and_template(policy)?; + let (descriptor_template, keys) = utils::extract_keys_and_template::(policy)?; let wallet = WalletPolicy::new( name.to_string(), WalletVersion::V2, @@ -159,7 +158,8 @@ impl HWI for Ledger { async fn is_wallet_registered(&self, name: &str, policy: &str) -> Result { if let Some((wallet, hmac)) = &self.options.wallet { - let (descriptor_template, keys) = extract_keys_and_template(policy)?; + let (descriptor_template, keys) = + utils::extract_keys_and_template::(policy)?; Ok(hmac.is_some() && name == wallet.name && descriptor_template == wallet.descriptor_template @@ -194,31 +194,6 @@ impl HWI for Ledger { } } -pub fn extract_keys_and_template(policy: &str) -> Result<(String, Vec), HWIError> { - let re = Regex::new(r"((\[.+?\])?[xyYzZtuUvV]pub[1-9A-HJ-NP-Za-km-z]{79,108})").unwrap(); - let mut descriptor_template = policy.to_string(); - let mut pubkeys_str: Vec<&str> = Vec::new(); - for capture in re.find_iter(policy) { - if !pubkeys_str.contains(&capture.as_str()) { - pubkeys_str.push(capture.as_str()); - } - } - - let mut pubkeys: Vec = Vec::new(); - for (i, key_str) in pubkeys_str.iter().enumerate() { - descriptor_template = descriptor_template.replace(key_str, &format!("@{}", i)); - let pubkey = WalletPubKey::from_str(key_str).map_err(|_| HWIError::UnsupportedInput)?; - pubkeys.push(pubkey); - } - - // Do not include the hash in the descriptor template. - if let Some((descriptor_template, _hash)) = descriptor_template.rsplit_once('#') { - Ok((descriptor_template.to_string(), pubkeys)) - } else { - Ok((descriptor_template, pubkeys)) - } -} - impl Ledger { pub fn enumerate(api: &HidApi) -> impl Iterator { TransportNativeHID::list_ledgers(api) @@ -338,24 +313,3 @@ impl From> for HWIError { HWIError::Device(format!("{:#?}", e)) } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_extract_keys_and_template() { - let res = extract_keys_and_template("wsh(or_d(pk([f5acc2fd/49'/1'/0']tpubDCbK3Ysvk8HjcF6mPyrgMu3KgLiaaP19RjKpNezd8GrbAbNg6v5BtWLaCt8FNm6QkLseopKLf5MNYQFtochDTKHdfgG6iqJ8cqnLNAwtXuP/**),and_v(v:pkh(tpubDDtb2WPYwEWw2WWDV7reLV348iJHw2HmhzvPysKKrJw3hYmvrd4jasyoioVPdKGQqjyaBMEvTn1HvHWDSVqQ6amyyxRZ5YjpPBBGjJ8yu8S/**),older(100))))").unwrap(); - assert_eq!(res.0, "wsh(or_d(pk(@0/**),and_v(v:pkh(@1/**),older(100))))"); - assert_eq!(res.1.len(), 2); - assert_eq!(res.1[0].to_string(), "[f5acc2fd/49'/1'/0']tpubDCbK3Ysvk8HjcF6mPyrgMu3KgLiaaP19RjKpNezd8GrbAbNg6v5BtWLaCt8FNm6QkLseopKLf5MNYQFtochDTKHdfgG6iqJ8cqnLNAwtXuP".to_string()); - assert_eq!(res.1[1].to_string(), "tpubDDtb2WPYwEWw2WWDV7reLV348iJHw2HmhzvPysKKrJw3hYmvrd4jasyoioVPdKGQqjyaBMEvTn1HvHWDSVqQ6amyyxRZ5YjpPBBGjJ8yu8S".to_string()); - - let res = extract_keys_and_template("wsh(or_d(multi(2,[b0822927/48'/1'/0'/2']tpubDEvZxV86Br8Knbm9tWcr5Hvmg5cYTYsg92vinqH6Bie6U8ix8CsoN9W11NQygdqVwmHUJpsHXxNsi5gXn36g4xNfLWkMqPuFhRZAmMQ7jjQ/<0;1>/*,[7fc39c07/48'/1'/0'/2']tpubDEvjgXtrUuH3Qtkapny9aE8gN847xiXsf9MDM5XueGf9nrvStqAuBSva3ajGyTvtp8Ti55FvVXsgYSXuS1tQkBeopFuodx2hRUDmQbvKxbZ/<0;1>/*),and_v(v:thresh(2,pkh([b0822927/48'/1'/0'/2']tpubDEvZxV86Br8Knbm9tWcr5Hvmg5cYTYsg92vinqH6Bie6U8ix8CsoN9W11NQygdqVwmHUJpsHXxNsi5gXn36g4xNfLWkMqPuFhRZAmMQ7jjQ/<2;3>/*),a:pkh([7fc39c07/48'/1'/0'/2']tpubDEvjgXtrUuH3Qtkapny9aE8gN847xiXsf9MDM5XueGf9nrvStqAuBSva3ajGyTvtp8Ti55FvVXsgYSXuS1tQkBeopFuodx2hRUDmQbvKxbZ/<2;3>/*),a:pkh([1a1ffd98/48'/1'/0'/2']tpubDFZqzTvGijYb13BC73CkS1er8DrP5YdzMhziN3kWCKUFaW51Yj6ggvf99YpdrkTJy4RT85mxQMHXDiFAKRxzf6BykQgT4pRRBNPshSJJcKo/<0;1>/*)),older(300))))#wp0w3hlw").unwrap(); - assert_eq!(res.0, "wsh(or_d(multi(2,@0/<0;1>/*,@1/<0;1>/*),and_v(v:thresh(2,pkh(@0/<2;3>/*),a:pkh(@1/<2;3>/*),a:pkh(@2/<0;1>/*)),older(300))))"); - assert_eq!(res.1.len(), 3); - assert_eq!(res.1[0].to_string(), "[b0822927/48'/1'/0'/2']tpubDEvZxV86Br8Knbm9tWcr5Hvmg5cYTYsg92vinqH6Bie6U8ix8CsoN9W11NQygdqVwmHUJpsHXxNsi5gXn36g4xNfLWkMqPuFhRZAmMQ7jjQ".to_string()); - assert_eq!(res.1[1].to_string(), "[7fc39c07/48'/1'/0'/2']tpubDEvjgXtrUuH3Qtkapny9aE8gN847xiXsf9MDM5XueGf9nrvStqAuBSva3ajGyTvtp8Ti55FvVXsgYSXuS1tQkBeopFuodx2hRUDmQbvKxbZ".to_string()); - assert_eq!(res.1[2].to_string(), "[1a1ffd98/48'/1'/0'/2']tpubDFZqzTvGijYb13BC73CkS1er8DrP5YdzMhziN3kWCKUFaW51Yj6ggvf99YpdrkTJy4RT85mxQMHXDiFAKRxzf6BykQgT4pRRBNPshSJJcKo".to_string()); - } -} diff --git a/src/lib.rs b/src/lib.rs index 6387e3b..e6bde69 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,8 @@ pub mod bip389; pub mod bitbox; #[cfg(feature = "coldcard")] pub mod coldcard; +#[cfg(feature = "jade")] +pub mod jade; #[cfg(feature = "ledger")] pub mod ledger; #[cfg(feature = "specter")] @@ -175,6 +177,7 @@ pub enum DeviceKind { SpecterSimulator, Ledger, LedgerSimulator, + Jade, } impl std::fmt::Display for DeviceKind { @@ -186,6 +189,7 @@ impl std::fmt::Display for DeviceKind { DeviceKind::SpecterSimulator => write!(f, "specter-simulator"), DeviceKind::Ledger => write!(f, "ledger"), DeviceKind::LedgerSimulator => write!(f, "ledger-simulator"), + DeviceKind::Jade => write!(f, "jade"), } } } @@ -200,6 +204,7 @@ impl std::str::FromStr for DeviceKind { "specter-simulator" => Ok(DeviceKind::SpecterSimulator), "ledger" => Ok(DeviceKind::Ledger), "ledger-simulator" => Ok(DeviceKind::LedgerSimulator), + "jade" => Ok(DeviceKind::Jade), _ => Err(()), } } diff --git a/src/utils.rs b/src/utils.rs index 24ed1f9..df733db 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,4 +1,4 @@ -use std::{cmp::Ordering, collections::BTreeMap}; +use std::{cmp::Ordering, collections::BTreeMap, str::FromStr}; use bitcoin::{ bip32::{ChildNumber, DerivationPath, KeySource}, @@ -108,6 +108,32 @@ pub fn bip86_path_child_numbers(path: DerivationPath) -> Result } } +#[cfg(feature = "regex")] +pub fn extract_keys_and_template(policy: &str) -> Result<(String, Vec), Error> { + let re = regex::Regex::new(r"((\[.+?\])?[xyYzZtuUvV]pub[1-9A-HJ-NP-Za-km-z]{79,108})").unwrap(); + let mut descriptor_template = policy.to_string(); + let mut pubkeys_str: Vec<&str> = Vec::new(); + for capture in re.find_iter(policy) { + if !pubkeys_str.contains(&capture.as_str()) { + pubkeys_str.push(capture.as_str()); + } + } + + let mut pubkeys: Vec = Vec::new(); + for (i, key_str) in pubkeys_str.iter().enumerate() { + descriptor_template = descriptor_template.replace(key_str, &format!("@{}", i)); + let pubkey = T::from_str(key_str).map_err(|_| Error::UnsupportedInput)?; + pubkeys.push(pubkey); + } + + // Do not include the hash in the descriptor template. + if let Some((descriptor_template, _hash)) = descriptor_template.rsplit_once('#') { + Ok((descriptor_template.to_string(), pubkeys)) + } else { + Ok((descriptor_template, pubkeys)) + } +} + #[cfg(test)] mod tests { use std::str::FromStr; @@ -125,4 +151,20 @@ mod tests { assert_eq!(psbt.inputs[0].bip32_derivation.len(), 2); assert_eq!(psbt.inputs[1].bip32_derivation.len(), 2); } + + #[test] + fn test_extract_keys_and_template() { + let res = extract_keys_and_template::("wsh(or_d(pk([f5acc2fd/49'/1'/0']tpubDCbK3Ysvk8HjcF6mPyrgMu3KgLiaaP19RjKpNezd8GrbAbNg6v5BtWLaCt8FNm6QkLseopKLf5MNYQFtochDTKHdfgG6iqJ8cqnLNAwtXuP/**),and_v(v:pkh(tpubDDtb2WPYwEWw2WWDV7reLV348iJHw2HmhzvPysKKrJw3hYmvrd4jasyoioVPdKGQqjyaBMEvTn1HvHWDSVqQ6amyyxRZ5YjpPBBGjJ8yu8S/**),older(100))))").unwrap(); + assert_eq!(res.0, "wsh(or_d(pk(@0/**),and_v(v:pkh(@1/**),older(100))))"); + assert_eq!(res.1.len(), 2); + assert_eq!(res.1[0], "[f5acc2fd/49'/1'/0']tpubDCbK3Ysvk8HjcF6mPyrgMu3KgLiaaP19RjKpNezd8GrbAbNg6v5BtWLaCt8FNm6QkLseopKLf5MNYQFtochDTKHdfgG6iqJ8cqnLNAwtXuP".to_string()); + assert_eq!(res.1[1], "tpubDDtb2WPYwEWw2WWDV7reLV348iJHw2HmhzvPysKKrJw3hYmvrd4jasyoioVPdKGQqjyaBMEvTn1HvHWDSVqQ6amyyxRZ5YjpPBBGjJ8yu8S".to_string()); + + let res = extract_keys_and_template::("wsh(or_d(multi(2,[b0822927/48'/1'/0'/2']tpubDEvZxV86Br8Knbm9tWcr5Hvmg5cYTYsg92vinqH6Bie6U8ix8CsoN9W11NQygdqVwmHUJpsHXxNsi5gXn36g4xNfLWkMqPuFhRZAmMQ7jjQ/<0;1>/*,[7fc39c07/48'/1'/0'/2']tpubDEvjgXtrUuH3Qtkapny9aE8gN847xiXsf9MDM5XueGf9nrvStqAuBSva3ajGyTvtp8Ti55FvVXsgYSXuS1tQkBeopFuodx2hRUDmQbvKxbZ/<0;1>/*),and_v(v:thresh(2,pkh([b0822927/48'/1'/0'/2']tpubDEvZxV86Br8Knbm9tWcr5Hvmg5cYTYsg92vinqH6Bie6U8ix8CsoN9W11NQygdqVwmHUJpsHXxNsi5gXn36g4xNfLWkMqPuFhRZAmMQ7jjQ/<2;3>/*),a:pkh([7fc39c07/48'/1'/0'/2']tpubDEvjgXtrUuH3Qtkapny9aE8gN847xiXsf9MDM5XueGf9nrvStqAuBSva3ajGyTvtp8Ti55FvVXsgYSXuS1tQkBeopFuodx2hRUDmQbvKxbZ/<2;3>/*),a:pkh([1a1ffd98/48'/1'/0'/2']tpubDFZqzTvGijYb13BC73CkS1er8DrP5YdzMhziN3kWCKUFaW51Yj6ggvf99YpdrkTJy4RT85mxQMHXDiFAKRxzf6BykQgT4pRRBNPshSJJcKo/<0;1>/*)),older(300))))#wp0w3hlw").unwrap(); + assert_eq!(res.0, "wsh(or_d(multi(2,@0/<0;1>/*,@1/<0;1>/*),and_v(v:thresh(2,pkh(@0/<2;3>/*),a:pkh(@1/<2;3>/*),a:pkh(@2/<0;1>/*)),older(300))))"); + assert_eq!(res.1.len(), 3); + assert_eq!(res.1[0], "[b0822927/48'/1'/0'/2']tpubDEvZxV86Br8Knbm9tWcr5Hvmg5cYTYsg92vinqH6Bie6U8ix8CsoN9W11NQygdqVwmHUJpsHXxNsi5gXn36g4xNfLWkMqPuFhRZAmMQ7jjQ".to_string()); + assert_eq!(res.1[1], "[7fc39c07/48'/1'/0'/2']tpubDEvjgXtrUuH3Qtkapny9aE8gN847xiXsf9MDM5XueGf9nrvStqAuBSva3ajGyTvtp8Ti55FvVXsgYSXuS1tQkBeopFuodx2hRUDmQbvKxbZ".to_string()); + assert_eq!(res.1[2], "[1a1ffd98/48'/1'/0'/2']tpubDFZqzTvGijYb13BC73CkS1er8DrP5YdzMhziN3kWCKUFaW51Yj6ggvf99YpdrkTJy4RT85mxQMHXDiFAKRxzf6BykQgT4pRRBNPshSJJcKo".to_string()); + } }