ctaphid-types 0.2.0

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

use core::convert;

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

/// A CTAPHID packet.
///
/// See [ยง 11.2.4 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-message-and-packet-structure
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub enum Packet<T: AsRef<[u8]>> {
    /// A CTAPHID initialization packet.
    Initialization(InitializationPacket<T>),
    /// A CTAPHID continuation packet.
    Continuation(ContinuationPacket<T>),
}

impl<T: AsRef<[u8]>> Packet<T> {
    /// Returns the channel for this packet.
    pub fn channel(&self) -> Channel {
        match self {
            Self::Initialization(packet) => packet.channel,
            Self::Continuation(packet) => packet.channel,
        }
    }

    /// Returns the type of this packet.
    pub fn packet_type(&self) -> PacketType {
        match self {
            Self::Initialization(_) => PacketType::Initialization,
            Self::Continuation(_) => PacketType::Continuation,
        }
    }

    /// Serializes this packet to the given buffer.
    pub fn serialize(&self, buffer: &mut [u8]) -> Result<usize, SerializationError> {
        match self {
            Self::Initialization(packet) => packet.serialize(buffer),
            Self::Continuation(packet) => packet.serialize(buffer),
        }
    }
}

impl<T: AsRef<[u8]>> From<ContinuationPacket<T>> for Packet<T> {
    fn from(packet: ContinuationPacket<T>) -> Self {
        Self::Continuation(packet)
    }
}

impl<T: AsRef<[u8]>> From<InitializationPacket<T>> for Packet<T> {
    fn from(packet: InitializationPacket<T>) -> Self {
        Self::Initialization(packet)
    }
}

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

    fn try_from(data: &'a [u8]) -> Result<Self, Self::Error> {
        if data.len() < 5 {
            return Err(ParseError::NotEnoughData);
        }
        let command_or_sequence = data[4];

        if command_or_sequence.leading_ones() > 0 {
            InitializationPacket::try_from(data).map(From::from)
        } else {
            ContinuationPacket::try_from(data).map(From::from)
        }
    }
}

/// The type of a CTAPHID [`Packet`][].
///
/// See [ยง 11.2.4 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-message-and-packet-structure
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub enum PacketType {
    /// A CTAPHID initialization packet ([`Packet::Initialization`][], [`InitializationPacket`][]).
    Initialization,
    /// A CTAPHID continuation packet ([`Packet::Continuation`][], [`ContinuationPacket`][]).
    Continuation,
}

/// The size of the header of an initialization packet in bytes.
///
/// See [ยง 11.2.4 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-message-and-packet-structure
pub const INITIALIZATION_HEADER_SIZE: usize = 7;

/// A CTAPHID initialization packet.
///
/// See [ยง 11.2.4 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-message-and-packet-structure
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub struct InitializationPacket<T: AsRef<[u8]>> {
    /// The channel this packet is sent or received on.
    pub channel: Channel,
    /// The CTAPHID command.
    pub command: Command,
    /// The length of the payload data.
    pub length: u16,
    /// The payload data.
    pub data: T,
}

impl<T: AsRef<[u8]>> InitializationPacket<T> {
    /// Serializes this packet to the given buffer.
    pub fn serialize(&self, buffer: &mut [u8]) -> Result<usize, SerializationError> {
        let mut serializer = Serializer::new(buffer);
        serializer.push_slice(&self.channel.to_bytes())?;
        serializer.push_slice(&[u8::from(self.command) | 0b1000_0000])?;
        serializer.push_slice(&self.length.to_be_bytes())?;
        serializer.push_slice(self.data.as_ref())?;
        Ok(serializer.bytes_written())
    }
}

impl<T: AsRef<[u8]>> convert::TryFrom<Packet<T>> for InitializationPacket<T> {
    type Error = ParseError;

    fn try_from(packet: Packet<T>) -> Result<Self, Self::Error> {
        if let Packet::Initialization(packet) = packet {
            Ok(packet)
        } else {
            Err(ParseError::InvalidPacketType {
                expected: PacketType::Initialization,
                actual: packet.packet_type(),
            })
        }
    }
}

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

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

        if command.leading_ones() == 0 {
            return Err(ParseError::InvalidPacketType {
                expected: PacketType::Initialization,
                actual: PacketType::Continuation,
            });
        }

        Ok(InitializationPacket {
            channel,
            command: Command::from(command & 0b0111_1111),
            length: u16::from_be_bytes(length),
            data: T::try_from(rest).map_err(|_| ParseError::BufferCreationFailed)?,
        })
    }
}

/// The size of the header of a continuation packet in bytes.
///
/// See [ยง 11.2.4 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-message-and-packet-structure
pub const CONTINUATION_HEADER_SIZE: usize = 5;

/// A CTAPHID continuation packet.
///
/// See [ยง 11.2.4 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-message-and-packet-structure
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub struct ContinuationPacket<T: AsRef<[u8]>> {
    /// The channel this packet is sent or received on.
    pub channel: Channel,
    /// The sequence number of this packet.
    pub sequence: u8,
    /// The payload data.
    pub data: T,
}

