diff --git a/.env.template b/.env.template index 3c7e7d1e..9d6f75a1 100644 --- a/.env.template +++ b/.env.template @@ -259,9 +259,13 @@ ## A comma-separated list means only those users can create orgs: # ORG_CREATION_USERS=admin1@example.com,admin2@example.com -## Token for the admin interface, preferably use a long random string -## One option is to use 'openssl rand -base64 48' +## Token for the admin interface, preferably an Argon2 PCH string +## Vaultwarden has a built-in generator by calling `vaultwarden hash` +## For details see: https://github.com/dani-garcia/vaultwarden/wiki/Enabling-admin-page#secure-the-admin_token ## If not set, the admin panel is disabled +## New Argon2 PHC string +# ADMIN_TOKEN='$argon2id$v=19$m=65540,t=3,p=4$MmeKRnGK5RW5mJS7h3TOL89GrpLPXJPAtTK8FTqj9HM$DqsstvoSAETl9YhnsXbf43WeaUwJC6JhViIvuPoig78' +## Old plain text string (Will generate warnings in favor of Argon2) # ADMIN_TOKEN=Vy2VyYTTsKPv8W5aEOWUbB/Bt3DEKePbHmI4m9VcemUMS2rEviDowNAFqYi1xjmp ## Enable this to bypass the admin panel security. This option is only diff --git a/Cargo.lock b/Cargo.lock index 067718c9..0239a79c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -85,6 +85,17 @@ dependencies = [ "libc", ] +[[package]] +name = "argon2" +version = "0.5.0-pre.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0efde6c15a373abaefe544ddae9fc024eac3073798ba0c40043fd655f3535eb8" +dependencies = [ + "base64ct", + "blake2", + "password-hash", +] + [[package]] name = "async-channel" version = "1.8.0" @@ -324,6 +335,12 @@ version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a" +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + [[package]] name = "binascii" version = "0.1.4" @@ -336,6 +353,15 @@ 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" @@ -2006,6 +2032,17 @@ dependencies = [ "regex", ] +[[package]] +name = "password-hash" +version = "0.5.0-pre.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d9d7f72dbf886af2c2a8d4a2ddfb4eea37e4d77ea3bde49f79af7c577e37908" +dependencies = [ + "base64ct", + "rand_core", + "subtle", +] + [[package]] name = "paste" version = "1.0.11" @@ -2585,6 +2622,27 @@ dependencies = [ "uncased", ] +[[package]] +name = "rpassword" +version = "7.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6678cf63ab3491898c0d021b493c94c9b221d91295294a2a5746eacbe5928322" +dependencies = [ + "libc", + "rtoolbox", + "winapi", +] + +[[package]] +name = "rtoolbox" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "034e22c514f5c0cb8a10ff341b9b048b5ceb21591f31c8f44c43b960f9b3524a" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "rustc-demangle" version = "0.1.21" @@ -3425,6 +3483,7 @@ dependencies = [ name = "vaultwarden" version = "1.0.0" dependencies = [ + "argon2", "backtrace", "bytes", "cached", @@ -3464,6 +3523,7 @@ dependencies = [ "ring", "rmpv", "rocket", + "rpassword", "semver", "serde", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index a231d709..258b03a5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -157,8 +157,19 @@ semver = "1.0.16" mimalloc = { version = "0.1.34", features = ["secure"], default-features = false, optional = true } which = "4.4.0" +# Argon2 library with support for the PHC format +argon2 = "0.5.0-pre.0" + +# Reading a password from the cli for generating the Argon2id ADMIN_TOKEN +rpassword = "7.2" + # Strip debuginfo from the release builds # Also enable thin LTO for some optimizations [profile.release] strip = "debuginfo" lto = "thin" + +# Always build argon2 using opt-level 3 +# This is a huge speed improvement during testing +[profile.dev.package.argon2] +opt-level = 3 diff --git a/src/api/admin.rs b/src/api/admin.rs index a25809ed..651e51b1 100644 --- a/src/api/admin.rs +++ b/src/api/admin.rs @@ -201,6 +201,19 @@ fn post_admin_login(data: Form, cookies: &CookieJar<'_>, ip: ClientIp fn _validate_token(token: &str) -> bool { match CONFIG.admin_token().as_ref() { None => false, + Some(t) if t.starts_with("$argon2") => { + use argon2::password_hash::PasswordVerifier; + match argon2::password_hash::PasswordHash::new(t) { + Ok(h) => { + // NOTE: hash params from `ADMIN_TOKEN` are used instead of what is configured in the `Argon2` instance. + argon2::Argon2::default().verify_password(token.trim().as_ref(), &h).is_ok() + } + Err(e) => { + error!("The configured Argon2 PHC in `ADMIN_TOKEN` is invalid: {e}"); + false + } + } + } Some(t) => crate::crypto::ct_eq(t.trim(), token.trim()), } } diff --git a/src/config.rs b/src/config.rs index f3736a1f..6ed19a79 100644 --- a/src/config.rs +++ b/src/config.rs @@ -19,7 +19,7 @@ static CONFIG_FILE: Lazy = Lazy::new(|| { pub static CONFIG: Lazy = Lazy::new(|| { Config::load().unwrap_or_else(|e| { - println!("Error loading config:\n\t{e:?}\n"); + println!("Error loading config:\n {e:?}\n"); exit(12) }) }); @@ -872,6 +872,23 @@ fn validate_config(cfg: &ConfigItems) -> Result<(), Error> { err!("`EVENT_CLEANUP_SCHEDULE` is not a valid cron expression") } + if !cfg.disable_admin_token { + match cfg.admin_token.as_ref() { + Some(t) if t.starts_with("$argon2") => { + if let Err(e) = argon2::password_hash::PasswordHash::new(t) { + err!(format!("The configured Argon2 PHC in `ADMIN_TOKEN` is invalid: '{e}'")) + } + } + Some(_) => { + println!( + "[NOTICE] You are using a plain text `ADMIN_TOKEN` which is insecure.\n\ + Please generate a secure Argon2 PHC string by using `vaultwarden hash` or `argon2`.\n\ + See: https://github.com/dani-garcia/vaultwarden/wiki/Enabling-admin-page#secure-the-admin_token\n" + ); + } + _ => {} + } + } Ok(()) } diff --git a/src/main.rs b/src/main.rs index cd17a2f5..dbf527f7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -118,14 +118,22 @@ async fn main() -> Result<(), Error> { } const HELP: &str = "\ - Alternative implementation of the Bitwarden server API written in Rust +Alternative implementation of the Bitwarden server API written in Rust - USAGE: - vaultwarden +USAGE: + vaultwarden [FLAGS|COMMAND] + +FLAGS: + -h, --help Prints help information + -v, --version Prints the app version + +COMMAND: + hash [--preset {bitwarden|owasp}] Generate an Argon2id PHC ADMIN_TOKEN + +PRESETS: m= t= p= + bitwarden (default) 64MiB, 3 Iterations, 4 Threads + owasp 19MiB, 2 Iterations, 1 Thread - FLAGS: - -h, --help Prints help information - -v, --version Prints the app version "; pub const VERSION: Option<&str> = option_env!("VW_VERSION"); @@ -142,24 +150,88 @@ fn parse_args() { println!("vaultwarden {version}"); exit(0); } -} + if let Some(command) = pargs.subcommand().unwrap_or_default() { + if command == "hash" { + use argon2::{ + password_hash::SaltString, Algorithm::Argon2id, Argon2, ParamsBuilder, PasswordHasher, Version::V0x13, + }; + + let mut argon2_params = ParamsBuilder::new(); + let preset: Option = pargs.opt_value_from_str(["-p", "--preset"]).unwrap_or_default(); + let selected_preset; + match preset.as_deref() { + Some("owasp") => { + selected_preset = "owasp"; + argon2_params.m_cost(19456); + argon2_params.t_cost(2); + argon2_params.p_cost(1); + } + _ => { + // Bitwarden preset is the default + selected_preset = "bitwarden"; + argon2_params.m_cost(65540); + argon2_params.t_cost(3); + argon2_params.p_cost(4); + } + } + + println!("Generate an Argon2id PHC string using the '{selected_preset}' preset:\n"); + + let password = rpassword::prompt_password("Password: ").unwrap(); + if password.len() < 8 { + println!("\nPassword must contain at least 8 characters"); + exit(1); + } + + let password_verify = rpassword::prompt_password("Confirm Password: ").unwrap(); + if password != password_verify { + println!("\nPasswords do not match"); + exit(1); + } + + let argon2 = Argon2::new(Argon2id, V0x13, argon2_params.build().unwrap()); + let salt = SaltString::b64_encode(&crate::crypto::get_random_bytes::<32>()).unwrap(); + + let argon2_timer = tokio::time::Instant::now(); + if let Ok(password_hash) = argon2.hash_password(password.as_bytes(), &salt) { + println!( + "\n\ + ADMIN_TOKEN='{password_hash}'\n\n\ + Generation of the Argon2id PHC string took: {:?}", + argon2_timer.elapsed() + ); + } else { + error!("Unable to generate Argon2id PHC hash."); + exit(1); + } + } + exit(0); + } +} fn launch_info() { - println!("/--------------------------------------------------------------------\\"); - println!("| Starting Vaultwarden |"); + println!( + "\ + /--------------------------------------------------------------------\\\n\ + | Starting Vaultwarden |" + ); if let Some(version) = VERSION { println!("|{:^68}|", format!("Version {version}")); } - println!("|--------------------------------------------------------------------|"); - println!("| This is an *unofficial* Bitwarden implementation, DO NOT use the |"); - println!("| official channels to report bugs/features, regardless of client. |"); - println!("| Send usage/configuration questions or feature requests to: |"); - println!("| https://vaultwarden.discourse.group/ |"); - println!("| Report suspected bugs/issues in the software itself at: |"); - println!("| https://github.com/dani-garcia/vaultwarden/issues/new |"); - println!("\\--------------------------------------------------------------------/\n"); + println!( + "\ + |--------------------------------------------------------------------|\n\ + | This is an *unofficial* Bitwarden implementation, DO NOT use the |\n\ + | official channels to report bugs/features, regardless of client. |\n\ + | Send usage/configuration questions or feature requests to: |\n\ + | https://github.com/dani-garcia/vaultwarden/discussions or |\n\ + | https://vaultwarden.discourse.group/ |\n\ + | Report suspected bugs/issues in the software itself at: |\n\ + | https://github.com/dani-garcia/vaultwarden/issues/new |\n\ + \\--------------------------------------------------------------------/\n" + ); } fn init_logging(level: log::LevelFilter) -> Result<(), fern::InitError> { diff --git a/src/static/scripts/admin_settings.js b/src/static/scripts/admin_settings.js index 2e36795f..06f15e0a 100644 --- a/src/static/scripts/admin_settings.js +++ b/src/static/scripts/admin_settings.js @@ -157,6 +157,41 @@ function masterCheck(check_id, inputs_query) { } } +// This will check if the ADMIN_TOKEN is not a Argon2 hashed value. +// Else it will show a warning, unless someone has closed it. +// Then it will not show this warning for 30 days. +function checkAdminToken() { + const admin_token = document.getElementById("input_admin_token"); + const disable_admin_token = document.getElementById("input_disable_admin_token"); + if (!disable_admin_token.checked && !admin_token.value.startsWith("$argon2")) { + // Check if the warning has been closed before and 30 days have passed + const admin_token_warning_closed = localStorage.getItem("admin_token_warning_closed"); + if (admin_token_warning_closed !== null) { + const closed_date = new Date(parseInt(admin_token_warning_closed)); + const current_date = new Date(); + const thirtyDays = 1000*60*60*24*30; + if (current_date - closed_date < thirtyDays) { + return; + } + } + + // When closing the alert, store the current date/time in the browser + const admin_token_warning = document.getElementById("admin_token_warning"); + admin_token_warning.addEventListener("closed.bs.alert", function() { + const d = new Date(); + localStorage.setItem("admin_token_warning_closed", d.getTime()); + }); + + // Display the warning + admin_token_warning.classList.remove("d-none"); + } +} + +// This will check for specific configured values, and when needed will show a warning div +function showWarnings() { + checkAdminToken(); +} + const config_form = document.getElementById("config-form"); // onLoad events @@ -192,4 +227,6 @@ document.addEventListener("DOMContentLoaded", (/*event*/) => { } config_form.addEventListener("submit", saveConfig); + + showWarnings(); }); \ No newline at end of file diff --git a/src/static/templates/admin/settings.hbs b/src/static/templates/admin/settings.hbs index 50cd1a75..b8ee5f4b 100644 --- a/src/static/templates/admin/settings.hbs +++ b/src/static/templates/admin/settings.hbs @@ -1,4 +1,10 @@
+
+ + You are using a plain text `ADMIN_TOKEN` which is insecure.
+ Please generate a secure Argon2 PHC string by using `vaultwarden hash` or `argon2`.
+ See: Enabling admin page - Secure the `ADMIN_TOKEN` +
Configuration