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}