diff options
| author | Will Dillon <william@housedillon.com> | 2025-11-13 06:16:41 +0000 |
|---|---|---|
| committer | Will Dillon <william@housedillon.com> | 2025-11-13 06:16:41 +0000 |
| commit | ae789a540db3c7adbe3a85f890f865c01d18d9d4 (patch) | |
| tree | 4695262fb12a75636b3899bb60ca101ac0f9a61c /src | |
| parent | Added more packets (diff) | |
| download | meshcore-rs-ae789a540db3c7adbe3a85f890f865c01d18d9d4.tar.gz meshcore-rs-ae789a540db3c7adbe3a85f890f865c01d18d9d4.zip | |
Refactored packet_content and added way more tests
Diffstat (limited to 'src')
| -rw-r--r-- | src/ack.rs | 59 | ||||
| -rw-r--r-- | src/advert.rs | 214 | ||||
| -rw-r--r-- | src/anon_req.rs | 128 | ||||
| -rw-r--r-- | src/bin/packet_analyzer.rs | 2 | ||||
| -rw-r--r-- | src/crypto.rs | 2 | ||||
| -rw-r--r-- | src/identity.rs | 46 | ||||
| -rw-r--r-- | src/lib.rs | 9 | ||||
| -rw-r--r-- | src/multipart.rs | 50 | ||||
| -rw-r--r-- | src/packet.rs | 56 | ||||
| -rw-r--r-- | src/packet_content.rs | 996 | ||||
| -rw-r--r-- | src/path.rs | 61 | ||||
| -rw-r--r-- | src/request.rs | 202 | ||||
| -rw-r--r-- | src/response.rs | 69 | ||||
| -rw-r--r-- | src/text.rs | 348 | ||||
| -rw-r--r-- | src/trace.rs | 87 |
15 files changed, 1320 insertions, 1009 deletions
diff --git a/src/ack.rs b/src/ack.rs new file mode 100644 index 0000000..a5c4b63 --- /dev/null +++ b/src/ack.rs @@ -0,0 +1,59 @@ +use std::fmt::Display; + +use bytes::{Buf, Bytes}; +use structdiff::{Difference, StructDiff}; + +#[derive(PartialEq, Debug, Clone, Difference)] +#[difference(expose)] +pub struct Ack { + checksum: u32 +} + +impl From<Bytes> for Ack { + fn from(value: Bytes) -> Self { + let mut bytes = value; + Ack { checksum: bytes.get_u32() } + } +} + +impl Display for Ack { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("Checksum: {:4x?}", self.checksum)) + } +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + use hex::decode; + use bytes::Bytes; + use crate::{ack::Ack, packet::*, packet_content::PacketContent}; + + #[test] + fn ack() { + let sample = "0D0E78B5561B03CD70DA326066B8A0CF24F3214D"; + + let lhs_packet = Packet { + route_type: RouteType::Flood, + version: PayloadVersion::VersionOne, + path: vec![0x78, 0xB5, 0x56, 0x1B, 0x03, 0xCD, 0x70, 0xDA, 0x32, 0x60, 0x66, 0xB8, 0xA0, 0xCF], + transport: [0, 0], + raw_content: Bytes::copy_from_slice(&decode("24F3214D").unwrap()), + content: PacketContent::Ack(Ack { + checksum: 0x24F3214D + }), + incomplete: false + }; + + let rhs_packet = Packet::from_str(sample).unwrap(); + assert!(lhs_packet == rhs_packet, "{}", print_compare(lhs_packet, rhs_packet)); + } + + #[test] + fn display() { + let ack = Ack { checksum: 0x24F3214D }; + println!("{}", ack); + assert!(format!("{}", ack) == "Checksum: 24f3214d"); + } + +}
\ No newline at end of file diff --git a/src/advert.rs b/src/advert.rs new file mode 100644 index 0000000..dbbe84d --- /dev/null +++ b/src/advert.rs @@ -0,0 +1,214 @@ +use structdiff::{Difference, StructDiff}; +use crate::{crypto::PublicKey, packet_content::NodeType}; +use bytes::{Buf, Bytes}; +use std::fmt::{Debug, Display}; +use chrono::{DateTime, Local, Utc}; + +#[derive(PartialEq, Debug, Clone, Difference)] +#[difference(expose)] +pub struct Advert { + pub public_key: PublicKey, + pub timestamp: DateTime<Utc>, + pub signature: [u8; 64], + + pub node_type: NodeType, + pub latitude: Option<f32>, + pub longitude: Option<f32>, + pub feature1: Option<u16>, + pub feature2: Option<u16>, + pub name: String +} + +impl From<Bytes> for Advert { + fn from(value: Bytes) -> Self { + let mut bytes = value; + + let mut advert = Advert { + public_key: PublicKey::default(), + timestamp: Local::now().into(), + signature: [0_u8; 64], + + node_type: NodeType::Invalid, + latitude: None, + longitude: None, + feature1: None, + feature2: None, + name: "".to_string(), + }; + + if bytes.len() < 32 { return advert } + if let Ok(key) = PublicKey::try_from(bytes.split_to(32)) { + advert.public_key = key; + } else { + return advert + } + + if bytes.len() < 4 { return advert } + if let Some(time) = DateTime::from_timestamp(bytes.get_u32_le() as i64, 0) { + advert.timestamp = time; + } + + if bytes.len() < 64 { return advert } + _ = bytes.try_copy_to_slice(&mut advert.signature); + + if bytes.is_empty() { return advert } + let flags = bytes.get_u8(); + advert.node_type = NodeType::from(flags); + + if (flags & 0x10) != 0 { + // The location is 8 bytes (4 each for lat and lon) + if bytes.len() < 8 { return advert } + + advert.latitude = Some(bytes.get_i32_le() as f32 / 1_000_000.0); + advert.longitude = Some(bytes.get_i32_le() as f32 / 1_000_000.0); + } + + if (flags & 0x20) != 0 { + // Feature 1 is 2 bytes when it's included + if bytes.len() < 2 { return advert } + advert.feature1 = Some(bytes.get_u16_le()); + } + + if (flags & 0x40) != 0 { + // Feature 2 is the same + if bytes.len() < 2 { return advert } + advert.feature2 = Some(bytes.get_u16_le()); + } + + // Lastly the name... The flag is a little + // irrelevant because the only different is + // whether the name is omitted or just empty. + // I'm not going to make the distinction + + // The string is assumed to be utf8, but if that + // fails to parse correctly, try to make it lossy + // which will insert U+FFFD REPLACEMENT CHARACTER (�) + if let Ok(string) = String::from_utf8(bytes.to_vec()) { + advert.name = string + } else { + advert.name = String::from_utf8_lossy(&bytes).to_string(); + } + + advert + } +} + +impl Display for Advert { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.node_type.fmt(f)?; + f.write_str(" \"")?; + f.write_str(&self.name)?; + f.write_fmt(format_args!("\" ({:2x?}) at: ", self.public_key.hash_prefix()))?; + std::fmt::Display::fmt(&self.timestamp, f)?; + + match (self.latitude, self.longitude) { + (Some(lat), Some(lon)) => { + f.write_fmt(format_args!(" location: {}, {}", lat, lon))?; + }, + _ => {}, + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use chrono::DateTime; + use hex::decode; + + use crate::packet::*; + use crate::crypto::*; + use crate::packet_content::PacketContent; + + use super::*; + + + #[test] + fn advert_with_lat_lon() { + // Real sample packet from the air + let sample = "110c015f7e9b60661da0f2671512460728508c17ef336412a223144d3a623215162682045c44fef7241af0161923d3af0769d2d976b687a506dd5325ef526bf3eb52ae687277fcbde9969a5b0087e0eb0f7c1760a50c6a88bec13cc30a2a9b681d713166515e3bbc2bc27f20c0e4d7b67e08910c29dc02c468b8f8484f574c"; + + let mut signature_slice = [0 as u8; 64]; + hex::decode_to_slice("d2d976b687a506dd5325ef526bf3eb52ae687277fcbde9969a5b0087e0eb0f7c1760a50c6a88bec13cc30a2a9b681d713166515e3bbc2bc27f20c0e4d7b67e08", &mut signature_slice).unwrap(); + + let lhs_packet = Packet { + route_type: RouteType::Flood, + version: PayloadVersion::VersionOne, + path: vec![0x01, 0x5f, 0x7e, 0x9b, 0x60, 0x66, 0x1d, 0xa0, 0xf2, 0x67, 0x15, 0x12], + transport: [0x00, 0x00], + raw_content: Bytes::copy_from_slice(&decode("460728508c17ef336412a223144d3a623215162682045c44fef7241af0161923d3af0769d2d976b687a506dd5325ef526bf3eb52ae687277fcbde9969a5b0087e0eb0f7c1760a50c6a88bec13cc30a2a9b681d713166515e3bbc2bc27f20c0e4d7b67e08910c29dc02c468b8f8484f574c").unwrap()), + content: PacketContent::Advert(Advert { + public_key: PublicKey::from_str("460728508c17ef336412a223144d3a623215162682045c44fef7241af0161923").unwrap(), + timestamp: DateTime::from_timestamp(1762111443, 0).unwrap(), + signature: signature_slice, + node_type: NodeType::Chat, + latitude: Some(47.98286), + longitude: Some(-122.132286), + feature1: None, + feature2: None, + name: "HOWL".to_owned(), + }), + incomplete: false, + }; + let rhs_packet = Packet::from_str(sample).unwrap(); + + assert!(lhs_packet == rhs_packet, "{}", print_compare(lhs_packet, rhs_packet)); + } + + #[test] + fn advert() { + // Another on-air sample + let sample = "12007890b8573a6ba4a05b173d6ccfdfa73ac8ec4a12bf3c745ace636e1d191e132a4eb407695601f50907999735f3699a3c73ace2d8acc385ed209606abef6b914a346fbb88648c540ae794586b4d54eb08b473c8571a00c6b8d3dbcc77699afa169811fa098168707578373335"; + + let mut signature_slice = [0 as u8; 64]; + hex::decode_to_slice("5601f50907999735f3699a3c73ace2d8acc385ed209606abef6b914a346fbb88648c540ae794586b4d54eb08b473c8571a00c6b8d3dbcc77699afa169811fa09", &mut signature_slice).unwrap(); + + let lhs_packet = Packet { + route_type: RouteType::Direct, + version: PayloadVersion::VersionOne, + path: vec![], + transport: [0x00, 0x00], + raw_content: Bytes::copy_from_slice(&decode("7890b8573a6ba4a05b173d6ccfdfa73ac8ec4a12bf3c745ace636e1d191e132a4eb407695601f50907999735f3699a3c73ace2d8acc385ed209606abef6b914a346fbb88648c540ae794586b4d54eb08b473c8571a00c6b8d3dbcc77699afa169811fa098168707578373335").unwrap()), + content: PacketContent::Advert(Advert { + public_key: PublicKey::from_str("7890b8573a6ba4a05b173d6ccfdfa73ac8ec4a12bf3c745ace636e1d191e132a").unwrap(), + timestamp: DateTime::from_timestamp(1762112590, 0).unwrap(), + signature: signature_slice, + node_type: NodeType::Chat, + latitude: None, + longitude: None, + feature1: None, + feature2: None, + name: "hpux735".to_owned(), + }), + incomplete: false, + }; + + let rhs_packet = Packet::from_str(sample).unwrap(); + assert!(lhs_packet == rhs_packet, "{}", print_compare(lhs_packet, rhs_packet)); + } + + + #[test] + fn display() { + let mut signature_slice = [0 as u8; 64]; + hex::decode_to_slice("d2d976b687a506dd5325ef526bf3eb52ae687277fcbde9969a5b0087e0eb0f7c1760a50c6a88bec13cc30a2a9b681d713166515e3bbc2bc27f20c0e4d7b67e08", &mut signature_slice).unwrap(); + + let advert = Advert { + public_key: PublicKey::from_str("460728508c17ef336412a223144d3a623215162682045c44fef7241af0161923").unwrap(), + timestamp: DateTime::from_timestamp(1762111443, 0).unwrap(), + signature: signature_slice, + node_type: NodeType::Chat, + latitude: Some(47.98286), + longitude: Some(-122.132286), + feature1: None, + feature2: None, + name: "HOWL".to_owned(), + }; + println!("{}", advert); + assert!(format!("{}", advert) == "Chat \"HOWL\" (46) at: 2025-11-02 19:24:03 UTC location: 47.98286, -122.132286"); + } + +}
\ No newline at end of file diff --git a/src/anon_req.rs b/src/anon_req.rs new file mode 100644 index 0000000..2f449a7 --- /dev/null +++ b/src/anon_req.rs @@ -0,0 +1,128 @@ +use std::fmt::{Debug, Display}; +use chrono::{DateTime, Utc}; +use bytes::{Buf, Bytes}; +use structdiff::{Difference, StructDiff}; +use crate::crypto::PublicKey; + +#[derive(PartialEq, Debug, Clone, Difference)] +#[difference(expose)] +pub struct AnonReq { + pub dest: u8, + pub public_key: PublicKey, + pub mac: u16, + pub ciphertext: Bytes, + pub request: Option<ClearAnonRequest>, + incomplete: bool +} + +impl From<Bytes> 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 Display for AnonReq { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("({:2x?}) -> ({:2x?}) MAC: {:4x?} ", self.public_key.hash_prefix(), 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, Difference)] +#[difference(expose)] +pub struct ClearAnonRequest { + timestamp: DateTime<Utc>, + sync_timestamp: Option<DateTime<Utc>>, + password: String +} + +impl From<Bytes> for ClearAnonRequest { + fn from(value: Bytes) -> Self { + let mut bytes = value; + + let mut anon_req = ClearAnonRequest { + timestamp: DateTime::from_timestamp(0, 0).unwrap(), + sync_timestamp: None, + password: "".to_string(), + }; + + // 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() as i64, 0) { + anon_req.timestamp = timestamp; + } + + if bytes.len() < 4 { return anon_req; } + if let Some(timestamp) = DateTime::from_timestamp(bytes.get_u32() as i64, 0) { + anon_req.sync_timestamp = Some(timestamp); + } + + anon_req.password = String::from_utf8_lossy(&bytes).to_string(); + anon_req + } +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + use hex::decode; + use crate::packet::*; + use crate::crypto::*; + use crate::packet_content::PacketContent; + use super::*; + + #[test] + fn anon_req() { + let sample = "1d001234569df1f9661916901669666fb8025eccb9ddb0499cddad4c164fec219c8b8fd2db15a7138098557cc291928b4358fa7522ddd41d35c99fb78f0def2b3e673d73d2"; + + let lhs_packet = Packet { + route_type: RouteType::Flood, + version: PayloadVersion::VersionOne, + path: vec![], + transport: [0, 0], + raw_content: Bytes::copy_from_slice(&decode("1234569df1f9661916901669666fb8025eccb9ddb0499cddad4c164fec219c8b8fd2db15a7138098557cc291928b4358fa7522ddd41d35c99fb78f0def2b3e673d73d2").unwrap()), + content: PacketContent::AnonReq(AnonReq { + dest: 0x12, + public_key: PublicKey::try_from(Bytes::copy_from_slice(&decode("34569df1f9661916901669666fb8025eccb9ddb0499cddad4c164fec219c8b8f").unwrap())).unwrap(), + mac: 0xd2db, + ciphertext: Bytes::copy_from_slice(&decode("15a7138098557cc291928b4358fa7522ddd41d35c99fb78f0def2b3e673d73d2").unwrap()), + incomplete: false, + request: None + }), + incomplete: false + }; + + let rhs_packet = Packet::from_str(sample).unwrap(); + assert!(lhs_packet == rhs_packet, "{}", print_compare(lhs_packet, rhs_packet)); + } + +}
\ No newline at end of file diff --git a/src/bin/packet_analyzer.rs b/src/bin/packet_analyzer.rs index 96fcfd2..98f4862 100644 --- a/src/bin/packet_analyzer.rs +++ b/src/bin/packet_analyzer.rs @@ -2,7 +2,7 @@ use std::{borrow::Cow, path::PathBuf}; use hex::encode; use log::{error, trace}; use tokio::fs::File; -use tokio_util::bytes::Bytes; +use bytes::Bytes; use clap::Parser; use color_eyre::eyre::Result; diff --git a/src/crypto.rs b/src/crypto.rs index 3175501..f814f78 100644 --- a/src/crypto.rs +++ b/src/crypto.rs @@ -1,6 +1,6 @@ use std::{fmt::{Debug, Display}, str::FromStr}; use ed25519_dalek::{VerifyingKey, hazmat::ExpandedSecretKey}; -use tokio_util::bytes::{Buf, BufMut, Bytes, BytesMut}; +use bytes::{Buf, BufMut, Bytes, BytesMut}; use hex::{decode, decode_to_slice, encode}; // This seems to be an absolute nightmare. GenericArray sucks diff --git a/src/identity.rs b/src/identity.rs index 6e0e252..ccbc3de 100644 --- a/src/identity.rs +++ b/src/identity.rs @@ -1,10 +1,12 @@ -use std::{collections::HashMap, rc::Rc, str::FromStr}; -use hex::decode; +use std::{collections::HashMap, rc::Rc}; +use bytes::Bytes; +use crate::crypto::{PrivateKey, PublicKey, SharedSecret}; + +#[cfg(feature = "std")] use serde::{Deserialize, de}; -use tokio_util::bytes::Bytes; -use crate::crypto::{MeshcoreCryptoError, PrivateKey, PublicKey, SharedSecret}; -#[derive(PartialEq, Debug, Clone, Deserialize)] +#[derive(PartialEq, Debug, Clone)] +#[cfg_attr(feature = "std", derive(Deserialize))] /// The Identity structure contains the information to decrypt /// incoming messages and sign and encrypt outgoing messages. /// @@ -18,13 +20,13 @@ pub struct Identity { /// no ways in this library to generate a new private key /// from scratch (while ensuing it's sound) so it must be /// provided by the user. - #[serde(deserialize_with = "deserialize_private_key")] + #[cfg_attr(feature = "std", serde(deserialize_with = "deserialize_private_key"))] pub private_key: PrivateKey, /// The derived public key given this identity's /// private key. The hash prefix of this (typically 1-byte) /// is used as a first-pass to identity the recipient. - #[serde(skip)] + #[cfg_attr(feature = "std", serde(skip))] pub public_key: PublicKey, /// The secrets has is a collection of shared secrets @@ -32,11 +34,12 @@ pub struct Identity { /// connection. The whole remote public key must be /// used because there are many many hash collisions /// when using just the 1-byte hash prefix. - #[serde(skip)] + #[cfg_attr(feature = "std", serde(skip))] pub secrets: Vec<(Rc<String>, SharedSecret)> } -#[derive(PartialEq, Debug, Clone, Deserialize)] +#[derive(PartialEq, Debug, Clone)] +#[cfg_attr(feature = "std", derive(Deserialize))] /// The Contact structure contains the information needed /// to decrypt messages from a remote user intended for /// either an identity or a channel. @@ -51,11 +54,12 @@ pub struct Contact { pub name: Rc<String>, /// The provided public key of the remote contact - #[serde(deserialize_with = "deserialize_public_key")] + #[cfg_attr(feature = "std", serde(deserialize_with = "deserialize_public_key"))] pub public_key: PublicKey } -#[derive(PartialEq, Debug, Clone, Deserialize)] +#[derive(PartialEq, Debug, Clone)] +#[cfg_attr(feature = "std", derive(Deserialize))] /// A Group in MeshCore is a kind of contact, except that its /// secret is fixed and shared directly. It's not derived via /// a public and private key. Otherwise, it behaves more like a @@ -66,7 +70,7 @@ pub struct Group { pub name: Rc<String>, /// The group's shared secret - #[serde(deserialize_with = "deserialize_secret")] + #[cfg_attr(feature = "std", serde(deserialize_with = "deserialize_secret"))] pub secret: SharedSecret } @@ -109,7 +113,8 @@ impl Keystore { } } -#[derive(PartialEq, Debug, Clone, Deserialize)] +#[derive(PartialEq, Debug, Clone)] +#[cfg_attr(feature = "std", derive(Deserialize))] pub struct KeystoreInput { pub identities: Vec<Identity>, pub contacts: Vec<Contact>, @@ -157,8 +162,13 @@ impl KeystoreInput { } } +#[cfg(feature = "std")] fn deserialize_private_key<'de, D>(deserializer: D) -> Result<PrivateKey, D::Error> where D: de::Deserializer<'de> { + + use std::str::FromStr; + use crate::crypto::{PrivateKey}; + let s: String = de::Deserialize::deserialize(deserializer)?; match PrivateKey::from_str(&s) { Ok(key) => Ok(key), @@ -168,8 +178,13 @@ fn deserialize_private_key<'de, D>(deserializer: D) -> Result<PrivateKey, D::Err } } +#[cfg(feature = "std")] fn deserialize_public_key<'de, D>(deserializer: D) -> Result<PublicKey, D::Error> where D: de::Deserializer<'de> { + + use hex::decode; + use crate::crypto::{MeshcoreCryptoError, PublicKey}; + let s: String = de::Deserialize::deserialize(deserializer)?; match decode(s) { @@ -197,8 +212,13 @@ fn deserialize_public_key<'de, D>(deserializer: D) -> Result<PublicKey, D::Error } } +#[cfg(feature = "std")] fn deserialize_secret<'de, D>(deserializer: D) -> Result<SharedSecret, D::Error> where D: de::Deserializer<'de> { + + use std::str::FromStr; + use crate::crypto::SharedSecret; + let s: String = de::Deserialize::deserialize(deserializer)?; match SharedSecret::from_str(&s) { @@ -3,3 +3,12 @@ pub mod identity; pub mod packet; pub mod packet_content; +pub mod request; +pub mod response; +pub mod text; +pub mod ack; +pub mod advert; +pub mod anon_req; +pub mod path; +pub mod trace; +pub mod multipart;
\ No newline at end of file diff --git a/src/multipart.rs b/src/multipart.rs new file mode 100644 index 0000000..5b7147f --- /dev/null +++ b/src/multipart.rs @@ -0,0 +1,50 @@ +use std::fmt::{Debug, Display}; +use bytes::Bytes; +use structdiff::{Difference, StructDiff}; + +#[derive(PartialEq, Debug, Clone, Difference)] +#[difference(expose)] +pub struct MultiPart { + payload: Bytes +} + +impl From<Bytes> for MultiPart { + fn from(value: Bytes) -> Self { + MultiPart { payload: value } + } +} + +impl Display for MultiPart { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("Multipart isn't defined.")) + } +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + use hex::decode; + use crate::{packet::*, packet_content::PacketContent}; + + use super::*; + + #[test] + fn multipart() { + let sample = "2A0013E8C59624"; + + let lhs_packet = Packet { + route_type: RouteType::Direct, + version: PayloadVersion::VersionOne, + path: vec![], + transport: [0, 0], + raw_content: Bytes::copy_from_slice(&decode("13E8C59624").unwrap()), + content: PacketContent::Multipart(MultiPart { + payload: Bytes::copy_from_slice(&decode("13E8C59624").unwrap()) + }), + incomplete: false + }; + + let rhs_packet = Packet::from_str(sample).unwrap(); + assert!(lhs_packet == rhs_packet, "{}", print_compare(lhs_packet, rhs_packet)); + } +}
\ No newline at end of file diff --git a/src/packet.rs b/src/packet.rs index 73de4b5..d28fe90 100644 --- a/src/packet.rs +++ b/src/packet.rs @@ -1,9 +1,9 @@ use std::{fmt::{Debug, Display}, str::FromStr}; use hex::decode; -use tokio_util::bytes::{Buf, Bytes}; +use bytes::{Buf, Bytes}; use structdiff::{Difference, StructDiff}; -use crate::{identity::Keystore, packet_content::{ClearRequest, ClearText, PacketContent}}; +use crate::{identity::Keystore, packet_content::PacketContent, request::ClearRequest, text::ClearText}; #[derive(PartialEq, Debug, Clone, Difference)] #[difference(expose)] @@ -148,10 +148,10 @@ impl Display for Packet { let len = self.path.len(); match self.path.len() { 0 => f.write_fmt(format_args!(" [] | ")), - 1 => f.write_fmt(format_args!(" [{:2x?}] | ", self.path[0])), - 2 => f.write_fmt(format_args!(" [{:2x?}, {:2x?}] | ", self.path[0], self.path[1])), - 3 => f.write_fmt(format_args!(" [{:2x?}, {:2x?}, {:2x?}] | ", self.path[0], self.path[1], self.path[2])), - _ => f.write_fmt(format_args!(" [{:2x?}, {:2x?}, ... {:2x?}, {:2x?}] | ", self.path[0], self.path[1], self.path[len - 2], self.path[len -1])), + 1 => f.write_fmt(format_args!(" [{:02x?}] | ", self.path[0])), + 2 => f.write_fmt(format_args!(" [{:02x?}, {:02x?}] | ", self.path[0], self.path[1])), + 3 => f.write_fmt(format_args!(" [{:02x?}, {:02x?}, {:02x?}] | ", self.path[0], self.path[1], self.path[2])), + _ => f.write_fmt(format_args!(" [{:02x?}, {:02x?}, ... {:02x?}, {:02x?}] | ", self.path[0], self.path[1], self.path[len - 2], self.path[len -1])), }?; if self.incomplete { @@ -318,6 +318,12 @@ mod tests { assert!(RouteType::Direct == RouteType::from(0xFE)); assert!(RouteType::TransportDirect == RouteType::from(0x03)); assert!(RouteType::TransportDirect == RouteType::from(0xFF)); + + assert!(format!("{}", RouteType::TransportFlood) == "T-Flood "); + assert!(format!("{}", RouteType::Flood) == " Flood "); + assert!(format!("{}", RouteType::Direct) == " Direct"); + assert!(format!("{}", RouteType::TransportDirect) == "T-Direct"); + assert!(format!("{}", RouteType::Invalid) == "INVALID "); } #[test] @@ -330,6 +336,12 @@ mod tests { assert!(PayloadVersion::VersionThree == PayloadVersion::from(0xBF)); assert!(PayloadVersion::VersionFour == PayloadVersion::from(0xC0)); assert!(PayloadVersion::VersionFour == PayloadVersion::from(0xFF)); + + assert!(format!("{}", PayloadVersion::VersionOne) == "v1"); + assert!(format!("{}", PayloadVersion::VersionTwo) == "v2"); + assert!(format!("{}", PayloadVersion::VersionThree) == "v3"); + assert!(format!("{}", PayloadVersion::VersionFour) == "v4"); + assert!(format!("{}", PayloadVersion::Invalid) == "xx"); } #[test] @@ -375,7 +387,39 @@ mod tests { content: PacketContent::Raw(Raw { bytes: Bytes::copy_from_slice(&[0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19])}), incomplete: false, }; + let rhs_packet = Packet::from_str("3F0201040305060708090A10111213141516171819").unwrap(); + + let compare_string = "T-Direct | v1 | 102, 304 | [06, 07, ... 09, 0a] | | RAW | Raw { bytes: b\"\\x10\\x11\\x12\\x13\\x14\\x15\\x16\\x17\\x18\\x19\" }"; + assert!(format!("{}", rhs_packet) == compare_string); + assert!(lhs_packet == rhs_packet, "{}", print_compare(lhs_packet, rhs_packet)); } + + #[test] + fn packet_display() { + let mut packet = Packet { + route_type: RouteType::Direct, + version: PayloadVersion::VersionOne, + path: vec![], + transport: [0, 1], + raw_content: Bytes::new(), + content: PacketContent::Invalid, + incomplete: false + }; + + assert!(format!("{}", packet) == " Direct | v1 | | [] | | INVALID | INVALID"); + + packet.path = vec![0x01]; + assert!(format!("{}", packet) == " Direct | v1 | | [01] | | INVALID | INVALID"); + + packet.path = vec![0x01, 0x02]; + assert!(format!("{}", packet) == " Direct | v1 | | [01, 02] | | INVALID | INVALID"); + + packet.path = vec![0x01, 0x02, 0xff]; + assert!(format!("{}", packet) == " Direct | v1 | | [01, 02, ff] | | INVALID | INVALID"); + + packet.incomplete = true; + assert!(format!("{}", packet) == " Direct | v1 | | [01, 02, ff] | x | INVALID | INVALID"); + } }
\ No newline at end of file diff --git a/src/packet_content.rs b/src/packet_content.rs index cdad0ff..d13c663 100644 --- a/src/packet_content.rs +++ b/src/packet_content.rs @@ -1,11 +1,7 @@ -use std::{fmt::{Debug, Display}, rc::Rc}; - -use aes::cipher; -use chrono::{DateTime, Local, Utc}; -use hex::encode; -use tokio_util::bytes::{Buf, Bytes}; +use std::fmt::{Debug, Display}; +use bytes::{Buf, Bytes}; use structdiff::{Difference, StructDiff}; -use crate::{crypto::PublicKey, identity::Keystore}; +use crate::{ack::Ack, advert::Advert, anon_req::AnonReq, identity::Keystore, multipart::MultiPart, path::Path, request::Request, response::Response, text::{GroupData, GroupText, Text}, trace::Trace}; #[derive(PartialEq, Debug, Clone, Difference)] #[difference(expose)] @@ -113,11 +109,11 @@ impl From<u8> for NodeType { #[derive(PartialEq, Debug, Clone, Difference)] #[difference(expose)] pub struct PeerToPeerCipher { - destination: u8, - source: u8, - mac: u16, - pub(crate) ciphertext: Bytes, - pub(crate) cleartext: Option<Bytes> + pub destination: u8, + pub source: u8, + pub mac: u16, + pub ciphertext: Bytes, + pub cleartext: Option<Bytes> } impl From<Bytes> for PeerToPeerCipher { @@ -163,621 +159,6 @@ impl PeerToPeerCipher { } #[derive(PartialEq, Debug, Clone)] -pub struct Request { - pub cipher: PeerToPeerCipher, - pub cleartext: Option<ClearRequest>, -} - -#[derive(PartialEq, Debug, Clone)] -pub struct ClearRequest { - pub timestamp: DateTime<Utc>, - pub request_type: RequestType, - pub request_data: Bytes -} - -impl From<Bytes> for ClearRequest { - fn from(value: Bytes) -> Self { - let mut bytes = value; - - let mut clear_request = ClearRequest { - timestamp: DateTime::from_timestamp(0, 0).unwrap(), - request_type: RequestType::Invalid, - request_data: Bytes::new() - }; - - // Just check for the whole fixed-size part at once - if bytes.len() < 5 { return clear_request } - if let Some(timestamp) = DateTime::from_timestamp(bytes.get_u32_le() as i64, 0) { - clear_request.timestamp = timestamp; - } - - clear_request.request_type = RequestType::from(bytes.get_u8()); - - clear_request.request_data = bytes; - - clear_request - } -} - -#[derive(PartialEq, Debug, Clone)] -pub enum RequestType { - Stats, - Keepalive, - Telemetry, - MinMaxAvg, - ACL, - Invalid -} - -impl From<u8> for RequestType { - fn from(value: u8) -> Self { - todo!() - } -} - -impl From<Bytes> for Request { - fn from(value: Bytes) -> Self { - Request { - cipher: PeerToPeerCipher::from(value), - cleartext: None - } - } -} - -impl Display for Request { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::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.cipher.cleartext { - f.write_str("TODO") - // f.write_fmt(format_args!("{}", cleartext)) - } else { - f.write_str("ENCRYPTED") - } - } -} - -#[derive(PartialEq, Debug, Clone, Difference)] -#[difference(expose)] -pub struct Response { - pub(crate) cipher: PeerToPeerCipher, -} - -impl From<Bytes> for Response { - fn from(value: Bytes) -> Self { - Response { - cipher: PeerToPeerCipher::from(value) - } - } -} - -impl Display for Response { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::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.cipher.cleartext { - f.write_fmt(format_args!("")) - } else { - f.write_str("ENCRYPTED") - } - } -} - -#[derive(PartialEq, Debug, Clone)] -pub struct Text { - pub cipher: PeerToPeerCipher, - pub cleartext: Option<ClearText>, -} - -impl From<Bytes> for Text { - fn from(value: Bytes) -> Self { - Text { - cipher: PeerToPeerCipher::from(value), - cleartext: None - } - } -} - -impl Display for Text { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::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 { - if let Some(sender) = &cleartext.sender { - f.write_fmt(format_args!("at: {}, {} attempts, from {} to {}: {}", - cleartext.timestamp, - cleartext.attempts, - sender, - cleartext.crypto_recipient, - cleartext.message.replace("\n", "\\n")) - ) - } else { - f.write_fmt(format_args!("at: {}, {} attempts, from {} to {}: {}", - cleartext.timestamp, - cleartext.attempts, - "Unknown", - cleartext.crypto_recipient, - cleartext.message.replace("\n", "\\n")) - ) - } - - } else { - f.write_str("ENCRYPTED") - } - } -} - -#[derive(PartialEq, Debug, Clone)] -pub struct Ack { - checksum: u32 -} - -impl From<Bytes> for Ack { - fn from(value: Bytes) -> Self { - let mut bytes = value; - Ack { checksum: bytes.get_u32() } - } -} - -impl Display for Ack { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_fmt(format_args!("Checksum: {:4x?}", self.checksum)) - } -} - -#[derive(PartialEq, Debug, Clone, Difference)] -#[difference(expose)] -pub struct Advert { - pub public_key: PublicKey, - pub timestamp: DateTime<Utc>, - pub signature: [u8; 64], - - pub node_type: NodeType, - pub latitude: Option<f32>, - pub longitude: Option<f32>, - pub feature1: Option<u16>, - pub feature2: Option<u16>, - pub name: String -} - -impl From<Bytes> for Advert { - fn from(value: Bytes) -> Self { - let mut bytes = value; - - let mut advert = Advert { - public_key: PublicKey::default(), - timestamp: Local::now().into(), - signature: [0_u8; 64], - - node_type: NodeType::Invalid, - latitude: None, - longitude: None, - feature1: None, - feature2: None, - name: "".to_string(), - }; - - if bytes.len() < 32 { return advert } - if let Ok(key) = PublicKey::try_from(bytes.split_to(32)) { - advert.public_key = key; - } else { - return advert - } - - if bytes.len() < 4 { return advert } - if let Some(time) = DateTime::from_timestamp(bytes.get_u32_le() as i64, 0) { - advert.timestamp = time; - } - - if bytes.len() < 64 { return advert } - _ = bytes.try_copy_to_slice(&mut advert.signature); - - if bytes.is_empty() { return advert } - let flags = bytes.get_u8(); - advert.node_type = NodeType::from(flags); - - if (flags & 0x10) != 0 { - // The location is 8 bytes (4 each for lat and lon) - if bytes.len() < 8 { return advert } - - advert.latitude = Some(bytes.get_i32_le() as f32 / 1_000_000.0); - advert.longitude = Some(bytes.get_i32_le() as f32 / 1_000_000.0); - } - - if (flags & 0x20) != 0 { - // Feature 1 is 2 bytes when it's included - if bytes.len() < 2 { return advert } - advert.feature1 = Some(bytes.get_u16_le()); - } - - if (flags & 0x40) != 0 { - // Feature 2 is the same - if bytes.len() < 2 { return advert } - advert.feature2 = Some(bytes.get_u16_le()); - } - - // Lastly the name... The flag is a little - // irrelevant because the only different is - // whether the name is omitted or just empty. - // I'm not going to make the distinction - - // The string is assumed to be utf8, but if that - // fails to parse correctly, try to make it lossy - // which will insert U+FFFD REPLACEMENT CHARACTER (�) - if let Ok(string) = String::from_utf8(bytes.to_vec()) { - advert.name = string - } else { - advert.name = String::from_utf8_lossy(&bytes).to_string(); - } - - advert - } -} - -impl Display for Advert { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.node_type.fmt(f)?; - f.write_str(" \"")?; - f.write_str(&self.name)?; - f.write_fmt(format_args!("\" ({:2x?}) at: ", self.public_key.hash_prefix()))?; - std::fmt::Display::fmt(&self.timestamp, f)?; - - match (self.latitude, self.longitude) { - (Some(lat), Some(lon)) => { - f.write_fmt(format_args!(" location: {}, {}", lat, lon))?; - }, - _ => {}, - } - - Ok(()) - } -} - -#[derive(PartialEq, Debug, Clone, Difference)] -#[difference(expose)] -pub struct GroupText { - hash: u8, - mac: u16, - ciphertext: Bytes, - - cleartext: Option<ClearText>, - - incomplete: bool -} - -impl From<Bytes> 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 { - 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.name.clone(); - self.cleartext = Some(cleartext); - true - } else { - false - } - } -} - -impl Display for GroupText { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - if let Some(cleartext) = &self.cleartext { - f.write_fmt(format_args!("({:2x?}) Mac: {:4x?} Group {}: {}", - self.hash, - self.mac, - cleartext.crypto_recipient, - cleartext.message.replace("\n", "\\n")) - ) - } 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<u8> for MessageType { - fn from(value: u8) -> Self { - match (value & 0xFC) >> 3 { - 0x00 => MessageType::Plain, - 0x01 => MessageType::CLI, - 0x02 => MessageType::SignedPlain, - _ => MessageType::Invalid - } - } -} - -#[derive(PartialEq, Debug, Clone, Difference)] -#[difference(expose)] -pub struct ClearText { - pub timestamp: DateTime<Utc>, - pub message_type: MessageType, - pub attempts: u8, - pub sender_hash: u32, - pub sender: Option<String>, - pub message: String, - - pub crypto_recipient: Rc<String> -} - -impl From<Bytes> 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: None, - message: "".to_string(), - crypto_recipient: Rc::new("".to_string()), - }; - - // 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(); - } - - let raw_message = String::from_utf8_lossy(&bytes).to_owned(); - let trimmed_message: Vec<&str> = raw_message.splitn(2, "\0").collect(); - - // Apparently the sender is just whatever is before the first : - let splits: Vec<&str> = trimmed_message[0].splitn(2, ": ").collect(); - if splits.len() > 1 { - clear_text.sender = Some(splits[0].to_owned()); - clear_text.message = splits[1].to_owned(); - } else { - clear_text.message = splits[0].to_owned(); - } - - clear_text - } -} - -#[derive(PartialEq, Debug, Clone)] -pub struct GroupData { - payload: Bytes -} - -impl From<Bytes> for GroupData { - fn from(value: Bytes) -> Self { - GroupData { payload: value } - } -} - -impl Display for GroupData { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_fmt(format_args!("Payload: {}", encode(&self.payload))) - } -} - -#[derive(PartialEq, Debug, Clone, Difference)] -#[difference(expose)] -pub struct AnonReq { - pub dest: u8, - pub public_key: PublicKey, - pub mac: u16, - pub ciphertext: Bytes, - pub request: Option<ClearAnonRequest>, - incomplete: bool -} - -impl From<Bytes> 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 Display for AnonReq { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_fmt(format_args!("({:2x?}) -> ({:2x?}) MAC: {:4x?} ", self.public_key.hash_prefix(), 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, Difference)] -#[difference(expose)] -pub struct ClearAnonRequest { - timestamp: DateTime<Utc>, - sync_timestamp: Option<DateTime<Utc>>, - password: String -} - -impl From<Bytes> for ClearAnonRequest { - fn from(value: Bytes) -> Self { - let mut bytes = value; - - let mut anon_req = ClearAnonRequest { - timestamp: DateTime::from_timestamp(0, 0).unwrap(), - sync_timestamp: None, - password: "".to_string(), - }; - - // 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() as i64, 0) { - anon_req.timestamp = timestamp; - } - - if bytes.len() < 4 { return anon_req; } - if let Some(timestamp) = DateTime::from_timestamp(bytes.get_u32() as i64, 0) { - anon_req.sync_timestamp = Some(timestamp); - } - - anon_req.password = String::from_utf8_lossy(&bytes).to_string(); - anon_req - } -} - -#[derive(PartialEq, Debug, Clone)] -pub struct Path { - pub(crate) cipher: PeerToPeerCipher, -} - -impl From<Bytes> for Path { - fn from(value: Bytes) -> Self { - Path { - cipher: PeerToPeerCipher::from(value) - } - } -} - -impl Display for Path { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_fmt(format_args!("({:2x?}) -> ({:2x?}) MAC: {:4x?} ", self.cipher.source, self.cipher.destination, self.cipher.mac)) - } -} - -#[derive(PartialEq, Debug, Clone)] -pub struct Trace { - tag: u32, - auth: u32, - flags: u8, - pub(crate) path_snr: Vec<f32>, - invalid: bool, - - pub(crate) temp_path: Vec<u8> -} - -impl From<Bytes> for Trace { - fn from(value: Bytes) -> Self { - let mut bytes = value; - - let mut trace = Trace { - tag: 0, - auth: 0, - flags: 0, - path_snr: vec![], - invalid: true, - temp_path: vec![] - }; - - if bytes.len() < 10 { return trace; } - - trace.tag = bytes.get_u32(); - trace.auth = bytes.get_u32(); - trace.flags = bytes.get_u8(); - trace.temp_path = bytes.iter().copied().collect(); - trace.invalid = false; - trace - } -} - -impl Display for Trace { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - for (snr, hash) in self.path_snr.iter().zip(self.temp_path.clone()) { - f.write_fmt(format_args!("({:2x?}): {}dB ", hash, snr))?; - } - - Ok(()) - } -} - -#[derive(PartialEq, Debug, Clone)] -pub struct MultiPart { - payload: Bytes -} - -impl From<Bytes> for MultiPart { - fn from(value: Bytes) -> Self { - MultiPart { payload: value } - } -} - -impl Display for MultiPart { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_fmt(format_args!("Multipart isn't defined.")) - } -} - -#[derive(PartialEq, Debug, Clone)] pub struct Raw { pub(crate) bytes: Bytes } @@ -790,17 +171,6 @@ impl Display for Raw { #[cfg(test)] mod tests { - use std::rc::Rc; - use std::str::FromStr; - - use chrono::DateTime; - use hex::decode; - - use crate::identity::Group; - use crate::identity::KeystoreInput; - use crate::packet::*; - use crate::crypto::*; - use super::*; #[test] @@ -817,354 +187,4 @@ mod tests { assert!(NodeType::Sensor == NodeType::from(0xFC)); assert!(NodeType::Invalid == NodeType::from(0x05)); } - - #[test] - fn advert_with_lat_lon() { - // Real sample packet from the air - let sample = "110c015f7e9b60661da0f2671512460728508c17ef336412a223144d3a623215162682045c44fef7241af0161923d3af0769d2d976b687a506dd5325ef526bf3eb52ae687277fcbde9969a5b0087e0eb0f7c1760a50c6a88bec13cc30a2a9b681d713166515e3bbc2bc27f20c0e4d7b67e08910c29dc02c468b8f8484f574c"; - - let mut signature_slice = [0 as u8; 64]; - hex::decode_to_slice("d2d976b687a506dd5325ef526bf3eb52ae687277fcbde9969a5b0087e0eb0f7c1760a50c6a88bec13cc30a2a9b681d713166515e3bbc2bc27f20c0e4d7b67e08", &mut signature_slice).unwrap(); - - let lhs_packet = Packet { - route_type: RouteType::Flood, - version: PayloadVersion::VersionOne, - path: vec![0x01, 0x5f, 0x7e, 0x9b, 0x60, 0x66, 0x1d, 0xa0, 0xf2, 0x67, 0x15, 0x12], - transport: [0x00, 0x00], - raw_content: Bytes::copy_from_slice(&decode("460728508c17ef336412a223144d3a623215162682045c44fef7241af0161923d3af0769d2d976b687a506dd5325ef526bf3eb52ae687277fcbde9969a5b0087e0eb0f7c1760a50c6a88bec13cc30a2a9b681d713166515e3bbc2bc27f20c0e4d7b67e08910c29dc02c468b8f8484f574c").unwrap()), - content: PacketContent::Advert(Advert { - public_key: PublicKey::from_str("460728508c17ef336412a223144d3a623215162682045c44fef7241af0161923").unwrap(), - timestamp: DateTime::from_timestamp(1762111443, 0).unwrap(), - signature: signature_slice, - node_type: NodeType::Chat, - latitude: Some(47.98286), - longitude: Some(-122.132286), - feature1: None, - feature2: None, - name: "HOWL".to_owned(), - }), - incomplete: false, - }; - let rhs_packet = Packet::from_str(sample).unwrap(); - - assert!(lhs_packet == rhs_packet, "{}", print_compare(lhs_packet, rhs_packet)); - } - - #[test] - fn advert() { - // Another on-air sample - let sample = "12007890b8573a6ba4a05b173d6ccfdfa73ac8ec4a12bf3c745ace636e1d191e132a4eb407695601f50907999735f3699a3c73ace2d8acc385ed209606abef6b914a346fbb88648c540ae794586b4d54eb08b473c8571a00c6b8d3dbcc77699afa169811fa098168707578373335"; - - let mut signature_slice = [0 as u8; 64]; - hex::decode_to_slice("5601f50907999735f3699a3c73ace2d8acc385ed209606abef6b914a346fbb88648c540ae794586b4d54eb08b473c8571a00c6b8d3dbcc77699afa169811fa09", &mut signature_slice).unwrap(); - - let lhs_packet = Packet { - route_type: RouteType::Direct, - version: PayloadVersion::VersionOne, - path: vec![], - transport: [0x00, 0x00], - raw_content: Bytes::copy_from_slice(&decode("7890b8573a6ba4a05b173d6ccfdfa73ac8ec4a12bf3c745ace636e1d191e132a4eb407695601f50907999735f3699a3c73ace2d8acc385ed209606abef6b914a346fbb88648c540ae794586b4d54eb08b473c8571a00c6b8d3dbcc77699afa169811fa098168707578373335").unwrap()), - content: PacketContent::Advert(Advert { - public_key: PublicKey::from_str("7890b8573a6ba4a05b173d6ccfdfa73ac8ec4a12bf3c745ace636e1d191e132a").unwrap(), - timestamp: DateTime::from_timestamp(1762112590, 0).unwrap(), - signature: signature_slice, - node_type: NodeType::Chat, - latitude: None, - longitude: None, - feature1: None, - feature2: None, - name: "hpux735".to_owned(), - }), - incomplete: false, - }; - - let rhs_packet = Packet::from_str(sample).unwrap(); - assert!(lhs_packet == rhs_packet, "{}", print_compare(lhs_packet, rhs_packet)); - } - - #[test] - fn response() { - let sample = "06003412b9000cf1641739f7e4d49bff88bf5695b304111b15277de3a6031b9af3a2b6371cf75615ffefe7dcc0bbea2856c4e798a72d5b989d6de1aa646c1e2eef4cf13e6f92"; - - let lhs_packet = Packet { - route_type: RouteType::Direct, - version: PayloadVersion::VersionOne, - path: vec![], - transport: [0, 0], - raw_content: Bytes::copy_from_slice(&decode("3412b9000cf1641739f7e4d49bff88bf5695b304111b15277de3a6031b9af3a2b6371cf75615ffefe7dcc0bbea2856c4e798a72d5b989d6de1aa646c1e2eef4cf13e6f92").unwrap()), - content: PacketContent::Response(Response { - cipher: PeerToPeerCipher { - destination: 0x34, - source: 0x12, - mac: 0xb900, - ciphertext: Bytes::copy_from_slice(&decode("0cf1641739f7e4d49bff88bf5695b304111b15277de3a6031b9af3a2b6371cf75615ffefe7dcc0bbea2856c4e798a72d5b989d6de1aa646c1e2eef4cf13e6f92").unwrap()), - cleartext: None - }}), - incomplete: false, - }; - - let rhs_packet = Packet::from_str(sample).unwrap(); - assert!(lhs_packet == rhs_packet, "{}", print_compare(lhs_packet, rhs_packet)); - } - - #[test] - fn request() { - let sample = "020012341d87ccaac89563cbb39d2333b725e407a1a6"; - - let lhs_packet = Packet { - route_type: RouteType::Direct, - version: PayloadVersion::VersionOne, - path: vec![], - transport: [0, 0], - raw_content: Bytes::copy_from_slice(&decode("12341d87ccaac89563cbb39d2333b725e407a1a6").unwrap()), - content: PacketContent::Request(Request { - cipher: PeerToPeerCipher { - destination: 0x12, - source: 0x34, - mac: 0x1d87, - ciphertext: Bytes::copy_from_slice(&decode("ccaac89563cbb39d2333b725e407a1a6").unwrap()), - cleartext: None - }, - cleartext: None, }), - incomplete: false, - }; - - let rhs_packet = Packet::from_str(sample).unwrap(); - assert!(lhs_packet == rhs_packet, "{}", print_compare(lhs_packet, rhs_packet)); - } - - #[test] - fn text() { - let sample = "0a001234e91c8eb2b815e0eccf6781a3ff1820d0fb130fcfc87b914244fae227d4ad4c752fb9"; - - let lhs_packet = Packet { - route_type: RouteType::Direct, - version: PayloadVersion::VersionOne, - path: vec![], - 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!(lhs_packet == rhs_packet, "{}", print_compare(lhs_packet, rhs_packet)); - } - - #[test] - fn ack() { - let sample = "0D0E78B5561B03CD70DA326066B8A0CF24F3214D"; - - let lhs_packet = Packet { - route_type: RouteType::Flood, - version: PayloadVersion::VersionOne, - path: vec![0x78, 0xB5, 0x56, 0x1B, 0x03, 0xCD, 0x70, 0xDA, 0x32, 0x60, 0x66, 0xB8, 0xA0, 0xCF], - transport: [0, 0], - raw_content: Bytes::copy_from_slice(&decode("24F3214D").unwrap()), - content: PacketContent::Ack(Ack { - checksum: 0x24F3214D - }), - incomplete: false - }; - - let rhs_packet = Packet::from_str(sample).unwrap(); - assert!(lhs_packet == rhs_packet, "{}", print_compare(lhs_packet, rhs_packet)); - } - - #[test] - fn trace() { - let sample = "26032CE0F149EB5B240000000000ACA079"; - - let lhs_packet = Packet { - route_type: RouteType::Direct, - version: PayloadVersion::VersionOne, - path: vec![0xAC, 0xA0, 0x79], - transport: [0, 0], - raw_content: Bytes::copy_from_slice(&decode("49EB5B240000000000ACA079").unwrap()), - content: PacketContent::Trace(Trace { - tag: 0x49EB5B24, - auth: 0x00000000, - flags: 0x00, - path_snr: vec![11.0, -8.0, -3.75], - invalid: false, - temp_path: vec![] - } - ), - incomplete: false - }; - - let rhs_packet = Packet::from_str(sample).unwrap(); - assert!(lhs_packet == rhs_packet, "{}", print_compare(lhs_packet, rhs_packet)); - } - - #[test] - fn group_text() { - let sample = "15107B2CF1A31C03E8AACD7E6066B8A0ACB71176B87DDF8FE67B33E7A63036E015311EE39232F094FAAD13C4442947947DD098886BC677DE2B6F456149D0A1B05C72F0EECC20DC07EDF163D9D63EBC9BC29A7C79AF"; - - let lhs_packet = Packet { - route_type: RouteType::Flood, - version: PayloadVersion::VersionOne, - path: vec![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!(lhs_packet == rhs_packet, "{}", print_compare(lhs_packet, rhs_packet)); - } - - #[test] - fn group_text_decrypted() { - let sample = "150011C3C1354D619BAE9590E4D177DB7EEAF982F5BDCF78005D75157D9535FA90178F785D"; - - let lhs_packet = Packet { - route_type: RouteType::Flood, - version: PayloadVersion::VersionOne, - path: vec![], - 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: Some("🌲 Tree".to_owned()), - message: "☁️".to_owned(), - timestamp: DateTime::from_timestamp_secs(1758484279).unwrap(), - message_type: MessageType::Plain, - attempts: 0, - sender_hash: 0, - crypto_recipient: Rc::new("Public".to_owned()), - }), - incomplete: false - }), - incomplete: false - }; - - let mut rhs_packet = Packet::from_str(sample).unwrap(); - - let keystore = KeystoreInput { - identities: vec![], - contacts: vec![], - groups: vec![ - Group { - name: Rc::new("Public".to_owned()), - secret: SharedSecret::from_str("8b3387e9c5cdea6ac9e5edbaa115cd72").unwrap() - } - ] - }.compile(); - - _ = rhs_packet.try_decrypt(&keystore); - - assert!(lhs_packet == rhs_packet, "{}", print_compare(lhs_packet, rhs_packet)); - } - - #[test] - fn multipart() { - let sample = "2A0013E8C59624"; - - let lhs_packet = Packet { - route_type: RouteType::Direct, - version: PayloadVersion::VersionOne, - path: vec![], - transport: [0, 0], - raw_content: Bytes::copy_from_slice(&decode("13E8C59624").unwrap()), - content: PacketContent::Multipart(MultiPart { - payload: Bytes::copy_from_slice(&decode("13E8C59624").unwrap()) - }), - incomplete: false - }; - - let rhs_packet = Packet::from_str(sample).unwrap(); - assert!(lhs_packet == rhs_packet, "{}", print_compare(lhs_packet, rhs_packet)); - } - - #[test] - fn group_data() { - let sample = "18079DBB163F3C88707DF4C430C8A06A587CF7E827641C6521A5DE85581C8800793AF4A5497196CB5F24B92A33A9C8AA3BAE4F8C94E8E464849BCBF6333509C3941C47B7ABF85ECFD2FF06DA1A39575D155941F152F63300D2C31B5FAFBDA79637"; - - let lhs_packet = Packet { - route_type: RouteType::TransportFlood, - version: PayloadVersion::VersionOne, - path: vec![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!(lhs_packet == rhs_packet, "{}", print_compare(lhs_packet, rhs_packet)); - } - - #[test] - fn anon_req() { - let sample = "1d001234569df1f9661916901669666fb8025eccb9ddb0499cddad4c164fec219c8b8fd2db15a7138098557cc291928b4358fa7522ddd41d35c99fb78f0def2b3e673d73d2"; - - let lhs_packet = Packet { - route_type: RouteType::Flood, - version: PayloadVersion::VersionOne, - path: vec![], - transport: [0, 0], - raw_content: Bytes::copy_from_slice(&decode("1234569df1f9661916901669666fb8025eccb9ddb0499cddad4c164fec219c8b8fd2db15a7138098557cc291928b4358fa7522ddd41d35c99fb78f0def2b3e673d73d2").unwrap()), - content: PacketContent::AnonReq(AnonReq { - dest: 0x12, - public_key: PublicKey::try_from(Bytes::copy_from_slice(&decode("34569df1f9661916901669666fb8025eccb9ddb0499cddad4c164fec219c8b8f").unwrap())).unwrap(), - mac: 0xd2db, - ciphertext: Bytes::copy_from_slice(&decode("15a7138098557cc291928b4358fa7522ddd41d35c99fb78f0def2b3e673d73d2").unwrap()), - incomplete: false, - request: None - }), - incomplete: false - }; - - let rhs_packet = Packet::from_str(sample).unwrap(); - assert!(lhs_packet == rhs_packet, "{}", print_compare(lhs_packet, rhs_packet)); - } - - #[test] - fn path() { - let sample = "2107BA03127F7EA9221351768DD2DF32E1D02F5851379F5AFCC667AB273442FAB2943673F26DDBEB9595027474"; - - let lhs_packet = Packet { - route_type: RouteType::Flood, - version: PayloadVersion::VersionOne, - path: vec![0xBA, 0x03, 0x12, 0x7F, 0x7E, 0xA9, 0x22], - transport: [0, 0], - raw_content: Bytes::copy_from_slice(&decode("1351768DD2DF32E1D02F5851379F5AFCC667AB273442FAB2943673F26DDBEB9595027474").unwrap()), - content: PacketContent::Path(Path { - cipher: PeerToPeerCipher { - destination: 0x13, - source: 0x51, - mac: 0x768D, - ciphertext: Bytes::copy_from_slice(&decode("D2DF32E1D02F5851379F5AFCC667AB273442FAB2943673F26DDBEB9595027474").unwrap()), - cleartext: None - } - }), - incomplete: false - }; - - let rhs_packet = Packet::from_str(sample).unwrap(); - assert!(lhs_packet == rhs_packet, "{}", print_compare(lhs_packet, rhs_packet)); - } - }
\ No newline at end of file diff --git a/src/path.rs b/src/path.rs new file mode 100644 index 0000000..11eaa5a --- /dev/null +++ b/src/path.rs @@ -0,0 +1,61 @@ +use std::fmt::{Debug, Display}; +use bytes::Bytes; +use structdiff::{Difference, StructDiff}; +use crate::packet_content::PeerToPeerCipher; + + +#[derive(PartialEq, Debug, Clone, Difference)] +#[difference(expose)] +pub struct Path { + pub cipher: PeerToPeerCipher, +} + +impl From<Bytes> for Path { + fn from(value: Bytes) -> Self { + Path { + cipher: PeerToPeerCipher::from(value) + } + } +} + +impl Display for Path { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("({:2x?}) -> ({:2x?}) MAC: {:4x?} ", self.cipher.source, self.cipher.destination, self.cipher.mac)) + } +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + use hex::decode; + use crate::{packet::*, packet_content::{PacketContent, PeerToPeerCipher}}; + + use super::*; + + + #[test] + fn path() { + let sample = "2107BA03127F7EA9221351768DD2DF32E1D02F5851379F5AFCC667AB273442FAB2943673F26DDBEB9595027474"; + + let lhs_packet = Packet { + route_type: RouteType::Flood, + version: PayloadVersion::VersionOne, + path: vec![0xBA, 0x03, 0x12, 0x7F, 0x7E, 0xA9, 0x22], + transport: [0, 0], + raw_content: Bytes::copy_from_slice(&decode("1351768DD2DF32E1D02F5851379F5AFCC667AB273442FAB2943673F26DDBEB9595027474").unwrap()), + content: PacketContent::Path(Path { + cipher: PeerToPeerCipher { + destination: 0x13, + source: 0x51, + mac: 0x768D, + ciphertext: Bytes::copy_from_slice(&decode("D2DF32E1D02F5851379F5AFCC667AB273442FAB2943673F26DDBEB9595027474").unwrap()), + cleartext: None + } + }), + incomplete: false + }; + + let rhs_packet = Packet::from_str(sample).unwrap(); + assert!(lhs_packet == rhs_packet, "{}", print_compare(lhs_packet, rhs_packet)); + } +}
\ No newline at end of file diff --git a/src/request.rs b/src/request.rs new file mode 100644 index 0000000..7ce126c --- /dev/null +++ b/src/request.rs @@ -0,0 +1,202 @@ +use std::fmt::Display; + +use chrono::{DateTime, Utc}; +use structdiff::{Difference, StructDiff}; +use crate::packet_content::PeerToPeerCipher; +use bytes::{Buf, Bytes}; + +#[derive(PartialEq, Debug, Clone, Difference)] +pub struct Request { + pub cipher: PeerToPeerCipher, + pub cleartext: Option<ClearRequest>, +} + +#[derive(PartialEq, Debug, Clone, Difference)] +#[difference(expose)] +pub struct ClearRequest { + pub timestamp: DateTime<Utc>, + pub request_type: RequestType, + pub request_data: Bytes +} + +impl From<Bytes> for ClearRequest { + fn from(value: Bytes) -> Self { + let mut bytes = value; + + let mut clear_request = ClearRequest { + timestamp: DateTime::from_timestamp(0, 0).unwrap(), + request_type: RequestType::Invalid, + request_data: Bytes::new() + }; + + // Just check for the whole fixed-size part at once + if bytes.len() < 5 { return clear_request } + if let Some(timestamp) = DateTime::from_timestamp(bytes.get_u32_le() as i64, 0) { + clear_request.timestamp = timestamp; + } + + clear_request.request_type = RequestType::from(bytes.get_u8()); + + clear_request.request_data = bytes; + + clear_request + } +} + +#[derive(PartialEq, Debug, Clone)] +pub enum RequestType { + Stats, + Keepalive, + Telemetry, + MinMaxAvg, + ACL, + Invalid +} + +impl From<u8> for RequestType { + fn from(value: u8) -> Self { + match value { + 0x01 => RequestType::Stats, + 0x02 => RequestType::Keepalive, + 0x03 => RequestType::Telemetry, + 0x04 => RequestType::MinMaxAvg, + 0x05 => RequestType::ACL, + _ => RequestType::Invalid + } + } +} + +impl Display for RequestType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + RequestType::Stats => f.write_str("STATS"), + RequestType::Keepalive => f.write_str("KEEP ALIVE"), + RequestType::Telemetry => f.write_str("TELEMETRY"), + RequestType::MinMaxAvg => f.write_str("MIN/MAX/AVG"), + RequestType::ACL => f.write_str("ACL"), + RequestType::Invalid => f.write_str("INVALID"), + } + } +} + +impl From<Bytes> for Request { + fn from(value: Bytes) -> Self { + Request { + cipher: PeerToPeerCipher::from(value), + cleartext: None + } + } +} + +impl Display for Request { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::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: {} ", cleartext.timestamp))?; + f.write_fmt(format_args!("{}", &cleartext.request_type)) + } else { + f.write_str("ENCRYPTED") + } + } +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use chrono::DateTime; + use hex::decode; + use bytes::Bytes; + use crate::{crypto::{PrivateKey, PublicKey}, identity::{Contact, Identity, KeystoreInput}, packet::*, packet_content::{PacketContent, PeerToPeerCipher}, request::{ClearRequest, Request, RequestType}}; + + #[test] + fn request_type() { + assert!(RequestType::from(0x01) == RequestType::Stats); + assert!(RequestType::from(0x02) == RequestType::Keepalive); + assert!(RequestType::from(0x03) == RequestType::Telemetry); + assert!(RequestType::from(0x04) == RequestType::MinMaxAvg); + assert!(RequestType::from(0x05) == RequestType::ACL); + assert!(RequestType::from(0x06) == RequestType::Invalid); + assert!(RequestType::from(0xFF) == RequestType::Invalid); + + + assert!(format!("{}", RequestType::Stats) == "STATS"); + assert!(format!("{}", RequestType::Keepalive) == "KEEP ALIVE"); + assert!(format!("{}", RequestType::Telemetry) == "TELEMETRY"); + assert!(format!("{}", RequestType::MinMaxAvg) == "MIN/MAX/AVG"); + assert!(format!("{}", RequestType::ACL) == "ACL"); + assert!(format!("{}", RequestType::Invalid) == "INVALID"); + } + + #[test] + fn request() { + let sample = "020012341d87ccaac89563cbb39d2333b725e407a1a6"; + + let lhs_packet = Packet { + route_type: RouteType::Direct, + version: PayloadVersion::VersionOne, + path: vec![], + transport: [0, 0], + raw_content: Bytes::copy_from_slice(&decode("12341d87ccaac89563cbb39d2333b725e407a1a6").unwrap()), + content: PacketContent::Request(Request { + cipher: PeerToPeerCipher { + destination: 0x12, + source: 0x34, + mac: 0x1d87, + ciphertext: Bytes::copy_from_slice(&decode("ccaac89563cbb39d2333b725e407a1a6").unwrap()), + cleartext: Some(Bytes::copy_from_slice(b"\x9d\xd9\x16\xd2\x01\0\0\0\0\x9d0\x96\xbb\0\0\0")), + }, + cleartext: Some(ClearRequest { + timestamp: DateTime::from_timestamp_secs(3524712861).unwrap(), + request_type: crate::request::RequestType::Stats, + request_data: Bytes::copy_from_slice(b"\0\0\0\0\x9d0\x96\xbb\0\0\0"), + }) + }), + incomplete: false, + }; + + let mut rhs_packet = Packet::from_str(sample).unwrap(); + + let keystore = KeystoreInput { + identities: vec![ + Identity { + name: "Sample 1 ID".to_owned().into(), + private_key: PrivateKey::from_str("4885CF25975EA09742EF76DA587D0957E74EE02AAA34A001458E207E63CF7E6C4940C8C42C335862C71CC2F139633057D1FEE5687B172B27E1E0302A1D480E08").unwrap(), + public_key: PublicKey::default(), + secrets: vec![] + }, + Identity { + name: "Sample 2 ID".to_owned().into(), + private_key: PrivateKey::from_str("38DAA98490B7284697C7ADA6175FD1F8DAD12032AD7ABAE625B7EAD8FEC6444CA281C3370B97155D9C8CECD89A929FDDE0FBF3A9D5C92A1B3C24D711934CD69D").unwrap(), + public_key: PublicKey::default(), + secrets: vec![] + }, + ], + contacts: vec![ + Contact { + name: "Sample 1 CT".to_owned().into(), + public_key: PublicKey::from_str("34569df1f9661916901669666fb8025eccb9ddb0499cddad4c164fec219c8b8f").unwrap() + }, + Contact { + name: "Sample 2 CT".to_owned().into(), + public_key: PublicKey::from_str("12349bdc1f76a0c12149bb15f791dbe42fde02c209b04a85c6f512990c8cedec").unwrap() + }, + ], + groups: vec![] + }.compile(); + + assert!(format!("{}", rhs_packet.content) == " REQUEST | (34) -> (12) MAC: 1d87 ENCRYPTED"); + + rhs_packet.try_decrypt(&keystore); + + assert!(format!("{}", rhs_packet.content) == " REQUEST | (34) -> (12) MAC: 1d87 at: 2081-09-10 06:54:21 UTC STATS"); + + assert!(lhs_packet == rhs_packet, "{}", print_compare(lhs_packet, rhs_packet)); + } + +}
\ No newline at end of file diff --git a/src/response.rs b/src/response.rs new file mode 100644 index 0000000..a3463e0 --- /dev/null +++ b/src/response.rs @@ -0,0 +1,69 @@ +use std::fmt::Display; + +use bytes::Bytes; +use structdiff::{Difference, StructDiff}; +use crate::packet_content::PeerToPeerCipher; + +#[derive(PartialEq, Debug, Clone, Difference)] +#[difference(expose)] +pub struct Response { + pub(crate) cipher: PeerToPeerCipher, +} + +impl From<Bytes> for Response { + fn from(value: Bytes) -> Self { + Response { + cipher: PeerToPeerCipher::from(value) + } + } +} + +impl Display for Response { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::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.cipher.cleartext { + f.write_fmt(format_args!("")) + } else { + f.write_str("ENCRYPTED") + } + } +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use bytes::Bytes; + use hex::decode; + use crate::{packet::*, packet_content::{PacketContent, PeerToPeerCipher}, response::Response}; + + #[test] + fn response() { + let sample = "06003412b9000cf1641739f7e4d49bff88bf5695b304111b15277de3a6031b9af3a2b6371cf75615ffefe7dcc0bbea2856c4e798a72d5b989d6de1aa646c1e2eef4cf13e6f92"; + + let lhs_packet = Packet { + route_type: RouteType::Direct, + version: PayloadVersion::VersionOne, + path: vec![], + transport: [0, 0], + raw_content: Bytes::copy_from_slice(&decode("3412b9000cf1641739f7e4d49bff88bf5695b304111b15277de3a6031b9af3a2b6371cf75615ffefe7dcc0bbea2856c4e798a72d5b989d6de1aa646c1e2eef4cf13e6f92").unwrap()), + content: PacketContent::Response(Response { + cipher: PeerToPeerCipher { + destination: 0x34, + source: 0x12, + mac: 0xb900, + ciphertext: Bytes::copy_from_slice(&decode("0cf1641739f7e4d49bff88bf5695b304111b15277de3a6031b9af3a2b6371cf75615ffefe7dcc0bbea2856c4e798a72d5b989d6de1aa646c1e2eef4cf13e6f92").unwrap()), + cleartext: None + }}), + incomplete: false, + }; + + let rhs_packet = Packet::from_str(sample).unwrap(); + assert!(lhs_packet == rhs_packet, "{}", print_compare(lhs_packet, rhs_packet)); + } +}
\ No newline at end of file diff --git a/src/text.rs b/src/text.rs new file mode 100644 index 0000000..ae2b0b9 --- /dev/null +++ b/src/text.rs @@ -0,0 +1,348 @@ +use std::{fmt::Display, rc::Rc}; +use bytes::{Buf, Bytes}; +use chrono::{DateTime, Utc}; +use hex::encode; +use structdiff::{Difference, StructDiff}; + +use crate::{identity::Keystore, packet_content::PeerToPeerCipher}; + +#[derive(PartialEq, Debug, Clone, Difference)] +#[difference(expose)] +pub struct Text { + pub cipher: PeerToPeerCipher, + pub cleartext: Option<ClearText>, +} + +impl From<Bytes> for Text { + fn from(value: Bytes) -> Self { + Text { + cipher: PeerToPeerCipher::from(value), + cleartext: None + } + } +} + +impl Display for Text { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::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 { + if let Some(sender) = &cleartext.sender { + f.write_fmt(format_args!("at: {}, {} attempts, from {} to {}: {}", + cleartext.timestamp, + cleartext.attempts, + sender, + cleartext.crypto_recipient, + cleartext.message.replace("\n", "\\n")) + ) + } else { + f.write_fmt(format_args!("at: {}, {} attempts, from {} to {}: {}", + cleartext.timestamp, + cleartext.attempts, + "Unknown", + cleartext.crypto_recipient, + cleartext.message.replace("\n", "\\n")) + ) + } + + } else { + f.write_str("ENCRYPTED") + } + } +} + + +#[derive(PartialEq, Debug, Clone, Difference)] +#[difference(expose)] +pub struct ClearText { + pub timestamp: DateTime<Utc>, + pub message_type: MessageType, + pub attempts: u8, + pub sender_hash: u32, + pub sender: Option<String>, + pub message: String, + + pub crypto_recipient: Rc<String> +} + +impl From<Bytes> 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: None, + message: "".to_string(), + crypto_recipient: Rc::new("".to_string()), + }; + + // 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(); + } + + let raw_message = String::from_utf8_lossy(&bytes).to_owned(); + let trimmed_message: Vec<&str> = raw_message.splitn(2, "\0").collect(); + + // Apparently the sender is just whatever is before the first : + let splits: Vec<&str> = trimmed_message[0].splitn(2, ": ").collect(); + if splits.len() > 1 { + clear_text.sender = Some(splits[0].to_owned()); + clear_text.message = splits[1].to_owned(); + } else { + clear_text.message = splits[0].to_owned(); + } + + clear_text + } +} + +#[derive(PartialEq, Debug, Clone)] +pub struct GroupData { + pub(crate) payload: Bytes +} + +impl From<Bytes> for GroupData { + fn from(value: Bytes) -> Self { + GroupData { payload: value } + } +} + +impl Display for GroupData { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("Payload: {}", encode(&self.payload))) + } +} + +#[derive(PartialEq, Debug, Clone, Difference)] +#[difference(expose)] +pub struct GroupText { + hash: u8, + mac: u16, + ciphertext: Bytes, + + cleartext: Option<ClearText>, + + incomplete: bool +} + +impl From<Bytes> 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 { + 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.name.clone(); + self.cleartext = Some(cleartext); + true + } else { + false + } + } +} + +impl Display for GroupText { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if let Some(cleartext) = &self.cleartext { + f.write_fmt(format_args!("({:2x?}) Mac: {:4x?} Group {}: {}", + self.hash, + self.mac, + cleartext.crypto_recipient, + cleartext.message.replace("\n", "\\n")) + ) + } 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<u8> for MessageType { + fn from(value: u8) -> Self { + match (value & 0xFC) >> 3 { + 0x00 => MessageType::Plain, + 0x01 => MessageType::CLI, + 0x02 => MessageType::SignedPlain, + _ => MessageType::Invalid + } + } +} + +#[cfg(test)] +mod tests { + use std::{rc::Rc, str::FromStr}; + + use chrono::DateTime; + use hex::decode; + use bytes::Bytes; + use crate::{crypto::SharedSecret, identity::{Group, KeystoreInput}, packet::*, packet_content::{PacketContent, PeerToPeerCipher}, text::{ClearText, GroupData, GroupText, MessageType, Text}}; + + #[test] + fn text() { + let sample = "0a001234e91c8eb2b815e0eccf6781a3ff1820d0fb130fcfc87b914244fae227d4ad4c752fb9"; + + let lhs_packet = Packet { + route_type: RouteType::Direct, + version: PayloadVersion::VersionOne, + path: vec![], + 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!(lhs_packet == rhs_packet, "{}", print_compare(lhs_packet, rhs_packet)); + } + + #[test] + fn group_text() { + let sample = "15107B2CF1A31C03E8AACD7E6066B8A0ACB71176B87DDF8FE67B33E7A63036E015311EE39232F094FAAD13C4442947947DD098886BC677DE2B6F456149D0A1B05C72F0EECC20DC07EDF163D9D63EBC9BC29A7C79AF"; + + let lhs_packet = Packet { + route_type: RouteType::Flood, + version: PayloadVersion::VersionOne, + path: vec![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!(lhs_packet == rhs_packet, "{}", print_compare(lhs_packet, rhs_packet)); + } + + #[test] + fn group_text_decrypted() { + let sample = "150011C3C1354D619BAE9590E4D177DB7EEAF982F5BDCF78005D75157D9535FA90178F785D"; + + let lhs_packet = Packet { + route_type: RouteType::Flood, + version: PayloadVersion::VersionOne, + path: vec![], + 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: Some("🌲 Tree".to_owned()), + message: "☁️".to_owned(), + timestamp: DateTime::from_timestamp_secs(1758484279).unwrap(), + message_type: MessageType::Plain, + attempts: 0, + sender_hash: 0, + crypto_recipient: Rc::new("Public".to_owned()), + }), + incomplete: false + }), + incomplete: false + }; + + let mut rhs_packet = Packet::from_str(sample).unwrap(); + + let keystore = KeystoreInput { + identities: vec![], + contacts: vec![], + groups: vec![ + Group { + name: Rc::new("Public".to_owned()), + secret: SharedSecret::from_str("8b3387e9c5cdea6ac9e5edbaa115cd72").unwrap() + } + ] + }.compile(); + + _ = rhs_packet.try_decrypt(&keystore); + + assert!(lhs_packet == rhs_packet, "{}", print_compare(lhs_packet, rhs_packet)); + } + + #[test] + fn group_data() { + let sample = "18079DBB163F3C88707DF4C430C8A06A587CF7E827641C6521A5DE85581C8800793AF4A5497196CB5F24B92A33A9C8AA3BAE4F8C94E8E464849BCBF6333509C3941C47B7ABF85ECFD2FF06DA1A39575D155941F152F63300D2C31B5FAFBDA79637"; + + let lhs_packet = Packet { + route_type: RouteType::TransportFlood, + version: PayloadVersion::VersionOne, + path: vec![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!(lhs_packet == rhs_packet, "{}", print_compare(lhs_packet, rhs_packet)); + } +}
\ No newline at end of file diff --git a/src/trace.rs b/src/trace.rs new file mode 100644 index 0000000..c64efc0 --- /dev/null +++ b/src/trace.rs @@ -0,0 +1,87 @@ +use std::fmt::{Debug, Display}; +use bytes::{Buf, Bytes}; +use structdiff::{Difference, StructDiff}; + +#[derive(PartialEq, Debug, Clone, Difference)] +#[difference(expose)] +pub struct Trace { + tag: u32, + auth: u32, + flags: u8, + pub(crate) path_snr: Vec<f32>, + invalid: bool, + + pub(crate) temp_path: Vec<u8> +} + +impl From<Bytes> for Trace { + fn from(value: Bytes) -> Self { + let mut bytes = value; + + let mut trace = Trace { + tag: 0, + auth: 0, + flags: 0, + path_snr: vec![], + invalid: true, + temp_path: vec![] + }; + + if bytes.len() < 10 { return trace; } + + trace.tag = bytes.get_u32(); + trace.auth = bytes.get_u32(); + trace.flags = bytes.get_u8(); + trace.temp_path = bytes.iter().copied().collect(); + trace.invalid = false; + trace + } +} + +impl Display for Trace { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + for (snr, hash) in self.path_snr.iter().zip(self.temp_path.clone()) { + f.write_fmt(format_args!("({:2x?}): {}dB ", hash, snr))?; + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + use hex::decode; + use crate::{packet::*, packet_content::PacketContent}; + + use super::*; + + + #[test] + fn trace() { + let sample = "26032CE0F149EB5B240000000000ACA079"; + + let lhs_packet = Packet { + route_type: RouteType::Direct, + version: PayloadVersion::VersionOne, + path: vec![0xAC, 0xA0, 0x79], + transport: [0, 0], + raw_content: Bytes::copy_from_slice(&decode("49EB5B240000000000ACA079").unwrap()), + content: PacketContent::Trace(Trace { + tag: 0x49EB5B24, + auth: 0x00000000, + flags: 0x00, + path_snr: vec![11.0, -8.0, -3.75], + invalid: false, + temp_path: vec![0xAC, 0xA0, 0x79] + } + ), + incomplete: false + }; + + let rhs_packet = Packet::from_str(sample).unwrap(); + assert!(lhs_packet == rhs_packet, "{}", print_compare(lhs_packet, rhs_packet)); + } + + +}
\ No newline at end of file |
