tacacs_plus_protocol/
lib.rs

1//! # tacacs-plus-protocol
2//!
3//! Serialization & deserialization of (RFC8907) TACACS+ protocol packets.
4
5#![no_std]
6#![warn(missing_docs)]
7#![warn(clippy::cast_lossless)]
8#![warn(clippy::cast_possible_truncation)]
9// show std badge on feature-gated types/etc. on docs.rs (see also Cargo.toml)
10#![cfg_attr(docsrs, feature(doc_auto_cfg))]
11
12#[cfg(feature = "std")]
13extern crate std;
14
15use core::{fmt, num::TryFromIntError};
16
17mod util;
18
19pub mod accounting;
20pub mod authentication;
21pub mod authorization;
22
23mod packet;
24use getset::CopyGetters;
25pub use packet::header::HeaderInfo;
26pub use packet::{Packet, PacketFlags, PacketType};
27
28mod arguments;
29pub use arguments::{Argument, Arguments, InvalidArgument};
30
31mod fields;
32pub use fields::*;
33
34mod text;
35pub use text::{FieldText, InvalidText};
36
37#[cfg(feature = "std")]
38mod owned;
39
40/// An error that occurred when serializing a packet or any of its components into their binary format.
41#[non_exhaustive]
42#[derive(Debug, PartialEq, Eq)]
43pub enum SerializeError {
44    /// The provided buffer did not have enough space to serialize the object.
45    NotEnoughSpace,
46
47    /// The length of a field exceeded the maximum value encodeable on the wire.
48    LengthOverflow,
49
50    /// Mismatch between expected/actual number of bytes written.
51    LengthMismatch {
52        /// The expected number of bytes to have been written.
53        expected: usize,
54        /// That actual number of bytes written during serialization.
55        actual: usize,
56    },
57}
58
59impl fmt::Display for SerializeError {
60    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61        match self {
62            Self::NotEnoughSpace => write!(f, "not enough space in buffer"),
63            Self::LengthOverflow => write!(f, "field length overflowed"),
64            Self::LengthMismatch { expected, actual } => write!(
65                f,
66                "mismatch in number of bytes written: expected {expected}, actual {actual}"
67            ),
68        }
69    }
70}
71
72#[doc(hidden)]
73impl From<TryFromIntError> for SerializeError {
74    fn from(_value: TryFromIntError) -> Self {
75        Self::LengthOverflow
76    }
77}
78
79/// An error that occurred during deserialization of a full/partial packet.
80#[non_exhaustive]
81#[derive(Debug, PartialEq, Eq)]
82pub enum DeserializeError {
83    /// Invalid binary status representation in response.
84    InvalidStatus(u8),
85
86    /// Invalid packet type number on the wire.
87    InvalidPacketType(u8),
88
89    /// Invalid header flag byte.
90    InvalidHeaderFlags(u8),
91
92    /// Invalid body flag byte.
93    InvalidBodyFlags(u8),
94
95    /// Invalid version number.
96    InvalidVersion(u8),
97
98    /// Invalid arguments when deserializing
99    InvalidArgument(InvalidArgument),
100
101    /// Mismatch between expected/received packet types.
102    PacketTypeMismatch {
103        /// The expected packet type.
104        expected: PacketType,
105
106        /// The actual packet type that was parsed.
107        actual: PacketType,
108    },
109
110    /// Text field was not printable ASCII when it should have been.
111    BadText,
112
113    /// Unencrypted flag was not the expected value.
114    IncorrectUnencryptedFlag,
115
116    /// Buffer containing raw body had incorrect length with respect to length fields in the body.
117    WrongBodyBufferSize {
118        /// The expected buffer length, based on length fields in the packet body.
119        expected: usize,
120        /// The size of the buffer being deserialized, sliced to just the body section.
121        buffer_size: usize,
122    },
123
124    /// Object representation was cut off in some way.
125    UnexpectedEnd,
126}
127
128impl fmt::Display for DeserializeError {
129    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
130        match self {
131            Self::InvalidStatus(num) => write!(f, "invalid status byte in raw packet: {num:#x}"),
132            Self::InvalidPacketType(num) => write!(f, "invalid packet type byte: {num:#x}"),
133            Self::InvalidHeaderFlags(num) => write!(f, "invalid header flags: {num:#x}"),
134            Self::InvalidBodyFlags(num) => write!(f, "invalid body flags: {num:#x}"),
135            Self::InvalidVersion(num) => write!(
136                f,
137                "invalid version number: major {:#x}, minor {:#x}",
138                num >> 4,     // major version is 4 upper bits of byte
139                num & 0b1111  // minor version is 4 lower bits
140            ),
141            Self::InvalidArgument(reason) => write!(f, "invalid argument: {reason}"),
142            Self::BadText => write!(f, "text field was not printable ASCII"),
143            Self::IncorrectUnencryptedFlag => write!(f, "unencrypted flag had an incorrect value"),
144            Self::PacketTypeMismatch { expected, actual } => write!(f, "packet type mismatch: expected {expected:?} but got {actual:?}"),
145            Self::WrongBodyBufferSize { expected, buffer_size } => write!(f, "body buffer size didn't match length fields: expected {expected} bytes, but buffer was actually {buffer_size}"),
146            Self::UnexpectedEnd => write!(f, "unexpected end of buffer when deserializing object"),
147        }
148    }
149}
150
151// Error trait is only available on std (on stable; stabilized in nightly 1.81) so this has to be std-gated
152#[cfg(feature = "std")]
153mod error_impls {
154    use std::error::Error;
155    use std::fmt;
156
157    use super::text::InvalidText;
158    use super::{DeserializeError, InvalidArgument, SerializeError};
159
160    impl Error for DeserializeError {}
161    impl Error for SerializeError {}
162    impl Error for InvalidArgument {}
163    impl Error for super::authentication::BadStart {}
164    impl Error for super::authentication::DataTooLong {}
165    impl<T> Error for InvalidText<T> where InvalidText<T>: fmt::Debug + fmt::Display {}
166}
167
168// suggestion from Rust API guidelines: https://rust-lang.github.io/api-guidelines/future-proofing.html#sealed-traits-protect-against-downstream-implementations-c-sealed
169// seals the PacketBody trait
170mod sealed {
171    use super::{accounting, authentication, authorization};
172    use super::{Packet, PacketBody};
173
174    pub trait Sealed {}
175
176    // authentication packet types
177    impl Sealed for authentication::Start<'_> {}
178    impl Sealed for authentication::Continue<'_> {}
179    impl Sealed for authentication::Reply<'_> {}
180
181    // authorization packet bodies
182    impl Sealed for authorization::Request<'_> {}
183    impl Sealed for authorization::Reply<'_> {}
184
185    // accounting packet bodies
186    impl Sealed for accounting::Request<'_> {}
187    impl Sealed for accounting::Reply<'_> {}
188
189    // full packet type
190    impl<B: PacketBody> Sealed for Packet<B> {}
191}
192
193/// The major version of the TACACS+ protocol.
194#[repr(u8)]
195#[non_exhaustive]
196#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
197pub enum MajorVersion {
198    /// The only current major version specified in RFC8907.
199    RFC8907 = 0xc,
200}
201
202impl fmt::Display for MajorVersion {
203    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
204        write!(
205            f,
206            "{}",
207            match self {
208                MajorVersion::RFC8907 => "RFC 8907",
209            }
210        )
211    }
212}
213
214/// The minor version of the TACACS+ protocol in use, which specifies choices for authentication methods.
215#[repr(u8)]
216#[non_exhaustive]
217#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
218pub enum MinorVersion {
219    /// Default minor version, used for ASCII authentication.
220    Default = 0x0,
221    /// Minor version 1, which is used for (MS)CHAP and PAP authentication.
222    V1 = 0x1,
223}
224
225impl fmt::Display for MinorVersion {
226    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
227        write!(
228            f,
229            "{}",
230            match self {
231                Self::Default => "default",
232                Self::V1 => "1",
233            }
234        )
235    }
236}
237
238/// The full protocol version.
239#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, CopyGetters)]
240#[getset(get_copy = "pub")]
241pub struct Version {
242    /// The major TACACS+ version.
243    major: MajorVersion,
244
245    /// The minor TACACS+ version.
246    minor: MinorVersion,
247}
248
249impl Version {
250    /// Bundles together a TACACS+ protocol major and minor version.
251    pub fn new(major: MajorVersion, minor: MinorVersion) -> Self {
252        Self { major, minor }
253    }
254}
255
256impl Default for Version {
257    fn default() -> Self {
258        Self {
259            major: MajorVersion::RFC8907,
260            minor: MinorVersion::Default,
261        }
262    }
263}
264
265impl fmt::Display for Version {
266    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
267        write!(f, "major {}, minor {}", self.major(), self.minor())
268    }
269}
270
271impl PartialOrd for Version {
272    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
273        Some(self.cmp(other))
274    }
275}
276
277impl Ord for Version {
278    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
279        // compare major versions, then tiebreak with minor versions
280        self.major
281            .cmp(&other.major)
282            .then(self.minor.cmp(&other.minor))
283    }
284}
285
286impl TryFrom<u8> for Version {
287    type Error = DeserializeError;
288
289    fn try_from(value: u8) -> Result<Self, Self::Error> {
290        // only major version is 0xc currently
291        if value >> 4 == MajorVersion::RFC8907 as u8 {
292            let minor_version = match value & 0xf {
293                0 => Ok(MinorVersion::Default),
294                1 => Ok(MinorVersion::V1),
295                _ => Err(DeserializeError::InvalidVersion(value)),
296            }?;
297
298            Ok(Self {
299                major: MajorVersion::RFC8907,
300                minor: minor_version,
301            })
302        } else {
303            Err(DeserializeError::InvalidVersion(value))
304        }
305    }
306}
307
308impl From<Version> for u8 {
309    fn from(value: Version) -> Self {
310        ((value.major as u8) << 4) | (value.minor as u8 & 0xf)
311    }
312}
313
314/// A type that can be treated as a TACACS+ protocol packet body.
315///
316/// This trait is sealed per the [Rust API guidelines], so it cannot be implemented by external types.
317///
318/// [Rust API guidelines]: https://rust-lang.github.io/api-guidelines/future-proofing.html#sealed-traits-protect-against-downstream-implementations-c-sealed
319pub trait PacketBody: sealed::Sealed {
320    /// Type of the packet (one of authentication, authorization, or accounting).
321    const TYPE: PacketType;
322
323    /// Length of body just including required fields.
324    const REQUIRED_FIELDS_LENGTH: usize;
325
326    /// Required protocol minor version based on the contents of the packet body.
327    ///
328    /// This is used since [`AuthenticationMethod`]s are partitioned by protocol minor version.
329    fn required_minor_version(&self) -> Option<MinorVersion> {
330        None
331    }
332}
333
334/// Something that can be serialized into a binary format.
335#[doc(hidden)]
336pub trait Serialize: sealed::Sealed {
337    /// Returns the current size of the packet as represented on the wire.
338    fn wire_size(&self) -> usize;
339
340    /// Serializes data into a buffer, returning the resulting length on success.
341    fn serialize_into_buffer(&self, buffer: &mut [u8]) -> Result<usize, SerializeError>;
342}
343
344/// Something that can be deserialized from a binary format.
345#[doc(hidden)]
346pub trait Deserialize<'raw>: sealed::Sealed + Sized {
347    /// Attempts to deserialize an object from a buffer.
348    fn deserialize_from_buffer(buffer: &'raw [u8]) -> Result<Self, DeserializeError>;
349}