nitrokey3 0.3.0

Client library for Nitrokey 3 devices
Documentation
// Copyright (C) 2021-2022 Robin Krahl <robin.krahl@ireas.org>
// SPDX-License-Identifier: Apache-2.0 or MIT

//! Client library for Nitrokey 3 devices.
//!
//! # Quickstart
//!
//! ```no_run
//! # fn try_main() -> Result<(), nitrokey3::Error> {
//! let hidapi = hidapi::HidApi::new()?;
//! let devices = nitrokey3::list(&hidapi);
//! println!("Found {} Nitrokey 3 devices", devices.len());
//! for device in devices {
//!     let device = device.connect()?;
//!     println!("- Nitrokey 3 with firmware version {}", device.firmware_version()?);
//! }
//! #     Ok(())
//! # }
//! ```

#![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;
}

/// A collection of available Nitrokey 3 devices.
#[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
    }
}

/// A firmware version number.
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub struct Version {
    /// The major part of the version number.
    pub major: u16,
    /// The minor part of the version number.
    pub minor: u16,
    /// The patch part of the version number.
    pub patch: u8,
}

impl Version {
    /// Creates a new version with the given components.
    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)
    }
}

/// An available Nitrokey 3 device.
#[derive(Copy, Clone)]
pub struct DeviceInfo<'a> {
    hidapi: &'a hidapi::HidApi,
    device_info: &'a hidapi::DeviceInfo,
}

impl<'a> DeviceInfo<'a> {
    /// Connects to this Nitrokey 3 device.
    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))
    }

    /// Returns the HID path of this 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()
    }
}

/// A connected Nitrokey 3 device.
#[derive(Debug)]
pub struct Device<H: ctaphid::HidDevice> {
    device: ctaphid::Device<H>,
}

impl<H: ctaphid::HidDevice> Device<H> {
    /// Creates a new Nitrokey 3 instance from the given [`ctaphid::Device`][], assuming that it is
    /// a Nitrokey 3 device.
    ///
    /// Use [`list`][] to get a list of all available Nitrokey 3 devices and
    /// [`DeviceInfo::connect`][] to connect to one of them.
    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 }
    }

    /// Returns the HID path of this device.
    pub fn path(&self) -> borrow::Cow<'_, str> {
        use ctaphid::HidDeviceInfo as _;
        self.device.info().path()
    }

    /// Queries the firmware version of the device.
    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)
    }

    /// Queries the UUID of the device.
    ///
    /// This command requires the firmware version v1.0.1.  For older firmware versions, `None` is
    /// returned.
    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)
        }
    }

    /// Generates random data on the device.
    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)
    }

    /// Reboots the device into the given boot mode.
    ///
    /// This command has to be confirmed by the user by pressing the touch button.
    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))
    }

    /// Executes the wink command.
    ///
    /// For devices with firmware v1.0.1 or newer, this causes the LED to blink.  For older
    /// firmware versions, this does nothing.
    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);
        // TODO: limit ignored errors?
        Ok(())
    } else {
        Err(Error::from(error))
    }
}

/// The boot mode for a reboot command.
#[derive(Copy, Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub enum BootMode {
    /// Boot into the Nitrokey 3 firmware.
    Firmware,
    /// Boot into the bootrom that can be used to apply firmware updates.
    Bootrom,
}

/// The UUID for a Nitrokey 3 device.
#[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)
    }
}

/// Lists all available Nitrokey 3 devices.
pub fn list(hidapi: &hidapi::HidApi) -> Devices<'_> {
    Devices::new(hidapi)
}

/// Error type for Nitrokey 3 operations.
#[derive(Debug)]
pub enum Error {
    /// An error that occured during the CTAPHID communication.
    CtaphidError(ctaphid::error::Error),
    /// A command-specific error.
    CommandError(CommandError),
    /// An error that occured during the hidapi device connection.
    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)
    }
}

/// A command-specific error.
#[derive(Clone, Copy, Debug)]
pub enum CommandError {
    /// The response data does not have the expected length.
    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)
    }
}