From 6cef74968cb0493aef0d67c3a4add2f104ea8ff2 Mon Sep 17 00:00:00 2001 From: Andy Pack Date: Sun, 11 Feb 2024 22:30:07 +0000 Subject: [PATCH] working key handshake, client subcommands, just need to start encrypting and sending content --- dnstp-client/src/download.rs | 6 + dnstp-client/src/main.rs | 103 ++++++------- dnstp-client/src/test.rs | 52 +++++++ dnstp-client/src/upload.rs | 81 ++++++++++ dnstp/src/client_crypto_context.rs | 39 +++++ dnstp/src/clients.rs | 35 ++++- dnstp/src/crypto/mod.rs | 4 +- dnstp/src/crypto/tests.rs | 8 +- dnstp/src/lib.rs | 33 ++++- dnstp/src/message/message.rs | 52 ++++++- dnstp/src/message/record/mod.rs | 1 - dnstp/src/processor/request/download.rs | 0 dnstp/src/processor/request/encryption.rs | 45 ++++-- dnstp/src/processor/request/mod.rs | 165 +++++++++++++-------- dnstp/src/processor/request/tests.rs | 1 - dnstp/src/processor/request/upload.rs | 0 dnstp/src/processor/response/encryption.rs | 38 +++++ dnstp/src/processor/response/mod.rs | 18 ++- dnstp/src/string.rs | 45 ------ dnstp/src/string/mod.rs | 89 +++++++++++ dnstp/src/string/tests.rs | 35 +++++ dnstp/tests/key_swap.rs | 8 +- 22 files changed, 669 insertions(+), 189 deletions(-) create mode 100644 dnstp-client/src/download.rs create mode 100644 dnstp-client/src/test.rs create mode 100644 dnstp-client/src/upload.rs create mode 100644 dnstp/src/client_crypto_context.rs create mode 100644 dnstp/src/processor/request/download.rs create mode 100644 dnstp/src/processor/request/upload.rs create mode 100644 dnstp/src/processor/response/encryption.rs delete mode 100644 dnstp/src/string.rs create mode 100644 dnstp/src/string/mod.rs create mode 100644 dnstp/src/string/tests.rs diff --git a/dnstp-client/src/download.rs b/dnstp-client/src/download.rs new file mode 100644 index 0000000..5e9564f --- /dev/null +++ b/dnstp-client/src/download.rs @@ -0,0 +1,6 @@ +use crate::NetSettings; + +pub fn download(net_settings: NetSettings) +{ + +} \ No newline at end of file diff --git a/dnstp-client/src/main.rs b/dnstp-client/src/main.rs index 12c9e63..c3075b1 100644 --- a/dnstp-client/src/main.rs +++ b/dnstp-client/src/main.rs @@ -1,32 +1,58 @@ //! # Client Side //! -use std::fs::OpenOptions; -use std::net::SocketAddr; -use std::thread; -use std::time::Duration; -use clap::Parser; -use log::{info, LevelFilter}; -use rand::RngCore; -use simplelog::*; -use dnstplib::DomainConfig; +mod test; +mod upload; +mod download; -use dnstplib::message::DNSMessage; -use dnstplib::net::{DNSSocket, NetworkMessage}; -use dnstplib::processor::ResponseProcesor; +use std::fs::OpenOptions; +use clap::{Parser, Subcommand}; +use log::{LevelFilter}; +use simplelog::*; + +use crate::download::download; +use crate::test::send_test_requests; +use crate::upload::upload; #[derive(Parser, Debug)] #[command(author, version, about, long_about = None)] struct Args { - /// Addresses to send requests + #[clap(subcommand)] + command: Command, +} + +#[derive(Debug, Subcommand)] +enum Command { + /// Send test requests on loop to the server + Test { + #[clap(flatten)] + net_options: NetSettings + }, + /// Upload data to the remote server + Upload { + #[clap(flatten)] + net_options: NetSettings, + #[arg(short, long)] + value: String + }, + /// Download a payload from the remote server + Download { + #[clap(flatten)] + net_options: NetSettings + } +} + +#[derive(Parser, Debug)] +struct NetSettings { + /// Server address to send requests to #[arg(short, long)] address: String, - /// Base domain to operate on + /// Base domain server is operating on #[arg(long)] base_domain: String, /// Sub-domain to handle key handling when requested - #[arg(long, default_value = "static")] - key_endpoint: String + #[arg(short, long, default_value = "static")] + key_endpoint: String, } fn main() { @@ -44,40 +70,15 @@ fn main() { let args = Args::parse(); - let address = SocketAddr::from(([127, 0, 0, 1], 0)); - - let mut socket = DNSSocket::new(vec!(address)); - socket.bind(); - socket.run_tx(); - - let tx_channel = socket.get_tx_message_channel().unwrap(); - - let mut processor = ResponseProcesor::new(); - processor.run(); - - socket.run_rx(processor.get_message_channel().expect("couldn't get message processing channel")); - - let domain_config = DomainConfig { - base_domain: args.base_domain, - key_endpoint: args.key_endpoint - }; - - let domain = domain_config.get_fq_key_endpoint(); - - let mut rng = rand::thread_rng(); - loop { - - info!("sending..."); - - let message = DNSMessage::req_from_hostname(address, rng.next_u32() as u16, domain.clone()); - - let bytes = message.to_bytes(); - - tx_channel.send(Box::new(NetworkMessage { - buffer: Box::new(bytes), - peer: args.address.parse().unwrap() - })); - - thread::sleep(Duration::from_secs(1)); + match args.command { + Command::Test { net_options } => { + send_test_requests(net_options); + } + Command::Upload { net_options, value } => { + upload(net_options, value); + } + Command::Download { net_options } => { + download(net_options); + } } } diff --git a/dnstp-client/src/test.rs b/dnstp-client/src/test.rs new file mode 100644 index 0000000..c3cd29b --- /dev/null +++ b/dnstp-client/src/test.rs @@ -0,0 +1,52 @@ +use std::net::SocketAddr; +use std::sync::{Arc, Mutex}; +use std::thread; +use std::time::Duration; +use log::info; +use rand::RngCore; +use dnstplib::client_crypto_context::ClientCryptoContext; +use dnstplib::DomainConfig; +use dnstplib::message::DNSMessage; +use dnstplib::net::{DNSSocket, NetworkMessage}; +use dnstplib::processor::ResponseProcesor; +use crate::NetSettings; + +pub fn send_test_requests(args: NetSettings) +{ + let address = SocketAddr::from(([127, 0, 0, 1], 0)); + + let mut socket = DNSSocket::new(vec!(address)); + socket.bind(); + socket.run_tx(); + + let tx_channel = socket.get_tx_message_channel().unwrap(); + + let crypto_context = Arc::new(Mutex::new(ClientCryptoContext::new())); + let mut processor = ResponseProcesor::new(crypto_context.clone()); + processor.run(); + + socket.run_rx(processor.get_message_channel().expect("couldn't get message processing channel")); + + let domain_config = DomainConfig { + base_domain: args.base_domain, + key_endpoint: args.key_endpoint + }; + + let domain = domain_config.get_fq_key_endpoint(); + + let mut rng = rand::thread_rng(); + loop { + info!("sending..."); + + let message = DNSMessage::req_from_hostname(address, rng.next_u32() as u16, domain.clone()); + + let bytes = message.to_bytes(); + + tx_channel.send(Box::new(NetworkMessage { + buffer: Box::new(bytes), + peer: args.address.parse().unwrap() + })); + + thread::sleep(Duration::from_secs(1)); + } +} \ No newline at end of file diff --git a/dnstp-client/src/upload.rs b/dnstp-client/src/upload.rs new file mode 100644 index 0000000..0bee69e --- /dev/null +++ b/dnstp-client/src/upload.rs @@ -0,0 +1,81 @@ +use std::net::SocketAddr; +use std::sync::{Arc, Mutex}; +use std::thread; +use std::time::Duration; +use log::info; +use rand::RngCore; +use rand::rngs::OsRng; +use dnstplib::client_crypto_context::ClientCryptoContext; +use dnstplib::{DomainConfig, send_message}; +use dnstplib::message::{Direction, DNSHeader, DNSMessage, DNSQuestion, Opcode, QClass, QType, ResponseCode}; +use dnstplib::net::DNSSocket; +use dnstplib::processor::ResponseProcesor; +use crate::NetSettings; + +pub fn upload(net_settings: NetSettings, value: String) +{ + let address = SocketAddr::from(([127, 0, 0, 1], 0)); + + let mut socket = DNSSocket::new(vec!(address)); + socket.bind(); + socket.run_tx(); + + let tx_channel = socket.get_tx_message_channel().unwrap(); + + let crypto_context = Arc::new(Mutex::new(ClientCryptoContext::new())); + let mut processor = ResponseProcesor::new(crypto_context.clone()); + processor.run(); + + socket.run_rx(processor.get_message_channel().expect("couldn't get message processing channel")); + + let domain_config = DomainConfig { + base_domain: net_settings.base_domain, + key_endpoint: net_settings.key_endpoint + }; + + info!("sending handshake..."); + + let message = DNSMessage { + header: DNSHeader { + id: OsRng.next_u32() as u16, + direction: Direction::Request, + opcode: Opcode::Query, + authoritative: false, + truncation: false, + recursion_desired: false, + recursion_available: false, + valid_zeroes: true, + response: ResponseCode::NoError, + question_count: 2, + answer_record_count: 0, + authority_record_count: 0, + additional_record_count: 0, + }, + questions: vec![ + DNSQuestion { + qname: domain_config.get_fq_key_endpoint(), + qtype: QType::A, + qclass: QClass::Internet, + }, + DNSQuestion { + qname: crypto_context.lock().unwrap().get_public_key_domain(&domain_config.base_domain), + qtype: QType::A, + qclass: QClass::Internet, + } + ], + answer_records: vec![], + authority_records: vec![], + additional_records: vec![], + peer: net_settings.address.parse().unwrap(), + }; + + send_message(message, &tx_channel); + + while !crypto_context.lock().unwrap().is_complete() { + info!("waiting for crypto completion..."); + + thread::sleep(Duration::from_millis(100)); + } + + info!("crypto complete, sending data"); +} \ No newline at end of file diff --git a/dnstp/src/client_crypto_context.rs b/dnstp/src/client_crypto_context.rs new file mode 100644 index 0000000..ea4d102 --- /dev/null +++ b/dnstp/src/client_crypto_context.rs @@ -0,0 +1,39 @@ +use aes_gcm_siv::Aes256GcmSiv; +use p256::ecdh::EphemeralSecret; +use crate::crypto::{get_random_asym_pair, trim_public_key}; +use crate::string::append_base_domain_to_key; + +/// Represents the server from the perspective of a client +pub struct ClientCryptoContext { + pub shared_key: Option, + pub client_private: EphemeralSecret, + pub client_public: String, + pub server_public: Option +} + +impl ClientCryptoContext { + pub fn new() -> Self { + let (client_private, client_public) = get_random_asym_pair(); + + Self { + shared_key: None, + client_private, + client_public, + server_public: None + } + } + + pub fn is_complete(&self) -> bool + { + self.server_public.is_some() && self.shared_key.is_some() + } + + pub fn get_public_key_domain(&self, base_domain: &String) -> String + { + append_base_domain_to_key( + trim_public_key(&self.client_public), + base_domain + ) + } +} + diff --git a/dnstp/src/clients.rs b/dnstp/src/clients.rs index 91e1a50..079201d 100644 --- a/dnstp/src/clients.rs +++ b/dnstp/src/clients.rs @@ -1,29 +1,44 @@ +//! Structures for managing the state of connected clients from the perspective of the server + use std::collections::HashMap; use std::time::SystemTime; use aes_gcm_siv::Aes256GcmSiv; +/// A single client including when they connected and their shared cryptographic key pub struct Client { pub first_seen: SystemTime, + pub last_seen: SystemTime, pub shared_key: Aes256GcmSiv } impl Client { + /// Create a new client as pub fn new(shared_key: Aes256GcmSiv) -> Client { + let time = SystemTime::now(); + Client { - first_seen: SystemTime::now(), + first_seen: time, + last_seen: time, shared_key } } + + pub fn bump_last_seen(&mut self) + { + self.last_seen = SystemTime::now(); + } } +/// Container for managing connected clients and their keys pub struct Clients { client_map: HashMap } impl Clients { + /// Create a new collection of clients pub fn new() -> Clients { Clients { @@ -36,8 +51,26 @@ impl Clients { // self.client_map.insert(client_id, Client::new(shared_key)); // } + /// Add a newly connected client to the collection of connections. Index the client by public key. pub fn add(&mut self, client_id: String, client:Client) { self.client_map.insert(client_id, client); } + + pub fn client_is_connected(&self, client_id: &String) -> bool + { + self.client_map.contains_key(client_id) + } + + pub fn bump_last_seen(&mut self, client_id: &String) -> Result<(), ()> + { + match self.client_map.get_mut(client_id) + { + None => Err(()), + Some(client) => { + client.bump_last_seen(); + Ok(()) + } + } + } } \ No newline at end of file diff --git a/dnstp/src/crypto/mod.rs b/dnstp/src/crypto/mod.rs index 9e8df8b..d0d2c31 100644 --- a/dnstp/src/crypto/mod.rs +++ b/dnstp/src/crypto/mod.rs @@ -37,9 +37,9 @@ pub fn get_random_asym_pair() -> (EphemeralSecret, String) } /// Use one private key and an opposing public key to arrive at the same shared secret -pub fn get_shared_asym_secret(secret: EphemeralSecret, opposing_public_key: String) -> Result, ()> { +pub fn get_shared_asym_secret(secret: &EphemeralSecret, opposing_public_key: &String) -> Result, ()> { - match PublicKey::from_str(&opposing_public_key) { + match PublicKey::from_str(opposing_public_key) { Ok(other_public) => { Ok(secret.diffie_hellman(&other_public)) } diff --git a/dnstp/src/crypto/tests.rs b/dnstp/src/crypto/tests.rs index 0889f8a..e36d5f5 100644 --- a/dnstp/src/crypto/tests.rs +++ b/dnstp/src/crypto/tests.rs @@ -5,8 +5,8 @@ fn matching_shared_secrets() { let (secret_alice, point_alice) = get_random_asym_pair(); let (secret_bob, point_bob) = get_random_asym_pair(); - let shared_alice = get_shared_asym_secret(secret_alice, point_bob).unwrap(); - let shared_bob = get_shared_asym_secret(secret_bob, point_alice).unwrap(); + let shared_alice = get_shared_asym_secret(&secret_alice, &point_bob).unwrap(); + let shared_bob = get_shared_asym_secret(&secret_bob, &point_alice).unwrap(); assert_eq!(shared_alice.raw_secret_bytes(), shared_bob.raw_secret_bytes()); } @@ -19,8 +19,8 @@ fn arbitrary_string_back_and_forth() { let (secret_alice, point_alice) = get_random_asym_pair(); let (secret_bob, point_bob) = get_random_asym_pair(); - let shared_alice = get_shared_asym_secret(secret_alice, point_bob).unwrap(); - let shared_bob = get_shared_asym_secret(secret_bob, point_alice).unwrap(); + let shared_alice = get_shared_asym_secret(&secret_alice, &point_bob).unwrap(); + let shared_bob = get_shared_asym_secret(&secret_bob, &point_alice).unwrap(); assert_eq!(shared_alice.raw_secret_bytes(), shared_bob.raw_secret_bytes()); diff --git a/dnstp/src/lib.rs b/dnstp/src/lib.rs index e5cfe1c..c76129f 100644 --- a/dnstp/src/lib.rs +++ b/dnstp/src/lib.rs @@ -10,6 +10,35 @@ pub mod net; pub mod string; pub mod config; pub mod crypto; -mod clients; +pub mod clients; +pub mod client_crypto_context; -pub use config::DomainConfig; \ No newline at end of file +use std::sync::mpsc::{Sender}; +use log::error; +pub use config::DomainConfig; +use crate::message::DNSMessage; +use crate::net::{NetworkMessage, NetworkMessagePtr}; + +#[repr(u8)] +#[derive(Debug, Eq, PartialEq, Copy, Clone)] +pub enum RequestError { + /// Trying to perform an operation without having handshaked first + NoHandshake, + WrongNumberOfQuestions, + CryptoFailure +} + +pub fn send_message(response: DNSMessage, sending_channel: &Sender) +{ + match sending_channel.send(Box::new( + NetworkMessage { + buffer: Box::new(response.to_bytes()), + peer: response.peer + } + )){ + Ok(_) => {} + Err(e) => { + error!("failed to pass a message to the network layer for delivery [{}]", e.to_string()); + } + } +} \ No newline at end of file diff --git a/dnstp/src/message/message.rs b/dnstp/src/message/message.rs index efb33a3..5458e28 100644 --- a/dnstp/src/message/message.rs +++ b/dnstp/src/message/message.rs @@ -1,5 +1,6 @@ use std::net::{Ipv4Addr, SocketAddr}; -use crate::message::{DNSQuestion, DNSHeader, questions_to_bytes, Direction, ResponseCode, QType, QClass, ResourceRecord, records_to_bytes, ARdata}; +use crate::message::{DNSQuestion, DNSHeader, questions_to_bytes, Direction, ResponseCode, QType, QClass, ResourceRecord, records_to_bytes, ARdata, TXTRdata, RData}; +use crate::RequestError; /// A DNS message which can be used as either a request or response based on its direction and composition #[derive(Debug)] @@ -108,6 +109,26 @@ impl DNSMessage { response } + pub fn dumb_resp_from_request(&self) -> DNSMessage + { + let mut response = DNSMessage{ + header: self.header.clone(), + questions: self.questions.clone(), + answer_records: vec![], + authority_records: vec![], + additional_records: vec![], + peer: self.peer + }; + + response.header.direction = Direction::Response; + response.header.response = ResponseCode::NotImplemented; + response.header.answer_record_count = 0; + response.header.authority_record_count = 0; + response.header.additional_record_count = 0; + + response + } + pub fn empty_resp_from_request(&self) -> DNSMessage { let mut response = DNSMessage{ @@ -131,4 +152,33 @@ impl DNSMessage { response } + + pub fn protocol_error_from_request(&self, error_code: RequestError) -> DNSMessage + { + let txt = Box::new(TXTRdata::from(String::new())); + + let mut response = DNSMessage{ + header: self.header.clone(), + questions: self.questions.clone(), + answer_records: vec![ResourceRecord { + name_offset: 12, + answer_type: QType::TXT, + class: QClass::Internet, + ttl: 0, + rd_length: txt.to_bytes().len() as u16, + r_data: txt + }], + authority_records: vec![], + additional_records: vec![], + peer: self.peer + }; + + response.header.direction = Direction::Response; + response.header.response = ResponseCode::ServerFailure; + response.header.answer_record_count = 1; + response.header.authority_record_count = 0; + response.header.additional_record_count = 0; + + response + } } \ No newline at end of file diff --git a/dnstp/src/message/record/mod.rs b/dnstp/src/message/record/mod.rs index 413b43a..5115a07 100644 --- a/dnstp/src/message/record/mod.rs +++ b/dnstp/src/message/record/mod.rs @@ -17,7 +17,6 @@ pub use cname_rdata::CnameRdata; mod tests; use std::fmt::Debug; -use std::fmt::Display; use crate::byte::{four_byte_combine, four_byte_split, push_split_bytes, two_byte_combine}; use crate::message::question::{DNSQuestion, QClass, QType}; diff --git a/dnstp/src/processor/request/download.rs b/dnstp/src/processor/request/download.rs new file mode 100644 index 0000000..e69de29 diff --git a/dnstp/src/processor/request/encryption.rs b/dnstp/src/processor/request/encryption.rs index 2eac5b1..7dc8eea 100644 --- a/dnstp/src/processor/request/encryption.rs +++ b/dnstp/src/processor/request/encryption.rs @@ -2,17 +2,23 @@ use std::net::Ipv4Addr; use p256::ecdh::EphemeralSecret; use crate::clients::Client; use crate::crypto::{asym_to_sym_key, fatten_public_key, get_random_asym_pair, get_shared_asym_secret, trim_public_key}; -use crate::message::{ARdata, DNSMessage, DNSQuestion, QClass, QType, ResourceRecord}; +use crate::message::{ARdata, DNSMessage, QClass, QType, ResourceRecord}; use crate::message::record::CnameRdata; -use crate::string::{append_base_domain_to_key, strip_base_domain_from_key}; +use crate::string::{append_base_domain_to_key, encode_domain_name, strip_base_domain_from_key}; +/// Result of a client's handshake request including server key pair and prepared response pub struct KeySwapContext { + /// New client structure to track derived shared secret and last seen time pub new_client: Client, + /// Response message to send to the client with the server's public key pub response: DNSMessage, + /// Public key of the server's key pair pub server_public: String, + /// Public key extracted from the client's request pub client_public: String } +/// Generate a random asymmetric key pair, append the dnstp base domain to the secret pub fn get_key_request_with_base_domain(base_domain: String) -> (EphemeralSecret, String) { let (private, public) = get_random_asym_pair(); @@ -20,9 +26,10 @@ pub fn get_key_request_with_base_domain(base_domain: String) -> (EphemeralSecret (private, append_base_domain_to_key(trim_public_key(&public), &base_domain)) } -pub fn get_fattened_public_key(key_question: &DNSQuestion) -> (String, String) +/// Extract the client's public key from the DNS message, turn the hostname back into the full fat public key with --- BEGIN KEY --- headers and trailers +pub fn get_fattened_public_key(key_question: &String) -> (String, String) { - let public_key = &key_question.qname; + let public_key = key_question; let (trimmed_public_key, base_domain) = strip_base_domain_from_key(public_key); (fatten_public_key(&trimmed_public_key), base_domain) @@ -36,7 +43,10 @@ pub enum KeyDecodeError { SharedSecretDerivation, } -pub fn decode_key_request(message: DNSMessage) -> Result +/// Take a client's handshake request, process the crypto and prepare a response +/// +/// Includes generating a server key pair, using the public key in the response, deriving the shared secret. +pub fn decode_key_request(message: &DNSMessage) -> Result { if message.questions.len() == 2 { @@ -52,18 +62,20 @@ pub fn decode_key_request(message: DNSMessage) -> Result { let sym_key = asym_to_sym_key(&secret); let new_client = Client::new(sym_key); let mut response = message.empty_resp_from_request(); + // return an empty null response for the key hostname (static.BLANK.TLD) question, not that important let first_record = ResourceRecord { name_offset: 12, answer_type: QType::A, @@ -73,22 +85,25 @@ pub fn decode_key_request(message: DNSMessage) -> Result Result>, @@ -37,6 +40,7 @@ impl RequestProcesor { } } + /// Spin a thread to process parsed DNS requests and respond as appropriate pub fn run(&mut self, sending_channel: Sender) { let (tx, rx): (Sender, Receiver) = mpsc::channel(); @@ -50,8 +54,6 @@ impl RequestProcesor { thread::spawn(move || { - let clients = clients; - for m in rx { let peer = m.peer.clone(); @@ -60,64 +62,15 @@ impl RequestProcesor { Ok(r) => { info!("received dns message: {:?}", r); + // If there is a question containing the protocol base domain, treat it as a dnstp request + // (handshake, upload, download) and handle as such if r.questions.iter().any(|q| q.qname.ends_with(&base_domain_equality)) { - if r.questions[0].qname.eq_ignore_ascii_case(&fq_key_endpoint) - { - info!("[{}] received encryption key request", peer); - - match decode_key_request(r) - { - Ok(context) => { - - clients.lock().unwrap().add(context.client_public, context.new_client); - - sending_channel.send(Box::new( - NetworkMessage { - buffer: Box::new(context.response.to_bytes()), - peer: context.response.peer - } - )); - } - Err(e) => { - match e { - KeyDecodeError::QuestionCount(qc) => { - error!("[{}] failed to parse public key, wrong question count [{}]", peer, qc); - } - KeyDecodeError::FirstQuestionNotA(qtype) => { - error!("[{}] failed to parse public key, first question wasn't an A request [{}]", peer, qtype); - } - KeyDecodeError::SecondQuestionNotA(qtype) => { - error!("[{}] failed to parse public key, second question wasn't an A request [{}]", peer, qtype); - } - KeyDecodeError::SharedSecretDerivation => { - error!("[{}] failed to parse public key, failed to derived shared secret", peer); - } - } - } - } - } - else - { - let response = DNSMessage::a_resp_from_request(&r, |_| Ipv4Addr::from([127, 0, 0, 1])); - - sending_channel.send(Box::new( - NetworkMessage { - buffer: Box::new(response.to_bytes()), - peer: response.peer - } - )); - } + Self::handle_dnstp_request(r, &sending_channel, &clients, peer, &fq_key_endpoint); } - else { - let response = DNSMessage::a_resp_from_request(&r, |_| Ipv4Addr::from([127, 0, 0, 1])); - - sending_channel.send(Box::new( - NetworkMessage { - buffer: Box::new(response.to_bytes()), - peer: response.peer - } - )); + else // otherwise it's for something else and reply with some dumb empty answer + { + send_message(DNSMessage::dumb_resp_from_request(&r), &sending_channel); } } Err(e) => { @@ -130,8 +83,100 @@ impl RequestProcesor { }); } + /// The message is trying to do some dnstp, work out whether it's hello handshaking, uplaoding or downloading data and handoff the message to that workflow + fn handle_dnstp_request(r: DNSMessage, sending_channel: &Sender, clients: &Arc>, peer: SocketAddr, fq_key_endpoint: &String) + { + // if the first question is for the key domain (static.BLANK.TLD) it's a handshake for swapping keys + if r.questions[0].qname.eq_ignore_ascii_case(fq_key_endpoint) + { + Self::handle_encryption_handshake(r, sending_channel, clients, peer); + } + // if we're not handshaking then the client should be known to the server + else if clients.lock().unwrap().client_is_connected(&r.questions[0].qname) { + info!("[{}] received request from known client", peer); + + // for now lets deal with three questions, first one is the client id, second is the actual request, third is the nonce + if r.questions.len() == 3 + { + match r.questions[1].qtype { + QType::A => { + Self::handle_upload_request(r, sending_channel, clients, peer); + } + QType::CNAME => { // makes sense for a cname to return lots of text + Self::handle_download_request(r, sending_channel, clients, peer); + } + _ => {} + } + } + else + { + Self::send_protocol_error(RequestError::WrongNumberOfQuestions, &r, &sending_channel); + } + } + // otherwise return protocol error for trying to do something without handshaking + else + { + Self::send_protocol_error(RequestError::NoHandshake, &r, &sending_channel); + } + } + + /// Process a hello message from a new client with a public key and send the response. + /// + /// Generate a key pair and repspond with the public key, generate the shared secret and store this in the connected clients. + fn handle_encryption_handshake(r: DNSMessage, sending_channel: &Sender, clients: &Arc>, peer: SocketAddr) + { + info!("[{}] received encryption key request", peer); + + // crypto bulk happens in decode, result includes message to be responded with + match decode_key_request(&r) + { + Ok(context) => { + clients.lock().unwrap().add(context.client_public, context.new_client); + + send_message(context.response, &sending_channel); + } + Err(e) => { + match e { + KeyDecodeError::QuestionCount(qc) => { + error!("[{}] failed to parse public key, wrong question count [{}]", peer, qc); + } + KeyDecodeError::FirstQuestionNotA(qtype) => { + error!("[{}] failed to parse public key, first question wasn't an A request [{}]", peer, qtype); + } + KeyDecodeError::SecondQuestionNotA(qtype) => { + error!("[{}] failed to parse public key, second question wasn't an A request [{}]", peer, qtype); + } + KeyDecodeError::SharedSecretDerivation => { + error!("[{}] failed to parse public key, failed to derived shared secret", peer); + } + } + + Self::send_protocol_error(RequestError::CryptoFailure, &r, &sending_channel); + } + } + } + + fn handle_download_request(r: DNSMessage, sending_channel: &Sender, clients: &Arc>, peer: SocketAddr) + { + info!("[{}] received download request", peer); + let client_id = &r.questions[0].qname; + clients.lock().unwrap().bump_last_seen(client_id); + } + + fn handle_upload_request(r: DNSMessage, sending_channel: &Sender, clients: &Arc>, peer: SocketAddr) + { + info!("[{}] received upload request", peer); + let client_id = &r.questions[0].qname; + clients.lock().unwrap().bump_last_seen(client_id); + } + pub fn get_message_channel(&mut self) -> Option> { self.message_channel.clone() } + + pub fn send_protocol_error(error_code: RequestError, r: &DNSMessage, sending_channel: &Sender) + { + send_message(DNSMessage::protocol_error_from_request(&r, error_code), sending_channel); + } } diff --git a/dnstp/src/processor/request/tests.rs b/dnstp/src/processor/request/tests.rs index 76649d8..aaa5a01 100644 --- a/dnstp/src/processor/request/tests.rs +++ b/dnstp/src/processor/request/tests.rs @@ -1,4 +1,3 @@ -use crate::crypto::{fatten_public_key, get_random_asym_pair, trim_public_key}; use crate::string::encode_domain_name; use super::*; use super::encryption::*; diff --git a/dnstp/src/processor/request/upload.rs b/dnstp/src/processor/request/upload.rs new file mode 100644 index 0000000..e69de29 diff --git a/dnstp/src/processor/response/encryption.rs b/dnstp/src/processor/response/encryption.rs new file mode 100644 index 0000000..d34d96f --- /dev/null +++ b/dnstp/src/processor/response/encryption.rs @@ -0,0 +1,38 @@ +use std::sync::{Arc, Mutex}; +use crate::client_crypto_context::ClientCryptoContext; +use crate::crypto::{asym_to_sym_key, get_shared_asym_secret}; +use crate::message::DNSMessage; +use crate::processor::request::encryption::get_fattened_public_key; +use crate::string::decode_domain_name; + +pub fn decode_key_response(message: &DNSMessage, client_crypto_context: Arc>) +{ + if message.answer_records.len() == 2 { + // if message.questions[0].qtype != QType::A + // { + // return Err(KeyDecodeError::FirstQuestionNotA(message.questions[0].qtype)); + // } + + let key_answer = &message.answer_records[1]; + + // if key_answer.answer_type != QType::A + // { + // return Err(KeyDecodeError::SecondQuestionNotA(key_answer.answer_type)); + // } + + let data_string = decode_domain_name(key_answer.r_data.to_bytes()); + // key is transmitted wihout --- BEGIN KEY -- header and trailer bits and with '.' instead of new lines + let (fattened_public_key, _) = get_fattened_public_key(&data_string); + + let mut context = client_crypto_context.lock().unwrap(); + + match get_shared_asym_secret(&context.client_private, &fattened_public_key) + { + Ok(k) => { + context.server_public = Some(fattened_public_key); + context.shared_key = Some(asym_to_sym_key(&k)); + } + Err(_) => {} + } + } +} \ No newline at end of file diff --git a/dnstp/src/processor/response/mod.rs b/dnstp/src/processor/response/mod.rs index b0ac2af..ee1a281 100644 --- a/dnstp/src/processor/response/mod.rs +++ b/dnstp/src/processor/response/mod.rs @@ -1,19 +1,25 @@ -use std::sync::mpsc; +mod encryption; + +use std::sync::{Arc, mpsc, Mutex}; use std::sync::mpsc::{Receiver, Sender}; use std::thread; use log::info; +use crate::client_crypto_context::ClientCryptoContext; use crate::net::raw_request::NetworkMessagePtr; use crate::message_parser::parse_message; use crate::processor::print_error; +use crate::processor::response::encryption::decode_key_response; pub struct ResponseProcesor { - message_channel: Option> + message_channel: Option>, + crypto_context: Arc> } impl ResponseProcesor { - pub fn new() -> ResponseProcesor { + pub fn new(crypto_context: Arc>) -> ResponseProcesor { ResponseProcesor{ - message_channel: None + message_channel: None, + crypto_context } } @@ -22,6 +28,8 @@ impl ResponseProcesor { let (tx, rx): (Sender, Receiver) = mpsc::channel(); self.message_channel = Some(tx); + let crypto_context = self.crypto_context.clone(); + thread::spawn(move || { for m in rx @@ -31,6 +39,8 @@ impl ResponseProcesor { match parse_message(*m) { Ok(r) => { info!("received dns message: {:?}", r); + + decode_key_response(&r, crypto_context.clone()); } Err(e) => { print_error(e, &peer); diff --git a/dnstp/src/string.rs b/dnstp/src/string.rs deleted file mode 100644 index e20b3c5..0000000 --- a/dnstp/src/string.rs +++ /dev/null @@ -1,45 +0,0 @@ -//! Utility functions for manipulating strings - -use urlencoding::encode; - -pub fn encode_domain_name(name: &String) -> Vec -{ - let mut ret: Vec = Vec::with_capacity(name.len() + 3); - - for part in name.split(".") - { - let encoded_string = encode(part); - let count = encoded_string.len(); - - ret.push(count as u8); - for x in encoded_string.bytes() { - ret.push(x); - }; - } - - ret.push(0); - - ret -} - -pub fn strip_base_domain_from_key(public_key: &String) -> (String, String) -{ - let periods: Vec<_> = public_key.rmatch_indices(".").collect(); - - if periods.len() >= 2 { - (public_key[0 .. periods[1].0].to_string(), - public_key[periods[1].0 .. ].to_string()) - } - else if periods.len() == 1 { - (public_key[0 .. periods[0].0].to_string(), - public_key[periods[0].0 .. ].to_string()) - } - else { - (public_key.to_string(), String::new()) - } -} - -pub fn append_base_domain_to_key(trimmed_key: String, base_domain: &String) -> String -{ - vec![trimmed_key, base_domain.to_string()].join(".") -} \ No newline at end of file diff --git a/dnstp/src/string/mod.rs b/dnstp/src/string/mod.rs new file mode 100644 index 0000000..fc91946 --- /dev/null +++ b/dnstp/src/string/mod.rs @@ -0,0 +1,89 @@ +//! Utility functions for manipulating strings + +#[cfg(test)] +mod tests; + +use urlencoding::{decode, encode}; + +pub fn encode_domain_name(name: &String) -> Vec +{ + let mut ret: Vec = Vec::with_capacity(name.len() + 3); + + for part in name.split(".") + { + let encoded_string = encode(part); + let count = encoded_string.len(); + + ret.push(count as u8); + for x in encoded_string.bytes() { + ret.push(x); + }; + } + + ret.push(0); + + ret +} + +pub fn decode_domain_name(name: Vec) -> String +{ + let mut full_domain: String = String::new(); + let mut current_query: Vec = Vec::with_capacity(10); + let mut current_length: Option = None; + let mut remaining_length: u8 = 0; + + for char in name { + + match current_length { + None => { + current_length = Some(char); + remaining_length = char; + } + Some(_) => { + if remaining_length == 0 { + + let current_part = String::from_utf8(current_query.clone()).unwrap(); + let url_decoded = decode(current_part.as_str()).unwrap(); + + full_domain.push_str(&url_decoded.to_string()); + + if char != 0 { + full_domain.push('.'); + } + + current_query.clear(); + current_length = Some(char); + remaining_length = char; + } + else { + current_query.push(char); + remaining_length -= 1; + } + } + } + } + + full_domain +} + +pub fn strip_base_domain_from_key(public_key: &String) -> (String, String) +{ + let periods: Vec<_> = public_key.rmatch_indices(".").collect(); + + if periods.len() >= 2 { + (public_key[0 .. periods[1].0].to_string(), + public_key[periods[1].0 .. ].to_string()) + } + else if periods.len() == 1 { + (public_key[0 .. periods[0].0].to_string(), + public_key[periods[0].0 .. ].to_string()) + } + else { + (public_key.to_string(), String::new()) + } +} + +pub fn append_base_domain_to_key(trimmed_key: String, base_domain: &String) -> String +{ + vec![trimmed_key, base_domain.to_string()].join(".") +} \ No newline at end of file diff --git a/dnstp/src/string/tests.rs b/dnstp/src/string/tests.rs new file mode 100644 index 0000000..b7ad5e5 --- /dev/null +++ b/dnstp/src/string/tests.rs @@ -0,0 +1,35 @@ +use super::*; + +#[test] +fn test_encode() +{ + let payload = "google.com"; + + let encoded = encode_domain_name(&payload.to_string()); + + assert_eq!(encoded.len(), "google".len() + "com".len() + 1 + 1 + 1); + assert_eq!(encoded[0], "google".len() as u8); + // assert_eq!(encoded["google".len()], "com".len() as u8); +} + +#[test] +fn test_encode_decode() +{ + let payload = "google.com"; + + let encoded = encode_domain_name(&payload.to_string()); + let decoded = decode_domain_name(encoded); + + assert_eq!(payload, decoded); +} + +#[test] +fn test_encode_decode_two() +{ + let payload = "sub.domain.com"; + + let encoded = encode_domain_name(&payload.to_string()); + let decoded = decode_domain_name(encoded); + + assert_eq!(payload, decoded); +} \ No newline at end of file diff --git a/dnstp/tests/key_swap.rs b/dnstp/tests/key_swap.rs index 15534d0..eaa5317 100644 --- a/dnstp/tests/key_swap.rs +++ b/dnstp/tests/key_swap.rs @@ -45,15 +45,19 @@ fn test_key_swap() // SERVER ///////////////// + let question_count = message.questions.len(); // handle message "received by client" - let resp = decode_key_request(message).unwrap(); + let resp = decode_key_request(&message).unwrap(); + + assert_eq!(question_count, resp.response.questions.len()); + assert_eq!(question_count, resp.response.answer_records.len()); //////////// // CLIENT //////////// // client has received message from above and constructs shared secret - let shared_secret_client = asym_to_sym_key(&get_shared_asym_secret(client_private, resp.server_public).unwrap()); + let shared_secret_client = asym_to_sym_key(&get_shared_asym_secret(&client_private, &resp.server_public).unwrap()); /////////////////////////////// // TEST ENCRYPTION/DECRYPTION