geforkt von mirrored/vaultwarden
Added option to force 2fa at logins and made some changes to two factor code.
Added newlines to config options to keep them a reasonable length.
Dieser Commit ist enthalten in:
Ursprung
c6c00729e3
Commit
7d2bc9e162
4 geänderte Dateien mit 78 neuen und 79 gelöschten Zeilen
|
@ -102,6 +102,14 @@ fn recover(data: JsonUpcase<RecoverTwoFactor>, conn: DbConn) -> JsonResult {
|
||||||
Ok(Json(json!({})))
|
Ok(Json(json!({})))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn _generate_recover_code(user: &mut User, conn: &DbConn) {
|
||||||
|
if user.totp_recover.is_none() {
|
||||||
|
let totp_recover = BASE32.encode(&crypto::get_random(vec![0u8; 20]));
|
||||||
|
user.totp_recover = Some(totp_recover);
|
||||||
|
user.save(conn).ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
struct DisableTwoFactorData {
|
struct DisableTwoFactorData {
|
||||||
|
@ -196,9 +204,7 @@ fn activate_authenticator(data: JsonUpcase<EnableAuthenticatorData>, headers: He
|
||||||
let twofactor = TwoFactor::new(user.uuid.clone(), type_, key.to_uppercase());
|
let twofactor = TwoFactor::new(user.uuid.clone(), type_, key.to_uppercase());
|
||||||
|
|
||||||
// Validate the token provided with the key
|
// Validate the token provided with the key
|
||||||
if !twofactor.check_totp_code(token) {
|
validate_totp_code(token, &twofactor.data)?;
|
||||||
err!("Invalid totp code")
|
|
||||||
}
|
|
||||||
|
|
||||||
_generate_recover_code(&mut user, &conn);
|
_generate_recover_code(&mut user, &conn);
|
||||||
twofactor.save(&conn)?;
|
twofactor.save(&conn)?;
|
||||||
|
@ -215,12 +221,30 @@ fn activate_authenticator_put(data: JsonUpcase<EnableAuthenticatorData>, headers
|
||||||
activate_authenticator(data, headers, conn)
|
activate_authenticator(data, headers, conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn _generate_recover_code(user: &mut User, conn: &DbConn) {
|
pub fn validate_totp_code_str(totp_code: &str, secret: &str) -> EmptyResult {
|
||||||
if user.totp_recover.is_none() {
|
let totp_code: u64 = match totp_code.parse() {
|
||||||
let totp_recover = BASE32.encode(&crypto::get_random(vec![0u8; 20]));
|
Ok(code) => code,
|
||||||
user.totp_recover = Some(totp_recover);
|
_ => err!("TOTP code is not a number"),
|
||||||
user.save(conn).ok();
|
};
|
||||||
|
|
||||||
|
validate_totp_code(totp_code, secret)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn validate_totp_code(totp_code: u64, secret: &str) -> EmptyResult {
|
||||||
|
use data_encoding::BASE32;
|
||||||
|
use oath::{totp_raw_now, HashType};
|
||||||
|
|
||||||
|
let decoded_secret = match BASE32.decode(secret.as_bytes()) {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(_) => err!("Invalid TOTP secret"),
|
||||||
|
};
|
||||||
|
|
||||||
|
let generated = totp_raw_now(&decoded_secret, 6, 0, 30, &HashType::SHA1);
|
||||||
|
if generated != totp_code {
|
||||||
|
err!("Invalid TOTP code");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
use u2f::messages::{RegisterResponse, SignResponse, U2fSignRequest};
|
use u2f::messages::{RegisterResponse, SignResponse, U2fSignRequest};
|
||||||
|
@ -671,20 +695,12 @@ fn activate_yubikey_put(data: JsonUpcase<EnableYubikeyData>, headers: Headers, c
|
||||||
activate_yubikey(data, headers, conn)
|
activate_yubikey(data, headers, conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn validate_yubikey_login(user_uuid: &str, response: &str, conn: &DbConn) -> EmptyResult {
|
pub fn validate_yubikey_login(response: &str, twofactor_data: &str) -> EmptyResult {
|
||||||
if response.len() != 44 {
|
if response.len() != 44 {
|
||||||
err!("Invalid Yubikey OTP length");
|
err!("Invalid Yubikey OTP length");
|
||||||
}
|
}
|
||||||
|
|
||||||
let yubikey_type = TwoFactorType::YubiKey as i32;
|
let yubikey_metadata: YubikeyMetadata = serde_json::from_str(twofactor_data).expect("Can't parse Yubikey Metadata");
|
||||||
|
|
||||||
let twofactor = match TwoFactor::find_by_user_and_type(user_uuid, yubikey_type, &conn) {
|
|
||||||
Some(tf) => tf,
|
|
||||||
None => err!("No YubiKey devices registered"),
|
|
||||||
};
|
|
||||||
|
|
||||||
let yubikey_metadata: YubikeyMetadata =
|
|
||||||
serde_json::from_str(&twofactor.data).expect("Can't parse Yubikey Metadata");
|
|
||||||
let response_id = &response[..12];
|
let response_id = &response[..12];
|
||||||
|
|
||||||
if !yubikey_metadata.Keys.contains(&response_id.to_owned()) {
|
if !yubikey_metadata.Keys.contains(&response_id.to_owned()) {
|
||||||
|
|
|
@ -152,63 +152,45 @@ fn twofactor_auth(
|
||||||
conn: &DbConn,
|
conn: &DbConn,
|
||||||
) -> ApiResult<Option<String>> {
|
) -> ApiResult<Option<String>> {
|
||||||
let twofactors = TwoFactor::find_by_user(user_uuid, conn);
|
let twofactors = TwoFactor::find_by_user(user_uuid, conn);
|
||||||
let providers: Vec<_> = twofactors.iter().map(|tf| tf.type_).collect();
|
|
||||||
|
|
||||||
// No twofactor token if twofactor is disabled
|
// No twofactor token if twofactor is disabled
|
||||||
if twofactors.is_empty() {
|
if twofactors.is_empty() {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
let provider = data.two_factor_provider.unwrap_or(providers[0]); // If we aren't given a two factor provider, asume the first one
|
let twofactor_ids: Vec<_> = twofactors.iter().map(|tf| tf.type_).collect();
|
||||||
|
let selected_id = data.two_factor_provider.unwrap_or(twofactor_ids[0]); // If we aren't given a two factor provider, asume the first one
|
||||||
|
|
||||||
let twofactor_code = match data.two_factor_token {
|
let twofactor_code = match data.two_factor_token {
|
||||||
Some(ref code) => code,
|
Some(ref code) => code,
|
||||||
None => err_json!(_json_err_twofactor(&providers, user_uuid, conn)?),
|
None => err_json!(_json_err_twofactor(&twofactor_ids, user_uuid, conn)?),
|
||||||
};
|
};
|
||||||
|
|
||||||
let twofactor = twofactors.iter().filter(|tf| tf.type_ == provider).nth(0);
|
let selected_twofactor = twofactors.into_iter().filter(|tf| tf.type_ == selected_id).nth(0);
|
||||||
|
|
||||||
match TwoFactorType::from_i32(provider) {
|
use crate::api::core::two_factor as _tf;
|
||||||
Some(TwoFactorType::Remember) => {
|
|
||||||
use crate::crypto::ct_eq;
|
use crate::crypto::ct_eq;
|
||||||
|
|
||||||
|
let selected_data = _selected_data(selected_twofactor);
|
||||||
|
let mut remember = data.two_factor_remember.unwrap_or(0);
|
||||||
|
|
||||||
|
match TwoFactorType::from_i32(selected_id) {
|
||||||
|
Some(TwoFactorType::Authenticator) => _tf::validate_totp_code_str(twofactor_code, &selected_data?)?,
|
||||||
|
Some(TwoFactorType::U2f) => _tf::validate_u2f_login(user_uuid, twofactor_code, conn)?,
|
||||||
|
Some(TwoFactorType::YubiKey) => _tf::validate_yubikey_login(twofactor_code, &selected_data?)?,
|
||||||
|
|
||||||
|
Some(TwoFactorType::Remember) => {
|
||||||
match device.twofactor_remember {
|
match device.twofactor_remember {
|
||||||
Some(ref remember) if ct_eq(remember, twofactor_code) => return Ok(None), // No twofactor token needed here
|
Some(ref code) if !CONFIG.disable_2fa_remember() && ct_eq(code, twofactor_code) => {
|
||||||
_ => err_json!(_json_err_twofactor(&providers, user_uuid, conn)?),
|
remember = 1; // Make sure we also return the token here, otherwise it will only remember the first time
|
||||||
|
}
|
||||||
|
_ => err_json!(_json_err_twofactor(&twofactor_ids, user_uuid, conn)?),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(TwoFactorType::Authenticator) => {
|
|
||||||
let twofactor = match twofactor {
|
|
||||||
Some(tf) => tf,
|
|
||||||
None => err!("TOTP not enabled"),
|
|
||||||
};
|
|
||||||
|
|
||||||
let totp_code: u64 = match twofactor_code.parse() {
|
|
||||||
Ok(code) => code,
|
|
||||||
_ => err!("Invalid TOTP code"),
|
|
||||||
};
|
|
||||||
|
|
||||||
if !twofactor.check_totp_code(totp_code) {
|
|
||||||
err_json!(_json_err_twofactor(&providers, user_uuid, conn)?)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(TwoFactorType::U2f) => {
|
|
||||||
use crate::api::core::two_factor;
|
|
||||||
|
|
||||||
two_factor::validate_u2f_login(user_uuid, &twofactor_code, conn)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(TwoFactorType::YubiKey) => {
|
|
||||||
use crate::api::core::two_factor;
|
|
||||||
|
|
||||||
two_factor::validate_yubikey_login(user_uuid, twofactor_code, conn)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
_ => err!("Invalid two factor provider"),
|
_ => err!("Invalid two factor provider"),
|
||||||
}
|
}
|
||||||
|
|
||||||
if data.two_factor_remember.unwrap_or(0) == 1 {
|
if !CONFIG.disable_2fa_remember() && remember == 1 {
|
||||||
Ok(Some(device.refresh_twofactor_remember()))
|
Ok(Some(device.refresh_twofactor_remember()))
|
||||||
} else {
|
} else {
|
||||||
device.delete_twofactor_remember();
|
device.delete_twofactor_remember();
|
||||||
|
@ -216,6 +198,13 @@ fn twofactor_auth(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn _selected_data(tf: Option<TwoFactor>) -> ApiResult<String> {
|
||||||
|
match tf {
|
||||||
|
Some(tf) => Ok(tf.data),
|
||||||
|
None => err!("Two factor doesn't exist"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn _json_err_twofactor(providers: &[i32], user_uuid: &str, conn: &DbConn) -> ApiResult<Value> {
|
fn _json_err_twofactor(providers: &[i32], user_uuid: &str, conn: &DbConn) -> ApiResult<Value> {
|
||||||
use crate::api::core::two_factor;
|
use crate::api::core::two_factor;
|
||||||
|
|
||||||
|
|
|
@ -224,24 +224,27 @@ make_config! {
|
||||||
|
|
||||||
/// General settings
|
/// General settings
|
||||||
settings {
|
settings {
|
||||||
/// Domain URL |> This needs to be set to the URL used to access the server, including 'http[s]://' and port, if it's different than the default. Some server functions don't work correctly without this value
|
/// Domain URL |> This needs to be set to the URL used to access the server, including 'http[s]://'
|
||||||
|
/// and port, if it's different than the default. Some server functions don't work correctly without this value
|
||||||
domain: String, true, def, "http://localhost".to_string();
|
domain: String, true, def, "http://localhost".to_string();
|
||||||
/// Domain Set |> Indicates if the domain is set by the admin. Otherwise the default will be used.
|
/// Domain Set |> Indicates if the domain is set by the admin. Otherwise the default will be used.
|
||||||
domain_set: bool, false, def, false;
|
domain_set: bool, false, def, false;
|
||||||
/// Enable web vault
|
/// Enable web vault
|
||||||
web_vault_enabled: bool, false, def, true;
|
web_vault_enabled: bool, false, def, true;
|
||||||
|
|
||||||
/// Disable icon downloads |> Set to true to disable icon downloading, this would still serve icons from $ICON_CACHE_FOLDER,
|
/// Disable icon downloads |> Set to true to disable icon downloading, this would still serve icons from
|
||||||
/// but it won't produce any external network request. Needs to set $ICON_CACHE_TTL to 0,
|
/// $ICON_CACHE_FOLDER, but it won't produce any external network request. Needs to set $ICON_CACHE_TTL to 0,
|
||||||
/// otherwise it will delete them and they won't be downloaded again.
|
/// otherwise it will delete them and they won't be downloaded again.
|
||||||
disable_icon_download: bool, true, def, false;
|
disable_icon_download: bool, true, def, false;
|
||||||
/// Allow new signups |> Controls if new users can register. Note that while this is disabled, users could still be invited
|
/// Allow new signups |> Controls if new users can register. Note that while this is disabled, users could still be invited
|
||||||
signups_allowed: bool, true, def, true;
|
signups_allowed: bool, true, def, true;
|
||||||
/// Allow invitations |> Controls whether users can be invited by organization admins, even when signups are disabled
|
/// Allow invitations |> Controls whether users can be invited by organization admins, even when signups are disabled
|
||||||
invitations_allowed: bool, true, def, true;
|
invitations_allowed: bool, true, def, true;
|
||||||
/// Password iterations |> Number of server-side passwords hashing iterations. The changes only apply when a user changes their password. Not recommended to lower the value
|
/// Password iterations |> Number of server-side passwords hashing iterations.
|
||||||
|
/// The changes only apply when a user changes their password. Not recommended to lower the value
|
||||||
password_iterations: i32, true, def, 100_000;
|
password_iterations: i32, true, def, 100_000;
|
||||||
/// Show password hints |> Controls if the password hint should be shown directly in the web page. Otherwise, if email is disabled, there is no way to see the password hint
|
/// Show password hints |> Controls if the password hint should be shown directly in the web page.
|
||||||
|
/// Otherwise, if email is disabled, there is no way to see the password hint
|
||||||
show_password_hint: bool, true, def, true;
|
show_password_hint: bool, true, def, true;
|
||||||
|
|
||||||
/// Admin page token |> The token used to authenticate in this very same page. Changing it here won't deauthorize the current session
|
/// Admin page token |> The token used to authenticate in this very same page. Changing it here won't deauthorize the current session
|
||||||
|
@ -257,7 +260,12 @@ make_config! {
|
||||||
/// Icon download timeout |> Number of seconds when to stop attempting to download an icon.
|
/// Icon download timeout |> Number of seconds when to stop attempting to download an icon.
|
||||||
icon_download_timeout: u64, true, def, 10;
|
icon_download_timeout: u64, true, def, 10;
|
||||||
|
|
||||||
/// Reload templates (Dev) |> When this is set to true, the templates get reloaded with every request. ONLY use this during development, as it can slow down the server
|
/// Disable Two-Factor remember |> Enabling this would force the users to use a second factor to login every time.
|
||||||
|
/// Note that the checkbox would still be present, but ignored.
|
||||||
|
disable_2fa_remember: bool, true, def, false;
|
||||||
|
|
||||||
|
/// Reload templates (Dev) |> When this is set to true, the templates get reloaded with every request.
|
||||||
|
/// ONLY use this during development, as it can slow down the server
|
||||||
reload_templates: bool, true, def, false;
|
reload_templates: bool, true, def, false;
|
||||||
|
|
||||||
/// Log routes at launch (Dev)
|
/// Log routes at launch (Dev)
|
||||||
|
@ -267,7 +275,8 @@ make_config! {
|
||||||
/// Log file path
|
/// Log file path
|
||||||
log_file: String, false, option;
|
log_file: String, false, option;
|
||||||
|
|
||||||
/// Enable DB WAL |> Turning this off might lead to worse performance, but might help if using bitwarden_rs on some exotic filesystems, that do not support WAL. Please make sure you read project wiki on the topic before changing this setting.
|
/// Enable DB WAL |> Turning this off might lead to worse performance, but might help if using bitwarden_rs on some exotic filesystems,
|
||||||
|
/// that do not support WAL. Please make sure you read project wiki on the topic before changing this setting.
|
||||||
enable_db_wal: bool, false, def, true;
|
enable_db_wal: bool, false, def, true;
|
||||||
|
|
||||||
/// Disable Admin Token (Know the risks!) |> Disables the Admin Token for the admin page so you may use your own auth in-front
|
/// Disable Admin Token (Know the risks!) |> Disables the Admin Token for the admin page so you may use your own auth in-front
|
||||||
|
|
|
@ -42,21 +42,6 @@ impl TwoFactor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn check_totp_code(&self, totp_code: u64) -> bool {
|
|
||||||
let totp_secret = self.data.as_bytes();
|
|
||||||
|
|
||||||
use data_encoding::BASE32;
|
|
||||||
use oath::{totp_raw_now, HashType};
|
|
||||||
|
|
||||||
let decoded_secret = match BASE32.decode(totp_secret) {
|
|
||||||
Ok(s) => s,
|
|
||||||
Err(_) => return false,
|
|
||||||
};
|
|
||||||
|
|
||||||
let generated = totp_raw_now(&decoded_secret, 6, 0, 30, &HashType::SHA1);
|
|
||||||
generated == totp_code
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn to_json(&self) -> Value {
|
pub fn to_json(&self) -> Value {
|
||||||
json!({
|
json!({
|
||||||
"Enabled": self.enabled,
|
"Enabled": self.enabled,
|
||||||
|
|
Laden …
In neuem Issue referenzieren