From 131866ffb7c4c0ac89e2dc2b12bccba9004821d2 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 | 108 ++++++++++++++++++++++++++-- src/nusb_manager.rs | 87 ++++++++++++++++++++++ src/{manager.rs => rusb_manager.rs} | 99 ++----------------------- src/usb.rs | 105 +++++++++++++++++++++++++++ 8 files changed, 334 insertions(+), 99 deletions(-) create mode 100644 src/nusb_manager.rs rename src/{manager.rs => rusb_manager.rs} (50%) 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 c4e7568..ce1baa7 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] @@ -16,12 +23,17 @@ pub mod config; pub mod configure; pub mod error; pub mod hmacmode; -mod manager; +#[cfg(all(feature = "nusb", not(feature = "rusb")))] +mod nusb_manager; pub mod otpmode; +#[cfg(feature = "rusb")] +mod rusb_manager; mod sec; mod usb; use aes::cipher::generic_array::GenericArray; +#[cfg(feature = "rusb")] +use rusb::UsbContext; use config::Command; use config::{Config, Slot}; @@ -29,11 +41,8 @@ use configure::DeviceModeConfig; use error::ChallengeResponseError; use hmacmode::Hmac; use otpmode::Aes128Block; -use rusb::{Context, UsbContext}; use sec::{crc16, CRC_RESIDUAL_OK}; -use usb::{Flags, Frame}; - -use manager::{close_device, open_device, read_response, wait, write_frame}; +use usb::{close_device, open_device, read_response, wait, write_frame, Context, Flags, Frame}; const VENDOR_ID: [u16; 3] = [ 0x1050, // Yubico ( Yubikeys ) @@ -71,6 +80,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, @@ -78,7 +88,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]; @@ -105,6 +120,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, @@ -134,7 +150,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, @@ -169,7 +199,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() { @@ -203,6 +269,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/nusb_manager.rs b/src/nusb_manager.rs new file mode 100644 index 0000000..e2094b8 --- /dev/null +++ b/src/nusb_manager.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), + } +} diff --git a/src/manager.rs b/src/rusb_manager.rs similarity index 50% rename from src/manager.rs rename to src/rusb_manager.rs index 36197e2..2079663 100644 --- a/src/manager.rs +++ b/src/rusb_manager.rs @@ -1,14 +1,13 @@ use error::ChallengeResponseError; -use rusb::{request_type, Context, DeviceHandle, Direction, Recipient, RequestType, UsbContext}; +use rusb::{request_type, Context, Direction, Recipient, RequestType, UsbContext}; use std::time::Duration; -use std::{slice, thread}; -use usb::{Flags, Frame, HID_GET_REPORT, HID_SET_REPORT, REPORT_TYPE_FEATURE, WRITE_RESET_PAYLOAD}; +use usb::{DeviceHandle, HID_GET_REPORT, HID_SET_REPORT, REPORT_TYPE_FEATURE}; pub fn open_device( context: &mut Context, bus_id: u8, address_id: u8, -) -> Result<(DeviceHandle, Vec), ChallengeResponseError> { +) -> Result<(DeviceHandle, Vec), ChallengeResponseError> { let devices = match context.devices() { Ok(device) => device, Err(_) => { @@ -66,15 +65,12 @@ pub fn open_device( } #[cfg(any(target_os = "macos", target_os = "windows"))] -pub fn close_device( - _handle: DeviceHandle, - _interfaces: Vec, -) -> Result<(), ChallengeResponseError> { +pub fn close_device(_handle: DeviceHandle, _interfaces: Vec) -> Result<(), ChallengeResponseError> { Ok(()) } #[cfg(not(any(target_os = "macos", target_os = "windows")))] -pub fn close_device(handle: DeviceHandle, interfaces: Vec) -> Result<(), ChallengeResponseError> { +pub fn close_device(handle: DeviceHandle, interfaces: Vec) -> Result<(), ChallengeResponseError> { for interface in interfaces { handle.release_interface(interface)?; handle.attach_kernel_driver(interface)?; @@ -82,55 +78,14 @@ pub fn close_device(handle: DeviceHandle, interfaces: Vec) -> Resul Ok(()) } -pub fn wait bool>( - handle: &mut DeviceHandle, - f: F, - buf: &mut [u8], -) -> Result<(), ChallengeResponseError> { - loop { - read(handle, buf)?; - let flags = Flags::from_bits_truncate(buf[7]); - if flags.contains(Flags::SLOT_WRITE_FLAG) || flags.is_empty() { - // Should store the version - } - - if f(flags) { - return Ok(()); - } - thread::sleep(Duration::new(0, 1000000)); - } -} - -pub fn read(handle: &mut DeviceHandle, buf: &mut [u8]) -> Result { +pub fn read(handle: &mut DeviceHandle, buf: &mut [u8]) -> Result { assert_eq!(buf.len(), 8); let reqtype = request_type(Direction::In, RequestType::Class, Recipient::Interface); let value = REPORT_TYPE_FEATURE << 8; Ok(handle.read_control(reqtype, HID_GET_REPORT, value, 0, buf, Duration::new(2, 0))?) } -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) }; - - let mut seq = 0; - let mut buf = [0; 8]; - while !data.is_empty() { - let (a, b) = data.split_at(7); - - if seq == 0 || b.is_empty() || a.iter().any(|&x| x != 0) { - let mut packet = [0; 8]; - (&mut packet[..7]).copy_from_slice(a); - - packet[7] = Flags::SLOT_WRITE_FLAG.bits() + seq; - wait(handle, |x| !x.contains(Flags::SLOT_WRITE_FLAG), &mut buf)?; - raw_write(handle, &packet)? - } - data = b; - seq += 1 - } - Ok(()) -} - -pub fn raw_write(handle: &mut DeviceHandle, packet: &[u8]) -> Result<(), ChallengeResponseError> { +pub fn raw_write(handle: &mut DeviceHandle, packet: &[u8]) -> Result<(), ChallengeResponseError> { let reqtype = request_type(Direction::Out, RequestType::Class, Recipient::Interface); let value = REPORT_TYPE_FEATURE << 8; if handle.write_control(reqtype, HID_SET_REPORT, value, 0, &packet, Duration::new(2, 0))? != 8 { @@ -139,43 +94,3 @@ pub fn raw_write(handle: &mut DeviceHandle, packet: &[u8]) -> Result<() Ok(()) } } - -/// Reset the write state after a read. -pub fn write_reset(handle: &mut DeviceHandle) -> Result<(), ChallengeResponseError> { - raw_write(handle, &WRITE_RESET_PAYLOAD)?; - let mut buf = [0; 8]; - wait(handle, |x| !x.contains(Flags::SLOT_WRITE_FLAG), &mut buf)?; - Ok(()) -} - -pub fn read_response( - handle: &mut DeviceHandle, - response: &mut [u8], -) -> Result { - let mut r0 = 0; - wait( - handle, - |f| f.contains(Flags::RESP_PENDING_FLAG), - &mut response[..8], - )?; - r0 += 7; - loop { - if read(handle, &mut response[r0..r0 + 8])? < 8 { - break; - } - let flags = Flags::from_bits_truncate(response[r0 + 7]); - if flags.contains(Flags::RESP_PENDING_FLAG) { - let seq = response[r0 + 7] & 0b00011111; - if r0 > 0 && seq == 0 { - // If the sequence number is 0, and we have read at - // least one packet, stop. - break; - } - } else { - break; - } - r0 += 7; - } - write_reset(handle)?; - Ok(r0) -} diff --git a/src/usb.rs b/src/usb.rs index dc64a47..09bac90 100644 --- a/src/usb.rs +++ b/src/usb.rs @@ -1,6 +1,23 @@ +#[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}; + use config::Command; +use error::ChallengeResponseError; +#[cfg(all(feature = "nusb", not(feature = "rusb")))] +use nusb_manager::{raw_write, read}; +#[cfg(feature = "rusb")] +use rusb_manager::{raw_write, read}; use sec::crc16; +#[cfg(all(feature = "nusb", not(feature = "rusb")))] +pub use nusb_manager::{close_device, open_device}; +#[cfg(feature = "rusb")] +pub use rusb_manager::{close_device, open_device}; + /// The size of the payload when writing a request to the usb interface. pub(crate) const PAYLOAD_SIZE: usize = 64; /// The size of the response after writing a request to the usb interface. @@ -42,3 +59,91 @@ impl Frame { f } } + +#[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) }; + + let mut seq = 0; + let mut buf = [0; 8]; + while !data.is_empty() { + let (a, b) = data.split_at(7); + + if seq == 0 || b.is_empty() || a.iter().any(|&x| x != 0) { + let mut packet = [0; 8]; + (&mut packet[..7]).copy_from_slice(a); + + packet[7] = Flags::SLOT_WRITE_FLAG.bits() + seq; + wait(handle, |x| !x.contains(Flags::SLOT_WRITE_FLAG), &mut buf)?; + raw_write(handle, &packet)?; + } + data = b; + seq += 1 + } + Ok(()) +} + +pub fn wait bool>( + handle: &mut DeviceHandle, + f: F, + buf: &mut [u8], +) -> Result<(), ChallengeResponseError> { + loop { + read(handle, buf)?; + let flags = Flags::from_bits_truncate(buf[7]); + if flags.contains(Flags::SLOT_WRITE_FLAG) || flags.is_empty() { + // Should store the version + } + + if f(flags) { + return Ok(()); + } + thread::sleep(Duration::new(0, 1000000)); + } +} + +/// Reset the write state after a read. +pub fn write_reset(handle: &mut DeviceHandle) -> Result<(), ChallengeResponseError> { + raw_write(handle, &WRITE_RESET_PAYLOAD)?; + let mut buf = [0; 8]; + wait(handle, |x| !x.contains(Flags::SLOT_WRITE_FLAG), &mut buf)?; + Ok(()) +} + +pub fn read_response(handle: &mut DeviceHandle, response: &mut [u8]) -> Result { + let mut r0 = 0; + wait( + handle, + |f| f.contains(Flags::RESP_PENDING_FLAG), + &mut response[..8], + )?; + r0 += 7; + loop { + if read(handle, &mut response[r0..r0 + 8])? < 8 { + break; + } + let flags = Flags::from_bits_truncate(response[r0 + 7]); + if flags.contains(Flags::RESP_PENDING_FLAG) { + let seq = response[r0 + 7] & 0b00011111; + if r0 > 0 && seq == 0 { + // If the sequence number is 0, and we have read at + // least one packet, stop. + break; + } + } else { + break; + } + r0 += 7; + } + write_reset(handle)?; + Ok(r0) +}