diff --git a/Cargo.lock b/Cargo.lock index 00d3bcc..5ec232d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -208,10 +208,11 @@ dependencies = [ [[package]] name = "cryptohelpers" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31fca6629a75c37a8831f93133f86e20efb8dd7a94060e32635ebf9a62aca12f" +checksum = "1758ba574c79ae6db3ccf6623cacc5293c2c0a14de871a7b95d4286861cbd504" dependencies = [ + "futures", "getrandom 0.1.15", "hex-literal", "hmac", diff --git a/Cargo.toml b/Cargo.toml index 88b6a91..3f22a20 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,7 @@ base64 = "0.13.0" bitflags = "1.2.1" chrono = "0.4.19" color-eyre = {version = "0.5", default-features=false} -cryptohelpers = { version = "1.6", default-features=false, features = ["async", "sha256", "rsa", "serialise", "aes"] } +cryptohelpers = { version = "1.7", default-features=false, features = ["async", "sha256", "rsa", "serialise", "aes"] } futures = "0.3.8" generational-arena = {version = "0.2.8", features= ["serde"]} getrandom = "0.2.0" diff --git a/src/conv.rs b/src/conv.rs index dafbae1..bd2fbd9 100644 --- a/src/conv.rs +++ b/src/conv.rs @@ -123,9 +123,9 @@ impl ModifiedBase64String } /// Consume into decoded bytes, write those bytes into the provided buffer - pub fn decode(self, output: &mut Vec) + pub fn decode(self, output: &mut [u8]) -> usize { - base64::decode_config_buf(self.into_base64(), base64::STANDARD, output).expect("modified base64 string contained invalid formatted data") + base64::decode_config_slice(self.into_base64(), base64::STANDARD, output).expect("modified base64 string contained invalid formatted data") } /// Consume into decoded bytes, return those bytes as a new `Vec` diff --git a/src/server/web/auth.rs b/src/server/web/auth.rs index 1a8011b..dd8834c 100644 --- a/src/server/web/auth.rs +++ b/src/server/web/auth.rs @@ -1,21 +1,35 @@ //! Authentication use super::*; +use tokio::time; +use std::{error, fmt}; +#[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct Sha256Hash(pub sha256::Sha256Hash); + type RsaSignature = rsa::Signature; +#[derive(Debug)] +pub struct DecodeTokenError; + impl str::FromStr for Sha256Hash { - type Err = (); + type Err = DecodeTokenError; fn from_str(s: &str) -> Result { - todo!() //read encoded base64(?)/hex into `Signature` + conv::ModifiedBase64String::try_from_base64(s).map_err(|_| DecodeTokenError).and_then(|md| { + let mut output =sha256::Sha256Hash::default(); + if md.decode(output.as_mut()) == sha256::SIZE { + Ok(Self(output)) + } else { + Err(DecodeTokenError) + } + }) } } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct AuthRequest { - id: Uuid, + pub id: Uuid, sign_this: [u8; 32], salt: [u8; 16], @@ -25,6 +39,21 @@ pub struct AuthRequest impl AuthRequest { + pub fn hash_password(&self, _state: &State, passwd: &str) -> sha256::Sha256Hash + { + // NOTE: _state will be used when we have a 2nd global salt as well, for now, ignore it. + + sha256::compute_slices([passwd.as_bytes(), &self.salt[..]].iter()) + } +} + +impl AuthRequest +{ + /// The TTL for this auth request + pub fn ttl(&self) -> time::Duration + { + time::Duration::from_millis(self.ttl_ms) + } /// Create a new auth request pub fn new(cfg: &settings::Settings) -> Self { @@ -45,22 +74,107 @@ pub async fn auth_req(who: source::IpAddr, state: Arc) -> Result, req_id: Uuid, num: usize, body: Bytes) -> Result<(), Infallible> +async fn real_auth_key(state: Arc, req_id: Uuid, sigs: impl IntoIterator) -> Result<(), AuthError> { - trace!("{:?} auth resp key <{}>:{}", who, req_id, num); - Ok(()) } +pub async fn auth_key(who: source::IpAddr, state: Arc, req_id: Uuid, num: usize, body: Bytes) -> Result<(), warp::Rejection> +{ + trace!("{:?} auth resp key <{}>:{}", who, req_id, num); + + //TODO: Read keys from body, pass to `real_auth_key`. + todo!() +} -pub async fn auth_pass(who: source::IpAddr, state: Arc, req_id: Uuid, passhash: sha256::Sha256Hash) -> Result<(), Infallible> +async fn real_auth_pass(state: Arc, req_id: Uuid, passhash: sha256::Sha256Hash) -> Result<(), AuthError> { - trace!("{:?} auth resp pass <{}>: \"{}\"", who, req_id, passhash); + let req = { + state.auth_tokens().await.handle_req(req_id)? + }; + if !req.passwd_is_allowed { + return Err(AuthError::Method); + } + + //TODO: Grab valid password hash from `State` and compare + + //TODO: Generate real authoriseation token that maps to whichever user was authorised, insert into state with a TTL that gets refreshed when the token is used. Ok(()) } +pub async fn auth_pass(who: source::IpAddr, state: Arc, req_id: Uuid, passhash: sha256::Sha256Hash) -> Result<(), warp::Rejection> +{ + trace!("{:?} auth resp pass <{}>: \"{}\"", who, req_id, passhash); + real_auth_pass(state, req_id, passhash).await.map_err(warp::reject::custom) +} + +#[derive(Debug)] +pub enum AuthError +{ + Id, + Hash, + Sig, + Method, + + Internal, +} + +impl AuthError +{ + /// A warp recovery filter for auth errors + pub async fn recover(err: warp::Rejection) -> Result + { + use warp::http::StatusCode; + if let Some(this) = err.find::() { + let code = match this { + Self::Id + => return Err(warp::reject::not_found()), + Self::Hash + | Self::Sig + => StatusCode::FORBIDDEN, + Self::Method + => StatusCode::METHOD_NOT_ALLOWED, + _ => StatusCode::INTERNAL_SERVER_ERROR, + }; + Ok(warp::reply::with_status(format!("auth failed: {}", this), code)) + } else { + Err(err) + } + } +} + +impl error::Error for AuthError{} +impl warp::reject::Reject for AuthError{} +impl fmt::Display for AuthError +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result + { + match self + { + Self::Id => write!(f, "invalid response id"), + Self::Hash => write!(f, "no matching hash"), + Self::Sig => write!(f, "no matching signature"), + Self::Method => write!(f, "auth method not allowed"), + + _ => write!(f, "internal error"), + } + } +} + +impl From for AuthError +{ + fn from(_: state::AuthCacheError) -> Self + { + Self::Id + } +} + diff --git a/src/server/web/mod.rs b/src/server/web/mod.rs index 1b41070..90a88aa 100644 --- a/src/server/web/mod.rs +++ b/src/server/web/mod.rs @@ -92,7 +92,9 @@ pub async fn main(state: ServerState, cfg: Settings) -> eyre::Result<()> resp }; - warp::path("auth").and(req.or(resp)) + warp::path("auth") + .and(req.or(resp)) + .recover(auth::AuthError::recover) }; todo!() diff --git a/src/server/web/state.rs b/src/server/web/state.rs index d8a7009..d1aed71 100644 --- a/src/server/web/state.rs +++ b/src/server/web/state.rs @@ -1,15 +1,142 @@ //! Web server state use super::*; +use std::{ + collections::HashMap, + sync::Arc, + pin::Pin, + task::Context, + task::Poll, + + fmt,error, +}; use tokio::{ sync::{ RwLock, + RwLockWriteGuard, + RwLockReadGuard, + }, + time::{ + self, + DelayQueue, + delay_queue, }, }; +#[derive(Debug)] +pub struct AuthContainer +{ + active_req: HashMap, + timeouts: DelayQueue, +} + +pub struct AuthPurge<'a, F = fn(auth::AuthRequest)>(&'a mut AuthContainer, F); + +impl<'a, F> Future for AuthPurge<'a, F> +where F: FnMut(auth::AuthRequest) + 'a + Unpin +{ + type Output = Result<(), time::Error>; + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let this = self.get_mut(); + while let Some(res) = futures::ready!(this.0.timeouts.poll_expired(cx)) { + let ent = res?; + let _ = this.0.active_req.remove(ent.get_ref()).map(|x| x.0).map(&mut this.1); + } + + Poll::Ready(Ok(())) + } +} + +impl AuthContainer +{ + /// Gerate a new empty auth token container + fn new() -> Self + { + Self { + active_req: HashMap::new(), + timeouts: DelayQueue::new(), + } + } + /// Returns a future that purges expired entries, running the provided closure on them. + /// + /// The future will yield if: + /// * The stire is not empty + /// * There are non-expired entries in the store + pub fn purge_and_then<'a, F: FnMut(auth::AuthRequest) +Unpin +'a>(&'a mut self, and_then: F) -> AuthPurge<'a, F> + { + AuthPurge(self, and_then) + } + /// Returns a future that purges expired entries. See `purge_and_then`. + #[inline] pub fn purge(&mut self) -> AuthPurge<'_> + { + AuthPurge(self, std::mem::drop) + } + /// Purge all expired entries. + #[inline] pub fn purge_now(&mut self) + { + self.purge().now_or_never(); + } + /// Purge all expired entries, running the provided closure on them. + pub fn purge_now_and_then<'a, F: FnMut(auth::AuthRequest) +Unpin+'a>(&'a mut self, and_then: F) + { + self.purge_and_then(and_then).now_or_never(); + } + + /// Insert a request into the store, setting it to expire once its ttl is up. + pub fn insert_req(&mut self, req: auth::AuthRequest) + { + self.purge_now(); + + let k = self.timeouts.insert(req.id, req.ttl()); + self.active_req.insert(req.id, (req, k)); + } + + /// Attempt to retrieve a value from the store by its ID. + /// + /// # Notes + /// `AuthCacheError::Timeout` will only be returned if the request we're trying to extract has timed out *and not yet been removed* yet by an earlier, potentially unrelated, call to `handle_req` *or* `insert_req` (or an explicit purge). + /// If an error is returned you cannot rely on the accuracy of the error kind. + pub fn handle_req(&mut self, id: Uuid) -> Result + { + let mut timed_out=false; + self.purge_now_and_then(|other| if other.id==id { timed_out = true; }); + + if timed_out { + Err(AuthCacheError::Timeout) + } else { + self.active_req.remove(&id).ok_or(AuthCacheError::Removed).map(|(v, k)| { + self.timeouts.remove(&k); + v + }) + } + } +} + +#[derive(Debug)] +#[non_exhaustive] +pub enum AuthCacheError +{ + Removed, + Timeout, +} +impl error::Error for AuthCacheError{} +impl fmt::Display for AuthCacheError +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result + { + match self { + Self::Removed => write!(f, "id was not present"), + Self::Timeout => write!(f, "id timed out as we read it"), + } + } +} + #[derive(Debug)] pub struct State { backend: RwLock, + + auth_tokens: RwLock, + //TODO: user auths, public keys, hashed passwords, etc. settings: Settings, } @@ -19,6 +146,8 @@ impl State pub fn new(backend: ServerState, settings: Settings) -> Self { Self { + auth_tokens: RwLock::new(AuthContainer::new()), + backend: RwLock::new(backend), settings, } @@ -29,4 +158,18 @@ impl State { &self.settings } + + /// Get a write reference to the auth container + pub async fn auth_tokens(&self) -> RwLockWriteGuard<'_, AuthContainer> + { + self.auth_tokens.write().await + } + + /// Get a read reference to the auth container. + /// + /// Typically only useful for debugging/logging. + pub async fn auth_tokens_ref(&self) -> RwLockReadGuard<'_, AuthContainer> + { + self.auth_tokens.read().await + } }