use zeroize::{Zeroize, ZeroizeOnDrop};
use super::AuthProtocol;
use super::crypto::{CryptoProvider, CryptoResult};
pub const MIN_PASSWORD_LENGTH: usize = 8;
#[derive(Clone, Zeroize, ZeroizeOnDrop)]
pub struct MasterKey {
key: Vec<u8>,
#[zeroize(skip)]
protocol: AuthProtocol,
}
impl MasterKey {
pub fn from_password(protocol: AuthProtocol, password: &[u8]) -> CryptoResult<Self> {
if password.len() < MIN_PASSWORD_LENGTH {
tracing::warn!(target: "async_snmp::v3", { password_len = password.len(), min_len = MIN_PASSWORD_LENGTH }, "SNMPv3 password is shorter than recommended minimum; \
net-snmp rejects passwords shorter than 8 characters");
}
let key = password_to_key(protocol, password)?;
Ok(Self { key, protocol })
}
pub fn from_str_password(protocol: AuthProtocol, password: &str) -> CryptoResult<Self> {
Self::from_password(protocol, password.as_bytes())
}
pub fn from_bytes(protocol: AuthProtocol, key: impl Into<Vec<u8>>) -> Self {
Self {
key: key.into(),
protocol,
}
}
pub fn localize(&self, engine_id: &[u8]) -> CryptoResult<LocalizedKey> {
let localized = localize_key(self.protocol, &self.key, engine_id)?;
Ok(LocalizedKey {
key: localized,
protocol: self.protocol,
})
}
pub fn protocol(&self) -> AuthProtocol {
self.protocol
}
pub fn as_bytes(&self) -> &[u8] {
&self.key
}
}
impl std::fmt::Debug for MasterKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MasterKey")
.field("protocol", &self.protocol)
.field("key", &"[REDACTED]")
.finish()
}
}
impl AsRef<[u8]> for MasterKey {
fn as_ref(&self) -> &[u8] {
self.as_bytes()
}
}
#[derive(Clone, Zeroize, ZeroizeOnDrop)]
pub struct LocalizedKey {
key: Vec<u8>,
#[zeroize(skip)]
protocol: AuthProtocol,
}
impl LocalizedKey {
pub fn from_password(
protocol: AuthProtocol,
password: &[u8],
engine_id: &[u8],
) -> CryptoResult<Self> {
MasterKey::from_password(protocol, password)?.localize(engine_id)
}
pub fn from_str_password(
protocol: AuthProtocol,
password: &str,
engine_id: &[u8],
) -> CryptoResult<Self> {
Self::from_password(protocol, password.as_bytes(), engine_id)
}
pub fn from_master_key(master: &MasterKey, engine_id: &[u8]) -> CryptoResult<Self> {
master.localize(engine_id)
}
pub fn from_bytes(protocol: AuthProtocol, key: impl Into<Vec<u8>>) -> Self {
Self {
key: key.into(),
protocol,
}
}
pub fn protocol(&self) -> AuthProtocol {
self.protocol
}
pub fn as_bytes(&self) -> &[u8] {
&self.key
}
pub fn mac_len(&self) -> usize {
self.protocol.mac_len()
}
pub fn compute_hmac(&self, data: &[u8]) -> CryptoResult<Vec<u8>> {
compute_hmac(self.protocol, &self.key, data)
}
pub fn verify_hmac(&self, data: &[u8], expected: &[u8]) -> CryptoResult<bool> {
let computed = self.compute_hmac(data)?;
if computed.len() != expected.len() {
return Ok(false);
}
let mut result = 0u8;
for (a, b) in computed.iter().zip(expected.iter()) {
result |= a ^ b;
}
Ok(result == 0)
}
}
impl std::fmt::Debug for LocalizedKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("LocalizedKey")
.field("protocol", &self.protocol)
.field("key", &"[REDACTED]")
.finish()
}
}
impl AsRef<[u8]> for LocalizedKey {
fn as_ref(&self) -> &[u8] {
self.as_bytes()
}
}
fn password_to_key(protocol: AuthProtocol, password: &[u8]) -> CryptoResult<Vec<u8>> {
super::crypto::provider().password_to_key(protocol, password)
}
fn localize_key(
protocol: AuthProtocol,
master_key: &[u8],
engine_id: &[u8],
) -> CryptoResult<Vec<u8>> {
super::crypto::provider().localize_key(protocol, master_key, engine_id)
}
fn compute_hmac(protocol: AuthProtocol, key: &[u8], data: &[u8]) -> CryptoResult<Vec<u8>> {
super::crypto::provider().compute_hmac(protocol, key, &[data], protocol.mac_len())
}
fn compute_hmac_slices(
protocol: AuthProtocol,
key: &[u8],
slices: &[&[u8]],
) -> CryptoResult<Vec<u8>> {
super::crypto::provider().compute_hmac(protocol, key, slices, protocol.mac_len())
}
pub fn authenticate_message(
key: &LocalizedKey,
message: &mut [u8],
auth_offset: usize,
auth_len: usize,
) -> CryptoResult<()> {
let end = match auth_offset.checked_add(auth_len) {
Some(e) if e <= message.len() => e,
_ => return Ok(()),
};
let mac = key.compute_hmac(message)?;
message[auth_offset..end].copy_from_slice(&mac);
Ok(())
}
pub fn verify_message(
key: &LocalizedKey,
message: &[u8],
auth_offset: usize,
auth_len: usize,
) -> CryptoResult<bool> {
let end = match auth_offset.checked_add(auth_len) {
Some(e) if e <= message.len() => e,
_ => return Ok(false),
};
let received_mac = &message[auth_offset..end];
const MAX_MAC_LEN: usize = 48; let zeros = [0u8; MAX_MAC_LEN];
let computed = compute_hmac_slices(
key.protocol,
key.as_bytes(),
&[&message[..auth_offset], &zeros[..auth_len], &message[end..]],
)?;
if computed.len() != received_mac.len() {
return Ok(false);
}
let mut result = 0u8;
for (a, b) in computed.iter().zip(received_mac.iter()) {
result |= a ^ b;
}
Ok(result == 0)
}
#[derive(Clone, Zeroize, ZeroizeOnDrop)]
pub struct MasterKeys {
auth_master: MasterKey,
#[zeroize(skip)]
priv_protocol: Option<super::PrivProtocol>,
priv_master: Option<MasterKey>,
}
impl MasterKeys {
pub fn new(auth_protocol: AuthProtocol, auth_password: &[u8]) -> CryptoResult<Self> {
Ok(Self {
auth_master: MasterKey::from_password(auth_protocol, auth_password)?,
priv_protocol: None,
priv_master: None,
})
}
pub fn with_privacy_same_password(mut self, priv_protocol: super::PrivProtocol) -> Self {
self.priv_protocol = Some(priv_protocol);
self
}
pub fn with_privacy(
mut self,
priv_protocol: super::PrivProtocol,
priv_password: &[u8],
) -> CryptoResult<Self> {
self.priv_protocol = Some(priv_protocol);
self.priv_master = Some(MasterKey::from_password(
self.auth_master.protocol(),
priv_password,
)?);
Ok(self)
}
pub fn auth_master(&self) -> &MasterKey {
&self.auth_master
}
pub fn priv_master(&self) -> Option<&MasterKey> {
if self.priv_protocol.is_some() {
Some(self.priv_master.as_ref().unwrap_or(&self.auth_master))
} else {
None
}
}
pub fn priv_protocol(&self) -> Option<super::PrivProtocol> {
self.priv_protocol
}
pub fn auth_protocol(&self) -> AuthProtocol {
self.auth_master.protocol()
}
pub fn localize(
&self,
engine_id: &[u8],
) -> CryptoResult<(LocalizedKey, Option<crate::v3::PrivKey>)> {
let auth_key = self.auth_master.localize(engine_id)?;
let priv_key = self
.priv_protocol
.map(|priv_protocol| {
let master = self.priv_master.as_ref().unwrap_or(&self.auth_master);
crate::v3::PrivKey::from_master_key(master, priv_protocol, engine_id)
})
.transpose()?;
Ok((auth_key, priv_key))
}
}
impl std::fmt::Debug for MasterKeys {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MasterKeys")
.field("auth_protocol", &self.auth_master.protocol())
.field("priv_protocol", &self.priv_protocol)
.field("has_separate_priv_password", &self.priv_master.is_some())
.finish()
}
}
pub(crate) fn extend_key(
protocol: AuthProtocol,
key: &[u8],
target_len: usize,
) -> CryptoResult<Vec<u8>> {
if key.len() >= target_len {
return Ok(key[..target_len].to_vec());
}
let provider = super::crypto::provider();
let mut result = key.to_vec();
while result.len() < target_len {
let hash = provider.hash(protocol, &result)?;
result.extend_from_slice(&hash);
}
result.truncate(target_len);
Ok(result)
}
pub(crate) fn extend_key_reeder(
protocol: AuthProtocol,
key: &[u8],
engine_id: &[u8],
target_len: usize,
) -> CryptoResult<Vec<u8>> {
if key.len() >= target_len {
return Ok(key[..target_len].to_vec());
}
let mut result = key.to_vec();
let mut current_kul = key.to_vec();
while result.len() < target_len {
let ku = password_to_key(protocol, ¤t_kul)?;
let new_kul = localize_key(protocol, &ku, engine_id)?;
let bytes_needed = target_len - result.len();
let bytes_to_copy = bytes_needed.min(new_kul.len());
result.extend_from_slice(&new_kul[..bytes_to_copy]);
current_kul = new_kul;
}
Ok(result)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::format::hex::{decode as decode_hex, encode as encode_hex};
#[cfg(feature = "crypto-rustcrypto")]
#[test]
fn test_password_to_key_md5() {
let password = b"maplesyrup";
let key = password_to_key(AuthProtocol::Md5, password).unwrap();
assert_eq!(key.len(), 16);
assert_eq!(encode_hex(&key), "9faf3283884e92834ebc9847d8edd963");
}
#[test]
fn test_password_to_key_sha1() {
let password = b"maplesyrup";
let key = password_to_key(AuthProtocol::Sha1, password).unwrap();
assert_eq!(key.len(), 20);
assert_eq!(encode_hex(&key), "9fb5cc0381497b3793528939ff788d5d79145211");
}
#[cfg(feature = "crypto-rustcrypto")]
#[test]
fn test_localize_key_md5() {
let password = b"maplesyrup";
let engine_id = decode_hex("000000000000000000000002").unwrap();
let key = LocalizedKey::from_password(AuthProtocol::Md5, password, &engine_id).unwrap();
assert_eq!(key.as_bytes().len(), 16);
assert_eq!(
encode_hex(key.as_bytes()),
"526f5eed9fcce26f8964c2930787d82b"
);
}
#[test]
fn test_localize_key_sha1() {
let password = b"maplesyrup";
let engine_id = decode_hex("000000000000000000000002").unwrap();
let key = LocalizedKey::from_password(AuthProtocol::Sha1, password, &engine_id).unwrap();
assert_eq!(key.as_bytes().len(), 20);
assert_eq!(
encode_hex(key.as_bytes()),
"6695febc9288e36282235fc7151f128497b38f3f"
);
}
#[cfg(feature = "crypto-rustcrypto")]
#[test]
fn test_hmac_computation() {
let key = LocalizedKey::from_bytes(
AuthProtocol::Md5,
vec![
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
0x0f, 0x10,
],
);
let data = b"test message";
let mac = key.compute_hmac(data).unwrap();
assert_eq!(mac.len(), 12);
assert!(key.verify_hmac(data, &mac).unwrap());
let mut wrong_mac = mac.clone();
wrong_mac[0] ^= 0xFF;
assert!(!key.verify_hmac(data, &wrong_mac).unwrap());
}
#[cfg(feature = "crypto-rustcrypto")]
#[test]
fn test_empty_password() {
let key = password_to_key(AuthProtocol::Md5, b"").unwrap();
assert_eq!(key.len(), 16);
assert!(key.iter().all(|&b| b == 0));
}
#[test]
fn test_from_str_password() {
let engine_id = decode_hex("000000000000000000000002").unwrap();
let key_from_bytes =
LocalizedKey::from_password(AuthProtocol::Sha1, b"maplesyrup", &engine_id).unwrap();
let key_from_str =
LocalizedKey::from_str_password(AuthProtocol::Sha1, "maplesyrup", &engine_id).unwrap();
assert_eq!(key_from_bytes.as_bytes(), key_from_str.as_bytes());
assert_eq!(key_from_bytes.protocol(), key_from_str.protocol());
}
#[cfg(feature = "crypto-rustcrypto")]
#[test]
fn test_master_key_localize_md5() {
let password = b"maplesyrup";
let engine_id = decode_hex("000000000000000000000002").unwrap();
let master = MasterKey::from_password(AuthProtocol::Md5, password).unwrap();
let localized_via_master = master.localize(&engine_id).unwrap();
let localized_direct =
LocalizedKey::from_password(AuthProtocol::Md5, password, &engine_id).unwrap();
assert_eq!(localized_via_master.as_bytes(), localized_direct.as_bytes());
assert_eq!(localized_via_master.protocol(), localized_direct.protocol());
assert_eq!(
encode_hex(master.as_bytes()),
"9faf3283884e92834ebc9847d8edd963"
);
}
#[test]
fn test_master_key_localize_sha1() {
let password = b"maplesyrup";
let engine_id = decode_hex("000000000000000000000002").unwrap();
let master = MasterKey::from_password(AuthProtocol::Sha1, password).unwrap();
let localized_via_master = master.localize(&engine_id).unwrap();
let localized_direct =
LocalizedKey::from_password(AuthProtocol::Sha1, password, &engine_id).unwrap();
assert_eq!(localized_via_master.as_bytes(), localized_direct.as_bytes());
assert_eq!(
encode_hex(master.as_bytes()),
"9fb5cc0381497b3793528939ff788d5d79145211"
);
}
#[test]
fn test_master_key_reuse_for_multiple_engines() {
let password = b"maplesyrup";
let engine_id_1 = decode_hex("000000000000000000000001").unwrap();
let engine_id_2 = decode_hex("000000000000000000000002").unwrap();
let master = MasterKey::from_password(AuthProtocol::Sha256, password).unwrap();
let key1 = master.localize(&engine_id_1).unwrap();
let key2 = master.localize(&engine_id_2).unwrap();
assert_ne!(key1.as_bytes(), key2.as_bytes());
let direct1 =
LocalizedKey::from_password(AuthProtocol::Sha256, password, &engine_id_1).unwrap();
let direct2 =
LocalizedKey::from_password(AuthProtocol::Sha256, password, &engine_id_2).unwrap();
assert_eq!(key1.as_bytes(), direct1.as_bytes());
assert_eq!(key2.as_bytes(), direct2.as_bytes());
}
#[test]
fn test_from_master_key() {
let password = b"maplesyrup";
let engine_id = decode_hex("000000000000000000000002").unwrap();
let master = MasterKey::from_password(AuthProtocol::Sha256, password).unwrap();
let key_via_localize = master.localize(&engine_id).unwrap();
let key_via_from_master = LocalizedKey::from_master_key(&master, &engine_id).unwrap();
assert_eq!(key_via_localize.as_bytes(), key_via_from_master.as_bytes());
}
#[test]
fn test_master_keys_auth_only() {
let engine_id = decode_hex("000000000000000000000002").unwrap();
let master_keys = MasterKeys::new(AuthProtocol::Sha256, b"authpassword").unwrap();
assert_eq!(master_keys.auth_protocol(), AuthProtocol::Sha256);
assert!(master_keys.priv_protocol().is_none());
assert!(master_keys.priv_master().is_none());
let (auth_key, priv_key) = master_keys.localize(&engine_id).unwrap();
assert!(priv_key.is_none());
assert_eq!(auth_key.protocol(), AuthProtocol::Sha256);
}
#[test]
fn test_master_keys_with_privacy_same_password() {
use crate::v3::PrivProtocol;
let engine_id = decode_hex("000000000000000000000002").unwrap();
let master_keys = MasterKeys::new(AuthProtocol::Sha256, b"sharedpassword")
.unwrap()
.with_privacy_same_password(PrivProtocol::Aes128);
assert_eq!(master_keys.auth_protocol(), AuthProtocol::Sha256);
assert_eq!(master_keys.priv_protocol(), Some(PrivProtocol::Aes128));
let (auth_key, priv_key) = master_keys.localize(&engine_id).unwrap();
assert!(priv_key.is_some());
assert_eq!(auth_key.protocol(), AuthProtocol::Sha256);
}
#[test]
fn test_master_keys_with_privacy_different_password() {
use crate::v3::PrivProtocol;
let engine_id = decode_hex("000000000000000000000002").unwrap();
let master_keys = MasterKeys::new(AuthProtocol::Sha256, b"authpassword")
.unwrap()
.with_privacy(PrivProtocol::Aes128, b"privpassword")
.unwrap();
let (_auth_key, priv_key) = master_keys.localize(&engine_id).unwrap();
assert!(priv_key.is_some());
let same_password_keys = MasterKeys::new(AuthProtocol::Sha256, b"authpassword")
.unwrap()
.with_privacy_same_password(PrivProtocol::Aes128);
let (_, priv_key_same) = same_password_keys.localize(&engine_id).unwrap();
assert_ne!(
priv_key.as_ref().unwrap().encryption_key(),
priv_key_same.as_ref().unwrap().encryption_key()
);
}
#[cfg(feature = "crypto-rustcrypto")]
#[test]
fn test_reeder_extend_key_md5_kat() {
let password = b"maplesyrup";
let engine_id = decode_hex("000000000000000000000002").unwrap();
let k1 = LocalizedKey::from_password(AuthProtocol::Md5, password, &engine_id).unwrap();
assert_eq!(
encode_hex(k1.as_bytes()),
"526f5eed9fcce26f8964c2930787d82b"
);
let extended = extend_key_reeder(AuthProtocol::Md5, k1.as_bytes(), &engine_id, 32).unwrap();
assert_eq!(extended.len(), 32);
assert_eq!(
encode_hex(&extended),
"526f5eed9fcce26f8964c2930787d82b79eff44a90650ee0a3a40abfac5acc12"
);
}
#[test]
fn test_reeder_extend_key_sha1_kat() {
let password = b"maplesyrup";
let engine_id = decode_hex("000000000000000000000002").unwrap();
let k1 = LocalizedKey::from_password(AuthProtocol::Sha1, password, &engine_id).unwrap();
assert_eq!(
encode_hex(k1.as_bytes()),
"6695febc9288e36282235fc7151f128497b38f3f"
);
let extended =
extend_key_reeder(AuthProtocol::Sha1, k1.as_bytes(), &engine_id, 40).unwrap();
assert_eq!(extended.len(), 40);
assert_eq!(
encode_hex(&extended),
"6695febc9288e36282235fc7151f128497b38f3f9b8b6d78936ba6e7d19dfd9cd2d5065547743fb5"
);
}
#[test]
fn test_reeder_extend_key_sha1_to_32_bytes() {
let password = b"maplesyrup";
let engine_id = decode_hex("000000000000000000000002").unwrap();
let k1 = LocalizedKey::from_password(AuthProtocol::Sha1, password, &engine_id).unwrap();
let extended =
extend_key_reeder(AuthProtocol::Sha1, k1.as_bytes(), &engine_id, 32).unwrap();
assert_eq!(extended.len(), 32);
assert_eq!(
encode_hex(&extended),
"6695febc9288e36282235fc7151f128497b38f3f9b8b6d78936ba6e7d19dfd9c"
);
}
#[test]
fn test_reeder_extend_key_truncation() {
let long_key = vec![0xAAu8; 64];
let engine_id = decode_hex("000000000000000000000002").unwrap();
let extended = extend_key_reeder(AuthProtocol::Sha256, &long_key, &engine_id, 32).unwrap();
assert_eq!(extended.len(), 32);
assert_eq!(extended, vec![0xAAu8; 32]);
}
#[test]
fn test_reeder_vs_blumenthal_differ() {
let password = b"maplesyrup";
let engine_id = decode_hex("000000000000000000000002").unwrap();
let k1 = LocalizedKey::from_password(AuthProtocol::Sha1, password, &engine_id).unwrap();
let reeder = extend_key_reeder(AuthProtocol::Sha1, k1.as_bytes(), &engine_id, 32).unwrap();
let blumenthal = extend_key(AuthProtocol::Sha1, k1.as_bytes(), 32).unwrap();
assert_eq!(reeder.len(), 32);
assert_eq!(blumenthal.len(), 32);
assert_eq!(&reeder[..20], &blumenthal[..20]);
assert_ne!(&reeder[20..], &blumenthal[20..]);
}
}