diff options
| author | Will Dillon <william@housedillon.com> | 2025-11-17 04:35:15 +0000 |
|---|---|---|
| committer | Will Dillon <william@housedillon.com> | 2025-11-17 04:35:15 +0000 |
| commit | 57903774e1ea9d9ea44de089d606b630142b80b0 (patch) | |
| tree | 907695dff8be9db25f90602f586c23ee0b6b78df /src | |
| parent | Anon reqest complete (diff) | |
| download | meshcore-rs-57903774e1ea9d9ea44de089d606b630142b80b0.tar.gz meshcore-rs-57903774e1ea9d9ea44de089d606b630142b80b0.zip | |
Got identity to 95%
Diffstat (limited to 'src')
| -rw-r--r-- | src/crypto.rs | 118 | ||||
| -rw-r--r-- | src/identity.rs | 238 | ||||
| -rw-r--r-- | src/trace.rs | 2 |
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)); + + } |
