auth req tokens

master
Avril 3 years ago
parent 033d70589f
commit f43f5d238d
Signed by: flanchan
GPG Key ID: 284488987C31F630

5
Cargo.lock generated

@ -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",

@ -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"

@ -123,9 +123,9 @@ impl ModifiedBase64String
}
/// Consume into decoded bytes, write those bytes into the provided buffer
pub fn decode(self, output: &mut Vec<u8>)
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<u8>`

@ -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<Self, Self::Err> {
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<State>) -> Result<AuthRequ
{
let req = AuthRequest::new(state.cfg());
trace!("{:?} auth req", who);
// Add `req` into `state` auth hashmap (`req.id` is key) for verification.
// Use `DelayQueue` to remove `req.id` from the hashmap after `ttl` expires.
{
let mut auth = state.auth_tokens().await;
auth.insert_req(req.clone());
}
// TODO: Add `req` into `state` auth hashmap (`req.id` is key) for verification.
// TODO: Use `DelayQueue` to remove `req.id` from the hashmap after `ttl` expires.
Ok(req)
}
pub async fn auth_key(who: source::IpAddr, state: Arc<State>, req_id: Uuid, num: usize, body: Bytes) -> Result<(), Infallible>
async fn real_auth_key(state: Arc<State>, req_id: Uuid, sigs: impl IntoIterator<Item=RsaSignature>) -> Result<(), AuthError>
{
trace!("{:?} auth resp key <{}>:{}", who, req_id, num);
Ok(())
}
pub async fn auth_key(who: source::IpAddr, state: Arc<State>, 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<State>, req_id: Uuid, passhash: sha256::Sha256Hash) -> Result<(), Infallible>
async fn real_auth_pass(state: Arc<State>, 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<State>, 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<impl warp::Reply, warp::Rejection>
{
use warp::http::StatusCode;
if let Some(this) = err.find::<Self>() {
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<state::AuthCacheError> for AuthError
{
fn from(_: state::AuthCacheError) -> Self
{
Self::Id
}
}

@ -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!()

@ -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<Uuid, (auth::AuthRequest, delay_queue::Key)>,
timeouts: DelayQueue<Uuid>,
}
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<Self::Output> {
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<auth::AuthRequest, AuthCacheError>
{
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<ServerState>,
auth_tokens: RwLock<AuthContainer>,
//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
}
}

Loading…
Cancel
Save