tacacs_plus_protocol/
authentication.rs

1//! Authentication-related protocol packets.
2
3use core::fmt;
4
5use bitflags::bitflags;
6use byteorder::{ByteOrder, NetworkEndian};
7use getset::{CopyGetters, Getters};
8use num_enum::{TryFromPrimitive, TryFromPrimitiveError};
9
10use super::{
11    AuthenticationContext, AuthenticationType, DeserializeError, MinorVersion, PacketBody,
12    PacketType, Serialize, SerializeError, UserInformation,
13};
14use crate::{Deserialize, FieldText};
15
16#[cfg(test)]
17mod tests;
18
19#[cfg(feature = "std")]
20mod owned;
21
22mod data;
23pub use data::{DataTooLong, PacketData};
24
25#[cfg(feature = "std")]
26pub use owned::ReplyOwned;
27
28/// The authentication action, as indicated upon initiation of an authentication session.
29#[repr(u8)]
30#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
31pub enum Action {
32    /// Login request.
33    Login = 0x01,
34
35    /// Password change request.
36    ChangePassword = 0x02,
37
38    /// Outbound authentication request.
39    ///
40    /// Note that outbound authentication should not be used due to its security implications, according to [RFC8907 section 10.5.3].
41    ///
42    /// [RFC8907 section 10.5.3]: https://www.rfc-editor.org/rfc/rfc8907.html#section-10.5.3-4
43    SendAuth = 0x04,
44}
45
46impl Action {
47    /// The number of bytes an `Action` occupies on the wire.
48    const WIRE_SIZE: usize = 1;
49}
50
51/// The authentication status, as returned by a TACACS+ server.
52#[repr(u8)]
53#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, TryFromPrimitive)]
54pub enum Status {
55    /// Authentication succeeded.
56    Pass = 0x01,
57
58    /// Authentication failed.
59    Fail = 0x02,
60
61    /// Request for more domain-specific data.
62    GetData = 0x03,
63
64    /// Request for username.
65    GetUser = 0x04,
66
67    /// Request for password.
68    GetPassword = 0x05,
69
70    /// Restart session, discarding current one.
71    Restart = 0x06,
72
73    /// Server-side error while authenticating.
74    Error = 0x07,
75
76    /// Forward authentication request to an alternative daemon.
77    #[deprecated = "Forwarding to an alternative daemon was deprecated in RFC-8907."]
78    Follow = 0x21,
79}
80
81impl Status {
82    /// Number of bytes an authentication reply status occupies on the wire.
83    const WIRE_SIZE: usize = 1;
84}
85
86#[doc(hidden)]
87impl From<TryFromPrimitiveError<Status>> for DeserializeError {
88    fn from(value: TryFromPrimitiveError<Status>) -> Self {
89        Self::InvalidStatus(value.number)
90    }
91}
92
93/// An authentication start packet, used to initiate an authentication session.
94#[derive(Debug, Clone, PartialEq, Eq, Hash)]
95pub struct Start<'packet> {
96    action: Action,
97    authentication: AuthenticationContext,
98    user_information: UserInformation<'packet>,
99    data: Option<PacketData<'packet>>,
100}
101
102/// Error returned when attempting to construct an invalid start packet body.
103#[non_exhaustive]
104#[derive(Debug, PartialEq, Eq)]
105pub enum BadStart {
106    /// Authentication type was not set, which is invalid for authentication packets.
107    AuthTypeNotSet,
108
109    /// Action & authentication type were incompatible.
110    ///
111    /// See [Table 1] of RFC8907 for valid combinations.
112    ///
113    /// [Table 1]: https://www.rfc-editor.org/rfc/rfc8907.html#name-tacacs-protocol-versioning
114    IncompatibleActionAndType,
115}
116
117impl fmt::Display for BadStart {
118    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
119        match self {
120            Self::AuthTypeNotSet => write!(
121                f,
122                "authentication type must be set for authentication packets"
123            ),
124            Self::IncompatibleActionAndType => {
125                write!(f, "authentication action & type are incompatible")
126            }
127        }
128    }
129}
130
131impl<'packet> Start<'packet> {
132    /// Initializes a new start packet with the provided fields and an empty data field.
133    pub fn new(
134        action: Action,
135        authentication: AuthenticationContext,
136        user_information: UserInformation<'packet>,
137        data: Option<PacketData<'packet>>,
138    ) -> Result<Self, BadStart> {
139        if authentication.authentication_type == AuthenticationType::NotSet {
140            // authentication type must be set in an authentication start packet
141            Err(BadStart::AuthTypeNotSet)
142        } else if !Self::action_and_type_compatible(authentication.authentication_type, action) {
143            Err(BadStart::IncompatibleActionAndType)
144        } else {
145            Ok(Self {
146                action,
147                authentication,
148                user_information,
149                data,
150            })
151        }
152    }
153
154    /// Predicate for whether authentication type & authentication are compatible.
155    ///
156    /// NOTE: `NotSet` should not be passed to this function, as it is not allowed in authentication packets.
157    ///
158    /// Derived from [Table 1] in RFC8907.
159    ///
160    /// [Table 1]: https://www.rfc-editor.org/rfc/rfc8907.html#name-tacacs-protocol-versioning
161    fn action_and_type_compatible(auth_type: AuthenticationType, action: Action) -> bool {
162        match (auth_type, action) {
163            // ASCII authentication can be used with login/chpass actions
164            (AuthenticationType::Ascii, Action::Login | Action::ChangePassword) => true,
165
166            // ASCII authentication can't be used with sendauth option
167            (AuthenticationType::Ascii, Action::SendAuth) => false,
168
169            // change password is not valid for any other authentication types
170            (_, Action::ChangePassword) => false,
171
172            // NotSet is invalid anyways, so we don't handle it and provide a warning in the doc comment for this function
173            (AuthenticationType::NotSet, _) => unreachable!(),
174
175            // all other authentication types can be used for both sendauth/login
176            _ => true,
177        }
178    }
179}
180
181impl PacketBody for Start<'_> {
182    const TYPE: PacketType = PacketType::Authentication;
183
184    // extra byte for data length
185    const REQUIRED_FIELDS_LENGTH: usize = Action::WIRE_SIZE
186        + AuthenticationContext::WIRE_SIZE
187        + UserInformation::HEADER_INFORMATION_SIZE
188        + 1;
189
190    fn required_minor_version(&self) -> Option<MinorVersion> {
191        // NOTE: a check in Start::new() guarantees that the authentication type will not be NotSet
192        match self.authentication.authentication_type {
193            AuthenticationType::Ascii => Some(MinorVersion::Default),
194            _ => Some(MinorVersion::V1),
195        }
196    }
197}
198
199impl Serialize for Start<'_> {
200    fn wire_size(&self) -> usize {
201        Action::WIRE_SIZE
202            + AuthenticationContext::WIRE_SIZE
203            + self.user_information.wire_size()
204            + 1 // extra byte to include length of data
205            + self.data.as_ref().map_or(0, |data| data.as_bytes().len())
206    }
207
208    fn serialize_into_buffer(&self, buffer: &mut [u8]) -> Result<usize, SerializeError> {
209        let wire_size = self.wire_size();
210
211        if buffer.len() >= self.wire_size() {
212            buffer[0] = self.action as u8;
213
214            self.authentication.serialize(&mut buffer[1..4]);
215
216            self.user_information
217                .serialize_field_lengths(&mut buffer[4..7])?;
218
219            // information written before this occupies 8 bytes
220            let mut total_bytes_written = 8;
221
222            // user information values start at index 8
223            // cap slice with wire size to avoid overflows, although that shouldn't happen
224            let user_info_written_len = self
225                .user_information
226                .serialize_field_values(&mut buffer[8..wire_size])?;
227            total_bytes_written += user_info_written_len;
228
229            // data starts after the end of the user information values
230            let data_start = 8 + user_info_written_len;
231            if let Some(data) = self.data.as_ref() {
232                let data_len = data.len();
233
234                // length is verified to fit in a u8 in new(), but verify anyways
235                buffer[7] = data.len();
236
237                // copy over packet data
238                buffer[data_start..data_start + data_len as usize].copy_from_slice(data.as_bytes());
239
240                total_bytes_written += data_len as usize;
241            } else {
242                // set data_len field to 0; no data has to be copied to the data section of the packet
243                buffer[7] = 0;
244            }
245
246            if total_bytes_written == wire_size {
247                Ok(total_bytes_written)
248            } else {
249                Err(SerializeError::LengthMismatch {
250                    expected: wire_size,
251                    actual: total_bytes_written,
252                })
253            }
254        } else {
255            Err(SerializeError::NotEnoughSpace)
256        }
257    }
258}
259
260bitflags! {
261    /// Flags received in an authentication reply packet.
262    #[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
263    #[repr(transparent)]
264    pub struct ReplyFlags: u8 {
265        /// Indicates the client MUST NOT display user input.
266        const NO_ECHO = 0b00000001;
267    }
268}
269
270impl ReplyFlags {
271    /// Number of bytes reply flags occupy on the wire.
272    const WIRE_SIZE: usize = 1;
273}
274
275crate::util::bitflags_display_impl!(ReplyFlags);
276
277/// An authentication reply packet received from a server.
278#[derive(Debug, Clone, PartialEq, Eq, Hash, Getters, CopyGetters)]
279pub struct Reply<'packet> {
280    /// Gets the status of this authentication exchange, as returned from the server.
281    #[getset(get = "pub")]
282    status: Status,
283
284    /// Returns the message meant to be displayed to the user.
285    #[getset(get = "pub")]
286    server_message: FieldText<'packet>,
287
288    /// Returns the authentication data for processing by the client.
289    #[getset(get_copy = "pub")]
290    data: &'packet [u8],
291
292    /// Gets the flags returned from the server as part of this authentication exchange.
293    #[getset(get = "pub")]
294    flags: ReplyFlags,
295}
296
297struct ReplyFieldLengths {
298    server_message_length: u16,
299    data_length: u16,
300    total_length: u32,
301}
302
303impl Reply<'_> {
304    /// Server message offset within packet body as a zero-based index.
305    const SERVER_MESSAGE_OFFSET: usize = 6;
306
307    /// Attempts to extract the claimed reply packed body length from a buffer.
308    pub fn extract_total_length(buffer: &[u8]) -> Result<u32, DeserializeError> {
309        Self::extract_field_lengths(buffer).map(|lengths| lengths.total_length)
310    }
311
312    /// Extracts the server message and data field lengths from a buffer, treating it as if it were a serialized reply packet body.
313    fn extract_field_lengths(buffer: &[u8]) -> Result<ReplyFieldLengths, DeserializeError> {
314        // data length is the last required field
315        if buffer.len() >= Self::REQUIRED_FIELDS_LENGTH {
316            let server_message_length = NetworkEndian::read_u16(&buffer[2..4]);
317            let data_length = NetworkEndian::read_u16(&buffer[4..6]);
318
319            // total length is just the sum of field lengths & the encoded lengths themselves
320            // SAFETY: REQUIRED_FIELDS_LENGTH as defined is guaranteed to fit in a u32
321            let total_length = u32::try_from(Self::REQUIRED_FIELDS_LENGTH).unwrap()
322                + u32::from(server_message_length)
323                + u32::from(data_length);
324
325            Ok(ReplyFieldLengths {
326                server_message_length,
327                data_length,
328                total_length,
329            })
330        } else {
331            Err(DeserializeError::UnexpectedEnd)
332        }
333    }
334}
335
336impl PacketBody for Reply<'_> {
337    const TYPE: PacketType = PacketType::Authentication;
338
339    // extra 2 bytes each for lengths of server message & data
340    const REQUIRED_FIELDS_LENGTH: usize = Status::WIRE_SIZE + ReplyFlags::WIRE_SIZE + 4;
341}
342
343// Hide from docs, as this is meant for internal use only
344#[doc(hidden)]
345impl<'raw> Deserialize<'raw> for Reply<'raw> {
346    fn deserialize_from_buffer(buffer: &'raw [u8]) -> Result<Self, DeserializeError> {
347        let field_lengths = Self::extract_field_lengths(buffer)?;
348
349        // buffer is sliced to length reported in packet header in Packet::deserialize_body(), so we can compare against
350        // it using the buffer length
351        let length_from_header = buffer.len();
352
353        // ensure buffer is large enough to contain entire packet
354        if field_lengths.total_length as usize == length_from_header {
355            let status = Status::try_from(buffer[0])?;
356            let flag_byte = buffer[1];
357            let flags = ReplyFlags::from_bits(flag_byte)
358                .ok_or(DeserializeError::InvalidBodyFlags(flag_byte))?;
359
360            let data_begin =
361                Self::SERVER_MESSAGE_OFFSET + field_lengths.server_message_length as usize;
362
363            let server_message =
364                FieldText::try_from(&buffer[Self::SERVER_MESSAGE_OFFSET..data_begin])
365                    .map_err(|_| DeserializeError::BadText)?;
366            let data = &buffer[data_begin..data_begin + field_lengths.data_length as usize];
367
368            Ok(Reply {
369                status,
370                server_message,
371                data,
372                flags,
373            })
374        } else {
375            Err(DeserializeError::WrongBodyBufferSize {
376                expected: field_lengths.total_length as usize,
377                buffer_size: length_from_header,
378            })
379        }
380    }
381}
382
383bitflags! {
384    /// Flags to send as part of an authentication continue packet.
385    #[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
386    #[repr(transparent)]
387    pub struct ContinueFlags: u8 {
388        /// Indicates the client is prematurely aborting the authentication session.
389        const ABORT = 0b00000001;
390    }
391}
392
393crate::util::bitflags_display_impl!(ContinueFlags);
394
395/// A continue packet potentially sent as part of an authentication session.
396#[derive(PartialEq, Eq, Clone, Debug, Hash)]
397pub struct Continue<'packet> {
398    user_message: Option<&'packet [u8]>,
399    data: Option<&'packet [u8]>,
400    flags: ContinueFlags,
401}
402
403impl<'packet> Continue<'packet> {
404    /// Offset of the user message within a continue packet body, if present.
405    const USER_MESSAGE_OFFSET: usize = 5;
406
407    /// Constructs a continue packet, performing length checks on the user message and data fields to ensure encodable lengths.
408    pub fn new(
409        user_message: Option<&'packet [u8]>,
410        data: Option<&'packet [u8]>,
411        flags: ContinueFlags,
412    ) -> Option<Self> {
413        if user_message.map_or(true, |message| u16::try_from(message.len()).is_ok())
414            && data.map_or(true, |data_slice| u16::try_from(data_slice.len()).is_ok())
415        {
416            Some(Continue {
417                user_message,
418                data,
419                flags,
420            })
421        } else {
422            None
423        }
424    }
425}
426
427impl PacketBody for Continue<'_> {
428    const TYPE: PacketType = PacketType::Authentication;
429
430    // 2 bytes each for user message & data length; 1 byte for flags
431    const REQUIRED_FIELDS_LENGTH: usize = 5;
432}
433
434impl Serialize for Continue<'_> {
435    fn wire_size(&self) -> usize {
436        Self::REQUIRED_FIELDS_LENGTH
437            + self.user_message.map_or(0, <[u8]>::len)
438            + self.data.map_or(0, <[u8]>::len)
439    }
440
441    fn serialize_into_buffer(&self, buffer: &mut [u8]) -> Result<usize, SerializeError> {
442        let wire_size = self.wire_size();
443
444        if buffer.len() >= wire_size {
445            // write field lengths into beginning of body
446            let user_message_len = self.user_message.map_or(0, <[u8]>::len).try_into()?;
447            NetworkEndian::write_u16(&mut buffer[..2], user_message_len);
448
449            let data_len = self.data.map_or(0, <[u8]>::len).try_into()?;
450            NetworkEndian::write_u16(&mut buffer[2..4], data_len);
451
452            let data_offset = Self::USER_MESSAGE_OFFSET + user_message_len as usize;
453
454            // set abort flag if needed
455            buffer[4] = self.flags.bits();
456
457            // copy user message into buffer, if present
458            if let Some(message) = self.user_message {
459                buffer[Self::USER_MESSAGE_OFFSET..data_offset].copy_from_slice(message);
460            }
461
462            // copy data into buffer, again if present
463            if let Some(data) = self.data {
464                buffer[data_offset..data_offset + data_len as usize].copy_from_slice(data);
465            }
466
467            // total number of bytes written includes required "header" fields & two variable length fields
468            let actual_written_len =
469                Self::REQUIRED_FIELDS_LENGTH + user_message_len as usize + data_len as usize;
470
471            if actual_written_len == wire_size {
472                Ok(actual_written_len)
473            } else {
474                Err(SerializeError::LengthMismatch {
475                    expected: wire_size,
476                    actual: actual_written_len,
477                })
478            }
479        } else {
480            Err(SerializeError::NotEnoughSpace)
481        }
482    }
483}