From 5184302faa91d328bce1a2ac54bc722bef481ef1 Mon Sep 17 00:00:00 2001 From: Ting Lou Date: Tue, 23 Dec 2025 11:12:19 +0800 Subject: [PATCH 1/7] export field version of session --- src/asyncsession.rs | 6 ++++++ src/syncsession.rs | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/src/asyncsession.rs b/src/asyncsession.rs index 4eed072..f564e2e 100644 --- a/src/asyncsession.rs +++ b/src/asyncsession.rs @@ -108,6 +108,12 @@ impl AsyncSession { }) } + /// Returns the version of the session, e.g. Version::V1. + /// This is useful for determining whether bulk get is supported. + pub fn version(&self) -> Version { + self.version + } + #[cfg(not(feature = "v3"))] #[allow(clippy::unused_self, clippy::unused_async)] pub async fn init(&mut self) -> Result<()> { diff --git a/src/syncsession.rs b/src/syncsession.rs index deaf48f..2c14379 100644 --- a/src/syncsession.rs +++ b/src/syncsession.rs @@ -113,6 +113,12 @@ impl SyncSession { }) } + /// Returns the version of the session, e.g. Version::V1. + /// This is useful for determining whether bulk get is supported. + pub fn version(&self) -> Version { + self.version + } + #[cfg(feature = "v3")] pub fn with_security(mut self, mut security: v3::Security) -> Result { security.username.clone_from(&self.community); From e33e056c99fdb4e6d1eda71f0e522d386a502393 Mon Sep 17 00:00:00 2001 From: Ting Lou Date: Wed, 7 Jan 2026 12:07:25 +0800 Subject: [PATCH 2/7] features --- Cargo.toml | 12 ++++++++++++ build.rs | 12 ++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 build.rs diff --git a/Cargo.toml b/Cargo.toml index c7ffb42..7d94649 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,12 +21,24 @@ snmptools = { version = "^0.1.2", optional = true } tokio = { version = "1.47", features = ["net"], optional = true } openssl = { version = "0.10", optional = true } +md-5 = { version = "0.10.6", optional = true } +sha1 = { version = "0.10.6", optional = true } +digest = { version = "0.10.7", optional = true } +sha2 = { version = "0.10.9", optional = true } +aes = { version = "0.8.4", optional = true } +des = { version = "0.8.1", optional = true } + [dev-dependencies] tokio = { version = "=1.47" } [features] +default = ["tokio", "v3", "openssl/vendored"] + mibs = ["dep:snmptools"] tokio = ["dep:tokio"] v3 = ["openssl"] heap_buffers = [] full = ["mibs", "tokio", "v3"] + +v3_openssl = ["v3", "openssl"] +v3_rust = ["v3", "md-5", "sha1", "digest", "sha2", "aes", "des"] diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..266b83b --- /dev/null +++ b/build.rs @@ -0,0 +1,12 @@ +fn main() { + let has_v3_openssl = std::env::var("CARGO_FEATURE_V3_OPENSSL").is_ok(); + let has_v3_rust = std::env::var("CARGO_FEATURE_V3_RUST").is_ok(); + + match (has_v3_openssl, has_v3_rust) { + (true, false) | (false, true) => {}, // OK + (true, true) => panic!("feature_v3_openssl and feature_v3_rust are mutually exclusive!"), + (false, false)=> { + // panic!("feature_v3_openssl or feature_v3_rust is required") + }, + } +} \ No newline at end of file From 9091c64c251f9a7a64b6b243af639cbcfcf48bd0 Mon Sep 17 00:00:00 2001 From: Ting Lou Date: Wed, 7 Jan 2026 13:26:51 +0800 Subject: [PATCH 3/7] v3_rust --- Cargo.toml | 12 ++++++++---- build.rs | 5 ++++- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 7d94649..4a62b57 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,18 +27,22 @@ digest = { version = "0.10.7", optional = true } sha2 = { version = "0.10.9", optional = true } aes = { version = "0.8.4", optional = true } des = { version = "0.8.1", optional = true } +hmac = { version = "0.12", optional = true } +cbc = { version = "0.1", optional = true } +cfb-mode = { version = "0.8", optional = true } +rand = { version = "0.8", optional = true } [dev-dependencies] tokio = { version = "=1.47" } [features] -default = ["tokio", "v3", "openssl/vendored"] +default = ["tokio", "v3_rust", "openssl/vendored"] mibs = ["dep:snmptools"] tokio = ["dep:tokio"] -v3 = ["openssl"] +v3 = [] heap_buffers = [] -full = ["mibs", "tokio", "v3"] +full = ["mibs", "tokio", "v3_rust"] v3_openssl = ["v3", "openssl"] -v3_rust = ["v3", "md-5", "sha1", "digest", "sha2", "aes", "des"] +v3_rust = ["v3", "md-5", "sha1", "digest", "sha2", "aes", "des", "hmac", "cbc", "cfb-mode", "rand"] diff --git a/build.rs b/build.rs index 266b83b..7bb66a2 100644 --- a/build.rs +++ b/build.rs @@ -1,4 +1,5 @@ fn main() { + let has_v3 = std::env::var("CARGO_FEATURE_V3").is_ok(); let has_v3_openssl = std::env::var("CARGO_FEATURE_V3_OPENSSL").is_ok(); let has_v3_rust = std::env::var("CARGO_FEATURE_V3_RUST").is_ok(); @@ -6,7 +7,9 @@ fn main() { (true, false) | (false, true) => {}, // OK (true, true) => panic!("feature_v3_openssl and feature_v3_rust are mutually exclusive!"), (false, false)=> { - // panic!("feature_v3_openssl or feature_v3_rust is required") + if has_v3 { + panic!("feature_v3_openssl or feature_v3_rust is required") + } }, } } \ No newline at end of file From 130d89717a1b6f07eca7255a96a8ec7634ce3a23 Mon Sep 17 00:00:00 2001 From: Ting Lou Date: Wed, 7 Jan 2026 13:47:24 +0800 Subject: [PATCH 4/7] v3_rust --- Cargo.toml | 19 +-- build.rs | 12 +- src/lib.rs | 4 +- src/v3.rs | 381 +++++++++++++++++++++++++++++++++++++++++------------ 4 files changed, 312 insertions(+), 104 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4a62b57..e5759b4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,19 +21,20 @@ snmptools = { version = "^0.1.2", optional = true } tokio = { version = "1.47", features = ["net"], optional = true } openssl = { version = "0.10", optional = true } -md-5 = { version = "0.10.6", optional = true } -sha1 = { version = "0.10.6", optional = true } -digest = { version = "0.10.7", optional = true } -sha2 = { version = "0.10.9", optional = true } -aes = { version = "0.8.4", optional = true } -des = { version = "0.8.1", optional = true } +md-5 = { version = "0.10", optional = true } +sha1 = { version = "0.10", optional = true } +digest = { version = "0.10", optional = true } +sha2 = { version = "0.10", optional = true } +aes = { version = "0.8", optional = true } +cipher = { version = "0.4", optional = true } +des = { version = "0.8", optional = true } hmac = { version = "0.12", optional = true } cbc = { version = "0.1", optional = true } cfb-mode = { version = "0.8", optional = true } -rand = { version = "0.8", optional = true } +rand = { version = "0.9", optional = true } [dev-dependencies] -tokio = { version = "=1.47" } +tokio = { version = "1" } [features] default = ["tokio", "v3_rust", "openssl/vendored"] @@ -45,4 +46,4 @@ heap_buffers = [] full = ["mibs", "tokio", "v3_rust"] v3_openssl = ["v3", "openssl"] -v3_rust = ["v3", "md-5", "sha1", "digest", "sha2", "aes", "des", "hmac", "cbc", "cfb-mode", "rand"] +v3_rust = ["v3", "md-5", "sha1", "digest", "sha2", "aes", "cipher", "des", "hmac", "cbc", "cfb-mode", "rand"] diff --git a/build.rs b/build.rs index 7bb66a2..2ff266d 100644 --- a/build.rs +++ b/build.rs @@ -4,12 +4,12 @@ fn main() { let has_v3_rust = std::env::var("CARGO_FEATURE_V3_RUST").is_ok(); match (has_v3_openssl, has_v3_rust) { - (true, false) | (false, true) => {}, // OK - (true, true) => panic!("feature_v3_openssl and feature_v3_rust are mutually exclusive!"), - (false, false)=> { + (true, false) | (false, true) => {} // OK + (true, true) => panic!("feature_v3_openssl and feature_v3_rust are mutually exclusive!"), + (false, false) => { if has_v3 { panic!("feature_v3_openssl or feature_v3_rust is required") - } - }, + } + } } -} \ No newline at end of file +} diff --git a/src/lib.rs b/src/lib.rs index 50510bb..8209c4a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,7 +12,7 @@ pub mod snmp; mod syncsession; #[cfg(feature = "v3")] pub mod v3; -#[cfg(feature = "v3")] +#[cfg(feature = "v3_openssl")] pub use openssl; pub use syncsession::SyncSession; #[cfg(feature = "tokio")] @@ -141,7 +141,7 @@ impl fmt::Display for Error { } } -#[cfg(feature = "v3")] +#[cfg(feature = "v3_openssl")] impl From for Error { fn from(err: openssl::error::ErrorStack) -> Error { Error::Crypto(err.to_string()) diff --git a/src/v3.rs b/src/v3.rs index a829771..3e21157 100644 --- a/src/v3.rs +++ b/src/v3.rs @@ -1,11 +1,23 @@ use std::{fmt, time::Instant}; +#[cfg(feature = "v3_openssl")] use openssl::{ hash::{Hasher, MessageDigest}, pkey::PKey, sign::Signer, }; +#[cfg(feature = "v3_rust")] +use { + aes::{Aes128, Aes192, Aes256}, + cbc::{Decryptor as CbcDecryptor, Encryptor as CbcEncryptor}, + cfb_mode::{Decryptor as CfbDecryptor, Encryptor as CfbEncryptor}, + cipher::{AsyncStreamCipher, BlockDecryptMut, BlockEncryptMut, KeyIvInit}, + des::Des, + digest::{Digest, DynDigest}, + hmac::{Hmac, Mac}, +}; + use crate::{ asn1, pdu::{self, Buf}, @@ -324,10 +336,55 @@ impl Security { if self.engine_id().is_empty() { return Err(Error::AuthFailure(AuthErrorKind::SecurityNotReady)); } - let pkey = PKey::hmac(&self.authoritative_state.auth_key)?; - let mut signer = Signer::new(self.auth_protocol.digest(), &pkey)?; - signer.update(data)?; - signer.sign_to_vec().map_err(Error::from) + #[cfg(feature = "v3_openssl")] + { + let pkey = PKey::hmac(&self.authoritative_state.auth_key)?; + let mut signer = Signer::new(self.auth_protocol.digest(), &pkey)?; + signer.update(data)?; + signer.sign_to_vec().map_err(Error::from) + } + #[cfg(feature = "v3_rust")] + { + let key = &self.authoritative_state.auth_key; + match self.auth_protocol { + AuthProtocol::Md5 => { + let mut mac = Hmac::::new_from_slice(key) + .map_err(|_| Error::Crypto("Invalid key length".into()))?; + mac.update(data); + Ok(mac.finalize().into_bytes().to_vec()) + } + AuthProtocol::Sha1 => { + let mut mac = Hmac::::new_from_slice(key) + .map_err(|_| Error::Crypto("Invalid key length".into()))?; + mac.update(data); + Ok(mac.finalize().into_bytes().to_vec()) + } + AuthProtocol::Sha224 => { + let mut mac = Hmac::::new_from_slice(key) + .map_err(|_| Error::Crypto("Invalid key length".into()))?; + mac.update(data); + Ok(mac.finalize().into_bytes().to_vec()) + } + AuthProtocol::Sha256 => { + let mut mac = Hmac::::new_from_slice(key) + .map_err(|_| Error::Crypto("Invalid key length".into()))?; + mac.update(data); + Ok(mac.finalize().into_bytes().to_vec()) + } + AuthProtocol::Sha384 => { + let mut mac = Hmac::::new_from_slice(key) + .map_err(|_| Error::Crypto("Invalid key length".into()))?; + mac.update(data); + Ok(mac.finalize().into_bytes().to_vec()) + } + AuthProtocol::Sha512 => { + let mut mac = Hmac::::new_from_slice(key) + .map_err(|_| Error::Crypto("Invalid key length".into()))?; + mac.update(data); + Ok(mac.finalize().into_bytes().to_vec()) + } + } + } } pub(crate) fn update_key(&mut self) -> Result<()> { @@ -388,7 +445,13 @@ impl Security { fn encrypt_des(&self, data: &[u8]) -> Result<(Vec, Vec)> { let mut salt = [0; 8]; salt[..4].copy_from_slice(&u32::try_from(self.engine_boots())?.to_be_bytes()); + #[cfg(feature = "v3_openssl")] openssl::rand::rand_bytes(&mut salt[4..])?; + #[cfg(feature = "v3_rust")] + { + use rand::RngCore; + rand::thread_rng().fill_bytes(&mut salt[4..]); + } if data.is_empty() { return Ok((vec![], salt.to_vec())); @@ -400,65 +463,124 @@ impl Security { let des_key = &self.authoritative_state.priv_key[..8]; let pre_iv = &self.authoritative_state.priv_key[8..16]; - let cipher = openssl::symm::Cipher::des_cbc(); let mut iv = [0; 8]; for (i, (a, b)) in pre_iv.iter().zip(salt.iter()).enumerate() { iv[i] = a ^ b; } - let mut encrypted = vec![0; data.len() + cipher.block_size()]; - let mut crypter = - openssl::symm::Crypter::new(cipher, openssl::symm::Mode::Encrypt, des_key, Some(&iv))?; - let mut count = crypter.update(data, &mut encrypted)?; + #[cfg(feature = "v3_openssl")] + { + let cipher = openssl::symm::Cipher::des_cbc(); + let mut encrypted = vec![0; data.len() + cipher.block_size()]; + let mut crypter = openssl::symm::Crypter::new( + cipher, + openssl::symm::Mode::Encrypt, + des_key, + Some(&iv), + )?; + let mut count = crypter.update(data, &mut encrypted)?; - if count < encrypted.len() { - count += crypter.finalize(&mut encrypted[count..])?; + if count < encrypted.len() { + count += crypter.finalize(&mut encrypted[count..])?; + } + + encrypted.truncate(count); + Ok((encrypted, salt.to_vec())) } + #[cfg(feature = "v3_rust")] + { + let mut encryptor = CbcEncryptor::::new_from_slices(des_key, &iv) + .map_err(|e| Error::Crypto(e.to_string()))?; + + // PKCS#7 padding + let len = data.len(); + let pad_len = 8 - (len % 8); + let mut padded = Vec::with_capacity(len + pad_len); + padded.extend_from_slice(data); + padded.extend(std::iter::repeat(pad_len as u8).take(pad_len)); + + for chunk in padded.chunks_mut(8) { + let block = cipher::generic_array::GenericArray::from_mut_slice(chunk); + encryptor.encrypt_block_mut(block); + } - encrypted.truncate(count); - Ok((encrypted, salt.to_vec())) + Ok((padded, salt.to_vec())) + } } - fn encrypt_aes( - &self, - data: &[u8], - cipher: openssl::symm::Cipher, - block_size: usize, - ) -> Result<(Vec, Vec)> { - let iv_len = cipher - .iv_len() - .ok_or_else(|| Error::Crypto("no IV len".to_owned()))?; - + fn encrypt_aes(&self, data: &[u8], key_len: usize) -> Result<(Vec, Vec)> { + let iv_len = 16; let mut iv = Vec::with_capacity(iv_len); iv.extend_from_slice(&u32::try_from(self.engine_boots())?.to_be_bytes()); iv.extend_from_slice(&u32::try_from(self.engine_time())?.to_be_bytes()); let salt_pos = iv.len(); iv.resize(iv_len, 0); + #[cfg(feature = "v3_openssl")] openssl::rand::rand_bytes(&mut iv[salt_pos..])?; - let key_len = cipher.key_len(); + #[cfg(feature = "v3_rust")] + { + use rand::RngCore; + rand::thread_rng().fill_bytes(&mut iv[salt_pos..]); + } if self.authoritative_state.priv_key.len() < key_len { return Err(Error::AuthFailure(AuthErrorKind::KeyLengthMismatch)); } - let mut crypter = openssl::symm::Crypter::new( - cipher, - openssl::symm::Mode::Encrypt, - &self.authoritative_state.priv_key[..key_len], - Some(&iv), - )?; + #[cfg(feature = "v3_openssl")] + { + let cipher = match key_len { + 16 => openssl::symm::Cipher::aes_128_cfb128(), + 24 => openssl::symm::Cipher::aes_192_cfb128(), + 32 => openssl::symm::Cipher::aes_256_cfb128(), + _ => return Err(Error::Crypto("Invalid key len".into())), + }; + + let mut crypter = openssl::symm::Crypter::new( + cipher, + openssl::symm::Mode::Encrypt, + &self.authoritative_state.priv_key[..key_len], + Some(&iv), + )?; + + let mut encrypted = vec![0; data.len() + 16]; + let mut count = crypter.update(data, &mut encrypted)?; - let mut encrypted = vec![0; data.len() + block_size]; - let mut count = crypter.update(data, &mut encrypted)?; + if count < encrypted.len() { + count += crypter.finalize(&mut encrypted[count..])?; + } - if count < encrypted.len() { - count += crypter.finalize(&mut encrypted[count..])?; + encrypted.truncate(count); + Ok((encrypted, iv[salt_pos..].to_vec())) } + #[cfg(feature = "v3_rust")] + { + let key = &self.authoritative_state.priv_key[..key_len]; + let mut encrypted = data.to_vec(); + + match key_len { + 16 => { + let encryptor = CfbEncryptor::::new_from_slices(key, &iv) + .map_err(|e| Error::Crypto(e.to_string()))?; + encryptor.encrypt(&mut encrypted); + } + 24 => { + let encryptor = CfbEncryptor::::new_from_slices(key, &iv) + .map_err(|e| Error::Crypto(e.to_string()))?; + encryptor.encrypt(&mut encrypted); + } + 32 => { + let encryptor = CfbEncryptor::::new_from_slices(key, &iv) + .map_err(|e| Error::Crypto(e.to_string()))?; + encryptor.encrypt(&mut encrypted); + } + _ => return Err(Error::Crypto("Invalid key len".into())), + } - encrypted.truncate(count); - Ok((encrypted, iv[salt_pos..].to_vec())) + Ok((encrypted, iv[salt_pos..].to_vec())) + } } /// encrypts the data @@ -477,12 +599,13 @@ impl Security { match cipher_kind { Cipher::Des => self.encrypt_des(data), - Cipher::Aes128 => self.encrypt_aes(data, openssl::symm::Cipher::aes_128_cfb128(), 16), - Cipher::Aes192 => self.encrypt_aes(data, openssl::symm::Cipher::aes_192_cfb128(), 24), - Cipher::Aes256 => self.encrypt_aes(data, openssl::symm::Cipher::aes_256_cfb128(), 32), + Cipher::Aes128 => self.encrypt_aes(data, 16), + Cipher::Aes192 => self.encrypt_aes(data, 24), + Cipher::Aes256 => self.encrypt_aes(data, 32), } } + #[cfg(feature = "v3_openssl")] fn decrypt_data_to_plain_buf( &mut self, mut crypter: openssl::symm::Crypter, @@ -509,13 +632,6 @@ impl Security { return Err(Error::AuthFailure(AuthErrorKind::KeyLengthMismatch)); } - let cipher = openssl::symm::Cipher::des_cbc(); - let block_size = 8; - - if encrypted.len() % block_size > 0 { - return Err(Error::AuthFailure(AuthErrorKind::PayloadLengthMismatch)); - } - let des_key = &self.authoritative_state.priv_key[..8]; let pre_iv = &self.authoritative_state.priv_key[8..16]; let mut iv = [0; 8]; @@ -524,22 +640,58 @@ impl Security { iv[i] = a ^ b; } - let crypter = - openssl::symm::Crypter::new(cipher, openssl::symm::Mode::Decrypt, des_key, Some(&iv))?; - self.decrypt_data_to_plain_buf(crypter, block_size, encrypted) - } + #[cfg(feature = "v3_openssl")] + { + let cipher = openssl::symm::Cipher::des_cbc(); + let block_size = 8; - fn decrypt_aes( - &mut self, - encrypted: &[u8], - priv_params: &[u8], - cipher: openssl::symm::Cipher, - block_size: usize, - ) -> Result<()> { - let iv_len = cipher - .iv_len() - .ok_or_else(|| Error::Crypto("no IV len".to_owned()))?; + if encrypted.len() % block_size > 0 { + return Err(Error::AuthFailure(AuthErrorKind::PayloadLengthMismatch)); + } + + let crypter = openssl::symm::Crypter::new( + cipher, + openssl::symm::Mode::Decrypt, + des_key, + Some(&iv), + )?; + self.decrypt_data_to_plain_buf(crypter, block_size, encrypted) + } + #[cfg(feature = "v3_rust")] + { + let block_size = 8; + if encrypted.len() % block_size > 0 { + return Err(Error::AuthFailure(AuthErrorKind::PayloadLengthMismatch)); + } + + let mut decryptor = CbcDecryptor::::new_from_slices(des_key, &iv) + .map_err(|e| Error::Crypto(e.to_string()))?; + + self.plain_buf.clear(); + self.plain_buf.extend_from_slice(encrypted); + for chunk in self.plain_buf.chunks_mut(8) { + let block = cipher::generic_array::GenericArray::from_mut_slice(chunk); + decryptor.decrypt_block_mut(block); + } + + if let Some(&pad_len) = self.plain_buf.last() { + let pad_len = pad_len as usize; + if pad_len > 0 && pad_len <= block_size && pad_len <= self.plain_buf.len() { + let new_len = self.plain_buf.len() - pad_len; + self.plain_buf.truncate(new_len); + Ok(()) + } else { + Err(Error::Crypto("Invalid padding".into())) + } + } else { + Ok(()) + } + } + } + + fn decrypt_aes(&mut self, encrypted: &[u8], priv_params: &[u8], key_len: usize) -> Result<()> { + let iv_len = 16; let mut iv = Vec::with_capacity(iv_len); iv.extend_from_slice(&u32::try_from(self.engine_boots())?.to_be_bytes()); iv.extend_from_slice(&u32::try_from(self.engine_time())?.to_be_bytes()); @@ -549,19 +701,54 @@ impl Security { return Err(Error::AuthFailure(AuthErrorKind::PrivLengthMismatch)); } - let key_len = cipher.key_len(); if self.authoritative_state.priv_key.len() < key_len { return Err(Error::AuthFailure(AuthErrorKind::KeyLengthMismatch)); } - let crypter = openssl::symm::Crypter::new( - cipher, - openssl::symm::Mode::Decrypt, - &self.authoritative_state.priv_key[..key_len], - Some(&iv), - )?; + #[cfg(feature = "v3_openssl")] + { + let cipher = match key_len { + 16 => openssl::symm::Cipher::aes_128_cfb128(), + 24 => openssl::symm::Cipher::aes_192_cfb128(), + 32 => openssl::symm::Cipher::aes_256_cfb128(), + _ => return Err(Error::Crypto("Invalid key len".into())), + }; + + let crypter = openssl::symm::Crypter::new( + cipher, + openssl::symm::Mode::Decrypt, + &self.authoritative_state.priv_key[..key_len], + Some(&iv), + )?; - self.decrypt_data_to_plain_buf(crypter, block_size, encrypted) + self.decrypt_data_to_plain_buf(crypter, 16, encrypted) + } + #[cfg(feature = "v3_rust")] + { + let key = &self.authoritative_state.priv_key[..key_len]; + self.plain_buf.clear(); + self.plain_buf.extend_from_slice(encrypted); + + match key_len { + 16 => { + let decryptor = CfbDecryptor::::new_from_slices(key, &iv) + .map_err(|e| Error::Crypto(e.to_string()))?; + decryptor.decrypt(&mut self.plain_buf); + } + 24 => { + let decryptor = CfbDecryptor::::new_from_slices(key, &iv) + .map_err(|e| Error::Crypto(e.to_string()))?; + decryptor.decrypt(&mut self.plain_buf); + } + 32 => { + let decryptor = CfbDecryptor::::new_from_slices(key, &iv) + .map_err(|e| Error::Crypto(e.to_string()))?; + decryptor.decrypt(&mut self.plain_buf); + } + _ => return Err(Error::Crypto("Invalid key len".into())), + } + Ok(()) + } } /// decrypts the data, the result is stored in `self.plain_buf` @@ -576,24 +763,9 @@ impl Security { match cipher_kind { Cipher::Des => self.decrypt_des(encrypted, priv_params), - Cipher::Aes128 => self.decrypt_aes( - encrypted, - priv_params, - openssl::symm::Cipher::aes_128_cfb128(), - 16, - ), - Cipher::Aes192 => self.decrypt_aes( - encrypted, - priv_params, - openssl::symm::Cipher::aes_192_cfb128(), - 24, - ), - Cipher::Aes256 => self.decrypt_aes( - encrypted, - priv_params, - openssl::symm::Cipher::aes_256_cfb128(), - 32, - ), + Cipher::Aes128 => self.decrypt_aes(encrypted, priv_params, 16), + Cipher::Aes192 => self.decrypt_aes(encrypted, priv_params, 24), + Cipher::Aes256 => self.decrypt_aes(encrypted, priv_params, 32), } } } @@ -610,6 +782,25 @@ pub enum Auth { }, } +#[cfg(feature = "v3_rust")] +pub struct Hasher(Box); + +#[cfg(feature = "v3_rust")] +impl Hasher { + pub fn new(d: Box) -> Result { + Ok(Self(d)) + } + + pub fn update(&mut self, data: &[u8]) -> Result<()> { + self.0.update(data); + Ok(()) + } + + pub fn finish(&mut self) -> Result> { + Ok(self.0.finalize_reset().to_vec()) + } +} + #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub enum AuthProtocol { Md5, @@ -622,8 +813,24 @@ pub enum AuthProtocol { impl AuthProtocol { fn create_hasher(self) -> Result { - Hasher::new(self.digest()).map_err(Into::into) + #[cfg(feature = "v3_openssl")] + { + Hasher::new(self.digest()).map_err(Into::into) + } + #[cfg(feature = "v3_rust")] + { + let d: Box = match self { + AuthProtocol::Md5 => Box::new(md5::Md5::new()), + AuthProtocol::Sha1 => Box::new(sha1::Sha1::new()), + AuthProtocol::Sha224 => Box::new(sha2::Sha224::new()), + AuthProtocol::Sha256 => Box::new(sha2::Sha256::new()), + AuthProtocol::Sha384 => Box::new(sha2::Sha384::new()), + AuthProtocol::Sha512 => Box::new(sha2::Sha512::new()), + }; + Hasher::new(d) + } } + #[cfg(feature = "v3_openssl")] fn digest(self) -> MessageDigest { match self { AuthProtocol::Md5 => MessageDigest::md5(), From 2b28ad9b3e37bb85528eba5d2df57966f1077db8 Mon Sep 17 00:00:00 2001 From: Ting Lou Date: Wed, 7 Jan 2026 13:49:51 +0800 Subject: [PATCH 5/7] remove deprecated function call --- src/v3.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/v3.rs b/src/v3.rs index 3e21157..127bc5d 100644 --- a/src/v3.rs +++ b/src/v3.rs @@ -449,8 +449,8 @@ impl Security { openssl::rand::rand_bytes(&mut salt[4..])?; #[cfg(feature = "v3_rust")] { - use rand::RngCore; - rand::thread_rng().fill_bytes(&mut salt[4..]); + use rand::Rng; + rand::rng().fill(&mut salt[4..]); } if data.is_empty() { @@ -521,8 +521,8 @@ impl Security { openssl::rand::rand_bytes(&mut iv[salt_pos..])?; #[cfg(feature = "v3_rust")] { - use rand::RngCore; - rand::thread_rng().fill_bytes(&mut iv[salt_pos..]); + use rand::Rng; + rand::rng().fill(&mut iv[salt_pos..]); } if self.authoritative_state.priv_key.len() < key_len { From e95da2bd3600b3093648753a75ce7d84140915f5 Mon Sep 17 00:00:00 2001 From: Ting Lou Date: Wed, 7 Jan 2026 13:58:50 +0800 Subject: [PATCH 6/7] readme and version --- Cargo.toml | 4 ++-- README.md | 41 ++++++++++++++++++++++++++++++----------- 2 files changed, 32 insertions(+), 13 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index e5759b4..1837cee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "snmp2" -version = "0.4.14" +version = "0.4.15" edition = "2021" description = "SNMP v1/v2/v3 sync/async client library with traps and MIB support" authors = ["Serhij S. "] @@ -37,7 +37,7 @@ rand = { version = "0.9", optional = true } tokio = { version = "1" } [features] -default = ["tokio", "v3_rust", "openssl/vendored"] +default = ["tokio", "v3_rust"] mibs = ["dep:snmptools"] tokio = ["dep:tokio"] diff --git a/README.md b/README.md index 75593a8..7cd060a 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ Supports: - Synchronous/Asynchronous requests - UDP transport - MIBs (with `mibs` feature, requires `libnetsnmp`) -- SNMP v3 (requires `v3` feature) +- SNMP v3 (enable `v3_openssl` or `v3_rust` feature) # Examples @@ -139,7 +139,7 @@ let socket = UdpSocket::bind("0.0.0.0:1161").unwrap(); socket.send_to(&bytes, target_addr).unwrap(); ``` -### With SNMPv3 (requires `v3` feature) +### With SNMPv3 (enable `v3_openssl` or `v3_rust`) When using SNMPv3, you need to provide the security context to convert the PDU to bytes: @@ -210,9 +210,13 @@ assert_eq!(snmp_oid, snmp_oid2); # SNMPv3 -- Requires `v3` crate feature. +- Requires enabling one of the features: `v3_openssl` or `v3_rust`. + - `v3_openssl`: uses OpenSSL for hashing/HMAC and symmetric encryption. + - `v3_rust`: uses pure Rust crypto crates for hashing/HMAC and encryption. -- All cryptographic algorithms are provided by [openssl](https://www.openssl.org/). +- Cryptographic algorithms are provided by the selected backend: + - `v3_openssl`: [openssl](https://www.openssl.org/) + - `v3_rust`: pure Rust crates [Rust Crypto](https://github.com/RustCrypto): (`md-5`, `sha1`, `sha2`, `hmac`, `aes`, `des`, etc.) - For authentication, supports: MD5 (RFC3414), SHA1 (RFC3414) and non-standard SHA224, SHA256, SHA384, SHA512. @@ -221,8 +225,22 @@ assert_eq!(snmp_oid, snmp_oid2); AES192-CFB, AES256-CFB. Additional/different AES modes are not supported and may require patching the crate. -Note: DES legacy encryption may be disabled in openssl by default or even not -supported at all. Refer to the library documentation how to enable it. +Note: For `v3_openssl`, DES legacy encryption may be disabled in OpenSSL by default +or not supported at all. Refer to the library documentation how to enable it. + +### Feature selection examples + +Pure Rust backend: + +```shell +cargo add snmp2 --features v3_rust +``` + +OpenSSL backend (Windows-friendly vendored build): + +```shell +cargo add snmp2 --features "v3_openssl,openssl/vendored" +``` ## Example @@ -261,21 +279,22 @@ loop { } ``` -## Building +## Building (`v3_openssl`) -In case of problems (e.g. with [cross-rs](https://github.com/cross-rs/cross)), -add `openssl` with `vendored` feature: +When using the `v3_openssl` backend, in case of problems (e.g. with +[cross-rs](https://github.com/cross-rs/cross)), add `openssl` with `vendored` feature: ```shell cargo add openssl --features vendored ``` -## FIPS-140 support +## FIPS-140 support (`v3_openssl`) -The crate uses openssl cryptography only and becomes FIPS-140 compliant as soon +When using the `v3_openssl` backend, the crate becomes FIPS-140 compliant as soon as FIPS mode is activated in `openssl`. Refer to the [openssl crate](https://docs.rs/openssl) crate and [openssl library](https://www.openssl.org/) documentation for more details. +The `v3_rust` backend does not rely on OpenSSL and is not FIPS-certified. ## MSRV From 3e2ab7a4fa74a86ae56b490c552d1b89959c2ff6 Mon Sep 17 00:00:00 2001 From: Ting Lou Date: Wed, 7 Jan 2026 14:05:12 +0800 Subject: [PATCH 7/7] fmt --- src/v3.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/v3.rs b/src/v3.rs index 127bc5d..f8313a7 100644 --- a/src/v3.rs +++ b/src/v3.rs @@ -449,8 +449,8 @@ impl Security { openssl::rand::rand_bytes(&mut salt[4..])?; #[cfg(feature = "v3_rust")] { - use rand::Rng; - rand::rng().fill(&mut salt[4..]); + use rand::Rng; + rand::rng().fill(&mut salt[4..]); } if data.is_empty() { @@ -521,8 +521,8 @@ impl Security { openssl::rand::rand_bytes(&mut iv[salt_pos..])?; #[cfg(feature = "v3_rust")] { - use rand::Rng; - rand::rng().fill(&mut iv[salt_pos..]); + use rand::Rng; + rand::rng().fill(&mut iv[salt_pos..]); } if self.authoritative_state.priv_key.len() < key_len {