diff --git a/Cargo.toml b/Cargo.toml index faaa945330..c3afc28de5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -76,6 +76,7 @@ members = [ "service-providers/network-statistics", "nym-api", "nym-api/nym-api-requests", + "nym-outfox", "tools/nym-cli", "tools/ts-rs-cli" ] diff --git a/nym-outfox/Cargo.toml b/nym-outfox/Cargo.toml new file mode 100644 index 0000000000..48a05c842c --- /dev/null +++ b/nym-outfox/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "nym-outfox" +version = "0.1.0" +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +rayon = "1.5.1" +blake3 = "1.3" +zeroize = "1.5" +chacha20 = "0.9.0" +curve25519-dalek = "3.2" +chacha20poly1305 = "0.10.1" +# Need this star over here to pull in js into getrandom +getrandom = { version = "*", features = ["js"] } +thiserror = "1" + +[dev-dependencies] +criterion = "0.4" +fastrand = "1.8" diff --git a/nym-outfox/src/error.rs b/nym-outfox/src/error.rs new file mode 100644 index 0000000000..afbba2fc4b --- /dev/null +++ b/nym-outfox/src/error.rs @@ -0,0 +1,20 @@ +use crate::lion::MIN_MESSAGE_LEN; +use chacha20::cipher::InvalidLength; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum OutFoxError { + #[error("Lengths mismatch, expected: {expected}, got: {got}")] + LenMismatch { expected: usize, got: usize }, + #[error("{source}")] + ChaCha20InvalidLength { + #[from] + source: InvalidLength, + }, + #[error("ChaCha20Poly1305 - Opaque error")] + ChaCha20Poly1305Error, + #[error("Key length must be 32 bytes")] + InvalidKeyLength, + #[error("Message length must be greater then {MIN_MESSAGE_LEN} bytes")] + InvalidMessageLength, +} diff --git a/nym-outfox/src/format.rs b/nym-outfox/src/format.rs new file mode 100644 index 0000000000..2ddf895e1f --- /dev/null +++ b/nym-outfox/src/format.rs @@ -0,0 +1,267 @@ +//! # The `outfox` format +//! +//! We define a simple mix packet format geared towards simplicity and performance with the features that are +//! specifically required by a stratified mix topology. +//! +//! `Outfox` assumes that all +//! paths are the same length (no need to hide path lengths), mixes are arranged in layers and therefore +//! know their position in a message path (no need to hide this position). These assumptions allow us +//! to do away with some of the padding traditionally used; further we prioritize efficient computation +//! over very low-bandwidth, as it seems the rate of public key operations is a bottleneck for mixes +//! rather than the availablility of bandwidth. +//! +//! ## Overview and Parameters +//! +//! In a mix network with a stratified topology packets are mixed by nodes at each of the layers. Each layer +//! 'strips' the packet from one layer of encryption, recovers the address of the mix at the next layer, and +//! passes the decoded packet to them. An identifier per processed message is stored and checked to prevent +//! replays of processed messages at each layer. Additional measures, such as adding delays, adding dummy +//! 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]: +//! * 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. +//! * The `payload_length_bytes` (`P`). +//! +//! In addition we define two system-wide constants, namely `GROUPELEMENTBYTES` (`GE`=32) and +//! `TAGBYTES` (`T`=24). +//! +//! ## Packet format, decoding +//! +//! A mix at this layer takes in messages of length `GE+T+R+H+P`, and outputs messages of length `H+P`. +//! +//! An input message is processed as follows: +//! +//! * The input packet is parsed as a `[Pk, Tag, Header, Payload]` of length `[GE, T, R+H, P]` respectivelly. +//! * A master key is derived by performing scalar multiplication with the mix secret 's', ie `K = s * Pk`. +//! The master key is stored and checked for duplicates (if it is found processing ends.) +//! * The master key is used to perform AEAD decryption of the `Header` with an IV of zeros and the `tag`. If +//! decryption fails processing ends. Otherwise the Header is parsed as `[Routing, Next_Header]` of length +//! `[R, H]` respectivelly. The routing data `Routing` can be used by the mix to dertermine the next mix. +//! * Finally, the master key is used to perform lion decoding of the `Payload` into `Next_Payload`. +//! * The output packet for the next mix is `[Next_Header, Next_Payload]`. +//! +//! As an AEAD we use `chacha20poly1305_ietf` and for public key operations we use `curve25519`. +//! +//! ## Packet encoding +//! +//! Encoding is +//! performed layer by layer starting with the last hop on the route, and ending with the first. At each stage +//! of encoding a new Secret key `Sk` and corresponding `Pk` is chosen. The layer master key for the layer is +//! derived using the mix public key. And the master key is used to AEAD encrypt the concatenation of the +//! routing data for the layer, and the remaining Header; separately the master key is used to lion encrypt +//! the payload. The process is repeated for each layer (from last to first) to construct the full message. + +use chacha20poly1305::AeadInPlace; +use chacha20poly1305::ChaCha20Poly1305; +use chacha20poly1305::KeyInit; + +use chacha20poly1305::Tag; +use curve25519_dalek::constants::ED25519_BASEPOINT_TABLE; +use curve25519_dalek::montgomery::MontgomeryPoint; +use curve25519_dalek::scalar::Scalar; + +use std::convert::TryInto; + +const GROUPELEMENTBYTES: usize = 32; +const TAGBYTES: usize = 16; + +use std::ops::Range; + +use crate::error::OutFoxError; +use crate::lion::*; + +/// A structure that holds mix packet construction parameters. These incluse the length +/// of the routing information at each hop, the number of hops, and the payload length. +pub struct MixCreationParameters { + /// The routing length is inner first, so \[0\] is the innermost routing length, etc (in bytes) + pub routing_information_length_by_stage: Vec, + /// The payload length (in bytes) + pub payload_length_bytes: usize, +} + +impl MixCreationParameters { + /// Create a set of parameters for a mix packet format. + pub fn new(payload_length_bytes: usize) -> MixCreationParameters { + MixCreationParameters { + routing_information_length_by_stage: Vec::new(), + payload_length_bytes, + } + } + + /// Add another outer layer containing some byte length of routing data. + pub fn add_outer_layer(&mut self, routing_information_length_bytes: usize) { + self.routing_information_length_by_stage + .push(routing_information_length_bytes); + } + + /// The length of the buffer needed to build a packet. + pub fn total_packet_length(&self) -> usize { + let mut len = self.payload_length_bytes; + for stage_len in &self.routing_information_length_by_stage { + len += stage_len + GROUPELEMENTBYTES + TAGBYTES + } + len + } + + /// Get the mix packet parameters for a single stage of mixing. + pub fn get_stage_params(&self, layer_number: usize) -> (Range, MixStageParameters) { + assert!(layer_number < self.routing_information_length_by_stage.len()); + + let mut remaining_header_length_bytes = 0; + for (i, stage_len) in self.routing_information_length_by_stage.iter().enumerate() { + if i == layer_number { + let params = MixStageParameters { + routing_information_length_bytes: *stage_len, + remaining_header_length_bytes, + payload_length_bytes: self.payload_length_bytes, + }; + + let total_size = self.total_packet_length(); + let inner_size = params.incoming_packet_length(); + + return (total_size - inner_size..total_size, params); + } else { + remaining_header_length_bytes += stage_len + GROUPELEMENTBYTES + TAGBYTES; + } + } + + unreachable!(); + } +} + +/// A structure representing the parameters of a single stage of mixing. +pub struct MixStageParameters { + /// The routing information length for this stage of mixing + pub routing_information_length_bytes: usize, + /// The reamining header length for this stage of mixing + pub remaining_header_length_bytes: usize, + /// The payload length + pub payload_length_bytes: usize, +} + +impl MixStageParameters { + pub fn incoming_packet_length(&self) -> usize { + GROUPELEMENTBYTES + TAGBYTES + self.outgoing_packet_length() + } + + pub fn outgoing_packet_length(&self) -> usize { + self.routing_information_length_bytes + + self.remaining_header_length_bytes + + self.payload_length_bytes + } + + pub fn pub_element_range(&self) -> Range { + 0..GROUPELEMENTBYTES + } + + pub fn tag_range(&self) -> Range { + GROUPELEMENTBYTES..GROUPELEMENTBYTES + TAGBYTES + } + + pub fn routing_data_range(&self) -> Range { + GROUPELEMENTBYTES + TAGBYTES + ..GROUPELEMENTBYTES + TAGBYTES + self.routing_information_length_bytes + } + + pub fn header_range(&self) -> Range { + GROUPELEMENTBYTES + TAGBYTES + ..GROUPELEMENTBYTES + + TAGBYTES + + self.routing_information_length_bytes + + self.remaining_header_length_bytes + } + + pub fn payload_range(&self) -> Range { + self.incoming_packet_length() - self.payload_length_bytes..self.incoming_packet_length() + } + + pub fn encode_mix_layer( + &self, + buffer: &mut [u8], + user_secret_key: &Scalar, + mix_public_key: &MontgomeryPoint, + routing_data: &[u8], + ) -> Result { + if buffer.len() != self.incoming_packet_length() { + return Err(OutFoxError::LenMismatch { + expected: buffer.len(), + got: self.incoming_packet_length(), + }); + } + + if routing_data.len() != self.routing_information_length_bytes { + return Err(OutFoxError::LenMismatch { + expected: routing_data.len(), + got: self.routing_information_length_bytes, + }); + } + + 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); + + // 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 tag = header_aead_key + .encrypt_in_place_detached(&nonce.into(), &[], &mut buffer[self.header_range()]) + .map_err(|_| OutFoxError::ChaCha20Poly1305Error)?; + + // Copy Tag into buffer + buffer[self.tag_range()].copy_from_slice(&tag[..]); + + // Copy own public key into buffer + buffer[self.pub_element_range()].copy_from_slice(&user_public_key.0[..]); + + // Do a round of LION on the payload + lion_transform_encrypt(&mut buffer[self.payload_range()], &shared_key.0)?; + + Ok(shared_key) + } + + pub fn decode_mix_layer( + &self, + buffer: &mut [u8], + mix_secret_key: &Scalar, + ) -> Result { + // Check the length of the incoming buffer is correct. + if buffer.len() != self.incoming_packet_length() { + return Err(OutFoxError::LenMismatch { + expected: buffer.len(), + got: self.incoming_packet_length(), + }); + } + + // Derive the shared key for this packet + let user_public_key = MontgomeryPoint(buffer[self.pub_element_range()].try_into().unwrap()); + 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 tag_bytes = buffer[self.tag_range()].to_vec(); + let tag = Tag::from_slice(&tag_bytes); + + header_aead_key + .decrypt_in_place_detached( + &nonce.into(), + &[], + &mut buffer[self.header_range()], + tag.as_slice().try_into().unwrap(), + ) + .map_err(|_| OutFoxError::ChaCha20Poly1305Error)?; + + // Do a round of LION on the payload + lion_transform_decrypt(&mut buffer[self.payload_range()], &shared_key.0)?; + + Ok(shared_key) + } +} diff --git a/nym-outfox/src/lib.rs b/nym-outfox/src/lib.rs new file mode 100644 index 0000000000..fbfcc32078 --- /dev/null +++ b/nym-outfox/src/lib.rs @@ -0,0 +1,3 @@ +pub mod error; +pub mod format; +pub mod lion; diff --git a/nym-outfox/src/lion.rs b/nym-outfox/src/lion.rs new file mode 100644 index 0000000000..e4ea5e2c55 --- /dev/null +++ b/nym-outfox/src/lion.rs @@ -0,0 +1,114 @@ +//! # The lion all-or-nothing transform +//! +//! The lion transform implements a keyed permutation (block cipher) with a +//! variable length block size. It takes a key of 32 bytes, and a message of +//! length >= 48 bytes. +//! +//! The cryptographic primitives used to implement the transform are a +//! stream cipher `PRF1(IV, KEY)` (using `crypto_stream_xsalsa20`), a message authentication +//! code `PRF2(MSG, KEY)` (using `HMAC-SHA-512-256`) and a key derivation function `KDF(KEY, ID)` +//! (using `Blake2b`). +//! +//! The message to encode is split into two parts `M = [L0, R0]`, where L is 24 bytes, and +//! R is the remaining of the message. +//! +//! Encoding then proceeds in 3 steps: +//! * `R1 = PRF1(L0, KDF(key, subkey_0)) XOR R0;` +//! * `L1 = PRF2(R1, KDF(key, subkey_1)) XOR L0;` +//! * `R2 = PRF1(L1, KDF(key, subkey_2)) XOR R1;` +//! +//! The output of the transform is the concatenated byte string `M' = [L1, R2]` which has the same +//! length as the original message. +//! +//! ## Manual key schedule. +//! +//! If you just want to encode / decode using lion as a wide-block block cipher simply use the +//! [lion_transform_encrypt] and [lion_transform_decrypt] functions. +//! +//! If you know what you are doing you can determine your own key schedule for the transform. The +//! key schedule for encypt and decrypt are [1, 2, 3] and [3, 2, 1] respectivelly. You may define +//! a key schedule that is symmetric (such as [1, 2, 1]) to build a transform T(k,m) that has the +//! property m = T(k, T(k, m)). + +use chacha20::cipher::{KeyIvInit, StreamCipher}; +use chacha20::Key; +use chacha20::XChaCha20; +use chacha20::XNonce; +use zeroize::Zeroize; + +use crate::error::OutFoxError; + +pub const MIN_MESSAGE_LEN: usize = 24 * 2; +const CONTEXT: &str = "LIONKEYS"; +const TAG_LEN: usize = 24; + +/// The lion transform encryption function. +/// +/// The `key` must be 32 bytes, and the `message` >= 48. The message is +/// mutated to the encrypted message. +pub fn lion_transform_encrypt(message: &mut [u8], key: &[u8]) -> Result<(), OutFoxError> { + lion_transform(message, key, [1, 2, 3]) +} + +/// The lion transform decryption function. +/// +/// The `key` must be 32 bytes, and the `message` >= 48. The message +/// is mutated to the decrypted message. +pub fn lion_transform_decrypt(message: &mut [u8], key: &[u8]) -> Result<(), OutFoxError> { + lion_transform(message, key, [3, 2, 1]) +} + +/// The core of the lion transform function, that takes a message and a key, +/// and applies the all-or-nothing transform. The key schedule represents the +/// values of the 3 subkeys used by the 3 phases of the transform. +/// +/// The `key` must be 32 bytes, and the `message` >= 48. +/// +/// Unless you know what you are doing use [lion_transform_encrypt] and +/// [lion_transform_decrypt] instead. +pub fn lion_transform( + message: &mut [u8], + key: &[u8], + key_schedule: [u64; 3], +) -> Result<(), OutFoxError> { + if key.len() != 32 { + return Err(OutFoxError::InvalidKeyLength); + } + + if message.len() < MIN_MESSAGE_LEN { + return Err(OutFoxError::InvalidMessageLength); + } + + // Stage 1: Use stream cipher with Nonce from left size, to xor to the right side + let mut derived_key = blake3::derive_key(&format!("{}{}", CONTEXT, key_schedule[0]), key); + let lion_stage_1_key = Key::from_slice(&derived_key); + let left_short_message = XNonce::from_slice(&message[..TAG_LEN]); + + let mut cipher = XChaCha20::new(lion_stage_1_key, left_short_message); + cipher.apply_keystream(&mut message[TAG_LEN..]); + + // Stage 2: Use HMAC of right size, and xor to the left side + derived_key = blake3::derive_key(&format!("{}{}", CONTEXT, key_schedule[1]), key); + + let mac = blake3::keyed_hash(&derived_key, &message[TAG_LEN..]); + let tag_to_xor = mac.as_bytes(); + + // Xor resulting HMAC into the left (short) message + for i in 0..TAG_LEN { + message[i] ^= tag_to_xor[i]; + } + + // Stage 3: (same as 1) + derived_key = blake3::derive_key(&format!("{}{}", CONTEXT, key_schedule[2]), key); + let lion_stage_3_key = Key::from_slice(&derived_key); + let left_short_message_final = XNonce::from_slice(&message[..TAG_LEN]); + + let mut cipher = XChaCha20::new(lion_stage_3_key, left_short_message_final); + + cipher.apply_keystream(&mut message[TAG_LEN..]); + + // clean up temp key + derived_key.zeroize(); + + Ok(()) +} diff --git a/nym-outfox/tests/unittests.rs b/nym-outfox/tests/unittests.rs new file mode 100644 index 0000000000..ea5b46571d --- /dev/null +++ b/nym-outfox/tests/unittests.rs @@ -0,0 +1,142 @@ +extern crate nym_outfox; + +#[cfg(test)] +mod tests { + + use curve25519_dalek::constants::ED25519_BASEPOINT_TABLE; + use curve25519_dalek::scalar::Scalar; + use std::convert::TryInto; + + use nym_outfox::format::*; + use nym_outfox::lion::*; + + use std::iter::repeat_with; + + pub fn randombytes(n: usize) -> Vec { + repeat_with(|| fastrand::u8(..)).take(n).collect() + } + + #[test] + fn test_encode_decode() { + let mix_params = MixStageParameters { + routing_information_length_bytes: 32, + remaining_header_length_bytes: (32 + 16 + 32) * 4, + 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 routing = [0; 32]; + + let buffer = randombytes(mix_params.incoming_packet_length()); + + let mut new_buffer = buffer.clone(); + + let _ = mix_params + .encode_mix_layer( + &mut new_buffer[..], + &user_secret, + &mix_public_key, + &routing[..], + ) + .unwrap(); + + assert!(&new_buffer[mix_params.payload_range()] != &buffer[mix_params.payload_range()]); + assert!(&new_buffer[mix_params.routing_data_range()] != &routing[..]); + + let _ = mix_params + .decode_mix_layer(&mut new_buffer[..], &mix_secret) + .unwrap(); + + assert!(&new_buffer[mix_params.payload_range()] == &buffer[mix_params.payload_range()]); + assert!(&new_buffer[mix_params.routing_data_range()] == &routing[..]); + } + + #[test] + fn test_lion() { + let key = randombytes(32); + let message = randombytes(1024); + + let mut message_clone = message.clone(); + lion_transform(&mut message_clone[..], &key, [1, 2, 3]).unwrap(); + assert!(&message_clone[..] != &message[..]); + + let mut message_clone_2 = message.clone(); + lion_transform_encrypt(&mut message_clone_2, &key).unwrap(); + assert_eq!(message_clone_2, message_clone); + + lion_transform(&mut message_clone[..], &key[..], [3, 2, 1]).unwrap(); + assert!(&message_clone[..] == &message[..]); + } + + #[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 = 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 _ = layer_params2 + .decode_mix_layer(&mut buf[range2], &mix_secret) + .unwrap(); + + let _ = layer_params1 + .decode_mix_layer(&mut buf[range1], &mix_secret) + .unwrap(); + + let _ = layer_params0 + .decode_mix_layer(&mut buf[range0], &mix_secret) + .unwrap(); + } +}