Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
target/
Cargo.lock
.vscode
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ members = [
"cli",
]

[workspace.package]
# 1.85+ is needed for its support of the 2024 edition, which is in turn needed by bluez-async-0.8.2
rust-version = "1.85"

[patch.crates-io]
ledger-proto = { path = "proto" }
ledger-lib = { path = "lib" }
Expand Down
10 changes: 9 additions & 1 deletion cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ keywords = [ "ledger", "wallet", "cli" ]
version = "0.1.0"
edition = "2021"
license = "Apache-2.0"
rust-version.workspace = true

[features]

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI: changes in this file are only needed to be able to override the backend from command line when building ledger-cli (e.g. cargo run --no-default-features --features transport_usb_hidraw --bin ledger-cli -- list)

transport_usb_libusb = [ "ledger-lib/transport_usb_libusb" ]
transport_usb_hidraw = [ "ledger-lib/transport_usb_hidraw" ]

# Same default as in lib/Cargo.toml
default = [ "transport_usb_libusb" ]

[dependencies]
clap = { version = "4.2", features = [ "derive" ] }
Expand All @@ -18,5 +26,5 @@ humantime = "2.1"
hex = "0.4"
serde_json = "1.0"

ledger-lib = { version = "0.1", features = [ "clap" ] }
ledger-lib = { version = "0.1", default-features = false, features = [ "clap", "transport_usb", "transport_tcp", "transport_ble" ] }
ledger-proto = { version = "0.1" }
30 changes: 13 additions & 17 deletions lib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,44 +6,40 @@ keywords = [ "ledger", "wallet", "usb", "hid", "bluetooth" ]
version = "0.1.0"
edition = "2021"
license = "Apache-2.0"
rust-version.workspace = true

[features]
# Select enabled transports
transport_usb = [ "hidapi" ]
transport_tcp = []
transport_ble = [ "btleplug" ]

# Switch libusb backends, `libusb` works better with WSL so we're using that by default
# Switch libusb backends, `libusb` works better with WSL and plain Linux so we're using that by default

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI: I had a situation on my Ubuntu where HidApi::devices_list only returned the FIDO interface and not the APDU one when using the hidraw backend, but it returned both interfaces when using libusb. The issue was solved by physically disconnecting and reconnecting the device. So it looks like hidraw is unreliable in general and not only with WSL.

transport_usb_libusb = [ "hidapi/linux-static-libusb" ]
transport_usb_hidraw = [ "hidapi/linux-static-hidraw" ]

# Enable `clap` attributes on exported objects
clap = [ "dep:clap" ]

# enable `async_fn_in_trait` nightly feature, removes need for `async_trait` macros
unstable_async_trait = []

default = [ "transport_usb", "transport_tcp", "transport_ble", "transport_usb_libusb" ]

[dependencies]

