diff --git a/quicktap/src/noise/error.rs b/quicktap/src/noise/error.rs index 0211fc4..21474f0 100644 --- a/quicktap/src/noise/error.rs +++ b/quicktap/src/noise/error.rs @@ -10,14 +10,17 @@ pub enum NoiseError { /// Represents an error while parsing a Noise packet PacketParseError(NoisePacketParseError), /// Represents an opaque error from ChaCha - ChaCha20Error(chacha20poly1305::Error) + ChaCha20Error(chacha20poly1305::Error), + /// Represents that the packet had a missing or incorrect cookie MAC. + PacketUnauthenticated } impl Error for NoiseError {} impl Display for NoiseError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match &self { Self::PacketParseError(err) => write!(f, "{}", err), - Self::ChaCha20Error(error) => write!(f, "Encryption error: {}", error) + Self::ChaCha20Error(error) => write!(f, "Encryption error: {}", error), + Self::PacketUnauthenticated => write!(f, "Unauthenticated packet") } } } diff --git a/quicktap/src/noise/handshake.rs b/quicktap/src/noise/handshake.rs index 53bc964..9e1014f 100644 --- a/quicktap/src/noise/handshake.rs +++ b/quicktap/src/noise/handshake.rs @@ -1,9 +1,10 @@ //! `Noise_IKpsk2` handshake packets +use std::fmt::{Debug, Formatter}; use tai64::Tai64N; use x25519_dalek::{EphemeralSecret, PublicKey, StaticSecret}; use crate::noise::error::NoiseError; -use crate::qcrypto::aead::qcrypto_aead; +use crate::qcrypto::aead::{qcrypto_aead, qcrypto_aead_decrypt}; use crate::qcrypto::hashes::{qcrypto_hash_twice, qcrypto_mac}; use crate::qcrypto::hkdf::qcrypto_hkdf; use crate::qcrypto::pki::{qcrypto_dh_ephemeral, qcrypto_dh_generate_ephemeral, qcrypto_dh_longterm}; @@ -22,6 +23,7 @@ pub const HANDSHAKE_INITIATOR_CHAIN_KEY_HASH: [u8; 32] = [ ]; /// Represents a cookie we got from the other peer +#[derive(Debug)] pub struct Cookie { time: Tai64N, cookie: [u8; 16] @@ -41,12 +43,35 @@ pub struct HandshakeState { pub e_priv_me: EphemeralSecret, pub s_priv_me: StaticSecret, + pub s_pub_them: PublicKey, pub i_i: u32, pub i_r: u32, pub cookies: Vec } +impl HandshakeState { + /// Determines if the state variables of this `HandshakeState` are the same as another + pub fn is_eq(&self, other: &HandshakeState) -> bool { + self.h_i == other.h_i && self.c_i == other.c_i && 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 + } +} +impl Debug for HandshakeState { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("HandshakeState") + .field("h_i", &self.h_i) + .field("c_i", &self.c_i) + .field("e_pub_i", &self.e_pub_i) + .field("s_pub_i", &self.s_pub_i) + .field("s_pub_r", &self.s_pub_r) + .field("e_priv_me", &"") + .field("s_priv_me", &"") + .field("s_pub_them", &self.s_pub_them) + .field("i_i", &self.i_i) + .field("i_r", &self.i_r) + .field("cookies", &self.cookies).finish() + } +} /// Generate a handshake initiator packet and encrypt it using the given session state, starting a new handshake state /// # Errors @@ -55,11 +80,16 @@ pub struct HandshakeState { /// While containing unwraps, this function will never panic. #[allow(clippy::module_name_repetitions)] pub fn handshake_init_to(session: &mut HandshakeState) -> Result<[u8; 148], NoiseError> { + session.s_pub_i = PublicKey::from(&session.s_priv_me); + session.s_pub_r = session.s_pub_them; + let mut msg = HandshakeInitiatorRaw { sender: session.i_i.to_le_bytes(), ephemeral: [0u8; 32], static_pub: [0u8; 32 + 16], timestamp: [0u8; 12 + 16], + mac1: [0u8; 16], + mac2: [0u8; 16] }; session.c_i = HANDSHAKE_INITIATOR_CHAIN_KEY; @@ -69,6 +99,7 @@ pub fn handshake_init_to(session: &mut HandshakeState) -> Result<[u8; 148], Nois let eph_keypair = qcrypto_dh_generate_ephemeral(); session.c_i = qcrypto_hkdf::<1>(&session.c_i, eph_keypair.1.as_bytes())[0]; + session.e_pub_i = eph_keypair.1; msg.ephemeral = eph_keypair.1.to_bytes(); @@ -106,7 +137,9 @@ struct HandshakeInitiatorRaw { sender: [u8; 4], ephemeral: [u8; 32], static_pub: [u8; 32 + 16], - timestamp: [u8; 12 + 16] + timestamp: [u8; 12 + 16], + mac1: [u8; 16], + mac2: [u8; 16] } impl HandshakeInitiatorRaw { fn to_bytes(&self, session: &HandshakeState) -> [u8; 148] { @@ -124,17 +157,96 @@ impl HandshakeInitiatorRaw { let mut mac2 = [0u8; 16]; - let past_cookie_timeout = match session.cookies[session.cookies.len()-1].time.duration_since(×tamp()) { - Ok(t) => t.as_secs() >= 120, - Err(e) => true - }; - - if !session.cookies.is_empty() && !past_cookie_timeout { - mac2 = qcrypto_mac(&session.cookies[session.cookies.len()-1].cookie, &output[..132]); + if !session.cookies.is_empty() { + let past_cookie_timeout = session.cookies[session.cookies.len()-1].time.duration_since(×tamp()).map_or(true, |t| t.as_secs() >= 120); + if !past_cookie_timeout { + mac2 = qcrypto_mac(&session.cookies[session.cookies.len()-1].cookie, &output[..132]); + } } output[132..148].copy_from_slice(&mac2); output } + + fn from_bytes(bytes: [u8; 148]) -> Self { + Self { + sender: bytes[4..8].try_into().unwrap(), + ephemeral: bytes[8..40].try_into().unwrap(), + static_pub: bytes[40..88].try_into().unwrap(), + timestamp: bytes[88..116].try_into().unwrap(), + mac1: bytes[116..132].try_into().unwrap(), + mac2: bytes[132..148].try_into().unwrap() + } + } +} + +/// Parse a handshake initiator packet and encrypt it using the given session state, updating the session state with decrypted and authenticated values +/// # Errors +/// This function will error if decryption was unsuccessful +/// # Panics +/// While containing unwraps, this function will never panic. +#[allow(clippy::module_name_repetitions)] +pub fn handshake_init_from(session: &mut HandshakeState, packet: [u8; 148]) -> Result<(), NoiseError> { + session.s_pub_i = session.s_pub_them; + session.s_pub_r = PublicKey::from(&session.s_priv_me); + + let mut msg = HandshakeInitiatorRaw::from_bytes(packet); + + session.c_i = HANDSHAKE_INITIATOR_CHAIN_KEY; + session.h_i = HANDSHAKE_INITIATOR_CHAIN_KEY_HASH; + session.h_i = qcrypto_hash_twice(&session.h_i, session.s_pub_r.as_bytes()); + + let ephemeral_public = msg.ephemeral; + let eph_pub = PublicKey::from(ephemeral_public); + session.e_pub_i = eph_pub; + + session.c_i = qcrypto_hkdf::<1>(&session.c_i, eph_pub.as_bytes())[0]; + + session.h_i = qcrypto_hash_twice(&session.h_i, &msg.ephemeral); + + let ci_k_pair = qcrypto_hkdf::<2>(&session.c_i, qcrypto_dh_longterm(&session.s_priv_me, &eph_pub).as_bytes()); + session.c_i = ci_k_pair[0]; + let k = ci_k_pair[1]; + + // This unwrap is safe because the output length is a known constant with these inputs + + + session.s_pub_i = PublicKey::from( as TryInto<[u8; 32]>>::try_into(match qcrypto_aead_decrypt(&k, 0, &msg.static_pub, &session.h_i) { + Ok(s) => s, + Err(e) => return Err(NoiseError::ChaCha20Error(e)) + }).unwrap()); + + session.h_i = qcrypto_hash_twice(&session.h_i, &msg.static_pub); + + let ci_k_pair = qcrypto_hkdf::<2>(&session.c_i, qcrypto_dh_longterm(&session.s_priv_me, &session.s_pub_i).as_bytes()); + session.c_i = ci_k_pair[0]; + let k = ci_k_pair[1]; + + // This unwrap is safe because the output length is a known constant with these inputs + let sent_timestamp: [u8; 12] = match qcrypto_aead_decrypt(&k, 0, &msg.timestamp, &session.h_i) { + Ok(s) => s, + Err(e) => return Err(NoiseError::ChaCha20Error(e)) + }.try_into().unwrap(); + + session.h_i = qcrypto_hash_twice(&session.h_i, &msg.timestamp); + + // we need to check mac1 and mac2 + + let mac1: [u8; 16] = qcrypto_mac(&qcrypto_hash_twice(LABEL_MAC1.as_bytes(), session.s_pub_i.as_bytes()), &packet[..116]); + + let mut mac2 = [0u8; 16]; + + if !session.cookies.is_empty() { + let past_cookie_timeout = session.cookies[session.cookies.len()-1].time.duration_since(×tamp()).map_or(true, |t| t.as_secs() >= 120); + if !past_cookie_timeout { + mac2 = qcrypto_mac(&session.cookies[session.cookies.len() - 1].cookie, &packet[..132]); + } + } + + if mac1 != msg.mac1 || mac2 != msg.mac2 { + return Err(NoiseError::PacketUnauthenticated) + } + + Ok(()) } \ No newline at end of file diff --git a/quicktap/src/qcrypto/tests.rs b/quicktap/src/qcrypto/tests.rs index f45aa9a..cc91489 100644 --- a/quicktap/src/qcrypto/tests.rs +++ b/quicktap/src/qcrypto/tests.rs @@ -1,5 +1,7 @@ use hex_lit::hex; -use x25519_dalek::PublicKey; +use rand::rngs::OsRng; +use x25519_dalek::{EphemeralSecret, PublicKey, StaticSecret}; +use crate::noise::handshake::{handshake_init_from, handshake_init_to, HandshakeState}; use crate::qcrypto::aead::{qcrypto_aead, qcrypto_aead_decrypt, qcrypto_xaead, qcrypto_xaead_decrypt}; use crate::qcrypto::{CONSTURCTION, IDENTIFIER}; use crate::qcrypto::hashes::{qcrypto_hash, qcrypto_hash_twice, qcrypto_hmac, qcrypto_mac}; @@ -56,4 +58,45 @@ fn qcrypto_xaead_test() { fn qcrypto_hkdf_test() { let derived = qcrypto_hkdf::<1>(&[0u8; 32], &[0u8; 32]); assert_eq!(derived, [hex!("1090894613df8aef670b0b867e222daebc0d3e436cdddbc16c65855ab93cc91a")]); +} + +#[test] +fn noise_halfhandshake_test() { + let alice_keypair = qcrypto_dh_generate_longterm(); + let bob_keypair = qcrypto_dh_generate_longterm(); + + let mut alice_session = HandshakeState { + h_i: [0u8; 32], + c_i: [0u8; 32], + e_pub_i: PublicKey::from([0u8; 32]), + s_pub_i: PublicKey::from([0u8; 32]), + s_pub_r: PublicKey::from([0u8; 32]), + e_priv_me: EphemeralSecret::new(OsRng), + s_priv_me: alice_keypair.0, + s_pub_them: bob_keypair.1, + i_i: 0, + i_r: 0, + cookies: vec![], + }; + let mut bob_session = HandshakeState { + h_i: [0u8; 32], + c_i: [0u8; 32], + e_pub_i: PublicKey::from([0u8; 32]), + s_pub_i: PublicKey::from([0u8; 32]), + s_pub_r: PublicKey::from([0u8; 32]), + e_priv_me: EphemeralSecret::new(OsRng), + s_priv_me: bob_keypair.0, + s_pub_them: alice_keypair.1, + i_i: 0, + i_r: 0, + cookies: vec![], + }; + + let handshake_init = handshake_init_to(&mut alice_session).unwrap(); + handshake_init_from(&mut bob_session, handshake_init).unwrap(); + + println!("{:?}", alice_session); + println!("{:?}", bob_session); + + assert!(alice_session.is_eq(&bob_session)); } \ No newline at end of file