async_snmp/notification/
types.rs

1//! USM configuration types for SNMPv3 authentication.
2//!
3//! These types store authentication and privacy settings for SNMPv3 operations,
4//! used by both the client and notification receiver.
5
6use bytes::Bytes;
7
8use crate::message::SecurityLevel;
9use crate::v3::{AuthProtocol, LocalizedKey, PrivKey, PrivProtocol};
10
11/// USM user credentials for SNMPv3 authentication.
12///
13/// Stores the credentials needed for authenticated and/or encrypted communication.
14/// Keys are derived when the engine ID is discovered.
15///
16/// # Master Key Caching
17///
18/// When polling many engines with shared credentials, use
19/// [`MasterKeys`](crate::MasterKeys) to cache the expensive password-to-key
20/// derivation. When `master_keys` is set, passwords are ignored and keys are
21/// derived from the cached master keys.
22#[derive(Clone)]
23pub struct UsmConfig {
24    /// Username for USM authentication
25    pub username: Bytes,
26    /// Authentication protocol and password
27    pub auth: Option<(AuthProtocol, Vec<u8>)>,
28    /// Privacy protocol and password
29    pub privacy: Option<(PrivProtocol, Vec<u8>)>,
30    /// Pre-computed master keys for efficient key derivation
31    pub master_keys: Option<crate::v3::MasterKeys>,
32}
33
34impl UsmConfig {
35    /// Create a new USM config with just a username (noAuthNoPriv).
36    pub fn new(username: impl Into<Bytes>) -> Self {
37        Self {
38            username: username.into(),
39            auth: None,
40            privacy: None,
41            master_keys: None,
42        }
43    }
44
45    /// Add authentication (authNoPriv or authPriv).
46    pub fn auth(mut self, protocol: AuthProtocol, password: impl AsRef<[u8]>) -> Self {
47        self.auth = Some((protocol, password.as_ref().to_vec()));
48        self
49    }
50
51    /// Add privacy/encryption (authPriv).
52    pub fn privacy(mut self, protocol: PrivProtocol, password: impl AsRef<[u8]>) -> Self {
53        self.privacy = Some((protocol, password.as_ref().to_vec()));
54        self
55    }
56
57    /// Use pre-computed master keys for efficient key derivation.
58    ///
59    /// When set, passwords are ignored and keys are derived from the cached
60    /// master keys. This avoids the expensive ~850us password expansion for
61    /// each engine.
62    pub fn with_master_keys(mut self, master_keys: crate::v3::MasterKeys) -> Self {
63        self.master_keys = Some(master_keys);
64        self
65    }
66
67    /// Get the security level based on configured auth/privacy.
68    pub fn security_level(&self) -> SecurityLevel {
69        // Check master_keys first, then fall back to auth/privacy
70        if let Some(ref master_keys) = self.master_keys {
71            if master_keys.priv_protocol().is_some() {
72                return SecurityLevel::AuthPriv;
73            }
74            return SecurityLevel::AuthNoPriv;
75        }
76
77        match (&self.auth, &self.privacy) {
78            (None, _) => SecurityLevel::NoAuthNoPriv,
79            (Some(_), None) => SecurityLevel::AuthNoPriv,
80            (Some(_), Some(_)) => SecurityLevel::AuthPriv,
81        }
82    }
83
84    /// Derive localized keys for a specific engine ID.
85    ///
86    /// If master keys are configured, uses the cached master keys for efficient
87    /// localization (~1us). Otherwise, performs full password-to-key derivation
88    /// (~850us for SHA-256).
89    pub fn derive_keys(&self, engine_id: &[u8]) -> DerivedKeys {
90        // Use master keys if available (efficient path)
91        if let Some(ref master_keys) = self.master_keys {
92            tracing::trace!(target: "async_snmp::client", { engine_id_len = engine_id.len(), auth_protocol = ?master_keys.auth_protocol(), priv_protocol = ?master_keys.priv_protocol() }, "localizing from cached master keys");
93            let (auth_key, priv_key) = master_keys.localize(engine_id);
94            tracing::trace!(target: "async_snmp::client", "key localization complete");
95            return DerivedKeys {
96                auth_key: Some(auth_key),
97                priv_key,
98            };
99        }
100
101        // Fall back to password-based derivation
102        tracing::trace!(target: "async_snmp::client", { engine_id_len = engine_id.len(), has_auth = self.auth.is_some(), has_priv = self.privacy.is_some() }, "deriving localized keys from passwords");
103
104        let auth_key = self.auth.as_ref().map(|(protocol, password)| {
105            tracing::trace!(target: "async_snmp::client", { auth_protocol = ?protocol }, "deriving auth key");
106            LocalizedKey::from_password(*protocol, password, engine_id)
107        });
108
109        let priv_key = match (&self.auth, &self.privacy) {
110            (Some((auth_protocol, _)), Some((priv_protocol, priv_password))) => {
111                tracing::trace!(target: "async_snmp::client", { priv_protocol = ?priv_protocol }, "deriving privacy key");
112                Some(PrivKey::from_password(
113                    *auth_protocol,
114                    *priv_protocol,
115                    priv_password,
116                    engine_id,
117                ))
118            }
119            _ => None,
120        };
121
122        tracing::trace!(target: "async_snmp::client", "key derivation complete");
123        DerivedKeys { auth_key, priv_key }
124    }
125}
126
127impl std::fmt::Debug for UsmConfig {
128    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
129        f.debug_struct("UsmConfig")
130            .field("username", &String::from_utf8_lossy(&self.username))
131            .field("auth", &self.auth.as_ref().map(|(p, _)| p))
132            .field("privacy", &self.privacy.as_ref().map(|(p, _)| p))
133            .field(
134                "master_keys",
135                &self.master_keys.as_ref().map(|mk| {
136                    format!(
137                        "MasterKeys({:?}, {:?})",
138                        mk.auth_protocol(),
139                        mk.priv_protocol()
140                    )
141                }),
142            )
143            .finish()
144    }
145}
146
147/// Type alias for backward compatibility.
148pub type UsmUserConfig = UsmConfig;
149
150/// Derived keys for a specific engine ID.
151///
152/// Used internally for V3 authentication in both client and notification receiver.
153pub struct DerivedKeys {
154    /// Localized authentication key
155    pub auth_key: Option<LocalizedKey>,
156    /// Privacy key
157    pub priv_key: Option<PrivKey>,
158}
159
160#[cfg(test)]
161mod tests {
162    use super::*;
163
164    #[test]
165    fn test_usm_user_config_no_auth() {
166        let config = UsmUserConfig::new(Bytes::from_static(b"testuser"));
167        assert_eq!(config.security_level(), SecurityLevel::NoAuthNoPriv);
168        assert!(config.auth.is_none());
169        assert!(config.privacy.is_none());
170    }
171
172    #[test]
173    fn test_usm_user_config_auth_only() {
174        let config = UsmUserConfig::new(Bytes::from_static(b"testuser"))
175            .auth(AuthProtocol::Sha1, b"password123");
176        assert_eq!(config.security_level(), SecurityLevel::AuthNoPriv);
177        assert!(config.auth.is_some());
178        assert!(config.privacy.is_none());
179    }
180
181    #[test]
182    fn test_usm_user_config_auth_priv() {
183        let config = UsmUserConfig::new(Bytes::from_static(b"testuser"))
184            .auth(AuthProtocol::Sha256, b"authpass")
185            .privacy(PrivProtocol::Aes128, b"privpass");
186        assert_eq!(config.security_level(), SecurityLevel::AuthPriv);
187        assert!(config.auth.is_some());
188        assert!(config.privacy.is_some());
189    }
190
191    #[test]
192    fn test_usm_user_config_derive_keys() {
193        let config = UsmUserConfig::new(Bytes::from_static(b"testuser"))
194            .auth(AuthProtocol::Sha1, b"password123");
195
196        let engine_id = b"test-engine-id";
197        let keys = config.derive_keys(engine_id);
198
199        assert!(keys.auth_key.is_some());
200        assert!(keys.priv_key.is_none());
201    }
202
203    #[test]
204    fn test_usm_user_config_derive_keys_with_privacy() {
205        let config = UsmUserConfig::new(Bytes::from_static(b"testuser"))
206            .auth(AuthProtocol::Sha256, b"authpass")
207            .privacy(PrivProtocol::Aes128, b"privpass");
208
209        let engine_id = b"test-engine-id";
210        let keys = config.derive_keys(engine_id);
211
212        assert!(keys.auth_key.is_some());
213        assert!(keys.priv_key.is_some());
214    }
215}