Skip to main content

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//! When polling 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").unwrap()
17//!     .with_privacy(PrivProtocol::Aes128, b"privpassword").unwrap();
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, PartialOrd, Ord, Hash)]
29pub enum CommunityVersion {
30    /// `SNMPv1`
31    V1,
32    /// `SNMPv2c`
33    #[default]
34    V2c,
35}
36
37/// Authentication configuration for SNMP clients.
38#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
39pub enum Auth {
40    /// Community string authentication (`SNMPv1` or v2c).
41    Community {
42        /// SNMP version (V1 or V2c)
43        version: CommunityVersion,
44        /// Community string
45        community: String,
46    },
47    /// User-based Security Model (`SNMPv3`).
48    Usm(UsmAuth),
49}
50
51impl Default for Auth {
52    /// Returns `Auth::v2c("public")`.
53    fn default() -> Self {
54        Auth::v2c("public")
55    }
56}
57
58impl Auth {
59    /// `SNMPv1` community authentication.
60    ///
61    /// Creates authentication configuration for `SNMPv1`, which only supports
62    /// community string authentication without encryption.
63    ///
64    /// # Example
65    ///
66    /// ```rust
67    /// use async_snmp::Auth;
68    ///
69    /// // Create SNMPv1 authentication with "private" community
70    /// let auth = Auth::v1("private");
71    /// ```
72    pub fn v1(community: impl Into<String>) -> Self {
73        Auth::Community {
74            version: CommunityVersion::V1,
75            community: community.into(),
76        }
77    }
78
79    /// `SNMPv2c` community authentication.
80    ///
81    /// Creates authentication configuration for `SNMPv2c`, which supports
82    /// community string authentication without encryption but adds GETBULK
83    /// and improved error handling over `SNMPv1`.
84    ///
85    /// # Example
86    ///
87    /// ```rust
88    /// use async_snmp::Auth;
89    ///
90    /// // Create SNMPv2c authentication with "public" community
91    /// let auth = Auth::v2c("public");
92    ///
93    /// // Auth::default() is equivalent to Auth::v2c("public")
94    /// let auth = Auth::default();
95    /// ```
96    pub fn v2c(community: impl Into<String>) -> Self {
97        Auth::Community {
98            version: CommunityVersion::V2c,
99            community: community.into(),
100        }
101    }
102
103    /// Start building `SNMPv3` USM authentication.
104    ///
105    /// Returns a builder that allows configuring authentication and privacy
106    /// protocols. `SNMPv3` supports three security levels:
107    /// - noAuthNoPriv: username only (no security)
108    /// - authNoPriv: username with authentication (integrity)
109    /// - authPriv: username with authentication and encryption (confidentiality)
110    ///
111    /// # Example
112    ///
113    /// ```rust
114    /// use async_snmp::{Auth, AuthProtocol, PrivProtocol};
115    ///
116    /// // noAuthNoPriv: username only
117    /// let auth: Auth = Auth::usm("readonly").into();
118    ///
119    /// // authNoPriv: with authentication
120    /// let auth: Auth = Auth::usm("admin")
121    ///     .auth(AuthProtocol::Sha256, "authpassword")
122    ///     .into();
123    ///
124    /// // authPriv: with authentication and encryption
125    /// let auth: Auth = Auth::usm("admin")
126    ///     .auth(AuthProtocol::Sha256, "authpassword")
127    ///     .privacy(PrivProtocol::Aes128, "privpassword")
128    ///     .into();
129    /// ```
130    pub fn usm(username: impl Into<String>) -> UsmBuilder {
131        UsmBuilder::new(username)
132    }
133}
134
135/// `SNMPv3` USM authentication parameters.
136#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
137pub struct UsmAuth {
138    /// `SNMPv3` username
139    pub username: String,
140    /// Authentication protocol (None for noAuthNoPriv)
141    pub auth_protocol: Option<AuthProtocol>,
142    /// Authentication password
143    pub auth_password: Option<String>,
144    /// Privacy protocol (None for noPriv)
145    pub priv_protocol: Option<PrivProtocol>,
146    /// Privacy password
147    pub priv_password: Option<String>,
148    /// `SNMPv3` context name for VACM context selection.
149    /// Most deployments use empty string (default).
150    pub context_name: Option<String>,
151    /// Pre-computed master keys for caching.
152    /// When set, passwords are ignored and keys are derived from master keys.
153    pub master_keys: Option<MasterKeys>,
154}
155
156/// Builder for `SNMPv3` USM authentication.
157#[derive(Debug)]
158pub struct UsmBuilder {
159    username: String,
160    auth: Option<(AuthProtocol, String)>,
161    privacy: Option<(PrivProtocol, String)>,
162    context_name: Option<String>,
163    master_keys: Option<MasterKeys>,
164}
165
166impl UsmBuilder {
167    /// Create a new USM builder with the given username.
168    ///
169    /// # Example
170    ///
171    /// ```rust
172    /// use async_snmp::Auth;
173    ///
174    /// let builder = Auth::usm("admin");
175    /// ```
176    pub fn new(username: impl Into<String>) -> Self {
177        Self {
178            username: username.into(),
179            auth: None,
180            privacy: None,
181            context_name: None,
182            master_keys: None,
183        }
184    }
185
186    /// Add authentication (authNoPriv or authPriv).
187    ///
188    /// This method performs the full key derivation (~850us for SHA-256) when
189    /// the client connects. When polling many engines with shared credentials,
190    /// consider using [`with_master_keys`](Self::with_master_keys) instead.
191    ///
192    /// # Supported Protocols
193    ///
194    /// - `AuthProtocol::Md5` - HMAC-MD5-96 (legacy)
195    /// - `AuthProtocol::Sha1` - HMAC-SHA-96 (legacy)
196    /// - `AuthProtocol::Sha224` - HMAC-SHA-224
197    /// - `AuthProtocol::Sha256` - HMAC-SHA-256
198    /// - `AuthProtocol::Sha384` - HMAC-SHA-384
199    /// - `AuthProtocol::Sha512` - HMAC-SHA-512
200    ///
201    /// # Example
202    ///
203    /// ```rust
204    /// use async_snmp::{Auth, AuthProtocol};
205    ///
206    /// let auth: Auth = Auth::usm("admin")
207    ///     .auth(AuthProtocol::Sha256, "mypassword")
208    ///     .into();
209    /// ```
210    #[must_use]
211    pub fn auth(mut self, protocol: AuthProtocol, password: impl Into<String>) -> Self {
212        self.auth = Some((protocol, password.into()));
213        self
214    }
215
216    /// Add privacy/encryption (authPriv).
217    ///
218    /// Privacy requires authentication; this is validated at connection time.
219    ///
220    /// # Supported Protocols
221    ///
222    /// - `PrivProtocol::Des` - DES-CBC (legacy, insecure)
223    /// - `PrivProtocol::Aes128` - AES-128-CFB
224    /// - `PrivProtocol::Aes192` - AES-192-CFB
225    /// - `PrivProtocol::Aes256` - AES-256-CFB
226    ///
227    /// # Example
228    ///
229    /// ```rust
230    /// use async_snmp::{Auth, AuthProtocol, PrivProtocol};
231    ///
232    /// let auth: Auth = Auth::usm("admin")
233    ///     .auth(AuthProtocol::Sha256, "authpassword")
234    ///     .privacy(PrivProtocol::Aes128, "privpassword")
235    ///     .into();
236    /// ```
237    #[must_use]
238    pub fn privacy(mut self, protocol: PrivProtocol, password: impl Into<String>) -> Self {
239        self.privacy = Some((protocol, password.into()));
240        self
241    }
242
243    /// Use pre-computed master keys for authentication and privacy.
244    ///
245    /// This is the efficient path when polling many engines with shared
246    /// credentials. The expensive password-to-key derivation
247    /// (~850μs) is done once when creating the [`MasterKeys`], and only the
248    /// cheap localization (~1μs) is performed per engine.
249    ///
250    /// When master keys are set, the [`auth`](Self::auth) and
251    /// [`privacy`](Self::privacy) methods are ignored.
252    ///
253    /// # Example
254    ///
255    /// ```rust
256    /// use async_snmp::{Auth, AuthProtocol, PrivProtocol, MasterKeys};
257    ///
258    /// // Derive master keys once
259    /// let master_keys = MasterKeys::new(AuthProtocol::Sha256, b"authpassword").unwrap()
260    ///     .with_privacy(PrivProtocol::Aes128, b"privpassword").unwrap();
261    ///
262    /// // Use with multiple clients
263    /// let auth: Auth = Auth::usm("admin")
264    ///     .with_master_keys(master_keys)
265    ///     .into();
266    /// ```
267    #[must_use]
268    pub fn with_master_keys(mut self, master_keys: MasterKeys) -> Self {
269        self.master_keys = Some(master_keys);
270        self
271    }
272
273    /// Set the `SNMPv3` context name for VACM context selection.
274    ///
275    /// The context name allows selecting different MIB views on the same agent.
276    /// Most deployments use empty string (default).
277    ///
278    /// # Example
279    ///
280    /// ```rust
281    /// use async_snmp::{Auth, AuthProtocol};
282    ///
283    /// let auth: Auth = Auth::usm("admin")
284    ///     .auth(AuthProtocol::Sha256, "password")
285    ///     .context_name("vlan100")
286    ///     .into();
287    /// ```
288    #[must_use]
289    pub fn context_name(mut self, name: impl Into<String>) -> Self {
290        self.context_name = Some(name.into());
291        self
292    }
293}
294
295impl From<UsmBuilder> for Auth {
296    fn from(b: UsmBuilder) -> Auth {
297        Auth::Usm(UsmAuth {
298            username: b.username,
299            auth_protocol: b
300                .master_keys
301                .as_ref()
302                .map(super::super::v3::auth::MasterKeys::auth_protocol)
303                .or(b.auth.as_ref().map(|(p, _)| *p)),
304            auth_password: b.auth.map(|(_, pw)| pw),
305            priv_protocol: b
306                .master_keys
307                .as_ref()
308                .and_then(super::super::v3::auth::MasterKeys::priv_protocol)
309                .or(b.privacy.as_ref().map(|(p, _)| *p)),
310            priv_password: b.privacy.map(|(_, pw)| pw),
311            context_name: b.context_name,
312            master_keys: b.master_keys,
313        })
314    }
315}
316
317#[cfg(test)]
318mod tests {
319    use super::*;
320
321    #[test]
322    fn test_default_auth() {
323        let auth = Auth::default();
324        match auth {
325            Auth::Community { version, community } => {
326                assert_eq!(version, CommunityVersion::V2c);
327                assert_eq!(community, "public");
328            }
329            Auth::Usm(_) => panic!("expected Community variant"),
330        }
331    }
332
333    #[test]
334    fn test_v1_auth() {
335        let auth = Auth::v1("private");
336        match auth {
337            Auth::Community { version, community } => {
338                assert_eq!(version, CommunityVersion::V1);
339                assert_eq!(community, "private");
340            }
341            Auth::Usm(_) => panic!("expected Community variant"),
342        }
343    }
344
345    #[test]
346    fn test_v2c_auth() {
347        let auth = Auth::v2c("secret");
348        match auth {
349            Auth::Community { version, community } => {
350                assert_eq!(version, CommunityVersion::V2c);
351                assert_eq!(community, "secret");
352            }
353            Auth::Usm(_) => panic!("expected Community variant"),
354        }
355    }
356
357    #[test]
358    fn test_community_version_default() {
359        let version = CommunityVersion::default();
360        assert_eq!(version, CommunityVersion::V2c);
361    }
362
363    #[test]
364    fn test_usm_no_auth_no_priv() {
365        let auth: Auth = Auth::usm("readonly").into();
366        match auth {
367            Auth::Usm(usm) => {
368                assert_eq!(usm.username, "readonly");
369                assert!(usm.auth_protocol.is_none());
370                assert!(usm.auth_password.is_none());
371                assert!(usm.priv_protocol.is_none());
372                assert!(usm.priv_password.is_none());
373                assert!(usm.context_name.is_none());
374            }
375            Auth::Community { .. } => panic!("expected Usm variant"),
376        }
377    }
378
379    #[test]
380    fn test_usm_auth_no_priv() {
381        let auth: Auth = Auth::usm("admin")
382            .auth(AuthProtocol::Sha256, "authpass123")
383            .into();
384        match auth {
385            Auth::Usm(usm) => {
386                assert_eq!(usm.username, "admin");
387                assert_eq!(usm.auth_protocol, Some(AuthProtocol::Sha256));
388                assert_eq!(usm.auth_password, Some("authpass123".to_string()));
389                assert!(usm.priv_protocol.is_none());
390                assert!(usm.priv_password.is_none());
391            }
392            Auth::Community { .. } => panic!("expected Usm variant"),
393        }
394    }
395
396    #[test]
397    fn test_usm_auth_priv() {
398        let auth: Auth = Auth::usm("admin")
399            .auth(AuthProtocol::Sha256, "authpass")
400            .privacy(PrivProtocol::Aes128, "privpass")
401            .into();
402        match auth {
403            Auth::Usm(usm) => {
404                assert_eq!(usm.username, "admin");
405                assert_eq!(usm.auth_protocol, Some(AuthProtocol::Sha256));
406                assert_eq!(usm.auth_password, Some("authpass".to_string()));
407                assert_eq!(usm.priv_protocol, Some(PrivProtocol::Aes128));
408                assert_eq!(usm.priv_password, Some("privpass".to_string()));
409            }
410            Auth::Community { .. } => panic!("expected Usm variant"),
411        }
412    }
413
414    #[test]
415    fn test_usm_with_context_name() {
416        let auth: Auth = Auth::usm("admin")
417            .auth(AuthProtocol::Sha256, "authpass")
418            .context_name("vlan100")
419            .into();
420        match auth {
421            Auth::Usm(usm) => {
422                assert_eq!(usm.username, "admin");
423                assert_eq!(usm.context_name, Some("vlan100".to_string()));
424            }
425            Auth::Community { .. } => panic!("expected Usm variant"),
426        }
427    }
428
429    #[test]
430    fn test_usm_builder_chaining() {
431        // Verify all methods can be chained
432        let auth: Auth = Auth::usm("user")
433            .auth(AuthProtocol::Sha512, "auth")
434            .privacy(PrivProtocol::Aes256, "priv")
435            .context_name("ctx")
436            .into();
437
438        match auth {
439            Auth::Usm(usm) => {
440                assert_eq!(usm.username, "user");
441                assert_eq!(usm.auth_protocol, Some(AuthProtocol::Sha512));
442                assert_eq!(usm.auth_password, Some("auth".to_string()));
443                assert_eq!(usm.priv_protocol, Some(PrivProtocol::Aes256));
444                assert_eq!(usm.priv_password, Some("priv".to_string()));
445                assert_eq!(usm.context_name, Some("ctx".to_string()));
446            }
447            Auth::Community { .. } => panic!("expected Usm variant"),
448        }
449    }
450}