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