aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorWill Dillon <william@housedillon.com>2025-11-13 18:21:18 +0000
committerWill Dillon <william@housedillon.com>2025-11-13 18:21:18 +0000
commit6dd5ee6c774e470e6837d63780c6a2660e8538fc (patch)
tree8249c65d9b702e05b598af03f317f40ca2581838 /src
parentRefactored packet_content and added way more tests (diff)
downloadmeshcore-rs-6dd5ee6c774e470e6837d63780c6a2660e8538fc.tar.gz
meshcore-rs-6dd5ee6c774e470e6837d63780c6a2660e8538fc.zip
Updated TOML formate and added test
Diffstat (limited to 'src')
-rw-r--r--src/anon_req.rs41
-rw-r--r--src/crypto.rs23
-rw-r--r--src/identity.rs139
-rw-r--r--src/packet.rs5
-rw-r--r--src/request.rs38
-rw-r--r--src/text.rs17
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);