#![warn(
missing_copy_implementations,
missing_debug_implementations,
missing_docs,
non_ascii_idents,
trivial_casts,
unused,
unused_qualifications
)]
#![deny(unsafe_code)]
use std::{
borrow,
convert::{TryFrom, TryInto},
error, fmt, ops,
};
use tap::TapFallible as _;
const VID_NITROKEY_3: u16 = 0x20a0;
const PID_NITROKEY_3: u16 = 0x42b2;
mod commands {
use ctaphid::types::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;
pub const UUID: VendorCommand = VendorCommand::H62;
}
#[derive(Clone, Debug)]
pub struct Devices<'a> {
device_infos: Vec<DeviceInfo<'a>>,
}
impl<'a> Devices<'a> {
fn new(hidapi: &'a hidapi::HidApi) -> Self {
let device_infos = hidapi
.device_list()
.inspect(|device| {
log::trace!(
"Found HID device {:#06x}:{:#06x} at {}",
device.vendor_id(),
device.product_id(),
String::from_utf8_lossy(device.path().to_bytes()),
)
})
.filter(|device| device.vendor_id() == VID_NITROKEY_3)
.filter(|device| device.product_id() == PID_NITROKEY_3)
.map(|device_info| DeviceInfo {
hidapi,
device_info,
})
.inspect(|device| log::debug!("Found Nitrokey 3 at {}", device.path()))
.collect();
Devices { device_infos }
}
}
impl<'a> IntoIterator for Devices<'a> {
type Item = DeviceInfo<'a>;
type IntoIter = std::vec::IntoIter<DeviceInfo<'a>>;
fn into_iter(self) -> Self::IntoIter {
self.device_infos.into_iter()
}
}
impl<'a> ops::Deref for Devices<'a> {
type Target = [DeviceInfo<'a>];
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 Version {
pub fn new(major: u16, minor: u16, patch: u8) -> Self {
Self {
major,
minor,
patch,
}
}
}
impl From<[u8; 4]> for Version {
fn from(version: [u8; 4]) -> Self {
u32::from_be_bytes(version).into()
}
}
impl From<u32> for Version {
fn from(version: u32) -> Self {
let major = (version >> 22) as u16;
let minor = ((version >> 6) & 0b1111_1111_1111_1111) as u16;
let patch = (version & 0b11_1111) as u8;
Self {
major,
minor,
patch,
}
}
}
impl TryFrom<Vec<u8>> for Version {
type Error = CommandError;
fn try_from(version: Vec<u8>) -> Result<Self, Self::Error> {
<[u8; 4]>::try_from(version)
.map(From::from)
.map_err(|_| CommandError::InvalidResponseLength)
}
}
impl fmt::Display for Version {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "v{}.{}.{}", self.major, self.minor, self.patch)
}
}
#[derive(Copy, Clone)]
pub struct DeviceInfo<'a> {
hidapi: &'a hidapi::HidApi,
device_info: &'a hidapi::DeviceInfo,
}
impl<'a> DeviceInfo<'a> {
pub fn connect(&self) -> Result<Device<hidapi::HidDevice>, Error> {
log::info!("Connecting to Nitrokey 3 device at {}", self.path());
let device = self.device_info.open_device(self.hidapi)?;
let device = ctaphid::Device::new(device, self.device_info.to_owned())?;
Ok(Device::new(device))
}
pub fn path(&self) -> borrow::Cow<'_, str> {
ctaphid::HidDeviceInfo::path(self.device_info)
}
}
impl<'a> AsRef<hidapi::DeviceInfo> for DeviceInfo<'a> {
fn as_ref(&self) -> &hidapi::DeviceInfo {
self.device_info
}
}
impl<'a> fmt::Debug for DeviceInfo<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("DeviceInfo")
.field("device_info", &self.device_info)
.finish()
}
}
#[derive(Debug)]
pub struct Device<H: ctaphid::HidDevice> {
device: ctaphid::Device<H>,
}
impl<H: ctaphid::HidDevice> Device<H> {
pub fn new(device: ctaphid::Device<H>) -> Self {
use ctaphid::HidDeviceInfo as _;
log::debug!(
"Created new Nitrokey 3 device with path {}",
device.info().path()
);
Self { device }
}
pub fn path(&self) -> borrow::Cow<'_, str> {
use ctaphid::HidDeviceInfo as _;
self.device.info().path()
}
pub fn firmware_version(&self) -> Result<Version, Error> {
log::info!("{}: Querying firmware version", self.path());
self.device
.vendor_command(commands::VERSION, &[])?
.try_into()
.tap_ok(|version| log::info!("{}: Received firmware version {}", self.path(), version))
.tap_err(|err| log::warn!("{}: Failed to parse firmware version: {}", self.path(), err))
.map_err(From::from)
}
pub fn uuid(&self) -> Result<Option<Uuid>, Error> {
log::info!("{}: Querying uuid", self.path());
let response = self.device.vendor_command(commands::UUID, &[])?;
if response.is_empty() {
log::info!("{}: uuid command not supported", self.path());
Ok(None)
} else {
response
.try_into()
.tap_ok(|uuid| log::info!("{}: Received uuid {}", self.path(), uuid))
.tap_err(|err| log::warn!("{}: Failed to parse uuid: {}", self.path(), err))
.map(Some)
.map_err(From::from)
}
}
pub fn rng(&self) -> Result<Vec<u8>, Error> {
log::info!("{}: Generating random data", self.path());
self.device
.vendor_command(commands::RNG, &[])
.tap_err(|err| log::warn!("{}: Failed to generate random data: {}", self.path(), err))
.map_err(From::from)
}
pub fn reboot(&self, mode: BootMode) -> Result<(), Error> {
log::info!("{}: Reboot to {mode:?}", self.path());
let command = match mode {
BootMode::Firmware => commands::REBOOT,
BootMode::Bootrom => commands::UPDATE,
};
self.device
.vendor_command(command, &[])
.map(|_| ())
.or_else(ignore_closed_connection)
.tap_err(|err| log::warn!("{}: Failed to reboot: {}", self.path(), err))
}
pub fn wink(&self) -> Result<(), Error> {
log::info!("{}: Executing wink command", self.path());
self.device
.wink()
.map_err(From::from)
.tap_err(|err| log::warn!("{}: Failed to execute wink command: {}", self.path(), err))
}
}
fn ignore_closed_connection(error: ctaphid::error::Error) -> Result<(), Error> {
use ctaphid::error::{Error as CtaphidError, ResponseError};
if let CtaphidError::ResponseError(ResponseError::PacketReceivingFailed(_)) = error {
log::debug!("Ignoring error due to closed connection: {}", error);
Ok(())
} else {
Err(Error::from(error))
}
}
#[derive(Copy, Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub enum BootMode {
Firmware,
Bootrom,
}
#[derive(Copy, Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub struct Uuid(u128);
impl fmt::Display for Uuid {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::UpperHex::fmt(self, f)
}
}
impl fmt::LowerHex for Uuid {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:032x}", self.0)
}
}
impl fmt::UpperHex for Uuid {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:032X}", self.0)
}
}
impl From<[u8; 16]> for Uuid {
fn from(uuid: [u8; 16]) -> Self {
u128::from_be_bytes(uuid).into()
}
}
impl From<u128> for Uuid {
fn from(uuid: u128) -> Self {
Self(uuid)
}
}
impl From<Uuid> for u128 {
fn from(uuid: Uuid) -> Self {
uuid.0
}
}
impl TryFrom<Vec<u8>> for Uuid {
type Error = CommandError;
fn try_from(uuid: Vec<u8>) -> Result<Self, Self::Error> {
<[u8; 16]>::try_from(uuid)
.map(From::from)
.map_err(|_| CommandError::InvalidResponseLength)
}
}
pub fn list(hidapi: &hidapi::HidApi) -> Devices<'_> {
Devices::new(hidapi)
}
#[derive(Debug)]
pub enum Error {
CtaphidError(ctaphid::error::Error),
CommandError(CommandError),
HidapiError(hidapi::HidError),
}
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),
Self::HidapiError(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",
Self::HidapiError(_) => "hidapi connection 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)
}
}
impl From<hidapi::HidError> for Error {
fn from(error: hidapi::HidError) -> Self {
Self::HidapiError(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)
}
}