Skip to content

Commit

Permalink
Merge pull request dani-garcia#3289 from BlackDex/admin-token-hash-su…
Browse files Browse the repository at this point in the history
…pport

Admin token Argon2 hashing support
  • Loading branch information
dani-garcia authored Mar 6, 2023
2 parents 337cbfa + de157b2 commit 97ffd17
Show file tree
Hide file tree
Showing 8 changed files with 240 additions and 20 deletions.
8 changes: 6 additions & 2 deletions .env.template
Original file line number Diff line number Diff line change
Expand Up @@ -259,9 +259,13 @@
## A comma-separated list means only those users can create orgs:
# [email protected],[email protected]

## 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
Expand Down
60 changes: 60 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
13 changes: 13 additions & 0 deletions src/api/admin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()),
}
}
Expand Down
19 changes: 18 additions & 1 deletion src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
});
Expand Down Expand Up @@ -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(())
}

Expand Down
106 changes: 89 additions & 17 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -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> {
Expand Down
37 changes: 37 additions & 0 deletions src/static/scripts/admin_settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -192,4 +227,6 @@ document.addEventListener("DOMContentLoaded", (/*event*/) => {
}

config_form.addEventListener("submit", saveConfig);

showWarnings();
});
6 changes: 6 additions & 0 deletions src/static/templates/admin/settings.hbs
Original file line number Diff line number Diff line change
@@ -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>
Expand Down

0 comments on commit 97ffd17

Please sign in to comment.