tacacs_plus_protocol/
packet.rs

1use core::fmt;
2use core::iter::zip;
3
4use bitflags::bitflags;
5use byteorder::{ByteOrder, NetworkEndian};
6use getset::Getters;
7use md5::{Digest, Md5};
8use num_enum::{TryFromPrimitive, TryFromPrimitiveError};
9
10use super::{Deserialize, PacketBody, Serialize};
11use super::{DeserializeError, SerializeError};
12
13pub(super) mod header;
14use header::HeaderInfo;
15
16#[cfg(test)]
17mod tests;
18
19/// Flags to indicate information about packets or the client/server.
20#[repr(transparent)]
21#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
22pub struct PacketFlags(u8);
23
24bitflags! {
25    impl PacketFlags: u8 {
26        /// Indicates the body of the packet is unobfuscated.
27        ///
28        /// Note that RFC 8907 specifies that "this option is deprecated and **MUST NOT** be used in production" ([section 4.5]).
29        ///
30        /// [section 4.5]: https://www.rfc-editor.org/rfc/rfc8907.html#section-4.5-16
31        const UNENCRYPTED       = 0b00000001;
32
33        /// Signals to the server that the client would like to reuse a TCP connection across multiple sessions.
34        const SINGLE_CONNECTION = 0b00000100;
35    }
36}
37
38impl fmt::Display for PacketFlags {
39    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
40        if self.is_empty() {
41            write!(f, "no flags set")
42        } else {
43            for (name, _) in self.iter_names() {
44                write!(f, "{name} ")?;
45            }
46
47            Ok(())
48        }
49    }
50}
51
52/// The type of a protocol packet.
53#[repr(u8)]
54#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, TryFromPrimitive)]
55pub enum PacketType {
56    /// Authentication packet.
57    Authentication = 0x1,
58
59    /// Authorization packet.
60    Authorization = 0x2,
61
62    /// Accounting packet.
63    Accounting = 0x3,
64}
65
66impl fmt::Display for PacketType {
67    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68        write!(
69            f,
70            "{}",
71            match self {
72                Self::Authentication => "authentication",
73                Self::Authorization => "authorization",
74                Self::Accounting => "accounting",
75            }
76        )
77    }
78}
79
80#[doc(hidden)]
81impl From<TryFromPrimitiveError<PacketType>> for DeserializeError {
82    fn from(value: TryFromPrimitiveError<PacketType>) -> Self {
83        Self::InvalidPacketType(value.number)
84    }
85}
86
87/// A full TACACS+ protocol packet.
88#[derive(Clone, Debug, PartialEq, Eq, Hash, Getters)]
89#[getset(get = "pub")]
90pub struct Packet<B> {
91    /// Some of the header information associated with a packet.
92    header: HeaderInfo,
93
94    /// The body of the packet.
95    body: B,
96}
97
98impl<B: PacketBody> Packet<B> {
99    /// Location of the start of the packet body, after the header.
100    pub(super) const BODY_START: usize = 12;
101
102    /// Assembles a header and body into a full packet.
103    ///
104    /// NOTE: Some fields in the provided header may be updated for consistency.
105    /// These may include:
106    /// - The protocol minor version, depending on authentication method choice
107    /// - The [`UNENCRYPTED`](PacketFlags::UNENCRYPTED) flag, depending on if a key is specified
108    pub fn new(mut header: HeaderInfo, body: B) -> Self {
109        // update minor version to what is required by the body, if applicable
110        if let Some(minor) = body.required_minor_version() {
111            header.version_mut().minor = minor;
112        }
113
114        Self { header, body }
115    }
116}
117
118/// MD5 hash output size, in bytes.
119const MD5_OUTPUT_SIZE: usize = 16;
120
121/// (De)obfuscates the body of a packet as specified in [RFC8907 section 4.5].
122///
123/// Since obfuscation is done by XOR, obfuscating & deobfuscating are the same operation.
124///
125/// [RFC8907 section 4.5]: https://www.rfc-editor.org/rfc/rfc8907.html#name-data-obfuscation
126pub(super) fn xor_body_with_pad(header: &HeaderInfo, secret_key: &[u8], body_buffer: &mut [u8]) {
127    let mut pseudo_pad = [0; MD5_OUTPUT_SIZE];
128
129    // prehash common prefix for all hash invocations
130    // prefix: session id -> key -> version -> sequence number
131    let mut prefix_hasher = Md5::new();
132    prefix_hasher.update(header.session_id().to_be_bytes());
133    prefix_hasher.update(secret_key);
134
135    // technically these to_be_bytes calls don't do anything since both fields end up as `u8`s but still
136    prefix_hasher.update(u8::from(header.version()).to_be_bytes());
137    prefix_hasher.update(header.sequence_number().to_be_bytes());
138
139    let mut chunks_iter = body_buffer.chunks_mut(MD5_OUTPUT_SIZE);
140
141    // first chunk just uses hashed prefix
142    prefix_hasher
143        .clone()
144        .finalize_into((&mut pseudo_pad).into());
145
146    // SAFETY: the body of a packet is guaranteed to be nonempty due to checks against REQUIRED_FIELD_SIZE,
147    // so this unwrap won't panic
148    let first_chunk = chunks_iter.next().unwrap();
149
150    // xor pseudo-pad with chunk
151    xor_slices(first_chunk, &pseudo_pad);
152
153    for chunk in chunks_iter {
154        // previous pad chunk is appended to prefix prehashed above
155        let mut hasher = prefix_hasher.clone();
156        hasher.update(pseudo_pad);
157        hasher.finalize_into((&mut pseudo_pad).into());
158
159        // xor pseudo-pad with chunk
160        xor_slices(chunk, &pseudo_pad);
161    }
162}
163
164/// XORs two byte slices together, truncating to the shorter of the two argument lengths.
165fn xor_slices(output: &mut [u8], pseudo_pad: &[u8]) {
166    for (out, pad) in zip(output, pseudo_pad) {
167        *out ^= pad;
168    }
169}
170
171// The Serialize trait is not meant to be exposed publicly, but we still use it internally for serializing packet bodies so we silence the lint here
172impl<B: PacketBody + Serialize> Packet<B> {
173    /// Calculates the size of this packet as encoded into its binary format.
174    pub fn wire_size(&self) -> usize {
175        HeaderInfo::HEADER_SIZE_BYTES + self.body.wire_size()
176    }
177
178    /// Serializes the packet into a buffer, obfuscating the body using a pseudo-pad generated by iterating the MD5 hash function.
179    ///
180    /// This consumes the packet and also ensures the [`UNENCRYPTED`](PacketFlags::UNENCRYPTED) flag is unset.
181    pub fn serialize<K: AsRef<[u8]>>(
182        mut self,
183        secret_key: K,
184        buffer: &mut [u8],
185    ) -> Result<usize, SerializeError> {
186        // remove unencrypted flag from header
187        self.header.flags_mut().remove(PacketFlags::UNENCRYPTED);
188
189        let packet_length = self.serialize_packet(buffer)?;
190
191        xor_body_with_pad(
192            &self.header,
193            secret_key.as_ref(),
194            &mut buffer[Self::BODY_START..packet_length],
195        );
196
197        Ok(packet_length)
198    }
199
200    /// Serializes the packet into a buffer, leaving the body as cleartext.
201    ///
202    /// This consumes the packet and sets the [`UNENCRYPTED`](PacketFlags::UNENCRYPTED) flag if necessary.
203    ///
204    /// Note that RFC8907 deprecated the UNENCRYPTED flag and states that it "**MUST NOT** be used in production" ([section 4.5]).
205    ///
206    /// [section 4.5]: https://www.rfc-editor.org/rfc/rfc8907.html#section-4.5-16
207    pub fn serialize_unobfuscated(mut self, buffer: &mut [u8]) -> Result<usize, SerializeError> {
208        // ensure unencrypted flag is set
209        self.header.flags_mut().insert(PacketFlags::UNENCRYPTED);
210
211        self.serialize_packet(buffer)
212    }
213
214    fn serialize_packet(&self, buffer: &mut [u8]) -> Result<usize, SerializeError> {
215        let wire_size = self.wire_size();
216
217        if buffer.len() >= wire_size {
218            // serialize body first to get its length, which is stored in the header
219            let body_length = self
220                .body
221                .serialize_into_buffer(&mut buffer[Self::BODY_START..wire_size])?;
222
223            // fill in header information
224            let header_bytes = self.header.serialize(
225                &mut buffer[..HeaderInfo::HEADER_SIZE_BYTES],
226                B::TYPE,
227                body_length.try_into()?,
228            )?;
229
230            // return total length written
231            Ok(header_bytes + body_length)
232        } else {
233            Err(SerializeError::NotEnoughSpace)
234        }
235    }
236}
237
238impl<'raw, B: PacketBody + Deserialize<'raw>> Packet<B> {
239    /// Attempts to deserialize an obfuscated packet with the provided secret key.
240    ///
241    /// This function also ensures that the [`UNENCRYPTED`](PacketFlags::UNENCRYPTED)
242    /// is not set, and returns an error if it is.
243    pub fn deserialize<K: AsRef<[u8]>>(
244        secret_key: K,
245        buffer: &'raw mut [u8],
246    ) -> Result<Self, DeserializeError> {
247        let header = HeaderInfo::try_from(&buffer[..HeaderInfo::HEADER_SIZE_BYTES])?;
248
249        // ensure unencrypted flag is not set
250        if !header.flags().contains(PacketFlags::UNENCRYPTED) {
251            xor_body_with_pad(
252                &header,
253                secret_key.as_ref(),
254                &mut buffer[Self::BODY_START..],
255            );
256
257            let body = Self::deserialize_body(buffer)?;
258
259            Ok(Self::new(header, body))
260        } else {
261            Err(DeserializeError::IncorrectUnencryptedFlag)
262        }
263    }
264
265    /// Attempts to deserialize a cleartext packet from a buffer.
266    ///
267    /// This function also ensures that the [`UNENCRYPTED`](PacketFlags::UNENCRYPTED)
268    /// is set, and returns an error if it is not.
269    pub fn deserialize_unobfuscated(buffer: &'raw [u8]) -> Result<Self, DeserializeError> {
270        let header = HeaderInfo::try_from(&buffer[..HeaderInfo::HEADER_SIZE_BYTES])?;
271
272        // ensure unencrypted flag is set
273        if header.flags().contains(PacketFlags::UNENCRYPTED) {
274            let body = Self::deserialize_body(buffer)?;
275            Ok(Self::new(header, body))
276        } else {
277            Err(DeserializeError::IncorrectUnencryptedFlag)
278        }
279    }
280
281    fn deserialize_body(buffer: &'raw [u8]) -> Result<B, DeserializeError> {
282        if buffer.len() > HeaderInfo::HEADER_SIZE_BYTES {
283            let actual_packet_type = PacketType::try_from(buffer[1])?;
284            if actual_packet_type == B::TYPE {
285                // body length is stored at the end of the 12-byte header
286                let body_length = NetworkEndian::read_u32(&buffer[8..12]) as usize;
287
288                // NOTE: the rest of the buffer is checked here to avoid a panic if it's shorter than body_length when trying to slice that large
289                // ensure buffer actually contains whole body
290                if buffer[Self::BODY_START..].len() >= body_length {
291                    let body = B::deserialize_from_buffer(
292                        &buffer[Self::BODY_START..Self::BODY_START + body_length],
293                    )?;
294                    Ok(body)
295                } else {
296                    Err(DeserializeError::UnexpectedEnd)
297                }
298            } else {
299                Err(DeserializeError::PacketTypeMismatch {
300                    expected: B::TYPE,
301                    actual: actual_packet_type,
302                })
303            }
304        } else {
305            Err(DeserializeError::UnexpectedEnd)
306        }
307    }
308
309    /// Converts this packet to one with a body that owns its fields.
310    #[cfg(feature = "std")]
311    pub fn to_owned<'b, O: super::owned::FromBorrowedBody<Borrowed<'b> = B>>(&self) -> Packet<O> {
312        Packet {
313            header: self.header,
314            body: O::from_borrowed(&self.body),
315        }
316    }
317}