Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Admin token Argon2 hashing support #3289

Merged
merged 1 commit into from
Mar 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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