From 305c1bd01179d1f56be85678a6261c9ac1a84b24 Mon Sep 17 00:00:00 2001 From: Daniel Flanagan Date: Mon, 20 May 2024 16:03:37 -0500 Subject: [PATCH] Now that I've rolled my own crappy auth --- Cargo.lock | 157 ++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 2 + src/db.rs | 48 +++++++++++--- src/router.rs | 25 ++++++- src/service/auth.rs | 57 ++++++++++++---- src/user.rs | 23 +++++-- 6 files changed, 283 insertions(+), 29 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6403a2f..f1e39c7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,6 +26,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anstream" version = "0.6.14" @@ -205,6 +220,15 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -238,6 +262,12 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + [[package]] name = "byteorder" version = "1.5.0" @@ -262,6 +292,21 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-targets 0.52.5", +] + [[package]] name = "clap" version = "4.5.4" @@ -384,6 +429,12 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "core-foundation-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + [[package]] name = "cpufeatures" version = "0.2.12" @@ -766,6 +817,29 @@ dependencies = [ "tokio", ] +[[package]] +name = "iana-time-zone" +version = "0.1.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "indenter" version = "0.3.3" @@ -823,6 +897,15 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +[[package]] +name = "js-sys" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +dependencies = [ + "wasm-bindgen", +] + [[package]] name = "json5" version = "0.4.1" @@ -894,6 +977,8 @@ version = "0.1.0" dependencies = [ "argon2", "axum", + "bincode", + "chrono", "clap", "color-eyre", "config", @@ -1039,6 +1124,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "num_cpus" version = "1.16.0" @@ -1918,6 +2012,60 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasm-bindgen" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" + [[package]] name = "winapi" version = "0.3.9" @@ -1949,6 +2097,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.5", +] + [[package]] name = "windows-sys" version = "0.48.0" diff --git a/Cargo.toml b/Cargo.toml index d96cf2a..0145fe5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,8 @@ panic = "abort" [dependencies] argon2 = { version = "0.5.3", features = ["std"] } +bincode = "1.3.3" +chrono = { version = "0.4.38", features = ["serde"] } clap = { version = "4.5.4", features = ["derive", "env"] } color-eyre = "0.6.3" config = "0.14.0" diff --git a/src/db.rs b/src/db.rs index 723fe8c..58aa1ff 100644 --- a/src/db.rs +++ b/src/db.rs @@ -1,3 +1,4 @@ +use serde::de::DeserializeOwned; use sled::{Db, IVec}; use thiserror::Error; @@ -10,6 +11,9 @@ pub struct Data { pub enum Error { #[error("sled error: {0}")] Sled(#[from] sled::Error), + + #[error("bincode error: {0}")] + Binccode(#[from] Box), } impl Data { @@ -19,16 +23,15 @@ impl Data { }) } - pub fn get, V: From>( - &self, - tree_name: &str, - key: K, - ) -> Result, Error> { - Ok(self - .db - .open_tree(tree_name)? - .get(key.as_ref())? - .map(V::from)) + pub fn get(&self, tree_name: &str, key: K) -> Result, Error> + where + K: AsRef<[u8]>, + V: DeserializeOwned, + { + match self.db.open_tree(tree_name)?.get(key.as_ref())? { + Some(v) => Ok(Some(bincode::deserialize::(&v)?)), + None => Ok(None), + } } pub fn insert, V: Into>( @@ -39,4 +42,29 @@ impl Data { ) -> Result, Error> { Ok(self.db.open_tree(tree_name)?.insert(key, value.into())?) } + + pub fn all<'de, K, V>( + &self, + tree_name: &str, + ) -> Result>, Error> + where + V: DeserializeOwned, + K: From, + { + Ok(self + .db + .open_tree(tree_name)? + .scan_prefix([]) + .map(|r| match r { + Ok((k, v)) => { + let key = K::from(k); + match bincode::deserialize::(&v).map_err(Error::from) { + Ok(v) => Ok((key, v)), + Err(err) => Err(Error::from(err)), + } + } + Err(err) => Err(Error::from(err)), + }) + .into_iter()) + } } diff --git a/src/router.rs b/src/router.rs index 2c0cc71..5092fe6 100644 --- a/src/router.rs +++ b/src/router.rs @@ -1,10 +1,13 @@ use crate::partials::page; +use crate::user::User; +use crate::{db, user}; use crate::{ file_watcher::FileWatcher, prelude::*, service::{auth, static_files}, state::State as AppState, }; +use axum::extract::State; use axum::{ http::StatusCode, response::{Html, IntoResponse}, @@ -12,6 +15,7 @@ use axum::{ Router, }; use maud::html; +use sled::IVec; use thiserror::Error; use tower_http::trace::TraceLayer; use tower_livereload::LiveReloadLayer; @@ -26,6 +30,15 @@ pub enum NewRouterError { pub enum ReqError { #[error("argon2 error: {0}")] Argon2(#[from] argon2::password_hash::Error), + + #[error("bincode error: {0}")] + Bincode(#[from] bincode::Error), + + #[error("database error: {0}")] + Database(#[from] db::Error), + + #[error("user error: {0}")] + User(#[from] user::Error), } impl IntoResponse for ReqError { @@ -61,11 +74,12 @@ pub async fn router( }; let (static_file_service, static_file_watcher) = static_files::router(orl())?; - let auth_service = auth::router().unwrap(); + let auth_service = auth::router(state.clone()).unwrap(); let mut result = Router::new() .route("/", get(index)) .route("/about", get(about)) + .route("/users", get(users)) .nest_service("/auth", auth_service) .nest_service("/static", static_file_service) .layer(TraceLayer::new_for_http()) @@ -87,3 +101,12 @@ async fn index() -> ReqResult> { async fn about() -> ReqResult> { page("index", html! { "About" }) } + +async fn users(State(state): State) -> ReqResult { + let mut s = String::new(); + let mut users = state.db.all::(User::tree())?; + while let Some(Ok((_, user))) = users.next() { + s.push_str(&format!("{}: {:?}", user.username, user.registered_at)) + } + Ok(s) +} diff --git a/src/service/auth.rs b/src/service/auth.rs index aa0ecd8..709e803 100644 --- a/src/service/auth.rs +++ b/src/service/auth.rs @@ -14,7 +14,7 @@ use axum::{ Router, }; -use crate::prelude::*; +use crate::{prelude::*, user}; pub fn router(state: AppState) -> Result { Ok(Router::new() @@ -37,7 +37,7 @@ async fn login() -> ReqResult> { let subaction = html! { small class="mt-4" { "Need an account? " - a href="/register" {"Get one"} + a href="/auth/register" {"Get one"} "." } }; @@ -56,7 +56,7 @@ async fn register() -> ReqResult> { let subaction = html! { small class="mt-4" { "Already have an account? " - a href="/login" {"Login"} + a href="/auth/login" {"Login"} "." } }; @@ -69,15 +69,34 @@ struct Creds { password: Secret, } -#[instrument] -async fn authenticate(Form(creds): Form) -> ReqResult> { - info!("login attempt"); - Ok(Html( - html! { - "no" - } - .into_string(), - )) +#[instrument(skip(state))] +async fn authenticate( + State(state): State, + Form(creds): Form, +) -> ReqResult> { + let existing_user: Option = state.db.get(User::tree(), &creds.username)?; + + if existing_user.is_none() { + // timing/enumeration attacks or something + return Err(user::Error::UsernameNotFound(Box::new(creds.username)).into()); + } + + let existing_user = existing_user.unwrap(); + if let Err(err) = existing_user.verify(creds.password.expose_secret()) { + Ok(Html( + html! { + "failed to login: " ({err}) + } + .into_string(), + )) + } else { + Ok(Html( + html! { + "logged in" + } + .into_string(), + )) + } } #[instrument(skip(state))] @@ -86,9 +105,21 @@ async fn create_user( Form(creds): Form, ) -> ReqResult> { let user = User::try_new(&creds.username, creds.password.expose_secret())?; + let existing_user: Option = state.db.get(User::tree(), &creds.username)?; + + // TODO: fail2ban? + if existing_user.is_some() { + // timing/enumeration attacks or something + return Err(user::Error::UsernameExists(Box::new(creds.username)).into()); + } + + state + .db + .insert(User::tree(), &user.username, bincode::serialize(&user)?)?; + Ok(Html( html! { - ({user.username}) " has been registered" + ({&user.username}) " has been registered" } .into_string(), )) diff --git a/src/user.rs b/src/user.rs index 5215674..f2011ab 100644 --- a/src/user.rs +++ b/src/user.rs @@ -1,14 +1,19 @@ use crate::prelude::*; -use redact::Secret; -use serde::Deserialize; +use chrono::Utc; +use redact::{expose_secret, Secret}; +use serde::{Deserialize, Serialize}; use thiserror::Error; pub const USER_TREE: &str = "user"; -#[derive(Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug)] pub struct User { pub username: String, - password_digest: Secret, + + #[serde(serialize_with = "expose_secret")] + password_digest: Secret>, + + pub registered_at: chrono::DateTime, } #[derive(Error, Debug)] @@ -28,7 +33,8 @@ impl User { pub fn try_new(username: &str, password: &str) -> Result { Ok(Self { username: username.to_owned(), - password_digest: Secret::new(crate::auth::password_digest(password)?.to_string()), + registered_at: Utc::now(), + password_digest: Secret::new(crate::auth::password_digest(password)?.into()), }) } @@ -36,3 +42,10 @@ impl User { crate::auth::verified_password(password, self.password_digest.expose_secret()) } } + +impl TryFrom for User { + type Error = bincode::Error; + fn try_from(value: sled::IVec) -> Result { + bincode::deserialize(&value) + } +}