vw_small

Hardened fork of Vaultwarden (https://github.com/dani-garcia/vaultwarden) with fewer features.
git clone https://git.philomathiclife.com/repos/vw_small
Log | Files | Refs | README

commit de157b26543172fe48aa44af578e229b1db65475
parent 337cbfaf22ee28316ea09e859be0527416fe7da5
Author: BlackDex <black.dex@gmail.com>
Date:   Tue, 28 Feb 2023 23:09:51 +0100

Admin token Argon2 hashing support

Added support for Argon2 hashing support for the `ADMIN_TOKEN` instead
of only supporting a plain text string.

The hash must be a PHC string which can be generated via the `argon2`
CLI **or** via the also built-in hash command in Vaultwarden.

You can simply run `vaultwarden hash` to generate a hash based upon a
password the user provides them self.

Added a warning during startup and within the admin settings panel is
the `ADMIN_TOKEN` is not an Argon2 hash.

Within the admin environment a user can ignore that warning and it will
not be shown for at least 30 days. After that the warning will appear
again unless the `ADMIN_TOKEN` has be converted to an Argon2 hash.

I have also tested this on my RaspberryPi 2b and there the `Bitwarden`
preset takes almost 4.5 seconds to generate/verify the Argon2 hash.

Using the `OWASP` preset it is below 1 second, which I think should be
fine for low-graded hardware. If it is needed people could use lower
memory settings, but in those cases I even doubt Vaultwarden it self
would run. They can always use the `argon2` CLI and generate a faster hash.

Diffstat:
M.env.template | 8++++++--
MCargo.lock | 60++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
MCargo.toml | 11+++++++++++
Msrc/api/admin.rs | 13+++++++++++++
Msrc/config.rs | 19++++++++++++++++++-
Msrc/main.rs | 106++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
Msrc/static/scripts/admin_settings.js | 37+++++++++++++++++++++++++++++++++++++
Msrc/static/templates/admin/settings.hbs | 6++++++
8 files changed, 240 insertions(+), 20 deletions(-)

diff --git 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 @@ -86,6 +86,17 @@ dependencies = [ ] [[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" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -325,6 +336,12 @@ 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" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -337,6 +354,15 @@ 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" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2007,6 +2033,17 @@ dependencies = [ ] [[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" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2586,6 +2623,27 @@ dependencies = [ ] [[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" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -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 @@ -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 @@ -201,6 +201,19 @@ fn post_admin_login(data: Form<LoginForm>, 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 @@ -19,7 +19,7 @@ static CONFIG_FILE: Lazy<String> = Lazy::new(|| { pub static CONFIG: Lazy<Config> = 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 @@ -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<String> = 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 @@ -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 @@ -1,4 +1,10 @@ <main class="container-xl"> + <div id="admin_token_warning" class="alert alert-warning alert-dismissible fade show d-none"> + <button type="button" class="btn-close" data-bs-target="admin_token_warning" data-bs-dismiss="alert" aria-label="Close"></button> + You are using a plain text `ADMIN_TOKEN` which is insecure.<br> + Please generate a secure Argon2 PHC string by using `vaultwarden hash` or `argon2`.<br> + See: <a href="https://github.com/dani-garcia/vaultwarden/wiki/Enabling-admin-page#secure-the-admin_token" target="_blank" rel="noopener noreferrer">Enabling admin page - Secure the `ADMIN_TOKEN`</a> + </div> <div id="config-block" class="align-items-center p-3 mb-3 bg-secondary rounded shadow"> <div> <h6 class="text-white mb-3">Configuration</h6>