diff options
| author | Will Dillon <william@housedillon.com> | 2025-11-13 18:21:18 +0000 |
|---|---|---|
| committer | Will Dillon <william@housedillon.com> | 2025-11-13 18:21:18 +0000 |
| commit | 6dd5ee6c774e470e6837d63780c6a2660e8538fc (patch) | |
| tree | 8249c65d9b702e05b598af03f317f40ca2581838 /src | |
| parent | Refactored packet_content and added way more tests (diff) | |
| download | meshcore-rs-6dd5ee6c774e470e6837d63780c6a2660e8538fc.tar.gz meshcore-rs-6dd5ee6c774e470e6837d63780c6a2660e8538fc.zip | |
Updated TOML formate and added test
Diffstat (limited to 'src')
| -rw-r--r-- | src/anon_req.rs | 41 | ||||
| -rw-r--r-- | src/crypto.rs | 23 | ||||
| -rw-r--r-- | src/identity.rs | 139 | ||||
| -rw-r--r-- | src/packet.rs | 5 | ||||
| -rw-r--r-- | src/request.rs | 38 | ||||
| -rw-r--r-- | src/text.rs | 17 |
6 files changed, 188 insertions, 75 deletions
diff --git a/src/anon_req.rs b/src/anon_req.rs index 2f449a7..b3e8d0c 100644 --- a/src/anon_req.rs +++ b/src/anon_req.rs @@ -93,8 +93,10 @@ impl From<Bytes> for ClearAnonRequest { #[cfg(test)] mod tests { + use std::collections::HashMap; use std::str::FromStr; use hex::decode; + use crate::identity::KeystoreInput; use crate::packet::*; use crate::crypto::*; use crate::packet_content::PacketContent; @@ -102,27 +104,52 @@ mod tests { #[test] fn anon_req() { - let sample = "1d001234569df1f9661916901669666fb8025eccb9ddb0499cddad4c164fec219c8b8fd2db15a7138098557cc291928b4358fa7522ddd41d35c99fb78f0def2b3e673d73d2"; + let sample = "1D003412349BDC1F76A0C12149BB15F791DBE42FDE02C209B04A85C6F512990C8CEDEC4E7B8DBF1C3C928D64D87AA8293B9603EEE0"; let lhs_packet = Packet { route_type: RouteType::Flood, version: PayloadVersion::VersionOne, path: vec![], transport: [0, 0], - raw_content: Bytes::copy_from_slice(&decode("1234569df1f9661916901669666fb8025eccb9ddb0499cddad4c164fec219c8b8fd2db15a7138098557cc291928b4358fa7522ddd41d35c99fb78f0def2b3e673d73d2").unwrap()), + raw_content: Bytes::copy_from_slice(&decode("3412349BDC1F76A0C12149BB15F791DBE42FDE02C209B04A85C6F512990C8CEDEC4E7B8DBF1C3C928D64D87AA8293B9603EEE0").unwrap()), content: PacketContent::AnonReq(AnonReq { - dest: 0x12, - public_key: PublicKey::try_from(Bytes::copy_from_slice(&decode("34569df1f9661916901669666fb8025eccb9ddb0499cddad4c164fec219c8b8f").unwrap())).unwrap(), - mac: 0xd2db, - ciphertext: Bytes::copy_from_slice(&decode("15a7138098557cc291928b4358fa7522ddd41d35c99fb78f0def2b3e673d73d2").unwrap()), + dest: 0x34, + public_key: PublicKey::try_from(Bytes::copy_from_slice(&decode("12349bdc1f76a0c12149bb15f791dbe42fde02c209b04a85c6f512990c8cedec").unwrap())).unwrap(), + mac: 0x4e7b, + ciphertext: Bytes::copy_from_slice(&decode("8DBF1C3C928D64D87AA8293B9603EEE0").unwrap()), incomplete: false, request: None }), incomplete: false }; - let 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()), + ("Sample 3 ID".to_owned(), "08976A389FA16B077492BA7403A2178F4DF22B74A44DA0BE780CD0A51F5796437BB76B51320EE216F483F741FD73ED32F7DF5BBCBE811F405E579DD45AA8280A".to_owned()), + ("Sample 4 ID".to_owned(), "f8285b33f3949770f4668be015c325d3ee6dd99c86e44d15ec2a48e157d355470161800938619f83944af343ae996ce433536b324f38e2ae242e8e1cf40649d5".to_owned()), + ("Sample 5 ID".to_owned(), "60B176D579DD1F874C38B1AA6476F30BB77E7CCDD325BC718D9C926628B9037445AE4968856CF19453E8D72C19160EB34FDF7E7EC9EFE384A1ACA5573D6F28E5".to_owned()), + ]), + contacts: HashMap::from([ + ("Sample 1 CT".to_owned(), "34569df1f9661916901669666fb8025eccb9ddb0499cddad4c164fec219c8b8f".to_owned()), + ("Sample 2 CT".to_owned(), "12349bdc1f76a0c12149bb15f791dbe42fde02c209b04a85c6f512990c8cedec".to_owned()), + ("Sample 3 CT".to_owned(), "9012aa245cbb5fc7a4512ce62350aa528dac49e0dd4a724ded59da83e27267aa".to_owned()), + ("Sample 4 CT".to_owned(), "7890b8573a6ba4a05b173d6ccfdfa73ac8ec4a12bf3c745ace636e1d191e132a".to_owned()), + ("Sample 5 CT".to_owned(), "12387717b1b00763ce666d710ad216348c7cdf5a5791c67c1e8120d40c550ac1".to_owned()), + ]), + groups: HashMap::new() + }.compile(); + + let mut rhs_packet = Packet::from_str(sample).unwrap(); assert!(lhs_packet == rhs_packet, "{}", print_compare(lhs_packet, rhs_packet)); + + println!("\"{}\"", rhs_packet); + assert!(format!("{}", rhs_packet) == " Flood | v1 | | [] | | ANON REQ. | (12) -> (34) MAC: 4e7b ENCRYPTED"); + rhs_packet.try_decrypt(&keystore); + println!("\"{}\"", rhs_packet); + // assert!(format!("{}", rhs_packet) == ""); + } }
\ No newline at end of file diff --git a/src/crypto.rs b/src/crypto.rs index f814f78..c5cad32 100644 --- a/src/crypto.rs +++ b/src/crypto.rs @@ -87,7 +87,7 @@ impl PublicKey { } } -#[derive(Clone)] +#[derive(Clone, Eq, Hash)] pub struct SharedSecret(MontgomeryPoint); impl PartialEq for SharedSecret { @@ -123,6 +123,20 @@ impl FromStr for SharedSecret { } } +impl TryFrom<Bytes> for SharedSecret { + type Error = MeshcoreCryptoError; + + fn try_from(value: Bytes) -> Result<Self, Self::Error> { + if value.len() != 32 { Err(MeshcoreCryptoError::KeyLengthError) } + else { + let mut value = value; + let mut retval = Self(MontgomeryPoint([0_u8; 32])); + value.copy_to_slice(&mut retval.0.0); + Ok(retval) + } + } +} + impl SharedSecret { fn get_key(&self) -> &[u8; 16] { // Safety: The size of the slice ensures that this will never be wrong. @@ -337,13 +351,6 @@ impl SharedSecret { } } -pub struct Keychain { - // pub private_keys: Vec<PrivateKey>, - // pub public_keys: Vec<PublicKey>, - - // secrets: HashMap<String, Ha> -} - #[cfg(test)] mod tests { use hex::{decode_to_slice, encode}; diff --git a/src/identity.rs b/src/identity.rs index ccbc3de..e68b7f2 100644 --- a/src/identity.rs +++ b/src/identity.rs @@ -1,11 +1,14 @@ -use std::{collections::HashMap, rc::Rc}; +use std::{collections::{HashMap, HashSet}, process::id, 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)] +#[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. @@ -35,10 +38,11 @@ pub struct Identity { /// used because there are many many hash collisions /// when using just the 1-byte hash prefix. #[cfg_attr(feature = "std", serde(skip))] - pub secrets: Vec<(Rc<String>, SharedSecret)> + pub secrets: HashSet<(Rc<String>, SharedSecret)> } -#[derive(PartialEq, Debug, Clone)] +#[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 @@ -58,7 +62,8 @@ pub struct Contact { pub public_key: PublicKey } -#[derive(PartialEq, Debug, Clone)] +#[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 @@ -74,7 +79,8 @@ pub struct Group { pub secret: SharedSecret } -#[derive(PartialEq, Debug, Clone)] +#[derive(PartialEq, Debug, Clone, Difference)] +#[difference(expose)] pub struct Keystore { pub identities: HashMap<Rc<String>, Identity>, pub contacts: HashMap<Rc<String>, Contact>, @@ -82,6 +88,14 @@ pub struct Keystore { } impl Keystore { + pub fn identity_by_name(&self, name: &str) -> Option<Identity> { + 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<String>, Rc<String>)> { // Just brute-force all the secrets until one matches... for identity in self.identities.iter() { @@ -113,12 +127,13 @@ impl Keystore { } } -#[derive(PartialEq, Debug, Clone)] +#[derive(PartialEq, Debug, Clone, Difference)] +#[difference(expose)] #[cfg_attr(feature = "std", derive(Deserialize))] pub struct KeystoreInput { - pub identities: Vec<Identity>, - pub contacts: Vec<Contact>, - pub groups: Vec<Group> + pub identities: HashMap<String, String>, + pub contacts: HashMap<String, String>, + pub groups: HashMap<String, String>, } impl KeystoreInput { @@ -132,28 +147,66 @@ impl KeystoreInput { // 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. - let identities = self.identities.into_iter().map(|mut i| { - i.public_key = PublicKey::from(&i.private_key); - - i.secrets = self.contacts.iter().map(|c| { - (c.name.clone(), i.private_key.create_secret(&c.public_key)) - }).collect(); - - (i.name.clone(), i) + let contacts: Vec<Contact> = self.contacts.into_iter().filter_map(|(name, pub_key)| { + match PublicKey::from_str(&pub_key) { + Ok(pub_key) => { + Some(Contact { + name: Rc::new(name), + public_key: pub_key + }) + }, + Err(e) => { + warn!("Unable to add contact named \"{}\" because there was a problem with the public key: {}", name, e); + None + } + } + }).collect(); + + let identities = self.identities.into_iter().filter_map(|(name, priv_key)| { + match PrivateKey::from_str(&priv_key) { + Ok(private_key) => { + let public_key = PublicKey::from(&private_key); + let mut i = Identity { + name: Rc::new(name), + private_key: private_key, + public_key: public_key, + secrets: HashSet::new() + }; + + i.secrets = contacts.iter().map(|c| { + (c.name.clone(), i.private_key.create_secret(&c.public_key)) + }).collect(); + + Some((i.name.clone(), i)) + }, + Err(e) => { + warn!("Unable to add identity named \"{}\" because there was a problem with the private key: {}", name, e); + None + } + } }); retval.identities = HashMap::from_iter(identities); // Create a hash of contacts - let contacts = self.contacts.into_iter().map(|c| { + 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().map(|g| { - (g.name.clone(), g) + 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 contact named \"{}\" because there was a problem with the public key: {}", name, e); + None + } + } }); retval.groups = HashMap::from_iter(groups); @@ -229,3 +282,47 @@ fn deserialize_secret<'de, D>(deserializer: D) -> Result<SharedSecret, D::Error> } } +#[cfg(test)] +mod tests { + use std::str::FromStr; + use hex::decode; + + use crate::identity::KeystoreInput; + use crate::crypto::*; + use super::*; + + #[allow(dead_code)] + pub(crate) fn print_compare(lhs: Identity, rhs: Identity) -> String { + let mut output_string = format!("Left hand side: \n{:#?}\nRight hand side: \n{:#?}\nDifferences: \n", lhs, rhs); + + for diff in lhs.diff(&rhs) { + output_string.push_str(&format!("{:#?}", diff)); + } + output_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, "{}", print_compare(lhs, rhs)); + } +}
\ No newline at end of file diff --git a/src/packet.rs b/src/packet.rs index d28fe90..61b8b21 100644 --- a/src/packet.rs +++ b/src/packet.rs @@ -3,7 +3,7 @@ use std::{fmt::{Debug, Display}, str::FromStr}; use hex::decode; use bytes::{Buf, Bytes}; use structdiff::{Difference, StructDiff}; -use crate::{identity::Keystore, packet_content::PacketContent, request::ClearRequest, text::ClearText}; +use crate::{anon_req::ClearAnonRequest, identity::Keystore, packet_content::PacketContent, request::ClearRequest, text::ClearText}; #[derive(PartialEq, Debug, Clone, Difference)] #[difference(expose)] @@ -205,7 +205,8 @@ impl Packet { &anon_req.ciphertext ); - if let Some(_cleartext) = decrypt_result { + if let Some(cleartext) = decrypt_result { + anon_req.request = Some(ClearAnonRequest::from(cleartext.0)); true } else { false diff --git a/src/request.rs b/src/request.rs index 7ce126c..ffd7f3a 100644 --- a/src/request.rs +++ b/src/request.rs @@ -107,12 +107,12 @@ impl Display for Request { #[cfg(test)] mod tests { - use std::str::FromStr; + use std::{collections::HashMap, str::FromStr}; use chrono::DateTime; use hex::decode; use bytes::Bytes; - use crate::{crypto::{PrivateKey, PublicKey}, identity::{Contact, Identity, KeystoreInput}, packet::*, packet_content::{PacketContent, PeerToPeerCipher}, request::{ClearRequest, Request, RequestType}}; + use crate::{identity::KeystoreInput, packet::*, packet_content::{PacketContent, PeerToPeerCipher}, request::{ClearRequest, Request, RequestType}}; #[test] fn request_type() { @@ -163,31 +163,15 @@ mod tests { let mut rhs_packet = Packet::from_str(sample).unwrap(); let keystore = KeystoreInput { - identities: vec![ - Identity { - name: "Sample 1 ID".to_owned().into(), - private_key: PrivateKey::from_str("4885CF25975EA09742EF76DA587D0957E74EE02AAA34A001458E207E63CF7E6C4940C8C42C335862C71CC2F139633057D1FEE5687B172B27E1E0302A1D480E08").unwrap(), - public_key: PublicKey::default(), - secrets: vec![] - }, - Identity { - name: "Sample 2 ID".to_owned().into(), - private_key: PrivateKey::from_str("38DAA98490B7284697C7ADA6175FD1F8DAD12032AD7ABAE625B7EAD8FEC6444CA281C3370B97155D9C8CECD89A929FDDE0FBF3A9D5C92A1B3C24D711934CD69D").unwrap(), - public_key: PublicKey::default(), - secrets: vec![] - }, - ], - contacts: vec![ - Contact { - name: "Sample 1 CT".to_owned().into(), - public_key: PublicKey::from_str("34569df1f9661916901669666fb8025eccb9ddb0499cddad4c164fec219c8b8f").unwrap() - }, - Contact { - name: "Sample 2 CT".to_owned().into(), - public_key: PublicKey::from_str("12349bdc1f76a0c12149bb15f791dbe42fde02c209b04a85c6f512990c8cedec").unwrap() - }, - ], - groups: vec![] + 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"); diff --git a/src/text.rs b/src/text.rs index ae2b0b9..3f74646 100644 --- a/src/text.rs +++ b/src/text.rs @@ -222,12 +222,12 @@ impl From<u8> for MessageType { #[cfg(test)] mod tests { - use std::{rc::Rc, str::FromStr}; + use std::{collections::HashMap, rc::Rc, str::FromStr}; use chrono::DateTime; use hex::decode; use bytes::Bytes; - use crate::{crypto::SharedSecret, identity::{Group, KeystoreInput}, packet::*, packet_content::{PacketContent, PeerToPeerCipher}, text::{ClearText, GroupData, GroupText, MessageType, Text}}; + use crate::{identity::KeystoreInput, packet::*, packet_content::{PacketContent, PeerToPeerCipher}, text::{ClearText, GroupData, GroupText, MessageType, Text}}; #[test] fn text() { @@ -311,14 +311,11 @@ mod tests { let mut rhs_packet = Packet::from_str(sample).unwrap(); let keystore = KeystoreInput { - identities: vec![], - contacts: vec![], - groups: vec![ - Group { - name: Rc::new("Public".to_owned()), - secret: SharedSecret::from_str("8b3387e9c5cdea6ac9e5edbaa115cd72").unwrap() - } - ] + identities: HashMap::new(), + contacts: HashMap::new(), + groups: HashMap::from([ + ("Public".to_owned(), "8b3387e9c5cdea6ac9e5edbaa115cd72".to_owned()) + ]) }.compile(); _ = rhs_packet.try_decrypt(&keystore); |
