diff --git a/src/api/core/two_factor.rs b/src/api/core/two_factor.rs index b1dd9f0..c097c78 100644 --- a/src/api/core/two_factor.rs +++ b/src/api/core/two_factor.rs @@ -102,6 +102,14 @@ fn recover(data: JsonUpcase, conn: DbConn) -> JsonResult { 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)] #[allow(non_snake_case)] struct DisableTwoFactorData { @@ -196,9 +204,7 @@ fn activate_authenticator(data: JsonUpcase, headers: He let twofactor = TwoFactor::new(user.uuid.clone(), type_, key.to_uppercase()); // Validate the token provided with the key - if !twofactor.check_totp_code(token) { - err!("Invalid totp code") - } + validate_totp_code(token, &twofactor.data)?; _generate_recover_code(&mut user, &conn); twofactor.save(&conn)?; @@ -215,12 +221,30 @@ fn activate_authenticator_put(data: JsonUpcase, headers activate_authenticator(data, headers, conn) } -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(); +pub fn validate_totp_code_str(totp_code: &str, secret: &str) -> EmptyResult { + let totp_code: u64 = match totp_code.parse() { + Ok(code) => code, + _ => err!("TOTP code is not a number"), + }; + + 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}; @@ -671,20 +695,12 @@ fn activate_yubikey_put(data: JsonUpcase, headers: Headers, c 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 { err!("Invalid Yubikey OTP length"); } - let yubikey_type = TwoFactorType::YubiKey as i32; - - 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 yubikey_metadata: YubikeyMetadata = serde_json::from_str(twofactor_data).expect("Can't parse Yubikey Metadata"); let response_id = &response[..12]; if !yubikey_metadata.Keys.contains(&response_id.to_owned()) { diff --git a/src/api/identity.rs b/src/api/identity.rs index b73b0d5..309613f 100644 --- a/src/api/identity.rs +++ b/src/api/identity.rs @@ -152,63 +152,45 @@ fn twofactor_auth( conn: &DbConn, ) -> ApiResult> { 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 if twofactors.is_empty() { 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 { 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); + + use crate::api::core::two_factor as _tf; + 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?)?, - match TwoFactorType::from_i32(provider) { Some(TwoFactorType::Remember) => { - use crate::crypto::ct_eq; match device.twofactor_remember { - Some(ref remember) if ct_eq(remember, twofactor_code) => return Ok(None), // No twofactor token needed here - _ => err_json!(_json_err_twofactor(&providers, user_uuid, conn)?), + Some(ref code) if !CONFIG.disable_2fa_remember() && ct_eq(code, twofactor_code) => { + 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"), } - if data.two_factor_remember.unwrap_or(0) == 1 { + if !CONFIG.disable_2fa_remember() && remember == 1 { Ok(Some(device.refresh_twofactor_remember())) } else { device.delete_twofactor_remember(); @@ -216,6 +198,13 @@ fn twofactor_auth( } } +fn _selected_data(tf: Option) -> ApiResult { + 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 { use crate::api::core::two_factor; diff --git a/src/config.rs b/src/config.rs index 1bcb755..33d03ae 100644 --- a/src/config.rs +++ b/src/config.rs @@ -224,24 +224,27 @@ make_config! { /// General 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 Set |> Indicates if the domain is set by the admin. Otherwise the default will be used. domain_set: bool, false, def, false; /// Enable web vault 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, - /// but it won't produce any external network request. Needs to set $ICON_CACHE_TTL to 0, + /// Disable icon downloads |> Set to true to disable icon downloading, this would still serve icons from + /// $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. 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 signups_allowed: bool, true, def, true; /// Allow invitations |> Controls whether users can be invited by organization admins, even when signups are disabled 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; - /// 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; /// Admin page token |> The token used to authenticate in this very same page. Changing it here won't deauthorize the current session @@ -255,9 +258,14 @@ make_config! { /// Negative icon cache expiry |> Number of seconds before trying to download an icon that failed again. icon_cache_negttl: u64, true, def, 259_200; /// 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; /// Log routes at launch (Dev) @@ -267,7 +275,8 @@ make_config! { /// Log file path 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; /// Disable Admin Token (Know the risks!) |> Disables the Admin Token for the admin page so you may use your own auth in-front diff --git a/src/db/models/two_factor.rs b/src/db/models/two_factor.rs index 0772ef9..fc48313 100644 --- a/src/db/models/two_factor.rs +++ b/src/db/models/two_factor.rs @@ -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 { json!({ "Enabled": self.enabled,