check the password on when the file is downloaded
This commit is contained in:
parent
b1292a2f47
commit
eb92676a47
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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())
|
||||||
|
|
|
@ -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>> {
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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())
|
||||||
|
}
|
|
@ -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())))?;
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
Loading…
Reference in New Issue