diff --git a/Cargo.lock b/Cargo.lock index a91d916..69792b8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,6 +26,17 @@ version = "1.0.68" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2cb2f989d18dd141ab8ae82f64d1a8cdd37e0840f73a406896cf5e99502fab61" +[[package]] +name = "argon2" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db4ce4441f99dbd377ca8a8f57b698c44d0d6e712d8329b5040da5a64aa1ce73" +dependencies = [ + "base64ct", + "blake2", + "password-hash", +] + [[package]] name = "async-trait" version = "0.1.60" @@ -93,12 +104,27 @@ dependencies = [ "tower-service", ] +[[package]] +name = "base64ct" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b645a089122eccb6111b4f81cbc1a49f5900ac4666bb93ac027feaecf15607bf" + [[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[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.3" @@ -290,6 +316,7 @@ checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" dependencies = [ "block-buffer", "crypto-common", + "subtle", ] [[package]] @@ -672,6 +699,17 @@ dependencies = [ "regex", ] +[[package]] +name = "password-hash" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700" +dependencies = [ + "base64ct", + "rand_core", + "subtle", +] + [[package]] name = "percent-encoding" version = "2.2.0" @@ -1009,6 +1047,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "subtle" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" + [[package]] name = "syn" version = "1.0.107" @@ -1279,6 +1323,7 @@ name = "v" version = "0.1.0" dependencies = [ "anyhow", + "argon2", "axum", "dashmap", "lazy_static", diff --git a/Cargo.toml b/Cargo.toml index b4df6ad..66c1463 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" [dependencies] anyhow = "1.0.68" +argon2 = { version = "0.4.1", features = ["std"] } axum = "0.6.1" dashmap = "5.4.0" lazy_static = "1.4.0" diff --git a/src/data.rs b/src/data.rs index 97a83f2..6787af6 100644 --- a/src/data.rs +++ b/src/data.rs @@ -8,6 +8,10 @@ use std::{ #[cfg(target_family = "unix")] use std::os::unix::fs::DirBuilderExt; +use argon2::{ + password_hash::{rand_core::OsRng, SaltString}, + Argon2, PasswordHash, PasswordHasher, PasswordVerifier, +}; use dashmap::DashMap; use lazy_static::lazy_static; @@ -32,6 +36,12 @@ pub enum Error { FS, #[error("id collision wtf")] IDCollision, + #[error("thing exists")] + Exists, + #[error("argon2 error")] + Argon2(#[from] argon2::password_hash::Error), + #[error("bad password")] + BadPassword, } fn meta_str_to_map(from: &str) -> Result, Error> { @@ -49,6 +59,15 @@ fn meta_str_to_map(from: &str) -> Result, Error> { Ok(map) } +fn hash_password(password: String) -> Result { + let salt = SaltString::generate(&mut OsRng); + let argon2 = Argon2::default(); + let hash = argon2 + .hash_password(&password.into_bytes(), &salt)? + .to_string(); + Ok(hash) +} + struct TextPaste { pub text: String, pub meta: TextMeta, @@ -110,6 +129,38 @@ impl UserData { is_admin, }) } + + pub fn new(username: String, password: String, is_admin: bool) -> Result { + let pw_hash = hash_password(password)?; + Ok(Self { + username, + pw_hash, + is_admin, + }) + } + + pub fn set_password(&mut self, password: String) -> Result<(), Error> { + self.pw_hash = hash_password(password)?; + Ok(()) + } + + pub fn set_password_checked(&mut self, old: String, new: String) -> Result<(), Error> { + self.validate_password(old)?; + self.set_password(new); + Ok(()) + } + + pub fn validate_password(&self, password: String) -> Result<(), Error> { + let parsed_hash = PasswordHash::new(&self.pw_hash)?; + Argon2::default().verify_password(&password.into_bytes(), &parsed_hash)?; + Ok(()) + } +} + +impl std::fmt::Display for UserData { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}: {}", self.username, self.pw_hash) + } } pub struct DataStorage { @@ -225,6 +276,47 @@ impl DataStorage { Ok(()) } + pub fn create_user( + &mut self, + username: String, + password: String, + is_admin: bool, + ) -> Result<(), Error> { + if self.users.contains_key(&username) { + return Err(Error::Exists); + } + + self.users + .insert(username, UserData::new(username, password, is_admin)?); + + self.write_users()?; + Ok(()) + } + + pub fn user_change_password( + &mut self, + username: String, + old_password: String, + new_password: String, + ) -> Result<(), Error> { + let mut user = *self + .users + .get_mut(&username) + .ok_or(Error::ShitMissing(username))?; + user.set_password_checked(old_password, new_password)?; + + self.write_users()?; + Ok(()) + } + + pub fn delete_user(&mut self, username: String) -> Result<(), Error> { + self.users + .remove(&username) + .ok_or(Error::ShitMissing(username))?; + self.write_users()?; + Ok(()) + } + fn ensure_folder_structure(&self) -> Result<(), Error> { let dir_builder = DirBuilder::new().recursive(true); if cfg!(target_family = "unix") { @@ -244,14 +336,11 @@ impl DataStorage { .read(true) .open(self.base_dir.join(*REDIRECTS_FILE))?; - // TODO: replace with meta_str_to_map and then self.redirects.extend - for line in io::BufReader::new(f).lines() { line?; let (id, url) = line?.split_once(": ").ok_or(Error::Metadata)?; self.redirects .insert(id.trim().to_string(), url.trim().to_string()); - // if there's a better way to do this let me know } Ok(()) @@ -329,6 +418,21 @@ impl DataStorage { Ok(()) } + fn write_users(&self) -> Result<(), Error> { + let fuckery = String::from("---"); + self.users.into_iter().map(|(_, user)| { + if user.is_admin { + fuckery = format!("{}\n{}", user.to_string(), fuckery) + } else { + fuckery += format!("\n{}", user.to_string()).as_str() + } + }); + + std::fs::write(self.base_dir.join(*USERS_FILE), fuckery)?; + + Ok(()) + } + fn write_text(&self, id: String) -> Result<(), Error> { let text_paste = self.text.get(&id).ok_or(Error::ShitMissing(id))?; let fuckery = text_paste.meta.to_string();