From bf21ebaaae44b267cfefc87eee9608c09aab96b7 Mon Sep 17 00:00:00 2001 From: Will Dillon Date: Thu, 4 Dec 2025 17:07:12 +0000 Subject: Getting closer to no-std being done --- src/ack.rs | 16 +- src/advert.rs | 128 ++++++++---- src/anon_req.rs | 53 ++--- src/bin/packet_analyzer.rs | 2 +- src/crypto.rs | 117 ++++++----- src/identity.rs | 482 --------------------------------------------- src/lib.rs | 20 +- src/multipart.rs | 17 +- src/no_std_identity.rs | 108 ++++++++++ src/packet.rs | 166 +++++++++------- src/packet_content.rs | 71 +++---- src/path.rs | 18 +- src/request.rs | 125 +++++++----- src/response.rs | 219 +++++++++++++++++--- src/std_identity.rs | 469 +++++++++++++++++++++++++++++++++++++++++++ src/string_helper.rs | 70 +++++++ src/text.rs | 144 +++++++------- src/trace.rs | 37 ++-- 18 files changed, 1380 insertions(+), 882 deletions(-) delete mode 100644 src/identity.rs create mode 100644 src/no_std_identity.rs create mode 100644 src/std_identity.rs create mode 100644 src/string_helper.rs (limited to 'src') diff --git a/src/ack.rs b/src/ack.rs index 7b30205..236ddc4 100644 --- a/src/ack.rs +++ b/src/ack.rs @@ -1,10 +1,7 @@ -use std::fmt::Display; use bytes::{Buf, Bytes}; -use structdiff::{Difference, StructDiff}; -#[derive(PartialEq, Debug, Clone, Difference)] -#[difference(expose)] +#[derive(PartialEq, Debug, Clone)] pub struct Ack { checksum: u32 } @@ -16,17 +13,18 @@ impl From for Ack { } } -impl Display for Ack { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl core::fmt::Display for Ack { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { f.write_fmt(format_args!("Checksum: {:4x?}", self.checksum)) } } #[cfg(test)] mod tests { - use std::str::FromStr; + use core::str::FromStr; use hex::decode; use bytes::Bytes; + use tinyvec::array_vec; use crate::{ack::Ack, packet::*, packet_content::PacketContent}; #[test] @@ -36,7 +34,7 @@ mod tests { 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], + path: array_vec!([u16; 64] => 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 { @@ -53,7 +51,7 @@ mod tests { fn display() { let ack = Ack { checksum: 0x24F3214D }; println!("{}", ack); - assert!(format!("{}", ack) == "Checksum: 24f3214d"); + assert_eq!(format!("{}", ack), "Checksum: 24f3214d"); } } \ No newline at end of file diff --git a/src/advert.rs b/src/advert.rs index 8d29949..24533cf 100644 --- a/src/advert.rs +++ b/src/advert.rs @@ -1,11 +1,8 @@ -use structdiff::{Difference, StructDiff}; -use crate::{crypto::PublicKey, packet_content::NodeType}; +use crate::{crypto::PublicKey, packet_content::NodeType, string_helper::NameString}; use bytes::{Buf, Bytes}; -use std::fmt::{Debug, Display}; use chrono::{DateTime, Local, Utc}; -#[derive(PartialEq, Debug, Clone, Difference)] -#[difference(expose)] +#[derive(PartialEq, Clone, core::fmt::Debug)] pub struct Advert { pub public_key: PublicKey, pub timestamp: DateTime, @@ -16,25 +13,70 @@ pub struct Advert { pub longitude: Option, pub feature1: Option, pub feature2: Option, - pub name: String + + pub name: Option, } -impl From for Advert { - fn from(value: Bytes) -> Self { - let mut bytes = value; +impl Advert { + #[cfg(feature = "std")] + fn name_from_bytes(bytes: Bytes) -> Option { + let mut name_split = bytes.chunk().split(|c| *c == 0); + + if let Some(name) = name_split.next() { + + if let Ok(name) = NameString::from_utf8(name.to_vec()) { + Some(name) + } else { + None + } + } else { + None + } + } + + #[cfg(not(feature = "std"))] + fn name_from_bytes(bytes: Bytes) -> Option { + let mut name_split = bytes.chunk().split(|c| *c == 0); - let mut advert = Advert { + if let Some(name) = name_split.next() { + + if let Ok(name) = NameString::from_utf8(name) { + Some(name) + } else { + None + } + } else { + None + } + } +} + +impl Default for Advert { + fn default() -> Self { + Self { 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(), - }; + + #[cfg(feature = "std")] + name: None, + + #[cfg(not(feature = "std"))] + name: None, + } + } +} + +impl From for Advert { + fn from(value: Bytes) -> Self { + let mut bytes = value; + + let mut advert = Advert::default(); if bytes.len() < 32 { return advert } if let Ok(key) = PublicKey::try_from(bytes.split_to(32)) { @@ -76,30 +118,30 @@ impl From for Advert { } // Lastly the name... The flag is a little - // irrelevant because the only different is + // irrelevant because the only difference 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(); - } - + // Find only the bytes (characters) before the first null. + advert.name = Self::name_from_bytes(bytes); + advert } } -impl Display for Advert { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.node_type.fmt(f)?; +impl core::fmt::Display for Advert { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + core::fmt::Debug::fmt(&self.node_type, 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)?; + + if let Some(name) = &self.name { + f.write_str(&name)?; + } else { + f.write_str("")?; + } + + f.write_fmt(format_args!("\" ({:2x?}) at: ", self.public_key.hash_prefix() >> 24))?; + core::fmt::Display::fmt(&self.timestamp, f)?; match (self.latitude, self.longitude) { (Some(lat), Some(lon)) => { @@ -112,32 +154,41 @@ impl Display for Advert { } } +// Tests for std operations #[cfg(test)] mod tests { - use std::str::FromStr; + use core::str::FromStr; use chrono::DateTime; use hex::decode; + use tinyvec::ArrayVec; use crate::packet::*; use crate::crypto::*; use crate::packet_content::PacketContent; + use crate::string_helper::name_string_from_slice; use super::*; - #[test] fn advert_with_lat_lon() { // Real sample packet from the air + + use tinyvec::array_vec; + let sample = "110c015f7e9b60661da0f2671512460728508c17ef336412a223144d3a623215162682045c44fef7241af0161923d3af0769d2d976b687a506dd5325ef526bf3eb52ae687277fcbde9969a5b0087e0eb0f7c1760a50c6a88bec13cc30a2a9b681d713166515e3bbc2bc27f20c0e4d7b67e08910c29dc02c468b8f8484f574c"; let mut signature_slice = [0 as u8; 64]; hex::decode_to_slice("d2d976b687a506dd5325ef526bf3eb52ae687277fcbde9969a5b0087e0eb0f7c1760a50c6a88bec13cc30a2a9b681d713166515e3bbc2bc27f20c0e4d7b67e08", &mut signature_slice).unwrap(); + let mut name_slice = [0u8; 32]; + let string_slice = "HOWL".as_bytes(); + name_slice[0..string_slice.len()].copy_from_slice(string_slice); + 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], + path: array_vec!([u16; 64] => 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 { @@ -149,7 +200,7 @@ mod tests { longitude: Some(-122.132286), feature1: None, feature2: None, - name: "HOWL".to_owned(), + name: Some(name_string_from_slice(b"HOWL")), }), incomplete: false, }; @@ -169,7 +220,7 @@ mod tests { let lhs_packet = Packet { route_type: RouteType::Direct, version: PayloadVersion::VersionOne, - path: vec![], + path: ArrayVec::new(), transport: [0x00, 0x00], raw_content: Bytes::copy_from_slice(&decode("7890b8573a6ba4a05b173d6ccfdfa73ac8ec4a12bf3c745ace636e1d191e132a4eb407695601f50907999735f3699a3c73ace2d8acc385ed209606abef6b914a346fbb88648c540ae794586b4d54eb08b473c8571a00c6b8d3dbcc77699afa169811fa098168707578373335").unwrap()), content: PacketContent::Advert(Advert { @@ -181,7 +232,7 @@ mod tests { longitude: None, feature1: None, feature2: None, - name: "hpux735".to_owned(), + name: Some(name_string_from_slice(b"hpux735")), }), incomplete: false, }; @@ -205,10 +256,11 @@ mod tests { longitude: Some(-122.132286), feature1: None, feature2: None, - name: "HOWL".to_owned(), + name: Some(name_string_from_slice(b"HOWL")), }; - println!("{}", advert); - assert!(format!("{}", advert) == "Chat \"HOWL\" (46) at: 2025-11-02 19:24:03 UTC location: 47.98286, -122.132286"); + + #[cfg(feature = "std")] + assert_eq!(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 index b468e9e..60d58e4 100644 --- a/src/anon_req.rs +++ b/src/anon_req.rs @@ -1,11 +1,8 @@ -use std::fmt::{Debug, Display}; use chrono::{DateTime, Utc}; use bytes::{Buf, Bytes}; -use structdiff::{Difference, StructDiff}; -use crate::crypto::PublicKey; +use crate::{MeshcoreStringError, crypto::PublicKey, string_helper::{PasswordString, password_string_from_slice}}; -#[derive(PartialEq, Debug, Clone, Difference)] -#[difference(expose)] +#[derive(PartialEq, Clone, core::fmt::Debug)] pub struct AnonReq { pub dest: u8, pub public_key: PublicKey, @@ -15,6 +12,17 @@ pub struct AnonReq { 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; @@ -45,9 +53,9 @@ impl From for AnonReq { } } -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))?; +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)) @@ -57,12 +65,11 @@ impl Display for AnonReq { } } -#[derive(PartialEq, Debug, Clone, Difference)] -#[difference(expose)] +#[derive(PartialEq, Debug, Clone)] pub struct ClearAnonRequest { timestamp: DateTime, sync_timestamp: Option>, - password: String + password: PasswordString, } impl From for ClearAnonRequest { @@ -70,9 +77,11 @@ impl From for ClearAnonRequest { 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: "".to_string(), + password: PasswordString::new() }; // Just check for the whole fixed-size part at once @@ -81,23 +90,23 @@ impl From for ClearAnonRequest { anon_req.timestamp = timestamp; } - // Strip-off any null characters after the password - let raw_password = String::from_utf8_lossy(&bytes).to_string(); - let trimmed_password: Vec<&str> = raw_password.splitn(2, "\0").collect(); - if let Some(password) = trimmed_password.first() { - anon_req.password = password.to_string(); - + // 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 std::collections::HashMap; use std::str::FromStr; use hex::decode; - use crate::identity::KeystoreInput; + use tinyvec::ArrayVec; + use crate::std_identity::KeystoreInput; use crate::packet::*; use crate::crypto::*; use crate::packet_content::PacketContent; @@ -110,7 +119,7 @@ mod tests { let lhs_packet = Packet { route_type: RouteType::Flood, version: PayloadVersion::VersionOne, - path: vec![], + path: ArrayVec::new(), transport: [0, 0], raw_content: Bytes::copy_from_slice(&decode("3412349BDC1F76A0C12149BB15F791DBE42FDE02C209B04A85C6F512990C8CEDEC4E7B8DBF1C3C928D64D87AA8293B9603EEE0").unwrap()), content: PacketContent::AnonReq(AnonReq { @@ -146,13 +155,13 @@ mod tests { assert_eq!(lhs_packet, rhs_packet); println!("\"{}\"", rhs_packet); - assert!(format!("{}", rhs_packet) == " Flood | v1 | | [] | | ANON REQ. | (12) -> (34) MAC: 4e7b ENCRYPTED"); + 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!(lhs_string == rhs_string); + assert_eq!(lhs_string, rhs_string); } } \ No newline at end of file diff --git a/src/bin/packet_analyzer.rs b/src/bin/packet_analyzer.rs index 98f4862..54d2f5b 100644 --- a/src/bin/packet_analyzer.rs +++ b/src/bin/packet_analyzer.rs @@ -6,7 +6,7 @@ use bytes::Bytes; use clap::Parser; use color_eyre::eyre::Result; -use meshcore::{identity::{Keystore, KeystoreInput}, packet::Packet}; +use meshcore::{std_identity::{Keystore, KeystoreInput}, packet::Packet}; use pretty_env_logger; use pcap_file_tokio::pcapng::PcapNgReader; diff --git a/src/crypto.rs b/src/crypto.rs index b62fb5d..cd184dc 100644 --- a/src/crypto.rs +++ b/src/crypto.rs @@ -1,7 +1,3 @@ -use std::{fmt::{Debug, Display}, str::FromStr}; -use ed25519_dalek::{VerifyingKey, hazmat::ExpandedSecretKey}; -use bytes::{Buf, BufMut, Bytes, BytesMut}; -use hex::{decode, decode_to_slice, encode}; // This seems to be an absolute nightmare. GenericArray sucks // but I can't seem to figure out how to pull it out of this @@ -12,8 +8,21 @@ use curve25519_dalek::MontgomeryPoint; use aes::Aes128; use sha2::{Sha256}; use hmac::{Hmac, Mac}; +use ed25519_dalek::{VerifyingKey, hazmat::ExpandedSecretKey}; +use bytes::{Buf, BufMut, Bytes, BytesMut}; + +use crate::string_helper::NameString; + + type HmacSha256 = Hmac; +pub trait Keystore { + fn decrypt_and_id_p2p(&self, source: u8, _dest: u8, mac: u16, data: &Bytes) -> Option<(Bytes, u32, u32)>; + + fn decrypt_and_id_group(&self, group_hash_prefix: u8, mac: u16, data: &Bytes) -> Option<(Bytes, Option)>; + + fn decrypt_anon(&self, dest: u8, pub_key: &PublicKey, mac: u16, data: &Bytes) -> Option<(Bytes, u32)>; +} #[derive(Debug, PartialEq)] pub enum MeshcoreCryptoError { @@ -23,10 +32,11 @@ pub enum MeshcoreCryptoError { KeyCreationError, } -impl std::error::Error for MeshcoreCryptoError {} -impl Display for MeshcoreCryptoError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl core::error::Error for MeshcoreCryptoError {} + +impl core::fmt::Display for MeshcoreCryptoError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { MeshcoreCryptoError::KeyLengthError => f.write_str("Key Length Error"), MeshcoreCryptoError::TryFromSliceError => f.write_str("Try From Slice Error"), @@ -39,9 +49,9 @@ impl Display for MeshcoreCryptoError { #[derive(PartialEq)] pub struct PrivateKey(ExpandedSecretKey); -impl std::fmt::Debug for PrivateKey { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_tuple("PrivateKey").field(&encode(self.0.scalar.as_bytes())).finish() +impl core::fmt::Debug for PrivateKey { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_tuple("PrivateKey").field(&hex::encode(self.0.scalar.as_bytes())).finish() } } @@ -74,18 +84,28 @@ impl Default for PrivateKey { } } +impl PrivateKey { + pub fn hash_prefix(&self) -> u32 { + // The has prefix is the beginning of the public key of the secret + let public_key = PublicKey::from(self); + public_key.hash_prefix() + } +} + #[derive(PartialEq, Clone)] pub struct PublicKey(ed25519_dalek::VerifyingKey); -impl std::fmt::Debug for PublicKey { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_tuple("PublicKey").field(&encode(self.0.as_bytes())).finish() +impl core::fmt::Debug for PublicKey { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_tuple("PublicKey").field(&hex::encode(self.0.as_bytes())).finish() } } impl PublicKey { - pub fn hash_prefix(&self) -> u8 { - self.0.as_bytes()[0] + pub fn hash_prefix(&self) -> u32 { + let mut bytes = Bytes::copy_from_slice(self.0.as_bytes()); + + bytes.get_u32() } } @@ -102,13 +122,13 @@ impl PartialEq for SharedSecret { } } -impl std::fmt::Debug for SharedSecret { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_tuple("SharedSecret").field(&encode(self.0.as_bytes())).finish() +impl core::fmt::Debug for SharedSecret { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_tuple("SharedSecret").field(&hex::encode(self.0.as_bytes())).finish() } } -impl FromStr for SharedSecret { +impl core::str::FromStr for SharedSecret { type Err = MeshcoreCryptoError; fn from_str(s: &str) -> Result { @@ -117,7 +137,7 @@ impl FromStr for SharedSecret { // The provided group secrets are only 16 bytes, // but they're zero-paded to be 32. So, we're // going to get the hex from the string and copy it in. - if decode_to_slice(s, &mut array[0..16]).is_err() { + if hex::decode_to_slice(s, &mut array[0..16]).is_err() { return Err(MeshcoreCryptoError::TryFromSliceError) } else { Ok(SharedSecret(MontgomeryPoint(array))) @@ -139,6 +159,15 @@ impl TryFrom for SharedSecret { } } +// This is kinda meaningless because a shared secret only works +// when it's connected to a key pair, but it needs to exist for +// Arrayvec. +impl Default for SharedSecret { + fn default() -> Self { + Self(curve25519_dalek::MontgomeryPoint([0u8; 32])) + } +} + impl SharedSecret { fn get_key(&self) -> &[u8; 16] { // Safety: The size of the slice ensures that this will never be wrong. @@ -289,11 +318,11 @@ impl TryFrom for PublicKey { } -impl FromStr for PublicKey { +impl core::str::FromStr for PublicKey { type Err = MeshcoreCryptoError; fn from_str(hex_str: &str) -> Result { - if let Ok(hex) = decode(hex_str) { + if let Ok(hex) = hex::decode(hex_str) { if let Ok(slice) = TryInto::<[u8; 32]>::try_into(hex) { if let Ok(key) = VerifyingKey::from_bytes(&slice) { Ok(PublicKey(key)) @@ -309,11 +338,11 @@ impl FromStr for PublicKey { } } -impl FromStr for PrivateKey { +impl core::str::FromStr for PrivateKey { type Err = MeshcoreCryptoError; fn from_str(hex_str: &str) -> Result { - if let Ok(hex) = decode(hex_str) { + if let Ok(hex) = hex::decode(hex_str) { if let Ok(bytes) = TryInto::<[u8; 64]>::try_into(hex) { Ok(PrivateKey(ExpandedSecretKey::from_bytes(&bytes))) } else { @@ -355,9 +384,11 @@ impl SharedSecret { } } +// Tests for std operations #[cfg(test)] mod tests { - use hex::{decode_to_slice, encode}; + use core::str::FromStr; + use hex::{decode_to_slice, encode, decode}; use super::*; #[test] @@ -365,7 +396,7 @@ mod tests { let mut slice = [0_u8; 32]; decode_to_slice("12349bdc1f76a0c12149bb15f791dbe42fde02c209b04a85c6f512990c8cedec", &mut slice).unwrap(); let public_key = PublicKey::from_str("12349bdc1f76a0c12149bb15f791dbe42fde02c209b04a85c6f512990c8cedec"); - assert!(Ok(PublicKey(VerifyingKey::from_bytes(&slice).unwrap())) == public_key); + assert_eq!(Ok(PublicKey(VerifyingKey::from_bytes(&slice).unwrap())), public_key); } #[test] @@ -373,7 +404,7 @@ mod tests { let private_key = PrivateKey::from_str("38DAA98490B7284697C7ADA6175FD1F8DAD12032AD7ABAE625B7EAD8FEC6444CA281C3370B97155D9C8CECD89A929FDDE0FBF3A9D5C92A1B3C24D711934CD69D").unwrap(); let public_key = PublicKey::from(&private_key); println!("Public key: {:#?}", public_key); - assert!(PublicKey::from_str("12349bdc1f76a0c12149bb15f791dbe42fde02c209b04a85c6f512990c8cedec").unwrap() == public_key); + assert_eq!(PublicKey::from_str("12349bdc1f76a0c12149bb15f791dbe42fde02c209b04a85c6f512990c8cedec").unwrap(), public_key); } #[test] @@ -385,8 +416,8 @@ mod tests { let alice_public = PublicKey::from(&alice_private); let bob_public = PublicKey::from(&bob_private); - assert!(alice_public.0.as_bytes().to_vec() == decode("34569df1f9661916901669666fb8025eccb9ddb0499cddad4c164fec219c8b8f").unwrap()); - assert!( bob_public.0.as_bytes().to_vec() == decode("12349bdc1f76a0c12149bb15f791dbe42fde02c209b04a85c6f512990c8cedec").unwrap()); + assert_eq!(alice_public.0.as_bytes().to_vec(), decode("34569df1f9661916901669666fb8025eccb9ddb0499cddad4c164fec219c8b8f").unwrap()); + assert_eq!( bob_public.0.as_bytes().to_vec(), decode("12349bdc1f76a0c12149bb15f791dbe42fde02c209b04a85c6f512990c8cedec").unwrap()); println!("Alice's public key: {}", encode(&alice_public.0.to_bytes())); println!("Bob's public key: {}", encode(&bob_public.0.to_bytes())); @@ -394,12 +425,12 @@ mod tests { let left_secret = alice_private.create_secret(&bob_public); let right_secret = bob_private.create_secret(&alice_public); - assert!(left_secret.0.as_bytes().to_vec() == decode("eb7a365363bd8548ee2b54b9234247be5e42e96be9625adcdf3a55b6c1d04850").unwrap()); + assert_eq!(left_secret.0.as_bytes().to_vec(), decode("eb7a365363bd8548ee2b54b9234247be5e42e96be9625adcdf3a55b6c1d04850").unwrap()); println!("Left shared secret: {}", encode(&left_secret.0.as_bytes())); println!("Right shared secret: {}", encode(&right_secret.0.as_bytes())); - assert!(left_secret == right_secret); + assert_eq!(left_secret, right_secret); } #[test] @@ -407,12 +438,12 @@ mod tests { // Test using a group secret let group_secret = SharedSecret::new_from_group_secret(Bytes::copy_from_slice(&decode("8b3387e9c5cdea6ac9e5edbaa115cd72").unwrap())); let test_group_secret =SharedSecret::from_str("8b3387e9c5cdea6ac9e5edbaa115cd72").unwrap(); - assert!(group_secret == test_group_secret); + assert_eq!(group_secret, test_group_secret); // Test using the secret and ciphertext to make a MAC and ensure it matches an example for a group secret let sample_data = Bytes::copy_from_slice(&decode("354D619BAE9590E4D177DB7EEAF982F5BDCF78005D75157D9535FA90178F785D").unwrap()); let mac = group_secret.get_hmac(&sample_data); - assert!(0xC3C1 == mac); + assert_eq!(0xC3C1, mac); } #[test] @@ -430,7 +461,7 @@ mod tests { let cleartext = secret.decrypt(&ciphertext); let vec = cleartext.to_vec(); let string = String::from_utf8_lossy(&vec); - assert!("Hello my world!!" == string); + assert_eq!("Hello my world!!", string); } #[test] @@ -438,7 +469,7 @@ mod tests { let plaintext = Bytes::copy_from_slice("Meshcore!".as_bytes()); let secret = SharedSecret::new_from_group_secret(Bytes::copy_from_slice(&decode("44A6F78DAD2E54D73A32CDE3ECAA9E75").unwrap())); let ciphertext = secret.encrypt(plaintext); - assert!(ciphertext == decode("62374852B6A11405A081F87356C88861").unwrap()); + assert_eq!(ciphertext, decode("62374852B6A11405A081F87356C88861").unwrap()); } #[test] @@ -468,7 +499,7 @@ mod tests { println!("Result: {:#?}", encode(&text)); - assert!(text == decode("102030405060708090A0B0C0D0E0F0").unwrap()); + assert_eq!(text, decode("102030405060708090A0B0C0D0E0F0").unwrap()); } @@ -525,15 +556,15 @@ mod tests { let mac = 0xC3C1; let cleartext = group_secret.mac_then_decrypt(mac, &sample_data).unwrap(); - assert!(cleartext == decode("3757d06800f09f8cb220547265653a20e29881efb88f00000000000000000000").unwrap()); + assert_eq!(cleartext, decode("3757d06800f09f8cb220547265653a20e29881efb88f00000000000000000000").unwrap()); } #[test] fn test_error_display() { - assert!(format!("{}", MeshcoreCryptoError::KeyLengthError) == "Key Length Error"); - assert!(format!("{}", MeshcoreCryptoError::TryFromSliceError) == "Try From Slice Error"); - assert!(format!("{}", MeshcoreCryptoError::HexDecodeError) == "Hex Decode Error"); - assert!(format!("{}", MeshcoreCryptoError::KeyCreationError) == "Key Creation Error"); + assert_eq!(format!("{}", MeshcoreCryptoError::KeyLengthError), "Key Length Error"); + assert_eq!(format!("{}", MeshcoreCryptoError::TryFromSliceError), "Try From Slice Error"); + assert_eq!(format!("{}", MeshcoreCryptoError::HexDecodeError), "Hex Decode Error"); + assert_eq!(format!("{}", MeshcoreCryptoError::KeyCreationError), "Key Creation Error"); } #[test] @@ -565,9 +596,7 @@ mod tests { .first() .unwrap(); - assert!(lhs_string == cleartext, "{}", - format!("\"{}\" != \"{}\"", lhs_string, cleartext) - ); + assert_eq!(lhs_string, cleartext); } else { assert!(false, "Unable to decrypt"); } diff --git a/src/identity.rs b/src/identity.rs deleted file mode 100644 index c32bd85..0000000 --- a/src/identity.rs +++ /dev/null @@ -1,482 +0,0 @@ -use std::{collections::{HashMap, HashSet}, rc::Rc, str::FromStr}; -use bytes::Bytes; -use log::warn; -use crate::{crypto::{PrivateKey, PublicKey, SharedSecret}}; -use structdiff::{Difference, StructDiff}; - -#[cfg(feature = "std")] -use serde::{Deserialize, de}; - -#[derive(PartialEq, Debug, Clone, Difference)] -#[difference(expose)] -#[cfg_attr(feature = "std", derive(Deserialize))] -/// The Identity structure contains the information to decrypt -/// incoming messages and sign and encrypt outgoing messages. -/// -/// It has the necessary cryptographic material needed to appear -/// as this identity when creating messages. -pub struct Identity { - /// The display name if this user identity. - pub name: Rc, - - /// The private key of this identity. There are currently - /// 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. - #[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. - #[cfg_attr(feature = "std", serde(skip))] - pub public_key: PublicKey, - - /// The secrets has is a collection of shared secrets - /// indexed by the public key of the other side of the - /// connection. The whole remote public key must be - /// used because there are many many hash collisions - /// when using just the 1-byte hash prefix. - #[cfg_attr(feature = "std", serde(skip))] - pub secrets: HashSet<(Rc, SharedSecret)> -} - -#[derive(PartialEq, Debug, Clone, Difference)] -#[difference(expose)] -#[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. -/// -/// It's cryptographic material is also needed to uniquely -/// identify the origin of a message. Most source values -/// are only one byte, so that information just limits the -/// number of entries in the contact list that must be searched -/// before the correct contact is found. -pub struct Contact { - /// The display name of the contact - pub name: Rc, - - /// The provided public key of the remote contact - #[cfg_attr(feature = "std", serde(deserialize_with = "deserialize_public_key"))] - pub public_key: PublicKey -} - -#[derive(PartialEq, Debug, Clone, Difference)] -#[difference(expose)] -#[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 -/// contact in the sense that it's full identity is discovered -/// by whether its secret can correctly decrypt messages. -pub struct Group { - /// The display name of the group - pub name: Rc, - - /// The group's shared secret - #[cfg_attr(feature = "std", serde(deserialize_with = "deserialize_secret"))] - pub secret: SharedSecret -} - -#[derive(PartialEq, Debug, Clone, Difference)] -#[difference(expose)] -pub struct Keystore { - pub identities: HashMap, Identity>, - pub contacts: HashMap, Contact>, - pub groups: HashMap, Group>, -} - -impl Keystore { - pub fn identity_by_name(&self, name: &str) -> Option { - if let Some(identity) = self.identities.get(&Rc::new(name.to_owned())) { - Some(identity.clone()) - } else { - None - } - } - - pub fn decrypt_and_id_p2p(&self, _source: u8, _dest: u8, mac: u16, data: &Bytes) -> Option<(Bytes, Rc, Rc)> { - // Just brute-force all the secrets until one matches... - for identity in self.identities.iter() { - for secret in identity.1.secrets.iter() { - let result = secret.1.mac_then_decrypt(mac, data); - if let Some(result) = result { - return Some((result, identity.0.clone(), secret.0.clone())); - } - } - } - None - } - - pub fn decrypt_and_id_group(&self, _group_hash_prefix: u8, mac: u16, data: &Bytes) -> Option<(Bytes, &Group)> { - for group in self.groups.iter() { - let result = group.1.secret.mac_then_decrypt(mac, data); - if let Some(result) = result { - return Some((result, group.1)); - } - } - - None - } - - pub fn decrypt_anon(&self, _dest: u8, pub_key: &PublicKey, mac: u16, data: &Bytes) -> Option<(Bytes, Rc)> { - // For each identity, create a shared secret against the provided public key and check it. - for identity in self.identities.iter() { - let secret = identity.1.private_key.create_secret(pub_key); - let result = secret.mac_then_decrypt(mac, data); - if let Some(result) = result { - return Some((result, identity.0.clone())); - } - } - None - } -} - -#[derive(PartialEq, Debug, Clone, Difference)] -#[difference(expose)] -#[cfg_attr(feature = "std", derive(Deserialize))] -pub struct KeystoreInput { - pub identities: HashMap, - pub contacts: HashMap, - pub groups: HashMap, -} - -impl KeystoreInput { - pub fn compile(self) -> Keystore { - let mut retval = Keystore { - identities: HashMap::new(), - contacts: HashMap::new(), - groups: HashMap::new() - }; - - // Iterate through the input keystore file - // for each one, make sure it's in the map correctly - // and use the opportunity to compute the shared keys. - - // Unfortunately, for testing, we really need this to be - // sorted so we can ensure that the order of values - // is in the same order every time. - let mut contact_names: Vec<&String> = self.contacts.keys().collect(); - contact_names.sort(); - let mut contacts: Vec = vec![]; - for name in contact_names { - if let Some(key_string) = self.contacts.get(name) { - match PublicKey::from_str(&key_string) { - Ok(pub_key) => { - contacts.push(Contact { - name: Rc::new(name.to_string()), - public_key: pub_key - }); - }, - Err(e) => { - warn!("Unable to add contact named \"{}\" because there was a problem with the public key: {}", name, e); - } - } - } - } - - let mut identity_names: Vec<&String> = self.identities.keys().collect(); - identity_names.sort(); - let mut identities: Vec<(Rc, Identity)> = vec![]; - for name in identity_names { - if let Some(key_string) = self.identities.get(name) { - match PrivateKey::from_str(&key_string) { - Ok(private_key) => { - let public_key = PublicKey::from(&private_key); - let mut i = Identity { - name: Rc::new(name.to_string()), - private_key, - public_key, - secrets: HashSet::new() - }; - - i.secrets = contacts.iter().map(|c| { - (c.name.clone(), i.private_key.create_secret(&c.public_key)) - }).collect(); - - - identities.push((i.name.clone(), i)); - }, - Err(e) => { - warn!("Unable to add identity named \"{}\" because there was a problem with the private key: {}", name, e); - } - } - } - } - - retval.identities = HashMap::from_iter(identities); - - // Create a hash of contacts - let contacts = contacts.into_iter().map(|c| { - (c.name.clone(), c) - }); - - retval.contacts = HashMap::from_iter(contacts); - - // Create a hash of groups - let groups = self.groups.into_iter().filter_map(|(name, secret)| { - let name = Rc::new(name); - match SharedSecret::from_str(&secret) { - Ok(secret) => { - Some((name.clone(), Group { name: name.clone(), secret })) - } - Err(e) => { - warn!("Unable to add group named \"{}\" because there was a problem with the secret: {}", name, e); - None - } - } - }); - - retval.groups = HashMap::from_iter(groups); - - retval - } -} - -#[cfg(feature = "std")] -fn deserialize_private_key<'de, D>(deserializer: D) -> Result - 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), - Err(e) => { - Err(de::Error::custom(format!("{}", e))) - } - } -} - -#[cfg(feature = "std")] -fn deserialize_public_key<'de, D>(deserializer: D) -> Result - where D: de::Deserializer<'de> { - - use hex::decode; - use crate::crypto::{MeshcoreCryptoError, PublicKey}; - - let s: String = de::Deserialize::deserialize(deserializer)?; - - match decode(s) { - Err(hex::FromHexError::InvalidHexCharacter { c, index }) => { - Err(de::Error::custom(format!("Invalid hex character {} at {} when decoding Public Key.", c, index))) - }, - Err(hex::FromHexError::OddLength) => { - Err(de::Error::custom(format!("Odd number of characters when decoding Public Key."))) - }, - Err(hex::FromHexError::InvalidStringLength) => { - Err(de::Error::custom(format!("Invalid hex string length when decoding Public Key."))) - } - Ok(hex) => { - let key = PublicKey::try_from(hex.as_slice()); - match key { - Ok(key) => Ok(key), - Err(MeshcoreCryptoError::KeyCreationError) => { - Err(de::Error::custom(format!("Unable to decompress public key points"))) - } - Err(_) => { - Err(de::Error::custom(format!("Invalid hex string length when decoding Public Key."))) - } - } - } - } -} - -#[cfg(feature = "std")] -fn deserialize_secret<'de, D>(deserializer: D) -> Result - 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) { - Ok(secret) => Ok(secret), - Err(e) => { - Err(de::Error::custom(format!("{}", e))) - } - } -} - -#[cfg(test)] -mod tests { - use std::io; - use std::str::FromStr; - use std::sync::mpsc::{Sender, channel}; - use std::vec; - use hex::decode; - use log::{LevelFilter}; - use serde::de::IntoDeserializer; - use serde::de::value::{StrDeserializer, Error as ValueError}; - - use crate::identity::KeystoreInput; - use crate::crypto::*; - use super::*; - - #[test] - fn secret_deserializer() { - let deserializer: StrDeserializer = "eb50a1bcb3e4e5d7bf69a57c9dada211".into_deserializer(); - let result = deserialize_secret(deserializer); - match result { - Ok(result) => assert!(result == SharedSecret::from_str("eb50a1bcb3e4e5d7bf69a57c9dada211").unwrap()), - Err(err) => assert!(false, "Shouldn't have had an error, but got one: {}", err), - } - - // Test failure conditions - let deserializer: StrDeserializer = "eb50a1bcb3e4e5d7bf69a57c9dada21".into_deserializer(); - let result = deserialize_secret(deserializer); - let error = result.unwrap_err(); - assert_eq!(error.to_string(), "Try From Slice Error", "Unexpected error response: {}", error.to_string()); - } - - #[test] - fn public_key_deserializer() { - let deserializer: StrDeserializer = "34569df1f9661916901669666fb8025eccb9ddb0499cddad4c164fec219c8b8f".into_deserializer(); - let result = deserialize_public_key(deserializer); - match result { - Ok(result) => assert!(result == PublicKey::from_str("34569df1f9661916901669666fb8025eccb9ddb0499cddad4c164fec219c8b8f").unwrap()), - Err(err) => assert!(false, "Shouldn't have had an error, but got one: {}", err), - } - - // Test failure conditions - let deserializer: StrDeserializer = "34569df1f9661916901669666fb8025eccb9ddb0499cddad4c164fec219".into_deserializer(); - let result = deserialize_public_key(deserializer); - let error = result.unwrap_err(); - assert_eq!(error.to_string(), "Odd number of characters when decoding Public Key.", "Unexpected error response: {}", error.to_string()); - - let deserializer: StrDeserializer = "34569df1f9661916901669666fb8025eccb9ddb0499cddad4c164fec21".into_deserializer(); - let result = deserialize_public_key(deserializer); - let error = result.unwrap_err(); - assert_eq!(error.to_string(), "Invalid hex string length when decoding Public Key.", "Unexpected error response: {}", error.to_string()); - - let deserializer: StrDeserializer = "34569df1f9661916901669666fb8025eccb9ddb0499cddad4b164f4c219a8b8f".into_deserializer(); - let result = deserialize_public_key(deserializer); - let error = result.unwrap_err(); - assert_eq!(error.to_string(), "Unable to decompress public key points", "Unexpected error response: {}", error.to_string()); - - let deserializer: StrDeserializer = "34569df1f9661916901669666fb8z25eccb9ddb0499cddad4b164f4c219a8b8f".into_deserializer(); - let result = deserialize_public_key(deserializer); - let error = result.unwrap_err(); - assert_eq!(error.to_string(), "Invalid hex character z at 28 when decoding Public Key.", "Unexpected error response: {}", error.to_string()); - } - - #[test] - fn private_key_deserializer() { - let deserializer: StrDeserializer = "4885CF25975EA09742EF76DA587D0957E74EE02AAA34A001458E207E63CF7E6C4940C8C42C335862C71CC2F139633057D1FEE5687B172B27E1E0302A1D480E08".into_deserializer(); - let result = deserialize_private_key(deserializer); - match result { - Ok(result) => assert!(result == PrivateKey::from_str("4885CF25975EA09742EF76DA587D0957E74EE02AAA34A001458E207E63CF7E6C4940C8C42C335862C71CC2F139633057D1FEE5687B172B27E1E0302A1D480E08").unwrap()), - Err(err) => assert!(false, "Shouldn't have had an error, but got one: {}", err), - } - - // Test failure conditions - let deserializer: StrDeserializer = "4885CF25975EA09742EF76DA587D0957E74EE02AAA34A001458E207E63CF7E6C4940C8C42C335862C71CC2F139633057D1FEE5687B172B27E1E0302A1D480E0".into_deserializer(); - let result = deserialize_private_key(deserializer); - let error = result.unwrap_err(); - assert_eq!(error.to_string(), "Hex Decode Error", "Unexpected error response: {}", error.to_string()); - - let deserializer: StrDeserializer = "4885CF25975EA09742EF76DA587D0957E74EE02AAA34A001458E207E63CF7E6C4940C8C42C335862C71CC2F139633057D1FEE5687B172B27E1E0302A1D480E".into_deserializer(); - let result = deserialize_private_key(deserializer); - let error = result.unwrap_err(); - assert_eq!(error.to_string(), "Try From Slice Error", "Unexpected error response: {}", error.to_string()); - } - - #[test] - fn deserialize_toml() { - let file_contents = include_str!("../test_identities_file.toml"); - let keystore_in: KeystoreInput = toml::from_str(file_contents).unwrap(); - let keystore = keystore_in.compile(); - - println!("{:#?}", keystore.identity_by_name("Sample 1 ID").unwrap()); - - let lhs = keystore.identity_by_name("Sample 1 ID").unwrap(); - - let rhs = Identity { - name: Rc::new("Sample 1 ID".to_owned()), - private_key: PrivateKey::from_str("4885CF25975EA09742EF76DA587D0957E74EE02AAA34A001458E207E63CF7E6C4940C8C42C335862C71CC2F139633057D1FEE5687B172B27E1E0302A1D480E08").unwrap(), - public_key: PublicKey::from_str("34569df1f9661916901669666fb8025eccb9ddb0499cddad4c164fec219c8b8f").unwrap(), - secrets: HashSet::from([ - (Rc::new("Sample 1 CT".to_owned()), SharedSecret::try_from(Bytes::copy_from_slice(&decode("15f857c60a0672e999fa20f022ffc7d5f8c7a1a1808868273aa7d6caa772f818").unwrap())).unwrap()), - (Rc::new("Sample 2 CT".to_owned()), SharedSecret::try_from(Bytes::copy_from_slice(&decode("eb7a365363bd8548ee2b54b9234247be5e42e96be9625adcdf3a55b6c1d04850").unwrap())).unwrap()), - (Rc::new("Sample 3 CT".to_owned()), SharedSecret::try_from(Bytes::copy_from_slice(&decode("87c4bbfc210ef8c6eda4ab46ac40e27d4a9f7c649df63d932ccb94279e26a549").unwrap())).unwrap()), - (Rc::new("Sample 4 CT".to_owned()), SharedSecret::try_from(Bytes::copy_from_slice(&decode("02237f72fcd9405ee890b3d706800d1fd4e44005552c00252c3f3a9726c46d13").unwrap())).unwrap()), - (Rc::new("Sample 5 CT".to_owned()), SharedSecret::try_from(Bytes::copy_from_slice(&decode("d7c2916d671ee530ce7acba8b235414cce5b6e5b9079e714b77179359b2f5d4c").unwrap())).unwrap()), - ]) - }; - assert!(lhs == rhs); - - } - - // This struct is used as an adaptor, it implements io::Write and forwards the buffer to a mpsc::Sender - struct WriteAdapter { - sender: Sender, - } - - impl io::Write for WriteAdapter { - // On write we forward each u8 of the buffer to the sender and return the length of the buffer - fn write(&mut self, buf: &[u8]) -> io::Result { - for chr in buf { - self.sender.send(*chr).unwrap(); - } - Ok(buf.len()) - } - - fn flush(&mut self) -> io::Result<()> { - Ok(()) - } - } - - #[test] - fn deserialize_toml_errors() { - let (rx, tx) = channel(); - let expect: Vec<&str> = vec![ - "Hex Decode Error", - "Key Length Error", - "Key Creation Error", - "Hex Decode Error", - "Hex Decode Error", - "Try From Slice Error", - "Try From Slice Error", - ]; - - let _ = pretty_env_logger::env_logger::builder() - .is_test(true) - .filter_level(LevelFilter::max()) - .target(pretty_env_logger::env_logger::Target::Pipe(Box::new(WriteAdapter { sender: rx }))) - .try_init(); - - // Test failure conditions - let contents = r#" -[identities] -"1 Hex Decode Error" = "4885CF25975EA09742EF76DA587D0957E74EE02AAA34A001458E207E63CF7E6C4940C8C42C335862C71CC2F139633057D1FEE5687B172B27E1E0302A1D480E0" -"2 Slice Error" = "4885CF25975EA09742EF76DA587D0957E74EE02AAA34A001458E207E63CF7E6C4940C8C42C335862C71CC2F139633057D1FEE5687B172B27E1E0302A1D480E" -[contacts] -"1 Odd number of characters" = "34569df1f9661916901669666fb8025eccb9ddb0499cddad4c164fec219" -"2 Invalid hex length" = "34569df1f9661916901669666fb8025eccb9ddb0499cddad4c164fec21" -"3 Unable to decompress" = "34569df1f9661916901669666fb8025eccb9ddb0499cddad4b164f4c219a8b8f" -"4 Invalid hex character" = "34569df1f9661916901669666fb8z25eccb9ddb0499cddad4b164f4c219a8b8f" -[groups] -"Slice error" = "eb50a1bcb3e4e5d7bf69a57c9dada21" - "#; - - let keystore_in = toml::from_str::(contents).unwrap(); - let _keystore = keystore_in.compile(); - - String::from_utf8(tx.try_iter().collect::>()) - .unwrap() - .split('\n') - .into_iter() - .zip(expect.into_iter()) - .for_each(|(got, expect)| { - println!("{}", &got); - assert!(got.contains(expect), "Got log line: \"{}\" and didn't find \"{}\" within it as expected.", got, expect); - } - ); - - } - - -} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index dfd777f..621dcf4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,23 @@ +#![cfg_attr(not(feature = "std"), no_std)] + pub mod crypto; -pub mod identity; +pub mod string_helper; + +#[derive(Debug, PartialEq)] +pub enum MeshcoreStringError { + StringLengthError, + StringUnicodeError, + StringEncrypted, +} + +// This version of identity is pretty specific +// to the std version of this library. +#[cfg(feature = "std")] +pub mod std_identity; + +// This version of identity is no-std +#[cfg(not(feature = "std"))] +pub(crate) mod no_std_identity; pub mod packet; pub mod packet_content; diff --git a/src/multipart.rs b/src/multipart.rs index c68b442..ced4aaa 100644 --- a/src/multipart.rs +++ b/src/multipart.rs @@ -1,9 +1,7 @@ -use std::fmt::{Debug, Display}; + use bytes::Bytes; -use structdiff::{Difference, StructDiff}; -#[derive(PartialEq, Debug, Clone, Difference)] -#[difference(expose)] +#[derive(PartialEq, Debug, Clone)] pub struct MultiPart { payload: Bytes } @@ -14,16 +12,18 @@ impl From for MultiPart { } } -impl Display for MultiPart { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl core::fmt::Display for MultiPart { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { f.write_fmt(format_args!("Multipart isn't defined.")) } } +// Tests for std operations #[cfg(test)] mod tests { - use std::str::FromStr; + use core::str::FromStr; use hex::decode; + use tinyvec::ArrayVec; use crate::{packet::*, packet_content::PacketContent}; use super::*; @@ -35,7 +35,7 @@ mod tests { let lhs_packet = Packet { route_type: RouteType::Direct, version: PayloadVersion::VersionOne, - path: vec![], + path: ArrayVec::new(), transport: [0, 0], raw_content: Bytes::copy_from_slice(&decode("13E8C59624").unwrap()), content: PacketContent::Multipart(MultiPart { @@ -47,6 +47,7 @@ mod tests { let rhs_packet = Packet::from_str(sample).unwrap(); assert_eq!(lhs_packet, rhs_packet); + #[cfg(feature = "std")] assert_eq!(format!("{}", lhs_packet), " Direct | v1 | | [] | | MULTIPART | Multipart isn't defined."); } } \ No newline at end of file diff --git a/src/no_std_identity.rs b/src/no_std_identity.rs new file mode 100644 index 0000000..2a0018e --- /dev/null +++ b/src/no_std_identity.rs @@ -0,0 +1,108 @@ +use bytes::Bytes; +use crate::{NameString, crypto::{Keystore, PrivateKey, PublicKey, SharedSecret}}; +use tinyvec::ArrayVec; + +#[derive(PartialEq, Clone, Debug)] +pub struct Identity { + pub private_key: PrivateKey, + pub hash_prefix: u32, + pub secrets: ArrayVec<[(u32, SharedSecret); 10]>, +} + +impl Default for Identity { + fn default() -> Self { + let private_key = PrivateKey::default(); + let hash_prefix = private_key.hash_prefix(); + + Self { + private_key, + hash_prefix, + secrets: Default::default() + } + } +} + +#[derive(PartialEq, Clone, Debug)] +pub struct StaticKeystore { + pub identities: ArrayVec<[Identity; 10]>, + pub groups: ArrayVec<[SharedSecret; 10]>, +} + +impl Keystore for StaticKeystore { + fn decrypt_and_id_p2p(&self, _source: u8, _dest: u8, mac: u16, data: &Bytes) -> Option<(Bytes, u32, u32)> { + // Just brute-force all the secrets until one matches... + for identity in self.identities.iter() { + for (peer_prefix, secret) in identity.secrets.iter() { + let result = secret.mac_then_decrypt(mac, data); + if let Some(result) = result { + return Some((result, identity.hash_prefix, *peer_prefix)); + } + } + } + None + } + + fn decrypt_and_id_group(&self, _group_hash_prefix: u8, mac: u16, data: &Bytes) -> Option<(Bytes, Option)> { + for group in self.groups.iter() { + let result = group.mac_then_decrypt(mac, data); + if let Some(result) = result { + return Some((result, None)); + } + } + + None + } + + fn decrypt_anon(&self, _dest: u8, pub_key: &PublicKey, mac: u16, data: &Bytes) -> Option<(Bytes, u32)> { + // For each identity, create a shared secret against the provided public key and check it. + for identity in self.identities.iter() { + let secret = identity.private_key.create_secret(pub_key); + let result = secret.mac_then_decrypt(mac, data); + if let Some(result) = result { + return Some((result, identity.hash_prefix)); + } + } + None + } +} + +#[derive(PartialEq, Debug, Clone)] +pub struct StaticKeystoreInput { + pub identities: ArrayVec<[PrivateKey; 10]>, + pub contacts: ArrayVec<[PublicKey; 10]>, + pub groups: ArrayVec<[SharedSecret; 10]>, +} + +impl StaticKeystoreInput { + pub fn compile(self) -> StaticKeystore { + let mut retval = StaticKeystore { + identities: ArrayVec::new(), + groups: ArrayVec::new(), + }; + + for identity_in in self.identities { + let mut identity = Identity::default(); + identity.private_key = identity_in; + identity.hash_prefix = PublicKey::from(&identity.private_key).hash_prefix(); + for contact_in in self.contacts.iter() { + identity.secrets.push((contact_in.hash_prefix(), identity.private_key.create_secret(&contact_in))); + } + + retval.identities.push(identity); + } + + for group in self.groups.iter() { + retval.groups.push(group.clone()); + } + + retval + } +} + +#[cfg(test)] +mod tests { + #[test] + fn test_compile() { + + } +} \ No newline at end of file diff --git a/src/packet.rs b/src/packet.rs index 3fa26cb..50374a7 100644 --- a/src/packet.rs +++ b/src/packet.rs @@ -1,27 +1,29 @@ -use std::{fmt::{Debug, Display}, str::FromStr}; +#[cfg(feature = "std")] +use crate::std_identity::Keystore; + +#[cfg(not(feature = "std"))] +use crate::no_std_identity::Keystore; -use hex::decode; use bytes::{Buf, Bytes}; -use structdiff::{Difference, StructDiff}; -use crate::{anon_req::ClearAnonRequest, identity::Keystore, packet_content::PacketContent, request::ClearRequest, response::ClearResponse, text::ClearText}; +use tinyvec::ArrayVec; +use crate::{anon_req::ClearAnonRequest, packet_content::PacketContent, request::ClearRequest, response::ClearResponse, text::ClearText}; -#[derive(PartialEq, Debug, Clone, Difference)] -#[difference(expose)] +#[derive(PartialEq, Clone, core::fmt::Debug)] pub struct Packet { pub route_type: RouteType, pub version: PayloadVersion, - pub path: Vec, + pub path: ArrayVec<[u16; 64]>, pub transport: [u16; 2], pub raw_content: Bytes, pub content: PacketContent, pub incomplete: bool } -impl FromStr for Packet { +impl core::str::FromStr for Packet { type Err = hex::FromHexError; fn from_str(hex_str: &str) -> Result { - let hex = decode(hex_str)?; + let hex = hex::decode(hex_str)?; Ok(Packet::from(Bytes::copy_from_slice(&hex))) } } @@ -67,22 +69,24 @@ impl From for Packet { // The packet isn't long enough for the indicated route if bytes.len() < path_length { return packet; } - let route: Vec = bytes - .split_to(path_length) - .into_iter() - .map(|x| x as u16) - .collect(); + let mut route = ArrayVec::new(); + for _ in 0..path_length { + let path_element = bytes.get_u8() as u16; + route.push(path_element); + } + route }, + PayloadVersion::VersionTwo => { // The packet isn't long enough for the indicated route if bytes.len() < path_length * 2 { return packet; } - let route: Vec = bytes - .split_to(path_length * 2) - .chunks(2) - .map(|c| Bytes::copy_from_slice(c).get_u16_le()) - .collect(); + let mut route = ArrayVec::new(); + for i in 0..path_length { + let path_element = bytes.get_u16(); + route[i] = path_element; + } route }, @@ -101,12 +105,18 @@ impl From for Packet { if let PacketContent::Trace(trace) = packet.content { let mut trace = trace; let path_snr = packet.path; - packet.path = trace.temp_path.iter().map(|i| *i as u16).collect(); - trace.path_snr = path_snr.iter().map( |u| { + + for (index, element) in trace.temp_path.iter().enumerate() { + packet.path[index] = *element as u16; + } + + for u in path_snr.iter() { let i = *u as i8; let f = i as f32; - f / 4.0 - }).collect(); + + trace.path_snr.push(f / 4.0); + } + // DON'T delete the temp path, so Trace has access to the path // when it prints the SNR results in its Display function. packet.content = PacketContent::Trace(trace); @@ -124,7 +134,7 @@ impl Default for Packet { Packet { route_type: RouteType::Invalid, version: PayloadVersion::Invalid, - path: vec![], + path: ArrayVec::new(), transport: [0, 0], raw_content: Bytes::new(), content: PacketContent::Invalid, @@ -133,11 +143,11 @@ impl Default for Packet { } } -impl Display for Packet { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - std::fmt::Display::fmt(&self.route_type, f)?; +impl core::fmt::Display for Packet { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + core::fmt::Display::fmt(&self.route_type, f)?; f.write_str(" | ")?; - std::fmt::Display::fmt(&self.version, f)?; + core::fmt::Display::fmt(&self.version, f)?; if self.route_type == RouteType::TransportDirect || self.route_type == RouteType::TransportFlood { f.write_fmt(format_args!(" | {:4x?}, {:4x?} | ", self.transport[0], self.transport[1]))? @@ -160,11 +170,12 @@ impl Display for Packet { f.write_str(" | ") }?; - std::fmt::Display::fmt(&self.content, f) + core::fmt::Display::fmt(&self.content, f) } } impl Packet { + #[cfg(feature = "std")] pub fn try_decrypt(&mut self, keystore: &Keystore) -> bool { match self.content { // Encrypted packet types @@ -240,8 +251,8 @@ pub enum RouteType { Invalid, } -impl Display for RouteType { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl core::fmt::Display for RouteType { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { RouteType::TransportFlood => f.write_str("T-Flood "), RouteType::Flood => f.write_str(" Flood "), @@ -288,8 +299,8 @@ impl From for PayloadVersion { } } -impl Display for PayloadVersion { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl core::fmt::Display for PayloadVersion { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { PayloadVersion::VersionOne => f.write_str("v1"), PayloadVersion::VersionTwo => f.write_str("v2"), @@ -302,54 +313,57 @@ impl Display for PayloadVersion { #[cfg(test)] mod tests { + use core::str::FromStr; + use tinyvec::array_vec; + use crate::packet_content::{PacketContent, Raw}; use super::*; #[test] fn header_route_type() { // The route type is the lowest-order two bits - assert!(RouteType::TransportFlood == RouteType::from(0x00)); - assert!(RouteType::TransportFlood == RouteType::from(0xFC)); - assert!(RouteType::Flood == RouteType::from(0x01)); - assert!(RouteType::Flood == RouteType::from(0xFD)); - assert!(RouteType::Direct == RouteType::from(0x02)); - 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 "); + assert_eq!(RouteType::TransportFlood, RouteType::from(0x00)); + assert_eq!(RouteType::TransportFlood, RouteType::from(0xFC)); + assert_eq!(RouteType::Flood, RouteType::from(0x01)); + assert_eq!(RouteType::Flood, RouteType::from(0xFD)); + assert_eq!(RouteType::Direct, RouteType::from(0x02)); + assert_eq!(RouteType::Direct, RouteType::from(0xFE)); + assert_eq!(RouteType::TransportDirect, RouteType::from(0x03)); + assert_eq!(RouteType::TransportDirect, RouteType::from(0xFF)); + + assert_eq!(format!("{}", RouteType::TransportFlood), "T-Flood "); + assert_eq!(format!("{}", RouteType::Flood), " Flood "); + assert_eq!(format!("{}", RouteType::Direct), " Direct"); + assert_eq!(format!("{}", RouteType::TransportDirect), "T-Direct"); + assert_eq!(format!("{}", RouteType::Invalid), "INVALID "); } #[test] fn header_version() { - assert!(PayloadVersion::VersionOne == PayloadVersion::from(0x00)); - assert!(PayloadVersion::VersionOne == PayloadVersion::from(0x3F)); - assert!(PayloadVersion::VersionTwo == PayloadVersion::from(0x40)); - assert!(PayloadVersion::VersionTwo == PayloadVersion::from(0x7F)); - assert!(PayloadVersion::VersionThree == PayloadVersion::from(0x80)); - 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"); + assert_eq!(PayloadVersion::VersionOne, PayloadVersion::from(0x00)); + assert_eq!(PayloadVersion::VersionOne, PayloadVersion::from(0x3F)); + assert_eq!(PayloadVersion::VersionTwo, PayloadVersion::from(0x40)); + assert_eq!(PayloadVersion::VersionTwo, PayloadVersion::from(0x7F)); + assert_eq!(PayloadVersion::VersionThree, PayloadVersion::from(0x80)); + assert_eq!(PayloadVersion::VersionThree, PayloadVersion::from(0xBF)); + assert_eq!(PayloadVersion::VersionFour, PayloadVersion::from(0xC0)); + assert_eq!(PayloadVersion::VersionFour, PayloadVersion::from(0xFF)); + + assert_eq!(format!("{}", PayloadVersion::VersionOne), "v1"); + assert_eq!(format!("{}", PayloadVersion::VersionTwo), "v2"); + assert_eq!(format!("{}", PayloadVersion::VersionThree), "v3"); + assert_eq!(format!("{}", PayloadVersion::VersionFour), "v4"); + assert_eq!(format!("{}", PayloadVersion::Invalid), "xx"); } #[test] fn packet() { // Check the hex decode errors - assert!(Err(hex::FromHexError::InvalidHexCharacter { c: 's', index: 0 }) == Packet::from_str("s0")); - assert!(Err(hex::FromHexError::OddLength) == Packet::from_str("0")); + assert_eq!(Err(hex::FromHexError::InvalidHexCharacter { c: 's', index: 0 }), Packet::from_str("s0")); + assert_eq!(Err(hex::FromHexError::OddLength), Packet::from_str("0")); // Check errors related to packet length issues - assert!(Packet::default() == Packet::from_str("").unwrap()); + assert_eq!(Packet::default(), Packet::from_str("").unwrap()); let rhs_packet = Packet::from_str("01").unwrap(); assert_eq!(Packet::default(), rhs_packet); @@ -379,17 +393,17 @@ mod tests { let lhs_packet = Packet { route_type: RouteType::TransportDirect, version: PayloadVersion::VersionOne, - path: [0x06, 0x07, 0x08, 0x09, 0x0A].to_vec(), + path: array_vec!([u16; 64] => 0x06, 0x07, 0x08, 0x09, 0x0A), transport: [0x0102, 0x0304], raw_content: Bytes::copy_from_slice(&[0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19]), 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 rhs_packet: 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_eq!(format!("{}", rhs_packet), compare_string); assert_eq!(lhs_packet, rhs_packet); } @@ -399,25 +413,25 @@ mod tests { let mut packet = Packet { route_type: RouteType::Direct, version: PayloadVersion::VersionOne, - path: vec![], + path: ArrayVec::new(), transport: [0, 1], raw_content: Bytes::new(), content: PacketContent::Invalid, incomplete: false }; - assert!(format!("{}", packet) == " Direct | v1 | | [] | | INVALID | INVALID"); + assert_eq!(format!("{}", packet), " Direct | v1 | | [] | | INVALID | INVALID"); - packet.path = vec![0x01]; - assert!(format!("{}", packet) == " Direct | v1 | | [01] | | INVALID | INVALID"); + packet.path.push(0x01); + assert_eq!(format!("{}", packet), " Direct | v1 | | [01] | | INVALID | INVALID"); - packet.path = vec![0x01, 0x02]; - assert!(format!("{}", packet) == " Direct | v1 | | [01, 02] | | INVALID | INVALID"); + packet.path.push(0x02); + assert_eq!(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.path.push(0xff); + assert_eq!(format!("{}", packet), " Direct | v1 | | [01, 02, ff] | | INVALID | INVALID"); packet.incomplete = true; - assert!(format!("{}", packet) == " Direct | v1 | | [01, 02, ff] | x | INVALID | INVALID"); + assert_eq!(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 d13c663..26230e1 100644 --- a/src/packet_content.rs +++ b/src/packet_content.rs @@ -1,10 +1,10 @@ -use std::fmt::{Debug, Display}; use bytes::{Buf, Bytes}; -use structdiff::{Difference, StructDiff}; -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}; +use crate::{ack::Ack, advert::Advert, anon_req::AnonReq, multipart::MultiPart, path::Path, request::Request, response::Response, text::{GroupData, GroupText, Text}, trace::Trace}; -#[derive(PartialEq, Debug, Clone, Difference)] -#[difference(expose)] +#[cfg(feature = "std")] +use crate::std_identity::Keystore; + +#[derive(PartialEq, Clone, core::fmt::Debug)] pub enum PacketContent { Request(Request), Response(Response), @@ -62,23 +62,23 @@ impl PacketContent { } } -impl Display for PacketContent { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl core::fmt::Display for PacketContent { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { f.write_str(self.justified_name())?; match self { - PacketContent::Request(c) => std::fmt::Display::fmt(&c, f), - PacketContent::Response(c) => std::fmt::Display::fmt(&c, f), - PacketContent::Text(c) => std::fmt::Display::fmt(&c, f), - PacketContent::Ack(c) => std::fmt::Display::fmt(&c, f), - PacketContent::Advert(c) => std::fmt::Display::fmt(&c, f), - PacketContent::GroupText(c) => std::fmt::Display::fmt(&c, f), - PacketContent::GroupData(c) => std::fmt::Display::fmt(&c, f), - PacketContent::AnonReq(c) => std::fmt::Display::fmt(&c, f), - PacketContent::Path(c) => std::fmt::Display::fmt(&c, f), - PacketContent::Trace(c) => std::fmt::Display::fmt(&c, f), - PacketContent::Multipart(c) => std::fmt::Display::fmt(&c, f), - PacketContent::Raw(c) => std::fmt::Display::fmt(&c, f), + PacketContent::Request(c) => core::fmt::Display::fmt(&c, f), + PacketContent::Response(c) => core::fmt::Display::fmt(&c, f), + PacketContent::Text(c) => core::fmt::Display::fmt(&c, f), + PacketContent::Ack(c) => core::fmt::Display::fmt(&c, f), + PacketContent::Advert(c) => core::fmt::Display::fmt(&c, f), + PacketContent::GroupText(c) => core::fmt::Display::fmt(&c, f), + PacketContent::GroupData(c) => core::fmt::Display::fmt(&c, f), + PacketContent::AnonReq(c) => core::fmt::Display::fmt(&c, f), + PacketContent::Path(c) => core::fmt::Display::fmt(&c, f), + PacketContent::Trace(c) => core::fmt::Display::fmt(&c, f), + PacketContent::Multipart(c) => core::fmt::Display::fmt(&c, f), + PacketContent::Raw(c) => core::fmt::Display::fmt(&c, f), PacketContent::Invalid => f.write_str("INVALID") } } @@ -106,8 +106,7 @@ impl From for NodeType { } } -#[derive(PartialEq, Debug, Clone, Difference)] -#[difference(expose)] +#[derive(PartialEq, Debug, Clone)] pub struct PeerToPeerCipher { pub destination: u8, pub source: u8, @@ -141,6 +140,7 @@ impl From for PeerToPeerCipher { } impl PeerToPeerCipher { + #[cfg(feature = "std")] pub fn try_decrypt(&mut self, keystore: &Keystore) -> bool { let decrypt = keystore.decrypt_and_id_p2p( self.source, @@ -163,28 +163,29 @@ pub struct Raw { pub(crate) bytes: Bytes } -impl Display for Raw { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - std::fmt::Debug::fmt(&self, f) +impl core::fmt::Display for Raw { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + core::fmt::Debug::fmt(&self, f) } } +// Tests for std operations #[cfg(test)] mod tests { use super::*; #[test] fn node_type() { - assert!(NodeType::Invalid == NodeType::from(0x00)); - assert!(NodeType::Invalid == NodeType::from(0xF8)); - assert!(NodeType::Chat == NodeType::from(0x01)); - assert!(NodeType::Chat == NodeType::from(0xF9)); - assert!(NodeType::Repeater == NodeType::from(0x02)); - assert!(NodeType::Repeater == NodeType::from(0xFA)); - assert!(NodeType::Room == NodeType::from(0x03)); - assert!(NodeType::Room == NodeType::from(0xFB)); - assert!(NodeType::Sensor == NodeType::from(0x04)); - assert!(NodeType::Sensor == NodeType::from(0xFC)); - assert!(NodeType::Invalid == NodeType::from(0x05)); + assert_eq!(NodeType::Invalid, NodeType::from(0x00)); + assert_eq!(NodeType::Invalid, NodeType::from(0xF8)); + assert_eq!(NodeType::Chat, NodeType::from(0x01)); + assert_eq!(NodeType::Chat, NodeType::from(0xF9)); + assert_eq!(NodeType::Repeater, NodeType::from(0x02)); + assert_eq!(NodeType::Repeater, NodeType::from(0xFA)); + assert_eq!(NodeType::Room, NodeType::from(0x03)); + assert_eq!(NodeType::Room, NodeType::from(0xFB)); + assert_eq!(NodeType::Sensor, NodeType::from(0x04)); + assert_eq!(NodeType::Sensor, NodeType::from(0xFC)); + assert_eq!(NodeType::Invalid, NodeType::from(0x05)); } } \ No newline at end of file diff --git a/src/path.rs b/src/path.rs index 9cb499a..a2eb1c9 100644 --- a/src/path.rs +++ b/src/path.rs @@ -1,11 +1,7 @@ -use std::fmt::{Debug, Display}; use bytes::Bytes; -use structdiff::{Difference, StructDiff}; use crate::packet_content::PeerToPeerCipher; - -#[derive(PartialEq, Debug, Clone, Difference)] -#[difference(expose)] +#[derive(PartialEq, Debug, Clone)] pub struct Path { pub cipher: PeerToPeerCipher, } @@ -18,21 +14,22 @@ impl From for Path { } } -impl Display for Path { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl core::fmt::Display for Path { + 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)) } } +// Tests for std operations #[cfg(test)] mod tests { - use std::str::FromStr; + use core::str::FromStr; use hex::decode; + use tinyvec::array_vec; use crate::{packet::*, packet_content::{PacketContent, PeerToPeerCipher}}; use super::*; - #[test] fn path() { let sample = "2107BA03127F7EA9221351768DD2DF32E1D02F5851379F5AFCC667AB273442FAB2943673F26DDBEB9595027474"; @@ -40,7 +37,7 @@ mod tests { let lhs_packet = Packet { route_type: RouteType::Flood, version: PayloadVersion::VersionOne, - path: vec![0xBA, 0x03, 0x12, 0x7F, 0x7E, 0xA9, 0x22], + path: array_vec!([u16; 64] => 0xBA, 0x03, 0x12, 0x7F, 0x7E, 0xA9, 0x22), transport: [0, 0], raw_content: Bytes::copy_from_slice(&decode("1351768DD2DF32E1D02F5851379F5AFCC667AB273442FAB2943673F26DDBEB9595027474").unwrap()), content: PacketContent::Path(Path { @@ -58,6 +55,7 @@ mod tests { let rhs_packet = Packet::from_str(sample).unwrap(); assert_eq!(lhs_packet, rhs_packet); + #[cfg(feature = "std")] assert_eq!(format!("{}", lhs_packet), " Flood | v1 | | [ba, 03, ... a9, 22] | | PATH | (51) -> (13) MAC: 768d "); } } \ No newline at end of file diff --git a/src/request.rs b/src/request.rs index 400d3b9..fcf6278 100644 --- a/src/request.rs +++ b/src/request.rs @@ -1,18 +1,14 @@ -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)] +#[derive(PartialEq, Debug, Clone)] pub struct Request { pub cipher: PeerToPeerCipher, pub cleartext: Option, } -#[derive(PartialEq, Debug, Clone, Difference)] -#[difference(expose)] +#[derive(PartialEq, Debug, Clone)] pub struct ClearRequest { pub timestamp: DateTime, pub request_type: RequestType, @@ -31,7 +27,7 @@ impl From for ClearRequest { // 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) { + if let Some(timestamp) = DateTime::from_timestamp(bytes.get_u32() as i64, 0) { clear_request.timestamp = timestamp; } @@ -50,7 +46,8 @@ pub enum RequestType { Telemetry, MinMaxAvg, ACL, - Invalid + Invalid, + Neighbors, } impl From for RequestType { @@ -61,19 +58,21 @@ impl From for RequestType { 0x03 => RequestType::Telemetry, 0x04 => RequestType::MinMaxAvg, 0x05 => RequestType::ACL, + 0x06 => RequestType::Neighbors, _ => RequestType::Invalid } } } -impl Display for RequestType { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl core::fmt::Display for RequestType { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::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::Neighbors => f.write_str("NEIGHBORS"), RequestType::Invalid => f.write_str("INVALID"), } } @@ -88,8 +87,8 @@ impl From for Request { } } -impl Display for Request { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl core::fmt::Display for Request { + 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, @@ -107,30 +106,33 @@ impl Display for Request { #[cfg(test)] mod tests { - use std::{collections::HashMap, str::FromStr}; + use std::str::FromStr; use chrono::DateTime; use hex::decode; use bytes::Bytes; - use crate::{identity::KeystoreInput, packet::*, packet_content::{PacketContent, PeerToPeerCipher}, request::{ClearRequest, Request, RequestType}}; + use tinyvec::ArrayVec; + use crate::{std_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"); + assert_eq!(RequestType::from(0x01), RequestType::Stats); + assert_eq!(RequestType::from(0x02), RequestType::Keepalive); + assert_eq!(RequestType::from(0x03), RequestType::Telemetry); + assert_eq!(RequestType::from(0x04), RequestType::MinMaxAvg); + assert_eq!(RequestType::from(0x05), RequestType::ACL); + assert_eq!(RequestType::from(0x06), RequestType::Neighbors); + assert_eq!(RequestType::from(0x07), RequestType::Invalid); + assert_eq!(RequestType::from(0xFF), RequestType::Invalid); + + + assert_eq!(format!("{}", RequestType::Stats), "STATS"); + assert_eq!(format!("{}", RequestType::Keepalive), "KEEP ALIVE"); + assert_eq!(format!("{}", RequestType::Telemetry), "TELEMETRY"); + assert_eq!(format!("{}", RequestType::MinMaxAvg), "MIN/MAX/AVG"); + assert_eq!(format!("{}", RequestType::ACL), "ACL"); + assert_eq!(format!("{}", RequestType::Neighbors), "NEIGHBORS"); + assert_eq!(format!("{}", RequestType::Invalid), "INVALID"); } #[test] @@ -140,7 +142,7 @@ mod tests { let lhs_packet = Packet { route_type: RouteType::Direct, version: PayloadVersion::VersionOne, - path: vec![], + path: ArrayVec::new(), transport: [0, 0], raw_content: Bytes::copy_from_slice(&decode("12341d87ccaac89563cbb39d2333b725e407a1a6").unwrap()), content: PacketContent::Request(Request { @@ -162,25 +164,58 @@ mod tests { let mut rhs_packet = Packet::from_str(sample).unwrap(); - let keystore = KeystoreInput { - identities: HashMap::from([ - ("Sample 1 ID".to_owned(), "4885CF25975EA09742EF76DA587D0957E74EE02AAA34A001458E207E63CF7E6C4940C8C42C335862C71CC2F139633057D1FEE5687B172B27E1E0302A1D480E08".to_owned()), - ("Sample 2 ID".to_owned(), "38DAA98490B7284697C7ADA6175FD1F8DAD12032AD7ABAE625B7EAD8FEC6444CA281C3370B97155D9C8CECD89A929FDDE0FBF3A9D5C92A1B3C24D711934CD69D".to_owned()) - ]), - contacts: HashMap::from([ - ("Sample 1 CT".to_owned(), "34569df1f9661916901669666fb8025eccb9ddb0499cddad4c164fec219c8b8f".to_owned()), - ("Sample 2 CT".to_owned(), "12349bdc1f76a0c12149bb15f791dbe42fde02c209b04a85c6f512990c8cedec".to_owned()), - ]), - groups: HashMap::new() - }.compile(); - - assert!(format!("{}", rhs_packet.content) == " REQUEST | (34) -> (12) MAC: 1d87 ENCRYPTED"); + let file_contents = include_str!("../test_identities_file.toml"); + let keystore_in: KeystoreInput = toml::from_str(file_contents).unwrap(); + let keystore = keystore_in.compile(); + assert_eq!(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_eq!(format!("{}", rhs_packet.content) == " REQUEST | (34) -> (12) MAC: 1d87 at: 2081-09-10 06:54:21 UTC STATS"); - assert_eq!(lhs_packet, rhs_packet); + // assert_eq!(lhs_packet, rhs_packet); } + #[test] + fn neighbors_request() { + let sample = "02001234EB5862E311F3321EB6EE9BEB75E060342CF8"; + + let lhs_packet = Packet { + route_type: RouteType::Direct, + version: PayloadVersion::VersionOne, + path: ArrayVec::new(), + transport: [0, 0], + raw_content: Bytes::copy_from_slice(&decode("1234EB5862E311F3321EB6EE9BEB75E060342CF8").unwrap()), + content: PacketContent::Request(Request { + cipher: PeerToPeerCipher { + destination: 0x12, + source: 0x34, + mac: 0xEB58, + ciphertext: Bytes::copy_from_slice(&decode("62E311F3321EB6EE9BEB75E060342CF8").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::Neighbors, + request_data: Bytes::copy_from_slice( b"\0\n\0\0\0\x04\x07v\x95\xb1\0"), + }) + }), + incomplete: false, + }; + + let mut rhs_packet = Packet::from_str(sample).unwrap(); + + let file_contents = include_str!("../test_identities_file.toml"); + let keystore_in: KeystoreInput = toml::from_str(file_contents).unwrap(); + let keystore = keystore_in.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_eq!(lhs_packet, rhs_packet); + } + } \ No newline at end of file diff --git a/src/response.rs b/src/response.rs index a8ad867..f68e881 100644 --- a/src/response.rs +++ b/src/response.rs @@ -1,13 +1,9 @@ -use std::fmt::Display; -use bytes::Bytes; +use bytes::{Buf, Bytes}; use chrono::{DateTime, Utc}; -use hex::encode; -use structdiff::{Difference, StructDiff}; use crate::packet_content::PeerToPeerCipher; -#[derive(PartialEq, Debug, Clone, Difference)] -#[difference(expose)] +#[derive(PartialEq, Debug, Clone)] pub struct Response { pub(crate) cipher: PeerToPeerCipher, pub cleartext: Option @@ -22,41 +18,77 @@ impl From for Response { } } -#[derive(PartialEq, Debug, Clone, Difference)] -#[difference(expose)] +#[derive(PartialEq, Debug, Clone)] pub struct ClearResponse { pub timestamp: DateTime, - pub response: u8, - pub keepalive_interval: u8, - pub is_admin: bool, - pub permissions: u8, - pub nonce: u32, - pub fw_version: u8 + pub content: ResponseContent, +} + +#[derive(PartialEq, Debug, Clone)] +pub enum ResponseContent { + Stats(RepeaterStats), + Telemetry(Telemetry), + Invalid, +} + +#[repr(C)] +#[derive(PartialEq, Debug, Clone)] +pub struct RepeaterStats { + pub batt_milli_volts: u16, + pub curr_tx_queue_len: u16, + pub noise_floor: i16, + pub last_rssi: i16, + pub n_packets_recv: u32, + pub n_packets_sent: u32, + pub total_air_time_secs: u32, + pub total_up_time_secs: u32, + pub n_sent_flood: u32, + pub n_sent_direct: u32, + pub n_recv_flood: u32, + pub n_recv_direct: u32, + pub err_events: u16, + pub last_snr: i16, + pub n_direct_dups: u16, + pub n_flood_dups: u16, + pub total_rx_air_time_secs: u32, +} + +#[derive(PartialEq, Debug, Clone)] +pub struct Telemetry { + } impl From for ClearResponse { fn from(value: Bytes) -> Self { - let mut bytes = value; + let mut bytes = value.clone(); let mut clear_response = ClearResponse { timestamp: DateTime::from_timestamp(0, 0).unwrap(), - response: 0, - keepalive_interval: 0, - is_admin: false, - permissions: 0, - nonce: 0, - fw_version: 0, + content: ResponseContent::Invalid }; - println!("{} bytes: {}", bytes.len(), encode(&bytes)); + if bytes.len() < 4 { return clear_response } + if let Some(timestamp) = DateTime::from_timestamp(bytes.get_u32_le() as i64, 0) { + clear_response.timestamp = timestamp; + } + + // Unfortunately, there's no way to know for sure what kind of response this is, + // so we have to make some educated guesses. In particular, it seems like all + // telemetry responses start with voltage on channel 1. So, if the first two bytes + // are exactly 0x0174 then we'll assume it's a telemetry packet. + // let maybe_cayenne = bytes.clone().get_u16(); + // if maybe_cayenne == 0x0174 { + // println!("CayenneLPP: {} bytes: {}", bytes.len(), encode(&bytes)); + // } else { + // println!("{} bytes: {}", value.len(), encode(&value)); + // } - clear_response } } -impl Display for Response { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl core::fmt::Display for Response { + 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, @@ -74,10 +106,10 @@ impl Display for Response { #[cfg(test)] mod tests { use std::str::FromStr; - use bytes::Bytes; use hex::decode; - use crate::{identity::KeystoreInput, packet::*, packet_content::{PacketContent, PeerToPeerCipher}, response::Response}; + use tinyvec::ArrayVec; + use crate::{std_identity::KeystoreInput, packet::*, packet_content::{PacketContent, PeerToPeerCipher}, response::Response}; #[test] fn response_encrypted() { @@ -86,7 +118,7 @@ mod tests { let lhs_packet = Packet { route_type: RouteType::Direct, version: PayloadVersion::VersionOne, - path: vec![], + path: ArrayVec::new(), transport: [0, 0], raw_content: Bytes::copy_from_slice(&decode("3412b9000cf1641739f7e4d49bff88bf5695b304111b15277de3a6031b9af3a2b6371cf75615ffefe7dcc0bbea2856c4e798a72d5b989d6de1aa646c1e2eef4cf13e6f92").unwrap()), content: PacketContent::Response(Response { @@ -114,7 +146,7 @@ mod tests { let lhs_packet = Packet { route_type: RouteType::Direct, version: PayloadVersion::VersionOne, - path: vec![], + path: ArrayVec::new(), transport: [0, 0], raw_content: Bytes::copy_from_slice(&decode("3412b9000cf1641739f7e4d49bff88bf5695b304111b15277de3a6031b9af3a2b6371cf75615ffefe7dcc0bbea2856c4e798a72d5b989d6de1aa646c1e2eef4cf13e6f92").unwrap()), content: PacketContent::Response(Response { @@ -140,4 +172,133 @@ mod tests { // assert_eq!(lhs_packet, rhs_packet); // assert_eq!(format!("{}", lhs_packet), " Direct | v1 | | [] | | RESPONSE | (12) -> (34) MAC: b900 ENCRYPTED") } + + #[test] + fn stats_response() { + let sample = "06003412CF45D4856713A44CA5411C327DD96575CF632F626C5B17046BECC220B9E476362B9AC510637F3CC68160E263F8D9181A03965B989D6DE1AA646C1E2EEF4CF13E6F92"; + + 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 lhs_packet = Packet { + route_type: RouteType::Direct, + version: PayloadVersion::VersionOne, + path: ArrayVec::new(), + 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: Some(Bytes::copy_from_slice(&decode("c1cc1b69c8100000a7fff7ff1c000000160000000400000065060000050000001100000005000000170000000000300000000200040000000000000000000000").unwrap())) + }, + cleartext: None}), + incomplete: false, + }; + + let mut rhs_packet = Packet::from_str(sample).unwrap(); + rhs_packet.try_decrypt(&keystore); + println!("{:#?}", rhs_packet); + // assert_eq!(lhs_packet, rhs_packet); + } + + #[test] + fn acl_response() { + let sample = "06003412DE04889E2387AC81CC38108EF3E575B4B61FFA6AB9573DBAF589891F79D6C20FA33B6CE5CD672EE982F6DAE79DD8BC83F648"; + + 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 lhs_packet = Packet { + route_type: RouteType::Direct, + version: PayloadVersion::VersionOne, + path: ArrayVec::new(), + 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: Some(Bytes::copy_from_slice(&decode("efcc1b6987449a393ea60355ebee904f160398fe6b93288b0312349bdc1f760334569df1f96603000000000000000000").unwrap())) + }, + cleartext: None}), + incomplete: false, + }; + + let mut rhs_packet = Packet::from_str(sample).unwrap(); + rhs_packet.try_decrypt(&keystore); + println!("{:#?}", rhs_packet); + // assert_eq!(lhs_packet, rhs_packet); + } + + #[test] + fn unknown_response() { + let sample = "06003412879A1045F0290639ED9B6D4BB0B51C14E13D"; + + 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 lhs_packet = Packet { + route_type: RouteType::Direct, + version: PayloadVersion::VersionOne, + path: ArrayVec::new(), + 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: Some(Bytes::copy_from_slice(&decode("f6cc1b69000000000000000000000000").unwrap())) + }, + cleartext: None}), + incomplete: false, + }; + + let mut rhs_packet = Packet::from_str(sample).unwrap(); + rhs_packet.try_decrypt(&keystore); + println!("{:#?}", rhs_packet); + // assert_eq!(lhs_packet, rhs_packet); + } + + + #[test] + fn telemetry_response() { + let sample = "06003412187C3AE03E52D347B957D634221DB5E86815"; + + 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 lhs_packet = Packet { + route_type: RouteType::Direct, + version: PayloadVersion::VersionOne, + path: ArrayVec::new(), + 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: Some(Bytes::copy_from_slice(&decode("fdcc1b69017401ad0000000000000000").unwrap())) + }, + cleartext: None}), + incomplete: false, + }; + + let mut rhs_packet = Packet::from_str(sample).unwrap(); + rhs_packet.try_decrypt(&keystore); + println!("{:#?}", rhs_packet); + // assert_eq!(lhs_packet, rhs_packet); + } } \ No newline at end of file diff --git a/src/std_identity.rs b/src/std_identity.rs new file mode 100644 index 0000000..fdc8dff --- /dev/null +++ b/src/std_identity.rs @@ -0,0 +1,469 @@ +use std::{collections::{HashMap, HashSet}, rc::Rc, str::FromStr}; +use bytes::Bytes; +use log::warn; +use crate::{crypto::{PrivateKey, PublicKey, SharedSecret}}; + +use serde::{Deserialize, de}; + +#[derive(PartialEq, Clone, Deserialize, Debug)] +/// The Identity structure contains the information to decrypt +/// incoming messages and sign and encrypt outgoing messages. +/// +/// It has the necessary cryptographic material needed to appear +/// as this identity when creating messages. +pub struct Identity { + /// The display name if this user identity. + pub name: Rc, + + /// The private key of this identity. There are currently + /// 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")] + 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)] + pub public_key: PublicKey, + + /// The secrets has is a collection of shared secrets + /// indexed by the public key of the other side of the + /// 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)] + pub secrets: HashSet<(Rc, SharedSecret)> +} + +#[derive(PartialEq, Clone, Deserialize, Debug)] +/// The Contact structure contains the information needed +/// to decrypt messages from a remote user intended for +/// either an identity or a channel. +/// +/// It's cryptographic material is also needed to uniquely +/// identify the origin of a message. Most source values +/// are only one byte, so that information just limits the +/// number of entries in the contact list that must be searched +/// before the correct contact is found. +pub struct Contact { + /// The display name of the contact + pub name: Rc, + + /// The provided public key of the remote contact + #[serde(deserialize_with = "deserialize_public_key")] + pub public_key: PublicKey +} + +#[derive(PartialEq, Clone, Deserialize, Debug)] +/// 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 +/// contact in the sense that it's full identity is discovered +/// by whether its secret can correctly decrypt messages. +pub struct Group { + /// The display name of the group + pub name: Rc, + + /// The group's shared secret + #[serde(deserialize_with = "deserialize_secret")] + pub secret: SharedSecret +} + +#[derive(PartialEq, Clone, Deserialize, Debug)] +pub struct Keystore { + pub identities: HashMap, Identity>, + pub contacts: HashMap, Contact>, + pub groups: HashMap, Group>, +} + +impl Keystore { + pub fn identity_by_name(&self, name: &str) -> Option { + if let Some(identity) = self.identities.get(&Rc::new(name.to_owned())) { + Some(identity.clone()) + } else { + None + } + } + + pub fn decrypt_and_id_p2p(&self, _source: u8, _dest: u8, mac: u16, data: &Bytes) -> Option<(Bytes, Rc, Rc)> { + // Just brute-force all the secrets until one matches... + for identity in self.identities.iter() { + for secret in identity.1.secrets.iter() { + let result = secret.1.mac_then_decrypt(mac, data); + if let Some(result) = result { + return Some((result, identity.0.clone(), secret.0.clone())); + } + } + } + None + } + + pub fn decrypt_and_id_group(&self, _group_hash_prefix: u8, mac: u16, data: &Bytes) -> Option<(Bytes, u32)> { + for group in self.groups.iter() { + let result = group.1.secret.mac_then_decrypt(mac, data); + if let Some(result) = result { + return Some((result, 0)); + } + } + + None + } + + pub fn decrypt_anon(&self, _dest: u8, pub_key: &PublicKey, mac: u16, data: &Bytes) -> Option<(Bytes, Rc)> { + // For each identity, create a shared secret against the provided public key and check it. + for identity in self.identities.iter() { + let secret = identity.1.private_key.create_secret(pub_key); + let result = secret.mac_then_decrypt(mac, data); + if let Some(result) = result { + return Some((result, identity.0.clone())); + } + } + None + } +} + +#[derive(PartialEq, Debug, Clone, Deserialize)] +pub struct KeystoreInput { + pub identities: HashMap, + pub contacts: HashMap, + pub groups: HashMap, +} + +impl KeystoreInput { + pub fn compile(self) -> Keystore { + let mut retval = Keystore { + identities: HashMap::new(), + contacts: HashMap::new(), + groups: HashMap::new() + }; + + // Iterate through the input keystore file + // for each one, make sure it's in the map correctly + // and use the opportunity to compute the shared keys. + + // Unfortunately, for testing, we really need this to be + // sorted so we can ensure that the order of values + // is in the same order every time. + let mut contact_names: Vec<&String> = self.contacts.keys().collect(); + contact_names.sort(); + let mut contacts: Vec = vec![]; + for name in contact_names { + if let Some(key_string) = self.contacts.get(name) { + match PublicKey::from_str(&key_string) { + Ok(pub_key) => { + contacts.push(Contact { + name: Rc::new(name.to_string()), + public_key: pub_key + }); + }, + Err(e) => { + warn!("Unable to add contact named \"{}\" because there was a problem with the public key: {}", name, e); + } + } + } + } + + let mut identity_names: Vec<&String> = self.identities.keys().collect(); + identity_names.sort(); + let mut identities: Vec<(Rc, Identity)> = vec![]; + for name in identity_names { + if let Some(key_string) = self.identities.get(name) { + match PrivateKey::from_str(&key_string) { + Ok(private_key) => { + let public_key = PublicKey::from(&private_key); + let mut i = Identity { + name: Rc::new(name.to_string()), + private_key, + public_key, + secrets: HashSet::new() + }; + + i.secrets = contacts.iter().map(|c| { + (c.name.clone(), i.private_key.create_secret(&c.public_key)) + }).collect(); + + + identities.push((i.name.clone(), i)); + }, + Err(e) => { + warn!("Unable to add identity named \"{}\" because there was a problem with the private key: {}", name, e); + } + } + } + } + + retval.identities = HashMap::from_iter(identities); + + // Create a hash of contacts + let contacts = contacts.into_iter().map(|c| { + (c.name.clone(), c) + }); + + retval.contacts = HashMap::from_iter(contacts); + + // Create a hash of groups + let groups = self.groups.into_iter().filter_map(|(name, secret)| { + let name = Rc::new(name); + match SharedSecret::from_str(&secret) { + Ok(secret) => { + Some((name.clone(), Group { name: name.clone(), secret })) + } + Err(e) => { + warn!("Unable to add group named \"{}\" because there was a problem with the secret: {}", name, e); + None + } + } + }); + + retval.groups = HashMap::from_iter(groups); + + retval + } +} + +fn deserialize_private_key<'de, D>(deserializer: D) -> Result + 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), + Err(e) => { + Err(de::Error::custom(format!("{}", e))) + } + } +} + +fn deserialize_public_key<'de, D>(deserializer: D) -> Result + where D: de::Deserializer<'de> { + + use hex::decode; + use crate::crypto::{MeshcoreCryptoError, PublicKey}; + + let s: String = de::Deserialize::deserialize(deserializer)?; + + match decode(s) { + Err(hex::FromHexError::InvalidHexCharacter { c, index }) => { + Err(de::Error::custom(format!("Invalid hex character {} at {} when decoding Public Key.", c, index))) + }, + Err(hex::FromHexError::OddLength) => { + Err(de::Error::custom(format!("Odd number of characters when decoding Public Key."))) + }, + Err(hex::FromHexError::InvalidStringLength) => { + Err(de::Error::custom(format!("Invalid hex string length when decoding Public Key."))) + } + Ok(hex) => { + let key = PublicKey::try_from(hex.as_slice()); + match key { + Ok(key) => Ok(key), + Err(MeshcoreCryptoError::KeyCreationError) => { + Err(de::Error::custom(format!("Unable to decompress public key points"))) + } + Err(_) => { + Err(de::Error::custom(format!("Invalid hex string length when decoding Public Key."))) + } + } + } + } +} + +fn deserialize_secret<'de, D>(deserializer: D) -> Result + 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) { + Ok(secret) => Ok(secret), + Err(e) => { + Err(de::Error::custom(format!("{}", e))) + } + } +} + +// Tests for std operations +#[cfg(test)] +mod tests { + use std::io; + use std::str::FromStr; + use std::sync::mpsc::{Sender, channel}; + use std::vec; + use hex::decode; + use log::{LevelFilter}; + use serde::de::IntoDeserializer; + use serde::de::value::{StrDeserializer, Error as ValueError}; + + use crate::std_identity::KeystoreInput; + use crate::crypto::*; + use super::*; + + #[test] + fn secret_deserializer() { + let deserializer: StrDeserializer = "eb50a1bcb3e4e5d7bf69a57c9dada211".into_deserializer(); + let result = deserialize_secret(deserializer); + match result { + Ok(result) => assert_eq!(result, SharedSecret::from_str("eb50a1bcb3e4e5d7bf69a57c9dada211").unwrap()), + Err(err) => assert!(false, "Shouldn't have had an error, but got one: {}", err), + } + + // Test failure conditions + let deserializer: StrDeserializer = "eb50a1bcb3e4e5d7bf69a57c9dada21".into_deserializer(); + let result = deserialize_secret(deserializer); + let error = result.unwrap_err(); + assert_eq!(error.to_string(), "Try From Slice Error", "Unexpected error response: {}", error.to_string()); + } + + #[test] + fn public_key_deserializer() { + let deserializer: StrDeserializer = "34569df1f9661916901669666fb8025eccb9ddb0499cddad4c164fec219c8b8f".into_deserializer(); + let result = deserialize_public_key(deserializer); + match result { + Ok(result) => assert_eq!(result, PublicKey::from_str("34569df1f9661916901669666fb8025eccb9ddb0499cddad4c164fec219c8b8f").unwrap()), + Err(err) => assert!(false, "Shouldn't have had an error, but got one: {}", err), + } + + // Test failure conditions + let deserializer: StrDeserializer = "34569df1f9661916901669666fb8025eccb9ddb0499cddad4c164fec219".into_deserializer(); + let result = deserialize_public_key(deserializer); + let error = result.unwrap_err(); + assert_eq!(error.to_string(), "Odd number of characters when decoding Public Key.", "Unexpected error response: {}", error.to_string()); + + let deserializer: StrDeserializer = "34569df1f9661916901669666fb8025eccb9ddb0499cddad4c164fec21".into_deserializer(); + let result = deserialize_public_key(deserializer); + let error = result.unwrap_err(); + assert_eq!(error.to_string(), "Invalid hex string length when decoding Public Key.", "Unexpected error response: {}", error.to_string()); + + let deserializer: StrDeserializer = "34569df1f9661916901669666fb8025eccb9ddb0499cddad4b164f4c219a8b8f".into_deserializer(); + let result = deserialize_public_key(deserializer); + let error = result.unwrap_err(); + assert_eq!(error.to_string(), "Unable to decompress public key points", "Unexpected error response: {}", error.to_string()); + + let deserializer: StrDeserializer = "34569df1f9661916901669666fb8z25eccb9ddb0499cddad4b164f4c219a8b8f".into_deserializer(); + let result = deserialize_public_key(deserializer); + let error = result.unwrap_err(); + assert_eq!(error.to_string(), "Invalid hex character z at 28 when decoding Public Key.", "Unexpected error response: {}", error.to_string()); + } + + #[test] + fn private_key_deserializer() { + let deserializer: StrDeserializer = "4885CF25975EA09742EF76DA587D0957E74EE02AAA34A001458E207E63CF7E6C4940C8C42C335862C71CC2F139633057D1FEE5687B172B27E1E0302A1D480E08".into_deserializer(); + let result = deserialize_private_key(deserializer); + match result { + Ok(result) => assert_eq!(result, PrivateKey::from_str("4885CF25975EA09742EF76DA587D0957E74EE02AAA34A001458E207E63CF7E6C4940C8C42C335862C71CC2F139633057D1FEE5687B172B27E1E0302A1D480E08").unwrap()), + Err(err) => assert!(false, "Shouldn't have had an error, but got one: {}", err), + } + + // Test failure conditions + let deserializer: StrDeserializer = "4885CF25975EA09742EF76DA587D0957E74EE02AAA34A001458E207E63CF7E6C4940C8C42C335862C71CC2F139633057D1FEE5687B172B27E1E0302A1D480E0".into_deserializer(); + let result = deserialize_private_key(deserializer); + let error = result.unwrap_err(); + assert_eq!(error.to_string(), "Hex Decode Error", "Unexpected error response: {}", error.to_string()); + + let deserializer: StrDeserializer = "4885CF25975EA09742EF76DA587D0957E74EE02AAA34A001458E207E63CF7E6C4940C8C42C335862C71CC2F139633057D1FEE5687B172B27E1E0302A1D480E".into_deserializer(); + let result = deserialize_private_key(deserializer); + let error = result.unwrap_err(); + assert_eq!(error.to_string(), "Try From Slice Error", "Unexpected error response: {}", error.to_string()); + } + + #[test] + fn deserialize_toml() { + let file_contents = include_str!("../test_identities_file.toml"); + let keystore_in: KeystoreInput = toml::from_str(file_contents).unwrap(); + let keystore = keystore_in.compile(); + + println!("{:#?}", keystore.identity_by_name("Sample 1 ID").unwrap()); + + let lhs = keystore.identity_by_name("Sample 1 ID").unwrap(); + + let rhs = Identity { + name: Rc::new("Sample 1 ID".to_owned()), + private_key: PrivateKey::from_str("4885CF25975EA09742EF76DA587D0957E74EE02AAA34A001458E207E63CF7E6C4940C8C42C335862C71CC2F139633057D1FEE5687B172B27E1E0302A1D480E08").unwrap(), + public_key: PublicKey::from_str("34569df1f9661916901669666fb8025eccb9ddb0499cddad4c164fec219c8b8f").unwrap(), + secrets: HashSet::from([ + (Rc::new("Sample 1 CT".to_owned()), SharedSecret::try_from(Bytes::copy_from_slice(&decode("15f857c60a0672e999fa20f022ffc7d5f8c7a1a1808868273aa7d6caa772f818").unwrap())).unwrap()), + (Rc::new("Sample 2 CT".to_owned()), SharedSecret::try_from(Bytes::copy_from_slice(&decode("eb7a365363bd8548ee2b54b9234247be5e42e96be9625adcdf3a55b6c1d04850").unwrap())).unwrap()), + (Rc::new("Sample 3 CT".to_owned()), SharedSecret::try_from(Bytes::copy_from_slice(&decode("87c4bbfc210ef8c6eda4ab46ac40e27d4a9f7c649df63d932ccb94279e26a549").unwrap())).unwrap()), + (Rc::new("Sample 4 CT".to_owned()), SharedSecret::try_from(Bytes::copy_from_slice(&decode("02237f72fcd9405ee890b3d706800d1fd4e44005552c00252c3f3a9726c46d13").unwrap())).unwrap()), + (Rc::new("Sample 5 CT".to_owned()), SharedSecret::try_from(Bytes::copy_from_slice(&decode("d7c2916d671ee530ce7acba8b235414cce5b6e5b9079e714b77179359b2f5d4c").unwrap())).unwrap()), + ]) + }; + assert_eq!(lhs, rhs); + + } + + // This struct is used as an adaptor, it implements io::Write and forwards the buffer to a mpsc::Sender + struct WriteAdapter { + sender: Sender, + } + + impl io::Write for WriteAdapter { + // On write we forward each u8 of the buffer to the sender and return the length of the buffer + fn write(&mut self, buf: &[u8]) -> io::Result { + for chr in buf { + self.sender.send(*chr).unwrap(); + } + Ok(buf.len()) + } + + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } + } + + #[test] + fn deserialize_toml_errors() { + let (rx, tx) = channel(); + let expect: Vec<&str> = vec![ + "Hex Decode Error", + "Key Length Error", + "Key Creation Error", + "Hex Decode Error", + "Hex Decode Error", + "Try From Slice Error", + "Try From Slice Error", + ]; + + let _ = pretty_env_logger::env_logger::builder() + .is_test(true) + .filter_level(LevelFilter::max()) + .target(pretty_env_logger::env_logger::Target::Pipe(Box::new(WriteAdapter { sender: rx }))) + .try_init(); + + // Test failure conditions + let contents = r#" +[identities] +"1 Hex Decode Error" = "4885CF25975EA09742EF76DA587D0957E74EE02AAA34A001458E207E63CF7E6C4940C8C42C335862C71CC2F139633057D1FEE5687B172B27E1E0302A1D480E0" +"2 Slice Error" = "4885CF25975EA09742EF76DA587D0957E74EE02AAA34A001458E207E63CF7E6C4940C8C42C335862C71CC2F139633057D1FEE5687B172B27E1E0302A1D480E" +[contacts] +"1 Odd number of characters" = "34569df1f9661916901669666fb8025eccb9ddb0499cddad4c164fec219" +"2 Invalid hex length" = "34569df1f9661916901669666fb8025eccb9ddb0499cddad4c164fec21" +"3 Unable to decompress" = "34569df1f9661916901669666fb8025eccb9ddb0499cddad4b164f4c219a8b8f" +"4 Invalid hex character" = "34569df1f9661916901669666fb8z25eccb9ddb0499cddad4b164f4c219a8b8f" +[groups] +"Slice error" = "eb50a1bcb3e4e5d7bf69a57c9dada21" + "#; + + let keystore_in = toml::from_str::(contents).unwrap(); + let _keystore = keystore_in.compile(); + + String::from_utf8(tx.try_iter().collect::>()) + .unwrap() + .split('\n') + .into_iter() + .zip(expect.into_iter()) + .for_each(|(got, expect)| { + println!("{}", &got); + assert!(got.contains(expect), "Got log line: \"{}\" and didn't find \"{}\" within it as expected.", got, expect); + } + ); + + } + + +} \ No newline at end of file diff --git a/src/string_helper.rs b/src/string_helper.rs new file mode 100644 index 0000000..e5af42b --- /dev/null +++ b/src/string_helper.rs @@ -0,0 +1,70 @@ +#[cfg(not(feature = "std"))] +use arraystring::{ArrayString, typenum::{U32, U160}}; + +#[cfg(feature = "std")] +pub(crate) type NameString = String; + +#[cfg(feature = "std")] +pub(crate) fn name_string_from_slice(s: &[u8]) -> NameString { + match String::from_utf8(s.to_vec()) { + Ok(s) => s, + Err(_) => "Unable to create string".to_string() + } +} + +#[cfg(not(feature = "std"))] +pub(crate) type NameString = ArrayString; + +#[cfg(not(feature = "std"))] +pub(crate) fn name_string_from_slice(s: &[u8]) -> NameString { + NameString::from_str_truncate(s) +} + +#[cfg(feature = "std")] +pub(crate) type MessageString = String; + +#[cfg(not(feature = "std"))] +pub(crate) type MessageString = ArrayString; + +#[cfg(feature = "std")] +pub(crate) fn message_string_from_slice(s: &[u8]) -> MessageString { + let mut split = s.split(|c| *c == 0); + + if let Some(s) = split.next() { + match String::from_utf8(s.to_vec()) { + Ok(s) => { + s.trim().to_string() + } + Err(_) => "Unable to create string".to_string() + } + } else { + "".to_string() + } +} + +#[cfg(not(feature = "std"))] +pub(crate) fn message_string_from_slice(s: &[u8]) -> MessageString { + MessageString::from_str_truncate(s) +} + +#[cfg(feature = "std")] +pub(crate) type PasswordString = String; + +// I believe that a password can only be 141 characters, +// but given it's close enough to 160, let's just round +// up so it's the same as MessageString. +#[cfg(not(feature = "std"))] +pub(crate) type PasswordString = ArrayString; + +#[cfg(feature = "std")] +pub(crate) fn password_string_from_slice(s: &[u8]) -> PasswordString { + match String::from_utf8(s.to_vec()) { + Ok(s) => s, + Err(_) => "".to_string() + } +} + +#[cfg(not(feature = "std"))] +pub(crate) fn password_string_from_slice(s: &[u8]) -> PasswordString { + PasswordString::from_str_truncate(s) +} diff --git a/src/text.rs b/src/text.rs index 4b6b235..df02822 100644 --- a/src/text.rs +++ b/src/text.rs @@ -1,15 +1,17 @@ -use std::{fmt::Display, rc::Rc}; use bytes::{Buf, Bytes}; use chrono::{DateTime, Utc}; -use structdiff::{Difference, StructDiff}; +use crate::string_helper::{MessageString, NameString, message_string_from_slice, name_string_from_slice}; #[cfg(feature = "std")] -use hex::encode; +use crate::{packet_content::PeerToPeerCipher}; -use crate::{identity::Keystore, packet_content::PeerToPeerCipher}; +#[cfg(not(feature = "std"))] +use crate::no_std_identity::Keystore; -#[derive(PartialEq, Debug, Clone, Difference)] -#[difference(expose)] +#[cfg(feature = "std")] +use crate::std_identity::Keystore; + +#[derive(PartialEq, Debug, Clone)] pub struct Text { pub cipher: PeerToPeerCipher, pub cleartext: Option, @@ -24,8 +26,8 @@ impl From for Text { } } -impl Display for Text { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +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, @@ -33,24 +35,11 @@ impl Display for Text { ))?; 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")) - ) - } - + f.write_fmt(format_args!("at: {}, {} attempts, from {}: {}", + cleartext.timestamp, + cleartext.attempts, + cleartext.sender, + cleartext.message.replace("\n", "\\n"))) } else { f.write_str("ENCRYPTED") } @@ -58,17 +47,16 @@ impl Display for Text { } -#[derive(PartialEq, Debug, Clone, Difference)] -#[difference(expose)] +#[derive(PartialEq, Debug, Clone)] pub struct ClearText { pub timestamp: DateTime, pub message_type: MessageType, pub attempts: u8, pub sender_hash: u32, - pub sender: Option, - pub message: String, + pub sender: NameString, + pub message: MessageString, - pub crypto_recipient: Rc + pub crypto_recipient: u32, // Hash of the sender's public key } impl From for ClearText { @@ -80,9 +68,9 @@ impl From for ClearText { message_type: MessageType::Incomplete, attempts: 0, sender_hash: 0, - sender: None, - message: "".to_string(), - crypto_recipient: Rc::new("".to_string()), + sender: NameString::new(), + message: MessageString::new(), + crypto_recipient: 0, }; // Just check for the whole fixed-size part at once @@ -100,16 +88,32 @@ impl From for ClearText { 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(); + // 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. - // 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(); + 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 @@ -127,14 +131,13 @@ impl From for GroupData { } } -impl Display for GroupData { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_fmt(format_args!("Payload: {}", encode(&self.payload))) +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, Debug, Clone, Difference)] -#[difference(expose)] +#[derive(PartialEq, Clone, core::fmt::Debug)] pub struct GroupText { hash: u8, mac: u16, @@ -169,6 +172,7 @@ impl From for GroupText { } impl GroupText { + #[cfg(feature = "std")] pub fn try_decrypt(&mut self, keysore: &Keystore) -> bool { let decrypt_result = keysore.decrypt_and_id_group( self.hash, @@ -178,7 +182,7 @@ impl GroupText { if let Some((cleartext, group)) = decrypt_result { let mut cleartext = ClearText::from(cleartext); - cleartext.crypto_recipient = group.name.clone(); + cleartext.crypto_recipient = group; self.cleartext = Some(cleartext); true } else { @@ -187,14 +191,17 @@ impl GroupText { } } -impl Display for GroupText { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +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, - cleartext.message.replace("\n", "\\n")) + message) ) } else { f.write_fmt(format_args!("({:2x?}) Mac: {:4x?} ENCRYPTED", self.hash, self.mac)) @@ -222,14 +229,17 @@ impl From for MessageType { } } +// Tests for std operations +#[cfg(feature = "std")] #[cfg(test)] mod tests { - use std::{collections::HashMap, rc::Rc, str::FromStr}; + use std::{collections::HashMap, str::FromStr}; use chrono::DateTime; use hex::decode; use bytes::Bytes; - use crate::{identity::KeystoreInput, packet::*, packet_content::{PacketContent, PeerToPeerCipher}, text::{ClearText, GroupData, GroupText, MessageType, Text}}; + use tinyvec::{ArrayVec, array_vec}; + 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}}; #[test] fn text_encrypted() { @@ -238,7 +248,7 @@ mod tests { let lhs_packet = Packet { route_type: RouteType::Direct, version: PayloadVersion::VersionOne, - path: vec![], + path: ArrayVec::new(), transport: [0, 0], raw_content: Bytes::copy_from_slice(&decode("1234e91c8eb2b815e0eccf6781a3ff1820d0fb130fcfc87b914244fae227d4ad4c752fb9").unwrap()), content: PacketContent::Text(Text { @@ -266,7 +276,7 @@ mod tests { let lhs_packet = Packet { route_type: RouteType::Direct, version: PayloadVersion::VersionOne, - path: vec![], + path: ArrayVec::new(), transport: [0, 0], raw_content: Bytes::copy_from_slice(&decode("1234e91c8eb2b815e0eccf6781a3ff1820d0fb130fcfc87b914244fae227d4ad4c752fb9").unwrap()), content: PacketContent::Text(Text { @@ -283,9 +293,9 @@ mod tests { message_type: MessageType::Plain, attempts: 0, sender_hash: 0, - sender: None, - message: "01|get radio".to_string(), - crypto_recipient: "".to_string().into(), + sender: name_string_from_slice(b"Unknown"), + message: message_string_from_slice(b"01|get radio"), + crypto_recipient: 0, } ), }), @@ -299,7 +309,7 @@ mod tests { 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 to : 01|get radio"); + 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] @@ -309,7 +319,7 @@ mod tests { 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], + 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 { @@ -334,7 +344,7 @@ mod tests { let lhs_packet = Packet { route_type: RouteType::Flood, version: PayloadVersion::VersionOne, - path: vec![], + path: ArrayVec::new(), transport: [0, 0], raw_content: Bytes::copy_from_slice(&decode("11C3C1354D619BAE9590E4D177DB7EEAF982F5BDCF78005D75157D9535FA90178F785D").unwrap()), content: PacketContent::GroupText(GroupText { @@ -342,13 +352,13 @@ mod tests { mac: 0xC3C1, ciphertext: Bytes::copy_from_slice(&decode("354D619BAE9590E4D177DB7EEAF982F5BDCF78005D75157D9535FA90178F785D").unwrap()), cleartext: Some(ClearText { - sender: Some("🌲 Tree".to_owned()), - message: "☁️".to_owned(), + 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: Rc::new("Public".to_owned()), + crypto_recipient: 0, }), incomplete: false }), @@ -368,7 +378,7 @@ mod tests { _ = rhs_packet.try_decrypt(&keystore); assert_eq!(lhs_packet, rhs_packet); - assert_eq!(format!("{}", lhs_packet), " Flood | v1 | | [] | | GROUP TXT | (11) Mac: c3c1 Group Public: ☁\u{fe0f}"); + assert_eq!(format!("{}", lhs_packet), " Flood | v1 | | [] | | GROUP TXT | (11) Mac: c3c1 Group 0: ☁\u{fe0f}"); } #[test] @@ -378,7 +388,7 @@ mod tests { 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], + 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 { diff --git a/src/trace.rs b/src/trace.rs index 81b2877..3770702 100644 --- a/src/trace.rs +++ b/src/trace.rs @@ -1,17 +1,15 @@ -use std::fmt::{Debug, Display}; use bytes::{Buf, Bytes}; -use structdiff::{Difference, StructDiff}; +use tinyvec::ArrayVec; -#[derive(PartialEq, Debug, Clone, Difference)] -#[difference(expose)] +#[derive(PartialEq, Debug, Clone)] pub struct Trace { tag: u32, auth: u32, flags: u8, - pub(crate) path_snr: Vec, + pub(crate) path_snr: ArrayVec<[f32; 64]>, invalid: bool, - pub(crate) temp_path: Vec + pub(crate) temp_path: ArrayVec<[u16; 64]> } impl From for Trace { @@ -22,9 +20,9 @@ impl From for Trace { tag: 0, auth: 0, flags: 0, - path_snr: vec![], + path_snr: ArrayVec::new(), invalid: true, - temp_path: vec![] + temp_path: ArrayVec::new(), }; if bytes.len() < 10 { return trace; } @@ -32,14 +30,18 @@ impl From for Trace { trace.tag = bytes.get_u32(); trace.auth = bytes.get_u32(); trace.flags = bytes.get_u8(); - trace.temp_path = bytes.iter().copied().collect(); + + for byte in bytes.iter() { + trace.temp_path.push(*byte as u16); + } + trace.invalid = false; trace } } -impl Display for Trace { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +impl core::fmt::Display for Trace { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { for (snr, hash) in self.path_snr.iter().zip(self.temp_path.clone()) { f.write_fmt(format_args!("({:2x?}): {}dB ", hash, snr))?; } @@ -48,10 +50,12 @@ impl Display for Trace { } } +// Tests for std operations #[cfg(test)] mod tests { - use std::str::FromStr; + use core::str::FromStr; use hex::decode; + use tinyvec::array_vec; use crate::{packet::*, packet_content::PacketContent}; use super::*; @@ -64,16 +68,16 @@ mod tests { let lhs_packet = Packet { route_type: RouteType::Direct, version: PayloadVersion::VersionOne, - path: vec![0xAC, 0xA0, 0x79], + path: array_vec!([u16; 64] => 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], + path_snr: array_vec!([f32; 64] => 11.0, -8.0, -3.75), invalid: false, - temp_path: vec![0xAC, 0xA0, 0x79] + temp_path: array_vec!([u16; 64] => 0xAC, 0xA0, 0x79), } ), incomplete: false @@ -81,6 +85,9 @@ mod tests { let rhs_packet = Packet::from_str(sample).unwrap(); assert_eq!(lhs_packet, rhs_packet); + + // We can only check the description in std mode + #[cfg(feature = "std")] assert_eq!(format!("{}", rhs_packet), " Direct | v1 | | [ac, a0, 79] | | TRACE | (ac): 11dB (a0): -8dB (79): -3.75dB ") } -- cgit v1.2.3