From a50c9bfa1ae2cd8b00715aeab06b659a65cab6c4 Mon Sep 17 00:00:00 2001 From: Drazen Urch Date: Tue, 18 Apr 2023 14:07:34 +0200 Subject: [PATCH] Outfox compatibility layer (#3317) * Outfox compatibility layer * Lint --- Cargo.lock | 27 +++++- common/nymsphinx/forwarding/Cargo.toml | 1 + nym-connect/desktop/Cargo.lock | 27 +++++- nym-outfox/Cargo.toml | 3 + nym-outfox/src/error.rs | 11 ++- nym-outfox/src/format.rs | 35 ++++---- nym-outfox/src/packet.rs | 72 +++++++++++++++- nym-outfox/tests/unittests.rs | 110 +++++++++---------------- 8 files changed, 196 insertions(+), 90 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ee7de90b0d..19e3a63aff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3810,6 +3810,7 @@ dependencies = [ "fastrand", "getrandom 0.2.8", "rayon", + "sphinx-packet 0.1.0 (git+https://github.com/nymtech/sphinx.git)", "thiserror", "zeroize", ] @@ -4056,6 +4057,7 @@ dependencies = [ name = "nym-sphinx-forwarding" version = "0.1.0" dependencies = [ + "nym-outfox", "nym-sphinx-addressing", "nym-sphinx-params", "nym-sphinx-types", @@ -4086,7 +4088,7 @@ dependencies = [ name = "nym-sphinx-types" version = "0.2.0" dependencies = [ - "sphinx-packet", + "sphinx-packet 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -5887,6 +5889,29 @@ dependencies = [ "subtle 2.4.1", ] +[[package]] +name = "sphinx-packet" +version = "0.1.0" +source = "git+https://github.com/nymtech/sphinx.git#ca107d94360cdf8bbfbdb12fe5320ed74f80e40c" +dependencies = [ + "aes 0.7.5", + "arrayref", + "blake2", + "bs58", + "byteorder", + "chacha", + "curve25519-dalek", + "digest 0.9.0", + "hkdf 0.11.0", + "hmac 0.11.0", + "lioness", + "log", + "rand 0.7.3", + "rand_distr", + "sha2 0.9.9", + "subtle 2.4.1", +] + [[package]] name = "spin" version = "0.5.2" diff --git a/common/nymsphinx/forwarding/Cargo.toml b/common/nymsphinx/forwarding/Cargo.toml index e4931b7d8b..df071403a6 100644 --- a/common/nymsphinx/forwarding/Cargo.toml +++ b/common/nymsphinx/forwarding/Cargo.toml @@ -11,3 +11,4 @@ repository = { workspace = true } nym-sphinx-addressing = { path = "../addressing" } nym-sphinx-params = { path = "../params" } nym-sphinx-types = { path = "../types" } +nym-outfox = { path = "../../../nym-outfox" } diff --git a/nym-connect/desktop/Cargo.lock b/nym-connect/desktop/Cargo.lock index 0004985769..9b428b6a34 100644 --- a/nym-connect/desktop/Cargo.lock +++ b/nym-connect/desktop/Cargo.lock @@ -3566,6 +3566,7 @@ dependencies = [ "curve25519-dalek", "getrandom 0.2.8", "rayon", + "sphinx-packet 0.1.0 (git+https://github.com/nymtech/sphinx.git)", "thiserror", "zeroize", ] @@ -3736,6 +3737,7 @@ dependencies = [ name = "nym-sphinx-forwarding" version = "0.1.0" dependencies = [ + "nym-outfox", "nym-sphinx-addressing", "nym-sphinx-params", "nym-sphinx-types", @@ -3766,7 +3768,7 @@ dependencies = [ name = "nym-sphinx-types" version = "0.2.0" dependencies = [ - "sphinx-packet", + "sphinx-packet 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -5375,6 +5377,29 @@ dependencies = [ "subtle 2.4.1", ] +[[package]] +name = "sphinx-packet" +version = "0.1.0" +source = "git+https://github.com/nymtech/sphinx.git#ca107d94360cdf8bbfbdb12fe5320ed74f80e40c" +dependencies = [ + "aes 0.7.5", + "arrayref", + "blake2", + "bs58", + "byteorder", + "chacha", + "curve25519-dalek", + "digest 0.9.0", + "hkdf 0.11.0", + "hmac 0.11.0", + "lioness", + "log", + "rand 0.7.3", + "rand_distr", + "sha2 0.9.9", + "subtle 2.4.1", +] + [[package]] name = "spin" version = "0.5.2" diff --git a/nym-outfox/Cargo.toml b/nym-outfox/Cargo.toml index 48a05c842c..68102b5514 100644 --- a/nym-outfox/Cargo.toml +++ b/nym-outfox/Cargo.toml @@ -16,6 +16,9 @@ chacha20poly1305 = "0.10.1" getrandom = { version = "*", features = ["js"] } thiserror = "1" +sphinx-packet = { git = "https://github.com/nymtech/sphinx.git" } + + [dev-dependencies] criterion = "0.4" fastrand = "1.8" diff --git a/nym-outfox/src/error.rs b/nym-outfox/src/error.rs index d9a00d99b0..8e8711b579 100644 --- a/nym-outfox/src/error.rs +++ b/nym-outfox/src/error.rs @@ -1,3 +1,5 @@ +use std::array::TryFromSliceError; + use crate::lion::MIN_MESSAGE_LEN; use chacha20::cipher::InvalidLength; use thiserror::Error; @@ -11,10 +13,15 @@ pub enum OutfoxError { #[from] source: InvalidLength, }, - #[error("ChaCha20Poly1305 - Opaque error")] - ChaCha20Poly1305Error, + #[error("ChaCha20Poly1305 - {0}")] + ChaCha20Poly1305Error(String), #[error("Key length must be 32 bytes")] InvalidKeyLength, #[error("Message length must be greater then {MIN_MESSAGE_LEN} bytes")] InvalidMessageLength, + #[error("{source}")] + TryFromSluce { + #[from] + source: TryFromSliceError, + }, } diff --git a/nym-outfox/src/format.rs b/nym-outfox/src/format.rs index 1041b59c15..bc961377f0 100644 --- a/nym-outfox/src/format.rs +++ b/nym-outfox/src/format.rs @@ -19,7 +19,7 @@ //! traffic or dropping messages can be empued at each mix to frustrate traffic analysis. //! //! -//! A layer of mix processing is defined by three parameters, includes in the structure [MixStageParameters]: +//! A layer of mix processing is defined by three parameters, included in the structure [MixStageParameters]: //! * The `routing_information_length_bytes` (`R`) states the number of bytes representing //! routing information at this layer. //! * The `remaining_header_length_bytes` (`H`) represents the remaining bytes of the packet header. @@ -62,6 +62,7 @@ use chacha20poly1305::Tag; use curve25519_dalek::constants::ED25519_BASEPOINT_TABLE; use curve25519_dalek::montgomery::MontgomeryPoint; use curve25519_dalek::scalar::Scalar; +use sphinx_packet::route::Node; use std::convert::TryInto; @@ -69,6 +70,7 @@ const GROUPELEMENTBYTES: usize = 32; const TAGBYTES: usize = 16; use std::ops::Range; +use std::u8; use crate::error::OutfoxError; use crate::lion::*; @@ -181,10 +183,13 @@ impl MixStageParameters { pub fn encode_mix_layer( &self, buffer: &mut [u8], - user_secret_key: &Scalar, - mix_public_key: &MontgomeryPoint, - routing_data: &[u8], + user_secret_key: &[u8], + node: &Node, ) -> Result { + let routing_data = node.address.as_bytes().to_vec(); + let mix_public_key = MontgomeryPoint(*node.pub_key.as_bytes()); + let user_secret_key = Scalar::from_bytes_mod_order(user_secret_key.try_into()?); + if buffer.len() != self.incoming_packet_length() { return Err(OutfoxError::LenMismatch { expected: buffer.len(), @@ -199,20 +204,19 @@ impl MixStageParameters { }); } - let user_public_key = (&ED25519_BASEPOINT_TABLE * user_secret_key).to_montgomery(); + let user_public_key = (&ED25519_BASEPOINT_TABLE * &user_secret_key).to_montgomery(); let shared_key = user_secret_key * mix_public_key; // Copy rounting data into buffer - buffer[self.routing_data_range()].copy_from_slice(routing_data); + buffer[self.routing_data_range()].copy_from_slice(&routing_data); // Perform the AEAD let header_aead_key = ChaCha20Poly1305::new_from_slice(&shared_key.0[..])?; - // TODO: Should this be all 0s? - let nonce = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + let nonce = [0u8; 12]; let tag = header_aead_key .encrypt_in_place_detached(&nonce.into(), &[], &mut buffer[self.header_range()]) - .map_err(|_| OutfoxError::ChaCha20Poly1305Error)?; + .map_err(|e| OutfoxError::ChaCha20Poly1305Error(e.to_string()))?; // Copy Tag into buffer buffer[self.tag_range()].copy_from_slice(&tag[..]); @@ -229,9 +233,12 @@ impl MixStageParameters { pub fn decode_mix_layer( &self, buffer: &mut [u8], - mix_secret_key: &Scalar, + mix_secret_key: &[u8], ) -> Result { // Check the length of the incoming buffer is correct. + + let mix_secret_key = Scalar::from_bytes_mod_order(mix_secret_key.try_into()?); + if buffer.len() != self.incoming_packet_length() { return Err(OutfoxError::LenMismatch { expected: buffer.len(), @@ -240,12 +247,12 @@ impl MixStageParameters { } // Derive the shared key for this packet - let user_public_key = MontgomeryPoint(buffer[self.pub_element_range()].try_into().unwrap()); + let user_public_key = MontgomeryPoint(buffer[self.pub_element_range()].try_into()?); let shared_key = mix_secret_key * user_public_key; // Compute the AEAD and check the Tag, if wrong return Err - let header_aead_key = ChaCha20Poly1305::new_from_slice(&shared_key.0[..]).unwrap(); - let nonce = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + let header_aead_key = ChaCha20Poly1305::new_from_slice(&shared_key.0[..])?; + let nonce = [0; 12]; let tag_bytes = buffer[self.tag_range()].to_vec(); let tag = Tag::from_slice(&tag_bytes); @@ -257,7 +264,7 @@ impl MixStageParameters { &mut buffer[self.header_range()], tag.as_slice().try_into().unwrap(), ) - .map_err(|_| OutfoxError::ChaCha20Poly1305Error)?; + .map_err(|e| OutfoxError::ChaCha20Poly1305Error(e.to_string()))?; // Do a round of LION on the payload lion_transform_decrypt(&mut buffer[self.payload_range()], &shared_key.0)?; diff --git a/nym-outfox/src/packet.rs b/nym-outfox/src/packet.rs index 846bd433bc..ab17a88687 100644 --- a/nym-outfox/src/packet.rs +++ b/nym-outfox/src/packet.rs @@ -1,7 +1,73 @@ -use crate::format::MixCreationParameters; +use std::ops::Range; + +use crate::{ + error::OutfoxError, + format::{MixCreationParameters, MixStageParameters}, +}; + +use sphinx_packet::{packet::builder::DEFAULT_PAYLOAD_SIZE, route::Node}; -#[allow(dead_code)] pub struct OutfoxPacket { - params: MixCreationParameters, + mix_params: MixCreationParameters, payload: Vec, } + +pub const DEFAULT_ROUTING_INFO_SIZE: usize = 32; + +impl OutfoxPacket { + pub fn build( + payload: &[u8], + route: &[Node; 3], + user_secret_key: &[u8], + ) -> Result { + let mut mix_params = MixCreationParameters::new(DEFAULT_PAYLOAD_SIZE); + + for node in route.iter() { + mix_params.add_outer_layer(node.address.as_bytes_ref().len()); + } + + let padding = mix_params.total_packet_length() - payload.len(); + let mut buffer = vec![0; padding]; + buffer.extend_from_slice(payload); + + for (idx, node) in route.iter().rev().enumerate() { + let (range, stage_params) = mix_params.get_stage_params(idx); + stage_params.encode_mix_layer(&mut buffer[range], user_secret_key, node)?; + } + + Ok(OutfoxPacket { + mix_params, + payload: buffer, + }) + } + + pub fn stage_params(&self, layer_number: usize) -> (Range, MixStageParameters) { + self.mix_params().get_stage_params(layer_number) + } + + pub fn mix_params(&self) -> &MixCreationParameters { + &self.mix_params + } + + pub fn payload(&self) -> &[u8] { + &self.payload + } + + pub fn payload_range(&self) -> Range { + self.stage_params(2).1.payload_range() + } + + pub fn payload_mut(&mut self) -> &mut [u8] { + &mut self.payload + } + + pub fn decode_mix_layer( + &mut self, + layer: usize, + mix_secret_key: &[u8; 32], + ) -> Result<(), OutfoxError> { + let (range, params) = self.stage_params(layer); + params.decode_mix_layer(&mut self.payload_mut()[range], mix_secret_key)?; + Ok(()) + } +} diff --git a/nym-outfox/tests/unittests.rs b/nym-outfox/tests/unittests.rs index 0123e55779..b085210756 100644 --- a/nym-outfox/tests/unittests.rs +++ b/nym-outfox/tests/unittests.rs @@ -5,6 +5,12 @@ mod tests { use curve25519_dalek::constants::ED25519_BASEPOINT_TABLE; use curve25519_dalek::scalar::Scalar; + use nym_outfox::packet::OutfoxPacket; + use sphinx_packet::constants::NODE_ADDRESS_LENGTH; + use sphinx_packet::crypto::PublicKey; + use sphinx_packet::packet::builder::DEFAULT_PAYLOAD_SIZE; + use sphinx_packet::route::Node; + use sphinx_packet::route::NodeAddressBytes; use std::convert::TryInto; use nym_outfox::format::*; @@ -24,12 +30,11 @@ mod tests { payload_length_bytes: 1024, // 1kb }; - let user_secret_bytes = randombytes(32); - let mix_secret_bytes = randombytes(32); - - let user_secret = Scalar::from_bytes_mod_order(user_secret_bytes.try_into().unwrap()); - let mix_secret = Scalar::from_bytes_mod_order(mix_secret_bytes.try_into().unwrap()); - let mix_public_key = (&ED25519_BASEPOINT_TABLE * &mix_secret).to_montgomery(); + let user_secret = randombytes(32); + let mix_secret = randombytes(32); + let mix_secret_scalar = + Scalar::from_bytes_mod_order(mix_secret.clone().try_into().unwrap()); + let mix_public_key = (&ED25519_BASEPOINT_TABLE * &mix_secret_scalar).to_montgomery(); let routing = [0; 32]; @@ -37,13 +42,13 @@ mod tests { let mut new_buffer = buffer.clone(); + let node_address_bytes = NodeAddressBytes::from_bytes(routing); + let mix_public_key = PublicKey::from(*mix_public_key.as_bytes()); + + let node = Node::new(node_address_bytes, mix_public_key); + let _ = mix_params - .encode_mix_layer( - &mut new_buffer[..], - &user_secret, - &mix_public_key, - &routing[..], - ) + .encode_mix_layer(&mut new_buffer[..], &user_secret, &node) .unwrap(); assert!(new_buffer[mix_params.payload_range()] != buffer[mix_params.payload_range()]); @@ -76,67 +81,34 @@ mod tests { #[test] fn test_packet_params() { - // Dummy keys -- we will use the same key for each layer - let user_secret_bytes = randombytes(32); - let mix_secret_bytes = randombytes(32); + let user_secret = randombytes(32); - let user_secret = Scalar::from_bytes_mod_order(user_secret_bytes.try_into().unwrap()); - let mix_secret = Scalar::from_bytes_mod_order(mix_secret_bytes.try_into().unwrap()); - let mix_public_key = (&ED25519_BASEPOINT_TABLE * &mix_secret).to_montgomery(); - - let routing = [0; 32]; - - let mut params = MixCreationParameters::new(1025); - params.add_outer_layer(32); - params.add_outer_layer(32); - params.add_outer_layer(32); - - let mut buf = vec![0; params.total_packet_length()]; - - let (range0, layer_params0) = params.get_stage_params(0); - let _ = layer_params0 - .encode_mix_layer( - &mut buf[range0.clone()], - &user_secret, - &mix_public_key, - &routing[..], - ) - .unwrap(); - - let (range1, layer_params1) = params.get_stage_params(1); - let _ = layer_params1 - .encode_mix_layer( - &mut buf[range1.clone()], - &user_secret, - &mix_public_key, - &routing[..], - ) - .unwrap(); - - let (range2, layer_params2) = params.get_stage_params(2); - let _ = layer_params2 - .encode_mix_layer( - &mut buf[range2.clone()], - &user_secret, - &mix_public_key, - &routing[..], - ) - .unwrap(); - - assert!( - buf[params.total_packet_length() - 1025..params.total_packet_length()] != [0; 1025] + let (node1_pk, node1_pub) = sphinx_packet::crypto::keygen(); + let node1 = Node::new( + NodeAddressBytes::from_bytes([0u8; NODE_ADDRESS_LENGTH]), + node1_pub, + ); + let (node2_pk, node2_pub) = sphinx_packet::crypto::keygen(); + let node2 = Node::new( + NodeAddressBytes::from_bytes([1u8; NODE_ADDRESS_LENGTH]), + node2_pub, + ); + let (node3_pk, node3_pub) = sphinx_packet::crypto::keygen(); + let node3 = Node::new( + NodeAddressBytes::from_bytes([2u8; NODE_ADDRESS_LENGTH]), + node3_pub, ); - let _ = layer_params2 - .decode_mix_layer(&mut buf[range2], &mix_secret) - .unwrap(); + let route = [node1, node2, node3]; - let _ = layer_params1 - .decode_mix_layer(&mut buf[range1], &mix_secret) - .unwrap(); + let payload = randombytes(DEFAULT_PAYLOAD_SIZE); - let _ = layer_params0 - .decode_mix_layer(&mut buf[range0], &mix_secret) - .unwrap(); + let mut packet = OutfoxPacket::build(&payload, &route, &user_secret).unwrap(); + + packet.decode_mix_layer(2, &node1_pk.to_bytes()).unwrap(); + packet.decode_mix_layer(1, &node2_pk.to_bytes()).unwrap(); + packet.decode_mix_layer(0, &node3_pk.to_bytes()).unwrap(); + + assert_eq!(payload, &packet.payload()[packet.payload_range()]); } }