1use digest::{Digest, KeyInit, Mac, OutputSizeUser, core_api::BlockSizeUser};
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!(target: "async_snmp::v3", { password_len = password.len(), min_len = MIN_PASSWORD_LENGTH }, "SNMPv3 password is shorter than recommended minimum; \
103 net-snmp rejects passwords shorter than 8 characters");
104 }
105 let key = password_to_key(protocol, password);
106 Self { key, protocol }
107 }
108
109 pub fn from_str_password(protocol: AuthProtocol, password: &str) -> Self {
111 Self::from_password(protocol, password.as_bytes())
112 }
113
114 pub fn from_bytes(protocol: AuthProtocol, key: impl Into<Vec<u8>>) -> Self {
119 Self {
120 key: key.into(),
121 protocol,
122 }
123 }
124
125 pub fn localize(&self, engine_id: &[u8]) -> LocalizedKey {
132 let localized = localize_key(self.protocol, &self.key, engine_id);
133 LocalizedKey {
134 key: localized,
135 protocol: self.protocol,
136 }
137 }
138
139 pub fn protocol(&self) -> AuthProtocol {
141 self.protocol
142 }
143
144 pub fn as_bytes(&self) -> &[u8] {
146 &self.key
147 }
148}
149
150impl std::fmt::Debug for MasterKey {
151 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
152 f.debug_struct("MasterKey")
153 .field("protocol", &self.protocol)
154 .field("key", &"[REDACTED]")
155 .finish()
156 }
157}
158
159#[derive(Clone, Zeroize, ZeroizeOnDrop)]
170pub struct LocalizedKey {
171 key: Vec<u8>,
172 #[zeroize(skip)]
173 protocol: AuthProtocol,
174}
175
176impl LocalizedKey {
177 pub fn from_password(protocol: AuthProtocol, password: &[u8], engine_id: &[u8]) -> Self {
200 MasterKey::from_password(protocol, password).localize(engine_id)
201 }
202
203 pub fn from_str_password(protocol: AuthProtocol, password: &str, engine_id: &[u8]) -> Self {
208 Self::from_password(protocol, password.as_bytes(), engine_id)
209 }
210
211 pub fn from_master_key(master: &MasterKey, engine_id: &[u8]) -> Self {
216 master.localize(engine_id)
217 }
218
219 pub fn from_bytes(protocol: AuthProtocol, key: impl Into<Vec<u8>>) -> Self {
223 Self {
224 key: key.into(),
225 protocol,
226 }
227 }
228
229 pub fn protocol(&self) -> AuthProtocol {
231 self.protocol
232 }
233
234 pub fn as_bytes(&self) -> &[u8] {
236 &self.key
237 }
238
239 pub fn mac_len(&self) -> usize {
241 self.protocol.mac_len()
242 }
243
244 pub fn compute_hmac(&self, data: &[u8]) -> Vec<u8> {
249 compute_hmac(self.protocol, &self.key, data)
250 }
251
252 pub fn verify_hmac(&self, data: &[u8], expected: &[u8]) -> bool {
256 let computed = self.compute_hmac(data);
257 if computed.len() != expected.len() {
259 return false;
260 }
261 let mut result = 0u8;
262 for (a, b) in computed.iter().zip(expected.iter()) {
263 result |= a ^ b;
264 }
265 result == 0
266 }
267}
268
269impl std::fmt::Debug for LocalizedKey {
270 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
271 f.debug_struct("LocalizedKey")
272 .field("protocol", &self.protocol)
273 .field("key", &"[REDACTED]")
274 .finish()
275 }
276}
277
278macro_rules! dispatch_auth {
283 ($protocol:expr, $fn:ident, $($arg:expr),*) => {
284 match $protocol {
285 AuthProtocol::Md5 => $fn::<md5::Md5>($($arg),*),
286 AuthProtocol::Sha1 => $fn::<sha1::Sha1>($($arg),*),
287 AuthProtocol::Sha224 => $fn::<sha2::Sha224>($($arg),*),
288 AuthProtocol::Sha256 => $fn::<sha2::Sha256>($($arg),*),
289 AuthProtocol::Sha384 => $fn::<sha2::Sha384>($($arg),*),
290 AuthProtocol::Sha512 => $fn::<sha2::Sha512>($($arg),*),
291 }
292 };
293}
294
295fn password_to_key(protocol: AuthProtocol, password: &[u8]) -> Vec<u8> {
299 const EXPANSION_SIZE: usize = 1_048_576; dispatch_auth!(protocol, password_to_key_impl, password, EXPANSION_SIZE)
301}
302
303fn password_to_key_impl<D>(password: &[u8], expansion_size: usize) -> Vec<u8>
304where
305 D: Digest + Default,
306{
307 if password.is_empty() {
308 return vec![0u8; <D as OutputSizeUser>::output_size()];
310 }
311
312 let mut hasher = D::new();
313
314 let mut buf = [0u8; 64];
317 let password_len = password.len();
318 let mut password_index = 0;
319 let mut count = 0;
320
321 while count < expansion_size {
322 for byte in &mut buf {
324 *byte = password[password_index];
325 password_index = (password_index + 1) % password_len;
326 }
327 hasher.update(buf);
328 count += 64;
329 }
330
331 hasher.finalize().to_vec()
332}
333
334fn localize_key(protocol: AuthProtocol, master_key: &[u8], engine_id: &[u8]) -> Vec<u8> {
339 dispatch_auth!(protocol, localize_key_impl, master_key, engine_id)
340}
341
342fn localize_key_impl<D>(master_key: &[u8], engine_id: &[u8]) -> Vec<u8>
343where
344 D: Digest + Default,
345{
346 let mut hasher = D::new();
347 hasher.update(master_key);
348 hasher.update(engine_id);
349 hasher.update(master_key);
350 hasher.finalize().to_vec()
351}
352
353fn compute_hmac(protocol: AuthProtocol, key: &[u8], data: &[u8]) -> Vec<u8> {
355 let truncate_len = protocol.mac_len();
356 dispatch_auth!(protocol, compute_hmac_impl, key, data, truncate_len)
357}
358
359fn compute_hmac_impl<D>(key: &[u8], data: &[u8], truncate_len: usize) -> Vec<u8>
361where
362 D: Digest + BlockSizeUser + Clone,
363{
364 use hmac::SimpleHmac;
365
366 let mut mac =
367 <SimpleHmac<D> as KeyInit>::new_from_slice(key).expect("HMAC can take key of any size");
368 Mac::update(&mut mac, data);
369 let result = mac.finalize().into_bytes();
370 result[..truncate_len].to_vec()
371}
372
373pub fn authenticate_message(
379 key: &LocalizedKey,
380 message: &mut [u8],
381 auth_offset: usize,
382 auth_len: usize,
383) {
384 let end = match auth_offset.checked_add(auth_len) {
385 Some(e) if e <= message.len() => e,
386 _ => return,
387 };
388
389 let mac = key.compute_hmac(message);
391
392 message[auth_offset..end].copy_from_slice(&mac);
394}
395
396pub fn verify_message(
400 key: &LocalizedKey,
401 message: &[u8],
402 auth_offset: usize,
403 auth_len: usize,
404) -> bool {
405 let end = match auth_offset.checked_add(auth_len) {
406 Some(e) if e <= message.len() => e,
407 _ => return false,
408 };
409
410 let received_mac = &message[auth_offset..end];
412
413 let mut msg_copy = message.to_vec();
415 msg_copy[auth_offset..end].fill(0);
416
417 key.verify_hmac(&msg_copy, received_mac)
419}
420
421#[derive(Clone, Zeroize, ZeroizeOnDrop)]
440pub struct MasterKeys {
441 auth_master: MasterKey,
443 #[zeroize(skip)]
446 priv_protocol: Option<super::PrivProtocol>,
447 priv_master: Option<MasterKey>,
448}
449
450impl MasterKeys {
451 pub fn new(auth_protocol: AuthProtocol, auth_password: &[u8]) -> Self {
461 Self {
462 auth_master: MasterKey::from_password(auth_protocol, auth_password),
463 priv_protocol: None,
464 priv_master: None,
465 }
466 }
467
468 pub fn with_privacy_same_password(mut self, priv_protocol: super::PrivProtocol) -> Self {
473 self.priv_protocol = Some(priv_protocol);
474 self
476 }
477
478 pub fn with_privacy(
483 mut self,
484 priv_protocol: super::PrivProtocol,
485 priv_password: &[u8],
486 ) -> Self {
487 self.priv_protocol = Some(priv_protocol);
488 self.priv_master = Some(MasterKey::from_password(
490 self.auth_master.protocol(),
491 priv_password,
492 ));
493 self
494 }
495
496 pub fn auth_master(&self) -> &MasterKey {
498 &self.auth_master
499 }
500
501 pub fn priv_master(&self) -> Option<&MasterKey> {
506 if self.priv_protocol.is_some() {
507 Some(self.priv_master.as_ref().unwrap_or(&self.auth_master))
508 } else {
509 None
510 }
511 }
512
513 pub fn priv_protocol(&self) -> Option<super::PrivProtocol> {
515 self.priv_protocol
516 }
517
518 pub fn auth_protocol(&self) -> AuthProtocol {
520 self.auth_master.protocol()
521 }
522
523 pub fn localize(&self, engine_id: &[u8]) -> (LocalizedKey, Option<crate::v3::PrivKey>) {
549 let auth_key = self.auth_master.localize(engine_id);
550
551 let priv_key = self.priv_protocol.map(|priv_protocol| {
552 let master = self.priv_master.as_ref().unwrap_or(&self.auth_master);
553 crate::v3::PrivKey::from_master_key(master, priv_protocol, engine_id)
554 });
555
556 (auth_key, priv_key)
557 }
558}
559
560impl std::fmt::Debug for MasterKeys {
561 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
562 f.debug_struct("MasterKeys")
563 .field("auth_protocol", &self.auth_master.protocol())
564 .field("priv_protocol", &self.priv_protocol)
565 .field("has_separate_priv_password", &self.priv_master.is_some())
566 .finish()
567 }
568}
569
570pub(crate) fn extend_key(protocol: AuthProtocol, key: &[u8], target_len: usize) -> Vec<u8> {
583 if key.len() >= target_len {
585 return key[..target_len].to_vec();
586 }
587
588 match protocol {
589 AuthProtocol::Md5 => extend_key_impl::<md5::Md5>(key, target_len),
590 AuthProtocol::Sha1 => extend_key_impl::<sha1::Sha1>(key, target_len),
591 AuthProtocol::Sha224 => extend_key_impl::<sha2::Sha224>(key, target_len),
592 AuthProtocol::Sha256 => extend_key_impl::<sha2::Sha256>(key, target_len),
593 AuthProtocol::Sha384 => extend_key_impl::<sha2::Sha384>(key, target_len),
594 AuthProtocol::Sha512 => extend_key_impl::<sha2::Sha512>(key, target_len),
595 }
596}
597
598fn extend_key_impl<D>(key: &[u8], target_len: usize) -> Vec<u8>
602where
603 D: Digest + Default,
604{
605 let mut result = key.to_vec();
606
607 while result.len() < target_len {
609 let mut hasher = D::new();
610 hasher.update(&result);
611 let hash = hasher.finalize();
612 result.extend_from_slice(&hash);
613 }
614
615 result.truncate(target_len);
617 result
618}
619
620pub(crate) fn extend_key_reeder(
640 protocol: AuthProtocol,
641 key: &[u8],
642 engine_id: &[u8],
643 target_len: usize,
644) -> Vec<u8> {
645 if key.len() >= target_len {
647 return key[..target_len].to_vec();
648 }
649
650 let mut result = key.to_vec();
651 let mut current_kul = key.to_vec();
652
653 while result.len() < target_len {
655 let ku = password_to_key(protocol, ¤t_kul);
658
659 let new_kul = localize_key(protocol, &ku, engine_id);
661
662 let bytes_needed = target_len - result.len();
664 let bytes_to_copy = bytes_needed.min(new_kul.len());
665 result.extend_from_slice(&new_kul[..bytes_to_copy]);
666
667 current_kul = new_kul;
669 }
670
671 result
672}
673
674#[cfg(test)]
675mod tests {
676 use super::*;
677 use crate::format::hex::{decode as decode_hex, encode as encode_hex};
678
679 #[test]
680 fn test_password_to_key_md5() {
681 let password = b"maplesyrup";
685 let key = password_to_key(AuthProtocol::Md5, password);
686
687 assert_eq!(key.len(), 16);
688 assert_eq!(encode_hex(&key), "9faf3283884e92834ebc9847d8edd963");
689 }
690
691 #[test]
692 fn test_password_to_key_sha1() {
693 let password = b"maplesyrup";
697 let key = password_to_key(AuthProtocol::Sha1, password);
698
699 assert_eq!(key.len(), 20);
700 assert_eq!(encode_hex(&key), "9fb5cc0381497b3793528939ff788d5d79145211");
701 }
702
703 #[test]
704 fn test_localize_key_md5() {
705 let password = b"maplesyrup";
710 let engine_id = decode_hex("000000000000000000000002").unwrap();
711
712 let key = LocalizedKey::from_password(AuthProtocol::Md5, password, &engine_id);
713
714 assert_eq!(key.as_bytes().len(), 16);
715 assert_eq!(
716 encode_hex(key.as_bytes()),
717 "526f5eed9fcce26f8964c2930787d82b"
718 );
719 }
720
721 #[test]
722 fn test_localize_key_sha1() {
723 let password = b"maplesyrup";
727 let engine_id = decode_hex("000000000000000000000002").unwrap();
728
729 let key = LocalizedKey::from_password(AuthProtocol::Sha1, password, &engine_id);
730
731 assert_eq!(key.as_bytes().len(), 20);
732 assert_eq!(
733 encode_hex(key.as_bytes()),
734 "6695febc9288e36282235fc7151f128497b38f3f"
735 );
736 }
737
738 #[test]
739 fn test_hmac_computation() {
740 let key = LocalizedKey::from_bytes(
741 AuthProtocol::Md5,
742 vec![
743 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
744 0x0f, 0x10,
745 ],
746 );
747
748 let data = b"test message";
749 let mac = key.compute_hmac(data);
750
751 assert_eq!(mac.len(), 12);
753
754 assert!(key.verify_hmac(data, &mac));
756
757 let mut wrong_mac = mac.clone();
759 wrong_mac[0] ^= 0xFF;
760 assert!(!key.verify_hmac(data, &wrong_mac));
761 }
762
763 #[test]
764 fn test_empty_password() {
765 let key = password_to_key(AuthProtocol::Md5, b"");
766 assert_eq!(key.len(), 16);
767 assert!(key.iter().all(|&b| b == 0));
768 }
769
770 #[test]
771 fn test_from_str_password() {
772 let engine_id = decode_hex("000000000000000000000002").unwrap();
774
775 let key_from_bytes =
776 LocalizedKey::from_password(AuthProtocol::Sha1, b"maplesyrup", &engine_id);
777 let key_from_str =
778 LocalizedKey::from_str_password(AuthProtocol::Sha1, "maplesyrup", &engine_id);
779
780 assert_eq!(key_from_bytes.as_bytes(), key_from_str.as_bytes());
781 assert_eq!(key_from_bytes.protocol(), key_from_str.protocol());
782 }
783
784 #[test]
785 fn test_master_key_localize_md5() {
786 let password = b"maplesyrup";
788 let engine_id = decode_hex("000000000000000000000002").unwrap();
789
790 let master = MasterKey::from_password(AuthProtocol::Md5, password);
791 let localized_via_master = master.localize(&engine_id);
792 let localized_direct = LocalizedKey::from_password(AuthProtocol::Md5, password, &engine_id);
793
794 assert_eq!(localized_via_master.as_bytes(), localized_direct.as_bytes());
795 assert_eq!(localized_via_master.protocol(), localized_direct.protocol());
796
797 assert_eq!(
799 encode_hex(master.as_bytes()),
800 "9faf3283884e92834ebc9847d8edd963"
801 );
802 }
803
804 #[test]
805 fn test_master_key_localize_sha1() {
806 let password = b"maplesyrup";
807 let engine_id = decode_hex("000000000000000000000002").unwrap();
808
809 let master = MasterKey::from_password(AuthProtocol::Sha1, password);
810 let localized_via_master = master.localize(&engine_id);
811 let localized_direct =
812 LocalizedKey::from_password(AuthProtocol::Sha1, password, &engine_id);
813
814 assert_eq!(localized_via_master.as_bytes(), localized_direct.as_bytes());
815
816 assert_eq!(
818 encode_hex(master.as_bytes()),
819 "9fb5cc0381497b3793528939ff788d5d79145211"
820 );
821 }
822
823 #[test]
824 fn test_master_key_reuse_for_multiple_engines() {
825 let password = b"maplesyrup";
827 let engine_id_1 = decode_hex("000000000000000000000001").unwrap();
828 let engine_id_2 = decode_hex("000000000000000000000002").unwrap();
829
830 let master = MasterKey::from_password(AuthProtocol::Sha256, password);
831
832 let key1 = master.localize(&engine_id_1);
833 let key2 = master.localize(&engine_id_2);
834
835 assert_ne!(key1.as_bytes(), key2.as_bytes());
837
838 let direct1 = LocalizedKey::from_password(AuthProtocol::Sha256, password, &engine_id_1);
840 let direct2 = LocalizedKey::from_password(AuthProtocol::Sha256, password, &engine_id_2);
841
842 assert_eq!(key1.as_bytes(), direct1.as_bytes());
843 assert_eq!(key2.as_bytes(), direct2.as_bytes());
844 }
845
846 #[test]
847 fn test_from_master_key() {
848 let password = b"maplesyrup";
849 let engine_id = decode_hex("000000000000000000000002").unwrap();
850
851 let master = MasterKey::from_password(AuthProtocol::Sha256, password);
852 let key_via_localize = master.localize(&engine_id);
853 let key_via_from_master = LocalizedKey::from_master_key(&master, &engine_id);
854
855 assert_eq!(key_via_localize.as_bytes(), key_via_from_master.as_bytes());
856 }
857
858 #[test]
859 fn test_master_keys_auth_only() {
860 let engine_id = decode_hex("000000000000000000000002").unwrap();
861 let master_keys = MasterKeys::new(AuthProtocol::Sha256, b"authpassword");
862
863 assert_eq!(master_keys.auth_protocol(), AuthProtocol::Sha256);
864 assert!(master_keys.priv_protocol().is_none());
865 assert!(master_keys.priv_master().is_none());
866
867 let (auth_key, priv_key) = master_keys.localize(&engine_id);
868 assert!(priv_key.is_none());
869 assert_eq!(auth_key.protocol(), AuthProtocol::Sha256);
870 }
871
872 #[test]
873 fn test_master_keys_with_privacy_same_password() {
874 use crate::v3::PrivProtocol;
875
876 let engine_id = decode_hex("000000000000000000000002").unwrap();
877 let master_keys = MasterKeys::new(AuthProtocol::Sha256, b"sharedpassword")
878 .with_privacy_same_password(PrivProtocol::Aes128);
879
880 assert_eq!(master_keys.auth_protocol(), AuthProtocol::Sha256);
881 assert_eq!(master_keys.priv_protocol(), Some(PrivProtocol::Aes128));
882
883 let (auth_key, priv_key) = master_keys.localize(&engine_id);
884 assert!(priv_key.is_some());
885 assert_eq!(auth_key.protocol(), AuthProtocol::Sha256);
886 }
887
888 #[test]
889 fn test_master_keys_with_privacy_different_password() {
890 use crate::v3::PrivProtocol;
891
892 let engine_id = decode_hex("000000000000000000000002").unwrap();
893 let master_keys = MasterKeys::new(AuthProtocol::Sha256, b"authpassword")
894 .with_privacy(PrivProtocol::Aes128, b"privpassword");
895
896 let (_auth_key, priv_key) = master_keys.localize(&engine_id);
897 assert!(priv_key.is_some());
898
899 let same_password_keys = MasterKeys::new(AuthProtocol::Sha256, b"authpassword")
901 .with_privacy_same_password(PrivProtocol::Aes128);
902 let (_, priv_key_same) = same_password_keys.localize(&engine_id);
903
904 assert_ne!(
907 priv_key.as_ref().unwrap().encryption_key(),
908 priv_key_same.as_ref().unwrap().encryption_key()
909 );
910 }
911
912 #[test]
916 fn test_reeder_extend_key_md5_kat() {
917 let password = b"maplesyrup";
924 let engine_id = decode_hex("000000000000000000000002").unwrap();
925
926 let k1 = LocalizedKey::from_password(AuthProtocol::Md5, password, &engine_id);
928 assert_eq!(
929 encode_hex(k1.as_bytes()),
930 "526f5eed9fcce26f8964c2930787d82b"
931 );
932
933 let extended = extend_key_reeder(AuthProtocol::Md5, k1.as_bytes(), &engine_id, 32);
935 assert_eq!(extended.len(), 32);
936 assert_eq!(
937 encode_hex(&extended),
938 "526f5eed9fcce26f8964c2930787d82b79eff44a90650ee0a3a40abfac5acc12"
939 );
940 }
941
942 #[test]
943 fn test_reeder_extend_key_sha1_kat() {
944 let password = b"maplesyrup";
951 let engine_id = decode_hex("000000000000000000000002").unwrap();
952
953 let k1 = LocalizedKey::from_password(AuthProtocol::Sha1, password, &engine_id);
955 assert_eq!(
956 encode_hex(k1.as_bytes()),
957 "6695febc9288e36282235fc7151f128497b38f3f"
958 );
959
960 let extended = extend_key_reeder(AuthProtocol::Sha1, k1.as_bytes(), &engine_id, 40);
962 assert_eq!(extended.len(), 40);
963 assert_eq!(
964 encode_hex(&extended),
965 "6695febc9288e36282235fc7151f128497b38f3f9b8b6d78936ba6e7d19dfd9cd2d5065547743fb5"
966 );
967 }
968
969 #[test]
970 fn test_reeder_extend_key_sha1_to_32_bytes() {
971 let password = b"maplesyrup";
974 let engine_id = decode_hex("000000000000000000000002").unwrap();
975
976 let k1 = LocalizedKey::from_password(AuthProtocol::Sha1, password, &engine_id);
977 let extended = extend_key_reeder(AuthProtocol::Sha1, k1.as_bytes(), &engine_id, 32);
978
979 assert_eq!(extended.len(), 32);
980 assert_eq!(
982 encode_hex(&extended),
983 "6695febc9288e36282235fc7151f128497b38f3f9b8b6d78936ba6e7d19dfd9c"
984 );
985 }
986
987 #[test]
988 fn test_reeder_extend_key_truncation() {
989 let long_key = vec![0xAAu8; 64];
991 let engine_id = decode_hex("000000000000000000000002").unwrap();
992
993 let extended = extend_key_reeder(AuthProtocol::Sha256, &long_key, &engine_id, 32);
994 assert_eq!(extended.len(), 32);
995 assert_eq!(extended, vec![0xAAu8; 32]);
996 }
997
998 #[test]
999 fn test_reeder_vs_blumenthal_differ() {
1000 let password = b"maplesyrup";
1002 let engine_id = decode_hex("000000000000000000000002").unwrap();
1003
1004 let k1 = LocalizedKey::from_password(AuthProtocol::Sha1, password, &engine_id);
1005
1006 let reeder = extend_key_reeder(AuthProtocol::Sha1, k1.as_bytes(), &engine_id, 32);
1007 let blumenthal = extend_key(AuthProtocol::Sha1, k1.as_bytes(), 32);
1008
1009 assert_eq!(reeder.len(), 32);
1010 assert_eq!(blumenthal.len(), 32);
1011
1012 assert_eq!(&reeder[..20], &blumenthal[..20]);
1014 assert_ne!(&reeder[20..], &blumenthal[20..]);
1016 }
1017}