diff --git a/Cargo.lock b/Cargo.lock index 251a756..566169c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3,10 +3,39 @@ version = 4 [[package]] -name = "bitflags" -version = "2.9.1" +name = "aead" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[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 = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] [[package]] name = "block-buffer" @@ -18,13 +47,10 @@ dependencies = [ ] [[package]] -name = "cc" -version = "1.2.20" +name = "bumpalo" +version = "3.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04da6a0d40b948dfc4fa8f5bbf402b0fc1a64a28dbf7d12ffd683550f2c1b63a" -dependencies = [ - "shlex", -] +checksum = "793db76d6187cd04dff33004d8e6c9cc4e05cd330500379d2394209271b4aeee" [[package]] name = "cfg-if" @@ -32,6 +58,16 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[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 = "cpufeatures" version = "0.2.17" @@ -48,9 +84,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", + "rand_core", "typenum", ] +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + [[package]] name = "curve25519-dalek" version = "4.1.3" @@ -111,22 +157,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", + "js-sys", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi", + "wasm-bindgen", ] [[package]] -name = "getrandom" -version = "0.3.3" +name = "ghash" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" dependencies = [ - "cfg-if", - "libc", - "r-efi", - "wasi 0.14.2+wasi-0.2.4", + "opaque-debug", + "polyval", ] +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "hmac" version = "0.12.1" @@ -136,32 +188,74 @@ dependencies = [ "digest", ] +[[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.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + [[package]] name = "libc" version = "0.2.172" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + [[package]] name = "ntor" version = "0.1.0" dependencies = [ + "aes-gcm", "curve25519-dalek", + "getrandom", + "hex", "hmac", - "rand", - "rand_core 0.6.4", - "ring", + "log", + "serde", "sha2", "x25519-dalek", ] [[package]] -name = "ppv-lite86" -version = "0.2.21" +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "polyval" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" dependencies = [ - "zerocopy", + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", ] [[package]] @@ -182,62 +276,13 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "r-efi" -version = "5.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" - -[[package]] -name = "rand" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" -dependencies = [ - "rand_chacha", - "rand_core 0.9.3", -] - -[[package]] -name = "rand_chacha" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" -dependencies = [ - "ppv-lite86", - "rand_core 0.9.3", -] - [[package]] name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "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.3", -] - -[[package]] -name = "ring" -version = "0.17.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" -dependencies = [ - "cc", - "cfg-if", - "getrandom 0.2.16", - "libc", - "untrusted", - "windows-sys", + "getrandom", ] [[package]] @@ -286,12 +331,6 @@ dependencies = [ "digest", ] -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - [[package]] name = "subtle" version = "2.6.1" @@ -322,10 +361,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" [[package]] -name = "untrusted" -version = "0.9.0" +name = "universal-hash" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] [[package]] name = "version_check" @@ -340,94 +383,60 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] -name = "wasi" -version = "0.14.2+wasi-0.2.4" +name = "wasm-bindgen" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" dependencies = [ - "wit-bindgen-rt", + "cfg-if", + "once_cell", + "wasm-bindgen-macro", ] [[package]] -name = "windows-sys" -version = "0.52.0" +name = "wasm-bindgen-backend" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" dependencies = [ - "windows-targets", + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", ] [[package]] -name = "windows-targets" -version = "0.52.6" +name = "wasm-bindgen-macro" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "quote", + "wasm-bindgen-macro-support", ] [[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" +name = "wasm-bindgen-macro-support" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] [[package]] -name = "wit-bindgen-rt" -version = "0.39.0" +name = "wasm-bindgen-shared" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" dependencies = [ - "bitflags", + "unicode-ident", ] [[package]] @@ -437,31 +446,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" dependencies = [ "curve25519-dalek", - "rand_core 0.6.4", + "rand_core", "serde", "zeroize", ] -[[package]] -name = "zerocopy" -version = "0.8.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "zeroize" version = "1.8.1" diff --git a/Cargo.toml b/Cargo.toml index c814d4d..6946914 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,11 +4,21 @@ version = "0.1.0" edition = "2024" [dependencies] -ring = "0.17.8" hmac = "0.12" sha2 = "0.10" curve25519-dalek = "4.1.1" x25519-dalek = {version="^2.0.1", features = ["static_secrets"] } -rand_core = {version="0.6.4", features = ["std"]} -rand = "0.9.1" + +# wasm incompatible +#ring = "0.17.8" +#rand_core = {version="0.6.4", features = ["std"]} +#rand = "0.9.1" + +#wasm compatible +getrandom = { version = "0.2", features = ["js"] } +aes-gcm = "0.10" +serde = { version = "1.0.219", features = ["derive"] } + +log = "0.4.26" +hex = "0.4.3" diff --git a/src/client.rs b/src/client.rs new file mode 100644 index 0000000..56410e4 --- /dev/null +++ b/src/client.rs @@ -0,0 +1,130 @@ +use x25519_dalek::PublicKey; +use sha2::Sha256; +use hmac::{Hmac, Mac}; +use hmac::digest::Digest; +use crate::common::{ + NTorCertificate, + InitSessionMessage, + InitSessionResponse, + NTorParty, + PrivatePublicKeyPair +}; +use crate::helpers::{generate_private_public_key_pair}; + +#[derive(Clone)] +pub struct NTorClient { + ephemeral_key_pair: PrivatePublicKeyPair, + pub(crate) shared_secret: Option>, +} + +impl NTorClient { + pub fn new() -> Self { + Self { + ephemeral_key_pair: PrivatePublicKeyPair { + private_key: None, + public_key: PublicKey::from([0; 32]), + }, + shared_secret: None, + } + } + + pub fn initialise_session(&mut self) -> InitSessionMessage { + self.ephemeral_key_pair = generate_private_public_key_pair(); + + InitSessionMessage { + client_ephemeral_public_key: self.ephemeral_key_pair.public_key, + } + } + + // Steps 15 - 20 of the Goldberg 2012 paper. + pub fn handle_response_from_server( + &mut self, + server_certificate: &NTorCertificate, + msg: &InitSessionResponse, + ) -> bool { + println!("Client:"); + + // Step 18: Compute the shared secret. + let mut buffer: Vec = Vec::new(); + + // ECDH Client private ephemeral * server static public key + let taken_private_key = self.ephemeral_key_pair.private_key.take().unwrap(); + let mut ecdh_result_1 = taken_private_key + .diffie_hellman(&msg.server_ephemeral_public_key) + .to_bytes() + .to_vec(); + println!("[Debug] ECDH result 1: {:?}", ecdh_result_1); + buffer.append(&mut ecdh_result_1); + + // ECDH Client private ephemeral * server ephemeral public Key + let mut ecdh_result_2 = taken_private_key + .diffie_hellman(&server_certificate.public_key) + .to_bytes() + .to_vec(); + println!("[Debug] ECDH result 2: {:?}", ecdh_result_2); + buffer.append(&mut ecdh_result_2); + + // Server id + buffer.append(&mut server_certificate.server_id.as_bytes().to_vec()); + + // Client ephemeral public + buffer.append(&mut self.ephemeral_key_pair.public_key.as_bytes().to_vec()); + + // Server ephemeral public + buffer.append(&mut msg.server_ephemeral_public_key.as_bytes().to_vec()); + + // "ntor" string identifier + buffer.append(&mut "ntor".as_bytes().to_vec()); + + // Instantiate and run hashing function + let mut hasher = Sha256::new(); + hasher.update(buffer); + let sha256_hash = hasher.finalize(); + let sha256_hash: &[u8; 32] = match sha256_hash.as_slice().try_into() { + Ok(array_ref) => array_ref, + Err(_) => { + panic!("Invalid sha256 hash length"); + } + }; + + let secret_key_prime = &sha256_hash[0..16]; + println!("[Debug] Client secret key prime: {:?}", secret_key_prime); + + let secret_key = &sha256_hash[16..]; + + // Step 19: Compute HMAC (t_b in the paper) + + let mut buffer: Vec = Vec::new(); + buffer.append(&mut server_certificate.server_id.as_bytes().to_vec()); + buffer.append(&mut msg.server_ephemeral_public_key.as_bytes().to_vec()); + buffer.append(&mut self.ephemeral_key_pair.public_key.as_bytes().to_vec()); + buffer.append(&mut "ntor".as_bytes().to_vec()); + buffer.append(&mut "server".as_bytes().to_vec()); + + let mut hmac_hash = Hmac::::new_from_slice(&buffer).unwrap(); + hmac_hash.update(secret_key_prime); + let computed_t_b_hash = hmac_hash.finalize().into_bytes().to_vec(); + + // assert that computed_t_b_hash equals t_hash generated by server + if computed_t_b_hash == msg.t_b_hash { + self.shared_secret = Some(secret_key.to_vec()); + + println!("Shared secret:"); + println!("{:?}\n", secret_key); + true + } else { + println!("Failed to verify the shared secret: try again bro."); + false + } + } +} + +impl NTorParty for NTorClient { + fn get_shared_secret(&self) -> Option> { + self.shared_secret.clone() + } + + fn set_shared_secret(&mut self, shared_secret: Vec) { + self.shared_secret = Some(shared_secret); + } +} diff --git a/src/common.rs b/src/common.rs new file mode 100644 index 0000000..1bd3478 --- /dev/null +++ b/src/common.rs @@ -0,0 +1,148 @@ +use x25519_dalek::{PublicKey, StaticSecret}; +use crate::helpers; +use crate::helpers::{key_derivation, vec_to_array32}; + +#[derive(Clone)] +pub struct PrivatePublicKeyPair { + // In the future, type StaticSecret should be reserved for the server's static and the EphemeralSecret reserved for the ephemeral private key. + // However, as a quirk of the nTOR protocol, we also need to use StaticSecret for the client's ephemeral private key hence why it is adopted here. + pub(crate) private_key: Option, + pub(crate) public_key: PublicKey, +} + +impl PrivatePublicKeyPair { + pub fn get_public_key(&self) -> PublicKey { + self.public_key + } + + pub fn get_private_key(&self) -> Option { + self.private_key.clone() + } +} + +#[derive(Clone)] +pub struct NTorCertificate { + pub(crate) public_key: PublicKey, + pub(crate) server_id: String, +} + +impl NTorCertificate { + pub fn new(public_key: Vec, server_id: String) -> Self { + let pub_key = TryInto::<[u8; 32]>::try_into(public_key).unwrap(); + NTorCertificate { + public_key: PublicKey::from(pub_key), + server_id + } + } + + pub fn public_key(&self) -> Vec { + self.public_key.to_bytes().to_vec() + } +} + +// In the paper, the outgoing message is ("ntor", B_id, client_ephemeral_public_key). +pub struct InitSessionMessage { + pub(crate) client_ephemeral_public_key: PublicKey, +} + +impl InitSessionMessage { + pub fn from(bytes: Vec) -> Self { + let u8_array = vec_to_array32(bytes); + InitSessionMessage { + client_ephemeral_public_key: PublicKey::from(u8_array), + } + } + + pub fn public_key(&self) -> Vec { + self.client_ephemeral_public_key.to_bytes().to_vec() + } +} + +// In the paper, the return message is ("ntor", server_ephemeral_public_key, t_b_hash). +pub struct InitSessionResponse { + pub(crate) server_ephemeral_public_key: PublicKey, + pub(crate) t_b_hash: Vec, +} + +impl InitSessionResponse { + pub fn new(public_key: Vec, t_b_hash: Vec) -> Self { + let pub_key = TryInto::<[u8; 32]>::try_into(public_key).unwrap(); + return InitSessionResponse { + server_ephemeral_public_key: PublicKey::from(pub_key), + t_b_hash, + } + } + + pub fn public_key(&self) -> Vec { + self.server_ephemeral_public_key.to_bytes().to_vec() + } + + pub fn t_b_hash(&self) -> Vec { + self.t_b_hash.clone() + } +} + +pub struct EncryptedMessage { + pub nonce: [u8; 12], + pub data: Vec +} + +pub trait NTorParty { + fn get_shared_secret(&self) -> Option>; + + fn set_shared_secret(&mut self, shared_secret: Vec); + + fn encrypt(&self, data: Vec) -> Result { + if let Some(key) = self.get_shared_secret() { + let encrypt_key = key_derivation(&key); + return match helpers::encrypt(encrypt_key, data) { + Ok((nonce, encrypted_message)) => { + Ok(EncryptedMessage { + nonce, + data: encrypted_message, + }) + } + Err(err) => Err(err) + } + } + Err("no encryption key found") + } + + fn decrypt(&self, encrypted_message: EncryptedMessage) -> Result, &'static str> { + if let Some(key) = self.get_shared_secret() { + let decrypt_key = key_derivation(&key); + return helpers::decrypt(encrypted_message.nonce, decrypt_key, encrypted_message.data); + } + Err("no decryption key found") + } + + fn wasm_encrypt(&self, data: Vec) -> Result<(Vec, Vec), &'static str> { + if let Some(key) = self.get_shared_secret() { + let encrypt_key = key_derivation(&key); + return match helpers::encrypt(encrypt_key, data) { + Ok((nonce, encrypted_message)) => { + Ok((nonce.to_vec(), encrypted_message)) + } + Err(err) => Err(err) + } + } + Err("no encryption key found") + } + + fn wasm_decrypt(&self, nonce: Vec, data: Vec) -> Result, &'static str> { + if let Some(key) = self.get_shared_secret() { + let decrypt_key = key_derivation(&key); + // return helpers::wasm_decrypt(nonce, decrypt_key, encrypted_message.data); + return match TryInto::<[u8; 12]>::try_into(nonce) { + Ok(nonce12) => { + return match helpers::decrypt(nonce12, decrypt_key, data) { + Ok(decrypted) => Ok(decrypted), + Err(err) => Err(err) + } + }, + Err(_err) => Err("invalid nonce") + } + } + Err("no decryption key found") + } +} \ No newline at end of file diff --git a/src/helpers.rs b/src/helpers.rs new file mode 100644 index 0000000..fe47e83 --- /dev/null +++ b/src/helpers.rs @@ -0,0 +1,71 @@ +use std::convert::TryInto; +use aes_gcm::{Aes256Gcm, Key, KeyInit, Nonce}; +use aes_gcm::aead::Aead; +use x25519_dalek::{PublicKey, StaticSecret}; +use crate::common::PrivatePublicKeyPair; + +/// Returns an array of zeros if conversion fails. +pub fn vec_to_array32(vec: Vec) -> [u8; 32] { + if vec.len() == 32 { + vec.try_into().unwrap() + } else { + [0u8; 32] + } +} + +pub fn generate_private_public_key_pair() -> PrivatePublicKeyPair { + let mut buf = [0u8; 32]; + getrandom::getrandom(&mut buf).expect("generate random failed"); + let private_key = StaticSecret::from(buf); + let public_key = PublicKey::from(&private_key); + + PrivatePublicKeyPair { + private_key: Some(private_key), + public_key, + } +} + +pub fn key_derivation(shared_secret: &Vec) -> Vec { + let mut encrypt_key = shared_secret.clone(); // fixme use a reliable kdf + encrypt_key.extend(shared_secret.clone()); + encrypt_key +} + +pub(crate) fn encrypt(key_bytes: Vec, data: Vec) -> Result<([u8; 12], Vec), &'static str> { + if key_bytes.len() != 32 { + return Err("Invalid key length for AES-256"); + } + + let key = Key::::from_slice(&key_bytes); + let cipher = Aes256Gcm::new(key); + + let mut nonce_bytes = [0u8; 12]; + getrandom::getrandom(&mut nonce_bytes).map_err(|_| "Random generation failed")?; + let nonce = Nonce::from_slice(&nonce_bytes); // 96-bits; unique per message + + let ciphertext = cipher + .encrypt(nonce, data.as_ref()) + .map_err(|_| "Encryption failed")?; + + Ok((nonce_bytes, ciphertext)) +} + +pub(crate) fn decrypt(nonce_bytes: [u8; 12], key: Vec, ciphertext: Vec) -> Result, &'static str> { + return match TryInto::<[u8; 32]>::try_into(key) { + Ok(key_bytes) => { + let key = Key::::from_slice(&key_bytes); + let cipher = Aes256Gcm::new(key); + let nonce = Nonce::from_slice(&nonce_bytes); + + let decrypted_data = cipher + .decrypt(nonce, ciphertext.as_ref()) + .map_err(|_| "Decryption failed")?; + + Ok(decrypted_data) + } + Err(_) => { + Err("Invalid key") + } + } +} + diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..ddadf0d --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,6 @@ +pub mod common; +pub mod server; +pub mod client; + +mod test; +pub mod helpers; \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 94943aa..adeac7f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,270 +1,13 @@ -use hmac::{Hmac, Mac}; -use rand_core::OsRng; -use sha2::{Digest, Sha256}; -use x25519_dalek::{PublicKey, StaticSecret}; - -mod test; - -struct PrivatePublicKeyPair { - // In the future, type StaticSecret should be reserved for the server's static and the EphemeralSecret reserved for the ephemeral private key. - // However, as a quirk of the nTOR protocol, we also need to use StaticSecret for the client's ephemeral private key hence why it is adopted here. - private_key: Option, - public_key: PublicKey, -} - -fn generate_private_public_key_pair() -> PrivatePublicKeyPair { - let private_key = StaticSecret::random_from_rng(OsRng); - let public_key = PublicKey::from(&private_key); - - PrivatePublicKeyPair { - private_key: Some(private_key), - public_key, - } -} - -struct Client { - ephemeral_key_pair: PrivatePublicKeyPair, - shared_secret: Option>, -} - -struct Server { - static_key_pair: PrivatePublicKeyPair, - ephemeral_key_pair: PrivatePublicKeyPair, - server_id: String, - shared_secret: Option>, -} - -struct Certificate { - public_key: PublicKey, - server_id: String, -} - -// In the paper, the outgoing message is ("ntor", B_id, client_ephemeral_public_key). -struct InitSessionMessage { - client_ephemeral_public_key: PublicKey, -} - -// In the paper, the return message is ("ntor", server_ephemeral_public_key, t_b_hash). -struct InitSessionResponse { - server_ephemeral_public_key: PublicKey, - t_b_hash: Vec, -} - -impl Client { - fn new() -> Self { - Self { - ephemeral_key_pair: PrivatePublicKeyPair { - private_key: None, - public_key: PublicKey::from([0; 32]), - }, - shared_secret: None, - } - } - - fn initialise_session(&mut self) -> InitSessionMessage { - self.ephemeral_key_pair = generate_private_public_key_pair(); - - InitSessionMessage { - client_ephemeral_public_key: self.ephemeral_key_pair.public_key, - } - } - - // Steps 15 - 20 of the Goldberg 2012 paper. - fn handle_response_from_server( - &mut self, - server_certificate: &Certificate, - msg: &InitSessionResponse, - ) -> bool { - println!("Client:"); - - // Step 18: Compute the shared secret. - let mut buffer: Vec = Vec::new(); - - // ECDH Client private ephemeral * server static public key - let taken_private_key = self.ephemeral_key_pair.private_key.take().unwrap(); - let mut ecdh_result_1 = taken_private_key - .diffie_hellman(&msg.server_ephemeral_public_key) - .to_bytes() - .to_vec(); - println!("[Debug] ECDH result 1: {:?}", ecdh_result_1); - buffer.append(&mut ecdh_result_1); - - // ECDH Client private ephemeral * server ephemeral public Key - let mut ecdh_result_2 = taken_private_key - .diffie_hellman(&server_certificate.public_key) - .to_bytes() - .to_vec(); - println!("[Debug] ECDH result 2: {:?}", ecdh_result_2); - buffer.append(&mut ecdh_result_2); - - // Server id - buffer.append(&mut server_certificate.server_id.as_bytes().to_vec()); - - // Client ephemeral public - buffer.append(&mut self.ephemeral_key_pair.public_key.as_bytes().to_vec()); - - // Server ephemeral public - buffer.append(&mut msg.server_ephemeral_public_key.as_bytes().to_vec()); - - // "ntor" string identifier - buffer.append(&mut "ntor".as_bytes().to_vec()); - - // Instantiate and run hashing function - let mut hasher = Sha256::new(); - hasher.update(buffer); - let sha256_hash = hasher.finalize(); - let sha256_hash: &[u8; 32] = match sha256_hash.as_slice().try_into() { - Ok(array_ref) => array_ref, - Err(_) => { - panic!("Invalid sha256 hash length"); - } - }; - - let secret_key_prime = &sha256_hash[0..16]; - println!("[Debug] Client secret key prime: {:?}", secret_key_prime); - - let secret_key = &sha256_hash[16..]; - - // Step 19: Compute HMAC (t_b in the paper) - - let mut buffer: Vec = Vec::new(); - buffer.append(&mut server_certificate.server_id.as_bytes().to_vec()); - buffer.append(&mut msg.server_ephemeral_public_key.as_bytes().to_vec()); - buffer.append(&mut self.ephemeral_key_pair.public_key.as_bytes().to_vec()); - buffer.append(&mut "ntor".as_bytes().to_vec()); - buffer.append(&mut "server".as_bytes().to_vec()); - - let mut hmac_hash = Hmac::::new_from_slice(&buffer).unwrap(); - hmac_hash.update(secret_key_prime); - let computed_t_b_hash = hmac_hash.finalize().into_bytes().to_vec(); - - // assert that computed_t_b_hash equals t_hash generated by server - if computed_t_b_hash == msg.t_b_hash { - self.shared_secret = Some(secret_key.to_vec()); - - println!("Shared secret:"); - println!("{:?}\n", secret_key); - true - } else { - println!("Failed to verify the shared secret: try again bro."); - false - } - } -} - -impl Server { - fn new(server_id: String) -> Self { - // In the future, implementations of static and ephemeral key pair generation should differ. - Self { - ephemeral_key_pair: PrivatePublicKeyPair { - private_key: None, - public_key: PublicKey::from([0; 32]), - }, - server_id, - shared_secret: None, - static_key_pair: generate_private_public_key_pair(), - } - } - - fn get_certificate(&self) -> Certificate { - // Upon implementation and deployment, it's the Service Provider that will create and then upload a certificate to the Layer8 Authentication Server. Likely, Layer8 will also provide the necessary functions to create one for the client. - Certificate { - public_key: self.static_key_pair.public_key, - server_id: self.server_id.clone(), - } - } - - fn accept_init_session_request( - &mut self, - init_msg: &InitSessionMessage, - ) -> InitSessionResponse { - println!("Server:"); - - // generate session-specific ephemeral key pair - self.ephemeral_key_pair = generate_private_public_key_pair(); - - let mut buffer: Vec = Vec::new(); - // client_ephemeral_public^server_ephemeral_private (X^y), - let taken_private_key = self.ephemeral_key_pair.private_key.take().unwrap(); - let mut ecdh_results_1 = taken_private_key - .diffie_hellman(&init_msg.client_ephemeral_public_key) - .to_bytes() - .to_vec(); - println!("[Debug] ECDH result 1: {:?}", ecdh_results_1); - buffer.append(&mut ecdh_results_1); - - // client_ephemeral_public^server_static_private (X^b), - let taken_private_key = self.static_key_pair.private_key.take().unwrap(); - let mut ecdh_results_2 = taken_private_key - .diffie_hellman(&init_msg.client_ephemeral_public_key) - .to_bytes() - .to_vec(); - println!("[Debug] ECDH result 2: {:?}", ecdh_results_2); - buffer.append(&mut ecdh_results_2); - - // server id - buffer.append(&mut self.server_id.as_bytes().to_vec()); - - // client_ephemeral_public (X) - buffer.append(&mut init_msg.client_ephemeral_public_key.to_bytes().to_vec()); - - // server_ephemeral_public (Y) - buffer.append(&mut self.ephemeral_key_pair.public_key.to_bytes().to_vec()); - - // "ntor" - buffer.append(&mut "ntor".as_bytes().to_vec()); - - // Instantiate sha256 hash function and compute - let mut hasher = Sha256::new(); - hasher.update(buffer); - let sha256_hash = hasher.finalize(); - let sha256_hash: &[u8; 32] = match sha256_hash.as_slice().try_into() { - Ok(array_ref) => array_ref, - Err(_) => { - panic!("Invalid sha256 hash length"); - } - }; - - let secret_key_prime = &sha256_hash[0..16]; - let secret_key = &sha256_hash[16..]; - println!("[Debug] Server secret key prime: {:?}", secret_key_prime); - - // Step 12: Compute HMAC (t_b in the paper): - let mut hmac_key_buffer: Vec = Vec::new(); - // server id - hmac_key_buffer.append(&mut self.server_id.as_bytes().to_vec()); - // server_ephemeral_public_key - hmac_key_buffer.append(&mut self.ephemeral_key_pair.public_key.to_bytes().to_vec()); - // client_ephemeral_public_key - hmac_key_buffer.append(&mut init_msg.client_ephemeral_public_key.to_bytes().to_vec()); - // "ntor" - hmac_key_buffer.append(&mut "ntor".as_bytes().to_vec()); - // "server" - hmac_key_buffer.append(&mut "server".as_bytes().to_vec()); - - let mut hmac_hash = Hmac::::new_from_slice(&hmac_key_buffer).unwrap(); - hmac_hash.update(secret_key_prime); - let t_b_hash = hmac_hash.finalize().into_bytes().to_vec(); - - self.shared_secret = Some(secret_key.to_vec()); - - println!("Shared secret:"); - println!("{:?}\n", secret_key); - - InitSessionResponse { - server_ephemeral_public_key: self.ephemeral_key_pair.public_key, - t_b_hash, - } - } -} +use ntor::client::NTorClient; +use ntor::server::NTorServer; fn main() { // Create a new client - let mut client = Client::new(); + let mut client = NTorClient::new(); // Spin up a server let server_id = String::from("my server id"); - let mut server = Server::new(server_id); + let mut server = NTorServer::new(server_id); // Client initializes session with the server let init_session_msg = client.initialise_session(); diff --git a/src/server.rs b/src/server.rs new file mode 100644 index 0000000..7a14eaa --- /dev/null +++ b/src/server.rs @@ -0,0 +1,145 @@ +use x25519_dalek::{PublicKey, StaticSecret}; +use sha2::Sha256; +use hmac::{Hmac, Mac}; +use hmac::digest::Digest; +use crate::common::{NTorCertificate, InitSessionMessage, InitSessionResponse, NTorParty, PrivatePublicKeyPair}; +use crate::helpers::{generate_private_public_key_pair}; + +pub struct NTorServer { + static_key_pair: PrivatePublicKeyPair, + ephemeral_key_pair: PrivatePublicKeyPair, + server_id: String, + pub(crate) shared_secret: Option>, +} + +impl NTorServer { + pub fn new(server_id: String) -> Self { + // In the future, implementations of static and ephemeral key pair generation should differ. + Self { + ephemeral_key_pair: PrivatePublicKeyPair { + private_key: None, + public_key: PublicKey::from([0; 32]), + }, + server_id, + shared_secret: None, + static_key_pair: generate_private_public_key_pair(), + } + } + + pub fn new_with_secret(server_id: String, secret: [u8; 32]) -> Self { + let static_private_key = StaticSecret::from(secret); + Self { + ephemeral_key_pair: PrivatePublicKeyPair { + private_key: None, + public_key: PublicKey::from([0; 32]), + }, + server_id, + shared_secret: None, + static_key_pair: PrivatePublicKeyPair { + private_key: Some(static_private_key.clone()), + public_key: PublicKey::from(&static_private_key), + }, + } + } + + pub fn get_certificate(&self) -> NTorCertificate { + // Upon implementation and deployment, it's the Service Provider that will create and then upload a certificate to the Layer8 Authentication Server. Likely, Layer8 will also provide the necessary functions to create one for the client. + NTorCertificate { + public_key: self.static_key_pair.public_key, + server_id: self.server_id.clone(), + } + } + + pub fn accept_init_session_request( + &mut self, + init_msg: &InitSessionMessage, + ) -> InitSessionResponse { + println!("Server:"); + + // generate session-specific ephemeral key pair + self.ephemeral_key_pair = generate_private_public_key_pair(); + + let mut buffer: Vec = Vec::new(); + // client_ephemeral_public^server_ephemeral_private (X^y), + let taken_private_key = self.ephemeral_key_pair.private_key.take().unwrap(); + let mut ecdh_results_1 = taken_private_key + .diffie_hellman(&init_msg.client_ephemeral_public_key) + .to_bytes() + .to_vec(); + println!("[Debug] ECDH result 1: {:?}", ecdh_results_1); + buffer.append(&mut ecdh_results_1); + + // client_ephemeral_public^server_static_private (X^b), + let taken_private_key = self.static_key_pair.private_key.take().unwrap(); + let mut ecdh_results_2 = taken_private_key + .diffie_hellman(&init_msg.client_ephemeral_public_key) + .to_bytes() + .to_vec(); + println!("[Debug] ECDH result 2: {:?}", ecdh_results_2); + buffer.append(&mut ecdh_results_2); + + // server id + buffer.append(&mut self.server_id.as_bytes().to_vec()); + + // client_ephemeral_public (X) + buffer.append(&mut init_msg.client_ephemeral_public_key.to_bytes().to_vec()); + + // server_ephemeral_public (Y) + buffer.append(&mut self.ephemeral_key_pair.public_key.to_bytes().to_vec()); + + // "ntor" + buffer.append(&mut "ntor".as_bytes().to_vec()); + + // Instantiate sha256 hash function and compute + let mut hasher = Sha256::new(); + hasher.update(buffer); + let sha256_hash = hasher.finalize(); + let sha256_hash: &[u8; 32] = match sha256_hash.as_slice().try_into() { + Ok(array_ref) => array_ref, + Err(_) => { + panic!("Invalid sha256 hash length"); + } + }; + + let secret_key_prime = &sha256_hash[0..16]; + let secret_key = &sha256_hash[16..]; + println!("[Debug] Server secret key prime: {:?}", secret_key_prime); + + // Step 12: Compute HMAC (t_b in the paper): + let mut hmac_key_buffer: Vec = Vec::new(); + // server id + hmac_key_buffer.append(&mut self.server_id.as_bytes().to_vec()); + // server_ephemeral_public_key + hmac_key_buffer.append(&mut self.ephemeral_key_pair.public_key.to_bytes().to_vec()); + // client_ephemeral_public_key + hmac_key_buffer.append(&mut init_msg.client_ephemeral_public_key.to_bytes().to_vec()); + // "ntor" + hmac_key_buffer.append(&mut "ntor".as_bytes().to_vec()); + // "server" + hmac_key_buffer.append(&mut "server".as_bytes().to_vec()); + + let mut hmac_hash = Hmac::::new_from_slice(&hmac_key_buffer).unwrap(); + hmac_hash.update(secret_key_prime); + let t_b_hash = hmac_hash.finalize().into_bytes().to_vec(); + + self.shared_secret = Some(secret_key.to_vec()); + + println!("Shared secret:"); + println!("{:?}\n", secret_key); + + InitSessionResponse { + server_ephemeral_public_key: self.ephemeral_key_pair.public_key, + t_b_hash, + } + } +} + +impl NTorParty for NTorServer { + fn get_shared_secret(&self) -> Option> { + self.shared_secret.clone() + } + + fn set_shared_secret(&mut self, shared_secret: Vec) { + self.shared_secret = Some(shared_secret); + } +} \ No newline at end of file diff --git a/src/test.rs b/src/test.rs index 64c1429..bdb35fc 100644 --- a/src/test.rs +++ b/src/test.rs @@ -1,6 +1,8 @@ #[cfg(test)] mod tests { - use crate::{Client, Server, generate_private_public_key_pair}; + use crate::client::NTorClient; + use crate::helpers::generate_private_public_key_pair; + use crate::server::NTorServer; #[test] fn test_generate_private_public_key_pair() { @@ -24,10 +26,10 @@ mod tests { #[test] fn test_ntor_handshake_shared_secret_generation() { // Create a new client - let mut client = Client::new(); + let mut client = NTorClient::new(); // Spin up a server - let mut server = Server::new(String::from("test_server_id")); + let mut server = NTorServer::new(String::from("test_server_id")); // Client initializes session with the server let init_session_msg = client.initialise_session();