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
16 changes: 16 additions & 0 deletions cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ pub enum Command {
#[clap(long)]
app_name: String,
},
/// List applications installed on device
ListApp,
}

#[derive(Clone, Debug, Default, PartialEq)]
Expand Down Expand Up @@ -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::<String>(),
info.hash_code_data.encode_hex::<String>()
);
}
}
}
Ok(())
}
Expand Down
50 changes: 48 additions & 2 deletions lib/src/device.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -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<Vec<AppData>, Error> {
let mut buff = [0u8; APDU_BUFF_LEN];

let mut app_data_list: Vec<AppData> = Default::default();

let mut start: bool = true;

loop {
let r = match start {
true => {
self.request::<GenericApdu>(AppListStartReq {}, &mut buff[..], timeout)
.await
}
false => {
self.request::<GenericApdu>(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]
Expand Down
9 changes: 2 additions & 7 deletions lib/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 {
Expand Down
5 changes: 1 addition & 4 deletions lib/src/transport/usb.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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}");
Expand Down
95 changes: 95 additions & 0 deletions proto/src/apdus/app_list.rs
Original file line number Diff line number Diff line change
@@ -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,
}

Comment thread
yogh333 marked this conversation as resolved.
/// 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`)

Check warning on line 48 in proto/src/apdus/app_list.rs

View workflow job for this annotation

GitHub Actions / docs

unresolved link to `0`
/// - [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)

Check warning on line 52 in proto/src/apdus/app_list.rs

View workflow job for this annotation

GitHub Actions / docs

unresolved link to `69`
/// - [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<AppData, ApduError> {
*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;
Comment thread
yogh333 marked this conversation as resolved.
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();
Comment thread
yogh333 marked this conversation as resolved.
*offset += name_len;

Ok(app_info)
}
3 changes: 3 additions & 0 deletions proto/src/apdus/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Loading