diff --git a/flake.nix b/flake.nix index cd2c196..89bfdb9 100644 --- a/flake.nix +++ b/flake.nix @@ -28,15 +28,17 @@ fd #(rust-bin.stable.latest.default.override { # go with nightly to have async fn in traits - (rust-bin.nightly."2023-02-01".default.override { - #extensions = [ "rust-src" ]; - #targets = [ "arm-unknown-linux-gnueabihf" ]; - }) + #(rust-bin.nightly."2023-02-01".default.override { + # #extensions = [ "rust-src" ]; + # #targets = [ "arm-unknown-linux-gnueabihf" ]; + #}) clippy - lld cargo-watch - rustfmt cargo-license + lld + rust-bin.stable.latest.default + rustfmt + rust-analyzer ]; shellHook = '' # use zsh or other custom shell diff --git a/src/auth/mod.rs b/src/auth/mod.rs new file mode 100644 index 0000000..f7fd3ec --- /dev/null +++ b/src/auth/mod.rs @@ -0,0 +1,44 @@ +//! Authentication reslated struct definitions + +/// User identifier. 16 bytes for easy uuid conversion +#[derive(Debug, Copy, Clone)] +pub struct UserID([u8; 16]); + +impl From<[u8; 16]> for UserID { + fn from(raw: [u8; 16]) -> Self { + UserID(raw) + } +} + +impl UserID { + /// length of the User ID in bytes + pub const fn len() -> usize { + 16 + } +} +/// Authentication Token, basically just 32 random bytes +#[derive(Copy, Clone)] +pub struct Token([u8; 32]); + +impl Token { + /// length of the token in bytes + pub const fn len() -> usize { + 32 + } +} + +impl From<[u8; 32]> for Token { + fn from(raw: [u8; 32]) -> Self { + Token(raw) + } +} + +// Fake debug implementation to avoid leaking tokens +impl ::core::fmt::Debug for Token { + fn fmt( + &self, + f: &mut core::fmt::Formatter<'_>, + ) -> Result<(), ::std::fmt::Error> { + ::core::fmt::Debug::fmt("[hidden token]", f) + } +} diff --git a/src/connection/handshake/dirsync.rs b/src/connection/handshake/dirsync.rs index 95fc4f2..788a231 100644 --- a/src/connection/handshake/dirsync.rs +++ b/src/connection/handshake/dirsync.rs @@ -10,13 +10,16 @@ use super::{Error, HandshakeData}; use crate::{ + auth, connection::ID, enc::{ asym::{ExchangePubKey, KeyExchange, KeyID}, sym::CipherKind, }, }; -use ::std::vec::Vec; +use ::std::{collections::VecDeque, num::NonZeroU64, vec::Vec}; + +type Nonce = [u8; 16]; /// Parsed handshake #[derive(Debug, Clone)] @@ -39,7 +42,14 @@ pub struct Req { /// Client ephemeral public key used for key exchanges pub exchange_key: ExchangePubKey, /// encrypted data - pub enc: Vec, + pub data: ReqInner, +} + +impl Req { + /// Set the cleartext data after it was parsed + pub fn set_data(&mut self, data: ReqData) { + self.data = ReqInner::Data(data); + } } impl super::HandshakeParsing for Req { @@ -63,30 +73,102 @@ impl super::HandshakeParsing for Req { Ok(exchange_key) => exchange_key, Err(e) => return Err(e.into()), }; - let enc = raw[(4 + len)..].to_vec(); + let mut vec = VecDeque::with_capacity(raw.len() - (4 + len)); + vec.extend(raw[(4 + len)..].iter().copied()); + let _ = vec.make_contiguous(); + let data = ReqInner::Ciphertext(vec); Ok(HandshakeData::DirSync(DirSync::Req(Self { key_id, exchange, cipher, exchange_key, - enc, + data, }))) } } +/// Quick way to avoid mixing cipher and clear text +#[derive(Debug, Clone)] +pub enum ReqInner { + /// Client data, still in ciphertext + Ciphertext(VecDeque), + /// Client data, decrypted but unprocessed + Cleartext(VecDeque), + /// Parsed client data + Data(ReqData), +} +impl ReqInner { + /// Get the ciptertext, or panic + pub fn ciphertext<'a>(&'a mut self) -> &'a mut VecDeque { + match self { + ReqInner::Ciphertext(data) => data, + _ => panic!(), + } + } + /// switch from ciphertext to cleartext + pub fn mark_as_cleartext(&mut self) { + let mut newdata: VecDeque; + match self { + ReqInner::Ciphertext(data) => { + newdata = VecDeque::new(); + ::core::mem::swap(&mut newdata, data); + } + _ => return, + } + *self = ReqInner::Cleartext(newdata); + } +} + /// Decrypted request data #[derive(Debug, Clone, Copy)] pub struct ReqData { /// Random nonce, the client can use this to track multiple key exchanges - pub nonce: [u8; 16], + pub nonce: Nonce, /// Client key id so the client can use and rotate keys pub client_key_id: KeyID, + /// User of the domain + pub user: auth::UserID, /// Authentication token - pub token: [u8; 32], + pub token: auth::Token, /// Receiving connection id for the client pub id: ID, // TODO: service info } +impl ReqData { + /// Parse the cleartext raw data + pub fn parse(raw: &ReqInner) -> Result { + const MIN_PKT_LEN: usize = 16 + + KeyID::len() + + auth::UserID::len() + + auth::Token::len() + + ID::len(); + let raw = match raw { + ReqInner::Cleartext(raw) => raw.as_slices().0, + _ => return Err(Error::Parsing), + }; + if raw.len() < MIN_PKT_LEN { + return Err(Error::NotEnoughData); + } + let nonce: Nonce = raw.try_into().unwrap(); + let client_key_id = + KeyID(u16::from_le_bytes(raw[16..17].try_into().unwrap())); + let raw_user: [u8; 16] = raw[18..34].try_into().unwrap(); + let user: auth::UserID = raw_user.into(); + let raw_token: [u8; 32] = raw[34..66].try_into().unwrap(); + let token: auth::Token = raw_token.into(); + let id: ID = u64::from_le_bytes(raw[66..74].try_into().unwrap()).into(); + if id.is_handshake() { + return Err(Error::Parsing); + } + Ok(Self { + nonce, + client_key_id, + user, + token, + id, + }) + } +} /// Server response in a directory synchronized handshake #[derive(Debug, Clone)] @@ -107,7 +189,7 @@ impl super::HandshakeParsing for Resp { #[derive(Debug, Clone, Copy)] pub struct RespData { /// Client nonce, copied from the request - client_nonce: [u8; 16], + client_nonce: Nonce, /// Server Connection ID id: ID, /// Service Connection ID @@ -115,3 +197,27 @@ pub struct RespData { /// Service encryption key service_key: [u8; 32], } + +impl RespData { + const NONCE_LEN: usize = ::core::mem::size_of::(); + /// Return the expected length for buffer allocation + pub fn len() -> usize { + Self::NONCE_LEN + ID::len() + ID::len() + 32 + } + /// Serialize the data into a buffer + pub fn serialize(&self, out: &mut [u8]) { + assert!(out.len() == Self::len(), "wrong buffer size"); + let mut start = 0; + let mut end = Self::NONCE_LEN; + out[start..end].copy_from_slice(&self.client_nonce); + start = end; + end = end + Self::NONCE_LEN; + self.id.serialize(&mut out[start..end]); + start = end; + end = end + Self::NONCE_LEN; + self.service_id.serialize(&mut out[start..end]); + start = end; + end = end + Self::NONCE_LEN; + out[start..end].copy_from_slice(&self.service_key); + } +} diff --git a/src/connection/packet.rs b/src/connection/packet.rs index a185838..db97c8b 100644 --- a/src/connection/packet.rs +++ b/src/connection/packet.rs @@ -17,6 +17,20 @@ impl ConnectionID { pub fn is_handshake(&self) -> bool { *self == ConnectionID::Handshake } + /// length if the connection ID in bytes + pub const fn len() -> usize { + 8 + } + /// write the ID to a buffer + pub fn serialize(&self, out: &mut [u8]) { + assert!(out.len() == 8, "Insufficient buffer"); + match self { + ConnectionID::Handshake => out[..].copy_from_slice(&[0; 8]), + ConnectionID::ID(id) => { + out[..].copy_from_slice(&id.get().to_le_bytes()) + } + } + } } impl From for ConnectionID { diff --git a/src/enc/asym.rs b/src/enc/asym.rs index 7eccda4..f7a79fa 100644 --- a/src/enc/asym.rs +++ b/src/enc/asym.rs @@ -10,6 +10,13 @@ use crate::enc::sym::Secret; #[derive(Debug, Copy, Clone, PartialEq)] pub struct KeyID(pub u16); +impl KeyID { + /// Length of the Key ID in bytes + pub const fn len() -> usize { + 2 + } +} + /// Kind of key used in the handshake #[derive(Debug, Copy, Clone, PartialEq, ::num_derive::FromPrimitive)] #[repr(u8)] diff --git a/src/enc/errors.rs b/src/enc/errors.rs index 86af252..ae1961b 100644 --- a/src/enc/errors.rs +++ b/src/enc/errors.rs @@ -19,4 +19,7 @@ pub enum Error { /// Unsupported cipher #[error("unsupported cipher")] UnsupportedCipher, + /// Can not decrypt. Either corrupted or malicious data + #[error("decrypt: corrupted data")] + Decrypt, } diff --git a/src/enc/sym.rs b/src/enc/sym.rs index 4e819d0..a7e1c80 100644 --- a/src/enc/sym.rs +++ b/src/enc/sym.rs @@ -1,5 +1,7 @@ //! Symmetric cypher stuff +use super::Error; +use ::std::collections::VecDeque; use ::zeroize::Zeroize; /// Secret, used for keys. @@ -106,31 +108,41 @@ impl Cipher { } } } - fn decrypt(&self, aad: AAD, data: &mut [u8]) -> Result<(), ()> { + fn decrypt<'a>( + &self, + aad: AAD, + data: &mut VecDeque, + ) -> Result<(), Error> { match self { Cipher::XChaCha20Poly1305(cipher) => { use ::chacha20poly1305::{ aead::generic_array::GenericArray, AeadInPlace, }; - // FIXME: check min data length - let (nonce_bytes, data_and_tag) = data.split_at_mut(13); - let (data_notag, tag_bytes) = data_and_tag.split_at_mut( - data_and_tag.len() + 1 - - ::ring::aead::CHACHA20_POLY1305.tag_len(), - ); - let nonce = GenericArray::from_slice(nonce_bytes); - let tag = GenericArray::from_slice(tag_bytes); - let maybe = cipher.cipher.decrypt_in_place_detached( - nonce.into(), - aad.0, - data_notag, - tag, - ); - if maybe.is_err() { - Err(()) - } else { - Ok(()) + let final_len: usize; + { + let raw_data = data.as_mut_slices().0; + // FIXME: check min data length + let (nonce_bytes, data_and_tag) = raw_data.split_at_mut(13); + let (data_notag, tag_bytes) = data_and_tag.split_at_mut( + data_and_tag.len() + 1 + - ::ring::aead::CHACHA20_POLY1305.tag_len(), + ); + let nonce = GenericArray::from_slice(nonce_bytes); + let tag = GenericArray::from_slice(tag_bytes); + let maybe = cipher.cipher.decrypt_in_place_detached( + nonce.into(), + aad.0, + data_notag, + tag, + ); + if maybe.is_err() { + return Err(Error::Decrypt); + } + final_len = data_notag.len(); } + data.drain(..Nonce::len()); + data.truncate(final_len); + Ok(()) } } } @@ -151,7 +163,11 @@ impl CipherRecv { } /// Decrypt a paket. Nonce and Tag are taken from the packet, /// while you need to provide AAD (Additional Authenticated Data) - pub fn decrypt(&self, aad: AAD, data: &mut [u8]) -> Result<(), ()> { + pub fn decrypt<'a>( + &self, + aad: AAD, + data: &mut VecDeque, + ) -> Result<(), Error> { self.0.decrypt(aad, data) } } @@ -236,6 +252,10 @@ impl Nonce { } } } + /// Length of this nonce in bytes + pub fn len() -> usize { + return 12; + } /// Get reference to the nonce bytes pub fn as_bytes(&self) -> &[u8] { #[allow(unsafe_code)] diff --git a/src/lib.rs b/src/lib.rs index c97fc38..fa19e94 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,13 +13,15 @@ //! //! libFenrir is the official rust library implementing the Fenrir protocol +pub mod auth; mod config; pub mod connection; pub mod dnssec; pub mod enc; -use ::arc_swap::{ArcSwap, ArcSwapAny}; -use ::std::{net::SocketAddr, sync::Arc}; +use ::arc_swap::{ArcSwap, ArcSwapAny, ArcSwapOption}; +use ::std::{net::SocketAddr, pin::Pin, sync::Arc}; +use ::tokio::macros::support::Future; use ::tokio::{net::UdpSocket, task::JoinHandle}; use crate::enc::{ @@ -50,18 +52,35 @@ pub enum Error { Key(#[from] crate::enc::Error), } +// No async here struct FenrirInner { key_exchanges: ArcSwapAny>>, ciphers: ArcSwapAny>>, keys: ArcSwapAny>>, } +/// Intermediate actions to be taken while parsing the handshake +#[derive(Debug, Clone)] +pub enum HandshakeAction { + /// Parsing finished, all ok, nothing to do + None, + /// Packet parsed, now go perform authentication + AuthNeeded(Handshake), +} + +// No async here impl FenrirInner { - fn recv_handshake(&self, handshake: Handshake) -> Result<(), Error> { - use connection::handshake::{dirsync::DirSync, HandshakeData}; + fn recv_handshake( + &self, + mut handshake: Handshake, + ) -> Result { + use connection::handshake::{ + dirsync::{self, DirSync}, + HandshakeData, + }; match handshake.data { - HandshakeData::DirSync(ds) => match ds { - DirSync::Req(mut req) => { + HandshakeData::DirSync(ref mut ds) => match ds { + DirSync::Req(ref mut req) => { let ephemeral_key = { // Keep this block short to avoid contention // on self.keys @@ -70,7 +89,7 @@ impl FenrirInner { keys.iter().find(|k| k.id == req.key_id) { use enc::asym::PrivKey; - // Directory synchronized can only used keys + // Directory synchronized can only use keys // for key exchange, not signing keys if let PrivKey::Exchange(k) = &h_k.key { Some(k.clone()) @@ -115,9 +134,15 @@ impl FenrirInner { let cipher_recv = CipherRecv::new(req.cipher, secret_recv); use crate::enc::sym::AAD; let aad = AAD(&mut []); // no aad for now - let _ = cipher_recv.decrypt(aad, &mut req.enc); + match cipher_recv.decrypt(aad, &mut req.data.ciphertext()) { + Ok(()) => req.data.mark_as_cleartext(), + Err(e) => { + return Err(handshake::Error::Key(e).into()); + } + } + req.set_data(dirsync::ReqData::parse(&req.data)?); - todo!(); + return Ok(HandshakeAction::AuthNeeded(handshake)); } DirSync::Resp(resp) => { todo!(); @@ -127,6 +152,12 @@ impl FenrirInner { } } +type TokenChecker = + fn( + user: auth::UserID, + token: auth::Token, + ) -> ::futures::future::BoxFuture<'static, Result>; + /// Instance of a fenrir endpoint #[allow(missing_copy_implementations, missing_debug_implementations)] pub struct Fenrir { @@ -140,6 +171,8 @@ pub struct Fenrir { stop_working: ::tokio::sync::broadcast::Sender, /// Private keys used in the handshake _inner: Arc, + /// where to ask for token check + token_check: Arc>, } // TODO: graceful vs immediate stop @@ -165,6 +198,7 @@ impl Fenrir { key_exchanges: ArcSwapAny::new(Arc::new(Vec::new())), keys: ArcSwapAny::new(Arc::new(Vec::new())), }), + token_check: Arc::new(ArcSwapOption::from(None)), }; Ok(endpoint) } @@ -259,6 +293,7 @@ impl Fenrir { async fn listen_udp( mut stop_working: ::tokio::sync::broadcast::Receiver, fenrir: Arc, + token_check: Arc>, socket: Arc, ) -> ::std::io::Result<()> { // jumbo frames are 9K max @@ -272,7 +307,13 @@ impl Fenrir { result? } }; - Self::recv(fenrir.clone(), &buffer[0..bytes], sock_from).await; + Self::recv( + fenrir.clone(), + token_check.clone(), + &buffer[0..bytes], + sock_from, + ) + .await; } Ok(()) } @@ -293,6 +334,7 @@ impl Fenrir { let join = ::tokio::spawn(Self::listen_udp( stop_working, self._inner.clone(), + self.token_check.clone(), s.clone(), )); self.sockets.push((s, join)); @@ -323,6 +365,7 @@ impl Fenrir { /// Read and do stuff with the udp packet async fn recv( fenrir: Arc, + token_check: Arc>, buffer: &[u8], _sock_from: SocketAddr, ) { @@ -340,10 +383,52 @@ impl Fenrir { return; } }; - if let Err(err) = fenrir.recv_handshake(handshake) { - ::tracing::debug!("Handshake recv error {}", err); - return; - } + let action = match fenrir.recv_handshake(handshake) { + Ok(action) => action, + Err(err) => { + ::tracing::debug!("Handshake recv error {}", err); + return; + } + }; + match action { + HandshakeAction::AuthNeeded(hshake) => { + let tk_check = match token_check.load_full() { + Some(tokenchecker) => tokenchecker, + None => { + ::tracing::error!( + "Handshake received, but no tocken_checker" + ); + return; + } + }; + use handshake::{ + dirsync::{self, DirSync}, + HandshakeData, + }; + match hshake.data { + HandshakeData::DirSync(ds) => match ds { + DirSync::Req(req) => { + use dirsync::ReqInner; + let req_data = match req.data { + ReqInner::Data(req_data) => req_data, + _ => { + ::tracing::error!( + "token_check: expected Data" + ); + return; + } + }; + tk_check(req_data.user, req_data.token).await; + todo!() + } + _ => { + todo!() + } + }, + } + } + _ => {} + }; } // copy packet, spawn todo!();