aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorWill Dillon <william@housedillon.com>2025-11-17 04:35:15 +0000
committerWill Dillon <william@housedillon.com>2025-11-17 04:35:15 +0000
commit57903774e1ea9d9ea44de089d606b630142b80b0 (patch)
tree907695dff8be9db25f90602f586c23ee0b6b78df /src
parentAnon reqest complete (diff)
downloadmeshcore-rs-57903774e1ea9d9ea44de089d606b630142b80b0.tar.gz
meshcore-rs-57903774e1ea9d9ea44de089d606b630142b80b0.zip
Got identity to 95%
Diffstat (limited to 'src')
-rw-r--r--src/crypto.rs118
-rw-r--r--src/identity.rs238
-rw-r--r--src/trace.rs2
3 files changed, 278 insertions, 80 deletions
diff --git a/src/crypto.rs b/src/crypto.rs
index c5cad32..b62fb5d 100644
--- a/src/crypto.rs
+++ b/src/crypto.rs
@@ -23,6 +23,8 @@ 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 {
match self {
@@ -181,8 +183,8 @@ impl SharedSecret {
let chunk_size = 16;
let length = text.len();
let remainder = length % chunk_size;
- // Pad the bytes to it's an even multiple of the chunk size
+ // Pad the bytes to it's an even multiple of the chunk size
if remainder != 0 {
let padding = chunk_size - remainder;
text.reserve(padding);
@@ -207,40 +209,40 @@ impl SharedSecret {
}
}
- pub fn encrypt(&self, plaintext: Bytes) -> Result<Bytes, MeshcoreCryptoError> {
+ pub fn encrypt(&self, plaintext: Bytes) -> Bytes {
use aes::cipher::KeyInit;
- if let Ok(aes) = Aes128::new_from_slice(self.get_key()) {
- let mut text = BytesMut::from(plaintext);
+ // Safety: The get_key function is guarenteed to produce a key
+ // the right size. That's the only reason this method might
+ // fail.
+ let aes = Aes128::new_from_slice(self.get_key()).unwrap();
+ let mut text = BytesMut::from(plaintext);
- // The decryption function works on a 16-byte block of data.
- // we need to break the input ciphertext into blocks of this size
- // and work with them individually. Also, any "short" blocks have
- // to be zero-padded.
- // Copy the original length so we can truncate back down
- let chunk_size = 16;
- let length = text.len();
- let remainder = length % chunk_size;
- // Pad the bytes to it's an even multiple of the chunk size
-
- if remainder != 0 {
- let padding = chunk_size - remainder;
- text.reserve(padding);
- text.put_bytes(0, padding);
- }
-
- let chunks = text.chunks_exact_mut(chunk_size);
- for chunk in chunks {
- #[allow(deprecated)]
- let mut block = *GenericArray::from_slice(&chunk);
- aes.encrypt_block(&mut block);
- chunk.copy_from_slice(&block);
- }
+ // The decryption function works on a 16-byte block of data.
+ // we need to break the input ciphertext into blocks of this size
+ // and work with them individually. Also, any "short" blocks have
+ // to be zero-padded.
+ // Copy the original length so we can truncate back down
+ let chunk_size = 16;
+ let length = text.len();
+ let remainder = length % chunk_size;
- Ok(Bytes::from(text))
- } else {
- Err(MeshcoreCryptoError::KeyLengthError)
+ // Pad the bytes to it's an even multiple of the chunk size
+ if remainder != 0 {
+ let padding = chunk_size - remainder;
+ text.reserve(padding);
+ text.put_bytes(0, padding);
+ }
+
+ let chunks = text.chunks_exact_mut(chunk_size);
+ for chunk in chunks {
+ #[allow(deprecated)]
+ let mut block = *GenericArray::from_slice(&chunk);
+ aes.encrypt_block(&mut block);
+ chunk.copy_from_slice(&block);
}
+
+ Bytes::from(text)
}
pub fn hash_prefix(&self) -> u8 {
@@ -315,7 +317,7 @@ impl FromStr for PrivateKey {
if let Ok(bytes) = TryInto::<[u8; 64]>::try_into(hex) {
Ok(PrivateKey(ExpandedSecretKey::from_bytes(&bytes)))
} else {
- Err(MeshcoreCryptoError::HexDecodeError)
+ Err(MeshcoreCryptoError::TryFromSliceError)
}
} else {
Err(MeshcoreCryptoError::HexDecodeError)
@@ -346,8 +348,10 @@ impl SharedSecret {
Some(self.decrypt(&data))
}
- pub fn encrypt_then_mac(&self, _data: Bytes) -> Option<(u16, Bytes)> {
- None
+ pub fn encrypt_then_mac(&self, data: Bytes) -> (u16, Bytes) {
+ let ciphertext = self.encrypt(data);
+ let mac = self.get_hmac(&ciphertext);
+ (mac, ciphertext)
}
}
@@ -433,7 +437,7 @@ mod tests {
fn encrypt_online_example() {
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).unwrap();
+ let ciphertext = secret.encrypt(plaintext);
assert!(ciphertext == decode("62374852B6A11405A081F87356C88861").unwrap());
}
@@ -523,4 +527,50 @@ mod tests {
let cleartext = group_secret.mac_then_decrypt(mac, &sample_data).unwrap();
assert!(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");
+ }
+
+ #[test]
+ fn test_crypto_round_trip() {
+ let alice_private = PrivateKey::default();
+ let alice_public = PublicKey::from(&alice_private);
+
+ let bob_private = PrivateKey::default();
+ let bob_public = PublicKey::from(&bob_private);
+
+ let cleartext = "Hi Bob. This is alice. How are you?";
+ let alice_secret = alice_private.create_secret(&bob_public);
+
+ let (mac, ciphertext) = alice_secret.encrypt_then_mac(Bytes::copy_from_slice(cleartext.as_bytes()));
+
+ let bob_secret = bob_private.create_secret(&alice_public);
+ let decrypted_data = bob_secret.mac_then_decrypt(mac, &ciphertext);
+
+ if let Some(decrypted_data) = decrypted_data {
+ // Due to padding for the block cipher, the decrypted string may have up to
+ // 31 null characters at the end. Given that we want to compare the starting
+ // and ending, we have to strip these back out. We can't do this in the decrypt
+ // function because we don't actually know how long the contents of the is
+ // suppose to be.
+ let raw_message = String::from_utf8_lossy(&decrypted_data).to_owned();
+ let lhs_string: &str = raw_message
+ .splitn(2, "\0")
+ .collect::<Vec<&str>>()
+ .first()
+ .unwrap();
+
+ assert!(lhs_string == cleartext, "{}",
+ format!("\"{}\" != \"{}\"", lhs_string, cleartext)
+ );
+ } else {
+ assert!(false, "Unable to decrypt");
+ }
+
+ }
} \ No newline at end of file
diff --git a/src/identity.rs b/src/identity.rs
index 0b8d8ec..9389ace 100644
--- a/src/identity.rs
+++ b/src/identity.rs
@@ -1,7 +1,7 @@
-use std::{collections::{HashMap, HashSet}, process::id, rc::Rc, str::FromStr};
+use std::{collections::{HashMap, HashSet}, rc::Rc, str::FromStr};
use bytes::Bytes;
use log::warn;
-use crate::{crypto::{PrivateKey, PublicKey, SharedSecret}, identity};
+use crate::{crypto::{PrivateKey, PublicKey, SharedSecret}};
use structdiff::{Difference, StructDiff};
#[cfg(feature = "std")]
@@ -111,12 +111,10 @@ impl Keystore {
pub fn decrypt_and_id_group(&self, _group_hash_prefix: u8, mac: u16, data: &Bytes) -> Option<(Bytes, &Group)> {
for group in self.groups.iter() {
- // if group.1.secret.hash_prefix() == group_hash_prefix {
- let result = group.1.secret.mac_then_decrypt(mac, data);
- if let Some(result) = result {
- return Some((result, group.1));
- }
- // }
+ let result = group.1.secret.mac_then_decrypt(mac, data);
+ if let Some(result) = result {
+ return Some((result, group.1));
+ }
}
None
@@ -155,44 +153,57 @@ 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 contacts: Vec<Contact> = self.contacts.into_iter().filter_map(|(name, pub_key)| {
- match PublicKey::from_str(&pub_key) {
+
+ // 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<Contact> = vec![];
+ for name in contact_names {
+ if let Some(key_string) = self.contacts.get(name) {
+ match PublicKey::from_str(&key_string) {
Ok(pub_key) => {
- Some(Contact {
- name: Rc::new(name),
+ 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);
- 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
+ }
+
+ let mut identity_names: Vec<&String> = self.identities.keys().collect();
+ identity_names.sort();;
+ let mut identities: Vec<(Rc<String>, 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: 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();
+
+
+ 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);
@@ -211,7 +222,7 @@ impl KeystoreInput {
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);
+ warn!("Unable to add group named \"{}\" because there was a problem with the secret: {}", name, e);
None
}
}
@@ -292,21 +303,85 @@ fn deserialize_secret<'de, D>(deserializer: D) -> Result<SharedSecret, D::Error>
#[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::*;
- #[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));
+ #[test]
+ fn secret_deserializer() {
+ let deserializer: StrDeserializer<ValueError> = "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),
}
- output_string
+
+ // Test failure conditions
+ let deserializer: StrDeserializer<ValueError> = "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<ValueError> = "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<ValueError> = "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<ValueError> = "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<ValueError> = "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<ValueError> = "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<ValueError> = "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<ValueError> = "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<ValueError> = "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]
@@ -331,6 +406,77 @@ mod tests {
(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));
+ 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<u8>,
+ }
+
+ 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<usize> {
+ 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::<KeystoreInput>(contents).unwrap();
+ let _keystore = keystore_in.compile();
+
+ String::from_utf8(tx.try_iter().collect::<Vec<u8>>())
+ .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/trace.rs b/src/trace.rs
index c64efc0..5df2a24 100644
--- a/src/trace.rs
+++ b/src/trace.rs
@@ -81,6 +81,8 @@ mod tests {
let rhs_packet = Packet::from_str(sample).unwrap();
assert!(lhs_packet == rhs_packet, "{}", print_compare(lhs_packet, rhs_packet));
+
+
}