From 041915154a70c1d1b395aafa1a30fd631d99f0f8 Mon Sep 17 00:00:00 2001 From: core Date: Mon, 19 Dec 2022 21:37:16 -0500 Subject: [PATCH] [quicktap][transport] transport key derivation --- README.md | 5 +- quicktap/src/noise/data.rs | 67 +++++++++++++++++++++++ quicktap/src/noise/error.rs | 13 ++++- quicktap/src/noise/handshake/initiator.rs | 2 + quicktap/src/noise/handshake/mod.rs | 38 ++++++++++++- quicktap/src/noise/mod.rs | 6 +- quicktap/src/noise/tests.rs | 35 ++++++++++++ 7 files changed, 160 insertions(+), 6 deletions(-) create mode 100644 quicktap/src/noise/data.rs create mode 100644 quicktap/src/noise/tests.rs diff --git a/README.md b/README.md index 874d5ae..8897f7f 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,7 @@ # Elaeis 4.0 Elaeis is a implementation of the WireGuard protocol (quicktap) and a management system built around it (elaeis itself). -For more information, check [quicktap's README](quicktap/README.md) or [elaeis4's README](elaeis/README.md) \ No newline at end of file +For more information, check [quicktap's README](quicktap/README.md) or [elaeis4's README](elaeis/README.md) + +# Need help? +Get in touch! Find us on Matrix at [#e3team-software:e3t.cc](https://matrix.to/#/#e3team-software:e3t.cc). \ No newline at end of file diff --git a/quicktap/src/noise/data.rs b/quicktap/src/noise/data.rs new file mode 100644 index 0000000..8020441 --- /dev/null +++ b/quicktap/src/noise/data.rs @@ -0,0 +1,67 @@ +//! `WireGuard` transport data messages. see 5.4.6 + +use crate::noise::error::{NoiseError, NoisePacketParseError}; +use crate::noise::handshake::HandshakeState; +use crate::qcrypto::aead::{qcrypto_aead, qcrypto_aead_decrypt}; + +fn pad_packet(p: &Vec) -> Vec { + let mut padded = p.clone(); + padded.append(&mut vec![0u8; 16 - (p.len() % 16)]); + padded +} +fn unpad_packet(p: &Vec) -> Vec { + p.into_iter().rev().skip_while(|x| **x == 0).map(|x| x.clone()).collect::>() +} + +/// Given any arbitrary IP packet, pad and encrypt it with the current handshake state. +/// # Errors +/// This function will error if the encryption step is unsuccessful. +pub fn encapsulate(state: &mut HandshakeState, packet: &Vec) -> Result, NoiseError> { + let packet = pad_packet(packet); + let counter = state.n_send; + let packet_data = match qcrypto_aead(&state.t_send, state.n_send, &packet, &[]) { + Ok(d) => d, + Err(e) => return Err(NoiseError::ChaCha20Error(e)) + }; + + let i_other = if state.we_are_initiator { state.i_r } else { state.i_i }; + + let mut res = vec![4u8; 16 + packet_data.len()]; + res[1..4].copy_from_slice(&[0u8; 3]); + res[4..8].copy_from_slice(&i_other.to_le_bytes()); + res[8..16].copy_from_slice(&counter.to_le_bytes()); + res[16..16+packet_data.len()].copy_from_slice(&packet_data); + + Ok(res) +} + +/// Given an encrypted, encapsulated IP packet, decrypt and unpad it with the current handshake state. +/// # Errors +/// This function will error if the decryption step is unsuccessful. +/// # Panics +/// This function, while containing unwraps for type conversions, is safe and will never panic. +#[allow(clippy::unwrap_used)] +pub fn deencapsulate(state: &mut HandshakeState, packet: &[u8]) -> Result, NoiseError> { + if packet[0] != 4u8 { + return Err(NoiseError::PacketParseError(NoisePacketParseError::WrongPacketType(4, packet[0] as usize))) + } + if packet[1..4] != [0u8; 3] { + return Err(NoiseError::PacketParseError(NoisePacketParseError::MissingReserved)) + } + let index_me = if state.we_are_initiator { state.i_i } else { state.i_r }; + if packet[4..8] != index_me.to_le_bytes() { + return Err(NoiseError::PacketParseError(NoisePacketParseError::WrongIndex(index_me as usize, usize::from_le_bytes(packet[4..8].try_into().unwrap())))) + } + let counter = u64::from_le_bytes(packet[8..16].try_into().unwrap()); + if state.n_recv == counter { + + } + // TODO: Check if this counter is okay, sliding windows and stuff. Don't feel like implementing it fully right now + let packet = match qcrypto_aead_decrypt(&state.t_recv, counter, &packet[16..], &[]) { + Ok(p) => p, + Err(e) => return Err(NoiseError::ChaCha20Error(e)) + }; + state.n_recv = counter; + + Ok(unpad_packet(&packet)) +} \ No newline at end of file diff --git a/quicktap/src/noise/error.rs b/quicktap/src/noise/error.rs index 21474f0..af34821 100644 --- a/quicktap/src/noise/error.rs +++ b/quicktap/src/noise/error.rs @@ -12,7 +12,7 @@ pub enum NoiseError { /// Represents an opaque error from ChaCha ChaCha20Error(chacha20poly1305::Error), /// Represents that the packet had a missing or incorrect cookie MAC. - PacketUnauthenticated + PacketUnauthenticated, } impl Error for NoiseError {} impl Display for NoiseError { @@ -32,12 +32,21 @@ pub enum NoisePacketParseError { InvalidLength(usize, usize), /// Represents that the packet being parsed is of the incorrect type. The first value is the expected packet type, the second was the actual packet type WrongPacketType(usize, usize), + /// Represents that the expected 3 bytes of reserved space are missing + MissingReserved, + /// Represents that the index expected to be present on the message is incorrect + WrongIndex(usize, usize), + /// Indicates that this packet has been replayed and should be ignored. + ReplayAttack, } impl Display for NoisePacketParseError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match &self { Self::InvalidLength(expected, got) => write!(f, "Invalid packet length: expected {} got {}", expected, got), - Self::WrongPacketType(expected, got) => write!(f, "Incorrect packet type: expected {} got {}", expected, got) + Self::WrongPacketType(expected, got) => write!(f, "Incorrect packet type: expected {} got {}", expected, got), + Self::MissingReserved => write!(f, "Missing reserved bytes"), + Self::WrongIndex(expected, got) => write!(f, "Incorrect session index: expected {} got {}", expected, got), + Self::ReplayAttack => write!(f, "!!! !!! THIS PACKET HAS BEEN REPLAYED !!! !!! Something fishy is going on.") } } } \ No newline at end of file diff --git a/quicktap/src/noise/handshake/initiator.rs b/quicktap/src/noise/handshake/initiator.rs index 8197304..c76ce08 100644 --- a/quicktap/src/noise/handshake/initiator.rs +++ b/quicktap/src/noise/handshake/initiator.rs @@ -19,6 +19,7 @@ use crate::qcrypto::pki::{qcrypto_dh_generate_longterm, qcrypto_dh_longterm}; #[allow(clippy::module_name_repetitions)] #[allow(clippy::unwrap_used)] // Safe because it is only used for type conversions in known safe ways pub fn handshake_init_to(session: &mut HandshakeState) -> Result<[u8; 148], NoiseError> { + session.we_are_initiator = true; session.s_pub_i = PublicKey::from(session.s_priv_me); session.s_pub_r = session.s_pub_them; @@ -185,6 +186,7 @@ pub fn handshake_init_from(session: &mut HandshakeState, packet: [u8; 148]) -> R session.s_pub_i = s_pub_i; session.s_pub_r = s_pub_r; session.i_i = i_i; + session.we_are_initiator = false; Ok(()) } \ No newline at end of file diff --git a/quicktap/src/noise/handshake/mod.rs b/quicktap/src/noise/handshake/mod.rs index a62d2ad..1649a6f 100644 --- a/quicktap/src/noise/handshake/mod.rs +++ b/quicktap/src/noise/handshake/mod.rs @@ -3,8 +3,9 @@ use std::fmt::{Debug, Formatter}; use rand::rngs::OsRng; use tai64::Tai64N; -use x25519_dalek::{PublicKey, StaticSecret}; +use x25519_dalek::{EphemeralSecret, PublicKey, StaticSecret}; +use crate::qcrypto::hkdf::qcrypto_hkdf; use crate::qcrypto::timestamp; pub mod initiator; @@ -54,7 +55,15 @@ pub struct HandshakeState<'a> { pub q: [u8; 32], - pub cookies: Vec + pub cookies: Vec, + + pub t_send: [u8; 32], + pub t_recv: [u8; 32], + + pub n_send: u64, + pub n_recv: u64, + + pub we_are_initiator: bool } impl<'a> HandshakeState<'a> { /// Determines if the state variables of this `HandshakeState` are the same as another @@ -63,6 +72,26 @@ impl<'a> HandshakeState<'a> { self.h == other.h && self.ck == other.ck && self.e_pub_i == other.e_pub_i && self.s_pub_i == other.s_pub_i && self.s_pub_r == other.s_pub_r && self.i_i == other.i_i && self.i_r == other.i_r } + /// Zeroes out handshake state and derives data transport keys. The handshake must be completed and the other party must do this at the same time otherwise the handshake will be trashed and you have to start over. + pub fn derive_transport(&mut self) { + let t_send_recv = qcrypto_hkdf::<2>(&self.ck, &[]); + if self.we_are_initiator { + self.t_send = t_send_recv[0]; + self.t_recv = t_send_recv[1]; + } else { + self.t_send = t_send_recv[1]; + self.t_recv = t_send_recv[0]; + } + + self.n_send = 0; + self.n_recv = 0; + + self.e_pub_r = PublicKey::from([0u8; 32]); + self.e_priv_me = StaticSecret::from([0u8; 32]); + self.e_pub_i = PublicKey::from([0u8; 32]); + self.ck = [0u8; 32]; + } + /// Create a new handshake state representing a brand-new handshake. /// This function initializes the important values with their appropriate initialization vectors, and zeroes out all other values. pub fn new(private_key: &'a StaticSecret, other_pubkey: PublicKey, pre_shared_key: Option<[u8; 32]>) -> Self { @@ -80,6 +109,11 @@ impl<'a> HandshakeState<'a> { i_r: 0, q: pre_shared_key.unwrap_or([0u8; 32]), cookies: vec![], + t_send: [0u8; 32], + t_recv: [0u8; 32], + n_send: 0, + n_recv: 0, + we_are_initiator: false, } } } diff --git a/quicktap/src/noise/mod.rs b/quicktap/src/noise/mod.rs index fff7a3f..4a489be 100644 --- a/quicktap/src/noise/mod.rs +++ b/quicktap/src/noise/mod.rs @@ -1,3 +1,7 @@ //! Contains structs and functions for serializing and deserializing different packets in the `Noise_IKpsk2` handshake and data frames pub mod handshake; -pub mod error; \ No newline at end of file +pub mod error; +pub mod data; + +#[cfg(test)] +pub mod tests; \ No newline at end of file diff --git a/quicktap/src/noise/tests.rs b/quicktap/src/noise/tests.rs new file mode 100644 index 0000000..c9f7a35 --- /dev/null +++ b/quicktap/src/noise/tests.rs @@ -0,0 +1,35 @@ +#![allow(clippy::unwrap_used)] +#![allow(clippy::pedantic)] + +use crate::noise::data::{deencapsulate, encapsulate}; +use crate::noise::handshake::HandshakeState; +use crate::noise::handshake::initiator::{handshake_init_from, handshake_init_to}; +use crate::noise::handshake::response::{handshake_response_from, handshake_response_to}; +use crate::qcrypto::pki::qcrypto_dh_generate_longterm; + +#[test] +pub fn noise_transport_test() { + let alice_keypair = qcrypto_dh_generate_longterm(); + let bob_keypair = qcrypto_dh_generate_longterm(); + + let mut alice_session = HandshakeState::new(&alice_keypair.0, bob_keypair.1, None); + let mut bob_session = HandshakeState::new(&bob_keypair.0, alice_keypair.1, None); + + let handshake_init = handshake_init_to(&mut alice_session).unwrap(); + handshake_init_from(&mut bob_session, handshake_init).unwrap(); + + let handshake_response = handshake_response_to(&mut bob_session).unwrap(); + handshake_response_from(&mut alice_session, handshake_response).unwrap(); + + alice_session.derive_transport(); + bob_session.derive_transport(); + + assert!(alice_session.is_eq(&bob_session)); + + let packet_abc = vec![42u8; 17]; + let packet_encapsulated = encapsulate(&mut alice_session, &packet_abc).unwrap(); + + let packet_abc_deencapsulated = deencapsulate(&mut bob_session, &packet_encapsulated).unwrap(); + + assert_eq!(packet_abc, packet_abc_deencapsulated) +} \ No newline at end of file