Skip to main content

ctaphid_types/
message.rs

1// Copyright (C) 2021 Robin Krahl <robin.krahl@ireas.org>
2// SPDX-License-Identifier: Apache-2.0 or MIT
3
4use core::cmp;
5
6use crate::{
7    channel::Channel,
8    command::Command,
9    error::{DefragmentationError, FragmentationError},
10    packet::{
11        ContinuationPacket, InitializationPacket, Packet, PacketType, CONTINUATION_HEADER_SIZE,
12        INITIALIZATION_HEADER_SIZE,
13    },
14};
15
16/// A CTAPHID message.
17///
18/// See [§ 11.2.2 of the CTAP specification][spec].
19///
20/// [spec]: https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-20210615.html#usb-protocol-and-framing
21#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
22pub struct Message<T: AsRef<[u8]>> {
23    /// The channel this message is sent or received on.
24    pub channel: Channel,
25    /// The CTAPHID command.
26    pub command: Command,
27    /// The message payload.
28    pub data: T,
29}
30
31impl<T: AsRef<[u8]>> Message<T> {
32    /// Fragments this message into CTAPHID packets and returns an iterator over the packets.
33    ///
34    /// See [§ 11.2.4 of the CTAP specification][spec].
35    ///
36    /// [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
37    pub fn fragments(&self, packet_size: usize) -> Result<Fragments<'_>, FragmentationError> {
38        Fragments::new(self, packet_size)
39    }
40}
41
42impl<T: AsRef<[u8]> + Default + Extend<u8>> Message<T> {
43    /// Assembles a CTAPHID message from a sequence of packets, starting with the given
44    /// initialization packet.
45    ///
46    /// See [§ 11.2.4 of the CTAP specification][spec].
47    ///
48    /// [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
49    pub fn from_fragments<S: AsRef<[u8]>>(
50        packet: InitializationPacket<S>,
51    ) -> DefragmentedMessage<T> {
52        packet.into()
53    }
54
55    /// Tries to assemble a CTAPHID message from a sequence of packets, starting with the given
56    /// packet.
57    ///
58    /// The packet must be an initialization packet.  This is a shorthand for matching the packet
59    /// and calling [`Message::from_fragments`][].
60    ///
61    /// See [§ 11.2.4 of the CTAP specification][spec].
62    ///
63    /// [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
64    pub fn try_from_fragments<S: AsRef<[u8]>>(
65        packet: Packet<S>,
66    ) -> Result<DefragmentedMessage<T>, DefragmentationError> {
67        if let Packet::Initialization(packet) = packet {
68            Ok(Self::from_fragments(packet))
69        } else {
70            Err(DefragmentationError::InvalidPacketType {
71                expected: PacketType::Initialization,
72                actual: packet.packet_type(),
73            })
74        }
75    }
76}
77
78/// An iterator over CTAPHID packets with the data of a CTAPHID message.
79///
80/// See [§ 11.2.4 of the CTAP specification][spec].
81///
82/// [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
83#[derive(Clone, Debug)]
84pub struct Fragments<'a> {
85    channel: Channel,
86    init: Option<InitializationPacket<&'a [u8]>>,
87    chunks: core::iter::Enumerate<core::slice::Chunks<'a, u8>>,
88}
89
90impl<'a> Fragments<'a> {
91    fn new<T: AsRef<[u8]>>(
92        message: &'a Message<T>,
93        packet_size: usize,
94    ) -> Result<Self, FragmentationError> {
95        if INITIALIZATION_HEADER_SIZE >= packet_size || CONTINUATION_HEADER_SIZE >= packet_size {
96            return Err(FragmentationError::PacketSizeTooSmall);
97        }
98        let data_size_init = packet_size - INITIALIZATION_HEADER_SIZE;
99        let data_size_cont = packet_size - CONTINUATION_HEADER_SIZE;
100        if message.data.as_ref().len() > data_size_init + 128 * data_size_cont {
101            return Err(FragmentationError::DataTooLong);
102        }
103        let n = cmp::min(data_size_init, message.data.as_ref().len());
104        let (data_init, data_cont) = message.data.as_ref().split_at(n);
105        let init = InitializationPacket {
106            channel: message.channel,
107            command: message.command,
108            length: message.data.as_ref().len() as u16,
109            data: data_init,
110        };
111        Ok(Self {
112            channel: message.channel,
113            init: Some(init),
114            chunks: data_cont.chunks(data_size_cont).enumerate(),
115        })
116    }
117}
118
119impl<'a> Iterator for Fragments<'a> {
120    type Item = Packet<&'a [u8]>;
121
122    fn next(&mut self) -> Option<Self::Item> {
123        if let Some(init) = self.init.take() {
124            Some(Packet::Initialization(init))
125        } else if let Some((sequence, data)) = self.chunks.next() {
126            Some(Packet::Continuation(ContinuationPacket {
127                channel: self.channel,
128                sequence: sequence as u8,
129                data,
130            }))
131        } else {
132            None
133        }
134    }
135}
136
137/// A complete or partial message obtained by assembling one or more CTAPHID packets.
138///
139/// See [§ 11.2.4 of the CTAP specification][spec].
140///
141/// [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
142#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
143pub enum DefragmentedMessage<T: AsRef<[u8]> + Default + Extend<u8>> {
144    /// A complete message.
145    Complete(Message<T>),
146    /// A partial message.
147    Partial(PartialMessage<T>),
148}
149
150impl<T: AsRef<[u8]> + Default + Extend<u8>, S: AsRef<[u8]>> From<InitializationPacket<S>>
151    for DefragmentedMessage<T>
152{
153    fn from(packet: InitializationPacket<S>) -> Self {
154        PartialMessage::from(packet).into()
155    }
156}
157
158impl<T: AsRef<[u8]> + Default + Extend<u8>> From<PartialMessage<T>> for DefragmentedMessage<T> {
159    fn from(message: PartialMessage<T>) -> Self {
160        if message.length == message.data.as_ref().len() {
161            Self::Complete(Message {
162                channel: message.channel,
163                command: message.command,
164                data: message.data,
165            })
166        } else {
167            Self::Partial(message)
168        }
169    }
170}
171
172/// A [`Message`][] that has been partially assembled from CTAPHID packets.
173#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
174pub struct PartialMessage<T: AsRef<[u8]> + Default + Extend<u8>> {
175    channel: Channel,
176    command: Command,
177    length: usize,
178    data: T,
179    next_sequence: u8,
180}
181
182impl<T: AsRef<[u8]> + Default + Extend<u8>> PartialMessage<T> {
183    /// Continues assembling a [`Message`][] using the given continuation packet.
184    pub fn extend<S: AsRef<[u8]>>(
185        mut self,
186        packet: &ContinuationPacket<S>,
187    ) -> Result<DefragmentedMessage<T>, DefragmentationError> {
188        if self.channel != packet.channel {
189            return Err(DefragmentationError::InvalidChannel {
190                expected: self.channel,
191                actual: packet.channel,
192            });
193        }
194        if self.next_sequence != packet.sequence {
195            return Err(DefragmentationError::InvalidSequence {
196                expected: self.next_sequence,
197                actual: packet.sequence,
198            });
199        }
200        self.extend_data(packet.data.as_ref());
201        self.next_sequence += 1;
202        Ok(self.into())
203    }
204
205    /// Tries to continue assembling a [`Message`][] using the given packet.
206    ///
207    /// The packet must be a continuation packet.  This is a shorthand for matching the packet and
208    /// calling [`PartialMessage::extend`][].
209    pub fn try_extend<S: AsRef<[u8]>>(
210        self,
211        packet: &Packet<S>,
212    ) -> Result<DefragmentedMessage<T>, DefragmentationError> {
213        if let Packet::Continuation(packet) = packet {
214            self.extend(packet)
215        } else {
216            Err(DefragmentationError::InvalidPacketType {
217                expected: PacketType::Continuation,
218                actual: packet.packet_type(),
219            })
220        }
221    }
222
223    fn extend_data(&mut self, data: &[u8]) {
224        // TODO: use something like extend_from_slice
225        let n = cmp::min(data.len(), self.length - self.data.as_ref().len());
226        self.data.extend(data[..n].iter().cloned());
227    }
228}
229
230impl<T: AsRef<[u8]> + Default + Extend<u8>, S: AsRef<[u8]>> From<InitializationPacket<S>>
231    for PartialMessage<T>
232{
233    fn from(packet: InitializationPacket<S>) -> Self {
234        // TODO: initialize data with capacity
235        let mut message = Self {
236            channel: packet.channel,
237            command: packet.command,
238            length: usize::from(packet.length),
239            data: T::default(),
240            next_sequence: 0,
241        };
242        message.extend_data(packet.data.as_ref());
243        message
244    }
245}
246
247#[cfg(test)]
248mod test {
249    use quickcheck::Arbitrary;
250
251    use super::{DefragmentedMessage, Message};
252
253    impl Arbitrary for Message<Vec<u8>> {
254        fn arbitrary(g: &mut quickcheck::Gen) -> Self {
255            Self {
256                channel: Arbitrary::arbitrary(g),
257                command: Arbitrary::arbitrary(g),
258                data: Arbitrary::arbitrary(g),
259            }
260        }
261
262        fn shrink(&self) -> Box<dyn Iterator<Item = Self>> {
263            let channel = self.channel;
264            let command = self.command;
265            Box::new(self.data.shrink().map(move |data| Self {
266                channel,
267                command,
268                data,
269            }))
270        }
271    }
272
273    quickcheck::quickcheck! {
274        fn fragments(message: Message<Vec<u8>>) -> bool {
275            use std::convert::TryInto;
276
277            let mut d: Option<DefragmentedMessage<Vec<u8>>> = None;
278            for fragment in message.fragments(64).unwrap() {
279                if let Some(dm) = d.take() {
280                    match dm {
281                        DefragmentedMessage::Partial(p) => {
282                            d = Some(p.extend(&fragment.try_into().unwrap()).unwrap());
283                        },
284                        DefragmentedMessage::Complete(_) => unreachable!(),
285                    }
286                } else {
287                    d = Some(Message::from_fragments(fragment.try_into().unwrap()));
288                }
289            }
290
291            match d.unwrap() {
292                DefragmentedMessage::Partial(_) => false,
293                DefragmentedMessage::Complete(m) => m == message,
294            }
295        }
296    }
297}