thiserror = "2.0"
async-trait = "0.1"
btleplug = { version = "0.11", optional = true }
clap = { version = "4.2", optional = true }
displaydoc = "0.2"
encdec = "0.10"
futures = "0.3"
hidapi = { version = "2.6", optional = true, default-features = false }
lazy_static = "1.5"
ledger-proto = { version = "0.1", default-features = false, features = [ "std" ] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
once_cell = "1.17"
strum = { version = "0.26", features = ["derive"] }
thiserror = "2.0"
tokio = { version = "1.27", features = ["full"] }
once_cell = "1.17"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
uuid = "1.3"
futures = "0.3"
async-trait = "0.1"
displaydoc = "0.2"

clap = { version = "4.2", optional = true }
hidapi = { version = "2.1", optional = true, default-features = false }
btleplug = { version = "0.11", optional = true }


[dev-dependencies]
anyhow = "1.0"
18 changes: 14 additions & 4 deletions lib/src/device.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

use std::time::Duration;

use async_trait::async_trait;
use encdec::{EncDec, Encode};
use tracing::{debug, error};

Expand All @@ -20,10 +21,19 @@ use crate::{

const APDU_BUFF_LEN: usize = 256;

/// [Device] provides a high-level interface exchanging APDU objects with implementers of [Exchange]
#[cfg_attr(not(feature = "unstable_async_trait"), async_trait::async_trait)]
// Note: replacing the `async_trait` macro below with the "modern" syntax, i.e.
// fn foo(...) -> impl Future<Output = ...> + Send {
// async move { ... }
// }
// results in a bunch of errors "lifetime bound not satisfied ... note: this is a known limitation
// that will be removed in the future (see issue #100013 for more information)".
// This happens with Rust 1.91 and below, while 1.92 is able to compile the code.
// So, `async_trait` serves as a workaround for this issue.

/// [Device] provides a high-level interface exchanging APDU objects with implementers of [Exchange].
#[async_trait]
pub trait Device {
/// Issue a request APDU, returning a reponse APDU
/// Issue a request APDU, returning a response APDU
async fn request<'a, 'b, RESP: EncDec<'b, ApduError>>(
&mut self,
request: impl ApduReq<'a> + Send,
Expand Down Expand Up @@ -107,7 +117,7 @@ pub trait Device {
}

/// Generic [Device] implementation for types supporting [Exchange]
#[cfg_attr(not(feature = "unstable_async_trait"), async_trait::async_trait)]
#[async_trait]
impl<T: Exchange + Send> Device for T {
/// Issue a request APDU to a device, encoding and decoding internally then returning a response APDU
async fn request<'a, 'b, RESP: EncDec<'b, ApduError>>(
Expand Down
36 changes: 30 additions & 6 deletions lib/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,23 @@ pub enum Error {
#[error(transparent)]
Ble(#[from] btleplug::Error),

#[error("Unknown ledger model: {0}")]
UnknownModel(u16),
#[error("Attempted to send APDU to unknown device handle")]
ApduSentToUnknownDeviceHandle,

#[error("Unknown error")]
Unknown,
#[error("Request channel closed")]
RequestChannelClosed,

#[error("Request response channel closed")]
RequestResponseChannelClosed,

#[error("Unexpected response while listing devices")]
UnexpectedResponseWhileListingDevices,

#[error("Unexpected response while connecting")]
UnexpectedResponseWhileConnecting,

#[error("Unexpected response while exchanging data")]
UnexpectedResponseWhileExchangingData,

#[error("No devices found")]
NoDevices,
Expand Down Expand Up @@ -55,8 +67,20 @@ pub enum Error {
#[error("Device in use")]
DeviceInUse,

#[error("Already running application ({0})")]
ApplicationLoaded(String),
#[error("Cannot read BLE device properties")]
CannotReadBleDeviceProperties,

#[error("Cannot find BLE device specs")]
CannotFindBleDeviceSpecs,

#[error("The BLE device is still not connected after a successful connect")]
NotConnectedAfterSuccessfulBleConnect,

#[error("Missing read or write BLE characteristics")]
MissingReadOrWriteBleCharacteristics,

#[error("Unexpected MTU response")]
UnexpectedMtuResponse,
}

impl From<tokio::time::error::Elapsed> for Error {
Expand Down
160 changes: 146 additions & 14 deletions lib/src/info.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
//! Device information types and connection filters

use std::collections::BTreeMap;

use strum::{Display, EnumString};
use uuid::{uuid, Uuid};

use crate::Filters;

Expand Down Expand Up @@ -37,7 +40,7 @@ impl LedgerInfo {
}

/// Ledger device models
#[derive(Clone, PartialEq, Debug, Display, EnumString)]
#[derive(Copy, Clone, PartialEq, Debug, Display, EnumString)]
pub enum Model {
/// Nano S
NanoS,
Expand All @@ -47,27 +50,156 @@ pub enum Model {
NanoX,
/// Stax
Stax,
/// Flex
Flex,
/// Nano Gen5
NanoGen5,
/// Unknown model
Unknown(u16),
Unknown { usb_pid: Option<u16> },
}

impl Model {
/// Convert a USB PID to a [Model] kind
///
/// Note that ledger PIDs vary depending on the device state so only the top byte is used
/// for matching.
pub fn from_pid(pid: u16) -> Model {
match pid & 0xFF00 {
// TODO: support all the models
//0x0001 => Ok(Model::NanoS),
0x4000 => Model::NanoX,
0x5000 => Model::NanoSPlus,
//0x0006 => Ok(Model::Stax),
_ => Model::Unknown(pid),
}
pub fn from_usb_pid(usb_pid: u16) -> Model {
INTERNAL_DEVICE_INFOS
.iter()
.find_map(|device_info| {
device_info
.matches_usb_pid(usb_pid)
.then_some(device_info.model)
})
.unwrap_or(Model::Unknown {
usb_pid: Some(usb_pid),
})
}
}

#[derive(Clone, PartialEq, Debug)]
pub struct BleSpec {
pub service_uuid: Uuid,
pub notify_uuid: Uuid,
pub write_uuid: Uuid,
pub write_cmd_uuid: Uuid,
}

struct InternalDeviceInfo {
model: Model,
legacy_usb_product_id: u16,
product_id_mm: u16,
ble_specs: Vec<BleSpec>,
}

impl InternalDeviceInfo {
fn matches_usb_pid(&self, usb_pid: u16) -> bool {
// First compare the passed pid with the legacy product id, if that doesn't match, use product_id_mm.
// The logic was taken from here:
// https://github.com/LedgerHQ/ledger-live/blob/b870b8018319b7489c39c92e743adcfd4e33e948/libs/ledgerjs/packages/devices/src/index.ts#L190
// (the legacy product id match will probably only work for some early variants of NanoS, but it's
// still better to be consistent with Ledger Live, just in case).
usb_pid == self.legacy_usb_product_id || usb_pid >> 8 == self.product_id_mm
}
}

// The table was taken from here:
// https://github.com/LedgerHQ/ledger-live/blob/b870b8018319b7489c39c92e743adcfd4e33e948/libs/ledgerjs/packages/devices/src/index.ts#L41
lazy_static::lazy_static! {
static ref INTERNAL_DEVICE_INFOS: Vec<InternalDeviceInfo> = {
vec![
InternalDeviceInfo {
model: Model::NanoS,
legacy_usb_product_id: 0x0001,
product_id_mm: 0x10,
ble_specs: vec![],
},
InternalDeviceInfo {
model: Model::NanoX,
legacy_usb_product_id: 0x0004,
product_id_mm: 0x40,
ble_specs: vec![
BleSpec {
service_uuid: uuid!("13d63400-2c97-0004-0000-4c6564676572"),
notify_uuid: uuid!("13d63400-2c97-0004-0001-4c6564676572"),
write_uuid: uuid!("13d63400-2c97-0004-0002-4c6564676572"),
write_cmd_uuid: uuid!("13d63400-2c97-0004-0003-4c6564676572"),
},
],
},
InternalDeviceInfo {
model: Model::NanoSPlus,
legacy_usb_product_id: 0x0005,
product_id_mm: 0x50,
ble_specs: vec![],
},
InternalDeviceInfo {
model: Model::Stax,
legacy_usb_product_id: 0x0006,
product_id_mm: 0x60,
ble_specs: vec![
BleSpec {
service_uuid: uuid!("13d63400-2c97-6004-0000-4c6564676572"),
notify_uuid: uuid!("13d63400-2c97-6004-0001-4c6564676572"),
write_uuid: uuid!("13d63400-2c97-6004-0002-4c6564676572"),
write_cmd_uuid: uuid!("13d63400-2c97-6004-0003-4c6564676572"),
},
],
},
InternalDeviceInfo {
model: Model::Flex,
legacy_usb_product_id: 0x0007,
product_id_mm: 0x70,
ble_specs: vec![
BleSpec {
service_uuid: uuid!("13d63400-2c97-3004-0000-4c6564676572"),
notify_uuid: uuid!("13d63400-2c97-3004-0001-4c6564676572"),
write_uuid: uuid!("13d63400-2c97-3004-0002-4c6564676572"),
write_cmd_uuid: uuid!("13d63400-2c97-3004-0003-4c6564676572"),
},
],
},
InternalDeviceInfo {
model: Model::NanoGen5,
legacy_usb_product_id: 0x0008,
product_id_mm: 0x80,
ble_specs: vec![
BleSpec {
service_uuid: uuid!("13d63400-2c97-8004-0000-4c6564676572"),
notify_uuid: uuid!("13d63400-2c97-8004-0001-4c6564676572"),
write_uuid: uuid!("13d63400-2c97-8004-0002-4c6564676572"),
write_cmd_uuid: uuid!("13d63400-2c97-8004-0003-4c6564676572"),
},
],
},
]
};

static ref BLE_SPECS_BY_SERVICE_UUID: BTreeMap<Uuid, &'static BleSpec> = {
INTERNAL_DEVICE_INFOS
.iter()
.flat_map(|dev_info| dev_info.ble_specs.iter())
.map(|ble_spec| (ble_spec.service_uuid, ble_spec))
.collect()
};

static ref INTERNAL_DEVICE_INFOS_BY_BLE_SERVICE_UUID: BTreeMap<Uuid, &'static InternalDeviceInfo> = {
INTERNAL_DEVICE_INFOS
.iter()
.flat_map(|dev_info| {
dev_info.ble_specs.iter().map(move |ble_spec| (ble_spec.service_uuid, dev_info))
})
.collect()
};
}

pub fn model_by_ble_service_uuid(service_uuid: &Uuid) -> Option<Model> {
INTERNAL_DEVICE_INFOS_BY_BLE_SERVICE_UUID
.get(service_uuid)
.map(|dev_info| dev_info.model)
}

pub fn ble_spec_by_service_uuid(service_uuid: &Uuid) -> Option<&'static BleSpec> {
BLE_SPECS_BY_SERVICE_UUID.get(service_uuid).copied()
}

/// Ledger connection information
#[derive(Clone, PartialEq, Debug)]
pub enum ConnInfo {
Expand Down
Loading
Loading