diff --git a/Cargo.lock b/Cargo.lock index b34e3b1..a0addab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -80,6 +80,17 @@ version = "1.0.65" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98161a4e3e2184da77bb14f02184cdd111e83bbbcc9979dfee3c44b9a85f5602" +[[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-attributes" version = "1.1.2" @@ -286,6 +297,12 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" +[[package]] +name = "base64ct" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2b2456fd614d856680dcd9fcc660a51a820fa09daef2e49772b56a193c8474" + [[package]] name = "bincode" version = "1.3.3" @@ -301,6 +318,15 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "blake2" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9cf849ee05b2ee5fba5e36f97ff8ec2533916700fc0758d40d92136a42f3388" +dependencies = [ + "digest 0.10.5", +] + [[package]] name = "block-buffer" version = "0.9.0" @@ -666,6 +692,7 @@ checksum = "adfbc57365a37acbd2ebf2b64d7e69bb766e2fea813521ed536f5d0520dcf86c" dependencies = [ "block-buffer 0.10.3", "crypto-common", + "subtle", ] [[package]] @@ -1241,6 +1268,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 0.6.4", + "subtle", +] + [[package]] name = "percent-encoding" version = "2.2.0" @@ -1567,6 +1605,16 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56770675ebc04927ded3e60633437841581c285dc6236109ea25fbf3beb7b59e" +[[package]] +name = "rpassword" +version = "7.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b763cb66df1c928432cc35053f8bd4cec3335d8559fc16010017d16b3c1680" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "rustc_version" version = "0.2.3" @@ -2310,12 +2358,15 @@ dependencies = [ name = "webcomment" version = "0.1.0" dependencies = [ + "argon2", "async-std", "base64", "clap", "directories", "log", "rand 0.8.5", + "rand_core 0.6.4", + "rpassword", "serde", "serde_json", "sha2 0.10.6", diff --git a/Cargo.toml b/Cargo.toml index 96b704e..1b146a4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ description = "Templatable comment web server" edition = "2021" [dependencies] -#argon2 = "0.4.1" +argon2 = "0.4.1" async-std = { version = "1.12.0", features = ["attributes"] } base64 = "0.13.0" clap = { version = "4.0.15", default-features = false, features = ["derive", "std"] } @@ -16,6 +16,8 @@ directories = "4.0.1" log = "0.4.17" #matrix-sdk = { version = "0.5.0", default-features = false } rand = "0.8.5" +rand_core = { version = "0.6.4", features = ["std"] } +rpassword = "7.0.0" serde = { version = "1.0.145", features = ["derive", "rc"] } serde_json = "1.0.86" sha2 = "0.10.6" diff --git a/src/cli.rs b/src/cli.rs index 7cc8795..5edfae6 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -53,6 +53,9 @@ pub enum MainSubcommand { /// Start server Start(StartOpt), + + /// Add admin password + Psw, } #[derive(Clone, Debug, Parser)] diff --git a/src/config.rs b/src/config.rs index 814b2b3..1b1a80e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -110,3 +110,9 @@ pub fn read_config(dir: &Path) -> Config { .expect("Bad JSON in config file") } } + +pub fn write_config(dir: &Path, config: &Config) { + let path = dir.join(CONFIG_FILE); + std::fs::write(path, toml_edit::easy::to_string_pretty(&config).unwrap()) + .expect("Cannot write config file"); +} diff --git a/src/main.rs b/src/main.rs index 3652f91..b54d8c0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,6 +6,10 @@ mod queries; mod server; mod templates; +use argon2::{ + password_hash::{rand_core::OsRng, PasswordHasher, SaltString}, + Argon2, +}; use clap::Parser; #[async_std::main] @@ -20,6 +24,18 @@ async fn main() { let (config, dbs, templates) = init_all(opt.opt, subopt); server::start_server(config, dbs, templates).await } + cli::MainSubcommand::Psw => { + let mut config = config::read_config(&opt.opt.dir.0); + let password = rpassword::prompt_password("Additional admin password: ").unwrap(); + let salt = SaltString::generate(&mut OsRng); + let argon2 = Argon2::default(); + let password_hash = argon2 + .hash_password(password.as_bytes(), &salt) + .unwrap() + .to_string(); + config.admin_passwords.push(password_hash); + config::write_config(&opt.opt.dir.0, &config); + } } } diff --git a/src/server.rs b/src/server.rs index 47fa72a..570f2d1 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,5 +1,6 @@ use crate::{config::*, db::*, helpers, queries::*, templates::*}; +use argon2::{Argon2, PasswordHash, PasswordVerifier}; use log::error; use std::sync::Arc; use tera::Context; @@ -54,7 +55,7 @@ async fn serve_comments<'a>( }; let admin = req.cookie("admin").map_or(false, |psw| { - config.admin_passwords.contains(&String::from(psw.value())) + check_admin_password_hash(&config, &String::from(psw.value())) }); let topic_hash = TopicHash::from_topic(topic); @@ -127,9 +128,9 @@ async fn serve_admin<'a>( &dbs.comment_pending .iter() .filter_map(|entry| { - let ((_topic_hash, _time, comment_id), ()) = dbg!(entry + let ((_topic_hash, _time, comment_id), ()) = entry .map_err(|e| error!("Reading comment_pending: {:?}", e)) - .ok()?); + .ok()?; let comment = dbs .comment .get(&comment_id) @@ -227,7 +228,7 @@ async fn handle_post_admin( dbs: Dbs, ) -> tide::Result { if let Some(psw) = req.cookie("admin") { - if config.admin_passwords.contains(&String::from(psw.value())) { + if check_admin_password(&config, &String::from(psw.value())).is_some() { match req.body_form::().await? { _ => serve_admin(req, config, templates, dbs).await, } @@ -235,11 +236,11 @@ async fn handle_post_admin( serve_admin_login(req, config, templates).await } } else if let AdminQuery::Login(query) = req.body_form::().await? { - if config.admin_passwords.contains(&query.psw) { + if let Some(password_hash) = check_admin_password(&config, &query.psw) { serve_admin(req, config.clone(), templates, dbs) .await .map(|mut r| { - let mut cookie = tide::http::Cookie::new("admin", query.psw); + let mut cookie = tide::http::Cookie::new("admin", password_hash); cookie.set_http_only(Some(true)); cookie.set_path(config.root_url.clone()); if let Some(domain) = &config.cookies_domain { @@ -258,3 +259,21 @@ async fn handle_post_admin( serve_admin_login(req, config, templates).await } } + +fn check_admin_password(config: &Config, password: &str) -> Option { + let argon2 = Argon2::default(); + config + .admin_passwords + .iter() + .filter_map(|admin_password| PasswordHash::new(admin_password).ok()) + .find(|admin_password| { + argon2 + .verify_password(password.as_bytes(), admin_password) + .is_ok() + }) + .map(|password_hash| password_hash.to_string()) +} + +fn check_admin_password_hash(config: &Config, password_hash: &str) -> bool { + config.admin_passwords.iter().any(|h| h == password_hash) +} diff --git a/templates/comments.html b/templates/comments.html index 8325c05..be86efe 100644 --- a/templates/comments.html +++ b/templates/comments.html @@ -5,6 +5,7 @@ Comments + {% if comments_pending %}
{% for comment in comments_pending %}
@@ -20,6 +21,7 @@
{% endfor %}
+ {% endif %}
{% for comment in comments %}