impl<T: AsRef<[u8]>> ContinuationPacket<T> {
    /// Serializes this packet to the given buffer.
    pub fn serialize(&self, buffer: &mut [u8]) -> Result<usize, SerializationError> {
        let mut serializer = Serializer::new(buffer);
        serializer.push_slice(&self.channel.to_bytes())?;
        serializer.push_slice(&[self.sequence])?;
        serializer.push_slice(self.data.as_ref())?;
        Ok(serializer.bytes_written())
    }
}

impl<T: AsRef<[u8]>> convert::TryFrom<Packet<T>> for ContinuationPacket<T> {
    type Error = ParseError;

    fn try_from(packet: Packet<T>) -> Result<Self, Self::Error> {
        if let Packet::Continuation(packet) = packet {
            Ok(packet)
        } else {
            Err(ParseError::InvalidPacketType {
                expected: PacketType::Continuation,
                actual: packet.packet_type(),
            })
        }
    }
}

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

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

        if sequence.leading_ones() > 0 {
            return Err(ParseError::InvalidPacketType {
                expected: PacketType::Continuation,
                actual: PacketType::Initialization,
            });
        }

        Ok(ContinuationPacket {
            channel,
            sequence,
            data: T::try_from(rest).map_err(|_| ParseError::BufferCreationFailed)?,
        })
    }
}

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

    use quickcheck::{Arbitrary, TestResult};

    use super::{ContinuationPacket, InitializationPacket, Packet, PacketType};

    impl Arbitrary for Packet<Vec<u8>> {
        fn arbitrary(g: &mut quickcheck::Gen) -> Self {
            match PacketType::arbitrary(g) {
                PacketType::Initialization => Packet::Initialization(Arbitrary::arbitrary(g)),
                PacketType::Continuation => Packet::Continuation(Arbitrary::arbitrary(g)),
            }
        }

        fn shrink(&self) -> Box<dyn Iterator<Item = Self>> {
            match self {
                Packet::Initialization(packet) => Box::new(packet.shrink().map(From::from)),
                Packet::Continuation(packet) => Box::new(packet.shrink().map(From::from)),
            }
        }
    }

    impl Arbitrary for PacketType {
        fn arbitrary(g: &mut quickcheck::Gen) -> Self {
            *g.choose(&[PacketType::Initialization, PacketType::Continuation])
                .unwrap()
        }
    }

    impl Arbitrary for InitializationPacket<Vec<u8>> {
        fn arbitrary(g: &mut quickcheck::Gen) -> Self {
            Self {
                channel: Arbitrary::arbitrary(g),
                command: Arbitrary::arbitrary(g),
                length: Arbitrary::arbitrary(g),
                data: Arbitrary::arbitrary(g),
            }
        }

        fn shrink(&self) -> Box<dyn Iterator<Item = Self>> {
            let channel = self.channel;
            let command = self.command;
            let length = self.length;
            Box::new(self.data.shrink().map(move |data| Self {
                channel,
                command,
                length,
                data,
            }))
        }
    }

    impl Arbitrary for ContinuationPacket<Vec<u8>> {
        fn arbitrary(g: &mut quickcheck::Gen) -> Self {
            Self {
                channel: Arbitrary::arbitrary(g),
                sequence: Arbitrary::arbitrary(g),
                data: Arbitrary::arbitrary(g),
            }
        }

        fn shrink(&self) -> Box<dyn Iterator<Item = Self>> {
            let channel = self.channel;
            let sequence = self.sequence;
            Box::new(self.data.shrink().map(move |data| Self {
                channel,
                sequence,
                data,
            }))
        }
    }

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

    quickcheck::quickcheck! {
        fn serialize_init(packet: InitializationPacket<Vec<u8>>) -> bool {
            let mut buffer = vec![0; packet.data.len() + 10];
            let _ = packet.serialize(&mut buffer);
            true
        }
    }

    quickcheck::quickcheck! {
        fn serialize_parse_init(packet: InitializationPacket<Vec<u8>>) -> TestResult {
            if usize::from(packet.length) < packet.data.len() {
                return TestResult::discard();
            }
            if u8::from(packet.command).leading_ones() > 0 {
                return TestResult::discard();
            }
            let mut buffer = vec![0; packet.data.len() + 10];
            let n = packet.serialize(&mut buffer).unwrap();
            let parsed = InitializationPacket::try_from(&buffer[..n]).unwrap();
            TestResult::from_bool(packet == parsed)
        }
    }

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

    quickcheck::quickcheck! {
        fn serialize_cont(packet: ContinuationPacket<Vec<u8>>) -> bool {
            let mut buffer = vec![0; packet.data.len() + 10];
            let _ = packet.serialize(&mut buffer);
            true
        }
    }

    quickcheck::quickcheck! {
        fn serialize_parse_cont(packet: ContinuationPacket<Vec<u8>>) -> TestResult {
            if u8::from(packet.sequence).leading_ones() > 0 {
                return TestResult::discard();
            }
            let mut buffer = vec![0; packet.data.len() + 10];
            let n = packet.serialize(&mut buffer).unwrap();
            let parsed = ContinuationPacket::try_from(&buffer[..n]).unwrap();
            TestResult::from_bool(packet == parsed)
        }
    }
}