Skip to main content

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    /// `SNMPv3` context name for VACM context selection.
31    pub context_name: Bytes,
32    /// Pre-computed master keys for efficient key derivation
33    pub master_keys: Option<crate::v3::MasterKeys>,
34}
35
36impl UsmConfig {
37    /// Create a new USM config with just a username (noAuthNoPriv).
38    pub fn new(username: impl Into<Bytes>) -> Self {
39        Self {
40            username: username.into(),
41            auth: None,
42            privacy: None,
43            context_name: Bytes::new(),
44            master_keys: None,
45        }
46    }
47
48    /// Add authentication (authNoPriv or authPriv).
49    #[must_use]
50    pub fn auth(mut self, protocol: AuthProtocol, password: impl AsRef<[u8]>) -> Self {
51        self.auth = Some((protocol, password.as_ref().to_vec()));
52        self
53    }
54
55    /// Add privacy/encryption (authPriv).
56    #[must_use]
57    pub fn privacy(mut self, protocol: PrivProtocol, password: impl AsRef<[u8]>) -> Self {
58        self.privacy = Some((protocol, password.as_ref().to_vec()));
59        self
60    }
61
62    /// Set the `SNMPv3` context name for scoped PDUs.
63    #[must_use]
64    pub fn context_name(mut self, context_name: impl Into<Bytes>) -> Self {
65        self.context_name = context_name.into();
66        self
67    }
68
69    /// Use pre-computed master keys for efficient key derivation.
70    ///
71    /// When set, passwords are ignored and keys are derived from the cached
72    /// master keys. This avoids the expensive ~850us password expansion for
73    /// each engine.
74    #[must_use]
75    pub fn with_master_keys(mut self, master_keys: crate::v3::MasterKeys) -> Self {
76        self.master_keys = Some(master_keys);
77        self
78    }
79
80    /// Get the security level based on configured auth/privacy.
81    pub fn security_level(&self) -> SecurityLevel {
82        // Check master_keys first, then fall back to auth/privacy
83        if let Some(ref master_keys) = self.master_keys {
84            if master_keys.priv_protocol().is_some() {
85                return SecurityLevel::AuthPriv;
86            }
87            return SecurityLevel::AuthNoPriv;
88        }
89
90        match (&self.auth, &self.privacy) {
91            (None, _) => SecurityLevel::NoAuthNoPriv,
92            (Some(_), None) => SecurityLevel::AuthNoPriv,
93            (Some(_), Some(_)) => SecurityLevel::AuthPriv,
94        }
95    }
96
97    /// Derive localized keys for a specific engine ID.
98    ///
99    /// If master keys are configured, uses the cached master keys for efficient
100    /// localization (~1us). Otherwise, performs full password-to-key derivation
101    /// (~850us for SHA-256).
102    pub fn derive_keys(&self, engine_id: &[u8]) -> crate::v3::CryptoResult<DerivedKeys> {
103        // Use master keys if available (efficient path)
104        if let Some(ref master_keys) = self.master_keys {
105            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");
106            let (auth_key, priv_key) = master_keys.localize(engine_id)?;
107            tracing::trace!(target: "async_snmp::client", "key localization complete");
108            return Ok(DerivedKeys {
109                auth_key: Some(auth_key),
110                priv_key,
111            });
112        }
113
114        // Fall back to password-based derivation
115        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");
116
117        let auth_key = self.auth.as_ref().map(|(protocol, password)| {
118            tracing::trace!(target: "async_snmp::client", { auth_protocol = ?protocol }, "deriving auth key");
119            LocalizedKey::from_password(*protocol, password, engine_id)
120        }).transpose()?;
121
122        let priv_key = match (&self.auth, &self.privacy) {
123            (Some((auth_protocol, _)), Some((priv_protocol, priv_password))) => {
124                tracing::trace!(target: "async_snmp::client", { priv_protocol = ?priv_protocol }, "deriving privacy key");
125                Some(PrivKey::from_password(
126                    *auth_protocol,
127                    *priv_protocol,
128                    priv_password,
129                    engine_id,
130                )?)
131            }
132            _ => None,
133        };
134
135        tracing::trace!(target: "async_snmp::client", "key derivation complete");
136        Ok(DerivedKeys { auth_key, priv_key })
137    }
138}
139
140impl std::fmt::Debug for UsmConfig {
141    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
142        f.debug_struct("UsmConfig")
143            .field("username", &String::from_utf8_lossy(&self.username))
144            .field("auth", &self.auth.as_ref().map(|(p, _)| p))
145            .field("privacy", &self.privacy.as_ref().map(|(p, _)| p))
146            .field("context_name", &String::from_utf8_lossy(&self.context_name))
147            .field(
148                "master_keys",
149                &self.master_keys.as_ref().map(|mk| {
150                    format!(
151                        "MasterKeys({:?}, {:?})",
152                        mk.auth_protocol(),
153                        mk.priv_protocol()
154                    )
155                }),
156            )
157            .finish()
158    }
159}
160
161/// Derived keys for a specific engine ID.
162///
163/// Used internally for V3 authentication in both client and notification receiver.
164#[derive(Debug)]
165pub struct DerivedKeys {
166    /// Localized authentication key
167    pub auth_key: Option<LocalizedKey>,
168    /// Privacy key
169    pub priv_key: Option<PrivKey>,
170}
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175
176    #[test]
177    fn test_usm_user_config_no_auth() {
178        let config = UsmConfig::new(Bytes::from_static(b"testuser"));
179        assert_eq!(config.security_level(), SecurityLevel::NoAuthNoPriv);
180        assert!(config.auth.is_none());
181        assert!(config.privacy.is_none());
182    }
183
184    #[test]
185    fn test_usm_user_config_auth_only() {
186        let config = UsmConfig::new(Bytes::from_static(b"testuser"))
187            .auth(AuthProtocol::Sha1, b"password123");
188        assert_eq!(config.security_level(), SecurityLevel::AuthNoPriv);
189        assert!(config.auth.is_some());
190        assert!(config.privacy.is_none());
191        assert!(config.context_name.is_empty());
192    }
193
194    #[test]
195    fn test_usm_user_config_auth_priv() {
196        let config = UsmConfig::new(Bytes::from_static(b"testuser"))
197            .auth(AuthProtocol::Sha256, b"authpass")
198            .privacy(PrivProtocol::Aes128, b"privpass");
199        assert_eq!(config.security_level(), SecurityLevel::AuthPriv);
200        assert!(config.auth.is_some());
201        assert!(config.privacy.is_some());
202    }
203
204    #[test]
205    fn test_usm_user_config_context_name() {
206        let config = UsmConfig::new(Bytes::from_static(b"testuser")).context_name("ctx");
207        assert_eq!(config.context_name.as_ref(), b"ctx");
208    }
209
210    #[test]
211    fn test_usm_user_config_derive_keys() {
212        let config = UsmConfig::new(Bytes::from_static(b"testuser"))
213            .auth(AuthProtocol::Sha1, b"password123");
214
215        let engine_id = b"test-engine-id";
216        let keys = config.derive_keys(engine_id).unwrap();
217
218        assert!(keys.auth_key.is_some());
219        assert!(keys.priv_key.is_none());
220    }
221
222    #[test]
223    fn test_usm_user_config_derive_keys_with_privacy() {
224        let config = UsmConfig::new(Bytes::from_static(b"testuser"))
225            .auth(AuthProtocol::Sha256, b"authpass")
226            .privacy(PrivProtocol::Aes128, b"privpass");
227
228        let engine_id = b"test-engine-id";
229        let keys = config.derive_keys(engine_id).unwrap();
230
231        assert!(keys.auth_key.is_some());
232        assert!(keys.priv_key.is_some());
233    }
234}