diff --git a/Cargo.lock b/Cargo.lock index b96cd60..cb7df14 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -269,6 +269,26 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom 0.2.17", + "once_cell", + "tiny-keccak", +] + [[package]] name = "convert_case" version = "0.10.0" @@ -350,6 +370,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + [[package]] name = "crypto-common" version = "0.1.7" @@ -578,6 +604,17 @@ dependencies = [ "version_check", ] +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "getrandom" version = "0.3.4" @@ -1757,6 +1794,15 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + [[package]] name = "tinyvec" version = "1.11.0" @@ -1824,6 +1870,7 @@ name = "unshell" version = "0.1.0" dependencies = [ "chrono", + "const-random", "crossbeam-channel", "ratatui", "rkyv", diff --git a/Cargo.toml b/Cargo.toml index ae45e4f..ef2c4bf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,12 +18,14 @@ repository = "https://github.com/Astatin3/unshell" include = ["LICENSE", "**/*.rs", "Cargo.toml"] [workspace.dependencies] -rkyv = "0.8.16" -thiserror = "2.0.18" -chrono = "0.4.44" -static_init = "1.0.4" +rkyv = "0.8.16" +thiserror = "2.0.18" +chrono = "0.4.44" +static_init = "1.0.4" portable-pty = "0.9.0" crossbeam-channel = "0.5.15" +const-random = "0.1.18" + ratatui = "0.30.0" @@ -44,7 +46,7 @@ edition.workspace = true description = "Pure no_std implementation of the UnShell Protocol" [features] -# default = ["interface_ratatui"] +default = ["counter_shuffle_feistel_lcg"] log = [] log_debug = ["log", "dep:chrono"] @@ -52,17 +54,26 @@ log_debug = ["log", "dep:chrono"] interface = [] interface_ratatui = ["interface", "dep:ratatui"] +counter_shuffle_none = [] +counter_shuffle_feistel = [] +counter_shuffle_feistel_lcg = [] + [dependencies] rkyv = { workspace = true } thiserror = { workspace = true, optional = true } chrono = { workspace = true, optional = true } static_init = { workspace = true } +const-random = { workspace = true } + ratatui = { workspace = true, optional = true } [dev-dependencies] crossbeam-channel.workspace = true + +[build-dependencies] + [profile.minimize] inherits = "release" strip = true # Strip symbols from the binary diff --git a/examples/hashtest.rs b/examples/hash_test.rs similarity index 100% rename from examples/hashtest.rs rename to examples/hash_test.rs diff --git a/src/crypto/ordering.rs b/src/crypto/feistel.rs similarity index 100% rename from src/crypto/ordering.rs rename to src/crypto/feistel.rs diff --git a/src/crypto/feistel_state.rs b/src/crypto/feistel_state.rs new file mode 100644 index 0000000..d554bd3 --- /dev/null +++ b/src/crypto/feistel_state.rs @@ -0,0 +1,67 @@ +use crate::crypto::feistel_shuffle; + +#[cfg(feature = "counter_shuffle_none")] +pub type Counter = NoShuffle; +#[cfg(feature = "counter_shuffle_feistel")] +pub type Counter = FeistelShuffle; +#[cfg(feature = "counter_shuffle_feistel_lcg")] +pub type Counter = FeistelLCGShuffle; + +const NONCE16_1: u16 = const_random::const_random!(u16); +const NONCE16_2: u16 = const_random::const_random!(u16); +const NONCE32: u32 = const_random::const_random!(u32); + +pub struct NoShuffle(u16); + +/// Linear shuffle, no randomization, just a random starting point and step size +impl NoShuffle { + pub fn new() -> Self { + Self(NONCE16_1) + } + + pub fn next(&mut self) -> u16 { + self.0 = self.0.wrapping_add(1); + self.0 + } +} + +/// Shuffle all 16 bit numbers, an actual shuffle +/// But this still stores local values in a linear format +pub struct FeistelShuffle(u16, u32); + +impl FeistelShuffle { + pub fn new() -> Self { + Self(NONCE16_1, NONCE32) + } + + pub fn next(&mut self) -> u16 { + self.0 = self.0.wrapping_add(NONCE16_2); + feistel_shuffle(self.0, self.1) + } +} + +/// Linear recursive shuffle, +/// feeds back into itself and doesn't store the actual state. +/// Harder to decompile +pub struct FeistelLCGShuffle { + state: u16, + a: u16, // Multiplier (must be 1 mod 4) + c: u16, // Increment (must be odd) +} + +impl FeistelLCGShuffle { + pub fn new() -> Self { + let seed = NONCE32; + let a = (((seed & 0x3FFF) as u16) << 2) | 1; + let c = ((seed >> 16) as u16) | 1; + Self { state: 0, a, c } + } + + pub fn next(&mut self) -> u16 { + // 1. Advance state using LCG (Guarantees single cycle of 65536) + self.state = self.state.wrapping_mul(self.a).wrapping_add(self.c); + + // 2. Apply Feistel shuffle to the state (Adds randomness) + feistel_shuffle(self.state, self.a as u32) + } +} diff --git a/src/crypto/mod.rs b/src/crypto/mod.rs index ebb81c4..beb35ca 100644 --- a/src/crypto/mod.rs +++ b/src/crypto/mod.rs @@ -1,10 +1,27 @@ use alloc::string::String; -mod hash; -mod ordering; +// TODO: Make this seed dependent on env var; +pub const GLOBAL_SEED: u32 = 0xDEAFBEEF; +// pub const GLOBAL_NONCE: u32 = { +// let time = match u128::from_str_radix(env!("BUILD_TIME"), 10) { +// Ok(i) => i, +// Err(_) => panic!("Failed to parse BUILD_TIME"), +// }; -pub use hash::sha256; -pub use ordering::feistel_shuffle; +// GLOBAL_SEED ^ (time as u32) +// }; + +mod feistel; +#[allow(dead_code)] +mod feistel_state; +mod sha256; + +pub use feistel::feistel_shuffle; +pub use feistel_state::{Counter, FeistelLCGShuffle, FeistelShuffle, NoShuffle}; +pub use sha256::sha256; + +#[cfg(test)] +mod tests; #[macro_export] macro_rules! hash_256 { diff --git a/src/crypto/hash.rs b/src/crypto/sha256.rs similarity index 100% rename from src/crypto/hash.rs rename to src/crypto/sha256.rs diff --git a/src/crypto/tests.rs b/src/crypto/tests.rs new file mode 100644 index 0000000..0a954a9 --- /dev/null +++ b/src/crypto/tests.rs @@ -0,0 +1,40 @@ +use crate::crypto::{FeistelLCGShuffle, FeistelShuffle, NoShuffle}; + +#[test] +fn test_linear_shuffle() { + let mut seen = [false; 65536]; + let mut counter = NoShuffle::new(); + for _ in 0..65535 { + let val = counter.next(); + + assert!(!seen[val as usize], "Collision detected"); + + seen[val as usize] = true; + } +} + +#[test] +fn test_feistel_shuffle() { + let mut seen = [false; 65536]; + let mut counter = FeistelShuffle::new(); + for _ in 0..65535 { + let val = counter.next(); + + assert!(!seen[val as usize], "Collision detected"); + + seen[val as usize] = true; + } +} + +#[test] +fn test_fristel_lcg_shuffle() { + let mut seen = [false; 65536]; + let mut counter = FeistelLCGShuffle::new(); + for _ in 0..65535 { + let val = counter.next(); + + assert!(!seen[val as usize], "Collision detected"); + + seen[val as usize] = true; + } +} diff --git a/src/protocol/endpoint/hooks.rs b/src/protocol/endpoint/hooks.rs index 6e14043..0b4f977 100644 --- a/src/protocol/endpoint/hooks.rs +++ b/src/protocol/endpoint/hooks.rs @@ -16,10 +16,11 @@ impl Endpoint { /// reuse an id before the previous route has closed. If every `u16` id is active /// the function panics; that is a hard local resource exhaustion condition, not a /// recoverable packet error. + /// + /// TODO: Reevaluate this method of allocation checking. It can be quite slow pub fn allocate_hook_id(&mut self) -> HookID { for _ in 0..=HookID::MAX { - let candidate = self.last_hook; - self.last_hook = self.last_hook.wrapping_add(1); + let candidate = self.last_hook.next(); if !self.hooks.contains_key(&candidate) { return candidate; diff --git a/src/protocol/endpoint/mod.rs b/src/protocol/endpoint/mod.rs index 495c985..10ac013 100644 --- a/src/protocol/endpoint/mod.rs +++ b/src/protocol/endpoint/mod.rs @@ -5,15 +5,17 @@ pub use hooks::HookID; use alloc::{boxed::Box, vec::Vec}; -use crate::protocol::{ConnectionSet, HookMap, Leaf, Packet, Path, RouteMap}; +use crate::{ + crypto::Counter, + protocol::{ConnectionSet, HookMap, Leaf, Packet, Path, RouteMap}, +}; pub struct Endpoint { // This endpoint's identifier pub id: u32, // A counter that creates unique hook IDs. - // TODO: Randomize the hooks for more obfuscation - pub(crate) last_hook: u16, + pub(crate) last_hook: Counter, // Absolute path for this node. Must be set by some leaf pub path: Path, @@ -36,7 +38,7 @@ impl Endpoint { Self { id, // Init the hook at 0, which will increment - last_hook: 0, + last_hook: Counter::new(), // Set the current path as an empty vec path: Vec::new(),