use std::str::FromStr;
use clap::Parser;
use hex::ToHex;
use ledger_proto::{ApduHeader, GenericApdu};
use tracing::{debug, error};
use tracing_subscriber::{filter::LevelFilter, EnvFilter, FmtSubscriber};
use ledger_lib::{Device, Error, Filters, LedgerHandle, LedgerInfo, LedgerProvider, Transport};
#[derive(Clone, Debug, PartialEq, Parser)]
pub struct Args {
#[clap(subcommand)]
cmd: Command,
#[clap(long, default_value = "0")]
index: usize,
#[clap(long, default_value = "any")]
filters: Filters,
#[clap(long, default_value = "3s")]
timeout: humantime::Duration,
#[clap(long, default_value = "debug")]
log_level: LevelFilter,
}
#[derive(Clone, Debug, PartialEq, Parser)]
pub enum Command {
List,
AppInfo,
DeviceInfo,
Apdu {
#[clap(long, value_parser=u8_parse_maybe_hex)]
cla: u8,
#[clap(long, value_parser=u8_parse_maybe_hex)]
ins: u8,
#[clap(long, value_parser=u8_parse_maybe_hex, default_value_t=0)]
p1: u8,
#[clap(long, value_parser=u8_parse_maybe_hex, default_value_t=0)]
p2: u8,
#[clap(default_value = "")]
data: ApduData,
},
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct ApduData(Vec<u8>);
impl FromStr for ApduData {
type Err = hex::FromHexError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let v = hex::decode(s)?;
Ok(Self(v))
}
}
fn u8_parse_maybe_hex(s: &str) -> Result<u8, std::num::ParseIntError> {
if let Some(s) = s.strip_prefix("0x") {
u8::from_str_radix(s, 16)
} else {
s.parse::<u8>()
}
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let args = Args::parse();
let filter = EnvFilter::from_default_env()
.add_directive("hyper=warn".parse()?)
.add_directive("rocket=warn".parse()?)
.add_directive("btleplug=warn".parse()?)
.add_directive(args.log_level.into());
let _ = FmtSubscriber::builder()
.compact()
.without_time()
.with_max_level(args.log_level)
.with_env_filter(filter)
.try_init();
debug!("args: {:?}", args);
let mut p = LedgerProvider::init().await;
let devices = p.list(args.filters).await?;
match args.cmd {
Command::List => {
println!("devices:");
for (i, d) in devices.iter().enumerate() {
println!(" {i} {} ({})", d.model, d.conn);
}
}
Command::AppInfo => {
let mut d = connect(&mut p, &devices, args.index).await?;
let i = d.app_info(args.timeout.into()).await?;
println!("app info: {:?}", i);
}
Command::DeviceInfo => {
let mut d = connect(&mut p, &devices, args.index).await?;
let i = d.device_info(args.timeout.into()).await?;
println!("device info: {:?}", i);
}
Command::Apdu {
cla,
ins,
p1,
p2,
data,
} => {
let req = GenericApdu {
header: ApduHeader { cla, ins, p1, p2 },
data: data.0,
};
let mut d = connect(&mut p, &devices, args.index).await?;
let mut buff = [0u8; 256];
let resp = d
.request::<GenericApdu>(req, &mut buff, args.timeout.into())
.await?;
println!("Response: {}", resp.data.encode_hex::<String>());
}
}
Ok(())
}
async fn connect(
p: &mut LedgerProvider,
devices: &[LedgerInfo],
index: usize,
) -> Result<LedgerHandle, Error> {
if devices.is_empty() {
return Err(Error::NoDevices);
}
if index > devices.len() {
return Err(Error::InvalidDeviceIndex(index));
}
let d = &devices[index];
debug!("Connecting to device: {:?}", d);
match p.connect(d.clone()).await {
Ok(v) => Ok(v),
Err(e) => {
error!("Failed to connect to device {:?}: {:?}", d, e);
Err(e)
}
}
}