diff --git a/filed/Cargo.lock b/filed/Cargo.lock index d72d7eb..88cd89f 100644 --- a/filed/Cargo.lock +++ b/filed/Cargo.lock @@ -58,6 +58,18 @@ version = "1.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" +[[package]] +name = "argon2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17ba4cac0a46bc1d2912652a751c47f2a9f3a7fe89bcae2275d418f5270402f9" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash", +] + [[package]] name = "askama" version = "0.12.0" @@ -131,6 +143,12 @@ version = "0.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ba43ea6f343b788c8764558649e08df62f86c6ef251fdaeb1ffd010a9ae50a2" +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + [[package]] name = "basic-toml" version = "0.1.4" @@ -140,6 +158,15 @@ dependencies = [ "serde", ] +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -280,6 +307,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", + "subtle", ] [[package]] @@ -326,7 +354,9 @@ dependencies = [ name = "filed" version = "0.1.0" dependencies = [ + "argon2", "askama", + "base64", "bytes", "chrono", "css-minify", @@ -337,6 +367,7 @@ dependencies = [ "log", "minify-js", "num", + "rand", "redis", "serde", "serde_json", @@ -893,6 +924,17 @@ dependencies = [ "memchr", ] +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core", + "subtle", +] + [[package]] name = "percent-encoding" version = "2.3.0" @@ -1186,6 +1228,12 @@ dependencies = [ "warp", ] +[[package]] +name = "subtle" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" + [[package]] name = "sval" version = "2.9.1" diff --git a/filed/Cargo.toml b/filed/Cargo.toml index 8924b1b..297b2c8 100644 --- a/filed/Cargo.toml +++ b/filed/Cargo.toml @@ -6,7 +6,9 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +argon2 = "0.5.2" askama = "0.12.0" +base64 = "0.21.4" bytes = "1.5.0" chrono = { version = "0.4.31", features = ["serde"] } dotenvy = "0.15.7" @@ -15,6 +17,7 @@ futures-util = "0.3.28" hex = "0.4.3" log = "0.4.20" num = { version = "0.4.1", features = ["serde"] } +rand = "0.8.5" redis = { version = "0.23.3", features = ["tokio", "tokio-comp"] } serde = { version = "1.0.188", features = ["derive"] } serde_json = "1.0.107" diff --git a/filed/src/env.rs b/filed/src/env.rs index f561384..7bcc11c 100644 --- a/filed/src/env.rs +++ b/filed/src/env.rs @@ -47,7 +47,7 @@ pub fn loadenv() -> Result> { let spath: String = get_var("USERCONTENT_DIR")?; let path = Path::new(&spath); if ! path.exists() { - fs::create_dir_all(path)?; + fs::create_dir_all(path).map_err(|err| format!("Could not create usercontent directory: {err}"))?; } if ! path.is_dir() { return Err(format!("USERCONTENT_DIR is set to \"{}\", which exists but is not a directory!", &spath).into()) diff --git a/filed/src/files/lookup.rs b/filed/src/files/lookup.rs index 1300e7d..c5270b7 100644 --- a/filed/src/files/lookup.rs +++ b/filed/src/files/lookup.rs @@ -33,7 +33,6 @@ impl FileManager { Ok(Some(serde_json::from_str(data.as_str())?)) } pub fn find_by_name(self: &Self, name: String) -> Result, Box> { - println!("{}-name-{}", self.env.redis.prefix, name); Ok(self.find(format!("{}-name-{}", self.env.redis.prefix, name))?) } pub fn find_by_hash(self: &Self, hash: String) -> Result, Box> { diff --git a/filed/src/files/mod.rs b/filed/src/files/mod.rs index a392e06..a2c21bd 100644 --- a/filed/src/files/mod.rs +++ b/filed/src/files/mod.rs @@ -2,6 +2,7 @@ use std::{sync::Arc, error::Error, ops::Add}; +use argon2::{PasswordHash, password_hash::SaltString, Params, PasswordHasher}; use chrono::{DateTime, Local}; use redis::AsyncCommands; use sha2::{Sha512, Digest, digest::FixedOutput}; @@ -20,6 +21,7 @@ pub struct File { pub mime: String, pub delete_at: DateTime, pub delete_mode: DeleteMode, + pub password: Option, // argon2id hash sha512: String } @@ -99,7 +101,7 @@ impl File { Ok(ndata) } - pub async fn create(data: Vec, mime: String, name: Option, env: Env, delete_mode: DeleteMode) -> Result> { + pub async fn create(data: Vec, mime: String, name: Option, env: Env, delete_mode: DeleteMode, password: Option) -> Result> { let mut filename = String::new(); let mut hash = Sha512::new(); @@ -127,7 +129,18 @@ impl File { mime, delete_at: expires, delete_mode, - sha512: hash + sha512: hash, + password: match password { + Some(pass) => { + // todo!("Remove possible panics on this one"); + let argon = crate::security::get_argon2(); + let salt = SaltString::generate(&mut rand::thread_rng()); + let hash = argon.hash_password(pass.bytes().collect::>().as_slice(), &salt).unwrap(); + + Some(hash.serialize().to_string()) + }, + None => None + } } ) } diff --git a/filed/src/main.rs b/filed/src/main.rs index 46e529b..3176c0a 100644 --- a/filed/src/main.rs +++ b/filed/src/main.rs @@ -7,10 +7,12 @@ mod env; mod web; mod db; +pub mod security; + #[tokio::main] async fn main() { dotenvy::dotenv().unwrap(); - let envy = env::loadenv().unwrap(); + let envy = env::loadenv().map_err(|err| format!("Could not load env: {err}")).unwrap(); // set up logging if envy.logging { diff --git a/filed/src/security.rs b/filed/src/security.rs new file mode 100644 index 0000000..481e4c1 --- /dev/null +++ b/filed/src/security.rs @@ -0,0 +1,5 @@ +use argon2::{Argon2, Params}; + +pub fn get_argon2() -> Argon2<'static> { + argon2::Argon2::new(argon2::Algorithm::Argon2id, argon2::Version::V0x13, Params::new(65535, 4, 4, Some(64)).unwrap()) +} \ No newline at end of file diff --git a/filed/src/web/forms.rs b/filed/src/web/forms.rs index 430041d..7a32c43 100644 --- a/filed/src/web/forms.rs +++ b/filed/src/web/forms.rs @@ -63,14 +63,28 @@ pub async fn upload(form: FormData, state: SharedState) -> Result )) } + let check_off = FormElement { data: "off".as_bytes().to_vec(), mime: "text/plain".into() }; + let data = params.get("file").unwrap(); let delmode = params.get("delmode").unwrap(); let named = params.get("named"); let filename = params.get("filename").unwrap(); let tos_check = match params.get("tos_consent") { Some(v) => (*v).clone(), - None => FormElement { data: "off".as_bytes().to_vec(), mime: "text/plain".into() } + None => check_off.clone() }; + + let protected = params.get("passworded").unwrap_or(&check_off.clone()).as_str_or_reject()?; + let protected = protected == "on"; + let password: Option = { + let pass = params.get("password"); + if protected && pass.is_some() { + Some(pass.unwrap().as_str_or_reject()?) + } else { + None + } + }; + let mut is_named = named.is_none(); let tos_check = tos_check.as_str_or_reject()?; if tos_check != "on" { @@ -123,7 +137,8 @@ pub async fn upload(form: FormData, state: SharedState) -> Result } else { DeleteMode::TimeOrDownload } - } + }, + password ).await .map_err(|err| warp::reject::custom(HttpReject::StringError(err.to_string())))?; diff --git a/filed/src/web/uploaded.rs b/filed/src/web/uploaded.rs index f69c5b2..c77d7e1 100644 --- a/filed/src/web/uploaded.rs +++ b/filed/src/web/uploaded.rs @@ -1,10 +1,17 @@ +use argon2::{PasswordVerifier, PasswordHash}; +use base64::{alphabet, engine, Engine}; use warp::{Filter, reply::Reply, reject::Rejection}; use crate::files::DeleteMode; use super::{state::SharedState, rejection::HttpReject}; -pub async fn uploaded((file, state): (String, SharedState)) -> Result, Rejection> { +fn btoa(base: String) -> Result> { + let decoder = engine::GeneralPurpose::new(&alphabet::STANDARD, engine::general_purpose::PAD); + Ok(String::from_utf8(decoder.decode(base)?)?) +} + +pub async fn uploaded((file, state): (String, SharedState), authorization: Option) -> Result, Rejection> { let mut file_res = state.file_mgr.find_by_hash(file.clone()) .map_err(|x| warp::reject::custom(HttpReject::StringError(x.to_string())))?; @@ -20,15 +27,56 @@ pub async fn uploaded((file, state): (String, SharedState)) -> Result(); + let user_pass = btoa(user_pass).unwrap(); + let user_pass = user_pass.split(':').collect::>(); + let user_pass = user_pass.last().unwrap().to_string(); + + log::debug!("User provided a password: \"{}\"", user_pass); + let argon = crate::security::get_argon2(); + let hash = PasswordHash::parse(&pass, argon2::password_hash::Encoding::B64).unwrap(); + + if ! argon.verify_password(user_pass.as_bytes(), &hash).is_ok() { + log::debug!("Password doesn't match"); + return Ok( + Box::new( + warp::reply::with_status( + warp::reply::with_header(warp::reply::html("Invalid password"), "WWW-Authenticate", "basic"), + warp::http::StatusCode::UNAUTHORIZED + ) + ) + ) + } else { + log::debug!("Password match"); + } + } else { + log::debug!("Password not provided"); + return Ok( + Box::new( + warp::reply::with_status( + warp::reply::with_header(warp::reply::html(""), "WWW-Authenticate", "basic realm=\"File is protected with a password. Login field is ignored\""), + warp::http::StatusCode::UNAUTHORIZED + ) + ) + ) + } + } + let data = file_res.read_unchecked().await.unwrap(); match file_res.delete_mode { DeleteMode::Time => { if file_res.expired() { + log::debug!("Deleting the file since it is expired"); let _ = file_res.delete(state.clone()).await; } }, DeleteMode::TimeOrDownload => { + log::debug!("Deleting the file since it is a 1-download file"); let _ = file_res.delete(state.clone()).await; } } @@ -46,5 +94,6 @@ pub async fn uploaded((file, state): (String, SharedState)) -> Result impl Filter + Clone { warp::path!("upload" / String) .map(move |x| (x, state.clone())) + .and(warp::header::optional("Authorization")) .and_then(uploaded) } \ No newline at end of file