ctaphid 0.2.0

Rust implementation of the CTAPHID protocol
Documentation
// Copyright (C) 2021-2022 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<(), Box<dyn std::error::Error>> {
//! let hidapi = hidapi::HidApi::new()?;
//! let devices = hidapi.device_list().filter(|device| ctaphid::is_known_device(*device));
//! for device_info in devices {
//!     let hid_device = device_info.open_device(&hidapi)?;
//!     let device = ctaphid::Device::new(hid_device, device_info.to_owned())?;
//!     print!(
//!         "Trying to ping CTAPHID device 0x{:x}:0x{:x} ...",
//!         device_info.vendor_id(),
//!         device_info.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 error;

mod buffer;
mod hid;

pub use hid::{Device as HidDevice, DeviceInfo as HidDeviceInfo};

use std::{convert::TryFrom, fmt, time::Duration};

use ctaphid_types::{
    Capabilities, Channel, Command, DeviceVersion, InitResponse, Message, ParseError, VendorCommand,
};
use rand_core::{OsRng, RngCore};

use error::{CommandError, Error, RequestError, ResponseError};

/// A connected CTAPHID device.
pub struct Device<D: HidDevice> {
    device: D,
    device_info: D::Info,
    channel: Channel,
    protocol_version: u8,
    device_version: DeviceVersion,
    capabilities: Capabilities,
    buffer: buffer::MessageBuffer,
    timeout: Option<Duration>,
}

impl<D: HidDevice> Device<D> {
    /// Connects to a HID device, assuming that it is a CTAPHID device, and initializes the
    /// communication channel using the default [`Options`][].
    pub fn new(device: D, device_info: D::Info) -> Result<Self, Error> {
        Self::with_options(device, device_info, Default::default())
    }

    /// Connects to a HID device, assuming that it is a CTAPHID device, and initializes the
    /// communication channel using the given options.
    pub fn with_options(device: D, device_info: D::Info, options: Options) -> Result<Self, Error> {
        let buffer = buffer::MessageBuffer::new(device_info.packet_size());
        let mut device = Self {
            device,
            device_info,
            channel: Channel::BROADCAST,
            protocol_version: Default::default(),
            device_version: Default::default(),
            capabilities: Default::default(),
            buffer,
            timeout: options.timeout,
        };

        let response = device.init(options.nonce)?;
        device.channel = response.channel;
        device.protocol_version = response.protocol_version;
        device.device_version = response.device_version;
        device.capabilities = response.capabilities;

        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> {
        if !self.capabilities.has_wink() {
            return Err(CommandError::NotSupported(Command::Wink).into());
        }
        let response = self.transaction(Command::Wink, &[])?;
        if response.is_empty() {
            Ok(())
        } else {
            Err(ResponseError::UnexpectedResponseData(response).into())
        }
    }

    /// Sends the given CTAP1/U2F command to the device and returns the response.
    pub fn ctap1(&self, data: &[u8]) -> Result<Vec<u8>, Error> {
        // TODO: investigate spec/impl differences
        if !self.capabilities.has_msg() {
            return Err(CommandError::NotSupported(Command::Message).into());
        }
        self.transaction(Command::Message, data)
    }

    /// Sends the given CTAP2/CBOR command and data to the device and returns the response status
    /// and data.
    pub fn ctap2(&self, command: u8, data: &[u8]) -> Result<Vec<u8>, Error> {
        if !self.capabilities.has_cbor() {
            return Err(CommandError::NotSupported(Command::Cbor).into());
        }
        let data = &[&[command], data].concat();
        let response = self.transaction(Command::Cbor, data)?;
        if response.is_empty() {
            Err(ResponseError::PacketParsingFailed(ParseError::NotEnoughData).into())
        } else if response[0] != 0 {
            Err(CommandError::CborError(response[0]).into())
        } else {
            Ok(response[1..].to_owned())
        }
    }

    /// Locks the device for the given duration to the current channel.
    ///
    /// The duration is truncated to whole seconds and capped at ten seconds.
    pub fn lock(&self, duration: Duration) -> Result<(), Error> {
        let mut duration = u8::try_from(duration.as_secs()).unwrap_or(u8::MAX);
        if duration > 10 {
            duration = 10;
        }
        let response = self.transaction(Command::Lock, &[duration])?;
        if response.is_empty() {
            Ok(())
        } else {
            Err(ResponseError::UnexpectedResponseData(response).into())
        }
    }

    /// 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<InitResponse<Vec<u8>>, Error> {
        self.send_message(Command::Init, &nonce)?;
        loop {
            let response = self.receive_message(Command::Init)?;
            let response = InitResponse::try_from(response.as_slice())
                .map_err(ResponseError::PacketParsingFailed)?;
            if nonce == response.nonce {
                return Ok(response);
            }
        }
    }

