nam_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, 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 Any,
89 /// List only HID devices
90 Hid,
91 /// List only TCP devices
92 Tcp,
93 /// List only BLE device
94 Ble,
95}
96
97impl Default for Filters {
98 fn default() -> Self {
99 Self::Any
100 }
101}
102
103/// [Exchange] trait provides a low-level interface for byte-wise exchange of APDU commands with a ledger devices
104#[cfg_attr(not(feature = "unstable_async_trait"), async_trait::async_trait)]
105pub trait Exchange {
106 async fn exchange(&mut self, command: &[u8], timeout: Duration) -> Result<Vec<u8>, Error>;
107}
108
109/// Blanket [Exchange] impl for mutable references
110#[cfg_attr(not(feature = "unstable_async_trait"), async_trait::async_trait)]
111impl<T: Exchange + Send> Exchange for &mut T {
112 async fn exchange(&mut self, command: &[u8], timeout: Duration) -> Result<Vec<u8>, Error> {
113 <T as Exchange>::exchange(self, command, timeout).await
114 }
115}
116
117/// Launch an application by name and return a device handle.
118///
119/// This checks whether an application is running, exits this if it
120/// is not the desired application, then launches the specified app
121/// by name.
122///
123/// # WARNING
124/// Due to the constant re-enumeration of devices when changing app
125/// contexts, and the lack of reported serial numbers by ledger devices,
126/// this is not incredibly reliable. Use at your own risk.
127///
128pub async fn launch_app<T>(
129 mut t: T,
130 info: <T as Transport>::Info,
131 app_name: &str,
132 opts: &LaunchAppOpts,
133 timeout: Duration,
134) -> Result<<T as Transport>::Device, Error>
135where
136 T: Transport<Info = LedgerInfo, Filters = Filters> + Send,
137 <T as Transport>::Device: Send,
138{
139 let mut buff = [0u8; 256];
140
141 debug!("Connecting to {info:?}");
142
143 // Connect to device and fetch the currently running application
144 let mut d = t.connect(info.clone()).await?;
145 let i = d.app_info(timeout).await?;
146
147 // Early-return if we're already running the correct app
148 if i.name == app_name {
149 debug!("Already running app {app_name}");
150 return Ok(d);
151 }
152
153 // Send an exit request to the running app
154 if i.name != "BOLOS" {
155 debug!("Exiting running app {}", i.name);
156
157 match d
158 .request::<GenericApdu>(ExitAppReq::new(), &mut buff, timeout)
159 .await
160 {
161 Ok(_) | Err(Error::Status(StatusCode::Ok)) => (),
162 Err(e) => return Err(e),
163 }
164
165 debug!("Exit complete, reconnecting to {info:?}");
166
167 // Close and re-connect to the device
168 drop(d);
169
170 tokio::time::sleep(Duration::from_secs(opts.reconnect_delay_s as u64)).await;
171
172 d = reconnect(&mut t, info.clone(), opts).await?;
173 }
174
175 // Send run request
176 for i in 0..10 {
177 debug!("Issuing run request ({i}/10)");
178
179 let resp = d
180 .request::<GenericApdu>(RunAppReq::new(app_name), &mut buff, timeout)
181 .await;
182
183 // Handle responses
184 match resp {
185 // Ok response or status, app opened
186 Ok(_) | Err(Error::Status(StatusCode::Ok)) => {
187 debug!("Run request complete, reconnecting to {info:?}");
188
189 // Re-connect to the device following app loading
190 drop(d);
191
192 tokio::time::sleep(Duration::from_secs(opts.reconnect_delay_s as u64)).await;
193
194 d = reconnect(&mut t, info.clone(), opts).await?;
195
196 return Ok(d);
197 }
198 // Empty response, pending reply
199 Err(Error::EmptyResponse) => tokio::time::sleep(Duration::from_secs(1)).await,
200 // Error response, something failed
201 Err(e) => return Err(e),
202 }
203 }
204
205 Err(Error::Timeout)
206}
207
208pub struct LaunchAppOpts {
209 /// Delay prior to attempting device re-connection in seconds.
210 ///
211 /// This delay is required to allow the OS to re-enumerate the HID
212 /// device.
213 pub reconnect_delay_s: usize,
214
215 /// Timeout for reconnect operations in seconds.
216 pub reconnect_timeout_s: usize,
217}
218
219impl Default for LaunchAppOpts {
220 fn default() -> Self {
221 Self {
222 reconnect_delay_s: 3,
223 reconnect_timeout_s: 10,
224 }
225 }
226}
227
228/// Helper to reconnect to devices
229async fn reconnect<T: Transport<Info = LedgerInfo, Filters = Filters>>(
230 mut t: T,
231 info: LedgerInfo,
232 opts: &LaunchAppOpts,
233) -> Result<<T as Transport>::Device, Error> {
234 let mut new_info = None;
235
236 // Build filter based on device connection type
237 let filters = Filters::from(info.kind());
238
239 debug!("Starting reconnect");
240
241 // Await device reconnection
242 for i in 0..opts.reconnect_timeout_s {
243 debug!("Listing devices ({i}/{})", opts.reconnect_timeout_s);
244
245 // List available devices
246 let devices = t.list(filters).await?;
247
248 // Look for matching device listing
249 // We can't use -paths- here because the VID changes on launch
250 // nor device serials, because these are always set to 1 (?!)
251 match devices
252 .iter()
253 .find(|i| i.model == info.model && i.kind() == info.kind())
254 {
255 Some(i) => {
256 new_info = Some(i.clone());
257 break;
258 }
259 None => tokio::time::sleep(Duration::from_secs(1)).await,
260 };
261 }
262
263 let new_info = match new_info {
264 Some(v) => v,
265 None => return Err(Error::Closed),
266 };
267
268 debug!("Device found, reconnecting!");
269
270 // Connect to device using new information object
271 let d = t.connect(new_info).await?;
272
273 // Return new device connection
274 Ok(d)
275}