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 the shared USM config - 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::UsmConfig;
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///
39/// The [`Debug`] implementation redacts community strings so that credentials
40/// are not leaked through logs or diagnostics.
41#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
42pub enum Auth {
43    /// Community string authentication (`SNMPv1` or v2c).
44    Community {
45        /// SNMP version (V1 or V2c)
46        version: CommunityVersion,
47        /// Community string
48        community: String,
49    },
50    /// User-based Security Model (`SNMPv3`).
51    Usm(UsmConfig),
52}
53
54impl Default for Auth {
55    /// Returns `Auth::v2c("public")`.
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    /// Create an `SNMPv3` USM configuration.
107    ///
108    /// Returns the shared [`UsmConfig`] used by clients, agents, notification
109    /// receivers, and trap sinks. `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_priv(
130    ///         AuthProtocol::Sha256,
131    ///         "authpassword",
132    ///         PrivProtocol::Aes128,
133    ///         "privpassword",
134    ///     )
135    ///     .into();
136    /// ```
137    pub fn usm(username: impl Into<String>) -> UsmConfig {
138        UsmConfig::new(bytes::Bytes::from(username.into()))
139    }
140}
141
142impl From<UsmConfig> for Auth {
143    fn from(config: UsmConfig) -> Self {
144        Self::Usm(config)
145    }
146}
147
148/// Placeholder printed in place of a redacted secret value.
149const REDACTED: &str = "[REDACTED]";
150
151impl std::fmt::Debug for Auth {
152    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
153        match self {
154            Auth::Community { version, .. } => f
155                .debug_struct("Auth::Community")
156                .field("version", version)
157                .field("community", &REDACTED)
158                .finish(),
159            Auth::Usm(usm) => f.debug_tuple("Auth::Usm").field(usm).finish(),
160        }
161    }
162}
163
164#[cfg(test)]
165mod tests {
166    use super::*;
167    use crate::message::SecurityLevel;
168    use crate::v3::{AuthProtocol, PrivProtocol};
169
170    #[test]
171    fn test_default_auth() {
172        let auth = Auth::default();
173        match auth {
174            Auth::Community { version, community } => {
175                assert_eq!(version, CommunityVersion::V2c);
176                assert_eq!(community, "public");
177            }
178            Auth::Usm(_) => panic!("expected Community variant"),
179        }
180    }
181
182    #[test]
183    fn test_v1_auth() {
184        let auth = Auth::v1("private");
185        match auth {
186            Auth::Community { version, community } => {
187                assert_eq!(version, CommunityVersion::V1);
188                assert_eq!(community, "private");
189            }
190            Auth::Usm(_) => panic!("expected Community variant"),
191        }
192    }
193
194    #[test]
195    fn test_v2c_auth() {
196        let auth = Auth::v2c("secret");
197        match auth {
198            Auth::Community { version, community } => {
199                assert_eq!(version, CommunityVersion::V2c);
200                assert_eq!(community, "secret");
201            }
202            Auth::Usm(_) => panic!("expected Community variant"),
203        }
204    }
205
206    #[test]
207    fn test_community_version_default() {
208        let version = CommunityVersion::default();
209        assert_eq!(version, CommunityVersion::V2c);
210    }
211
212    #[test]
213    fn test_usm_no_auth_no_priv() {
214        let auth: Auth = Auth::usm("readonly").into();
215        match auth {
216            Auth::Usm(usm) => {
217                assert_eq!(usm.username().as_ref(), b"readonly");
218                assert_eq!(usm.security_level(), SecurityLevel::NoAuthNoPriv);
219                assert!(usm.configured_context_name().is_empty());
220            }
221            Auth::Community { .. } => panic!("expected Usm variant"),
222        }
223    }
224
225    #[test]
226    fn test_usm_auth_no_priv() {
227        let auth: Auth = Auth::usm("admin")
228            .auth(AuthProtocol::Sha256, "authpass123")
229            .into();
230        match auth {
231            Auth::Usm(usm) => {
232                assert_eq!(usm.username().as_ref(), b"admin");
233                assert_eq!(usm.security_level(), SecurityLevel::AuthNoPriv);
234            }
235            Auth::Community { .. } => panic!("expected Usm variant"),
236        }
237    }
238
239    #[test]
240    fn test_usm_auth_priv() {
241        let auth: Auth = Auth::usm("admin")
242            .auth_priv(
243                AuthProtocol::Sha256,
244                "authpass",
245                PrivProtocol::Aes128,
246                "privpass",
247            )
248            .into();
249        match auth {
250            Auth::Usm(usm) => {
251                assert_eq!(usm.username().as_ref(), b"admin");
252                assert_eq!(usm.security_level(), SecurityLevel::AuthPriv);
253            }
254            Auth::Community { .. } => panic!("expected Usm variant"),
255        }
256    }
257
258    #[test]
259    fn test_usm_with_context_name() {
260        let auth: Auth = Auth::usm("admin")
261            .auth(AuthProtocol::Sha256, "authpass")
262            .context_name("vlan100")
263            .into();
264        match auth {
265            Auth::Usm(usm) => {
266                assert_eq!(usm.username().as_ref(), b"admin");
267                assert_eq!(usm.configured_context_name().as_ref(), b"vlan100");
268            }
269            Auth::Community { .. } => panic!("expected Usm variant"),
270        }
271    }
272
273    #[test]
274    fn test_usm_builder_chaining() {
275        // Verify all methods can be chained
276        let auth: Auth = Auth::usm("user")
277            .auth_priv(AuthProtocol::Sha512, "auth", PrivProtocol::Aes256, "priv")
278            .context_name("ctx")
279            .into();
280
281        match auth {
282            Auth::Usm(usm) => {
283                assert_eq!(usm.username().as_ref(), b"user");
284                assert_eq!(usm.security_level(), SecurityLevel::AuthPriv);
285                assert_eq!(usm.configured_context_name().as_ref(), b"ctx");
286            }
287            Auth::Community { .. } => panic!("expected Usm variant"),
288        }
289    }
290
291    #[test]
292    fn test_debug_redacts_secrets() {
293        // Community string must not appear in Debug output.
294        let community = Auth::v2c("supersecretcommunity");
295        let rendered = format!("{community:?}");
296        assert!(!rendered.contains("supersecretcommunity"), "{rendered}");
297        assert!(rendered.contains("[REDACTED]"), "{rendered}");
298
299        // USM auth/priv passwords must not appear in Debug output.
300        let config = Auth::usm("admin")
301            .auth_priv(
302                AuthProtocol::Sha256,
303                "authpassword123",
304                PrivProtocol::Aes128,
305                "privpassword456",
306            )
307            .context_name("vlan100");
308        let config_rendered = format!("{config:?}");
309        assert!(
310            !config_rendered.contains("authpassword123"),
311            "{config_rendered}"
312        );
313        assert!(
314            !config_rendered.contains("privpassword456"),
315            "{config_rendered}"
316        );
317        // Non-secret fields remain visible.
318        assert!(config_rendered.contains("admin"), "{config_rendered}");
319        assert!(config_rendered.contains("vlan100"), "{config_rendered}");
320
321        let usm: Auth = config.into();
322        let usm_rendered = format!("{usm:?}");
323        assert!(!usm_rendered.contains("authpassword123"), "{usm_rendered}");
324        assert!(!usm_rendered.contains("privpassword456"), "{usm_rendered}");
325        assert!(usm_rendered.contains("[REDACTED]"), "{usm_rendered}");
326        assert!(usm_rendered.contains("admin"), "{usm_rendered}");
327    }
328}