ctaphid 0.1.1

Rust implementation of the CTAPHID protocol
Documentation
// Copyright (C) 2021 Robin Krahl <robin.krahl@ireas.org>
// SPDX-License-Identifier: Apache-2.0 or MIT

//! Communicate with devices implementing the CTAPHID protocol.
//!
//! This implementation is based on the [FIDO Client to Authenticator Protocol (CTAP)][spec]
//! specification (version of June 15, 2021), section 11.2.
//!
//! # Quickstart
//!
//! ```no_run
//! # fn try_main() -> Result<(), ctaphid::error::Error> {
//! let devices = ctaphid::list()?;
//! for device in &devices {
//!     let device = devices.connect(device)?;
//!     print!(
//!         "Trying to ping CTAPHID device 0x{:x}:0x{:x} ...",
//!         device.vendor_id(),
//!         device.product_id(),
//!     );
//!     device.ping(&[0xde, 0xad, 0xbe, 0xef])?;
//!     println!("done");
//! }
//! #     Ok(())
//! # }
//! ```
//!
//! [spec]: https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-20210615.html

#![warn(
    missing_copy_implementations,
    missing_debug_implementations,
    missing_docs,
    non_ascii_idents,
    trivial_casts,
    unused,
    unused_qualifications
)]
#![deny(unsafe_code)]

pub mod command;
pub mod error;

mod hid;
mod message;
mod packet;
mod transaction;

use std::{fmt, ops};

use command::{Command, VendorCommand};
use error::{CommandError, Error};

/// A collection of available CTAPHID devices.
pub struct Devices {
    hidapi: hidapi::HidApi,
    devices: Vec<DeviceInfo>,
}

impl Devices {
    /// Connects to an available CTAPHID device.
    pub fn connect(&self, device_info: &DeviceInfo) -> Result<Device, Error> {
        Device::connect(&self.hidapi, &device_info.hid_device_info)
    }
}

impl<'a> IntoIterator for &'a Devices {
    type Item = &'a DeviceInfo;
    type IntoIter = std::slice::Iter<'a, DeviceInfo>;

    fn into_iter(self) -> Self::IntoIter {
        self.devices.iter()
    }
}

impl fmt::Debug for Devices {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("Devices")
            .field("devices", &self.devices)
            .finish()
    }
}

impl ops::Deref for Devices {
    type Target = [DeviceInfo];

    fn deref(&self) -> &Self::Target {
        &self.devices
    }
}

/// An available CTAPHID device.
#[derive(Clone)]
pub struct DeviceInfo {
    hid_device_info: hidapi::DeviceInfo,
}

impl DeviceInfo {
    // TODO: check which other fields we may want to expose

    /// Returns the vendor ID of this device.
    pub fn vendor_id(&self) -> u16 {
        self.hid_device_info.vendor_id()
    }

    /// Returns the product ID of this device.
    pub fn product_id(&self) -> u16 {
        self.hid_device_info.product_id()
    }
}

impl fmt::Debug for DeviceInfo {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("Device")
            .field("vendor_id", &self.vendor_id())
            .field("product_id", &self.product_id())
            .finish()
    }
}

/// A connected CTAPHID device.
pub struct Device {
    device_info: DeviceInfo,
    device: hidapi::HidDevice,
    channel: transaction::Channel,
}

impl Device {
    /// Connects to a HID device, assuming that it is a CTAPHID device.
    ///
    /// Generally, it is recommended to use [`list`][] instead to list all available CTAPHID
    /// devices and then connect to one of them using [`Devices::connect`][].
    pub fn connect(
        hidapi: &hidapi::HidApi,
        device_info: &hidapi::DeviceInfo,
    ) -> Result<Self, Error> {
        let device = device_info.open_device(hidapi)?;
        Device::new(device_info.to_owned(), device)
    }

    fn new(device_info: hidapi::DeviceInfo, device: hidapi::HidDevice) -> Result<Self, Error> {
        let device_info = DeviceInfo {
            hid_device_info: device_info,
        };
        let mut device = Self {
            device_info,
            device,
            channel: transaction::Channel::BROADCAST,
        };
        // TODO: Use random nonce
        device.channel = device.init([1, 2, 3, 4, 5, 6, 7, 8])?;
        Ok(device)
    }

    /// Pings the device sending the given data and checks that it sends the same data back.
    ///
    /// If the data returned by the device does not match the sent data,
    /// [`CommandError::InvalidPingData`][] is returned.
    pub fn ping(&self, data: &[u8]) -> Result<(), Error> {
        let response = self.transaction(Command::Ping, data)?;
        if data == response {
            Ok(())
        } else {
            Err(Error::from(CommandError::InvalidPingData))
        }
    }

    /// Executes the wink command, causing a vendor-defined action that provides some visual or
    /// audible identification of a particular authenticator.
    pub fn wink(&self) -> Result<(), Error> {
        let response = self.transaction(Command::Wink, &[])?;
        if response.is_empty() {
            Ok(())
        } else {
            unimplemented!()
        }
    }

    /// Executes the given vendor-specific command with the given data.
    pub fn vendor_command(&self, command: VendorCommand, data: &[u8]) -> Result<Vec<u8>, Error> {
        self.transaction(Command::Vendor(command), data)
    }

    fn init(&self, nonce: [u8; 8]) -> Result<transaction::Channel, Error> {
        // TODO: Should we wait until we have a matching nonce?
        // TODO: Read entire response
        let response = self.transaction(Command::Init, &nonce)?;
        if nonce == response[..8] {
            Ok(transaction::Channel::from([
                response[8],
                response[9],
                response[10],
                response[11],
            ]))
        } else {
            Err(Error::from(CommandError::InvalidNonce))
        }
    }

    fn transaction(&self, command: Command, data: &[u8]) -> Result<Vec<u8>, Error> {
        transaction::exec(&self.device, self.channel, command, data)
    }
}

impl fmt::Debug for Device {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("Device")
            .field("vendor_id", &self.device_info.vendor_id())
            .field("product_id", &self.device_info.product_id())
            .field("channel", &self.channel)
            .finish()
    }
}

impl ops::Deref for Device {
    type Target = DeviceInfo;

    fn deref(&self) -> &Self::Target {
        &self.device_info
    }
}

/// Lists all available CTAPHID devices.
///
/// Note that this function currently only detects Nitrokey FIDO2 and Nitrokey 3 devices as reading
/// the usage pages that identify CTAPHID devices currently does not work reliably using hidapi.
/// If you want to connect to other CTAPHID devices, manually identify the corresponding
/// [`hidapi::DeviceInfo`][] and use [`Device::connect`][] to connect to the device.
pub fn list() -> Result<Devices, Error> {
    // TODO: check usage page == 0xf1d0 and usage == 0x1
    let hidapi = hidapi::HidApi::new()?;
    let devices = hidapi
        .device_list()
        .filter(|device| device.vendor_id() == 0x20a0)
        .filter(|device| device.product_id() == 0x42b1 || device.product_id() == 0x42b2)
        .map(|device| DeviceInfo {
            hid_device_info: device.to_owned(),
        })
        .collect();
    Ok(Devices { hidapi, devices })
}