check the password on when the file is downloaded

This commit is contained in:
blek 2023-10-15 17:16:08 +10:00
parent b1292a2f47
commit eb92676a47
Signed by: blek
GPG Key ID: 14546221E3595D0C
9 changed files with 142 additions and 8 deletions

48
filed/Cargo.lock generated
View File

@ -58,6 +58,18 @@ version = "1.0.75"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" 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]] [[package]]
name = "askama" name = "askama"
version = "0.12.0" version = "0.12.0"
@ -131,6 +143,12 @@ version = "0.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ba43ea6f343b788c8764558649e08df62f86c6ef251fdaeb1ffd010a9ae50a2" checksum = "9ba43ea6f343b788c8764558649e08df62f86c6ef251fdaeb1ffd010a9ae50a2"
[[package]]
name = "base64ct"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
[[package]] [[package]]
name = "basic-toml" name = "basic-toml"
version = "0.1.4" version = "0.1.4"
@ -140,6 +158,15 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "blake2"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe"
dependencies = [
"digest",
]
[[package]] [[package]]
name = "block-buffer" name = "block-buffer"
version = "0.10.4" version = "0.10.4"
@ -280,6 +307,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [ dependencies = [
"block-buffer", "block-buffer",
"crypto-common", "crypto-common",
"subtle",
] ]
[[package]] [[package]]
@ -326,7 +354,9 @@ dependencies = [
name = "filed" name = "filed"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"argon2",
"askama", "askama",
"base64",
"bytes", "bytes",
"chrono", "chrono",
"css-minify", "css-minify",
@ -337,6 +367,7 @@ dependencies = [
"log", "log",
"minify-js", "minify-js",
"num", "num",
"rand",
"redis", "redis",
"serde", "serde",
"serde_json", "serde_json",
@ -893,6 +924,17 @@ dependencies = [
"memchr", "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]] [[package]]
name = "percent-encoding" name = "percent-encoding"
version = "2.3.0" version = "2.3.0"
@ -1186,6 +1228,12 @@ dependencies = [
"warp", "warp",
] ]
[[package]]
name = "subtle"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc"
[[package]] [[package]]
name = "sval" name = "sval"
version = "2.9.1" version = "2.9.1"

View File

@ -6,7 +6,9 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
argon2 = "0.5.2"
askama = "0.12.0" askama = "0.12.0"
base64 = "0.21.4"
bytes = "1.5.0" bytes = "1.5.0"
chrono = { version = "0.4.31", features = ["serde"] } chrono = { version = "0.4.31", features = ["serde"] }
dotenvy = "0.15.7" dotenvy = "0.15.7"
@ -15,6 +17,7 @@ futures-util = "0.3.28"
hex = "0.4.3" hex = "0.4.3"
log = "0.4.20" log = "0.4.20"
num = { version = "0.4.1", features = ["serde"] } num = { version = "0.4.1", features = ["serde"] }
rand = "0.8.5"
redis = { version = "0.23.3", features = ["tokio", "tokio-comp"] } redis = { version = "0.23.3", features = ["tokio", "tokio-comp"] }
serde = { version = "1.0.188", features = ["derive"] } serde = { version = "1.0.188", features = ["derive"] }
serde_json = "1.0.107" serde_json = "1.0.107"

View File

@ -47,7 +47,7 @@ pub fn loadenv() -> Result<Env, Box<dyn std::error::Error>> {
let spath: String = get_var("USERCONTENT_DIR")?; let spath: String = get_var("USERCONTENT_DIR")?;
let path = Path::new(&spath); let path = Path::new(&spath);
if ! path.exists() { 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() { if ! path.is_dir() {
return Err(format!("USERCONTENT_DIR is set to \"{}\", which exists but is not a directory!", &spath).into()) return Err(format!("USERCONTENT_DIR is set to \"{}\", which exists but is not a directory!", &spath).into())

View File

@ -33,7 +33,6 @@ impl FileManager {
Ok(Some(serde_json::from_str(data.as_str())?)) Ok(Some(serde_json::from_str(data.as_str())?))
} }
pub fn find_by_name(self: &Self, name: String) -> Result<Option<File>, Box<dyn Error>> { pub fn find_by_name(self: &Self, name: String) -> Result<Option<File>, Box<dyn Error>> {
println!("{}-name-{}", self.env.redis.prefix, name);
Ok(self.find(format!("{}-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<Option<File>, Box<dyn Error>> { pub fn find_by_hash(self: &Self, hash: String) -> Result<Option<File>, Box<dyn Error>> {

View File

@ -2,6 +2,7 @@
use std::{sync::Arc, error::Error, ops::Add}; use std::{sync::Arc, error::Error, ops::Add};
use argon2::{PasswordHash, password_hash::SaltString, Params, PasswordHasher};
use chrono::{DateTime, Local}; use chrono::{DateTime, Local};
use redis::AsyncCommands; use redis::AsyncCommands;
use sha2::{Sha512, Digest, digest::FixedOutput}; use sha2::{Sha512, Digest, digest::FixedOutput};
@ -20,6 +21,7 @@ pub struct File {
pub mime: String, pub mime: String,
pub delete_at: DateTime<Local>, pub delete_at: DateTime<Local>,
pub delete_mode: DeleteMode, pub delete_mode: DeleteMode,
pub password: Option<String>, // argon2id hash
sha512: String sha512: String
} }
@ -99,7 +101,7 @@ impl File {
Ok(ndata) Ok(ndata)
} }
pub async fn create(data: Vec<u8>, mime: String, name: Option<String>, env: Env, delete_mode: DeleteMode) -> Result<File, Box<dyn Error>> { pub async fn create(data: Vec<u8>, mime: String, name: Option<String>, env: Env, delete_mode: DeleteMode, password: Option<String>) -> Result<File, Box<dyn Error>> {
let mut filename = String::new(); let mut filename = String::new();
let mut hash = Sha512::new(); let mut hash = Sha512::new();
@ -127,7 +129,18 @@ impl File {
mime, mime,
delete_at: expires, delete_at: expires,
delete_mode, 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::<Vec<u8>>().as_slice(), &salt).unwrap();
Some(hash.serialize().to_string())
},
None => None
}
} }
) )
} }

View File

@ -7,10 +7,12 @@ mod env;
mod web; mod web;
mod db; mod db;
pub mod security;
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
dotenvy::dotenv().unwrap(); 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 // set up logging
if envy.logging { if envy.logging {

5
filed/src/security.rs Normal file
View File

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

View File

@ -63,14 +63,28 @@ pub async fn upload(form: FormData, state: SharedState) -> Result<Box<dyn Reply>
)) ))
} }
let check_off = FormElement { data: "off".as_bytes().to_vec(), mime: "text/plain".into() };
let data = params.get("file").unwrap(); let data = params.get("file").unwrap();
let delmode = params.get("delmode").unwrap(); let delmode = params.get("delmode").unwrap();
let named = params.get("named"); let named = params.get("named");
let filename = params.get("filename").unwrap(); let filename = params.get("filename").unwrap();
let tos_check = match params.get("tos_consent") { let tos_check = match params.get("tos_consent") {
Some(v) => (*v).clone(), 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<String> = {
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 mut is_named = named.is_none();
let tos_check = tos_check.as_str_or_reject()?; let tos_check = tos_check.as_str_or_reject()?;
if tos_check != "on" { if tos_check != "on" {
@ -123,7 +137,8 @@ pub async fn upload(form: FormData, state: SharedState) -> Result<Box<dyn Reply>
} else { } else {
DeleteMode::TimeOrDownload DeleteMode::TimeOrDownload
} }
} },
password
).await ).await
.map_err(|err| warp::reject::custom(HttpReject::StringError(err.to_string())))?; .map_err(|err| warp::reject::custom(HttpReject::StringError(err.to_string())))?;

