#![warn(
missing_copy_implementations,
missing_debug_implementations,
missing_docs,
non_ascii_idents,
trivial_casts,
unused,
unused_qualifications
)]
#![deny(unsafe_code)]
use std::{error, fmt, ops};
const VID_NITROKEY_3: u16 = 0x20a0;
const PID_NITROKEY_3: u16 = 0x42b2;
mod commands {
use ctaphid::command::VendorCommand;
pub const UPDATE: VendorCommand = VendorCommand::H51;
pub const REBOOT: VendorCommand = VendorCommand::H53;
pub const RNG: VendorCommand = VendorCommand::H60;
pub const VERSION: VendorCommand = VendorCommand::H61;
}
#[derive(Debug)]
pub struct Devices {
ctap_devices: ctaphid::Devices,
device_infos: Vec<DeviceInfo>,
}
impl Devices {
fn new(devices: ctaphid::Devices) -> Self {
let device_infos = devices
.iter()
.filter(|device| device.vendor_id() == VID_NITROKEY_3)
.filter(|device| device.product_id() == PID_NITROKEY_3)
.map(DeviceInfo::new)
.collect();
Devices {
ctap_devices: devices,
device_infos,
}
}
pub fn connect(&self, device_info: &DeviceInfo) -> Result<Device, Error> {
self.ctap_devices
.connect(&device_info.device_info)
.map(Device::new)
.map_err(From::from)
}
}
impl<'a> IntoIterator for &'a Devices {
type Item = &'a DeviceInfo;
type IntoIter = std::slice::Iter<'a, DeviceInfo>;
fn into_iter(self) -> Self::IntoIter {
self.device_infos.iter()
}
}
impl ops::Deref for Devices {
type Target = [DeviceInfo];
fn deref(&self) -> &Self::Target {
&self.device_infos
}
}
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub struct Version {
pub major: u16,
pub minor: u16,
pub patch: u8,
}
impl fmt::Display for Version {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "v{}.{}.{}", self.major, self.minor, self.patch)
}
}
#[derive(Clone, Debug)]
pub struct DeviceInfo {
device_info: ctaphid::DeviceInfo,
}
impl DeviceInfo {
fn new(device_info: &ctaphid::DeviceInfo) -> Self {
let device_info = device_info.to_owned();
Self { device_info }
}
}
#[derive(Debug)]
pub struct Device {
device: ctaphid::Device,
}
impl Device {
fn new(device: ctaphid::Device) -> Self {
Self { device }
}
pub fn firmware_version(&self) -> Result<Version, Error> {
let response = self.device.vendor_command(commands::VERSION, &[])?;
if let Ok(version) = std::convert::TryFrom::try_from(response) {
let version = u32::from_be_bytes(version);
let major = (version >> 22) as u16;
let minor = ((version >> 6) & 0b1111_1111_1111_1111) as u16;
let patch = (version & 0b11_1111) as u8;
Ok(Version {
major,
minor,
patch,
})
} else {
Err(Error::from(CommandError::InvalidResponseLength))
}
}
pub fn rng(&self) -> Result<Vec<u8>, Error> {
self.device
.vendor_command(commands::RNG, &[])
.map_err(From::from)
}
pub fn reboot(&self, mode: BootMode) -> Result<(), Error> {
let command = match mode {
BootMode::Firmware => commands::REBOOT,
BootMode::Bootrom => commands::UPDATE,
};
self.device
.vendor_command(command, &[])
.map(|_| ())
.or_else(ignore_closed_connection)
}
pub fn wink(&self) -> Result<(), Error> {
self.device.wink().map_err(From::from)
}
}
fn ignore_closed_connection(error: ctaphid::error::Error) -> Result<(), Error> {
if let ctaphid::error::Error::HidError(_) = error {
Ok(())
} else {
Err(Error::from(error))
}
}
#[derive(Copy, Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub enum BootMode {
Firmware,
Bootrom,
}
pub fn list() -> Result<Devices, Error> {
ctaphid::list().map(Devices::new).map_err(From::from)
}
#[derive(Debug)]
pub enum Error {
CtaphidError(ctaphid::error::Error),
CommandError(CommandError),
}
impl error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::CtaphidError(error) => Some(error),
Self::CommandError(error) => Some(error),
}
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::CtaphidError(_) => "CTAPHID communication failed",
Self::CommandError(_) => "command execution failed",
}
.fmt(f)
}
}
impl From<CommandError> for Error {
fn from(error: CommandError) -> Self {
Self::CommandError(error)
}
}
impl From<ctaphid::error::Error> for Error {
fn from(error: ctaphid::error::Error) -> Self {
Self::CtaphidError(error)
}
}
#[derive(Clone, Copy, Debug)]
pub enum CommandError {
InvalidResponseLength,
}
impl error::Error for CommandError {}
impl fmt::Display for CommandError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidResponseLength => "the response data does not have the expected length",
}
.fmt(f)
}
}