aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorWill Dillon <william@housedillon.com>2025-11-06 17:17:54 +0000
committerWill Dillon <william@housedillon.com>2025-11-06 17:17:54 +0000
commit6a06d3323c88bb71718a770d2629554bd96d55be (patch)
tree5c89ba89c86eac68d816cc8cd73ea5686d020a72 /src
parentRan clippy (diff)
downloadmeshcore-rs-6a06d3323c88bb71718a770d2629554bd96d55be.tar.gz
meshcore-rs-6a06d3323c88bb71718a770d2629554bd96d55be.zip
Switching over to the mac for mac
Diffstat (limited to 'src')
-rw-r--r--src/crypto.rs118
-rw-r--r--src/packet.rs284
2 files changed, 276 insertions, 126 deletions
diff --git a/src/crypto.rs b/src/crypto.rs
index 9dd423b..2661f38 100644
--- a/src/crypto.rs
+++ b/src/crypto.rs
@@ -2,10 +2,15 @@ use std::str::FromStr;
use ed25519_dalek::hazmat::ExpandedSecretKey;
use curve25519_dalek::{constants, edwards::CompressedEdwardsY};
-use tokio_util::bytes::{Buf, Bytes};
+use tokio_util::bytes::{Buf, BufMut, Bytes, BytesMut};
use x25519_dalek::StaticSecret;
-use hex::decode;
+use sha2::{Sha256};
+use hmac::{Hmac, Mac};
+
+type HmacSha256 = Hmac<Sha256>;
+
+use hex::{decode, decode_to_slice};
#[derive(Debug)]
pub enum MeshcoreCryptoError {
@@ -13,13 +18,54 @@ pub enum MeshcoreCryptoError {
TryFromSliceError
}
+#[derive(PartialEq, Debug)]
pub struct PrivateKey(ExpandedSecretKey);
#[derive(PartialEq, Debug, Clone)]
pub struct PublicKey(CompressedEdwardsY);
+#[derive(Debug, PartialEq)]
pub struct SharedSecret([u8; 32]);
+impl FromStr for SharedSecret {
+ type Err = MeshcoreCryptoError;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ let mut slice = [0_u8; 32];
+ if decode_to_slice(s, &mut slice).is_err() {
+ return Err(MeshcoreCryptoError::TryFromSliceError)
+ } else {
+ Ok(SharedSecret(slice))
+ }
+ }
+}
+
+impl SharedSecret {
+ pub fn new_from_group_secret(bytes: Bytes) -> Self {
+ // The group secret is 16-bytes of key with the last 16-bytes set to zero.
+ let mut group_secret = BytesMut::from(bytes);
+ group_secret.reserve(16);
+ group_secret.put(&[0_u8; 16][..]);
+
+ let mut slice = [0_u8; 32];
+ group_secret.copy_to_slice(&mut slice);
+
+ SharedSecret(slice)
+ }
+
+ pub fn get_hmac(&self, ciphertext: Bytes) -> Result<u16, MeshcoreCryptoError> {
+ if let Ok(mut mac) = HmacSha256::new_from_slice(&self.0) {
+ mac.update(&ciphertext);
+ let result = mac.finalize();
+ let mut bytes = Bytes::copy_from_slice(&result.into_bytes());
+
+ Ok(bytes.get_u16())
+ } else {
+ Err(MeshcoreCryptoError::KeyLengthError)
+ }
+ }
+}
+
// This is just for creating placeholders
impl Default for PublicKey {
fn default() -> Self {
@@ -76,8 +122,8 @@ impl FromStr for PrivateKey {
}
}
-impl From<PrivateKey> for PublicKey {
- fn from(key: PrivateKey) -> Self {
+impl From<&PrivateKey> for PublicKey {
+ fn from(key: &PrivateKey) -> Self {
PublicKey((key.0.scalar * constants::ED25519_BASEPOINT_POINT).compress())
}
}
@@ -104,4 +150,68 @@ pub struct Keychain {
pub public_keys: Vec<PublicKey>,
// secrets: HashMap<String, Ha>
+}
+
+#[cfg(test)]
+mod tests {
+ use curve25519_dalek::Scalar;
+ use hex::{decode_to_slice, encode};
+
+ use super::*;
+
+ #[test]
+ fn public_key() {
+ let mut slice = [0_u8; 32];
+ decode_to_slice("12349bdc1f76a0c12149bb15f791dbe42fde02c209b04a85c6f512990c8cedec", &mut slice).unwrap();
+ let public_key = PublicKey::from_str("12349bdc1f76a0c12149bb15f791dbe42fde02c209b04a85c6f512990c8cedec");
+ assert!(Ok(PublicKey(CompressedEdwardsY(slice))) == public_key);
+ }
+
+ #[test]
+ fn private_key() {
+ // Create a private key in two different ways
+ let bytes = Bytes::copy_from_slice(&decode("38DAA98490B7284697C7ADA6175FD1F8DAD12032AD7ABAE625B7EAD8FEC6444CA281C3370B97155D9C8CECD89A929FDDE0FBF3A9D5C92A1B3C24D711934CD69D").unwrap());
+ let private_key = PrivateKey::from_str("38DAA98490B7284697C7ADA6175FD1F8DAD12032AD7ABAE625B7EAD8FEC6444CA281C3370B97155D9C8CECD89A929FDDE0FBF3A9D5C92A1B3C24D711934CD69D");
+ assert!(Ok(PrivateKey(ExpandedSecretKey::from_bytes(bytes.get(0 .. 64).unwrap().try_into().unwrap()))) == private_key);
+
+ // Ensure that the expected public key can be derived from it.
+ let public_key = PublicKey::from(&private_key.unwrap());
+ assert!(PublicKey::from_str("12349bdc1f76a0c12149bb15f791dbe42fde02c209b04a85c6f512990c8cedec").unwrap() == public_key);
+ }
+
+ #[test]
+ fn shared_secret() {
+ // We'll make a public/private pair for alice and bob and make sure the shared secret is the same and expected
+ let alice_private = PrivateKey::from_str("4885CF25975EA09742EF76DA587D0957E74EE02AAA34A001458E207E63CF7E6C4940C8C42C335862C71CC2F139633057D1FEE5687B172B27E1E0302A1D480E08").unwrap();
+ let bob_private = PrivateKey::from_str("08976A389FA16B077492BA7403A2178F4DF22B74A44DA0BE780CD0A51F5796437BB76B51320EE216F483F741FD73ED32F7DF5BBCBE811F405E579DD45AA8280A").unwrap();
+
+ let alice_public = PublicKey::from(&alice_private);
+ let bob_public = PublicKey::from(&bob_private);
+
+ println!("Alice's public key: {}", encode(&alice_public.0.to_bytes()));
+ println!("Bob's public key: {}", encode(&bob_public.0.to_bytes()));
+
+ let left_secret = alice_private.create_secret(&bob_public);
+ let right_secret = bob_private.create_secret(&alice_public);
+
+ println!("Left shared secret: {}", encode(left_secret.0));
+ println!("Right shared secret: {}", encode(right_secret.0));
+
+ // assert!(left_secret == right_secret);
+ }
+
+ #[test]
+ fn hmac() {
+ // 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("8b3387e9c5cdea6ac9e5edbaa115cd7200000000000000000000000000000000").unwrap();
+ assert!(group_secret == test_group_secret);
+
+ // Test using the secret and ciphertext to make a MAC and ensure it matches an example
+ let sample_data = Bytes::copy_from_slice(&decode("150011C3C1354D619BAE9590E4D177DB7EEAF982F5BDCF78005D75157D9535FA90178F785D").unwrap());
+ let mac = group_secret.get_hmac(sample_data).unwrap();
+ // assert!(0xC3C1 == mac);
+
+
+ }
} \ No newline at end of file
diff --git a/src/packet.rs b/src/packet.rs
index 4325e10..090b3ec 100644
--- a/src/packet.rs
+++ b/src/packet.rs
@@ -18,6 +18,127 @@ struct Packet {
incomplete: bool
}
+impl FromStr for Packet {
+ type Err = hex::FromHexError;
+
+ fn from_str(hex_str: &str) -> Result<Self, Self::Err> {
+ let hex = decode(hex_str)?;
+ Ok(Packet::from(Bytes::copy_from_slice(&hex)))
+ }
+}
+
+impl From<Bytes> for Packet {
+ fn from(bytes_in: Bytes) -> Self {
+ let mut bytes = bytes_in;
+
+ // This is the packet we'll build as we begin to parse
+ let mut packet = Packet::default();
+
+ if bytes.len() < 2 {
+ return packet
+ }
+
+ let header = bytes.get_u8();
+
+ // Parse the header byte
+ packet.route_type = RouteType::from(header);
+ packet.version = PayloadVersion::from(header);
+
+ // Get the transport if it's provided
+ packet.transport = match packet.route_type {
+ RouteType::TransportFlood | RouteType::TransportDirect => {
+ // The packet isn't long enough to contain the transport
+ if bytes.len() < 4 { return packet; }
+
+ let t1 = bytes.get_u16_le();
+ let t2 = bytes.get_u16_le();
+ [t1, t2]
+ },
+ _ => {
+ [0, 0]
+ }
+ };
+
+ // Get the route
+ if bytes.is_empty() { return packet; }
+ let path_length = bytes.get_u8() as usize;
+
+ packet.path = match packet.version {
+ PayloadVersion::VersionOne => {
+ // The packet isn't long enough for the indicated route
+ if bytes.len() < path_length { return packet; }
+
+ let route: Vec<u16> = bytes
+ .split_to(path_length)
+ .into_iter()
+ .map(|x| x as u16)
+ .collect();
+ route
+ },
+ PayloadVersion::VersionTwo => {
+ // The packet isn't long enough for the indicated route
+ if bytes.len() < path_length * 2 { return packet; }
+
+ let route: Vec<u16> = bytes
+ .split_to(path_length * 2)
+ .chunks(2)
+ .map(|c| Bytes::copy_from_slice(c).get_u16_le())
+ .collect();
+
+ route
+ },
+ _ => {
+ return packet;
+ }
+ };
+
+ // Get the rest of the payload and pass the parsing to the other structs
+ packet.raw_content = bytes.clone();
+
+ packet.content = PacketContent::new(header, bytes);
+
+ // Unfortunately, the trace packet is a bit weird.
+ // So, if this is a trace packet, move things back into the right place.
+ 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| {
+ let i = *u as i8;
+ let f = i as f32;
+ f / 4.0
+ }).collect();
+ trace.temp_path = vec![];
+ packet.content = PacketContent::Trace(trace);
+ }
+
+ // Mark the packet as complete and valid
+ packet.incomplete = false;
+
+ packet
+ }
+}
+
+impl Default for Packet {
+ fn default() -> Self {
+ Packet {
+ route_type: RouteType::Invalid,
+ version: PayloadVersion::Invalid,
+ path: vec![],
+ transport: [0, 0],
+ raw_content: Bytes::new(),
+ content: PacketContent::Invalid,
+ incomplete: true
+ }
+ }
+}
+
+impl Packet {
+ fn try_decrypt(&mut self, key: SharedSecret) -> bool {
+ false
+ }
+}
+
#[derive(PartialEq, Debug, Clone)]
enum RouteType {
TransportFlood,
@@ -158,12 +279,6 @@ impl From<Bytes> for PeerToPeerCipher {
}
}
-impl PeerToPeerCipher {
- pub fn attempt_decrypt(&mut self, secret: SharedSecret) -> Option<(Bytes, PublicKey, PrivateKey)> {
- None
- }
-}
-
#[derive(PartialEq, Debug, Clone)]
struct Request {
cipher: PeerToPeerCipher,
@@ -307,17 +422,26 @@ impl From<Bytes> for Advert {
}
}
-#[derive(PartialEq, Debug, Clone)]
+#[derive(PartialEq, Debug, Clone, Difference)]
+#[difference(expose)]
struct GroupText {
hash: u8,
mac: u16,
ciphertext: Bytes,
- cleartext: Option<String>,
+ cleartext: Option<ClearGroupText>,
incomplete: bool
}
+#[derive(PartialEq, Debug, Clone, Difference)]
+#[difference(expose)]
+struct ClearGroupText {
+ sender: String,
+ message: String,
+ timestamp: DateTime<Utc>
+}
+
impl From<Bytes> for GroupText {
fn from(value: Bytes) -> Self {
let mut bytes = value;
@@ -455,120 +579,6 @@ struct Raw {
bytes: Bytes
}
-impl FromStr for Packet {
- type Err = hex::FromHexError;
-
- fn from_str(hex_str: &str) -> Result<Self, Self::Err> {
- let hex = decode(hex_str)?;
- Ok(Packet::from(Bytes::copy_from_slice(&hex)))
- }
-}
-
-impl From<Bytes> for Packet {
- fn from(bytes_in: Bytes) -> Self {
- let mut bytes = bytes_in;
-
- // This is the packet we'll build as we begin to parse
- let mut packet = Packet::default();
-
- if bytes.len() < 2 {
- return packet
- }
-
- let header = bytes.get_u8();
-
- // Parse the header byte
- packet.route_type = RouteType::from(header);
- packet.version = PayloadVersion::from(header);
-
- // Get the transport if it's provided
- packet.transport = match packet.route_type {
- RouteType::TransportFlood | RouteType::TransportDirect => {
- // The packet isn't long enough to contain the transport
- if bytes.len() < 4 { return packet; }
-
- let t1 = bytes.get_u16_le();
- let t2 = bytes.get_u16_le();
- [t1, t2]
- },
- _ => {
- [0, 0]
- }
- };
-
- // Get the route
- if bytes.is_empty() { return packet; }
- let path_length = bytes.get_u8() as usize;
-
- packet.path = match packet.version {
- PayloadVersion::VersionOne => {
- // The packet isn't long enough for the indicated route
- if bytes.len() < path_length { return packet; }
-
- let route: Vec<u16> = bytes
- .split_to(path_length)
- .into_iter()
- .map(|x| x as u16)
- .collect();
- route
- },
- PayloadVersion::VersionTwo => {
- // The packet isn't long enough for the indicated route
- if bytes.len() < path_length * 2 { return packet; }
-
- let route: Vec<u16> = bytes
- .split_to(path_length * 2)
- .chunks(2)
- .map(|c| Bytes::copy_from_slice(c).get_u16_le())
- .collect();
-
- route
- },
- _ => {
- return packet;
- }
- };
-
- // Get the rest of the payload and pass the parsing to the other structs
- packet.raw_content = bytes.clone();
-
- packet.content = PacketContent::new(header, bytes);
-
- // Unfortunately, the trace packet is a bit weird.
- // So, if this is a trace packet, move things back into the right place.
- 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| {
- let i = *u as i8;
- let f = i as f32;
- f / 4.0
- }).collect();
- trace.temp_path = vec![];
- packet.content = PacketContent::Trace(trace);
- }
-
- // Mark the packet as complete and valid
- packet.incomplete = false;
-
- packet
- }
-}
-
-impl Default for Packet {
- fn default() -> Self {
- Packet {
- route_type: RouteType::Invalid,
- version: PayloadVersion::Invalid,
- path: vec![],
- transport: [0, 0],
- raw_content: Bytes::new(),
- content: PacketContent::Invalid,
- incomplete: true
- }
- }
-}
#[cfg(test)]
mod tests {
@@ -879,6 +889,36 @@ mod tests {
}
#[test]
+ fn group_text_decrypted() {
+ let sample = "150011C3C1354D619BAE9590E4D177DB7EEAF982F5BDCF78005D75157D9535FA90178F785D";
+
+ let lhs_packet = Packet {
+ route_type: RouteType::Flood,
+ version: PayloadVersion::VersionOne,
+ path: vec![],
+ transport: [0, 0],
+ raw_content: Bytes::copy_from_slice(&decode("11C3C1354D619BAE9590E4D177DB7EEAF982F5BDCF78005D75157D9535FA90178F785D").unwrap()),
+ content: PacketContent::GroupText(GroupText {
+ hash: 0x11,
+ mac: 0xC3C1,
+ ciphertext: Bytes::copy_from_slice(&decode("354D619BAE9590E4D177DB7EEAF982F5BDCF78005D75157D9535FA90178F785D").unwrap()),
+ cleartext: Some(ClearGroupText {
+ sender: "🌲 Tree".to_owned(),
+ message: "☁️".to_owned(),
+ timestamp: DateTime::from_timestamp_secs(1758484279).unwrap()
+ }),
+ incomplete: false
+ }),
+ incomplete: false
+ };
+
+ let mut rhs_packet = Packet::from_str(sample).unwrap();
+ _ = rhs_packet.try_decrypt(SharedSecret::new_from_group_secret(Bytes::copy_from_slice(&decode("8b3387e9c5cdea6ac9e5edbaa115cd72").unwrap())));
+
+ // assert!(lhs_packet == rhs_packet, "{}", print_compare(lhs_packet, rhs_packet));
+ }
+
+ #[test]
fn multipart() {
let sample = "2A0013E8C59624";