diff --git a/.env.template b/.env.template index 578d1e93..7892652e 100644 --- a/.env.template +++ b/.env.template @@ -112,6 +112,21 @@ # YUBICO_SECRET_KEY=AAAAAAAAAAAAAAAAAAAAAAAA # YUBICO_SERVER=http://yourdomain.com/wsapi/2.0/verify +## Duo Settings +## You need to configure all options to enable Duo support +## Create an account and protect an application as mentioned in this link (only the first step, not the rest): +## https://help.bitwarden.com/article/setup-two-step-login-duo/#create-a-duo-security-account +## Then set the following options, based on the values obtained from the last step: +# DUO_IKEY= +# DUO_SKEY= +# DUO_HOST= +## The Aplication Key needs to be randomly generated. Recommended at least 40 characters in base64. +## Example command to generate it: 'openssl rand -base64 48' +## Note that this shouldn't change between runs. +# DUO_AKEY= +## After that, you should be able to follow the rest of the guide linked above, +## ignoring the fields that ask for the values that you already configured beforehand. + ## Rocket specific settings, check Rocket documentation to learn more # ROCKET_ENV=staging # ROCKET_ADDRESS=0.0.0.0 # Enable this to test mobile app diff --git a/src/api/core/two_factor.rs b/src/api/core/two_factor.rs index 31cca499..45258302 100644 --- a/src/api/core/two_factor.rs +++ b/src/api/core/two_factor.rs @@ -717,16 +717,12 @@ pub fn validate_yubikey_login(response: &str, twofactor_data: &str) -> EmptyResu } } -#[derive(Serialize, Deserialize, Default)] -pub struct DuoData { - pub host: String, - sk: String, - ik: String, - ak: String, -} - #[post("/two-factor/get-duo", data = "")] fn get_duo(data: JsonUpcase, headers: Headers, conn: DbConn) -> JsonResult { + if CONFIG.duo_host().is_none() { + err!("Duo is disabled. Refer to the Wiki for instructions in how to enable it") + } + let data: PasswordData = data.into_inner().data; if !headers.user.check_valid_password(&data.MasterPasswordHash) { @@ -736,24 +732,22 @@ fn get_duo(data: JsonUpcase, headers: Headers, conn: DbConn) -> Js let type_ = TwoFactorType::Duo as i32; let twofactor = TwoFactor::find_by_user_and_type(&headers.user.uuid, type_, &conn); - let (enabled, data) = match twofactor { - Some(tf) => (true, serde_json::from_str(&tf.data)?), - _ => (false, DuoData::default()), + let (enabled, msg) = match twofactor { + Some(_) => (true, ""), + _ => (false, ""), }; - // TODO: It's probably not the best idea to return the keys here - Ok(Json(json!({ "Enabled": enabled, - "Host": data.host, - "SecretKey": data.sk, - "IntegrationKey": data.ik, + "Host": msg, + "SecretKey": msg, + "IntegrationKey": msg, "Object": "twoFactorDuo" }))) } #[derive(Deserialize)] -#[allow(non_snake_case)] +#[allow(non_snake_case, dead_code)] struct EnableDuoData { MasterPasswordHash: String, Host: String, @@ -769,27 +763,15 @@ fn activate_duo(data: JsonUpcase, headers: Headers, conn: DbConn) err!("Invalid password"); } - let data = DuoData { - host: data.Host, - sk: data.SecretKey, - ik: data.IntegrationKey, - ak: BASE64.encode(&crypto::get_random_64()), - }; - - // Validate parameters with the server - duo_api_request("GET", "/auth/v2/check", "", &data)?; - - let data_json = serde_json::to_string(&data)?; let type_ = TwoFactorType::Duo; - let twofactor = TwoFactor::new(headers.user.uuid.clone(), type_, data_json); + let twofactor = TwoFactor::new(headers.user.uuid.clone(), type_, String::new()); twofactor.save(&conn)?; - // TODO: It's probably not the best idea to return the keys here Ok(Json(json!({ "Enabled": true, - "Host": data.host, - "SecretKey": data.sk, - "IntegrationKey": data.ik, + "Host": "", + "SecretKey": "", + "IntegrationKey": "", "Object": "twoFactorDuo" }))) } @@ -799,7 +781,8 @@ fn activate_duo_put(data: JsonUpcase, headers: Headers, conn: DbC activate_duo(data, headers, conn) } -fn duo_api_request(method: &str, path: &str, params: &str, data: &DuoData) -> EmptyResult { +// duo_api_request("GET", "/auth/v2/check", "", &data)?; +fn _duo_api_request(method: &str, path: &str, params: &str) -> EmptyResult { const AGENT: &str = "bitwarden_rs:Duo/1.0 (Rust)"; use std::str::FromStr; @@ -807,13 +790,15 @@ fn duo_api_request(method: &str, path: &str, params: &str, data: &DuoData) -> Em use chrono::Utc; use reqwest::{header::*, Client, Method}; - let url = format!("https://{}{}", data.host, path); + let ik = CONFIG.duo_ikey().unwrap(); + let sk = CONFIG.duo_skey().unwrap(); + let host = CONFIG.duo_host().unwrap(); + let url = format!("https://{}{}", host, path); let date = Utc::now().to_rfc2822(); - - let username = &data.ik; - let fields = [&date, method, &data.host, path, params]; - let password = crypto::hmac_sign(&data.sk, &fields.join("\n")); + let username = &ik; + let fields = [&date, method, &host, path, params]; + let password = crypto::hmac_sign(&sk, &fields.join("\n")); let m = Method::from_str(method).unwrap_or_default(); @@ -837,11 +822,15 @@ const APP_PREFIX: &str = "APP"; use chrono::Utc; -pub fn generate_duo_signature(data: &DuoData, email: &str) -> String { +pub fn generate_duo_signature(email: &str) -> String { let now = Utc::now().timestamp(); - let duo_sign = sign_duo_values(&data.sk, email, &data.ik, DUO_PREFIX, now + DUO_EXPIRE); - let app_sign = sign_duo_values(&data.ak, email, &data.ik, APP_PREFIX, now + APP_EXPIRE); + let ik = CONFIG.duo_ikey().unwrap(); + let sk = CONFIG.duo_skey().unwrap(); + let ak = CONFIG.duo_akey().unwrap(); + + let duo_sign = sign_duo_values(&sk, email, &ik, DUO_PREFIX, now + DUO_EXPIRE); + let app_sign = sign_duo_values(&ak, email, &ik, APP_PREFIX, now + APP_EXPIRE); format!("{}:{}", duo_sign, app_sign) } @@ -853,10 +842,8 @@ fn sign_duo_values(key: &str, email: &str, ikey: &str, prefix: &str, expire: i64 format!("{}|{}", cookie, crypto::hmac_sign(key, &cookie)) } -pub fn validate_duo_login(response: &str, twofactor_data: &str) -> EmptyResult { - let data: DuoData = serde_json::from_str(twofactor_data)?; - - let split: Vec<&str> = response.split(":").collect(); +pub fn validate_duo_login(email: &str, response: &str) -> EmptyResult { + let split: Vec<&str> = response.split(':').collect(); if split.len() != 2 { err!("Invalid response length"); } @@ -866,10 +853,14 @@ pub fn validate_duo_login(response: &str, twofactor_data: &str) -> EmptyResult { let now = Utc::now().timestamp(); - let auth_user = parse_duo_values(&data.sk, auth_sig, &data.ik, AUTH_PREFIX, now)?; - let app_user = parse_duo_values(&data.ak, app_sig, &data.ik, APP_PREFIX, now)?; + let ik = CONFIG.duo_ikey().unwrap(); + let sk = CONFIG.duo_skey().unwrap(); + let ak = CONFIG.duo_akey().unwrap(); + + let auth_user = parse_duo_values(&sk, auth_sig, &ik, AUTH_PREFIX, now)?; + let app_user = parse_duo_values(&ak, app_sig, &ik, APP_PREFIX, now)?; - if !crypto::ct_eq(auth_user, app_user) { + if !crypto::ct_eq(&auth_user, app_user) || !crypto::ct_eq(&auth_user, email) { err!("Error validating duo authentication") } @@ -877,7 +868,7 @@ pub fn validate_duo_login(response: &str, twofactor_data: &str) -> EmptyResult { } fn parse_duo_values(key: &str, val: &str, ikey: &str, prefix: &str, time: i64) -> ApiResult { - let split: Vec<&str> = val.split("|").collect(); + let split: Vec<&str> = val.split('|').collect(); if split.len() != 3 { err!("Invalid value length") } @@ -906,7 +897,7 @@ fn parse_duo_values(key: &str, val: &str, ikey: &str, prefix: &str, time: i64) - Err(_) => err!("Invalid Duo cookie encoding"), }; - let cookie_split: Vec<&str> = cookie.split("|").collect(); + let cookie_split: Vec<&str> = cookie.split('|').collect(); if cookie_split.len() != 3 { err!("Invalid cookie length") } diff --git a/src/api/identity.rs b/src/api/identity.rs index b881cbe9..d3535ab7 100644 --- a/src/api/identity.rs +++ b/src/api/identity.rs @@ -9,7 +9,7 @@ use num_traits::FromPrimitive; use crate::db::models::*; use crate::db::DbConn; -use crate::util::{self, JsonMap}; +use crate::util; use crate::api::{ApiResult, EmptyResult, JsonResult}; @@ -178,7 +178,7 @@ fn twofactor_auth( 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::Duo) => _tf::validate_duo_login(twofactor_code, &selected_data?)?, + Some(TwoFactorType::Duo) => _tf::validate_duo_login(data.username.as_ref().unwrap(), twofactor_code)?, Some(TwoFactorType::Remember) => { match device.twofactor_remember { @@ -227,42 +227,33 @@ fn _json_err_twofactor(providers: &[i32], user_uuid: &str, conn: &DbConn) -> Api let mut challenge_list = Vec::new(); for key in request.registered_keys { - let mut challenge_map = JsonMap::new(); - - challenge_map.insert("appId".into(), Value::String(request.app_id.clone())); - challenge_map.insert("challenge".into(), Value::String(request.challenge.clone())); - challenge_map.insert("version".into(), Value::String(key.version)); - challenge_map.insert("keyHandle".into(), Value::String(key.key_handle.unwrap_or_default())); - - challenge_list.push(Value::Object(challenge_map)); + challenge_list.push(json!({ + "appId": request.app_id, + "challenge": request.challenge, + "version": key.version, + "keyHandle": key.key_handle, + })); } - let mut map = JsonMap::new(); let challenge_list_str = serde_json::to_string(&challenge_list).unwrap(); - map.insert("Challenges".into(), Value::String(challenge_list_str)); - result["TwoFactorProviders2"][provider.to_string()] = Value::Object(map); + result["TwoFactorProviders2"][provider.to_string()] = json!({ + "Challenges": challenge_list_str, + }); } - Some(tf_type @ TwoFactorType::Duo) => { - let twofactor = match TwoFactor::find_by_user_and_type(user_uuid, tf_type as i32, &conn) { - Some(tf) => tf, - None => err!("No Duo devices registered"), - }; - - let duo_data: two_factor::DuoData = serde_json::from_str(&twofactor.data)?; - + Some(TwoFactorType::Duo) => { let email = match User::find_by_uuid(user_uuid, &conn) { Some(u) => u.email, - None => err!("User does not exist") + None => err!("User does not exist"), }; - let signature = two_factor::generate_duo_signature(&duo_data, &email); + let signature = two_factor::generate_duo_signature(&email); - let mut map = JsonMap::new(); - map.insert("Host".into(), Value::String(duo_data.host)); - map.insert("Signature".into(), Value::String(signature)); - result["TwoFactorProviders2"][provider.to_string()] = Value::Object(map); + result["TwoFactorProviders2"][provider.to_string()] = json!({ + "Host": CONFIG.duo_host(), + "Signature": signature, + }); } Some(tf_type @ TwoFactorType::YubiKey) => { @@ -271,12 +262,11 @@ fn _json_err_twofactor(providers: &[i32], user_uuid: &str, conn: &DbConn) -> Api None => err!("No YubiKey devices registered"), }; - let yubikey_metadata: two_factor::YubikeyMetadata = - serde_json::from_str(&twofactor.data)?; + let yubikey_metadata: two_factor::YubikeyMetadata = serde_json::from_str(&twofactor.data)?; - let mut map = JsonMap::new(); - map.insert("Nfc".into(), Value::Bool(yubikey_metadata.Nfc)); - result["TwoFactorProviders2"][provider.to_string()] = Value::Object(map); + result["TwoFactorProviders2"][provider.to_string()] = json!({ + "Nfc": yubikey_metadata.Nfc, + }) } _ => {} diff --git a/src/config.rs b/src/config.rs index f27afaed..3a7a85f1 100644 --- a/src/config.rs +++ b/src/config.rs @@ -302,6 +302,20 @@ make_config! { yubico_server: String, true, option; }, + /// Duo settings + duo: _enable_duo { + /// Enabled + _enable_duo: bool, true, def, true; + /// Integration Key + duo_ikey: String, true, option; + /// Secret Key + duo_skey: Pass, true, option; + /// Host + duo_host: String, true, option; + /// Application Key + duo_akey: Pass, true, option; + }, + /// SMTP Email Settings smtp: _enable_smtp { /// Enabled @@ -332,6 +346,12 @@ fn validate_config(cfg: &ConfigItems) -> Result<(), Error> { } } + if (cfg.duo_host.is_some() || cfg.duo_ikey.is_some() || cfg.duo_skey.is_some() || cfg.duo_akey.is_some()) + && !(cfg.duo_host.is_some() && cfg.duo_ikey.is_some() && cfg.duo_skey.is_some() && cfg.duo_akey.is_some()) + { + err!("All Duo options need to be set for Duo support") + } + if cfg.yubico_client_id.is_some() != cfg.yubico_secret_key.is_some() { err!("Both `YUBICO_CLIENT_ID` and `YUBICO_SECRET_KEY` need to be set for Yubikey OTP support") }