// Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 use nym_crypto::shared_key::new_ephemeral_shared_key; use nym_crypto::symmetric::stream_cipher; use nym_sphinx_acknowledgements::surb_ack::{SurbAck, SurbAckRecoveryError}; use nym_sphinx_acknowledgements::AckKey; use nym_sphinx_addressing::clients::Recipient; use nym_sphinx_addressing::nodes::NymNodeRoutingAddress; use nym_sphinx_chunking::fragment::COVER_FRAG_ID; use nym_sphinx_forwarding::packet::MixPacket; use nym_sphinx_params::packet_sizes::PacketSize; use nym_sphinx_params::{ PacketEncryptionAlgorithm, PacketHkdfAlgorithm, PacketType, DEFAULT_NUM_MIX_HOPS, }; use nym_sphinx_types::NymPacket; use nym_topology::{NymTopology, NymTopologyError}; use rand::{CryptoRng, RngCore}; use std::time; use thiserror::Error; pub const LOOP_COVER_MESSAGE_PAYLOAD: &[u8] = b"The cake is a lie!"; #[derive(Debug, Error)] pub enum CoverMessageError { #[error("Could not construct cover message due to invalid topology - {0}")] InvalidTopologyError(#[from] NymTopologyError), #[error("SurbAck: {0}")] SurbAck(#[from] SurbAckRecoveryError), #[error("NymPacket: {0}")] NymPacket(#[from] nym_sphinx_types::NymPacketError), } pub fn generate_loop_cover_surb_ack( rng: &mut R, topology: &NymTopology, ack_key: &AckKey, full_address: &Recipient, average_ack_delay: time::Duration, packet_type: PacketType, ) -> Result where R: RngCore + CryptoRng, { Ok(SurbAck::construct( rng, full_address, ack_key, COVER_FRAG_ID.to_bytes(), average_ack_delay, topology, packet_type, )?) } #[allow(clippy::too_many_arguments)] pub fn generate_loop_cover_packet( rng: &mut R, topology: &NymTopology, ack_key: &AckKey, full_address: &Recipient, average_ack_delay: time::Duration, average_packet_delay: time::Duration, packet_size: PacketSize, packet_type: PacketType, ) -> Result where R: RngCore + CryptoRng, { // we don't care about total ack delay - we will not be retransmitting it anyway let (_, ack_bytes) = generate_loop_cover_surb_ack( rng, topology, ack_key, full_address, average_ack_delay, packet_type, )? .prepare_for_sending()?; // cover message can't be distinguishable from a normal traffic so we have to go through // all the effort of key generation, encryption, etc. Note here we are generating shared key // with ourselves! let (ephemeral_keypair, shared_key) = new_ephemeral_shared_key::< PacketEncryptionAlgorithm, PacketHkdfAlgorithm, _, >(rng, full_address.encryption_key()); let public_key_bytes = ephemeral_keypair.public_key().to_bytes(); let cover_size = packet_size.plaintext_size() - public_key_bytes.len() - ack_bytes.len(); let mut cover_content: Vec<_> = LOOP_COVER_MESSAGE_PAYLOAD .iter() .cloned() .chain(std::iter::once(1)) .chain(std::iter::repeat(0)) .take(cover_size) .collect(); let zero_iv = stream_cipher::zero_iv::(); stream_cipher::encrypt_in_place::( &shared_key, &zero_iv, &mut cover_content, ); // combine it together as follows: // SURB_ACK_FIRST_HOP || SURB_ACK_DATA || EPHEMERAL_KEY || COVER_CONTENT // (note: surb_ack_bytes contains SURB_ACK_FIRST_HOP || SURB_ACK_DATA ) let packet_payload: Vec<_> = ack_bytes .into_iter() .chain(ephemeral_keypair.public_key().to_bytes().iter().cloned()) .chain(cover_content) .collect(); let route = topology.random_route_to_gateway(rng, DEFAULT_NUM_MIX_HOPS, full_address.gateway())?; let delays = nym_sphinx_routing::generate_hop_delays(average_packet_delay, route.len()); let destination = full_address.as_sphinx_destination(); let first_hop_address = NymNodeRoutingAddress::try_from(route.first().unwrap().address).unwrap(); // once merged, that's an easy rng injection point for sphinx packets : ) let packet = match packet_type { PacketType::Mix => NymPacket::sphinx_build( packet_size.payload_size(), packet_payload, &route, &destination, &delays, )?, #[allow(deprecated)] PacketType::Vpn => NymPacket::sphinx_build( packet_size.payload_size(), packet_payload, &route, &destination, &delays, )?, PacketType::Outfox => NymPacket::outfox_build( packet_payload, &route, &destination, Some(packet_size.plaintext_size()), )?, }; Ok(MixPacket::new(first_hop_address, packet, packet_type)) } /// Helper function used to determine if given message represents a loop cover message. // It kinda seems like there must exist "prefix" or "starts_with" method for bytes // or something, but I couldn't find anything pub fn is_cover(data: &[u8]) -> bool { if data.len() < LOOP_COVER_MESSAGE_PAYLOAD.len() { return false; } for i in 0..LOOP_COVER_MESSAGE_PAYLOAD.len() { if data[i] != LOOP_COVER_MESSAGE_PAYLOAD[i] { return false; } } true } #[cfg(test)] mod tests { use super::*; #[test] fn is_cover_works_for_identical_input() { assert!(is_cover(LOOP_COVER_MESSAGE_PAYLOAD)) } #[test] fn is_cover_works_for_longer_input() { let input: Vec<_> = LOOP_COVER_MESSAGE_PAYLOAD .iter() .cloned() .chain(std::iter::repeat(42).take(100)) .collect(); assert!(is_cover(&input)) } #[test] fn is_cover_returns_false_for_unrelated_input() { // make sure the length checks out let input: Vec<_> = LOOP_COVER_MESSAGE_PAYLOAD.iter().map(|_| 42).collect(); assert!(!is_cover(&input)) } #[test] fn is_cover_returns_false_for_part_of_correct_input() { let input: Vec<_> = LOOP_COVER_MESSAGE_PAYLOAD .iter() .cloned() .take(LOOP_COVER_MESSAGE_PAYLOAD.len() - 1) .chain(std::iter::once(42)) .collect(); assert!(!is_cover(&input)) } #[test] fn is_cover_returns_false_for_empty_input() { let empty = Vec::new(); assert!(!is_cover(&empty)) } }