nitrokey3 0.1.1

Client library for Nitrokey 3 devices
Documentation
// Copyright (C) 2021 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 devices = nitrokey3::list()?;
//! println!("Found {} Nitrokey 3 devices", devices.len());
//! for device in &devices {
//!     let device = devices.connect(device)?;
//!     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::{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;
    // const UUID: VendorCommand = VendorCommand::H62;
}

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

    /// Connects to an available Nitrokey 3 device.
    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
    }
}

/// 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 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(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 }
    }
}

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

impl Device {
    fn new(device: ctaphid::Device) -> Self {
        Self { device }
    }

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

    /// Generates random data on the device.
    pub fn rng(&self) -> Result<Vec<u8>, Error> {
        self.device
            .vendor_command(commands::RNG, &[])
            .map_err(From::from)
    }

    /// Reboots the device into the given boot mode.
    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)
    }

    /// Executes the wink command.
    ///
    /// Currently, this does nothing.
    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 {
        // TODO: limit ignored HidErrors?
        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,
}

/// Lists all available Nitrokey 3 devices.
pub fn list() -> Result<Devices, Error> {
    ctaphid::list().map(Devices::new).map_err(From::from)
}

/// 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),
}

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)
    }
}

/// 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)
    }
}