Skip to content

Commit

Permalink
feat: add nusb support
Browse files Browse the repository at this point in the history
  • Loading branch information
louib committed Aug 31, 2024
1 parent f410578 commit 49a5ffd
Show file tree
Hide file tree
Showing 7 changed files with 235 additions and 2 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
8 changes: 7 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#[cfg(feature = "rusb")]
use rusb::Error as usbError;
use std::error;
use std::fmt;
Expand All @@ -6,33 +7,40 @@ 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"),
}
}
}

impl error::Error for ChallengeResponseError {
fn cause(&self) -> Option<&dyn error::Error> {
match *self {
#[cfg(feature = "rusb")]
ChallengeResponseError::UsbError(ref err) => Some(err),
_ => None,
}
Expand All @@ -45,6 +53,7 @@ impl From<ioError> for ChallengeResponseError {
}
}

#[cfg(feature = "rusb")]
impl From<usbError> for ChallengeResponseError {
fn from(err: usbError) -> ChallengeResponseError {
ChallengeResponseError::UsbError(err)
Expand Down
97 changes: 97 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -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]
Expand Down Expand Up @@ -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};
Expand Down Expand Up @@ -68,14 +76,20 @@ pub struct ChallengeResponse {

impl ChallengeResponse {
/// Creates a new ChallengeResponse instance.
#[cfg(feature = "rusb")]
pub fn new() -> Result<Self> {
let context = match Context::new() {
Ok(c) => c,
Err(e) => return Err(ChallengeResponseError::UsbError(e)),
};
Ok(ChallengeResponse { context })
}
#[cfg(all(feature = "nusb", not(feature = "rusb")))]
pub fn new() -> Result<Self> {
Ok(ChallengeResponse { context: () })
}

#[cfg(feature = "rusb")]
fn read_serial_from_device(&mut self, device: rusb::Device<Context>) -> Result<u32> {
let (mut handle, interfaces) = open_device(&mut self.context, device.bus_number(), device.address())?;
let challenge = [0; CHALLENGE_SIZE];
Expand All @@ -102,6 +116,7 @@ impl ChallengeResponse {
Ok(serial.0)
}

#[cfg(feature = "rusb")]
pub fn find_device(&mut self) -> Result<Device> {
let devices = match self.context.devices() {
Ok(d) => d,
Expand Down Expand Up @@ -131,7 +146,21 @@ impl ChallengeResponse {

Err(ChallengeResponseError::DeviceNotFound)
}
#[cfg(all(feature = "nusb", not(feature = "rusb")))]
pub fn find_device(&mut self) -> Result<Device> {
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<Device> {
let devices = match self.context.devices() {
Ok(d) => d,
Expand Down Expand Up @@ -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<Device> {
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::<u32>() {
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<Vec<Device>> {
let mut result: Vec<Device> = Vec::new();
let devices = match self.context.devices() {
Expand Down Expand Up @@ -200,6 +265,38 @@ impl ChallengeResponse {

Err(ChallengeResponseError::DeviceNotFound)
}
#[cfg(all(feature = "nusb", not(feature = "rusb")))]
pub fn find_all_devices(&mut self) -> Result<Vec<Device>> {
let mut devices: Vec<Device> = 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::<u32>() {
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);
Expand Down
19 changes: 19 additions & 0 deletions src/usb.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand All @@ -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.
Expand Down Expand Up @@ -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<Context>;
#[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) };
Expand Down
87 changes: 87 additions & 0 deletions src/usb/nusb.rs
Original file line number Diff line number Diff line change
@@ -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<Interface>), 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<Interface> = 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<Interface>,
) -> Result<(), ChallengeResponseError> {
Ok(())
}

pub fn read(handle: &mut DeviceHandle, buf: &mut [u8]) -> Result<usize, ChallengeResponseError> {
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),
}
}

0 comments on commit 49a5ffd

Please sign in to comment.