From 0f2db05a075414145822ba316bfd4432abc4bc55 Mon Sep 17 00:00:00 2001 From: Lennart <18233294+lennart-k@users.noreply.github.com> Date: Sun, 3 Nov 2024 13:17:38 +0100 Subject: [PATCH] Add some CLI commands to generate a default configuration and password hashes --- Cargo.lock | 26 ++++++ Cargo.toml | 23 ++++-- crates/frontend/src/config.rs | 2 +- crates/store/src/auth/static_user_store.rs | 4 +- src/commands/mod.rs | 94 ++++++++++++++++++++++ src/main.rs | 55 ++++++++----- 6 files changed, 175 insertions(+), 29 deletions(-) create mode 100644 src/commands/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 4e37216..6c53225 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2393,6 +2393,17 @@ version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" +[[package]] +name = "rpassword" +version = "7.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80472be3c897911d0137b2d2b9055faf6eeac5b14e324073d83bc17b191d7e3f" +dependencies = [ + "libc", + "rtoolbox", + "windows-sys 0.48.0", +] + [[package]] name = "rsa" version = "0.9.6" @@ -2454,6 +2465,16 @@ dependencies = [ "syn", ] +[[package]] +name = "rtoolbox" +version = "0.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c247d24e63230cdb56463ae328478bd5eac8b8faa8c69461a77e8e323afac90e" +dependencies = [ + "libc", + "windows-sys 0.48.0", +] + [[package]] name = "rust-embed" version = "8.5.0" @@ -2509,6 +2530,7 @@ version = "0.1.0" dependencies = [ "actix-web", "anyhow", + "argon2", "async-trait", "clap", "console-subscriber", @@ -2517,6 +2539,10 @@ dependencies = [ "opentelemetry-otlp", "opentelemetry-semantic-conventions", "opentelemetry_sdk", + "password-hash", + "pbkdf2", + "rand", + "rpassword", "rustical_caldav", "rustical_carddav", "rustical_frontend", diff --git a/Cargo.toml b/Cargo.toml index 9e8060a..66cedc3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -81,6 +81,10 @@ rustical_caldav = { path = "./crates/caldav/" } rustical_carddav = { path = "./crates/carddav/" } rustical_frontend = { path = "./crates/frontend/" } chrono-tz = "0.10.0" +rand = "0.8" +argon2 = "0.5" +rpassword = "7.3" +password-hash = { version = "0.5" } [dependencies] rustical_store = { workspace = true } @@ -100,16 +104,21 @@ sqlx = { workspace = true } async-trait = { workspace = true } tracing-actix-web = { workspace = true } -# 0.25 is the latest version supported by tracing-opentelemetry -opentelemetry = "0.26.0" -opentelemetry-otlp = "0.26.0" -opentelemetry_sdk = { version = "0.26.0", features = ["rt-tokio"] } +opentelemetry = "0.26" +opentelemetry-otlp = "0.26" +opentelemetry_sdk = { version = "0.26", features = ["rt-tokio"] } opentelemetry-semantic-conventions = "0.26" -tracing-opentelemetry = "0.27.0" -tracing-subscriber = { version = "0.3.18", features = [ +tracing-opentelemetry = "0.27" +tracing-subscriber = { version = "0.3", features = [ "env-filter", "fmt", "registry", ] } -console-subscriber = "0.4.0" +console-subscriber = "0.4" + +rand.workspace = true +rpassword.workspace = true +argon2.workspace = true +pbkdf2.workspace = true +password-hash.workspace = true diff --git a/crates/frontend/src/config.rs b/crates/frontend/src/config.rs index 3d13679..9f8f6c3 100644 --- a/crates/frontend/src/config.rs +++ b/crates/frontend/src/config.rs @@ -5,5 +5,5 @@ use serde::{Deserialize, Serialize}; pub struct FrontendConfig { #[serde(serialize_with = "hex::serde::serialize")] #[serde(deserialize_with = "hex::serde::deserialize")] - pub secret_key: Vec, + pub secret_key: [u8; 64], } diff --git a/crates/store/src/auth/static_user_store.rs b/crates/store/src/auth/static_user_store.rs index 01827ad..699c0f7 100644 --- a/crates/store/src/auth/static_user_store.rs +++ b/crates/store/src/auth/static_user_store.rs @@ -5,9 +5,9 @@ use std::collections::HashMap; use super::AuthenticationProvider; -#[derive(Debug, Clone, Deserialize, Serialize)] +#[derive(Debug, Clone, Deserialize, Serialize, Default)] pub struct StaticUserStoreConfig { - users: Vec, + pub users: Vec, } #[derive(Debug, Clone, Deserialize, Serialize)] diff --git a/src/commands/mod.rs b/src/commands/mod.rs new file mode 100644 index 0000000..e197a2a --- /dev/null +++ b/src/commands/mod.rs @@ -0,0 +1,94 @@ +use argon2::password_hash::SaltString; +use clap::{Parser, ValueEnum}; +use password_hash::PasswordHasher; +use pbkdf2::Params; +use rand::{rngs::OsRng, RngCore}; +use rustical_frontend::FrontendConfig; +use rustical_store::auth::{static_user_store::UserEntry, StaticUserStoreConfig, User}; + +use crate::config::{ + AuthConfig, Config, DataStoreConfig, HttpConfig, SqliteDataStoreConfig, TracingConfig, +}; + +#[derive(Debug, Parser)] +pub struct GenConfigArgs {} + +fn generate_frontend_secret() -> [u8; 64] { + let mut rng = rand::thread_rng(); + + let mut secret = [0u8; 64]; + rng.fill_bytes(&mut secret); + secret +} + +pub fn cmd_gen_config(_args: GenConfigArgs) -> anyhow::Result<()> { + let config = Config { + http: HttpConfig::default(), + auth: AuthConfig::Static(StaticUserStoreConfig { + users: vec![UserEntry { + user: User { + id: "default".to_owned(), + displayname: Some("Default user".to_owned()), + password: Some("generate a password hash with rustical pwhash".to_owned()), + }, + app_tokens: vec![], + }], + }), + data_store: DataStoreConfig::Sqlite(SqliteDataStoreConfig { + db_url: "".to_owned(), + }), + tracing: TracingConfig::default(), + frontend: FrontendConfig { + secret_key: generate_frontend_secret(), + }, + }; + let generated_config = toml::to_string(&config)?; + println!("{generated_config}"); + Ok(()) +} + +#[derive(Debug, Clone, ValueEnum)] +enum PwhashAlgorithm { + #[value(help = "Use this for your password")] + Argon2, + #[value(help = "Significantly faster algorithm, use for app tokens")] + Pbkdf2, +} + +#[derive(Debug, Parser)] +pub struct PwhashArgs { + #[arg(long, short = 'a')] + algorithm: PwhashAlgorithm, + #[arg( + long, + short = 'r', + help = "ONLY for pbkdf2: Number of rounds to calculate", + default_value_t = 1000 + )] + rounds: u32, +} + +pub fn cmd_pwhash(args: PwhashArgs) -> anyhow::Result<()> { + println!("Enter your password:"); + let password = rpassword::read_password()?; + let salt = SaltString::generate(OsRng); + let password_hash = match args.algorithm { + PwhashAlgorithm::Argon2 => argon2::Argon2::default() + .hash_password(password.as_bytes(), &salt) + .unwrap(), + PwhashAlgorithm::Pbkdf2 => pbkdf2::Pbkdf2 + .hash_password_customized( + password.as_bytes(), + None, + None, + Params { + rounds: args.rounds, + ..Default::default() + }, + &salt, + ) + .unwrap(), + }; + println!("{password_hash}"); + Ok(()) +} diff --git a/src/main.rs b/src/main.rs index 68de819..811a04f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,7 +2,8 @@ use crate::config::Config; use actix_web::HttpServer; use anyhow::Result; use app::make_app; -use clap::Parser; +use clap::{Parser, Subcommand}; +use commands::{cmd_gen_config, cmd_pwhash}; use config::{DataStoreConfig, SqliteDataStoreConfig}; use rustical_store::auth::StaticUserStore; use rustical_store::{AddressbookStore, CalendarStore}; @@ -12,16 +13,26 @@ use std::fs; use std::sync::Arc; mod app; +mod commands; mod config; mod setup_tracing; #[derive(Parser, Debug)] #[command(author, version, about, long_about = None)] struct Args { - #[arg(short, long, env)] + #[arg(short, long, env, default_value = "/etc/rustical/config.toml")] config_file: String, #[arg(long, env, help = "Run database migrations (only for sql store)")] migrate: bool, + + #[command(subcommand)] + command: Option, +} + +#[derive(Debug, Subcommand)] +enum Command { + GenConfig(commands::GenConfigArgs), + Pwhash(commands::PwhashArgs), } async fn get_data_stores( @@ -40,27 +51,33 @@ async fn get_data_stores( #[tokio::main] async fn main() -> Result<()> { let args = Args::parse(); - let config: Config = toml::from_str(&fs::read_to_string(&args.config_file)?)?; - setup_tracing(&config.tracing); + match args.command { + Some(Command::GenConfig(gen_config_args)) => cmd_gen_config(gen_config_args)?, + Some(Command::Pwhash(pwhash_args)) => cmd_pwhash(pwhash_args)?, + None => { + let config: Config = toml::from_str(&fs::read_to_string(&args.config_file)?)?; - let (addr_store, cal_store) = get_data_stores(args.migrate, &config.data_store).await?; + setup_tracing(&config.tracing); - let user_store = Arc::new(match config.auth { - config::AuthConfig::Static(config) => StaticUserStore::new(config), - }); + let (addr_store, cal_store) = get_data_stores(args.migrate, &config.data_store).await?; - HttpServer::new(move || { - make_app( - addr_store.clone(), - cal_store.clone(), - user_store.clone(), - config.frontend.clone(), - ) - }) - .bind((config.http.host, config.http.port))? - .run() - .await?; + let user_store = Arc::new(match config.auth { + config::AuthConfig::Static(config) => StaticUserStore::new(config), + }); + HttpServer::new(move || { + make_app( + addr_store.clone(), + cal_store.clone(), + user_store.clone(), + config.frontend.clone(), + ) + }) + .bind((config.http.host, config.http.port))? + .run() + .await?; + } + } Ok(()) }