From 49a5ffd58dbd3a23330c77af5f4e33d9bd9bd595 Mon Sep 17 00:00:00 2001 From: louib Date: Tue, 27 Aug 2024 17:57:09 -0400 Subject: [PATCH] feat: add nusb support --- .github/workflows/ci.yml | 3 ++ Cargo.toml | 8 +++- README.md | 14 +++++- src/error.rs | 9 ++++ src/lib.rs | 97 ++++++++++++++++++++++++++++++++++++++++ src/usb.rs | 19 ++++++++ src/usb/nusb.rs | 87 +++++++++++++++++++++++++++++++++++ 7 files changed, 235 insertions(+), 2 deletions(-) create mode 100644 src/usb/nusb.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 98fe836..177edb8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -64,6 +64,9 @@ jobs: - name: Build the project with all the features run: cargo build --all-features + - name: Build the project with nusb support + run: cargo build --no-default-features --features nusb + - name: Build the examples run: cargo build --examples diff --git a/Cargo.toml b/Cargo.toml index b42414c..182df4a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,10 +20,16 @@ include = [ name = "challenge_response" path = "src/lib.rs" +[features] +rusb = ["dep:rusb"] +nusb = ["dep:nusb"] +default = ["rusb"] + [dependencies] rand = "0.8" bitflags = "2.4" -rusb = "0.9" +rusb = { version = "0.9", optional = true } +nusb = { version = "0.1", optional = true } structure = "0.1" aes = "0.8" block-modes = "0.9" diff --git a/README.md b/README.md index 6237a22..d818d92 100644 --- a/README.md +++ b/README.md @@ -31,13 +31,25 @@ ## Usage -Add this to your Cargo.toml +Add this to your `Cargo.toml` ```toml [dependencies] challenge_response = "0" ``` +### nusb backend (EXPERIMENTAL) + +You can enable the experimental [nusb](https://crates.io/crates/nusb) backend by adding the following to your `Cargo.toml` manifest: + +```toml +[dependencies] +challenge_response = { version = "0", default-features = false, features = ["nusb"] } +``` + +The `nusb` backend has the advantage of not depending on `libusb`, thus making it easier to add +`challenge_response` to your dependencies. + ### Perform a Challenge-Response (HMAC-SHA1 mode) If you are using a YubiKey, you can configure the HMAC-SHA1 Challenge-Response diff --git a/src/error.rs b/src/error.rs index 452972a..5ffcc87 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,3 +1,4 @@ +#[cfg(feature = "rusb")] use rusb::Error as usbError; use std::error; use std::fmt; @@ -6,26 +7,32 @@ use std::io::Error as ioError; #[derive(Debug)] pub enum ChallengeResponseError { IOError(ioError), + #[cfg(feature = "rusb")] UsbError(usbError), CommandNotSupported, DeviceNotFound, OpenDeviceError, CanNotWriteToDevice, + CanNotReadFromDevice, WrongCRC, ConfigNotWritten, + ListDevicesError, } impl fmt::Display for ChallengeResponseError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match *self { ChallengeResponseError::IOError(ref err) => write!(f, "IO error: {}", err), + #[cfg(feature = "rusb")] ChallengeResponseError::UsbError(ref err) => write!(f, "USB error: {}", err), ChallengeResponseError::DeviceNotFound => write!(f, "Device not found"), ChallengeResponseError::OpenDeviceError => write!(f, "Can not open device"), ChallengeResponseError::CommandNotSupported => write!(f, "Command Not Supported"), ChallengeResponseError::WrongCRC => write!(f, "Wrong CRC"), ChallengeResponseError::CanNotWriteToDevice => write!(f, "Can not write to Device"), + ChallengeResponseError::CanNotReadFromDevice => write!(f, "Can not read from Device"), ChallengeResponseError::ConfigNotWritten => write!(f, "Configuration has failed"), + ChallengeResponseError::ListDevicesError => write!(f, "Could not list available devices"), } } } @@ -33,6 +40,7 @@ impl fmt::Display for ChallengeResponseError { impl error::Error for ChallengeResponseError { fn cause(&self) -> Option<&dyn error::Error> { match *self { + #[cfg(feature = "rusb")] ChallengeResponseError::UsbError(ref err) => Some(err), _ => None, } @@ -45,6 +53,7 @@ impl From for ChallengeResponseError { } } +#[cfg(feature = "rusb")] impl From for ChallengeResponseError { fn from(err: usbError) -> ChallengeResponseError { ChallengeResponseError::UsbError(err) diff --git a/src/lib.rs b/src/lib.rs index 033136a..e2b88f8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,11 @@ #![doc = include_str!("../README.md")] + +#[cfg(not(any(feature = "rusb", feature = "nusb")))] +compile_error!("Either the rusb or nusb feature must be enabled for this crate"); + +#[cfg(feature = "nusb")] +extern crate nusb; +#[cfg(feature = "rusb")] extern crate rusb; #[macro_use] @@ -28,6 +35,7 @@ use configure::DeviceModeConfig; use error::ChallengeResponseError; use hmacmode::Hmac; use otpmode::Aes128Block; +#[cfg(feature = "rusb")] use rusb::UsbContext; use sec::{crc16, CRC_RESIDUAL_OK}; use usb::{close_device, open_device, read_response, wait, write_frame, Context, Flags, Frame}; @@ -68,6 +76,7 @@ pub struct ChallengeResponse { impl ChallengeResponse { /// Creates a new ChallengeResponse instance. + #[cfg(feature = "rusb")] pub fn new() -> Result { let context = match Context::new() { Ok(c) => c, @@ -75,7 +84,12 @@ impl ChallengeResponse { }; Ok(ChallengeResponse { context }) } + #[cfg(all(feature = "nusb", not(feature = "rusb")))] + pub fn new() -> Result { + Ok(ChallengeResponse { context: () }) + } + #[cfg(feature = "rusb")] fn read_serial_from_device(&mut self, device: rusb::Device) -> Result { let (mut handle, interfaces) = open_device(&mut self.context, device.bus_number(), device.address())?; let challenge = [0; CHALLENGE_SIZE]; @@ -102,6 +116,7 @@ impl ChallengeResponse { Ok(serial.0) } + #[cfg(feature = "rusb")] pub fn find_device(&mut self) -> Result { let devices = match self.context.devices() { Ok(d) => d, @@ -131,7 +146,21 @@ impl ChallengeResponse { Err(ChallengeResponseError::DeviceNotFound) } + #[cfg(all(feature = "nusb", not(feature = "rusb")))] + pub fn find_device(&mut self) -> Result { + match self.find_all_devices() { + Ok(devices) => { + if !devices.is_empty() { + Ok(devices[0].clone()) + } else { + Err(ChallengeResponseError::DeviceNotFound) + } + } + Err(e) => Err(e), + } + } + #[cfg(feature = "rusb")] pub fn find_device_from_serial(&mut self, serial: u32) -> Result { let devices = match self.context.devices() { Ok(d) => d, @@ -166,7 +195,43 @@ impl ChallengeResponse { Err(ChallengeResponseError::DeviceNotFound) } + #[cfg(all(feature = "nusb", not(feature = "rusb")))] + pub fn find_device_from_serial(&mut self, serial: u32) -> Result { + let nusb_devices = nusb::list_devices()?; + for device_info in nusb_devices { + let product_id = device_info.product_id(); + let vendor_id = device_info.vendor_id(); + + if !VENDOR_ID.contains(&vendor_id) || !PRODUCT_ID.contains(&product_id) { + continue; + } + + let device_serial = match device_info.serial_number() { + Some(s) => match s.parse::() { + Ok(s) => s, + Err(_) => continue, + }, + None => continue, + }; + if device_serial == serial { + return Ok(Device { + name: match device_info.manufacturer_string() { + Some(name) => Some(name.to_string()), + None => Some("unknown".to_string()), + }, + serial: Some(serial), + product_id, + vendor_id, + bus_id: device_info.bus_number(), + address_id: device_info.device_address(), + }); + } + } + Err(ChallengeResponseError::DeviceNotFound) + } + + #[cfg(feature = "rusb")] pub fn find_all_devices(&mut self) -> Result> { let mut result: Vec = Vec::new(); let devices = match self.context.devices() { @@ -200,6 +265,38 @@ impl ChallengeResponse { Err(ChallengeResponseError::DeviceNotFound) } + #[cfg(all(feature = "nusb", not(feature = "rusb")))] + pub fn find_all_devices(&mut self) -> Result> { + let mut devices: Vec = Vec::new(); + let nusb_devices = nusb::list_devices()?; + for device_info in nusb_devices { + let product_id = device_info.product_id(); + let vendor_id = device_info.vendor_id(); + + if !VENDOR_ID.contains(&vendor_id) || !PRODUCT_ID.contains(&product_id) { + continue; + } + + devices.push(Device { + name: match device_info.manufacturer_string() { + Some(name) => Some(name.to_string()), + None => Some("unknown".to_string()), + }, + serial: match device_info.serial_number() { + Some(serial) => match serial.parse::() { + Ok(s) => Some(s), + Err(_) => None, + }, + None => None, + }, + product_id, + vendor_id, + bus_id: device_info.bus_number(), + address_id: device_info.device_address(), + }); + } + Ok(devices) + } pub fn write_config(&mut self, conf: Config, device_config: &mut DeviceModeConfig) -> Result<()> { let d = device_config.to_frame(conf.command); diff --git a/src/usb.rs b/src/usb.rs index 91bf4d4..67c82d3 100644 --- a/src/usb.rs +++ b/src/usb.rs @@ -1,3 +1,6 @@ +#[cfg(all(feature = "nusb", not(feature = "rusb")))] +use nusb::Device; +#[cfg(feature = "rusb")] use rusb::{Context as RUSBContext, DeviceHandle as RUSBDeviceHandle}; use std::time::Duration; use std::{slice, thread}; @@ -6,9 +9,19 @@ use config::Command; use error::ChallengeResponseError; use sec::crc16; +#[cfg(all(feature = "nusb", not(feature = "rusb")))] +mod nusb; +#[cfg(feature = "rusb")] mod rusb; + +#[cfg(all(feature = "nusb", not(feature = "rusb")))] +use usb::nusb::{raw_write, read}; +#[cfg(feature = "rusb")] use usb::rusb::{raw_write, read}; +#[cfg(all(feature = "nusb", not(feature = "rusb")))] +pub use usb::nusb::{close_device, open_device}; +#[cfg(feature = "rusb")] pub use usb::rusb::{close_device, open_device}; /// The size of the payload when writing a request to the usb interface. @@ -53,9 +66,15 @@ impl Frame { } } +#[cfg(feature = "rusb")] pub type Context = RUSBContext; +#[cfg(all(feature = "nusb", not(feature = "rusb")))] +pub type Context = (); +#[cfg(feature = "rusb")] pub(crate) type DeviceHandle = RUSBDeviceHandle; +#[cfg(all(feature = "nusb", not(feature = "rusb")))] +pub(crate) type DeviceHandle = Device; pub fn write_frame(handle: &mut DeviceHandle, frame: &Frame) -> Result<(), ChallengeResponseError> { let mut data = unsafe { slice::from_raw_parts(frame as *const Frame as *const u8, 70) }; diff --git a/src/usb/nusb.rs b/src/usb/nusb.rs new file mode 100644 index 0000000..e2094b8 --- /dev/null +++ b/src/usb/nusb.rs @@ -0,0 +1,87 @@ +use error::ChallengeResponseError; +use nusb::Interface; +use std::time::Duration; +use usb::{DeviceHandle, HID_GET_REPORT, HID_SET_REPORT, REPORT_TYPE_FEATURE}; + +pub fn open_device( + _context: &mut (), + bus_id: u8, + address_id: u8, +) -> Result<(DeviceHandle, Vec), ChallengeResponseError> { + let nusb_devices = match nusb::list_devices() { + Ok(d) => d, + Err(e) => return Err(e.into()), + }; + for device_info in nusb_devices { + if device_info.bus_number() != bus_id || device_info.device_address() != address_id { + continue; + } + + let device = match device_info.open() { + Ok(d) => d, + Err(_) => { + return Err(ChallengeResponseError::OpenDeviceError); + } + }; + + let mut interfaces: Vec = Vec::new(); + for interface in device_info.interfaces() { + let interface = match device.detach_and_claim_interface(interface.interface_number()) { + Ok(interface) => interface, + Err(_) => continue, + }; + + interfaces.push(interface); + } + return Ok((device, interfaces)); + } + + Err(ChallengeResponseError::DeviceNotFound) +} + +pub fn close_device( + mut _handle: DeviceHandle, + _interfaces: Vec, +) -> Result<(), ChallengeResponseError> { + Ok(()) +} + +pub fn read(handle: &mut DeviceHandle, buf: &mut [u8]) -> Result { + assert_eq!(buf.len(), 8); + + let control_type = nusb::transfer::ControlType::Class; + let control_in = nusb::transfer::Control { + control_type, + recipient: nusb::transfer::Recipient::Interface, + request: HID_GET_REPORT, + value: REPORT_TYPE_FEATURE << 8, + index: 0, + }; + + match handle.control_in_blocking(control_in, buf, Duration::new(2, 0)) { + Ok(r) => Ok(r), + Err(_e) => Err(ChallengeResponseError::CanNotReadFromDevice), + } +} + +pub fn raw_write(handle: &mut DeviceHandle, packet: &[u8]) -> Result<(), ChallengeResponseError> { + let control_type = nusb::transfer::ControlType::Class; + let control_out = nusb::transfer::Control { + control_type, + recipient: nusb::transfer::Recipient::Interface, + request: HID_SET_REPORT, + value: REPORT_TYPE_FEATURE << 8, + index: 0, + }; + + match handle.control_out_blocking(control_out, packet, Duration::new(2, 0)) { + Ok(bytes_written) => { + if bytes_written != 8 { + Err(ChallengeResponseError::CanNotWriteToDevice) + } else { + Ok(()) + } + } + Err(_) => Err(ChallengeResponseError::CanNotWriteToDevice), + } +}