use bytes::{Buf, Bytes}; use chrono::{DateTime, Utc}; #[cfg(feature = "std")] use crate::packet_content::PeerToPeerCipher; use crate::string_helper::{ MessageString, NameString, message_string_from_slice, name_string_from_slice, }; #[cfg(not(feature = "std"))] use crate::no_std_identity::Keystore; #[cfg(feature = "std")] use crate::std_identity::Keystore; #[derive(PartialEq, Debug, Clone)] pub struct Text { pub cipher: PeerToPeerCipher, pub cleartext: Option, } impl From for Text { fn from(value: Bytes) -> Self { Text { cipher: PeerToPeerCipher::from(value), cleartext: None, } } } impl core::fmt::Display for Text { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { f.write_fmt(format_args!( "({:2x?}) -> ({:2x?}) MAC: {:4x?} ", self.cipher.source, self.cipher.destination, self.cipher.mac ))?; if let Some(cleartext) = &self.cleartext { f.write_fmt(format_args!( "at: {}, {} attempts, from {}: {}", cleartext.timestamp, cleartext.attempts, cleartext.sender, cleartext.message.replace("\n", "\\n") )) } else { f.write_str("ENCRYPTED") } } } #[derive(PartialEq, Debug, Clone)] pub struct ClearText { pub timestamp: DateTime, pub message_type: MessageType, pub attempts: u8, pub sender_hash: u32, pub sender: NameString, pub message: MessageString, pub crypto_recipient: u32, // Hash of the sender's public key } impl From for ClearText { fn from(value: Bytes) -> Self { let mut bytes = value; let mut clear_text = ClearText { timestamp: DateTime::from_timestamp(0, 0).unwrap(), message_type: MessageType::Incomplete, attempts: 0, sender_hash: 0, sender: NameString::new(), message: MessageString::new(), crypto_recipient: 0, }; // Just check for the whole fixed-size part at once if bytes.len() < 5 { return clear_text; } if let Some(timestamp) = DateTime::from_timestamp(bytes.get_u32_le() as i64, 0) { clear_text.timestamp = timestamp; } let flags = bytes.get_u8(); clear_text.message_type = MessageType::from(flags); clear_text.attempts = flags & 0x03; if clear_text.message_type == MessageType::SignedPlain && bytes.len() > 4 { clear_text.sender_hash = bytes.get_u32(); } // This is hackash, but I'm struggling to figure out a // better way. We need to split the remaining data // at the ':'. This separates the sender from the message // But, we can't assume the sender will be there because // the remote connection is responsible for creating the // message, and we can't assume anything about it. let splits_iter = bytes.splitn(2, |c| *c == b':'); let mut splits: [Option<&[u8]>; 2] = [None; 2]; for (index, split) in splits_iter.enumerate() { splits[index] = Some(split); } match (splits[0], splits[1]) { (Some(message), None) => { clear_text.sender = name_string_from_slice(b"Unknown"); clear_text.message = message_string_from_slice(message); } (Some(sender), Some(message)) => { clear_text.sender = name_string_from_slice(sender); clear_text.message = message_string_from_slice(message); } _ => {} } clear_text } } #[derive(PartialEq, Debug, Clone)] pub struct GroupData { pub(crate) payload: Bytes, } impl From for GroupData { fn from(value: Bytes) -> Self { GroupData { payload: value } } } impl core::fmt::Display for GroupData { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { f.write_fmt(format_args!("Payload: {}", hex::encode(&self.payload))) } } #[derive(PartialEq, Clone, core::fmt::Debug)] pub struct GroupText { hash: u8, mac: u16, ciphertext: Bytes, cleartext: Option, incomplete: bool, } impl From for GroupText { fn from(value: Bytes) -> Self { let mut bytes = value; let mut group_text = GroupText { hash: 0x00, mac: 0x0000, ciphertext: Bytes::new(), cleartext: None, incomplete: true, }; // Just check for the whole fixed-size part at once if bytes.len() < 3 { return group_text; } group_text.hash = bytes.get_u8(); group_text.mac = bytes.get_u16(); group_text.ciphertext = bytes; group_text.incomplete = false; group_text } } impl GroupText { #[cfg(feature = "std")] pub fn try_decrypt(&mut self, keysore: &Keystore) -> bool { let decrypt_result = keysore.decrypt_and_id_group(self.hash, self.mac, &self.ciphertext); if let Some((cleartext, group)) = decrypt_result { let mut cleartext = ClearText::from(cleartext); cleartext.crypto_recipient = group; self.cleartext = Some(cleartext); true } else { false } } } impl core::fmt::Display for GroupText { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { if let Some(cleartext) = &self.cleartext { let message = cleartext.message.replace("\n", "\\n"); f.write_fmt(format_args!( "({:2x?}) Mac: {:4x?} Group {}: {}", self.hash, self.mac, cleartext.crypto_recipient, message )) } else { f.write_fmt(format_args!( "({:2x?}) Mac: {:4x?} ENCRYPTED", self.hash, self.mac )) } } } #[derive(PartialEq, Debug, Clone)] pub enum MessageType { Plain, CLI, SignedPlain, Invalid, Incomplete, } impl From for MessageType { fn from(value: u8) -> Self { match (value & 0xFC) >> 3 { 0x00 => MessageType::Plain, 0x01 => MessageType::CLI, 0x02 => MessageType::SignedPlain, _ => MessageType::Invalid, } } } // Tests for std operations #[cfg(feature = "std")] #[cfg(test)] mod tests { use std::{collections::HashMap, str::FromStr}; use crate::{ packet::*, packet_content::{PacketContent, PeerToPeerCipher}, std_identity::KeystoreInput, string_helper::{NameString, message_string_from_slice, name_string_from_slice}, text::{ClearText, GroupData, GroupText, MessageType, Text}, }; use bytes::Bytes; use chrono::DateTime; use hex::decode; use tinyvec::{ArrayVec, array_vec}; #[test] fn text_encrypted() { let sample = "0a001234e91c8eb2b815e0eccf6781a3ff1820d0fb130fcfc87b914244fae227d4ad4c752fb9"; let lhs_packet = Packet { route_type: RouteType::Direct, version: PayloadVersion::VersionOne, path: ArrayVec::new(), transport: [0, 0], raw_content: Bytes::copy_from_slice( &decode("1234e91c8eb2b815e0eccf6781a3ff1820d0fb130fcfc87b914244fae227d4ad4c752fb9") .unwrap(), ), content: PacketContent::Text(Text { cipher: PeerToPeerCipher { destination: 0x12, source: 0x34, mac: 0xe91c, ciphertext: Bytes::copy_from_slice( &decode("8eb2b815e0eccf6781a3ff1820d0fb130fcfc87b914244fae227d4ad4c752fb9") .unwrap(), ), cleartext: None, }, cleartext: None, }), incomplete: false, }; let rhs_packet = Packet::from_str(sample).unwrap(); assert_eq!(lhs_packet, rhs_packet); assert_eq!( format!("{}", lhs_packet), " Direct | v1 | | [] | | TEXT | (34) -> (12) MAC: e91c ENCRYPTED" ); } #[test] fn text_decrypted() { let sample = "0a001234e91c8eb2b815e0eccf6781a3ff1820d0fb130fcfc87b914244fae227d4ad4c752fb9"; let lhs_packet = Packet { route_type: RouteType::Direct, version: PayloadVersion::VersionOne, path: ArrayVec::new(), transport: [0, 0], raw_content: Bytes::copy_from_slice( &decode("1234e91c8eb2b815e0eccf6781a3ff1820d0fb130fcfc87b914244fae227d4ad4c752fb9") .unwrap(), ), content: PacketContent::Text(Text { cipher: PeerToPeerCipher { destination: 0x12, source: 0x34, mac: 0xe91c, ciphertext: Bytes::copy_from_slice( &decode("8eb2b815e0eccf6781a3ff1820d0fb130fcfc87b914244fae227d4ad4c752fb9") .unwrap(), ), cleartext: Some(Bytes::copy_from_slice( b"-\xb3\x07i\x0401|get radio\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0", )), }, cleartext: Some(ClearText { timestamp: DateTime::from_timestamp(1762112301, 0).unwrap(), message_type: MessageType::Plain, attempts: 0, sender_hash: 0, sender: name_string_from_slice(b"Unknown"), message: message_string_from_slice(b"01|get radio"), crypto_recipient: 0, }), }), incomplete: false, }; let file_contents = include_str!("../test_identities_file.toml"); let keystore_in: KeystoreInput = toml::from_str(file_contents).unwrap(); let keystore = keystore_in.compile(); let mut rhs_packet = Packet::from_str(sample).unwrap(); rhs_packet.try_decrypt(&keystore); assert_eq!(lhs_packet, rhs_packet); assert_eq!( format!("{}", rhs_packet), " Direct | v1 | | [] | | TEXT | (34) -> (12) MAC: e91c at: 2025-11-02 19:38:21 UTC, 0 attempts, from Unknown: 01|get radio" ); } #[test] fn group_text() { let sample = "15107B2CF1A31C03E8AACD7E6066B8A0ACB71176B87DDF8FE67B33E7A63036E015311EE39232F094FAAD13C4442947947DD098886BC677DE2B6F456149D0A1B05C72F0EECC20DC07EDF163D9D63EBC9BC29A7C79AF"; let lhs_packet = Packet { route_type: RouteType::Flood, version: PayloadVersion::VersionOne, path: array_vec!([u16; 64] => 0x7B, 0x2C, 0xF1, 0xA3, 0x1C, 0x03, 0xE8, 0xAA, 0xCD, 0x7E, 0x60, 0x66, 0xB8, 0xA0, 0xAC, 0xB7), transport: [0, 0], raw_content: Bytes::copy_from_slice(&decode("1176B87DDF8FE67B33E7A63036E015311EE39232F094FAAD13C4442947947DD098886BC677DE2B6F456149D0A1B05C72F0EECC20DC07EDF163D9D63EBC9BC29A7C79AF").unwrap()), content: PacketContent::GroupText(GroupText { hash: 0x11, mac: 0x76B8, ciphertext: Bytes::copy_from_slice(&decode("7DDF8FE67B33E7A63036E015311EE39232F094FAAD13C4442947947DD098886BC677DE2B6F456149D0A1B05C72F0EECC20DC07EDF163D9D63EBC9BC29A7C79AF").unwrap()), cleartext: None, // "Just need some more high repeaters".to_owned() incomplete: false }), incomplete: false }; let rhs_packet = Packet::from_str(sample).unwrap(); assert_eq!(lhs_packet, rhs_packet); assert_eq!( format!("{}", lhs_packet), " Flood | v1 | | [7b, 2c, ... ac, b7] | | GROUP TXT | (11) Mac: 76b8 ENCRYPTED" ); } #[test] fn group_text_decrypted() { let sample = "150011C3C1354D619BAE9590E4D177DB7EEAF982F5BDCF78005D75157D9535FA90178F785D"; let lhs_packet = Packet { route_type: RouteType::Flood, version: PayloadVersion::VersionOne, path: ArrayVec::new(), transport: [0, 0], raw_content: Bytes::copy_from_slice( &decode("11C3C1354D619BAE9590E4D177DB7EEAF982F5BDCF78005D75157D9535FA90178F785D") .unwrap(), ), content: PacketContent::GroupText(GroupText { hash: 0x11, mac: 0xC3C1, ciphertext: Bytes::copy_from_slice( &decode("354D619BAE9590E4D177DB7EEAF982F5BDCF78005D75157D9535FA90178F785D") .unwrap(), ), cleartext: Some(ClearText { sender: name_string_from_slice(b"\xF0\x9F\x8C\xB2 Tree"), message: message_string_from_slice(b"\xE2\x98\x81\xEF\xB8\x8F"), timestamp: DateTime::from_timestamp_secs(1758484279).unwrap(), message_type: MessageType::Plain, attempts: 0, sender_hash: 0, crypto_recipient: 0, }), incomplete: false, }), incomplete: false, }; let mut rhs_packet = Packet::from_str(sample).unwrap(); let keystore = KeystoreInput { identities: HashMap::new(), contacts: HashMap::new(), groups: HashMap::from([( "Public".to_owned(), "8b3387e9c5cdea6ac9e5edbaa115cd72".to_owned(), )]), } .compile(); _ = rhs_packet.try_decrypt(&keystore); assert_eq!(lhs_packet, rhs_packet); assert_eq!( format!("{}", lhs_packet), " Flood | v1 | | [] | | GROUP TXT | (11) Mac: c3c1 Group 0: ☁\u{fe0f}" ); } #[test] fn group_data() { let sample = "18079DBB163F3C88707DF4C430C8A06A587CF7E827641C6521A5DE85581C8800793AF4A5497196CB5F24B92A33A9C8AA3BAE4F8C94E8E464849BCBF6333509C3941C47B7ABF85ECFD2FF06DA1A39575D155941F152F63300D2C31B5FAFBDA79637"; let lhs_packet = Packet { route_type: RouteType::TransportFlood, version: PayloadVersion::VersionOne, path: array_vec!([u16; 64] => 0x3C, 0x88, 0x70, 0x7D, 0xF4, 0xC4, 0x30, 0xC8, 0xA0, 0x6A, 0x58, 0x7C, 0xF7, 0xE8, 0x27, 0x64, 0x1C, 0x65, 0x21, 0xA5, 0xDE, 0x85, 0x58, 0x1C, 0x88, 0x00, 0x79, 0x3A, 0xF4, 0xA5, 0x49, 0x71, 0x96, 0xCB, 0x5F, 0x24, 0xB9, 0x2A, 0x33, 0xA9, 0xC8, 0xAA, 0x3B, 0xAE, 0x4F, 0x8C, 0x94, 0xE8, 0xE4, 0x64, 0x84, 0x9B, 0xCB, 0xF6, 0x33, 0x35, 0x09, 0xC3, 0x94, 0x1C, 0x47, 0xB7, 0xAB), transport: [0x9d07, 0x16bb], raw_content: Bytes::copy_from_slice( &decode("F85ECFD2FF06DA1A39575D155941F152F63300D2C31B5FAFBDA79637").unwrap(), ), content: PacketContent::GroupData(GroupData { payload: Bytes::copy_from_slice( &decode("F85ECFD2FF06DA1A39575D155941F152F63300D2C31B5FAFBDA79637").unwrap(), ), }), incomplete: false, }; let rhs_packet = Packet::from_str(sample).unwrap(); assert_eq!(lhs_packet, rhs_packet); assert_eq!( format!("{}", lhs_packet), "T-Flood | v1 | 9d07, 16bb | [3c, 88, ... b7, ab] | | GRP. DATA | Payload: f85ecfd2ff06da1a39575d155941f152f63300d2c31b5fafbda79637" ); } }