diff --git a/cli/src/main.rs b/cli/src/main.rs index f8fa445..ff9f6a7 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -79,6 +79,8 @@ pub enum Command { #[clap(long)] app_name: String, }, + /// List applications installed on device + ListApp, } #[derive(Clone, Debug, Default, PartialEq)] @@ -221,6 +223,20 @@ async fn main() -> anyhow::Result<()> { } } } + Command::ListApp => { + let mut d = connect(&mut p, &devices, args.index).await?; + let list = d.app_list(args.timeout.into()).await?; + println!("flags, name, hash, hash_code:"); + for info in &list { + println!( + "{:08x}, {}, {}, {}", + info.flags, + info.name, + info.hash.encode_hex::(), + info.hash_code_data.encode_hex::() + ); + } + } } Ok(()) } diff --git a/lib/src/device.rs b/lib/src/device.rs index d1fba7e..8315e27 100644 --- a/lib/src/device.rs +++ b/lib/src/device.rs @@ -6,8 +6,11 @@ use encdec::{EncDec, Encode}; use tracing::{debug, error}; use ledger_proto::{ - apdus::{AppInfoReq, AppInfoResp, DeviceInfoReq, DeviceInfoResp}, - ApduError, ApduReq, StatusCode, + apdus::{ + decode_app_data, AppData, AppInfoReq, AppInfoResp, AppListNextReq, AppListStartReq, + DeviceInfoReq, DeviceInfoResp, + }, + ApduError, ApduReq, GenericApdu, StatusCode, }; use crate::{ @@ -58,6 +61,49 @@ pub trait Device { flags: r.flags.to_vec(), }) } + + /// Fetch list of installed apps + async fn app_list(&mut self, timeout: Duration) -> Result, Error> { + let mut buff = [0u8; APDU_BUFF_LEN]; + + let mut app_data_list: Vec = Default::default(); + + let mut start: bool = true; + + loop { + let r = match start { + true => { + self.request::(AppListStartReq {}, &mut buff[..], timeout) + .await + } + false => { + self.request::(AppListNextReq {}, &mut buff[..], timeout) + .await + } + }; + + start = false; + + match r { + Ok(apdu_output) => { + let mut offset: usize = 1; + while offset < apdu_output.data.len() - 2 { + let data = decode_app_data(apdu_output.data.as_slice(), &mut offset) + .map_err(Error::from)?; + app_data_list.push(data); + } + } + Err(Error::Status(StatusCode::Ok)) => { + break; + } + Err(e) => { + error!("Command failed: {e:?}"); + return Err(e); + } + } + } + Ok(app_data_list) + } } /// Generic [Device] implementation for types supporting [Exchange] diff --git a/lib/src/lib.rs b/lib/src/lib.rs index da27300..b9048c0 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -80,11 +80,12 @@ pub use device::Device; pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(3); /// Device discovery filter -#[derive(Copy, Clone, Debug, PartialEq, strum::Display)] +#[derive(Copy, Clone, Debug, Default, PartialEq, strum::Display)] #[cfg_attr(feature = "clap", derive(clap::ValueEnum))] #[non_exhaustive] pub enum Filters { /// List all devices available using supported transport + #[default] Any, /// List only HID devices Hid, @@ -94,12 +95,6 @@ pub enum Filters { 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 { diff --git a/lib/src/transport/usb.rs b/lib/src/transport/usb.rs index 783b056..ca36c15 100644 --- a/lib/src/transport/usb.rs +++ b/lib/src/transport/usb.rs @@ -256,10 +256,7 @@ impl UsbDevice { trace!("Read chunk {seq_idx} ({rem} bytes remaining)"); // Read next chunk, constant timeout as chunks should be sent end-to-end - let n = match self.device.read_timeout(&mut buff, 500) { - Ok(n) => n, - Err(e) => return Err(e.into()), - }; + let n = self.device.read_timeout(&mut buff, 500)?; if n < 5 { error!("Invalid chunk length {n}"); diff --git a/proto/src/apdus/app_list.rs b/proto/src/apdus/app_list.rs new file mode 100644 index 0000000..0242bbb --- /dev/null +++ b/proto/src/apdus/app_list.rs @@ -0,0 +1,95 @@ +use encdec::{Decode, Encode}; + +extern crate alloc; +use alloc::string::String; +use alloc::vec::Vec; + +use crate::{ApduError, ApduStatic}; + +/// App List Start APDU command +#[derive(Copy, Clone, PartialEq, Debug, Default, Encode, Decode)] +#[encdec(error = "ApduError")] +pub struct AppListStartReq {} + +/// App List Next APDU command +#[derive(Copy, Clone, PartialEq, Debug, Default, Encode, Decode)] +#[encdec(error = "ApduError")] +pub struct AppListNextReq {} + +impl ApduStatic for AppListStartReq { + /// App list start request APDU is class `0xe0` + const CLA: u8 = 0xe0; + + /// App list start request APDU is instruction `0x01` + const INS: u8 = 0xde; +} + +impl ApduStatic for AppListNextReq { + /// App list next request APDU is class `0xe0` + const CLA: u8 = 0xe0; + + /// App list next request APDU is instruction `0x01` + const INS: u8 = 0xdf; +} + +#[derive(Clone, Debug, Default, PartialEq)] +pub struct AppData { + pub flags: u32, + pub hash_code_data: [u8; 32], + pub hash: [u8; 32], + pub name: String, +} + +/// Decodes an `AppData` structure from a binary slice, starting at the given offset. +/// +/// # Binary format +/// The expected format of the data is as follows (all fields are in order): +/// +/// - [0] : 1 byte - Reserved or tag byte (skipped by `*offset += 1`) +/// - [1..5] : 4 bytes - Flags (big-endian u32) +/// - [5..37] : 32 bytes - Hash code data +/// - [37..69] : 32 bytes - Hash +/// - [69] : 1 byte - Length of the name field (N) +/// - [70..70+N]: N bytes - Name (UTF-8 encoded) +/// +/// The function updates the provided `offset` as it parses each field. +/// +/// # Arguments +/// * `data` - The binary slice containing the encoded `AppData`. +/// * `offset` - A mutable reference to the current offset in the slice. This will be updated as fields are parsed. +/// +/// # Returns +/// * `Ok(AppData)` if decoding is successful. +/// * `Err(ApduError)` if decoding fails. +pub fn decode_app_data(data: &[u8], offset: &mut usize) -> Result { + *offset += 1; + let mut app_info: AppData = Default::default(); + let bytes = + <[u8; 4]>::try_from(&data[*offset..*offset + 4]).map_err(|_| ApduError::InvalidLength)?; + app_info.flags = u32::from_be_bytes(bytes); + *offset += 4; + if data.len() < *offset + 32 { + return Err(ApduError::InvalidLength); + } + app_info + .hash_code_data + .copy_from_slice(&data[*offset..*offset + 32]); + *offset += 32; + if data.len() < *offset + 32 { + return Err(ApduError::InvalidLength); + } + app_info.hash.copy_from_slice(&data[*offset..*offset + 32]); + *offset += 32; + if data.len() <= *offset { + return Err(ApduError::InvalidLength); + } + let name_len: usize = data[*offset] as usize; + *offset += 1; + if data.len() < *offset + name_len { + return Err(ApduError::InvalidLength); + } + app_info.name = String::from_utf8(Vec::from(&data[*offset..*offset + name_len])).unwrap(); + *offset += name_len; + + Ok(app_info) +} diff --git a/proto/src/apdus/mod.rs b/proto/src/apdus/mod.rs index 7c20825..ffad908 100644 --- a/proto/src/apdus/mod.rs +++ b/proto/src/apdus/mod.rs @@ -11,3 +11,6 @@ pub use run_app::RunAppReq; mod exit_app; pub use exit_app::ExitAppReq; + +mod app_list; +pub use app_list::{decode_app_data, AppData, AppListNextReq, AppListStartReq};