    fn transaction(&self, command: Command, data: &[u8]) -> Result<Vec<u8>, Error> {
        self.send_message(command, data)?;
        let response = self.receive_message(command)?;
        Ok(response)
    }

    fn send_message(&self, command: Command, data: &[u8]) -> Result<(), RequestError> {
        let message = Message {
            channel: self.channel,
            command,
            data,
        };
        self.buffer.send_message(&self.device, message)
    }

    fn receive_message(&self, command: Command) -> Result<Vec<u8>, ResponseError> {
        self.buffer
            .receive_message(&self.device, self.channel, command, self.timeout)
            .map(|message| message.data)
    }

    /// Returns information about this device.
    pub fn info(&self) -> &D::Info {
        &self.device_info
    }

    /// Returns the protocol version implemented by this device.
    pub fn protocol_version(&self) -> u8 {
        self.protocol_version
    }

    /// Returns the version of this device.
    pub fn device_version(&self) -> DeviceVersion {
        self.device_version
    }

    /// Returns the capabilities of this device.
    pub fn capabilities(&self) -> Capabilities {
        self.capabilities
    }
}

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

/// Connection options for a CTAPHID device.
#[derive(Clone, Debug)]
#[non_exhaustive]
#[allow(missing_copy_implementations)]
pub struct Options {
    /// The nonce for the initilization of the communication channel.
    ///
    /// Per default, this is generated using [`rand_core::OsRng`][].
    pub nonce: [u8; 8],

    /// The timeout for receiving a packet from the device.
    ///
    /// Per default, this is set to one second.
    pub timeout: Option<Duration>,
}

impl Options {
    /// Creates an `Options` instance with the default settings.
    pub fn new() -> Self {
        Self::default()
    }

    /// Creates an `Options` instance with the default settings and using the given RNG to generate
    /// the nonce.
    pub fn with_rng(rng: &mut dyn RngCore) -> Self {
        let mut nonce = [0; 8];
        rng.fill_bytes(&mut nonce);
        Self {
            nonce,
            timeout: Some(Duration::from_secs(1)),
        }
    }
}

impl Default for Options {
    fn default() -> Self {
        Self::with_rng(&mut OsRng)
    }
}

/// Checks whether the given device is a known CTAPHID device using its vendor and product ID.
///
/// Typically, CTAPHID devices can be identified by the usage page of the HID descriptor.
/// Unfortunately, hidapi does not reliably parse the HID descriptor ([issue][]).  VID/PID pairs
/// can be used as a fallback.
///
/// Currently, these devices are recognized:
/// - Nitrokey 3 and FIDO 2
/// - Solokeys Solo 2
///
/// Please submit a patch if you want to add a device to this list.
///
/// [issue]: https://github.com/signal11/hidapi/issues/385
pub fn is_known_device<D: HidDeviceInfo>(device: &D) -> bool {
    matches!(
        (device.vendor_id(), device.product_id()),
        // Solokeys Solo 2
        (0x1209, 0xbeee) |
        // Nitrokey FIDO2
        (0x20a0, 0x42b1) |
        // Nitrokey 3
        (0x20a0, 0x42b2)
    )
}