use bytes::Bytes;
use crate::message::SecurityLevel;
use crate::v3::{AuthProtocol, LocalizedKey, PrivKey, PrivProtocol};
#[derive(Clone)]
pub struct UsmConfig {
pub username: Bytes,
pub auth: Option<(AuthProtocol, Vec<u8>)>,
pub privacy: Option<(PrivProtocol, Vec<u8>)>,
pub context_name: Bytes,
pub master_keys: Option<crate::v3::MasterKeys>,
}
impl UsmConfig {
pub fn new(username: impl Into<Bytes>) -> Self {
Self {
username: username.into(),
auth: None,
privacy: None,
context_name: Bytes::new(),
master_keys: None,
}
}
pub fn auth(mut self, protocol: AuthProtocol, password: impl AsRef<[u8]>) -> Self {
self.auth = Some((protocol, password.as_ref().to_vec()));
self
}
pub fn privacy(mut self, protocol: PrivProtocol, password: impl AsRef<[u8]>) -> Self {
self.privacy = Some((protocol, password.as_ref().to_vec()));
self
}
pub fn context_name(mut self, context_name: impl Into<Bytes>) -> Self {
self.context_name = context_name.into();
self
}
pub fn with_master_keys(mut self, master_keys: crate::v3::MasterKeys) -> Self {
self.master_keys = Some(master_keys);
self
}
pub fn security_level(&self) -> SecurityLevel {
if let Some(ref master_keys) = self.master_keys {
if master_keys.priv_protocol().is_some() {
return SecurityLevel::AuthPriv;
}
return SecurityLevel::AuthNoPriv;
}
match (&self.auth, &self.privacy) {
(None, _) => SecurityLevel::NoAuthNoPriv,
(Some(_), None) => SecurityLevel::AuthNoPriv,
(Some(_), Some(_)) => SecurityLevel::AuthPriv,
}
}
pub fn derive_keys(&self, engine_id: &[u8]) -> crate::v3::CryptoResult<DerivedKeys> {
if let Some(ref master_keys) = self.master_keys {
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");
let (auth_key, priv_key) = master_keys.localize(engine_id)?;
tracing::trace!(target: "async_snmp::client", "key localization complete");
return Ok(DerivedKeys {
auth_key: Some(auth_key),
priv_key,
});
}
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");
let auth_key = self.auth.as_ref().map(|(protocol, password)| {
tracing::trace!(target: "async_snmp::client", { auth_protocol = ?protocol }, "deriving auth key");
LocalizedKey::from_password(*protocol, password, engine_id)
}).transpose()?;
let priv_key = match (&self.auth, &self.privacy) {
(Some((auth_protocol, _)), Some((priv_protocol, priv_password))) => {
tracing::trace!(target: "async_snmp::client", { priv_protocol = ?priv_protocol }, "deriving privacy key");
Some(PrivKey::from_password(
*auth_protocol,
*priv_protocol,
priv_password,
engine_id,
)?)
}
_ => None,
};
tracing::trace!(target: "async_snmp::client", "key derivation complete");
Ok(DerivedKeys { auth_key, priv_key })
}
}
impl std::fmt::Debug for UsmConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("UsmConfig")
.field("username", &String::from_utf8_lossy(&self.username))
.field("auth", &self.auth.as_ref().map(|(p, _)| p))
.field("privacy", &self.privacy.as_ref().map(|(p, _)| p))
.field("context_name", &String::from_utf8_lossy(&self.context_name))
.field(
"master_keys",
&self.master_keys.as_ref().map(|mk| {
format!(
"MasterKeys({:?}, {:?})",
mk.auth_protocol(),
mk.priv_protocol()
)
}),
)
.finish()
}
}
#[derive(Debug)]
pub struct DerivedKeys {
pub auth_key: Option<LocalizedKey>,
pub priv_key: Option<PrivKey>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_usm_user_config_no_auth() {
let config = UsmConfig::new(Bytes::from_static(b"testuser"));
assert_eq!(config.security_level(), SecurityLevel::NoAuthNoPriv);
assert!(config.auth.is_none());
assert!(config.privacy.is_none());
}
#[test]
fn test_usm_user_config_auth_only() {
let config = UsmConfig::new(Bytes::from_static(b"testuser"))
.auth(AuthProtocol::Sha1, b"password123");
assert_eq!(config.security_level(), SecurityLevel::AuthNoPriv);
assert!(config.auth.is_some());
assert!(config.privacy.is_none());
assert!(config.context_name.is_empty());
}
#[test]
fn test_usm_user_config_auth_priv() {
let config = UsmConfig::new(Bytes::from_static(b"testuser"))
.auth(AuthProtocol::Sha256, b"authpass")
.privacy(PrivProtocol::Aes128, b"privpass");
assert_eq!(config.security_level(), SecurityLevel::AuthPriv);
assert!(config.auth.is_some());
assert!(config.privacy.is_some());
}
#[test]
fn test_usm_user_config_context_name() {
let config = UsmConfig::new(Bytes::from_static(b"testuser")).context_name("ctx");
assert_eq!(config.context_name.as_ref(), b"ctx");
}
#[test]
fn test_usm_user_config_derive_keys() {
let config = UsmConfig::new(Bytes::from_static(b"testuser"))
.auth(AuthProtocol::Sha1, b"password123");
let engine_id = b"test-engine-id";
let keys = config.derive_keys(engine_id).unwrap();
assert!(keys.auth_key.is_some());
assert!(keys.priv_key.is_none());
}
#[test]
fn test_usm_user_config_derive_keys_with_privacy() {
let config = UsmConfig::new(Bytes::from_static(b"testuser"))
.auth(AuthProtocol::Sha256, b"authpass")
.privacy(PrivProtocol::Aes128, b"privpass");
let engine_id = b"test-engine-id";
let keys = config.derive_keys(engine_id).unwrap();
assert!(keys.auth_key.is_some());
assert!(keys.priv_key.is_some());
}
}