diff options
| author | Will Dillon <william@housedillon.com> | 2025-11-07 01:38:34 +0000 |
|---|---|---|
| committer | Will Dillon <william@housedillon.com> | 2025-11-07 01:38:34 +0000 |
| commit | dba14aa0e30901e7c13e9b417b3ae520d55887bf (patch) | |
| tree | 985df38847d7b4c1af6690ef87147ddd1ea405dc | |
| parent | Finally building again; what a nightmare (diff) | |
| download | meshcore-rs-dba14aa0e30901e7c13e9b417b3ae520d55887bf.tar.gz meshcore-rs-dba14aa0e30901e7c13e9b417b3ae520d55887bf.zip | |
Try moving from PrivateKey to Static Secret
| -rw-r--r-- | Cargo.lock | 80 | ||||
| -rw-r--r-- | Cargo.toml | 5 | ||||
| -rw-r--r-- | src/crypto.rs | 263 | ||||
| -rw-r--r-- | src/packet.rs | 4 |
4 files changed, 320 insertions, 32 deletions
@@ -3,6 +3,17 @@ version = 4 [[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] name = "android_system_properties" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -74,6 +85,16 @@ dependencies = [ ] [[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] name = "const-oid" version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -222,6 +243,18 @@ dependencies = [ ] [[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] name = "hex" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -261,6 +294,15 @@ dependencies = [ ] [[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] name = "js-sys" version = "0.3.82" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -286,11 +328,13 @@ checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" name = "meshcore" version = "0.1.0" dependencies = [ + "aes", "chrono", "curve25519-dalek", "ed25519-dalek", "hex", "hmac", + "rand_core 0.9.3", "sha2", "structdiff", "tokio-util", @@ -347,12 +391,27 @@ dependencies = [ ] [[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.16", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.4", ] [[package]] @@ -429,7 +488,7 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ - "rand_core", + "rand_core 0.6.4", ] [[package]] @@ -521,6 +580,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + +[[package]] name = "wasm-bindgen" version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -625,13 +693,19 @@ dependencies = [ ] [[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] name = "x25519-dalek" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" dependencies = [ "curve25519-dalek", - "rand_core", + "rand_core 0.6.4", "serde", "zeroize", ] @@ -14,4 +14,7 @@ sha2 = { version = "0.10", default-features = false } hmac = { version = "0.12", default-features = false } ed25519-dalek = { version = "2.2.0", features = ["hazmat"]} x25519-dalek = { version = "2.0.1", features = ["static_secrets"]} -curve25519-dalek = "4.1.3"
\ No newline at end of file +curve25519-dalek = "4.1.3" +aes = "0.8.4" + +rand_core = { version = "0.9.3", features = ["os_rng"]}
\ No newline at end of file diff --git a/src/crypto.rs b/src/crypto.rs index d967f50..63532ab 100644 --- a/src/crypto.rs +++ b/src/crypto.rs @@ -1,22 +1,30 @@ use std::str::FromStr; use tokio_util::bytes::{Buf, BufMut, Bytes, BytesMut}; -use hex::{decode, decode_to_slice}; +use hex::{decode, decode_to_slice, encode}; use ed25519_dalek::hazmat::ExpandedSecretKey; -use curve25519_dalek::{constants, edwards::CompressedEdwardsY}; +use curve25519_dalek::{Scalar, constants, edwards::CompressedEdwardsY}; use x25519_dalek::StaticSecret; -// use aes::{Aes256, cipher::{Block, BlockDecryptMut, KeyInit}}; - +// This seems to be an absolute nightmare. GenericArray sucks +// but I can't seem to figure out how to pull it out of this +// stack of software +#[allow(deprecated)] +use aes::cipher::{ + BlockCipher, BlockEncrypt, BlockDecrypt, + generic_array::GenericArray, +}; +use aes::Aes256; use sha2::{Sha256}; use hmac::{Hmac, Mac}; type HmacSha256 = Hmac<Sha256>; -#[derive(Debug)] +#[derive(Debug, PartialEq)] pub enum MeshcoreCryptoError { KeyLengthError, - TryFromSliceError + TryFromSliceError, + HexDecodeError } #[derive(PartialEq, Debug)] @@ -66,14 +74,79 @@ impl SharedSecret { } } - pub fn decrypt(&self, ciphertext: Bytes) -> Bytes { - // let mut aes = Aes256::new((&self.0).into()); - - // let mut block = Block::clone_from_slice(&ciphertext); - // aes.decrypt_block_mut(&mut block); + pub fn decrypt(&self, ciphertext: Bytes) -> Result<Bytes, MeshcoreCryptoError> { + use aes::cipher::KeyInit; + + if let Ok(mut aes) = Aes256::new_from_slice(&self.0) { + let mut text = BytesMut::from(ciphertext); + + // 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.decrypt_block(&mut block); + chunk.copy_from_slice(&block); + } + + // Drop the padding + text.truncate(length); + + Ok(Bytes::from(text)) + } else { + Err(MeshcoreCryptoError::KeyLengthError) + } + } - // Bytes::copy_from_slice(&block) - ciphertext + pub fn encrypt(&self, plaintext: Bytes) -> Result<Bytes, MeshcoreCryptoError> { + use aes::cipher::KeyInit; + + if let Ok(aes) = Aes256::new_from_slice(&self.0) { + 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); + } + + Ok(Bytes::from(text)) + } else { + Err(MeshcoreCryptoError::KeyLengthError) + } } } @@ -118,17 +191,28 @@ impl FromStr for PublicKey { } impl FromStr for PrivateKey { - type Err = hex::FromHexError; + type Err = MeshcoreCryptoError; fn from_str(hex_str: &str) -> Result<Self, Self::Err> { - let hex = decode(hex_str)?; - - if let Ok(bytes) = TryInto::<[u8; 64]>::try_into(hex) { - let exp_sec_key = ExpandedSecretKey::from_bytes(&bytes); - - Ok(PrivateKey(exp_sec_key)) + if let Ok(hex) = decode(hex_str) { + match hex.len() { + 32 => { + let bytes: [u8; 32] = hex.try_into().unwrap(); + let exp_sec_key = ExpandedSecretKey { + scalar: Scalar::from_bytes_mod_order(bytes), + hash_prefix: [0_u8; 32] + }; + + Ok(PrivateKey(exp_sec_key)) + }, + 64 => { + let bytes: [u8; 64] = hex.try_into().unwrap(); + Ok(PrivateKey(ExpandedSecretKey::from_bytes(&bytes))) + } + _ => Err(MeshcoreCryptoError::KeyLengthError) + } } else { - Err(hex::FromHexError::InvalidStringLength) + Err(MeshcoreCryptoError::HexDecodeError) } } } @@ -165,9 +249,8 @@ pub struct Keychain { #[cfg(test)] mod tests { - // use curve25519_dalek::Scalar; use hex::{decode_to_slice, encode}; - + use rand_core::TryRngCore; use super::*; #[test] @@ -193,22 +276,27 @@ mod tests { #[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_private = PrivateKey::from_str("58f5052c13275c8a3f4863a082555fed7ea08b9dec2eb00dd86f6b5412174458").unwrap(); + let bob_private = PrivateKey::from_str("586c4cc29635af5865abe4f231bafbd373969725493c07271e02c7fec8ff3b5f").unwrap(); let alice_public = PublicKey::from(&alice_private); let bob_public = PublicKey::from(&bob_private); + assert!(alice_public.0.as_bytes().to_vec() == decode("f673f022cf0466a95db086adf19084d4cf06e56fdabb202dc833bab227a78561").unwrap()); + assert!( bob_public.0.as_bytes().to_vec() == decode("47cb9867a8df6d2637eabe399b70f58f30f250da3911a4add950e7fac6aa3e39").unwrap()); + 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); + assert!(left_secret.0.to_vec() == decode("ddbb8b5e70099817db83b48caa73f44a120b12a26072e5c29a023f16a2cd8b2a").unwrap()); + println!("Left shared secret: {}", encode(left_secret.0)); println!("Right shared secret: {}", encode(right_secret.0)); - // assert!(left_secret == right_secret); + assert!(left_secret == right_secret); } #[test] @@ -223,4 +311,127 @@ mod tests { let mac = group_secret.get_hmac(sample_data).unwrap(); assert!(0xC3C1 == mac); } + + #[test] + fn decrypt() { + let ciphertext = Bytes::copy_from_slice(&decode("354D619BAE9590E4D177DB7EEAF982F5BDCF78005D75157D9535FA90178F785D").unwrap()); + let secret = SharedSecret::new_from_group_secret(Bytes::copy_from_slice(&decode("8b3387e9c5cdea6ac9e5edbaa115cd72").unwrap())); + let cleartext = secret.decrypt(ciphertext).unwrap(); + println!("Cleartext: {}", encode(&cleartext)); + } + + #[test] + fn decrypt_online_example() { + let ciphertext = Bytes::copy_from_slice(&decode("95CCD78CB3FE2DE57774D552558AE954").unwrap()); + let secret = SharedSecret::new_from_group_secret(Bytes::copy_from_slice(&decode("FB05C2FF72C9E7F931E5FA1232AFA962EC04367D016F10493F82884823A6B529").unwrap())); + let cleartext = secret.decrypt(ciphertext).unwrap(); + let vec = cleartext.to_vec(); + let string = String::from_utf8_lossy(&vec); + assert!("Hello my world!!" == string); + } + + #[test] + 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("C8ED5DFD3DC316A7FA22450D6DC7097F52255F91E4E30F90F8CA3D25AC3E90E4").unwrap())); + let ciphertext = secret.encrypt(plaintext).unwrap(); + assert!(ciphertext == decode("FE952E7104F0AC1CFC42CA5EE3783961").unwrap()); + } + + #[test] + fn chunks() { + // Test my understanding of how Byte's chunk_exact methods work for use in (en|de)cryption + let array = decode("0102030405060708090A0B0C0D0E0F").unwrap(); + let mut text = BytesMut::from(array.as_slice()); + + // Copy the original length so we can truncate back down + let chunk_size = 4; + let length = text.len(); + + // Pad the bytes to it's an even multiple of the chunk size + let padding = chunk_size - (length % chunk_size); + text.reserve(padding); + text.put_bytes(0, padding); + + let chunks = text.chunks_exact_mut(4); + { + for chunk in chunks { + for item in chunk { *item = *item << 4; } + } + } + + // Drop the padding + text.truncate(length); + + println!("Result: {:#?}", encode(&text)); + + assert!(text == decode("102030405060708090A0B0C0D0E0F0").unwrap()); + + } + + // Example from the crate we're using + #[test] + fn aes_test() { + use aes::Aes128; + use aes::cipher::{ + BlockCipher, BlockEncrypt, BlockDecrypt, KeyInit, + generic_array::GenericArray, + }; + + let key = GenericArray::from([0u8; 16]); + let mut block = GenericArray::from([42u8; 16]); + + // Initialize cipher + let cipher = Aes128::new(&key); + + let block_copy = block.clone(); + + // Encrypt block in-place + println!("Before: {}", encode(&block)); + cipher.encrypt_block(&mut block); + println!("Crypted: {}", encode(&block)); + // And decrypt it back + cipher.decrypt_block(&mut block); + println!("Decrypted: {}", encode(&block)); + + assert_eq!(block, block_copy); + + // Implementation supports parallel block processing. Number of blocks + // processed in parallel depends in general on hardware capabilities. + // This is achieved by instruction-level parallelism (ILP) on a single + // CPU core, which is differen from multi-threaded parallelism. + let mut blocks = [block; 100]; + cipher.encrypt_blocks(&mut blocks); + + for block in blocks.iter_mut() { + cipher.decrypt_block(block); + assert_eq!(block, &block_copy); + } + + // `decrypt_blocks` also supports parallel block processing. + cipher.decrypt_blocks(&mut blocks); + + for block in blocks.iter_mut() { + cipher.encrypt_block(block); + assert_eq!(block, &block_copy); + } + } + + #[test] + fn shared_secrets_example() { + use x25519_dalek::{StaticSecret, PublicKey}; + + let a_s: [u8; 32] = decode("58f5052c13275c8a3f4863a082555fed7ea08b9dec2eb00dd86f6b5412174458").unwrap().try_into().unwrap(); + let b_s: [u8; 32] = decode("586c4cc29635af5865abe4f231bafbd373969725493c07271e02c7fec8ff3b5f").unwrap().try_into().unwrap(); + let alice_secret = StaticSecret::from(a_s); + let alice_public = PublicKey::from(&alice_secret); + + let bob_secret = StaticSecret::from(b_s); + let bob_public = PublicKey::from(&bob_secret); + + let alice_shared_secret = alice_secret.diffie_hellman(&bob_public); + let bob_shared_secret = bob_secret.diffie_hellman(&alice_public); + + assert_eq!(alice_shared_secret.as_bytes(), bob_shared_secret.as_bytes()); + } }
\ No newline at end of file diff --git a/src/packet.rs b/src/packet.rs index 5788100..cd403fd 100644 --- a/src/packet.rs +++ b/src/packet.rs @@ -173,7 +173,7 @@ enum PayloadVersion { impl From<u8> for PayloadVersion { fn from(value: u8) -> Self { let value = (value & 0xC0) >> 6; - assert!(value < 4, "Programming error in masking and bitshifting"); + assert!(value < 4, "Programming error in masking and bit-shifting"); match value { 0x00 => PayloadVersion::VersionOne, @@ -407,7 +407,7 @@ impl From<Bytes> for Advert { // Lastly the name... The flag is a little // irrelevant because the only different is - // whether the name is ommitted or just empty. + // whether the name is omitted or just empty. // I'm not going to make the distinction // The string is assumed to be utf8, but if that |
