1use digest::{Digest, KeyInit, Mac, OutputSizeUser};
34use zeroize::{Zeroize, ZeroizeOnDrop};
35
36use super::AuthProtocol;
37
38pub const MIN_PASSWORD_LENGTH: usize = 8;
44
45#[derive(Clone, Zeroize, ZeroizeOnDrop)]
83pub struct MasterKey {
84 key: Vec<u8>,
85 #[zeroize(skip)]
86 protocol: AuthProtocol,
87}
88
89impl MasterKey {
90 pub fn from_password(protocol: AuthProtocol, password: &[u8]) -> Self {
101 if password.len() < MIN_PASSWORD_LENGTH {
102 tracing::warn!(
103 password_len = password.len(),
104 min_len = MIN_PASSWORD_LENGTH,
105 "SNMPv3 password is shorter than recommended minimum; \
106 net-snmp rejects passwords shorter than 8 characters"
107 );
108 }
109 let key = password_to_key(protocol, password);
110 Self { key, protocol }
111 }
112
113 pub fn from_str_password(protocol: AuthProtocol, password: &str) -> Self {
115 Self::from_password(protocol, password.as_bytes())
116 }
117
118 pub fn from_bytes(protocol: AuthProtocol, key: impl Into<Vec<u8>>) -> Self {
123 Self {
124 key: key.into(),
125 protocol,
126 }
127 }
128
129 pub fn localize(&self, engine_id: &[u8]) -> LocalizedKey {
136 let localized = localize_key(self.protocol, &self.key, engine_id);
137 LocalizedKey {
138 key: localized,
139 protocol: self.protocol,
140 }
141 }
142
143 pub fn protocol(&self) -> AuthProtocol {
145 self.protocol
146 }
147
148 pub fn as_bytes(&self) -> &[u8] {
150 &self.key
151 }
152}
153
154impl std::fmt::Debug for MasterKey {
155 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
156 f.debug_struct("MasterKey")
157 .field("protocol", &self.protocol)
158 .field("key", &"[REDACTED]")
159 .finish()
160 }
161}
162
163#[derive(Clone, Zeroize, ZeroizeOnDrop)]
174pub struct LocalizedKey {
175 key: Vec<u8>,
176 #[zeroize(skip)]
177 protocol: AuthProtocol,
178}
179
180impl LocalizedKey {
181 pub fn from_password(protocol: AuthProtocol, password: &[u8], engine_id: &[u8]) -> Self {
204 MasterKey::from_password(protocol, password).localize(engine_id)
205 }
206
207 pub fn from_str_password(protocol: AuthProtocol, password: &str, engine_id: &[u8]) -> Self {
212 Self::from_password(protocol, password.as_bytes(), engine_id)
213 }
214
215 pub fn from_master_key(master: &MasterKey, engine_id: &[u8]) -> Self {
220 master.localize(engine_id)
221 }
222
223 pub fn from_bytes(protocol: AuthProtocol, key: impl Into<Vec<u8>>) -> Self {
227 Self {
228 key: key.into(),
229 protocol,
230 }
231 }
232
233 pub fn protocol(&self) -> AuthProtocol {
235 self.protocol
236 }
237
238 pub fn as_bytes(&self) -> &[u8] {
240 &self.key
241 }
242
243 pub fn mac_len(&self) -> usize {
245 self.protocol.mac_len()
246 }
247
248 pub fn compute_hmac(&self, data: &[u8]) -> Vec<u8> {
253 compute_hmac(self.protocol, &self.key, data)
254 }
255
256 pub fn verify_hmac(&self, data: &[u8], expected: &[u8]) -> bool {
260 let computed = self.compute_hmac(data);
261 if computed.len() != expected.len() {
263 return false;
264 }
265 let mut result = 0u8;
266 for (a, b) in computed.iter().zip(expected.iter()) {
267 result |= a ^ b;
268 }
269 result == 0
270 }
271}
272
273impl std::fmt::Debug for LocalizedKey {
274 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
275 f.debug_struct("LocalizedKey")
276 .field("protocol", &self.protocol)
277 .field("key", &"[REDACTED]")
278 .finish()
279 }
280}
281
282fn password_to_key(protocol: AuthProtocol, password: &[u8]) -> Vec<u8> {
286 const EXPANSION_SIZE: usize = 1_048_576; match protocol {
289 AuthProtocol::Md5 => password_to_key_impl::<md5::Md5>(password, EXPANSION_SIZE),
290 AuthProtocol::Sha1 => password_to_key_impl::<sha1::Sha1>(password, EXPANSION_SIZE),
291 AuthProtocol::Sha224 => password_to_key_impl::<sha2::Sha224>(password, EXPANSION_SIZE),
292 AuthProtocol::Sha256 => password_to_key_impl::<sha2::Sha256>(password, EXPANSION_SIZE),
293 AuthProtocol::Sha384 => password_to_key_impl::<sha2::Sha384>(password, EXPANSION_SIZE),
294 AuthProtocol::Sha512 => password_to_key_impl::<sha2::Sha512>(password, EXPANSION_SIZE),
295 }
296}
297
298fn password_to_key_impl<D>(password: &[u8], expansion_size: usize) -> Vec<u8>
299where
300 D: Digest + Default,
301{
302 if password.is_empty() {
303 return vec![0u8; <D as OutputSizeUser>::output_size()];
305 }
306
307 let mut hasher = D::new();
308
309 let mut buf = [0u8; 64];
312 let password_len = password.len();
313 let mut password_index = 0;
314 let mut count = 0;
315
316 while count < expansion_size {
317 for byte in &mut buf {
319 *byte = password[password_index];
320 password_index = (password_index + 1) % password_len;
321 }
322 hasher.update(buf);
323 count += 64;
324 }
325
326 hasher.finalize().to_vec()
327}
328
329fn localize_key(protocol: AuthProtocol, master_key: &[u8], engine_id: &[u8]) -> Vec<u8> {
334 match protocol {
335 AuthProtocol::Md5 => localize_key_impl::<md5::Md5>(master_key, engine_id),
336 AuthProtocol::Sha1 => localize_key_impl::<sha1::Sha1>(master_key, engine_id),
337 AuthProtocol::Sha224 => localize_key_impl::<sha2::Sha224>(master_key, engine_id),
338 AuthProtocol::Sha256 => localize_key_impl::<sha2::Sha256>(master_key, engine_id),
339 AuthProtocol::Sha384 => localize_key_impl::<sha2::Sha384>(master_key, engine_id),
340 AuthProtocol::Sha512 => localize_key_impl::<sha2::Sha512>(master_key, engine_id),
341 }
342}
343
344fn localize_key_impl<D>(master_key: &[u8], engine_id: &[u8]) -> Vec<u8>
345where
346 D: Digest + Default,
347{
348 let mut hasher = D::new();
349 hasher.update(master_key);
350 hasher.update(engine_id);
351 hasher.update(master_key);
352 hasher.finalize().to_vec()
353}
354
355fn compute_hmac(protocol: AuthProtocol, key: &[u8], data: &[u8]) -> Vec<u8> {
357 match protocol {
358 AuthProtocol::Md5 => compute_hmac_md5(key, data, 12),
359 AuthProtocol::Sha1 => compute_hmac_sha1(key, data, 12),
360 AuthProtocol::Sha224 => compute_hmac_sha224(key, data, 16),
361 AuthProtocol::Sha256 => compute_hmac_sha256(key, data, 24),
362 AuthProtocol::Sha384 => compute_hmac_sha384(key, data, 32),
363 AuthProtocol::Sha512 => compute_hmac_sha512(key, data, 48),
364 }
365}
366
367fn compute_hmac_md5(key: &[u8], data: &[u8], truncate_len: usize) -> Vec<u8> {
369 use hmac::Hmac;
370 type HmacMd5 = Hmac<md5::Md5>;
371
372 let mut mac = <HmacMd5 as KeyInit>::new_from_slice(key).expect("HMAC can take key of any size");
373 Mac::update(&mut mac, data);
374 let result = mac.finalize().into_bytes();
375 result[..truncate_len].to_vec()
376}
377
378fn compute_hmac_sha1(key: &[u8], data: &[u8], truncate_len: usize) -> Vec<u8> {
380 use hmac::Hmac;
381 type HmacSha1 = Hmac<sha1::Sha1>;
382
383 let mut mac =
384 <HmacSha1 as KeyInit>::new_from_slice(key).expect("HMAC can take key of any size");
385 Mac::update(&mut mac, data);
386 let result = mac.finalize().into_bytes();
387 result[..truncate_len].to_vec()
388}
389
390fn compute_hmac_sha224(key: &[u8], data: &[u8], truncate_len: usize) -> Vec<u8> {
392 use hmac::Hmac;
393 type HmacSha224 = Hmac<sha2::Sha224>;
394
395 let mut mac =
396 <HmacSha224 as KeyInit>::new_from_slice(key).expect("HMAC can take key of any size");
397 Mac::update(&mut mac, data);
398 let result = mac.finalize().into_bytes();
399 result[..truncate_len].to_vec()
400}
401
402fn compute_hmac_sha256(key: &[u8], data: &[u8], truncate_len: usize) -> Vec<u8> {
404 use hmac::Hmac;
405 type HmacSha256 = Hmac<sha2::Sha256>;
406
407 let mut mac =
408 <HmacSha256 as KeyInit>::new_from_slice(key).expect("HMAC can take key of any size");
409 Mac::update(&mut mac, data);
410 let result = mac.finalize().into_bytes();
411 result[..truncate_len].to_vec()
412}
413
414fn compute_hmac_sha384(key: &[u8], data: &[u8], truncate_len: usize) -> Vec<u8> {
416 use hmac::Hmac;
417 type HmacSha384 = Hmac<sha2::Sha384>;
418
419 let mut mac =
420 <HmacSha384 as KeyInit>::new_from_slice(key).expect("HMAC can take key of any size");
421 Mac::update(&mut mac, data);
422 let result = mac.finalize().into_bytes();
423 result[..truncate_len].to_vec()
424}
425
426fn compute_hmac_sha512(key: &[u8], data: &[u8], truncate_len: usize) -> Vec<u8> {
428 use hmac::Hmac;
429 type HmacSha512 = Hmac<sha2::Sha512>;
430
431 let mut mac =
432 <HmacSha512 as KeyInit>::new_from_slice(key).expect("HMAC can take key of any size");
433 Mac::update(&mut mac, data);
434 let result = mac.finalize().into_bytes();
435 result[..truncate_len].to_vec()
436}
437
438pub fn authenticate_message(
444 key: &LocalizedKey,
445 message: &mut [u8],
446 auth_offset: usize,
447 auth_len: usize,
448) {
449 let mac = key.compute_hmac(message);
451
452 message[auth_offset..auth_offset + auth_len].copy_from_slice(&mac);
454}
455
456pub fn verify_message(
460 key: &LocalizedKey,
461 message: &[u8],
462 auth_offset: usize,
463 auth_len: usize,
464) -> bool {
465 let received_mac = &message[auth_offset..auth_offset + auth_len];
467
468 let mut msg_copy = message.to_vec();
470 msg_copy[auth_offset..auth_offset + auth_len].fill(0);
471
472 key.verify_hmac(&msg_copy, received_mac)
474}
475
476#[derive(Clone, Zeroize, ZeroizeOnDrop)]
496pub struct MasterKeys {
497 auth_master: MasterKey,
499 #[zeroize(skip)]
502 priv_protocol: Option<super::PrivProtocol>,
503 priv_master: Option<MasterKey>,
504}
505
506impl MasterKeys {
507 pub fn new(auth_protocol: AuthProtocol, auth_password: &[u8]) -> Self {
517 Self {
518 auth_master: MasterKey::from_password(auth_protocol, auth_password),
519 priv_protocol: None,
520 priv_master: None,
521 }
522 }
523
524 pub fn with_privacy_same_password(mut self, priv_protocol: super::PrivProtocol) -> Self {
529 self.priv_protocol = Some(priv_protocol);
530 self
532 }
533
534 pub fn with_privacy(
539 mut self,
540 priv_protocol: super::PrivProtocol,
541 priv_password: &[u8],
542 ) -> Self {
543 self.priv_protocol = Some(priv_protocol);
544 self.priv_master = Some(MasterKey::from_password(
546 self.auth_master.protocol(),
547 priv_password,
548 ));
549 self
550 }
551
552 pub fn auth_master(&self) -> &MasterKey {
554 &self.auth_master
555 }
556
557 pub fn priv_master(&self) -> Option<&MasterKey> {
562 if self.priv_protocol.is_some() {
563 Some(self.priv_master.as_ref().unwrap_or(&self.auth_master))
564 } else {
565 None
566 }
567 }
568
569 pub fn priv_protocol(&self) -> Option<super::PrivProtocol> {
571 self.priv_protocol
572 }
573
574 pub fn auth_protocol(&self) -> AuthProtocol {
576 self.auth_master.protocol()
577 }
578
579 pub fn localize(&self, engine_id: &[u8]) -> (LocalizedKey, Option<crate::v3::PrivKey>) {
584 let auth_key = self.auth_master.localize(engine_id);
585
586 let priv_key = self.priv_protocol.map(|priv_protocol| {
587 let master = self.priv_master.as_ref().unwrap_or(&self.auth_master);
588 crate::v3::PrivKey::from_master_key(master, priv_protocol, engine_id)
589 });
590
591 (auth_key, priv_key)
592 }
593}
594
595impl std::fmt::Debug for MasterKeys {
596 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
597 f.debug_struct("MasterKeys")
598 .field("auth_protocol", &self.auth_master.protocol())
599 .field("priv_protocol", &self.priv_protocol)
600 .field("has_separate_priv_password", &self.priv_master.is_some())
601 .finish()
602 }
603}
604
605#[cfg(test)]
606mod tests {
607 use super::*;
608 use crate::format::hex::{decode as decode_hex, encode as encode_hex};
609
610 #[test]
611 fn test_password_to_key_md5() {
612 let password = b"maplesyrup";
616 let key = password_to_key(AuthProtocol::Md5, password);
617
618 assert_eq!(key.len(), 16);
619 assert_eq!(encode_hex(&key), "9faf3283884e92834ebc9847d8edd963");
620 }
621
622 #[test]
623 fn test_password_to_key_sha1() {
624 let password = b"maplesyrup";
628 let key = password_to_key(AuthProtocol::Sha1, password);
629
630 assert_eq!(key.len(), 20);
631 assert_eq!(encode_hex(&key), "9fb5cc0381497b3793528939ff788d5d79145211");
632 }
633
634 #[test]
635 fn test_localize_key_md5() {
636 let password = b"maplesyrup";
641 let engine_id = decode_hex("000000000000000000000002").unwrap();
642
643 let key = LocalizedKey::from_password(AuthProtocol::Md5, password, &engine_id);
644
645 assert_eq!(key.as_bytes().len(), 16);
646 assert_eq!(
647 encode_hex(key.as_bytes()),
648 "526f5eed9fcce26f8964c2930787d82b"
649 );
650 }
651
652 #[test]
653 fn test_localize_key_sha1() {
654 let password = b"maplesyrup";
658 let engine_id = decode_hex("000000000000000000000002").unwrap();
659
660 let key = LocalizedKey::from_password(AuthProtocol::Sha1, password, &engine_id);
661
662 assert_eq!(key.as_bytes().len(), 20);
663 assert_eq!(
664 encode_hex(key.as_bytes()),
665 "6695febc9288e36282235fc7151f128497b38f3f"
666 );
667 }
668
669 #[test]
670 fn test_hmac_computation() {
671 let key = LocalizedKey::from_bytes(
672 AuthProtocol::Md5,
673 vec![
674 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
675 0x0f, 0x10,
676 ],
677 );
678
679 let data = b"test message";
680 let mac = key.compute_hmac(data);
681
682 assert_eq!(mac.len(), 12);
684
685 assert!(key.verify_hmac(data, &mac));
687
688 let mut wrong_mac = mac.clone();
690 wrong_mac[0] ^= 0xFF;
691 assert!(!key.verify_hmac(data, &wrong_mac));
692 }
693
694 #[test]
695 fn test_empty_password() {
696 let key = password_to_key(AuthProtocol::Md5, b"");
697 assert_eq!(key.len(), 16);
698 assert!(key.iter().all(|&b| b == 0));
699 }
700
701 #[test]
702 fn test_from_str_password() {
703 let engine_id = decode_hex("000000000000000000000002").unwrap();
705
706 let key_from_bytes =
707 LocalizedKey::from_password(AuthProtocol::Sha1, b"maplesyrup", &engine_id);
708 let key_from_str =
709 LocalizedKey::from_str_password(AuthProtocol::Sha1, "maplesyrup", &engine_id);
710
711 assert_eq!(key_from_bytes.as_bytes(), key_from_str.as_bytes());
712 assert_eq!(key_from_bytes.protocol(), key_from_str.protocol());
713 }
714
715 #[test]
716 fn test_master_key_localize_md5() {
717 let password = b"maplesyrup";
719 let engine_id = decode_hex("000000000000000000000002").unwrap();
720
721 let master = MasterKey::from_password(AuthProtocol::Md5, password);
722 let localized_via_master = master.localize(&engine_id);
723 let localized_direct = LocalizedKey::from_password(AuthProtocol::Md5, password, &engine_id);
724
725 assert_eq!(localized_via_master.as_bytes(), localized_direct.as_bytes());
726 assert_eq!(localized_via_master.protocol(), localized_direct.protocol());
727
728 assert_eq!(
730 encode_hex(master.as_bytes()),
731 "9faf3283884e92834ebc9847d8edd963"
732 );
733 }
734
735 #[test]
736 fn test_master_key_localize_sha1() {
737 let password = b"maplesyrup";
738 let engine_id = decode_hex("000000000000000000000002").unwrap();
739
740 let master = MasterKey::from_password(AuthProtocol::Sha1, password);
741 let localized_via_master = master.localize(&engine_id);
742 let localized_direct =
743 LocalizedKey::from_password(AuthProtocol::Sha1, password, &engine_id);
744
745 assert_eq!(localized_via_master.as_bytes(), localized_direct.as_bytes());
746
747 assert_eq!(
749 encode_hex(master.as_bytes()),
750 "9fb5cc0381497b3793528939ff788d5d79145211"
751 );
752 }
753
754 #[test]
755 fn test_master_key_reuse_for_multiple_engines() {
756 let password = b"maplesyrup";
758 let engine_id_1 = decode_hex("000000000000000000000001").unwrap();
759 let engine_id_2 = decode_hex("000000000000000000000002").unwrap();
760
761 let master = MasterKey::from_password(AuthProtocol::Sha256, password);
762
763 let key1 = master.localize(&engine_id_1);
764 let key2 = master.localize(&engine_id_2);
765
766 assert_ne!(key1.as_bytes(), key2.as_bytes());
768
769 let direct1 = LocalizedKey::from_password(AuthProtocol::Sha256, password, &engine_id_1);
771 let direct2 = LocalizedKey::from_password(AuthProtocol::Sha256, password, &engine_id_2);
772
773 assert_eq!(key1.as_bytes(), direct1.as_bytes());
774 assert_eq!(key2.as_bytes(), direct2.as_bytes());
775 }
776
777 #[test]
778 fn test_from_master_key() {
779 let password = b"maplesyrup";
780 let engine_id = decode_hex("000000000000000000000002").unwrap();
781
782 let master = MasterKey::from_password(AuthProtocol::Sha256, password);
783 let key_via_localize = master.localize(&engine_id);
784 let key_via_from_master = LocalizedKey::from_master_key(&master, &engine_id);
785
786 assert_eq!(key_via_localize.as_bytes(), key_via_from_master.as_bytes());
787 }
788
789 #[test]
790 fn test_master_keys_auth_only() {
791 let engine_id = decode_hex("000000000000000000000002").unwrap();
792 let master_keys = MasterKeys::new(AuthProtocol::Sha256, b"authpassword");
793
794 assert_eq!(master_keys.auth_protocol(), AuthProtocol::Sha256);
795 assert!(master_keys.priv_protocol().is_none());
796 assert!(master_keys.priv_master().is_none());
797
798 let (auth_key, priv_key) = master_keys.localize(&engine_id);
799 assert!(priv_key.is_none());
800 assert_eq!(auth_key.protocol(), AuthProtocol::Sha256);
801 }
802
803 #[test]
804 fn test_master_keys_with_privacy_same_password() {
805 use crate::v3::PrivProtocol;
806
807 let engine_id = decode_hex("000000000000000000000002").unwrap();
808 let master_keys = MasterKeys::new(AuthProtocol::Sha256, b"sharedpassword")
809 .with_privacy_same_password(PrivProtocol::Aes128);
810
811 assert_eq!(master_keys.auth_protocol(), AuthProtocol::Sha256);
812 assert_eq!(master_keys.priv_protocol(), Some(PrivProtocol::Aes128));
813
814 let (auth_key, priv_key) = master_keys.localize(&engine_id);
815 assert!(priv_key.is_some());
816 assert_eq!(auth_key.protocol(), AuthProtocol::Sha256);
817 }
818
819 #[test]
820 fn test_master_keys_with_privacy_different_password() {
821 use crate::v3::PrivProtocol;
822
823 let engine_id = decode_hex("000000000000000000000002").unwrap();
824 let master_keys = MasterKeys::new(AuthProtocol::Sha256, b"authpassword")
825 .with_privacy(PrivProtocol::Aes128, b"privpassword");
826
827 let (_auth_key, priv_key) = master_keys.localize(&engine_id);
828 assert!(priv_key.is_some());
829
830 let same_password_keys = MasterKeys::new(AuthProtocol::Sha256, b"authpassword")
832 .with_privacy_same_password(PrivProtocol::Aes128);
833 let (_, priv_key_same) = same_password_keys.localize(&engine_id);
834
835 assert_ne!(
838 priv_key.as_ref().unwrap().encryption_key(),
839 priv_key_same.as_ref().unwrap().encryption_key()
840 );
841 }
842}