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