async_snmp/v3/
mod.rs

1//! SNMPv3 security module.
2//!
3//! This module implements the User-based Security Model (USM) as defined
4//! in RFC 3414 and RFC 7860, including:
5//!
6//! - USM security parameters encoding/decoding
7//! - Key localization (password-to-key derivation)
8//! - Authentication (HMAC-MD5-96, HMAC-SHA-96, HMAC-SHA-224/256/384/512)
9//! - Privacy (DES-CBC, AES-128/192/256-CFB)
10//! - Engine discovery and time synchronization
11
12pub mod auth;
13mod engine;
14mod privacy;
15mod usm;
16
17pub use auth::{LocalizedKey, MasterKey, MasterKeys};
18pub use engine::{EngineCache, EngineState, TIME_WINDOW, parse_discovery_response};
19pub use engine::{
20    is_decryption_error_report, is_not_in_time_window_report, is_unknown_engine_id_report,
21    is_unknown_user_name_report, is_unsupported_sec_level_report, is_wrong_digest_report,
22};
23pub use privacy::{PrivKey, SaltCounter};
24pub use usm::UsmSecurityParams;
25
26/// Error returned when parsing a protocol name fails.
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct ParseProtocolError {
29    input: String,
30    kind: ProtocolKind,
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34enum ProtocolKind {
35    Auth,
36    Priv,
37}
38
39impl std::fmt::Display for ParseProtocolError {
40    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41        match self.kind {
42            ProtocolKind::Auth => write!(
43                f,
44                "unknown authentication protocol '{}'; expected one of: MD5, SHA, SHA-224, SHA-256, SHA-384, SHA-512",
45                self.input
46            ),
47            ProtocolKind::Priv => write!(
48                f,
49                "unknown privacy protocol '{}'; expected one of: DES, AES, AES-128, AES-192, AES-256",
50                self.input
51            ),
52        }
53    }
54}
55
56impl std::error::Error for ParseProtocolError {}
57
58/// Authentication protocol identifiers.
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
61pub enum AuthProtocol {
62    /// HMAC-MD5-96 (RFC 3414)
63    Md5,
64    /// HMAC-SHA-96 (RFC 3414)
65    Sha1,
66    /// HMAC-SHA-224 (RFC 7860)
67    Sha224,
68    /// HMAC-SHA-256 (RFC 7860)
69    Sha256,
70    /// HMAC-SHA-384 (RFC 7860)
71    Sha384,
72    /// HMAC-SHA-512 (RFC 7860)
73    Sha512,
74}
75
76impl std::fmt::Display for AuthProtocol {
77    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78        match self {
79            Self::Md5 => write!(f, "MD5"),
80            Self::Sha1 => write!(f, "SHA"),
81            Self::Sha224 => write!(f, "SHA-224"),
82            Self::Sha256 => write!(f, "SHA-256"),
83            Self::Sha384 => write!(f, "SHA-384"),
84            Self::Sha512 => write!(f, "SHA-512"),
85        }
86    }
87}
88
89impl std::str::FromStr for AuthProtocol {
90    type Err = ParseProtocolError;
91
92    fn from_str(s: &str) -> Result<Self, Self::Err> {
93        match s.to_ascii_uppercase().as_str() {
94            "MD5" => Ok(Self::Md5),
95            "SHA" | "SHA1" | "SHA-1" => Ok(Self::Sha1),
96            "SHA224" | "SHA-224" => Ok(Self::Sha224),
97            "SHA256" | "SHA-256" => Ok(Self::Sha256),
98            "SHA384" | "SHA-384" => Ok(Self::Sha384),
99            "SHA512" | "SHA-512" => Ok(Self::Sha512),
100            _ => Err(ParseProtocolError {
101                input: s.to_string(),
102                kind: ProtocolKind::Auth,
103            }),
104        }
105    }
106}
107
108impl AuthProtocol {
109    /// Get the digest output length in bytes.
110    ///
111    /// This is also the key length produced by the key localization algorithm,
112    /// which is used for privacy key derivation.
113    pub fn digest_len(self) -> usize {
114        match self {
115            Self::Md5 => 16,
116            Self::Sha1 => 20,
117            Self::Sha224 => 28,
118            Self::Sha256 => 32,
119            Self::Sha384 => 48,
120            Self::Sha512 => 64,
121        }
122    }
123
124    /// Get the truncated MAC length for authentication parameters.
125    pub fn mac_len(self) -> usize {
126        match self {
127            Self::Md5 | Self::Sha1 => 12, // HMAC-96
128            Self::Sha224 => 16,           // RFC 7860
129            Self::Sha256 => 24,           // RFC 7860
130            Self::Sha384 => 32,           // RFC 7860
131            Self::Sha512 => 48,           // RFC 7860
132        }
133    }
134
135    /// Check if this authentication protocol produces sufficient key material
136    /// for the given privacy protocol.
137    ///
138    /// Privacy keys are derived from the localized authentication key, so the
139    /// auth protocol must produce at least as many bytes as the privacy
140    /// protocol requires:
141    ///
142    /// | Privacy Protocol | Required Key Length | Compatible Auth Protocols |
143    /// |------------------|--------------------|-----------------------|
144    /// | DES              | 16 bytes           | All (MD5+)           |
145    /// | AES-128          | 16 bytes           | All (MD5+)           |
146    /// | AES-192          | 24 bytes           | SHA-224, SHA-256, SHA-384, SHA-512 |
147    /// | AES-256          | 32 bytes           | SHA-256, SHA-384, SHA-512 |
148    ///
149    /// # Example
150    ///
151    /// ```rust
152    /// use async_snmp::{AuthProtocol, PrivProtocol};
153    ///
154    /// // SHA-256 works with all privacy protocols
155    /// assert!(AuthProtocol::Sha256.is_compatible_with(PrivProtocol::Aes256));
156    ///
157    /// // SHA-1 doesn't produce enough key material for AES-256
158    /// assert!(!AuthProtocol::Sha1.is_compatible_with(PrivProtocol::Aes256));
159    /// ```
160    pub fn is_compatible_with(self, priv_protocol: PrivProtocol) -> bool {
161        self.digest_len() >= priv_protocol.key_len()
162    }
163}
164
165/// Privacy protocol identifiers.
166#[derive(Debug, Clone, Copy, PartialEq, Eq)]
167#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
168pub enum PrivProtocol {
169    /// DES-CBC (RFC 3414)
170    Des,
171    /// AES-128-CFB (RFC 3826)
172    Aes128,
173    /// AES-192-CFB (RFC 3826)
174    Aes192,
175    /// AES-256-CFB (RFC 3826)
176    Aes256,
177}
178
179impl std::fmt::Display for PrivProtocol {
180    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
181        match self {
182            Self::Des => write!(f, "DES"),
183            Self::Aes128 => write!(f, "AES"),
184            Self::Aes192 => write!(f, "AES-192"),
185            Self::Aes256 => write!(f, "AES-256"),
186        }
187    }
188}
189
190impl std::str::FromStr for PrivProtocol {
191    type Err = ParseProtocolError;
192
193    fn from_str(s: &str) -> Result<Self, Self::Err> {
194        match s.to_ascii_uppercase().as_str() {
195            "DES" => Ok(Self::Des),
196            "AES" | "AES128" | "AES-128" => Ok(Self::Aes128),
197            "AES192" | "AES-192" => Ok(Self::Aes192),
198            "AES256" | "AES-256" => Ok(Self::Aes256),
199            _ => Err(ParseProtocolError {
200                input: s.to_string(),
201                kind: ProtocolKind::Priv,
202            }),
203        }
204    }
205}
206
207impl PrivProtocol {
208    /// Get the key length in bytes.
209    pub fn key_len(self) -> usize {
210        match self {
211            Self::Des => 16, // 8 key + 8 pre-IV
212            Self::Aes128 => 16,
213            Self::Aes192 => 24,
214            Self::Aes256 => 32,
215        }
216    }
217
218    /// Get the IV/salt length in bytes.
219    pub fn salt_len(self) -> usize {
220        8 // All protocols use 8-byte salt
221    }
222
223    /// Get the minimum authentication protocol required for this privacy protocol.
224    ///
225    /// Returns the weakest auth protocol that produces sufficient key material.
226    ///
227    /// | Privacy Protocol | Minimum Auth Protocol |
228    /// |------------------|-----------------------|
229    /// | DES, AES-128     | MD5 (16 bytes)       |
230    /// | AES-192          | SHA-224 (28 bytes)   |
231    /// | AES-256          | SHA-256 (32 bytes)   |
232    pub fn min_auth_protocol(self) -> AuthProtocol {
233        match self {
234            Self::Des | Self::Aes128 => AuthProtocol::Md5,
235            Self::Aes192 => AuthProtocol::Sha224,
236            Self::Aes256 => AuthProtocol::Sha256,
237        }
238    }
239}
240
241#[cfg(test)]
242mod tests {
243    use super::*;
244
245    #[test]
246    fn test_auth_protocol_compatibility_all_with_des() {
247        // All auth protocols work with DES (requires 16 bytes)
248        assert!(AuthProtocol::Md5.is_compatible_with(PrivProtocol::Des));
249        assert!(AuthProtocol::Sha1.is_compatible_with(PrivProtocol::Des));
250        assert!(AuthProtocol::Sha224.is_compatible_with(PrivProtocol::Des));
251        assert!(AuthProtocol::Sha256.is_compatible_with(PrivProtocol::Des));
252        assert!(AuthProtocol::Sha384.is_compatible_with(PrivProtocol::Des));
253        assert!(AuthProtocol::Sha512.is_compatible_with(PrivProtocol::Des));
254    }
255
256    #[test]
257    fn test_auth_protocol_compatibility_all_with_aes128() {
258        // All auth protocols work with AES-128 (requires 16 bytes)
259        assert!(AuthProtocol::Md5.is_compatible_with(PrivProtocol::Aes128));
260        assert!(AuthProtocol::Sha1.is_compatible_with(PrivProtocol::Aes128));
261        assert!(AuthProtocol::Sha224.is_compatible_with(PrivProtocol::Aes128));
262        assert!(AuthProtocol::Sha256.is_compatible_with(PrivProtocol::Aes128));
263        assert!(AuthProtocol::Sha384.is_compatible_with(PrivProtocol::Aes128));
264        assert!(AuthProtocol::Sha512.is_compatible_with(PrivProtocol::Aes128));
265    }
266
267    #[test]
268    fn test_auth_protocol_compatibility_with_aes192() {
269        // AES-192 requires 24 bytes - only SHA-224+ work
270        assert!(!AuthProtocol::Md5.is_compatible_with(PrivProtocol::Aes192)); // 16 < 24
271        assert!(!AuthProtocol::Sha1.is_compatible_with(PrivProtocol::Aes192)); // 20 < 24
272        assert!(AuthProtocol::Sha224.is_compatible_with(PrivProtocol::Aes192)); // 28 >= 24
273        assert!(AuthProtocol::Sha256.is_compatible_with(PrivProtocol::Aes192)); // 32 >= 24
274        assert!(AuthProtocol::Sha384.is_compatible_with(PrivProtocol::Aes192)); // 48 >= 24
275        assert!(AuthProtocol::Sha512.is_compatible_with(PrivProtocol::Aes192)); // 64 >= 24
276    }
277
278    #[test]
279    fn test_auth_protocol_compatibility_with_aes256() {
280        // AES-256 requires 32 bytes - only SHA-256+ work
281        assert!(!AuthProtocol::Md5.is_compatible_with(PrivProtocol::Aes256)); // 16 < 32
282        assert!(!AuthProtocol::Sha1.is_compatible_with(PrivProtocol::Aes256)); // 20 < 32
283        assert!(!AuthProtocol::Sha224.is_compatible_with(PrivProtocol::Aes256)); // 28 < 32
284        assert!(AuthProtocol::Sha256.is_compatible_with(PrivProtocol::Aes256)); // 32 >= 32
285        assert!(AuthProtocol::Sha384.is_compatible_with(PrivProtocol::Aes256)); // 48 >= 32
286        assert!(AuthProtocol::Sha512.is_compatible_with(PrivProtocol::Aes256)); // 64 >= 32
287    }
288
289    #[test]
290    fn test_priv_protocol_min_auth_protocol() {
291        assert_eq!(PrivProtocol::Des.min_auth_protocol(), AuthProtocol::Md5);
292        assert_eq!(PrivProtocol::Aes128.min_auth_protocol(), AuthProtocol::Md5);
293        assert_eq!(
294            PrivProtocol::Aes192.min_auth_protocol(),
295            AuthProtocol::Sha224
296        );
297        assert_eq!(
298            PrivProtocol::Aes256.min_auth_protocol(),
299            AuthProtocol::Sha256
300        );
301    }
302
303    #[test]
304    fn test_auth_protocol_display() {
305        assert_eq!(format!("{}", AuthProtocol::Md5), "MD5");
306        assert_eq!(format!("{}", AuthProtocol::Sha1), "SHA");
307        assert_eq!(format!("{}", AuthProtocol::Sha224), "SHA-224");
308        assert_eq!(format!("{}", AuthProtocol::Sha256), "SHA-256");
309        assert_eq!(format!("{}", AuthProtocol::Sha384), "SHA-384");
310        assert_eq!(format!("{}", AuthProtocol::Sha512), "SHA-512");
311    }
312
313    #[test]
314    fn test_auth_protocol_from_str() {
315        assert_eq!("MD5".parse::<AuthProtocol>().unwrap(), AuthProtocol::Md5);
316        assert_eq!("md5".parse::<AuthProtocol>().unwrap(), AuthProtocol::Md5);
317        assert_eq!("SHA".parse::<AuthProtocol>().unwrap(), AuthProtocol::Sha1);
318        assert_eq!("sha1".parse::<AuthProtocol>().unwrap(), AuthProtocol::Sha1);
319        assert_eq!("SHA-1".parse::<AuthProtocol>().unwrap(), AuthProtocol::Sha1);
320        assert_eq!(
321            "sha-224".parse::<AuthProtocol>().unwrap(),
322            AuthProtocol::Sha224
323        );
324        assert_eq!(
325            "SHA256".parse::<AuthProtocol>().unwrap(),
326            AuthProtocol::Sha256
327        );
328        assert_eq!(
329            "SHA-256".parse::<AuthProtocol>().unwrap(),
330            AuthProtocol::Sha256
331        );
332        assert_eq!(
333            "sha384".parse::<AuthProtocol>().unwrap(),
334            AuthProtocol::Sha384
335        );
336        assert_eq!(
337            "SHA-512".parse::<AuthProtocol>().unwrap(),
338            AuthProtocol::Sha512
339        );
340
341        assert!("invalid".parse::<AuthProtocol>().is_err());
342    }
343
344    #[test]
345    fn test_priv_protocol_display() {
346        assert_eq!(format!("{}", PrivProtocol::Des), "DES");
347        assert_eq!(format!("{}", PrivProtocol::Aes128), "AES");
348        assert_eq!(format!("{}", PrivProtocol::Aes192), "AES-192");
349        assert_eq!(format!("{}", PrivProtocol::Aes256), "AES-256");
350    }
351
352    #[test]
353    fn test_priv_protocol_from_str() {
354        assert_eq!("DES".parse::<PrivProtocol>().unwrap(), PrivProtocol::Des);
355        assert_eq!("des".parse::<PrivProtocol>().unwrap(), PrivProtocol::Des);
356        assert_eq!("AES".parse::<PrivProtocol>().unwrap(), PrivProtocol::Aes128);
357        assert_eq!("aes".parse::<PrivProtocol>().unwrap(), PrivProtocol::Aes128);
358        assert_eq!(
359            "AES128".parse::<PrivProtocol>().unwrap(),
360            PrivProtocol::Aes128
361        );
362        assert_eq!(
363            "AES-128".parse::<PrivProtocol>().unwrap(),
364            PrivProtocol::Aes128
365        );
366        assert_eq!(
367            "aes192".parse::<PrivProtocol>().unwrap(),
368            PrivProtocol::Aes192
369        );
370        assert_eq!(
371            "AES-192".parse::<PrivProtocol>().unwrap(),
372            PrivProtocol::Aes192
373        );
374        assert_eq!(
375            "aes256".parse::<PrivProtocol>().unwrap(),
376            PrivProtocol::Aes256
377        );
378        assert_eq!(
379            "AES-256".parse::<PrivProtocol>().unwrap(),
380            PrivProtocol::Aes256
381        );
382
383        assert!("invalid".parse::<PrivProtocol>().is_err());
384    }
385
386    #[test]
387    fn test_parse_protocol_error_display() {
388        let err = "bogus".parse::<AuthProtocol>().unwrap_err();
389        assert!(err.to_string().contains("bogus"));
390        assert!(err.to_string().contains("authentication protocol"));
391
392        let err = "bogus".parse::<PrivProtocol>().unwrap_err();
393        assert!(err.to_string().contains("bogus"));
394        assert!(err.to_string().contains("privacy protocol"));
395    }
396}