use crate::{ MeshcoreStringError, crypto::PublicKey, string_helper::{PasswordString, password_string_from_slice}, }; use bytes::{Buf, Bytes}; use chrono::{DateTime, Utc}; #[derive(PartialEq, Clone, core::fmt::Debug)] pub struct AnonReq { pub dest: u8, pub public_key: PublicKey, pub mac: u16, pub ciphertext: Bytes, pub request: Option, incomplete: bool, } impl AnonReq { #[allow(dead_code)] fn password(&self) -> Result { if let Some(clear_request) = &self.request { Ok(clear_request.password.clone()) } else { Err(MeshcoreStringError::StringEncrypted) } } } impl From for AnonReq { fn from(value: Bytes) -> Self { let mut bytes = value; let mut anon_req = AnonReq { dest: 0x00, public_key: PublicKey::default(), mac: 0x0000, ciphertext: Bytes::new(), request: None, incomplete: false, }; if bytes.is_empty() { return anon_req; } anon_req.dest = bytes.get_u8(); if bytes.len() < 32 { return anon_req; } if let Ok(pub_key) = PublicKey::try_from(bytes.split_to(32)) { anon_req.public_key = pub_key; } if bytes.len() < 2 { return anon_req; } anon_req.mac = bytes.get_u16(); anon_req.ciphertext = bytes; anon_req.incomplete = false; anon_req } } impl core::fmt::Display for AnonReq { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { f.write_fmt(format_args!( "({:2x?}) -> ({:2x?}) MAC: {:4x?} ", self.public_key.hash_prefix() >> 24, self.dest, self.mac ))?; if let Some(cleartext) = &self.request { f.write_fmt(format_args!( "at: {} password: \"{}\"", cleartext.timestamp, cleartext.password )) } else { f.write_str("ENCRYPTED") } } } #[derive(PartialEq, Debug, Clone)] pub struct ClearAnonRequest { timestamp: DateTime, sync_timestamp: Option>, password: PasswordString, } impl From for ClearAnonRequest { fn from(value: Bytes) -> Self { let mut bytes = value; let mut anon_req = ClearAnonRequest { // Safety: this is ok to unwrap because it's value isn't user controlled // and never changes, and it exercised extensively in tests. timestamp: DateTime::from_timestamp(0, 0).unwrap(), sync_timestamp: None, password: PasswordString::new(), }; // Just check for the whole fixed-size part at once if bytes.len() < 4 { return anon_req; } if let Some(timestamp) = DateTime::from_timestamp(bytes.get_u32_le() as i64, 0) { anon_req.timestamp = timestamp; } // Strip-off any null characters after the password if let Some(pass) = bytes.split(|b| *b == 0).next() { anon_req.password = password_string_from_slice(pass); } anon_req } } // Tests for std operations #[cfg(test)] mod tests { use super::*; use crate::crypto::*; use crate::packet::*; use crate::packet_content::PacketContent; use crate::std_identity::KeystoreInput; use hex::decode; use std::collections::HashMap; use std::str::FromStr; use tinyvec::ArrayVec; #[test] fn anon_req() { let sample = "1D003412349BDC1F76A0C12149BB15F791DBE42FDE02C209B04A85C6F512990C8CEDEC4E7B8DBF1C3C928D64D87AA8293B9603EEE0"; let lhs_packet = Packet { route_type: RouteType::Flood, version: PayloadVersion::VersionOne, path: ArrayVec::new(), transport: [0, 0], raw_content: Bytes::copy_from_slice(&decode("3412349BDC1F76A0C12149BB15F791DBE42FDE02C209B04A85C6F512990C8CEDEC4E7B8DBF1C3C928D64D87AA8293B9603EEE0").unwrap()), content: PacketContent::AnonReq(AnonReq { dest: 0x34, public_key: PublicKey::try_from(Bytes::copy_from_slice(&decode("12349bdc1f76a0c12149bb15f791dbe42fde02c209b04a85c6f512990c8cedec").unwrap())).unwrap(), mac: 0x4e7b, ciphertext: Bytes::copy_from_slice(&decode("8DBF1C3C928D64D87AA8293B9603EEE0").unwrap()), incomplete: false, request: None }), incomplete: false }; let keystore = KeystoreInput { identities: HashMap::from([ ("Sample 1 ID".to_owned(), "4885CF25975EA09742EF76DA587D0957E74EE02AAA34A001458E207E63CF7E6C4940C8C42C335862C71CC2F139633057D1FEE5687B172B27E1E0302A1D480E08".to_owned()), ("Sample 2 ID".to_owned(), "38DAA98490B7284697C7ADA6175FD1F8DAD12032AD7ABAE625B7EAD8FEC6444CA281C3370B97155D9C8CECD89A929FDDE0FBF3A9D5C92A1B3C24D711934CD69D".to_owned()), ("Sample 3 ID".to_owned(), "08976A389FA16B077492BA7403A2178F4DF22B74A44DA0BE780CD0A51F5796437BB76B51320EE216F483F741FD73ED32F7DF5BBCBE811F405E579DD45AA8280A".to_owned()), ("Sample 4 ID".to_owned(), "f8285b33f3949770f4668be015c325d3ee6dd99c86e44d15ec2a48e157d355470161800938619f83944af343ae996ce433536b324f38e2ae242e8e1cf40649d5".to_owned()), ("Sample 5 ID".to_owned(), "60B176D579DD1F874C38B1AA6476F30BB77E7CCDD325BC718D9C926628B9037445AE4968856CF19453E8D72C19160EB34FDF7E7EC9EFE384A1ACA5573D6F28E5".to_owned()), ]), contacts: HashMap::from([ ("Sample 1 CT".to_owned(), "34569df1f9661916901669666fb8025eccb9ddb0499cddad4c164fec219c8b8f".to_owned()), ("Sample 2 CT".to_owned(), "12349bdc1f76a0c12149bb15f791dbe42fde02c209b04a85c6f512990c8cedec".to_owned()), ("Sample 3 CT".to_owned(), "9012aa245cbb5fc7a4512ce62350aa528dac49e0dd4a724ded59da83e27267aa".to_owned()), ("Sample 4 CT".to_owned(), "7890b8573a6ba4a05b173d6ccfdfa73ac8ec4a12bf3c745ace636e1d191e132a".to_owned()), ("Sample 5 CT".to_owned(), "12387717b1b00763ce666d710ad216348c7cdf5a5791c67c1e8120d40c550ac1".to_owned()), ]), groups: HashMap::new() }.compile(); let mut rhs_packet = Packet::from_str(sample).unwrap(); assert_eq!(lhs_packet, rhs_packet); println!("\"{}\"", rhs_packet); assert_eq!( format!("{}", rhs_packet), " Flood | v1 | | [] | | ANON REQ. | (12) -> (34) MAC: 4e7b ENCRYPTED" ); rhs_packet.try_decrypt(&keystore); let lhs_string = format!("{}", rhs_packet); let rhs_string = " Flood | v1 | | [] | | ANON REQ. | (12) -> (34) MAC: 4e7b at: 2025-11-13 17:21:13 UTC password: \"12345\""; assert_eq!(lhs_string, rhs_string); } }