View File

@ -1,10 +1,17 @@
use argon2::{PasswordVerifier, PasswordHash};
use base64::{alphabet, engine, Engine};
use warp::{Filter, reply::Reply, reject::Rejection}; use warp::{Filter, reply::Reply, reject::Rejection};
use crate::files::DeleteMode; use crate::files::DeleteMode;
use super::{state::SharedState, rejection::HttpReject}; use super::{state::SharedState, rejection::HttpReject};
pub async fn uploaded((file, state): (String, SharedState)) -> Result<Box<dyn Reply>, Rejection> { fn btoa(base: String) -> Result<String, Box<dyn std::error::Error>> {
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<String>) -> Result<Box<dyn Reply>, Rejection> {
let mut file_res = state.file_mgr.find_by_hash(file.clone()) let mut file_res = state.file_mgr.find_by_hash(file.clone())
.map_err(|x| warp::reject::custom(HttpReject::StringError(x.to_string())))?; .map_err(|x| warp::reject::custom(HttpReject::StringError(x.to_string())))?;
@ -20,15 +27,56 @@ pub async fn uploaded((file, state): (String, SharedState)) -> Result<Box<dyn Re
) )
} }
let file_res = file_res.unwrap(); let file_res = file_res.unwrap();
if let Some(pass) = file_res.clone().password {
log::debug!("File is protected by a password");
if let Some(user_pass) = authorization {
let user_pass = user_pass.chars().skip(6).collect::<String>();
let user_pass = btoa(user_pass).unwrap();
let user_pass = user_pass.split(':').collect::<Vec<&str>>();
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(); let data = file_res.read_unchecked().await.unwrap();
match file_res.delete_mode { match file_res.delete_mode {
DeleteMode::Time => { DeleteMode::Time => {
if file_res.expired() { if file_res.expired() {
log::debug!("Deleting the file since it is expired");
let _ = file_res.delete(state.clone()).await; let _ = file_res.delete(state.clone()).await;
} }
}, },
DeleteMode::TimeOrDownload => { DeleteMode::TimeOrDownload => {
log::debug!("Deleting the file since it is a 1-download file");
let _ = file_res.delete(state.clone()).await; let _ = file_res.delete(state.clone()).await;
} }
} }
@ -46,5 +94,6 @@ pub async fn uploaded((file, state): (String, SharedState)) -> Result<Box<dyn Re
pub fn get_uploaded(state: SharedState) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone { pub fn get_uploaded(state: SharedState) -> impl Filter<Extract = impl Reply, Error = Rejection> + Clone {
warp::path!("upload" / String) warp::path!("upload" / String)
.map(move |x| (x, state.clone())) .map(move |x| (x, state.clone()))
.and(warp::header::optional("Authorization"))
.and_then(uploaded) .and_then(uploaded)
} }