ledger_lib/lib.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275
//! A Ledger hardware wallet communication library
//!
//! [Device] provides a high-level API for exchanging APDUs with Ledger devices using the [ledger_proto] traits.
//! This is suitable for extension with application-specific interface traits, and automatically
//! implemented over [Exchange] for low-level byte exchange with devices.
//!
//! [LedgerProvider] and [LedgerHandle] provide a high-level tokio-compatible [Transport]
//! for application integration, supporting connecting to and interacting with ledger devices.
//! This uses a pinned thread to avoid thread safety issues with `hidapi` and async executors.
//!
//! Low-level [Transport] implementations are provided for [USB/HID](transport::UsbTransport),
//! [BLE](transport::BleTransport) and [TCP](transport::TcpTransport), with a [Generic](transport::GenericTransport)
//! implementation providing a common interface over all enabled transports.
//!
//! ## Safety
//!
//! Transports are currently marked as `Send` due to limitations of [async_trait] and are NOT all
//! thread safe. If you're calling this from an async context, please use [LedgerProvider].
//!
//! This will be corrected when the unstable async trait feature is stabilised,
//! which until then can be opted-into using the `unstable_async_trait` feature
//!
//! ## Examples
//!
//! ```no_run
//! use ledger_lib::{LedgerProvider, Filters, Transport, Device, DEFAULT_TIMEOUT};
//!
//! #[tokio::main]
//! async fn main() -> anyhow::Result<()> {
//! // Fetch provider handle
//! let mut provider = LedgerProvider::init().await;
//!
//! // List available devices
//! let devices = provider.list(Filters::Any).await?;
//!
//! // Check we have -a- device to connect to
//! if devices.is_empty() {
//! return Err(anyhow::anyhow!("No devices found"));
//! }
//!
//! // Connect to the first device
//! let mut ledger = provider.connect(devices[0].clone()).await?;
//!
//! // Request device information
//! let info = ledger.app_info(DEFAULT_TIMEOUT).await?;
//! println!("info: {info:?}");
//!
//! Ok(())
//! }
//! ```
#![cfg_attr(feature = "unstable_async_trait", feature(async_fn_in_trait))]
#![cfg_attr(feature = "unstable_async_trait", feature(negative_impls))]
use std::time::Duration;
use tracing::debug;
use ledger_proto::{
apdus::{ExitAppReq, RunAppReq},
GenericApdu, StatusCode,
};
pub mod info;
pub use info::LedgerInfo;
mod error;
pub use error::Error;
pub mod transport;
pub use transport::Transport;
mod provider;
pub use provider::{LedgerHandle, LedgerProvider};
mod device;
pub use device::Device;
/// Default timeout helper for use with [Device] and [Exchange]
pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(3);
/// Device discovery filter
#[derive(Copy, Clone, Debug, PartialEq, strum::Display)]
#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
#[non_exhaustive]
pub enum Filters {
/// List all devices available using supported transport
Any,
/// List only HID devices
Hid,
/// List only TCP devices
Tcp,
/// List only BLE device
Ble,
}
impl Default for Filters {
fn default() -> Self {
Self::Any
}
}
/// [Exchange] trait provides a low-level interface for byte-wise exchange of APDU commands with a ledger devices
#[cfg_attr(not(feature = "unstable_async_trait"), async_trait::async_trait)]
pub trait Exchange {
async fn exchange(&mut self, command: &[u8], timeout: Duration) -> Result<Vec<u8>, Error>;
}
/// Blanket [Exchange] impl for mutable references
#[cfg_attr(not(feature = "unstable_async_trait"), async_trait::async_trait)]
impl<T: Exchange + Send> Exchange for &mut T {
async fn exchange(&mut self, command: &[u8], timeout: Duration) -> Result<Vec<u8>, Error> {
<T as Exchange>::exchange(self, command, timeout).await
}
}
/// Launch an application by name and return a device handle.
///
/// This checks whether an application is running, exits this if it
/// is not the desired application, then launches the specified app
/// by name.
///
/// # WARNING
/// Due to the constant re-enumeration of devices when changing app
/// contexts, and the lack of reported serial numbers by ledger devices,
/// this is not incredibly reliable. Use at your own risk.
///
pub async fn launch_app<T>(
mut t: T,
info: <T as Transport>::Info,
app_name: &str,
opts: &LaunchAppOpts,
timeout: Duration,
) -> Result<<T as Transport>::Device, Error>
where
T: Transport<Info = LedgerInfo, Filters = Filters> + Send,
<T as Transport>::Device: Send,
{
let mut buff = [0u8; 256];
debug!("Connecting to {info:?}");
// Connect to device and fetch the currently running application
let mut d = t.connect(info.clone()).await?;
let i = d.app_info(timeout).await?;
// Early-return if we're already running the correct app
if i.name == app_name {
debug!("Already running app {app_name}");
return Ok(d);
}
// Send an exit request to the running app
if i.name != "BOLOS" {
debug!("Exiting running app {}", i.name);
match d
.request::<GenericApdu>(ExitAppReq::new(), &mut buff, timeout)
.await
{
Ok(_) | Err(Error::Status(StatusCode::Ok)) => (),
Err(e) => return Err(e),
}
debug!("Exit complete, reconnecting to {info:?}");
// Close and re-connect to the device
drop(d);
tokio::time::sleep(Duration::from_secs(opts.reconnect_delay_s as u64)).await;
d = reconnect(&mut t, info.clone(), opts).await?;
}
// Send run request
for i in 0..10 {
debug!("Issuing run request ({i}/10)");
let resp = d
.request::<GenericApdu>(RunAppReq::new(app_name), &mut buff, timeout)
.await;
// Handle responses
match resp {
// Ok response or status, app opened
Ok(_) | Err(Error::Status(StatusCode::Ok)) => {
debug!("Run request complete, reconnecting to {info:?}");
// Re-connect to the device following app loading
drop(d);
tokio::time::sleep(Duration::from_secs(opts.reconnect_delay_s as u64)).await;
d = reconnect(&mut t, info.clone(), opts).await?;
return Ok(d);
}
// Empty response, pending reply
Err(Error::EmptyResponse) => tokio::time::sleep(Duration::from_secs(1)).await,
// Error response, something failed
Err(e) => return Err(e),
}
}
Err(Error::Timeout)
}
pub struct LaunchAppOpts {
/// Delay prior to attempting device re-connection in seconds.
///
/// This delay is required to allow the OS to re-enumerate the HID
/// device.
pub reconnect_delay_s: usize,
/// Timeout for reconnect operations in seconds.
pub reconnect_timeout_s: usize,
}
impl Default for LaunchAppOpts {
fn default() -> Self {
Self {
reconnect_delay_s: 3,
reconnect_timeout_s: 10,
}
}
}
/// Helper to reconnect to devices
async fn reconnect<T: Transport<Info = LedgerInfo, Filters = Filters>>(
mut t: T,
info: LedgerInfo,
opts: &LaunchAppOpts,
) -> Result<<T as Transport>::Device, Error> {
let mut new_info = None;
// Build filter based on device connection type
let filters = Filters::from(info.kind());
debug!("Starting reconnect");
// Await device reconnection
for i in 0..opts.reconnect_timeout_s {
debug!("Listing devices ({i}/{})", opts.reconnect_timeout_s);
// List available devices
let devices = t.list(filters).await?;
// Look for matching device listing
// We can't use -paths- here because the VID changes on launch
// nor device serials, because these are always set to 1 (?!)
match devices
.iter()
.find(|i| i.model == info.model && i.kind() == info.kind())
{
Some(i) => {
new_info = Some(i.clone());
break;
}
None => tokio::time::sleep(Duration::from_secs(1)).await,
};
}
let new_info = match new_info {
Some(v) => v,
None => return Err(Error::Closed),
};
debug!("Device found, reconnecting!");
// Connect to device using new information object
let d = t.connect(new_info).await?;
// Return new device connection
Ok(d)
}