Hash admin passwords

This commit is contained in:
Pascal Engélibert 2022-10-15 17:32:35 +02:00
parent 980a85d41b
commit 86495543ce
Signed by: tuxmain
GPG key ID: 3504BC6D362F7DCA
7 changed files with 106 additions and 7 deletions

51
Cargo.lock generated
View file

@ -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",

View file

@ -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"

View file

@ -53,6 +53,9 @@ pub enum MainSubcommand {
/// Start server
Start(StartOpt),
/// Add admin password
Psw,
}
#[derive(Clone, Debug, Parser)]

View file

@ -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");
}

View file

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

View file

@ -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<tide::Response> {
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::<AdminQuery>().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::<AdminQuery>().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<String> {
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)
}

View file

@ -5,6 +5,7 @@
<title>Comments</title>
</head>
<body>
{% if comments_pending %}
<div id="comments_pending">
{% for comment in comments_pending %}
<div class="comment{% if comment.needs_approval %} comment_pending{% endif %}" id="comment-{{ comment.id }}">
@ -20,6 +21,7 @@
</div>
{% endfor %}
</div>
{% endif %}
<div id="comments">
{% for comment in comments %}
<div class="comment{% if comment.needs_approval %} comment_pending{% endif %}" id="comment-{{ comment.id }}">