ctaphid-types 0.2.0

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

use core::convert::{TryFrom, TryInto};

use crate::{
    channel::Channel,
    error::{ParseError, SerializationError},
    util::{Parser, Serializer},
};

/// The response of the CTAPHID INIT command.
///
/// See [§ 11.2.9.1.3 of the CTAP specification][spec].
///
/// [spec]: https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-20210615.html#usb-hid-init
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub struct InitResponse<T: AsRef<[u8]>> {
    /// The nonce as sent in the init request.
    pub nonce: [u8; 8],
    /// The channel allocated for this session.
    pub channel: Channel,
    /// The protocol version implemented by the device.
    pub protocol_version: u8,
    /// The version of the device.
    pub device_version: DeviceVersion,
    /// The capabilities of the device.
    pub capabilities: Capabilities,
    /// Additional flags that are not supported by this library.
    pub rest: T,
}

impl<T: AsRef<[u8]>> InitResponse<T> {
    /// Serializes this response to the given buffer.
    pub fn serialize(&self, buffer: &mut [u8]) -> Result<usize, SerializationError> {
        let mut serializer = Serializer::new(buffer);
        serializer.push_slice(&self.nonce)?;
        serializer.push_slice(&self.channel.to_bytes())?;
        serializer.push_slice(&[self.protocol_version])?;
        serializer.push_slice(&self.device_version.to_bytes())?;
        serializer.push_slice(&[self.capabilities.bits()])?;
        serializer.push_slice(self.rest.as_ref())?;
        Ok(serializer.bytes_written())
    }
}

impl<'a, T: AsRef<[u8]> + TryFrom<&'a [u8]>> TryFrom<&'a [u8]> for InitResponse<T> {
    type Error = ParseError;

    fn try_from(data: &'a [u8]) -> Result<Self, Self::Error> {
        let mut parser = Parser::new(data);
        let nonce = parser.take_array()?;
        let channel = parser.take_into()?;
        let protocol_version = parser.take()?;
        let device_version = parser.take_into()?;
        let capabilities = parser.take()?;
        let rest = parser.into_rest();

        Ok(Self {
            nonce,
            channel,
            protocol_version,
            device_version,
            capabilities: Capabilities::from_bits_truncate(capabilities),
            rest: rest
                .try_into()
                .map_err(|_| ParseError::BufferCreationFailed)?,
        })
    }
}

/// The version of a CTAPHID device.
///
/// See [§ 11.2.9.1.3 of the CTAP specification][spec].
///
/// [spec]: https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-20210615.html#usb-hid-init
#[derive(Clone, Copy, Debug, Default, Eq, Ord, PartialEq, PartialOrd)]
pub struct DeviceVersion {
    /// The major device version.
    pub major: u8,
    /// The minor device version.
    pub minor: u8,
    /// The build device version.
    pub build: u8,
}

impl DeviceVersion {
    /// Encodes the device version as a byte array.
    pub fn to_bytes(self) -> [u8; 3] {
        self.into()
    }
}

impl From<DeviceVersion> for [u8; 3] {
    fn from(version: DeviceVersion) -> Self {
        [version.major, version.minor, version.build]
    }
}

impl From<&[u8; 3]> for DeviceVersion {
    fn from(data: &[u8; 3]) -> Self {
        Self {
            major: data[0],
            minor: data[1],
            build: data[2],
        }
    }
}

bitflags::bitflags! {
    /// The capabilities of a CTAPHID device.
    ///
    /// See [§ 11.2.9.1.3 of the CTAP specification][spec].
    ///
    /// [spec]: https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-20210615.html#usb-hid-init
    #[derive(Default)]
    pub struct Capabilities: u8 {
        /// Indicates that the device supports the wink command.
        const WINK = 0x01;
        /// Indicates that the device supports the CBOR command.
        const CBOR = 0x04;
        /// Indicates that the device does not support the MSG command.
        const NMSG = 0x08;
    }
}

impl Capabilities {
    /// Returns true if the device supports the wink command.
    pub fn has_wink(&self) -> bool {
        self.contains(Self::WINK)
    }

    /// Returns true if the device supports the CBOR command.
    pub fn has_cbor(&self) -> bool {
        self.contains(Self::CBOR)
    }

    /// Returns true if the device supports the MSG command.
    pub fn has_msg(&self) -> bool {
        !self.contains(Self::NMSG)
    }
}

#[cfg(test)]
mod tests {
    use std::convert::TryFrom as _;

    use quickcheck::{Arbitrary, TestResult};

    use super::{Capabilities, DeviceVersion, InitResponse};

    impl Arbitrary for Capabilities {
        fn arbitrary(g: &mut quickcheck::Gen) -> Self {
            Self::from_bits_truncate(Arbitrary::arbitrary(g))
        }
    }

    impl Arbitrary for DeviceVersion {
        fn arbitrary(g: &mut quickcheck::Gen) -> Self {
            Self {
                major: Arbitrary::arbitrary(g),
                minor: Arbitrary::arbitrary(g),
                build: Arbitrary::arbitrary(g),
            }
        }
    }

    impl Arbitrary for InitResponse<Vec<u8>> {
        fn arbitrary(g: &mut quickcheck::Gen) -> Self {
            Self {
                nonce: [
                    Arbitrary::arbitrary(g),
                    Arbitrary::arbitrary(g),
                    Arbitrary::arbitrary(g),
                    Arbitrary::arbitrary(g),
                    Arbitrary::arbitrary(g),
                    Arbitrary::arbitrary(g),
                    Arbitrary::arbitrary(g),
                    Arbitrary::arbitrary(g),
                ],
                channel: Arbitrary::arbitrary(g),
                protocol_version: Arbitrary::arbitrary(g),
                device_version: Arbitrary::arbitrary(g),
                capabilities: Arbitrary::arbitrary(g),
                rest: Arbitrary::arbitrary(g),
            }
        }

        fn shrink(&self) -> Box<dyn Iterator<Item = Self>> {
            let nonce = self.nonce;
            let channel = self.channel;
            let protocol_version = self.protocol_version;
            let device_version = self.device_version;
            let capabilities = self.capabilities;
            Box::new(self.rest.shrink().map(move |rest| Self {
                nonce,
                channel,
                protocol_version,
                device_version,
                capabilities,
                rest,
            }))
        }
    }

    quickcheck::quickcheck! {
        fn parse_init(data: Vec<u8>) -> bool {
            let _ = InitResponse::<Vec<u8>>::try_from(data.as_slice());
            true
        }
    }

    quickcheck::quickcheck! {
        fn serialize_init(init: InitResponse<Vec<u8>>) -> bool {
            let mut buffer = vec![0; init.rest.len() + 17];
            let _ = init.serialize(&mut buffer);
            true
        }
    }

    quickcheck::quickcheck! {
        fn serialize_parse_init(init: InitResponse<Vec<u8>>) -> TestResult {
            let mut buffer = vec![0; init.rest.len() + 17];
            let n = init.serialize(&mut buffer).unwrap();
            let parsed = InitResponse::try_from(&buffer[..n]).unwrap();
            TestResult::from_bool(init == parsed)
        }
    }
}