mqtt5_protocol/packet/
auth.rs

1use crate::error::{MqttError, Result};
2use crate::packet::{FixedHeader, MqttPacket, PacketType};
3use crate::protocol::v5::properties::Properties;
4use crate::protocol::v5::reason_codes::ReasonCode;
5use bytes::{Buf, BufMut};
6
7/// AUTH packet for MQTT v5.0 enhanced authentication
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct AuthPacket {
10    /// Reason code indicating the result of the authentication
11    pub reason_code: ReasonCode,
12    /// Properties associated with the authentication
13    pub properties: Properties,
14}
15
16impl AuthPacket {
17    /// Creates a new AUTH packet
18    #[must_use]
19    pub fn new(reason_code: ReasonCode) -> Self {
20        Self {
21            reason_code,
22            properties: Properties::default(),
23        }
24    }
25
26    #[must_use]
27    /// Creates a new AUTH packet with properties
28    pub fn with_properties(reason_code: ReasonCode, properties: Properties) -> Self {
29        Self {
30            reason_code,
31            properties,
32        }
33    }
34
35    /// Creates an AUTH packet for continuing authentication
36    ///
37    /// # Errors
38    ///
39    /// Returns an error if the operation fails
40    pub fn continue_authentication(
41        auth_method: String,
42        auth_data: Option<Vec<u8>>,
43    ) -> Result<Self> {
44        let mut properties = Properties::default();
45
46        // Authentication method is required
47        properties.add(
48            crate::protocol::v5::properties::PropertyId::AuthenticationMethod,
49            crate::protocol::v5::properties::PropertyValue::Utf8String(auth_method),
50        )?;
51
52        // Authentication data is optional
53        if let Some(data) = auth_data {
54            properties.add(
55                crate::protocol::v5::properties::PropertyId::AuthenticationData,
56                crate::protocol::v5::properties::PropertyValue::BinaryData(data.into()),
57            )?;
58        }
59
60        Ok(Self::with_properties(
61            ReasonCode::ContinueAuthentication,
62            properties,
63        ))
64    }
65
66    /// Creates an AUTH packet for re-authentication
67    ///
68    /// # Errors
69    ///
70    /// Returns an error if the operation fails
71    pub fn re_authenticate(auth_method: String, auth_data: Option<Vec<u8>>) -> Result<Self> {
72        let mut properties = Properties::default();
73
74        // Authentication method is required
75        properties.add(
76            crate::protocol::v5::properties::PropertyId::AuthenticationMethod,
77            crate::protocol::v5::properties::PropertyValue::Utf8String(auth_method),
78        )?;
79
80        // Authentication data is optional
81        if let Some(data) = auth_data {
82            properties.add(
83                crate::protocol::v5::properties::PropertyId::AuthenticationData,
84                crate::protocol::v5::properties::PropertyValue::BinaryData(data.into()),
85            )?;
86        }
87
88        Ok(Self::with_properties(
89            ReasonCode::ReAuthenticate,
90            properties,
91        ))
92    }
93
94    /// Creates a successful authentication response
95    ///
96    /// # Errors
97    ///
98    /// Returns an error if the operation fails
99    pub fn success(auth_method: String) -> Result<Self> {
100        let mut properties = Properties::default();
101
102        properties.add(
103            crate::protocol::v5::properties::PropertyId::AuthenticationMethod,
104            crate::protocol::v5::properties::PropertyValue::Utf8String(auth_method),
105        )?;
106
107        Ok(Self::with_properties(ReasonCode::Success, properties))
108    }
109
110    /// Creates an authentication failure response
111    ///
112    /// # Errors
113    ///
114    /// Returns an error if the operation fails
115    pub fn failure(reason_code: ReasonCode, reason_string: Option<String>) -> Result<Self> {
116        if reason_code.is_success() {
117            return Err(MqttError::ProtocolError(
118                "Cannot create failure AUTH packet with success reason code".to_string(),
119            ));
120        }
121
122        let mut properties = Properties::default();
123
124        if let Some(reason) = reason_string {
125            properties.add(
126                crate::protocol::v5::properties::PropertyId::ReasonString,
127                crate::protocol::v5::properties::PropertyValue::Utf8String(reason),
128            )?;
129        }
130
131        Ok(Self::with_properties(reason_code, properties))
132    }
133
134    #[must_use]
135    /// Gets the authentication method from properties
136    pub fn authentication_method(&self) -> Option<&str> {
137        if let Some(crate::protocol::v5::properties::PropertyValue::Utf8String(method)) = self
138            .properties
139            .get(crate::protocol::v5::properties::PropertyId::AuthenticationMethod)
140        {
141            Some(method)
142        } else {
143            None
144        }
145    }
146
147    #[must_use]
148    /// Gets the authentication data from properties
149    pub fn authentication_data(&self) -> Option<&[u8]> {
150        if let Some(crate::protocol::v5::properties::PropertyValue::BinaryData(data)) = self
151            .properties
152            .get(crate::protocol::v5::properties::PropertyId::AuthenticationData)
153        {
154            Some(data)
155        } else {
156            None
157        }
158    }
159
160    #[must_use]
161    /// Gets the reason string from properties
162    pub fn reason_string(&self) -> Option<&str> {
163        if let Some(crate::protocol::v5::properties::PropertyValue::Utf8String(reason)) = self
164            .properties
165            .get(crate::protocol::v5::properties::PropertyId::ReasonString)
166        {
167            Some(reason)
168        } else {
169            None
170        }
171    }
172
173    /// Validates the AUTH packet
174    ///
175    /// # Errors
176    ///
177    /// Returns an error if the operation fails
178    pub fn validate(&self) -> Result<()> {
179        // For AUTH packets, authentication method is required for Continue and ReAuth
180        if self.authentication_method().is_none()
181            && (self.reason_code == ReasonCode::ContinueAuthentication
182                || self.reason_code == ReasonCode::ReAuthenticate)
183        {
184            return Err(MqttError::ProtocolError(
185                "Authentication method is required for AUTH packets with ContinueAuthentication or ReAuthenticate reason codes".to_string()
186            ));
187        }
188
189        Ok(())
190    }
191}
192
193impl MqttPacket for AuthPacket {
194    fn packet_type(&self) -> PacketType {
195        PacketType::Auth
196    }
197
198    fn encode_body<B: BufMut>(&self, buf: &mut B) -> Result<()> {
199        self.validate()?;
200
201        // For AUTH packets, if reason code is Success and properties are empty,
202        // we can encode as an empty packet (remaining length = 0)
203        if self.reason_code == ReasonCode::Success && self.properties.is_empty() {
204            // Empty packet, nothing to encode
205            return Ok(());
206        }
207
208        // Encode reason code
209        buf.put_u8(u8::from(self.reason_code));
210
211        // Encode properties
212        self.properties.encode(buf)?;
213
214        Ok(())
215    }
216
217    fn decode_body<B: Buf>(buf: &mut B, fixed_header: &FixedHeader) -> Result<Self> {
218        if fixed_header.remaining_length == 0 {
219            // Empty AUTH packet defaults to Success with no properties
220            return Ok(Self::new(ReasonCode::Success));
221        }
222
223        // Decode reason code
224        if !buf.has_remaining() {
225            return Err(MqttError::MalformedPacket(
226                "Missing reason code in AUTH packet".to_string(),
227            ));
228        }
229
230        let reason_code_val = buf.get_u8();
231        let reason_code = ReasonCode::from_u8(reason_code_val)
232            .ok_or(MqttError::InvalidReasonCode(reason_code_val))?;
233
234        // Decode properties (if remaining length > 1)
235        let properties = if buf.has_remaining() {
236            Properties::decode(buf)?
237        } else {
238            Properties::default()
239        };
240
241        let packet = Self {
242            reason_code,
243            properties,
244        };
245
246        packet.validate()?;
247        Ok(packet)
248    }
249}
250
251#[cfg(test)]
252mod tests {
253    use super::*;
254    use crate::protocol::v5::properties::{PropertyId, PropertyValue};
255    use bytes::BytesMut;
256
257    #[test]
258    fn test_auth_packet_new() {
259        let packet = AuthPacket::new(ReasonCode::Success);
260        assert_eq!(packet.reason_code, ReasonCode::Success);
261        assert!(packet.properties.is_empty());
262    }
263
264    #[test]
265    fn test_auth_packet_continue_authentication() {
266        let packet = AuthPacket::continue_authentication(
267            "SCRAM-SHA-256".to_string(),
268            Some(b"challenge_data".to_vec()),
269        )
270        .unwrap();
271
272        assert_eq!(packet.reason_code, ReasonCode::ContinueAuthentication);
273        assert_eq!(packet.authentication_method(), Some("SCRAM-SHA-256"));
274        assert_eq!(
275            packet.authentication_data(),
276            Some(b"challenge_data".as_ref())
277        );
278    }
279
280    #[test]
281    fn test_auth_packet_re_authenticate() {
282        let packet = AuthPacket::re_authenticate("OAUTH2".to_string(), None).unwrap();
283
284        assert_eq!(packet.reason_code, ReasonCode::ReAuthenticate);
285        assert_eq!(packet.authentication_method(), Some("OAUTH2"));
286        assert!(packet.authentication_data().is_none());
287    }
288
289    #[test]
290    fn test_auth_packet_success() {
291        let packet = AuthPacket::success("PLAIN".to_string()).unwrap();
292
293        assert_eq!(packet.reason_code, ReasonCode::Success);
294        assert_eq!(packet.authentication_method(), Some("PLAIN"));
295    }
296
297    #[test]
298    fn test_auth_packet_failure() {
299        let packet = AuthPacket::failure(
300            ReasonCode::BadAuthenticationMethod,
301            Some("Unsupported method".to_string()),
302        )
303        .unwrap();
304
305        assert_eq!(packet.reason_code, ReasonCode::BadAuthenticationMethod);
306        assert_eq!(packet.reason_string(), Some("Unsupported method"));
307    }
308
309    #[test]
310    fn test_auth_packet_failure_with_success_code_fails() {
311        let result = AuthPacket::failure(ReasonCode::Success, None);
312        assert!(result.is_err());
313    }
314
315    #[test]
316    fn test_auth_packet_encode_decode_empty() {
317        let packet = AuthPacket::new(ReasonCode::Success);
318        let mut buf = BytesMut::new();
319
320        packet.encode(&mut buf).unwrap();
321
322        // Decode the complete packet (including fixed header)
323        let fixed_header = FixedHeader::decode(&mut buf).unwrap();
324        let decoded = AuthPacket::decode_body(&mut buf, &fixed_header).unwrap();
325
326        assert_eq!(decoded.reason_code, ReasonCode::Success);
327        assert!(decoded.properties.is_empty());
328    }
329
330    #[test]
331    fn test_auth_packet_encode_decode_with_properties() {
332        let packet = AuthPacket::continue_authentication(
333            "SCRAM-SHA-1".to_string(),
334            Some(b"server_nonce".to_vec()),
335        )
336        .unwrap();
337
338        let mut buf = BytesMut::new();
339        packet.encode(&mut buf).unwrap();
340
341        let fixed_header = FixedHeader::decode(&mut buf).unwrap();
342        let decoded = AuthPacket::decode_body(&mut buf, &fixed_header).unwrap();
343
344        assert_eq!(decoded.reason_code, ReasonCode::ContinueAuthentication);
345        assert_eq!(decoded.authentication_method(), Some("SCRAM-SHA-1"));
346        assert_eq!(
347            decoded.authentication_data(),
348            Some(b"server_nonce".as_ref())
349        );
350    }
351
352    #[test]
353    fn test_auth_packet_encode_decode_failure() {
354        let packet = AuthPacket::failure(
355            ReasonCode::NotAuthorized,
356            Some("Invalid credentials".to_string()),
357        )
358        .unwrap();
359
360        let mut buf = BytesMut::new();
361        packet.encode(&mut buf).unwrap();
362
363        let fixed_header = FixedHeader::decode(&mut buf).unwrap();
364        let decoded = AuthPacket::decode_body(&mut buf, &fixed_header).unwrap();
365
366        assert_eq!(decoded.reason_code, ReasonCode::NotAuthorized);
367        assert_eq!(decoded.reason_string(), Some("Invalid credentials"));
368    }
369
370    #[test]
371    fn test_auth_packet_validation_missing_auth_method() {
372        let packet = AuthPacket::new(ReasonCode::ContinueAuthentication);
373        // Packet without authentication method should be invalid
374        let result = packet.validate();
375        assert!(result.is_err());
376    }
377
378    #[test]
379    fn test_auth_packet_decode_malformed() {
380        let mut buf = BytesMut::new();
381        // Empty buffer should fail for packets with remaining length > 0
382
383        let result = AuthPacket::decode_body(&mut buf, &FixedHeader::new(PacketType::Auth, 0, 1));
384        assert!(result.is_err());
385    }
386
387    #[test]
388    fn test_auth_packet_decode_invalid_reason_code() {
389        let mut buf = BytesMut::new();
390        buf.put_u8(0xFF); // Invalid reason code
391        buf.put_u8(0x00); // Empty properties
392
393        let result = AuthPacket::decode_body(&mut buf, &FixedHeader::new(PacketType::Auth, 0, 2));
394        assert!(result.is_err());
395    }
396
397    #[test]
398    fn test_auth_packet_property_getters() {
399        let mut properties = Properties::default();
400        properties
401            .add(
402                PropertyId::AuthenticationMethod,
403                PropertyValue::Utf8String("TEST".to_string()),
404            )
405            .unwrap();
406        properties
407            .add(
408                PropertyId::AuthenticationData,
409                PropertyValue::BinaryData(b"data".to_vec().into()),
410            )
411            .unwrap();
412        properties
413            .add(
414                PropertyId::ReasonString,
415                PropertyValue::Utf8String("reason".to_string()),
416            )
417            .unwrap();
418
419        let packet = AuthPacket::with_properties(ReasonCode::Success, properties);
420
421        assert_eq!(packet.authentication_method(), Some("TEST"));
422        assert_eq!(packet.authentication_data(), Some(b"data".as_ref()));
423        assert_eq!(packet.reason_string(), Some("reason"));
424    }
425}