async_snmp/client/
auth.rs

1//! Authentication configuration types for the SNMP client.
2//!
3//! This module provides the [`Auth`] enum for specifying authentication
4//! configuration, supporting SNMPv1/v2c community strings and SNMPv3 USM.
5//!
6//! # Master Key Caching
7//!
8//! For high-throughput polling of many engines with shared credentials, use
9//! [`MasterKeys`] to cache the expensive password-to-key
10//! derivation:
11//!
12//! ```rust
13//! use async_snmp::{Auth, AuthProtocol, PrivProtocol, MasterKeys};
14//!
15//! // Derive master keys once (expensive: ~850μs for SHA-256)
16//! let master_keys = MasterKeys::new(AuthProtocol::Sha256, b"authpassword")
17//!     .with_privacy(PrivProtocol::Aes128, b"privpassword");
18//!
19//! // Use with USM builder - localization is cheap (~1μs per engine)
20//! let auth: Auth = Auth::usm("admin")
21//!     .with_master_keys(master_keys)
22//!     .into();
23//! ```
24
25use crate::v3::{AuthProtocol, MasterKeys, PrivProtocol};
26
27/// SNMP version for community-based authentication.
28#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
29#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
30pub enum CommunityVersion {
31    /// SNMPv1
32    V1,
33    /// SNMPv2c
34    #[default]
35    V2c,
36}
37
38/// Authentication configuration for SNMP clients.
39#[derive(Debug, Clone)]
40#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
41#[cfg_attr(feature = "serde", serde(untagged))]
42pub enum Auth {
43    /// Community string authentication (SNMPv1 or v2c).
44    Community {
45        /// SNMP version (V1 or V2c)
46        #[cfg_attr(feature = "serde", serde(default))]
47        version: CommunityVersion,
48        /// Community string
49        community: String,
50    },
51    /// User-based Security Model (SNMPv3).
52    Usm(UsmAuth),
53}
54
55impl Default for Auth {
56    fn default() -> Self {
57        Auth::v2c("public")
58    }
59}
60
61impl Auth {
62    /// SNMPv1 community authentication.
63    ///
64    /// Creates authentication configuration for SNMPv1, which only supports
65    /// community string authentication without encryption.
66    ///
67    /// # Example
68    ///
69    /// ```rust
70    /// use async_snmp::Auth;
71    ///
72    /// // Create SNMPv1 authentication with "private" community
73    /// let auth = Auth::v1("private");
74    /// ```
75    pub fn v1(community: impl Into<String>) -> Self {
76        Auth::Community {
77            version: CommunityVersion::V1,
78            community: community.into(),
79        }
80    }
81
82    /// SNMPv2c community authentication.
83    ///
84    /// Creates authentication configuration for SNMPv2c, which supports
85    /// community string authentication without encryption but adds GETBULK
86    /// and improved error handling over SNMPv1.
87    ///
88    /// # Example
89    ///
90    /// ```rust
91    /// use async_snmp::Auth;
92    ///
93    /// // Create SNMPv2c authentication with "public" community
94    /// let auth = Auth::v2c("public");
95    ///
96    /// // Auth::default() is equivalent to Auth::v2c("public")
97    /// let auth = Auth::default();
98    /// ```
99    pub fn v2c(community: impl Into<String>) -> Self {
100        Auth::Community {
101            version: CommunityVersion::V2c,
102            community: community.into(),
103        }
104    }
105
106    /// Start building SNMPv3 USM authentication.
107    ///
108    /// Returns a builder that allows configuring authentication and privacy
109    /// protocols. SNMPv3 supports three security levels:
110    /// - noAuthNoPriv: username only (no security)
111    /// - authNoPriv: username with authentication (integrity)
112    /// - authPriv: username with authentication and encryption (confidentiality)
113    ///
114    /// # Example
115    ///
116    /// ```rust
117    /// use async_snmp::{Auth, AuthProtocol, PrivProtocol};
118    ///
119    /// // noAuthNoPriv: username only
120    /// let auth: Auth = Auth::usm("readonly").into();
121    ///
122    /// // authNoPriv: with authentication
123    /// let auth: Auth = Auth::usm("admin")
124    ///     .auth(AuthProtocol::Sha256, "authpassword")
125    ///     .into();
126    ///
127    /// // authPriv: with authentication and encryption
128    /// let auth: Auth = Auth::usm("admin")
129    ///     .auth(AuthProtocol::Sha256, "authpassword")
130    ///     .privacy(PrivProtocol::Aes128, "privpassword")
131    ///     .into();
132    /// ```
133    pub fn usm(username: impl Into<String>) -> UsmBuilder {
134        UsmBuilder::new(username)
135    }
136}
137
138/// SNMPv3 USM authentication parameters.
139#[derive(Debug, Clone)]
140#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
141pub struct UsmAuth {
142    /// SNMPv3 username
143    pub username: String,
144    /// Authentication protocol (None for noAuthNoPriv)
145    #[cfg_attr(
146        feature = "serde",
147        serde(default, skip_serializing_if = "Option::is_none")
148    )]
149    pub auth_protocol: Option<AuthProtocol>,
150    /// Authentication password
151    #[cfg_attr(
152        feature = "serde",
153        serde(default, skip_serializing_if = "Option::is_none")
154    )]
155    pub auth_password: Option<String>,
156    /// Privacy protocol (None for noPriv)
157    #[cfg_attr(
158        feature = "serde",
159        serde(default, skip_serializing_if = "Option::is_none")
160    )]
161    pub priv_protocol: Option<PrivProtocol>,
162    /// Privacy password
163    #[cfg_attr(
164        feature = "serde",
165        serde(default, skip_serializing_if = "Option::is_none")
166    )]
167    pub priv_password: Option<String>,
168    /// SNMPv3 context name for VACM context selection.
169    /// Most deployments use empty string (default).
170    #[cfg_attr(
171        feature = "serde",
172        serde(default, skip_serializing_if = "Option::is_none")
173    )]
174    pub context_name: Option<String>,
175    /// Pre-computed master keys for caching.
176    /// When set, passwords are ignored and keys are derived from master keys.
177    #[cfg_attr(feature = "serde", serde(skip))]
178    pub master_keys: Option<MasterKeys>,
179}
180
181/// Builder for SNMPv3 USM authentication.
182pub struct UsmBuilder {
183    username: String,
184    auth: Option<(AuthProtocol, String)>,
185    privacy: Option<(PrivProtocol, String)>,
186    context_name: Option<String>,
187    master_keys: Option<MasterKeys>,
188}
189
190impl UsmBuilder {
191    /// Create a new USM builder with the given username.
192    ///
193    /// # Example
194    ///
195    /// ```rust
196    /// use async_snmp::Auth;
197    ///
198    /// let builder = Auth::usm("admin");
199    /// ```
200    pub fn new(username: impl Into<String>) -> Self {
201        Self {
202            username: username.into(),
203            auth: None,
204            privacy: None,
205            context_name: None,
206            master_keys: None,
207        }
208    }
209
210    /// Add authentication (authNoPriv or authPriv).
211    ///
212    /// This method performs the full key derivation (~850us for SHA-256) when
213    /// the client connects. For high-throughput polling of many engines,
214    /// consider using [`with_master_keys`](Self::with_master_keys) instead.
215    ///
216    /// # Supported Protocols
217    ///
218    /// - `AuthProtocol::Md5` - MD5 (legacy, not recommended)
219    /// - `AuthProtocol::Sha1` - SHA-1 (legacy)
220    /// - `AuthProtocol::Sha224` - SHA-224
221    /// - `AuthProtocol::Sha256` - SHA-256 (recommended)
222    /// - `AuthProtocol::Sha384` - SHA-384
223    /// - `AuthProtocol::Sha512` - SHA-512
224    ///
225    /// # Example
226    ///
227    /// ```rust
228    /// use async_snmp::{Auth, AuthProtocol};
229    ///
230    /// let auth: Auth = Auth::usm("admin")
231    ///     .auth(AuthProtocol::Sha256, "mypassword")
232    ///     .into();
233    /// ```
234    pub fn auth(mut self, protocol: AuthProtocol, password: impl Into<String>) -> Self {
235        self.auth = Some((protocol, password.into()));
236        self
237    }
238
239    /// Add privacy/encryption (authPriv).
240    ///
241    /// Privacy requires authentication; this is validated at connection time.
242    ///
243    /// # Supported Protocols
244    ///
245    /// - `PrivProtocol::Des` - DES (legacy, not recommended)
246    /// - `PrivProtocol::Aes128` - AES-128 (recommended)
247    /// - `PrivProtocol::Aes192` - AES-192
248    /// - `PrivProtocol::Aes256` - AES-256
249    ///
250    /// # Example
251    ///
252    /// ```rust
253    /// use async_snmp::{Auth, AuthProtocol, PrivProtocol};
254    ///
255    /// let auth: Auth = Auth::usm("admin")
256    ///     .auth(AuthProtocol::Sha256, "authpassword")
257    ///     .privacy(PrivProtocol::Aes128, "privpassword")
258    ///     .into();
259    /// ```
260    pub fn privacy(mut self, protocol: PrivProtocol, password: impl Into<String>) -> Self {
261        self.privacy = Some((protocol, password.into()));
262        self
263    }
264
265    /// Use pre-computed master keys for authentication and privacy.
266    ///
267    /// This is the efficient path for high-throughput polling of many engines
268    /// with shared credentials. The expensive password-to-key derivation
269    /// (~850μs) is done once when creating the [`MasterKeys`], and only the
270    /// cheap localization (~1μs) is performed per engine.
271    ///
272    /// When master keys are set, the [`auth`](Self::auth) and
273    /// [`privacy`](Self::privacy) methods are ignored.
274    ///
275    /// # Example
276    ///
277    /// ```rust
278    /// use async_snmp::{Auth, AuthProtocol, PrivProtocol, MasterKeys};
279    ///
280    /// // Derive master keys once
281    /// let master_keys = MasterKeys::new(AuthProtocol::Sha256, b"authpassword")
282    ///     .with_privacy(PrivProtocol::Aes128, b"privpassword");
283    ///
284    /// // Use with multiple clients
285    /// let auth: Auth = Auth::usm("admin")
286    ///     .with_master_keys(master_keys)
287    ///     .into();
288    /// ```
289    pub fn with_master_keys(mut self, master_keys: MasterKeys) -> Self {
290        self.master_keys = Some(master_keys);
291        self
292    }
293
294    /// Set the SNMPv3 context name for VACM context selection.
295    ///
296    /// The context name allows selecting different MIB views on the same agent.
297    /// Most deployments use empty string (default).
298    ///
299    /// # Example
300    ///
301    /// ```rust
302    /// use async_snmp::{Auth, AuthProtocol};
303    ///
304    /// let auth: Auth = Auth::usm("admin")
305    ///     .auth(AuthProtocol::Sha256, "password")
306    ///     .context_name("vlan100")
307    ///     .into();
308    /// ```
309    pub fn context_name(mut self, name: impl Into<String>) -> Self {
310        self.context_name = Some(name.into());
311        self
312    }
313}
314
315impl From<UsmBuilder> for Auth {
316    fn from(b: UsmBuilder) -> Auth {
317        Auth::Usm(UsmAuth {
318            username: b.username,
319            auth_protocol: b
320                .master_keys
321                .as_ref()
322                .map(|m| m.auth_protocol())
323                .or(b.auth.as_ref().map(|(p, _)| *p)),
324            auth_password: b.auth.map(|(_, pw)| pw),
325            priv_protocol: b
326                .master_keys
327                .as_ref()
328                .and_then(|m| m.priv_protocol())
329                .or(b.privacy.as_ref().map(|(p, _)| *p)),
330            priv_password: b.privacy.map(|(_, pw)| pw),
331            context_name: b.context_name,
332            master_keys: b.master_keys,
333        })
334    }
335}
336
337#[cfg(test)]
338mod tests {
339    use super::*;
340
341    #[test]
342    fn test_default_auth() {
343        let auth = Auth::default();
344        match auth {
345            Auth::Community { version, community } => {
346                assert_eq!(version, CommunityVersion::V2c);
347                assert_eq!(community, "public");
348            }
349            _ => panic!("expected Community variant"),
350        }
351    }
352
353    #[test]
354    fn test_v1_auth() {
355        let auth = Auth::v1("private");
356        match auth {
357            Auth::Community { version, community } => {
358                assert_eq!(version, CommunityVersion::V1);
359                assert_eq!(community, "private");
360            }
361            _ => panic!("expected Community variant"),
362        }
363    }
364
365    #[test]
366    fn test_v2c_auth() {
367        let auth = Auth::v2c("secret");
368        match auth {
369            Auth::Community { version, community } => {
370                assert_eq!(version, CommunityVersion::V2c);
371                assert_eq!(community, "secret");
372            }
373            _ => panic!("expected Community variant"),
374        }
375    }
376
377    #[test]
378    fn test_community_version_default() {
379        let version = CommunityVersion::default();
380        assert_eq!(version, CommunityVersion::V2c);
381    }
382
383    #[test]
384    fn test_usm_no_auth_no_priv() {
385        let auth: Auth = Auth::usm("readonly").into();
386        match auth {
387            Auth::Usm(usm) => {
388                assert_eq!(usm.username, "readonly");
389                assert!(usm.auth_protocol.is_none());
390                assert!(usm.auth_password.is_none());
391                assert!(usm.priv_protocol.is_none());
392                assert!(usm.priv_password.is_none());
393                assert!(usm.context_name.is_none());
394            }
395            _ => panic!("expected Usm variant"),
396        }
397    }
398
399    #[test]
400    fn test_usm_auth_no_priv() {
401        let auth: Auth = Auth::usm("admin")
402            .auth(AuthProtocol::Sha256, "authpass123")
403            .into();
404        match auth {
405            Auth::Usm(usm) => {
406                assert_eq!(usm.username, "admin");
407                assert_eq!(usm.auth_protocol, Some(AuthProtocol::Sha256));
408                assert_eq!(usm.auth_password, Some("authpass123".to_string()));
409                assert!(usm.priv_protocol.is_none());
410                assert!(usm.priv_password.is_none());
411            }
412            _ => panic!("expected Usm variant"),
413        }
414    }
415
416    #[test]
417    fn test_usm_auth_priv() {
418        let auth: Auth = Auth::usm("admin")
419            .auth(AuthProtocol::Sha256, "authpass")
420            .privacy(PrivProtocol::Aes128, "privpass")
421            .into();
422        match auth {
423            Auth::Usm(usm) => {
424                assert_eq!(usm.username, "admin");
425                assert_eq!(usm.auth_protocol, Some(AuthProtocol::Sha256));
426                assert_eq!(usm.auth_password, Some("authpass".to_string()));
427                assert_eq!(usm.priv_protocol, Some(PrivProtocol::Aes128));
428                assert_eq!(usm.priv_password, Some("privpass".to_string()));
429            }
430            _ => panic!("expected Usm variant"),
431        }
432    }
433
434    #[test]
435    fn test_usm_with_context_name() {
436        let auth: Auth = Auth::usm("admin")
437            .auth(AuthProtocol::Sha256, "authpass")
438            .context_name("vlan100")
439            .into();
440        match auth {
441            Auth::Usm(usm) => {
442                assert_eq!(usm.username, "admin");
443                assert_eq!(usm.context_name, Some("vlan100".to_string()));
444            }
445            _ => panic!("expected Usm variant"),
446        }
447    }
448
449    #[test]
450    fn test_usm_builder_chaining() {
451        // Verify all methods can be chained
452        let auth: Auth = Auth::usm("user")
453            .auth(AuthProtocol::Sha512, "auth")
454            .privacy(PrivProtocol::Aes256, "priv")
455            .context_name("ctx")
456            .into();
457
458        match auth {
459            Auth::Usm(usm) => {
460                assert_eq!(usm.username, "user");
461                assert_eq!(usm.auth_protocol, Some(AuthProtocol::Sha512));
462                assert_eq!(usm.auth_password, Some("auth".to_string()));
463                assert_eq!(usm.priv_protocol, Some(PrivProtocol::Aes256));
464                assert_eq!(usm.priv_password, Some("priv".to_string()));
465                assert_eq!(usm.context_name, Some("ctx".to_string()));
466            }
467            _ => panic!("expected Usm variant"),
468        }
469    }
470}
471
472#[cfg(all(test, feature = "serde"))]
473mod serde_tests {
474    use super::*;
475
476    #[test]
477    fn test_community_v2c_roundtrip() {
478        let auth = Auth::v2c("public");
479        let json = serde_json::to_string(&auth).unwrap();
480        let back: Auth = serde_json::from_str(&json).unwrap();
481
482        match back {
483            Auth::Community { version, community } => {
484                assert_eq!(version, CommunityVersion::V2c);
485                assert_eq!(community, "public");
486            }
487            _ => panic!("expected Community variant"),
488        }
489    }
490
491    #[test]
492    fn test_community_v1_roundtrip() {
493        let auth = Auth::v1("private");
494        let json = serde_json::to_string(&auth).unwrap();
495        let back: Auth = serde_json::from_str(&json).unwrap();
496
497        match back {
498            Auth::Community { version, community } => {
499                assert_eq!(version, CommunityVersion::V1);
500                assert_eq!(community, "private");
501            }
502            _ => panic!("expected Community variant"),
503        }
504    }
505
506    #[test]
507    fn test_usm_no_auth_roundtrip() {
508        let auth: Auth = Auth::usm("readonly").into();
509        let json = serde_json::to_string(&auth).unwrap();
510        let back: Auth = serde_json::from_str(&json).unwrap();
511
512        match back {
513            Auth::Usm(usm) => {
514                assert_eq!(usm.username, "readonly");
515                assert!(usm.auth_protocol.is_none());
516                assert!(usm.auth_password.is_none());
517                assert!(usm.priv_protocol.is_none());
518                assert!(usm.priv_password.is_none());
519            }
520            _ => panic!("expected Usm variant"),
521        }
522    }
523
524    #[test]
525    fn test_usm_auth_priv_roundtrip() {
526        let auth: Auth = Auth::usm("admin")
527            .auth(AuthProtocol::Sha256, "authpass")
528            .privacy(PrivProtocol::Aes128, "privpass")
529            .context_name("vlan100")
530            .into();
531
532        let json = serde_json::to_string(&auth).unwrap();
533        let back: Auth = serde_json::from_str(&json).unwrap();
534
535        match back {
536            Auth::Usm(usm) => {
537                assert_eq!(usm.username, "admin");
538                assert_eq!(usm.auth_protocol, Some(AuthProtocol::Sha256));
539                assert_eq!(usm.auth_password, Some("authpass".to_string()));
540                assert_eq!(usm.priv_protocol, Some(PrivProtocol::Aes128));
541                assert_eq!(usm.priv_password, Some("privpass".to_string()));
542                assert_eq!(usm.context_name, Some("vlan100".to_string()));
543            }
544            _ => panic!("expected Usm variant"),
545        }
546    }
547
548    #[test]
549    fn test_community_deserialize_without_version() {
550        // When deserializing, version should default to V2c if not present
551        let json = r#"{"community":"public"}"#;
552        let auth: Auth = serde_json::from_str(json).unwrap();
553
554        match auth {
555            Auth::Community { version, community } => {
556                assert_eq!(version, CommunityVersion::V2c);
557                assert_eq!(community, "public");
558            }
559            _ => panic!("expected Community variant"),
560        }
561    }
562
563    #[test]
564    fn test_usm_optional_fields_not_serialized_when_none() {
565        let auth: Auth = Auth::usm("readonly").into();
566        let json = serde_json::to_string(&auth).unwrap();
567
568        // Should only contain username, no None fields
569        assert!(json.contains("username"));
570        assert!(!json.contains("auth_protocol"));
571        assert!(!json.contains("auth_password"));
572        assert!(!json.contains("priv_protocol"));
573        assert!(!json.contains("priv_password"));
574        assert!(!json.contains("context_name"));
575    }
576
577    #[test]
578    fn test_walk_mode_roundtrip() {
579        use crate::client::walk::WalkMode;
580
581        let modes = [WalkMode::Auto, WalkMode::GetNext, WalkMode::GetBulk];
582
583        for mode in modes {
584            let json = serde_json::to_string(&mode).unwrap();
585            let back: WalkMode = serde_json::from_str(&json).unwrap();
586            assert_eq!(back, mode);
587        }
588    }
589
590    #[test]
591    fn test_oid_ordering_roundtrip() {
592        use crate::client::walk::OidOrdering;
593
594        let orderings = [OidOrdering::Strict, OidOrdering::AllowNonIncreasing];
595
596        for ordering in orderings {
597            let json = serde_json::to_string(&ordering).unwrap();
598            let back: OidOrdering = serde_json::from_str(&json).unwrap();
599            assert_eq!(back, ordering);
600        }
601    }
602
603    #[test]
604    fn test_auth_protocol_roundtrip() {
605        let protocols = [
606            AuthProtocol::Md5,
607            AuthProtocol::Sha1,
608            AuthProtocol::Sha224,
609            AuthProtocol::Sha256,
610            AuthProtocol::Sha384,
611            AuthProtocol::Sha512,
612        ];
613
614        for proto in protocols {
615            let json = serde_json::to_string(&proto).unwrap();
616            let back: AuthProtocol = serde_json::from_str(&json).unwrap();
617            assert_eq!(back, proto);
618        }
619    }
620
621    #[test]
622    fn test_priv_protocol_roundtrip() {
623        let protocols = [
624            PrivProtocol::Des,
625            PrivProtocol::Aes128,
626            PrivProtocol::Aes192,
627            PrivProtocol::Aes256,
628        ];
629
630        for proto in protocols {
631            let json = serde_json::to_string(&proto).unwrap();
632            let back: PrivProtocol = serde_json::from_str(&json).unwrap();
633            assert_eq!(back, proto);
634        }
635    }
636}