committed by
							
								
								GitHub
							
						
					
				
				 13 changed files with 1674 additions and 1064 deletions
			
			
		
								
									
										File diff suppressed because it is too large
									
								
							
						
					@ -0,0 +1,120 @@ | 
				
			|||||
 | 
					use data_encoding::BASE32; | 
				
			||||
 | 
					use rocket::Route; | 
				
			||||
 | 
					use rocket_contrib::json::Json; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					use crate::api::core::two_factor::_generate_recover_code; | 
				
			||||
 | 
					use crate::api::{EmptyResult, JsonResult, JsonUpcase, NumberOrString, PasswordData}; | 
				
			||||
 | 
					use crate::auth::Headers; | 
				
			||||
 | 
					use crate::crypto; | 
				
			||||
 | 
					use crate::db::{ | 
				
			||||
 | 
					    models::{TwoFactor, TwoFactorType}, | 
				
			||||
 | 
					    DbConn, | 
				
			||||
 | 
					}; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					pub fn routes() -> Vec<Route> { | 
				
			||||
 | 
					    routes![ | 
				
			||||
 | 
					        generate_authenticator, | 
				
			||||
 | 
					        activate_authenticator, | 
				
			||||
 | 
					        activate_authenticator_put, | 
				
			||||
 | 
					    ] | 
				
			||||
 | 
					} | 
				
			||||
 | 
					#[post("/two-factor/get-authenticator", data = "<data>")] | 
				
			||||
 | 
					fn generate_authenticator(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> JsonResult { | 
				
			||||
 | 
					    let data: PasswordData = data.into_inner().data; | 
				
			||||
 | 
					    let user = headers.user; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    if !user.check_valid_password(&data.MasterPasswordHash) { | 
				
			||||
 | 
					        err!("Invalid password"); | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let type_ = TwoFactorType::Authenticator as i32; | 
				
			||||
 | 
					    let twofactor = TwoFactor::find_by_user_and_type(&user.uuid, type_, &conn); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let (enabled, key) = match twofactor { | 
				
			||||
 | 
					        Some(tf) => (true, tf.data), | 
				
			||||
 | 
					        _ => (false, BASE32.encode(&crypto::get_random(vec![0u8; 20]))), | 
				
			||||
 | 
					    }; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    Ok(Json(json!({ | 
				
			||||
 | 
					        "Enabled": enabled, | 
				
			||||
 | 
					        "Key": key, | 
				
			||||
 | 
					        "Object": "twoFactorAuthenticator" | 
				
			||||
 | 
					    }))) | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					#[derive(Deserialize, Debug)] | 
				
			||||
 | 
					#[allow(non_snake_case)] | 
				
			||||
 | 
					struct EnableAuthenticatorData { | 
				
			||||
 | 
					    MasterPasswordHash: String, | 
				
			||||
 | 
					    Key: String, | 
				
			||||
 | 
					    Token: NumberOrString, | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					#[post("/two-factor/authenticator", data = "<data>")] | 
				
			||||
 | 
					fn activate_authenticator(data: JsonUpcase<EnableAuthenticatorData>, headers: Headers, conn: DbConn) -> JsonResult { | 
				
			||||
 | 
					    let data: EnableAuthenticatorData = data.into_inner().data; | 
				
			||||
 | 
					    let password_hash = data.MasterPasswordHash; | 
				
			||||
 | 
					    let key = data.Key; | 
				
			||||
 | 
					    let token = data.Token.into_i32()? as u64; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let mut user = headers.user; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    if !user.check_valid_password(&password_hash) { | 
				
			||||
 | 
					        err!("Invalid password"); | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    // Validate key as base32 and 20 bytes length
 | 
				
			||||
 | 
					    let decoded_key: Vec<u8> = match BASE32.decode(key.as_bytes()) { | 
				
			||||
 | 
					        Ok(decoded) => decoded, | 
				
			||||
 | 
					        _ => err!("Invalid totp secret"), | 
				
			||||
 | 
					    }; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    if decoded_key.len() != 20 { | 
				
			||||
 | 
					        err!("Invalid key length") | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let type_ = TwoFactorType::Authenticator; | 
				
			||||
 | 
					    let twofactor = TwoFactor::new(user.uuid.clone(), type_, key.to_uppercase()); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    // Validate the token provided with the key
 | 
				
			||||
 | 
					    validate_totp_code(token, &twofactor.data)?; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    _generate_recover_code(&mut user, &conn); | 
				
			||||
 | 
					    twofactor.save(&conn)?; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    Ok(Json(json!({ | 
				
			||||
 | 
					        "Enabled": true, | 
				
			||||
 | 
					        "Key": key, | 
				
			||||
 | 
					        "Object": "twoFactorAuthenticator" | 
				
			||||
 | 
					    }))) | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					#[put("/two-factor/authenticator", data = "<data>")] | 
				
			||||
 | 
					fn activate_authenticator_put(data: JsonUpcase<EnableAuthenticatorData>, headers: Headers, conn: DbConn) -> JsonResult { | 
				
			||||
 | 
					    activate_authenticator(data, headers, conn) | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					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 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(()) | 
				
			||||
 | 
					} | 
				
			||||
@ -0,0 +1,346 @@ | 
				
			|||||
 | 
					use chrono::Utc; | 
				
			||||
 | 
					use data_encoding::BASE64; | 
				
			||||
 | 
					use rocket::Route; | 
				
			||||
 | 
					use rocket_contrib::json::Json; | 
				
			||||
 | 
					use serde_json; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					use crate::api::{ApiResult, EmptyResult, JsonResult, JsonUpcase, PasswordData}; | 
				
			||||
 | 
					use crate::auth::Headers; | 
				
			||||
 | 
					use crate::crypto; | 
				
			||||
 | 
					use crate::db::{ | 
				
			||||
 | 
					    models::{TwoFactor, TwoFactorType, User}, | 
				
			||||
 | 
					    DbConn, | 
				
			||||
 | 
					}; | 
				
			||||
 | 
					use crate::error::MapResult; | 
				
			||||
 | 
					use crate::CONFIG; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					pub fn routes() -> Vec<Route> { | 
				
			||||
 | 
					    routes![ | 
				
			||||
 | 
					        get_duo, | 
				
			||||
 | 
					        activate_duo, | 
				
			||||
 | 
					        activate_duo_put, | 
				
			||||
 | 
					    ] | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					#[derive(Serialize, Deserialize)] | 
				
			||||
 | 
					struct DuoData { | 
				
			||||
 | 
					    host: String, | 
				
			||||
 | 
					    ik: String, | 
				
			||||
 | 
					    sk: String, | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					impl DuoData { | 
				
			||||
 | 
					    fn global() -> Option<Self> { | 
				
			||||
 | 
					        match CONFIG.duo_host() { | 
				
			||||
 | 
					            Some(host) => Some(Self { | 
				
			||||
 | 
					                host, | 
				
			||||
 | 
					                ik: CONFIG.duo_ikey().unwrap(), | 
				
			||||
 | 
					                sk: CONFIG.duo_skey().unwrap(), | 
				
			||||
 | 
					            }), | 
				
			||||
 | 
					            None => None, | 
				
			||||
 | 
					        } | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					    fn msg(s: &str) -> Self { | 
				
			||||
 | 
					        Self { | 
				
			||||
 | 
					            host: s.into(), | 
				
			||||
 | 
					            ik: s.into(), | 
				
			||||
 | 
					            sk: s.into(), | 
				
			||||
 | 
					        } | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					    fn secret() -> Self { | 
				
			||||
 | 
					        Self::msg("<global_secret>") | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					    fn obscure(self) -> Self { | 
				
			||||
 | 
					        let mut host = self.host; | 
				
			||||
 | 
					        let mut ik = self.ik; | 
				
			||||
 | 
					        let mut sk = self.sk; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        let digits = 4; | 
				
			||||
 | 
					        let replaced = "************"; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        host.replace_range(digits.., replaced); | 
				
			||||
 | 
					        ik.replace_range(digits.., replaced); | 
				
			||||
 | 
					        sk.replace_range(digits.., replaced); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        Self { host, ik, sk } | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					enum DuoStatus { | 
				
			||||
 | 
					    Global(DuoData), | 
				
			||||
 | 
					    // Using the global duo config
 | 
				
			||||
 | 
					    User(DuoData), | 
				
			||||
 | 
					    // Using the user's config
 | 
				
			||||
 | 
					    Disabled(bool), // True if there is a global setting
 | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					impl DuoStatus { | 
				
			||||
 | 
					    fn data(self) -> Option<DuoData> { | 
				
			||||
 | 
					        match self { | 
				
			||||
 | 
					            DuoStatus::Global(data) => Some(data), | 
				
			||||
 | 
					            DuoStatus::User(data) => Some(data), | 
				
			||||
 | 
					            DuoStatus::Disabled(_) => None, | 
				
			||||
 | 
					        } | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					const DISABLED_MESSAGE_DEFAULT: &str = "<To use the global Duo keys, please leave these fields untouched>"; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					#[post("/two-factor/get-duo", data = "<data>")] | 
				
			||||
 | 
					fn get_duo(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> JsonResult { | 
				
			||||
 | 
					    let data: PasswordData = data.into_inner().data; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    if !headers.user.check_valid_password(&data.MasterPasswordHash) { | 
				
			||||
 | 
					        err!("Invalid password"); | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let data = get_user_duo_data(&headers.user.uuid, &conn); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let (enabled, data) = match data { | 
				
			||||
 | 
					        DuoStatus::Global(_) => (true, Some(DuoData::secret())), | 
				
			||||
 | 
					        DuoStatus::User(data) => (true, Some(data.obscure())), | 
				
			||||
 | 
					        DuoStatus::Disabled(true) => (false, Some(DuoData::msg(DISABLED_MESSAGE_DEFAULT))), | 
				
			||||
 | 
					        DuoStatus::Disabled(false) => (false, None), | 
				
			||||
 | 
					    }; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let json = if let Some(data) = data { | 
				
			||||
 | 
					        json!({ | 
				
			||||
 | 
					            "Enabled": enabled, | 
				
			||||
 | 
					            "Host": data.host, | 
				
			||||
 | 
					            "SecretKey": data.sk, | 
				
			||||
 | 
					            "IntegrationKey": data.ik, | 
				
			||||
 | 
					            "Object": "twoFactorDuo" | 
				
			||||
 | 
					        }) | 
				
			||||
 | 
					    } else { | 
				
			||||
 | 
					        json!({ | 
				
			||||
 | 
					            "Enabled": enabled, | 
				
			||||
 | 
					            "Object": "twoFactorDuo" | 
				
			||||
 | 
					        }) | 
				
			||||
 | 
					    }; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    Ok(Json(json)) | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					#[derive(Deserialize)] | 
				
			||||
 | 
					#[allow(non_snake_case, dead_code)] | 
				
			||||
 | 
					struct EnableDuoData { | 
				
			||||
 | 
					    MasterPasswordHash: String, | 
				
			||||
 | 
					    Host: String, | 
				
			||||
 | 
					    SecretKey: String, | 
				
			||||
 | 
					    IntegrationKey: String, | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					impl From<EnableDuoData> for DuoData { | 
				
			||||
 | 
					    fn from(d: EnableDuoData) -> Self { | 
				
			||||
 | 
					        Self { | 
				
			||||
 | 
					            host: d.Host, | 
				
			||||
 | 
					            ik: d.IntegrationKey, | 
				
			||||
 | 
					            sk: d.SecretKey, | 
				
			||||
 | 
					        } | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					fn check_duo_fields_custom(data: &EnableDuoData) -> bool { | 
				
			||||
 | 
					    fn empty_or_default(s: &str) -> bool { | 
				
			||||
 | 
					        let st = s.trim(); | 
				
			||||
 | 
					        st.is_empty() || s == DISABLED_MESSAGE_DEFAULT | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    !empty_or_default(&data.Host) && !empty_or_default(&data.SecretKey) && !empty_or_default(&data.IntegrationKey) | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					#[post("/two-factor/duo", data = "<data>")] | 
				
			||||
 | 
					fn activate_duo(data: JsonUpcase<EnableDuoData>, headers: Headers, conn: DbConn) -> JsonResult { | 
				
			||||
 | 
					    let data: EnableDuoData = data.into_inner().data; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    if !headers.user.check_valid_password(&data.MasterPasswordHash) { | 
				
			||||
 | 
					        err!("Invalid password"); | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let (data, data_str) = if check_duo_fields_custom(&data) { | 
				
			||||
 | 
					        let data_req: DuoData = data.into(); | 
				
			||||
 | 
					        let data_str = serde_json::to_string(&data_req)?; | 
				
			||||
 | 
					        duo_api_request("GET", "/auth/v2/check", "", &data_req).map_res("Failed to validate Duo credentials")?; | 
				
			||||
 | 
					        (data_req.obscure(), data_str) | 
				
			||||
 | 
					    } else { | 
				
			||||
 | 
					        (DuoData::secret(), String::new()) | 
				
			||||
 | 
					    }; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let type_ = TwoFactorType::Duo; | 
				
			||||
 | 
					    let twofactor = TwoFactor::new(headers.user.uuid.clone(), type_, data_str); | 
				
			||||
 | 
					    twofactor.save(&conn)?; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    Ok(Json(json!({ | 
				
			||||
 | 
					        "Enabled": true, | 
				
			||||
 | 
					        "Host": data.host, | 
				
			||||
 | 
					        "SecretKey": data.sk, | 
				
			||||
 | 
					        "IntegrationKey": data.ik, | 
				
			||||
 | 
					        "Object": "twoFactorDuo" | 
				
			||||
 | 
					    }))) | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					#[put("/two-factor/duo", data = "<data>")] | 
				
			||||
 | 
					fn activate_duo_put(data: JsonUpcase<EnableDuoData>, headers: Headers, conn: DbConn) -> JsonResult { | 
				
			||||
 | 
					    activate_duo(data, headers, conn) | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					fn duo_api_request(method: &str, path: &str, params: &str, data: &DuoData) -> EmptyResult { | 
				
			||||
 | 
					    const AGENT: &str = "bitwarden_rs:Duo/1.0 (Rust)"; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    use reqwest::{header::*, Client, Method}; | 
				
			||||
 | 
					    use std::str::FromStr; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let url = format!("https://{}{}", &data.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 m = Method::from_str(method).unwrap_or_default(); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    Client::new() | 
				
			||||
 | 
					        .request(m, &url) | 
				
			||||
 | 
					        .basic_auth(username, Some(password)) | 
				
			||||
 | 
					        .header(USER_AGENT, AGENT) | 
				
			||||
 | 
					        .header(DATE, date) | 
				
			||||
 | 
					        .send()? | 
				
			||||
 | 
					        .error_for_status()?; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    Ok(()) | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					const DUO_EXPIRE: i64 = 300; | 
				
			||||
 | 
					const APP_EXPIRE: i64 = 3600; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					const AUTH_PREFIX: &str = "AUTH"; | 
				
			||||
 | 
					const DUO_PREFIX: &str = "TX"; | 
				
			||||
 | 
					const APP_PREFIX: &str = "APP"; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					fn get_user_duo_data(uuid: &str, conn: &DbConn) -> DuoStatus { | 
				
			||||
 | 
					    let type_ = TwoFactorType::Duo as i32; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    // If the user doesn't have an entry, disabled
 | 
				
			||||
 | 
					    let twofactor = match TwoFactor::find_by_user_and_type(uuid, type_, &conn) { | 
				
			||||
 | 
					        Some(t) => t, | 
				
			||||
 | 
					        None => return DuoStatus::Disabled(DuoData::global().is_some()), | 
				
			||||
 | 
					    }; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    // If the user has the required values, we use those
 | 
				
			||||
 | 
					    if let Ok(data) = serde_json::from_str(&twofactor.data) { | 
				
			||||
 | 
					        return DuoStatus::User(data); | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    // Otherwise, we try to use the globals
 | 
				
			||||
 | 
					    if let Some(global) = DuoData::global() { | 
				
			||||
 | 
					        return DuoStatus::Global(global); | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    // If there are no globals configured, just disable it
 | 
				
			||||
 | 
					    DuoStatus::Disabled(false) | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					// let (ik, sk, ak, host) = get_duo_keys();
 | 
				
			||||
 | 
					fn get_duo_keys_email(email: &str, conn: &DbConn) -> ApiResult<(String, String, String, String)> { | 
				
			||||
 | 
					    let data = User::find_by_mail(email, &conn) | 
				
			||||
 | 
					        .and_then(|u| get_user_duo_data(&u.uuid, &conn).data()) | 
				
			||||
 | 
					        .or_else(DuoData::global) | 
				
			||||
 | 
					        .map_res("Can't fetch Duo keys")?; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    Ok((data.ik, data.sk, CONFIG.get_duo_akey(), data.host)) | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					pub fn generate_duo_signature(email: &str, conn: &DbConn) -> ApiResult<(String, String)> { | 
				
			||||
 | 
					    let now = Utc::now().timestamp(); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let (ik, sk, ak, host) = get_duo_keys_email(email, conn)?; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    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); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    Ok((format!("{}:{}", duo_sign, app_sign), host)) | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					fn sign_duo_values(key: &str, email: &str, ikey: &str, prefix: &str, expire: i64) -> String { | 
				
			||||
 | 
					    let val = format!("{}|{}|{}", email, ikey, expire); | 
				
			||||
 | 
					    let cookie = format!("{}|{}", prefix, BASE64.encode(val.as_bytes())); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    format!("{}|{}", cookie, crypto::hmac_sign(key, &cookie)) | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					pub fn validate_duo_login(email: &str, response: &str, conn: &DbConn) -> EmptyResult { | 
				
			||||
 | 
					    let split: Vec<&str> = response.split(':').collect(); | 
				
			||||
 | 
					    if split.len() != 2 { | 
				
			||||
 | 
					        err!("Invalid response length"); | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let auth_sig = split[0]; | 
				
			||||
 | 
					    let app_sig = split[1]; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let now = Utc::now().timestamp(); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let (ik, sk, ak, _host) = get_duo_keys_email(email, conn)?; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    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) || !crypto::ct_eq(&auth_user, email) { | 
				
			||||
 | 
					        err!("Error validating duo authentication") | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    Ok(()) | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					fn parse_duo_values(key: &str, val: &str, ikey: &str, prefix: &str, time: i64) -> ApiResult<String> { | 
				
			||||
 | 
					    let split: Vec<&str> = val.split('|').collect(); | 
				
			||||
 | 
					    if split.len() != 3 { | 
				
			||||
 | 
					        err!("Invalid value length") | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let u_prefix = split[0]; | 
				
			||||
 | 
					    let u_b64 = split[1]; | 
				
			||||
 | 
					    let u_sig = split[2]; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let sig = crypto::hmac_sign(key, &format!("{}|{}", u_prefix, u_b64)); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    if !crypto::ct_eq(crypto::hmac_sign(key, &sig), crypto::hmac_sign(key, u_sig)) { | 
				
			||||
 | 
					        err!("Duo signatures don't match") | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    if u_prefix != prefix { | 
				
			||||
 | 
					        err!("Prefixes don't match") | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let cookie_vec = match BASE64.decode(u_b64.as_bytes()) { | 
				
			||||
 | 
					        Ok(c) => c, | 
				
			||||
 | 
					        Err(_) => err!("Invalid Duo cookie encoding"), | 
				
			||||
 | 
					    }; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let cookie = match String::from_utf8(cookie_vec) { | 
				
			||||
 | 
					        Ok(c) => c, | 
				
			||||
 | 
					        Err(_) => err!("Invalid Duo cookie encoding"), | 
				
			||||
 | 
					    }; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let cookie_split: Vec<&str> = cookie.split('|').collect(); | 
				
			||||
 | 
					    if cookie_split.len() != 3 { | 
				
			||||
 | 
					        err!("Invalid cookie length") | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let username = cookie_split[0]; | 
				
			||||
 | 
					    let u_ikey = cookie_split[1]; | 
				
			||||
 | 
					    let expire = cookie_split[2]; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    if !crypto::ct_eq(ikey, u_ikey) { | 
				
			||||
 | 
					        err!("Invalid ikey") | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let expire = match expire.parse() { | 
				
			||||
 | 
					        Ok(e) => e, | 
				
			||||
 | 
					        Err(_) => err!("Invalid expire time"), | 
				
			||||
 | 
					    }; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    if time >= expire { | 
				
			||||
 | 
					        err!("Expired authorization") | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    Ok(username.into()) | 
				
			||||
 | 
					} | 
				
			||||
@ -0,0 +1,341 @@ | 
				
			|||||
 | 
					use rocket::Route; | 
				
			||||
 | 
					use rocket_contrib::json::Json; | 
				
			||||
 | 
					use serde_json; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					use crate::api::{EmptyResult, JsonResult, JsonUpcase, PasswordData}; | 
				
			||||
 | 
					use crate::auth::Headers; | 
				
			||||
 | 
					use crate::crypto; | 
				
			||||
 | 
					use crate::db::{ | 
				
			||||
 | 
					    models::{TwoFactor, TwoFactorType}, | 
				
			||||
 | 
					    DbConn, | 
				
			||||
 | 
					}; | 
				
			||||
 | 
					use crate::error::Error; | 
				
			||||
 | 
					use crate::mail; | 
				
			||||
 | 
					use crate::CONFIG; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					use chrono::{Duration, NaiveDateTime, Utc}; | 
				
			||||
 | 
					use std::char; | 
				
			||||
 | 
					use std::ops::Add; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					pub fn routes() -> Vec<Route> { | 
				
			||||
 | 
					    routes![ | 
				
			||||
 | 
					        get_email, | 
				
			||||
 | 
					        send_email_login, | 
				
			||||
 | 
					        send_email, | 
				
			||||
 | 
					        email, | 
				
			||||
 | 
					    ] | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					#[derive(Deserialize)] | 
				
			||||
 | 
					#[allow(non_snake_case)] | 
				
			||||
 | 
					struct SendEmailLoginData { | 
				
			||||
 | 
					    Email: String, | 
				
			||||
 | 
					    MasterPasswordHash: String, | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					/// User is trying to login and wants to use email 2FA.
 | 
				
			||||
 | 
					/// Does not require Bearer token
 | 
				
			||||
 | 
					#[post("/two-factor/send-email-login", data = "<data>")] // JsonResult
 | 
				
			||||
 | 
					fn send_email_login(data: JsonUpcase<SendEmailLoginData>, conn: DbConn) -> EmptyResult { | 
				
			||||
 | 
					    let data: SendEmailLoginData = data.into_inner().data; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    use crate::db::models::User; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    // Get the user
 | 
				
			||||
 | 
					    let user = match User::find_by_mail(&data.Email, &conn) { | 
				
			||||
 | 
					        Some(user) => user, | 
				
			||||
 | 
					        None => err!("Username or password is incorrect. Try again."), | 
				
			||||
 | 
					    }; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    // Check password
 | 
				
			||||
 | 
					    if !user.check_valid_password(&data.MasterPasswordHash) { | 
				
			||||
 | 
					        err!("Username or password is incorrect. Try again.") | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    if !CONFIG._enable_email_2fa() { | 
				
			||||
 | 
					        err!("Email 2FA is disabled") | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let type_ = TwoFactorType::Email as i32; | 
				
			||||
 | 
					    let mut twofactor = TwoFactor::find_by_user_and_type(&user.uuid, type_, &conn)?; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let generated_token = generate_token(CONFIG.email_token_size())?; | 
				
			||||
 | 
					    let mut twofactor_data = EmailTokenData::from_json(&twofactor.data)?; | 
				
			||||
 | 
					    twofactor_data.set_token(generated_token); | 
				
			||||
 | 
					    twofactor.data = twofactor_data.to_json(); | 
				
			||||
 | 
					    twofactor.save(&conn)?; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    mail::send_token(&twofactor_data.email, &twofactor_data.last_token?)?; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    Ok(()) | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					/// When user clicks on Manage email 2FA show the user the related information
 | 
				
			||||
 | 
					#[post("/two-factor/get-email", data = "<data>")] | 
				
			||||
 | 
					fn get_email(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> JsonResult { | 
				
			||||
 | 
					    let data: PasswordData = data.into_inner().data; | 
				
			||||
 | 
					    let user = headers.user; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    if !user.check_valid_password(&data.MasterPasswordHash) { | 
				
			||||
 | 
					        err!("Invalid password"); | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let type_ = TwoFactorType::Email as i32; | 
				
			||||
 | 
					    let enabled = match TwoFactor::find_by_user_and_type(&user.uuid, type_, &conn) { | 
				
			||||
 | 
					        Some(x) => x.enabled, | 
				
			||||
 | 
					        _ => false, | 
				
			||||
 | 
					    }; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    Ok(Json(json!({ | 
				
			||||
 | 
					        "Email": user.email, | 
				
			||||
 | 
					        "Enabled": enabled, | 
				
			||||
 | 
					        "Object": "twoFactorEmail" | 
				
			||||
 | 
					    }))) | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					#[derive(Deserialize)] | 
				
			||||
 | 
					#[allow(non_snake_case)] | 
				
			||||
 | 
					struct SendEmailData { | 
				
			||||
 | 
					    /// Email where 2FA codes will be sent to, can be different than user email account.
 | 
				
			||||
 | 
					    Email: String, | 
				
			||||
 | 
					    MasterPasswordHash: String, | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					fn generate_token(token_size: u32) -> Result<String, Error> { | 
				
			||||
 | 
					    if token_size > 19 { | 
				
			||||
 | 
					        err!("Generating token failed") | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    // 8 bytes to create an u64 for up to 19 token digits
 | 
				
			||||
 | 
					    let bytes = crypto::get_random(vec![0; 8]); | 
				
			||||
 | 
					    let mut bytes_array = [0u8; 8]; | 
				
			||||
 | 
					    bytes_array.copy_from_slice(&bytes); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let number = u64::from_be_bytes(bytes_array) % 10u64.pow(token_size); | 
				
			||||
 | 
					    let token = format!("{:0size$}", number, size = token_size as usize); | 
				
			||||
 | 
					    Ok(token) | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					/// Send a verification email to the specified email address to check whether it exists/belongs to user.
 | 
				
			||||
 | 
					#[post("/two-factor/send-email", data = "<data>")] | 
				
			||||
 | 
					fn send_email(data: JsonUpcase<SendEmailData>, headers: Headers, conn: DbConn) -> EmptyResult { | 
				
			||||
 | 
					    let data: SendEmailData = data.into_inner().data; | 
				
			||||
 | 
					    let user = headers.user; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    if !user.check_valid_password(&data.MasterPasswordHash) { | 
				
			||||
 | 
					        err!("Invalid password"); | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    if !CONFIG._enable_email_2fa() { | 
				
			||||
 | 
					        err!("Email 2FA is disabled") | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let type_ = TwoFactorType::Email as i32; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    if let Some(tf) = TwoFactor::find_by_user_and_type(&user.uuid, type_, &conn) { | 
				
			||||
 | 
					        tf.delete(&conn)?; | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let generated_token = generate_token(CONFIG.email_token_size())?; | 
				
			||||
 | 
					    let twofactor_data = EmailTokenData::new(data.Email, generated_token); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    // Uses EmailVerificationChallenge as type to show that it's not verified yet.
 | 
				
			||||
 | 
					    let twofactor = TwoFactor::new( | 
				
			||||
 | 
					        user.uuid, | 
				
			||||
 | 
					        TwoFactorType::EmailVerificationChallenge, | 
				
			||||
 | 
					        twofactor_data.to_json(), | 
				
			||||
 | 
					    ); | 
				
			||||
 | 
					    twofactor.save(&conn)?; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    mail::send_token(&twofactor_data.email, &twofactor_data.last_token?)?; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    Ok(()) | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					#[derive(Deserialize, Serialize)] | 
				
			||||
 | 
					#[allow(non_snake_case)] | 
				
			||||
 | 
					struct EmailData { | 
				
			||||
 | 
					    Email: String, | 
				
			||||
 | 
					    MasterPasswordHash: String, | 
				
			||||
 | 
					    Token: String, | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					/// Verify email belongs to user and can be used for 2FA email codes.
 | 
				
			||||
 | 
					#[put("/two-factor/email", data = "<data>")] | 
				
			||||
 | 
					fn email(data: JsonUpcase<EmailData>, headers: Headers, conn: DbConn) -> JsonResult { | 
				
			||||
 | 
					    let data: EmailData = data.into_inner().data; | 
				
			||||
 | 
					    let user = headers.user; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    if !user.check_valid_password(&data.MasterPasswordHash) { | 
				
			||||
 | 
					        err!("Invalid password"); | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let type_ = TwoFactorType::EmailVerificationChallenge as i32; | 
				
			||||
 | 
					    let mut twofactor = TwoFactor::find_by_user_and_type(&user.uuid, type_, &conn)?; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let mut email_data = EmailTokenData::from_json(&twofactor.data)?; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let issued_token = match &email_data.last_token { | 
				
			||||
 | 
					        Some(t) => t, | 
				
			||||
 | 
					        _ => err!("No token available"), | 
				
			||||
 | 
					    }; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    if !crypto::ct_eq(issued_token, data.Token) { | 
				
			||||
 | 
					        err!("Token is invalid") | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    email_data.reset_token(); | 
				
			||||
 | 
					    twofactor.atype = TwoFactorType::Email as i32; | 
				
			||||
 | 
					    twofactor.data = email_data.to_json(); | 
				
			||||
 | 
					    twofactor.save(&conn)?; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    Ok(Json(json!({ | 
				
			||||
 | 
					        "Email": email_data.email, | 
				
			||||
 | 
					        "Enabled": "true", | 
				
			||||
 | 
					        "Object": "twoFactorEmail" | 
				
			||||
 | 
					    }))) | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					/// Validate the email code when used as TwoFactor token mechanism
 | 
				
			||||
 | 
					pub fn validate_email_code_str(user_uuid: &str, token: &str, data: &str, conn: &DbConn) -> EmptyResult { | 
				
			||||
 | 
					    let mut email_data = EmailTokenData::from_json(&data)?; | 
				
			||||
 | 
					    let mut twofactor = TwoFactor::find_by_user_and_type(&user_uuid, TwoFactorType::Email as i32, &conn)?; | 
				
			||||
 | 
					    let issued_token = match &email_data.last_token { | 
				
			||||
 | 
					        Some(t) => t, | 
				
			||||
 | 
					        _ => err!("No token available"), | 
				
			||||
 | 
					    }; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    if !crypto::ct_eq(issued_token, token) { | 
				
			||||
 | 
					        email_data.add_attempt(); | 
				
			||||
 | 
					        if email_data.attempts >= CONFIG.email_attempts_limit() { | 
				
			||||
 | 
					            email_data.reset_token(); | 
				
			||||
 | 
					        } | 
				
			||||
 | 
					        twofactor.data = email_data.to_json(); | 
				
			||||
 | 
					        twofactor.save(&conn)?; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        err!("Token is invalid") | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    email_data.reset_token(); | 
				
			||||
 | 
					    twofactor.data = email_data.to_json(); | 
				
			||||
 | 
					    twofactor.save(&conn)?; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let date = NaiveDateTime::from_timestamp(email_data.token_sent, 0); | 
				
			||||
 | 
					    let max_time = CONFIG.email_expiration_time() as i64; | 
				
			||||
 | 
					    if date.add(Duration::seconds(max_time)) < Utc::now().naive_utc() { | 
				
			||||
 | 
					        err!("Token has expired") | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    Ok(()) | 
				
			||||
 | 
					} | 
				
			||||
 | 
					/// Data stored in the TwoFactor table in the db
 | 
				
			||||
 | 
					#[derive(Serialize, Deserialize)] | 
				
			||||
 | 
					pub struct EmailTokenData { | 
				
			||||
 | 
					    /// Email address where the token will be sent to. Can be different from account email.
 | 
				
			||||
 | 
					    pub email: String, | 
				
			||||
 | 
					    /// Some(token): last valid token issued that has not been entered.
 | 
				
			||||
 | 
					    /// None: valid token was used and removed.
 | 
				
			||||
 | 
					    pub last_token: Option<String>, | 
				
			||||
 | 
					    /// UNIX timestamp of token issue.
 | 
				
			||||
 | 
					    pub token_sent: i64, | 
				
			||||
 | 
					    /// Amount of token entry attempts for last_token.
 | 
				
			||||
 | 
					    pub attempts: u64, | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					impl EmailTokenData { | 
				
			||||
 | 
					    pub fn new(email: String, token: String) -> EmailTokenData { | 
				
			||||
 | 
					        EmailTokenData { | 
				
			||||
 | 
					            email, | 
				
			||||
 | 
					            last_token: Some(token), | 
				
			||||
 | 
					            token_sent: Utc::now().naive_utc().timestamp(), | 
				
			||||
 | 
					            attempts: 0, | 
				
			||||
 | 
					        } | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    pub fn set_token(&mut self, token: String) { | 
				
			||||
 | 
					        self.last_token = Some(token); | 
				
			||||
 | 
					        self.token_sent = Utc::now().naive_utc().timestamp(); | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    pub fn reset_token(&mut self) { | 
				
			||||
 | 
					        self.last_token = None; | 
				
			||||
 | 
					        self.attempts = 0; | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    pub fn add_attempt(&mut self) { | 
				
			||||
 | 
					        self.attempts = self.attempts + 1; | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    pub fn to_json(&self) -> String { | 
				
			||||
 | 
					        serde_json::to_string(&self).unwrap() | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    pub fn from_json(string: &str) -> Result<EmailTokenData, Error> { | 
				
			||||
 | 
					        let res: Result<EmailTokenData, crate::serde_json::Error> = serde_json::from_str(&string); | 
				
			||||
 | 
					        match res { | 
				
			||||
 | 
					            Ok(x) => Ok(x), | 
				
			||||
 | 
					            Err(_) => err!("Could not decode EmailTokenData from string"), | 
				
			||||
 | 
					        } | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					/// Takes an email address and obscures it by replacing it with asterisks except two characters.
 | 
				
			||||
 | 
					pub fn obscure_email(email: &str) -> String { | 
				
			||||
 | 
					    let split: Vec<&str> = email.split("@").collect(); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let mut name = split[0].to_string(); | 
				
			||||
 | 
					    let domain = &split[1]; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let name_size = name.chars().count(); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let new_name = match name_size { | 
				
			||||
 | 
					        1..=3 => "*".repeat(name_size), | 
				
			||||
 | 
					        _ => { | 
				
			||||
 | 
					            let stars = "*".repeat(name_size - 2); | 
				
			||||
 | 
					            name.truncate(2); | 
				
			||||
 | 
					            format!("{}{}", name, stars) | 
				
			||||
 | 
					        } | 
				
			||||
 | 
					    }; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    format!("{}@{}", new_name, &domain) | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					#[cfg(test)] | 
				
			||||
 | 
					mod tests { | 
				
			||||
 | 
					    use super::*; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    #[test] | 
				
			||||
 | 
					    fn test_obscure_email_long() { | 
				
			||||
 | 
					        let email = "bytes@example.ext"; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        let result = obscure_email(&email); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        // Only first two characters should be visible.
 | 
				
			||||
 | 
					        assert_eq!(result, "by***@example.ext"); | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    #[test] | 
				
			||||
 | 
					    fn test_obscure_email_short() { | 
				
			||||
 | 
					        let email = "byt@example.ext"; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        let result = obscure_email(&email); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        // If it's smaller than 3 characters it should only show asterisks.
 | 
				
			||||
 | 
					        assert_eq!(result, "***@example.ext"); | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    #[test] | 
				
			||||
 | 
					    fn test_token() { | 
				
			||||
 | 
					        let result = generate_token(19).unwrap(); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        assert_eq!(result.chars().count(), 19); | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    #[test] | 
				
			||||
 | 
					    fn test_token_too_large() { | 
				
			||||
 | 
					        let result = generate_token(20); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        assert!(result.is_err(), "too large token should give an error"); | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					} | 
				
			||||
@ -0,0 +1,146 @@ | 
				
			|||||
 | 
					use data_encoding::BASE32; | 
				
			||||
 | 
					use rocket::Route; | 
				
			||||
 | 
					use rocket_contrib::json::Json; | 
				
			||||
 | 
					use serde_json; | 
				
			||||
 | 
					use serde_json::Value; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					use crate::api::{JsonResult, JsonUpcase, NumberOrString, PasswordData}; | 
				
			||||
 | 
					use crate::auth::Headers; | 
				
			||||
 | 
					use crate::crypto; | 
				
			||||
 | 
					use crate::db::{ | 
				
			||||
 | 
					    models::{TwoFactor, User}, | 
				
			||||
 | 
					    DbConn, | 
				
			||||
 | 
					}; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					pub(crate) mod authenticator; | 
				
			||||
 | 
					pub(crate) mod duo; | 
				
			||||
 | 
					pub(crate) mod email; | 
				
			||||
 | 
					pub(crate) mod u2f; | 
				
			||||
 | 
					pub(crate) mod yubikey; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					pub fn routes() -> Vec<Route> { | 
				
			||||
 | 
					    let mut routes = routes![ | 
				
			||||
 | 
					        get_twofactor, | 
				
			||||
 | 
					        get_recover, | 
				
			||||
 | 
					        recover, | 
				
			||||
 | 
					        disable_twofactor, | 
				
			||||
 | 
					        disable_twofactor_put, | 
				
			||||
 | 
					    ]; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    routes.append(&mut authenticator::routes()); | 
				
			||||
 | 
					    routes.append(&mut duo::routes()); | 
				
			||||
 | 
					    routes.append(&mut email::routes()); | 
				
			||||
 | 
					    routes.append(&mut u2f::routes()); | 
				
			||||
 | 
					    routes.append(&mut yubikey::routes()); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    routes | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					#[get("/two-factor")] | 
				
			||||
 | 
					fn get_twofactor(headers: Headers, conn: DbConn) -> JsonResult { | 
				
			||||
 | 
					    let twofactors = TwoFactor::find_by_user(&headers.user.uuid, &conn); | 
				
			||||
 | 
					    let twofactors_json: Vec<Value> = twofactors.iter().map(TwoFactor::to_json_list).collect(); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    Ok(Json(json!({ | 
				
			||||
 | 
					        "Data": twofactors_json, | 
				
			||||
 | 
					        "Object": "list", | 
				
			||||
 | 
					        "ContinuationToken": null, | 
				
			||||
 | 
					    }))) | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					#[post("/two-factor/get-recover", data = "<data>")] | 
				
			||||
 | 
					fn get_recover(data: JsonUpcase<PasswordData>, headers: Headers) -> JsonResult { | 
				
			||||
 | 
					    let data: PasswordData = data.into_inner().data; | 
				
			||||
 | 
					    let user = headers.user; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    if !user.check_valid_password(&data.MasterPasswordHash) { | 
				
			||||
 | 
					        err!("Invalid password"); | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    Ok(Json(json!({ | 
				
			||||
 | 
					        "Code": user.totp_recover, | 
				
			||||
 | 
					        "Object": "twoFactorRecover" | 
				
			||||
 | 
					    }))) | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					#[derive(Deserialize)] | 
				
			||||
 | 
					#[allow(non_snake_case)] | 
				
			||||
 | 
					struct RecoverTwoFactor { | 
				
			||||
 | 
					    MasterPasswordHash: String, | 
				
			||||
 | 
					    Email: String, | 
				
			||||
 | 
					    RecoveryCode: String, | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					#[post("/two-factor/recover", data = "<data>")] | 
				
			||||
 | 
					fn recover(data: JsonUpcase<RecoverTwoFactor>, conn: DbConn) -> JsonResult { | 
				
			||||
 | 
					    let data: RecoverTwoFactor = data.into_inner().data; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    use crate::db::models::User; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    // Get the user
 | 
				
			||||
 | 
					    let mut user = match User::find_by_mail(&data.Email, &conn) { | 
				
			||||
 | 
					        Some(user) => user, | 
				
			||||
 | 
					        None => err!("Username or password is incorrect. Try again."), | 
				
			||||
 | 
					    }; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    // Check password
 | 
				
			||||
 | 
					    if !user.check_valid_password(&data.MasterPasswordHash) { | 
				
			||||
 | 
					        err!("Username or password is incorrect. Try again.") | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    // Check if recovery code is correct
 | 
				
			||||
 | 
					    if !user.check_valid_recovery_code(&data.RecoveryCode) { | 
				
			||||
 | 
					        err!("Recovery code is incorrect. Try again.") | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    // Remove all twofactors from the user
 | 
				
			||||
 | 
					    TwoFactor::delete_all_by_user(&user.uuid, &conn)?; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    // Remove the recovery code, not needed without twofactors
 | 
				
			||||
 | 
					    user.totp_recover = None; | 
				
			||||
 | 
					    user.save(&conn)?; | 
				
			||||
 | 
					    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 { | 
				
			||||
 | 
					    MasterPasswordHash: String, | 
				
			||||
 | 
					    Type: NumberOrString, | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					#[post("/two-factor/disable", data = "<data>")] | 
				
			||||
 | 
					fn disable_twofactor(data: JsonUpcase<DisableTwoFactorData>, headers: Headers, conn: DbConn) -> JsonResult { | 
				
			||||
 | 
					    let data: DisableTwoFactorData = data.into_inner().data; | 
				
			||||
 | 
					    let password_hash = data.MasterPasswordHash; | 
				
			||||
 | 
					    let user = headers.user; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    if !user.check_valid_password(&password_hash) { | 
				
			||||
 | 
					        err!("Invalid password"); | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let type_ = data.Type.into_i32()?; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    if let Some(twofactor) = TwoFactor::find_by_user_and_type(&user.uuid, type_, &conn) { | 
				
			||||
 | 
					        twofactor.delete(&conn)?; | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    Ok(Json(json!({ | 
				
			||||
 | 
					        "Enabled": false, | 
				
			||||
 | 
					        "Type": type_, | 
				
			||||
 | 
					        "Object": "twoFactorProvider" | 
				
			||||
 | 
					    }))) | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					#[put("/two-factor/disable", data = "<data>")] | 
				
			||||
 | 
					fn disable_twofactor_put(data: JsonUpcase<DisableTwoFactorData>, headers: Headers, conn: DbConn) -> JsonResult { | 
				
			||||
 | 
					    disable_twofactor(data, headers, conn) | 
				
			||||
 | 
					} | 
				
			||||
@ -0,0 +1,315 @@ | 
				
			|||||
 | 
					use rocket::Route; | 
				
			||||
 | 
					use rocket_contrib::json::Json; | 
				
			||||
 | 
					use serde_json; | 
				
			||||
 | 
					use serde_json::Value; | 
				
			||||
 | 
					use u2f::messages::{RegisterResponse, SignResponse, U2fSignRequest}; | 
				
			||||
 | 
					use u2f::protocol::{Challenge, U2f}; | 
				
			||||
 | 
					use u2f::register::Registration; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					use crate::api::core::two_factor::_generate_recover_code; | 
				
			||||
 | 
					use crate::api::{ApiResult, EmptyResult, JsonResult, JsonUpcase, NumberOrString, PasswordData}; | 
				
			||||
 | 
					use crate::auth::Headers; | 
				
			||||
 | 
					use crate::db::{ | 
				
			||||
 | 
					    models::{TwoFactor, TwoFactorType}, | 
				
			||||
 | 
					    DbConn, | 
				
			||||
 | 
					}; | 
				
			||||
 | 
					use crate::error::Error; | 
				
			||||
 | 
					use crate::CONFIG; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					const U2F_VERSION: &str = "U2F_V2"; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					lazy_static! { | 
				
			||||
 | 
					    static ref APP_ID: String = format!("{}/app-id.json", &CONFIG.domain()); | 
				
			||||
 | 
					    static ref U2F: U2f = U2f::new(APP_ID.clone()); | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					pub fn routes() -> Vec<Route> { | 
				
			||||
 | 
					    routes![ | 
				
			||||
 | 
					        generate_u2f, | 
				
			||||
 | 
					        generate_u2f_challenge, | 
				
			||||
 | 
					        activate_u2f, | 
				
			||||
 | 
					        activate_u2f_put, | 
				
			||||
 | 
					    ] | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					#[post("/two-factor/get-u2f", data = "<data>")] | 
				
			||||
 | 
					fn generate_u2f(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> JsonResult { | 
				
			||||
 | 
					    if !CONFIG.domain_set() { | 
				
			||||
 | 
					        err!("`DOMAIN` environment variable is not set. U2F disabled") | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					    let data: PasswordData = data.into_inner().data; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    if !headers.user.check_valid_password(&data.MasterPasswordHash) { | 
				
			||||
 | 
					        err!("Invalid password"); | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let (enabled, keys) = get_u2f_registrations(&headers.user.uuid, &conn)?; | 
				
			||||
 | 
					    let keys_json: Vec<Value> = keys.iter().map(U2FRegistration::to_json).collect(); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    Ok(Json(json!({ | 
				
			||||
 | 
					        "Enabled": enabled, | 
				
			||||
 | 
					        "Keys": keys_json, | 
				
			||||
 | 
					        "Object": "twoFactorU2f" | 
				
			||||
 | 
					    }))) | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					#[post("/two-factor/get-u2f-challenge", data = "<data>")] | 
				
			||||
 | 
					fn generate_u2f_challenge(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> JsonResult { | 
				
			||||
 | 
					    let data: PasswordData = data.into_inner().data; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    if !headers.user.check_valid_password(&data.MasterPasswordHash) { | 
				
			||||
 | 
					        err!("Invalid password"); | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let _type = TwoFactorType::U2fRegisterChallenge; | 
				
			||||
 | 
					    let challenge = _create_u2f_challenge(&headers.user.uuid, _type, &conn).challenge; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    Ok(Json(json!({ | 
				
			||||
 | 
					        "UserId": headers.user.uuid, | 
				
			||||
 | 
					        "AppId": APP_ID.to_string(), | 
				
			||||
 | 
					        "Challenge": challenge, | 
				
			||||
 | 
					        "Version": U2F_VERSION, | 
				
			||||
 | 
					    }))) | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					#[derive(Deserialize, Debug)] | 
				
			||||
 | 
					#[allow(non_snake_case)] | 
				
			||||
 | 
					struct EnableU2FData { | 
				
			||||
 | 
					    Id: NumberOrString, | 
				
			||||
 | 
					    // 1..5
 | 
				
			||||
 | 
					    Name: String, | 
				
			||||
 | 
					    MasterPasswordHash: String, | 
				
			||||
 | 
					    DeviceResponse: String, | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					// This struct is referenced from the U2F lib
 | 
				
			||||
 | 
					// because it doesn't implement Deserialize
 | 
				
			||||
 | 
					#[derive(Serialize, Deserialize)] | 
				
			||||
 | 
					#[serde(rename_all = "camelCase")] | 
				
			||||
 | 
					#[serde(remote = "Registration")] | 
				
			||||
 | 
					struct RegistrationDef { | 
				
			||||
 | 
					    key_handle: Vec<u8>, | 
				
			||||
 | 
					    pub_key: Vec<u8>, | 
				
			||||
 | 
					    attestation_cert: Option<Vec<u8>>, | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					#[derive(Serialize, Deserialize)] | 
				
			||||
 | 
					struct U2FRegistration { | 
				
			||||
 | 
					    id: i32, | 
				
			||||
 | 
					    name: String, | 
				
			||||
 | 
					    #[serde(with = "RegistrationDef")] | 
				
			||||
 | 
					    reg: Registration, | 
				
			||||
 | 
					    counter: u32, | 
				
			||||
 | 
					    compromised: bool, | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					impl U2FRegistration { | 
				
			||||
 | 
					    fn to_json(&self) -> Value { | 
				
			||||
 | 
					        json!({ | 
				
			||||
 | 
					            "Id": self.id, | 
				
			||||
 | 
					            "Name": self.name, | 
				
			||||
 | 
					            "Compromised": self.compromised, | 
				
			||||
 | 
					        }) | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					// This struct is copied from the U2F lib
 | 
				
			||||
 | 
					// to add an optional error code
 | 
				
			||||
 | 
					#[derive(Deserialize)] | 
				
			||||
 | 
					#[serde(rename_all = "camelCase")] | 
				
			||||
 | 
					struct RegisterResponseCopy { | 
				
			||||
 | 
					    pub registration_data: String, | 
				
			||||
 | 
					    pub version: String, | 
				
			||||
 | 
					    pub client_data: String, | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    pub error_code: Option<NumberOrString>, | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					impl Into<RegisterResponse> for RegisterResponseCopy { | 
				
			||||
 | 
					    fn into(self) -> RegisterResponse { | 
				
			||||
 | 
					        RegisterResponse { | 
				
			||||
 | 
					            registration_data: self.registration_data, | 
				
			||||
 | 
					            version: self.version, | 
				
			||||
 | 
					            client_data: self.client_data, | 
				
			||||
 | 
					        } | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					#[post("/two-factor/u2f", data = "<data>")] | 
				
			||||
 | 
					fn activate_u2f(data: JsonUpcase<EnableU2FData>, headers: Headers, conn: DbConn) -> JsonResult { | 
				
			||||
 | 
					    let data: EnableU2FData = data.into_inner().data; | 
				
			||||
 | 
					    let mut user = headers.user; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    if !user.check_valid_password(&data.MasterPasswordHash) { | 
				
			||||
 | 
					        err!("Invalid password"); | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let tf_type = TwoFactorType::U2fRegisterChallenge as i32; | 
				
			||||
 | 
					    let tf_challenge = match TwoFactor::find_by_user_and_type(&user.uuid, tf_type, &conn) { | 
				
			||||
 | 
					        Some(c) => c, | 
				
			||||
 | 
					        None => err!("Can't recover challenge"), | 
				
			||||
 | 
					    }; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let challenge: Challenge = serde_json::from_str(&tf_challenge.data)?; | 
				
			||||
 | 
					    tf_challenge.delete(&conn)?; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let response: RegisterResponseCopy = serde_json::from_str(&data.DeviceResponse)?; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let error_code = response | 
				
			||||
 | 
					        .error_code | 
				
			||||
 | 
					        .clone() | 
				
			||||
 | 
					        .map_or("0".into(), NumberOrString::into_string); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    if error_code != "0" { | 
				
			||||
 | 
					        err!("Error registering U2F token") | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let registration = U2F.register_response(challenge.clone(), response.into())?; | 
				
			||||
 | 
					    let full_registration = U2FRegistration { | 
				
			||||
 | 
					        id: data.Id.into_i32()?, | 
				
			||||
 | 
					        name: data.Name, | 
				
			||||
 | 
					        reg: registration, | 
				
			||||
 | 
					        compromised: false, | 
				
			||||
 | 
					        counter: 0, | 
				
			||||
 | 
					    }; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let mut regs = get_u2f_registrations(&user.uuid, &conn)?.1; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    // TODO: Check that there is no repeat Id
 | 
				
			||||
 | 
					    regs.push(full_registration); | 
				
			||||
 | 
					    save_u2f_registrations(&user.uuid, ®s, &conn)?; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    _generate_recover_code(&mut user, &conn); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let keys_json: Vec<Value> = regs.iter().map(U2FRegistration::to_json).collect(); | 
				
			||||
 | 
					    Ok(Json(json!({ | 
				
			||||
 | 
					        "Enabled": true, | 
				
			||||
 | 
					        "Keys": keys_json, | 
				
			||||
 | 
					        "Object": "twoFactorU2f" | 
				
			||||
 | 
					    }))) | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					#[put("/two-factor/u2f", data = "<data>")] | 
				
			||||
 | 
					fn activate_u2f_put(data: JsonUpcase<EnableU2FData>, headers: Headers, conn: DbConn) -> JsonResult { | 
				
			||||
 | 
					    activate_u2f(data, headers, conn) | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					fn _create_u2f_challenge(user_uuid: &str, type_: TwoFactorType, conn: &DbConn) -> Challenge { | 
				
			||||
 | 
					    let challenge = U2F.generate_challenge().unwrap(); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    TwoFactor::new(user_uuid.into(), type_, serde_json::to_string(&challenge).unwrap()) | 
				
			||||
 | 
					        .save(conn) | 
				
			||||
 | 
					        .expect("Error saving challenge"); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    challenge | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					fn save_u2f_registrations(user_uuid: &str, regs: &[U2FRegistration], conn: &DbConn) -> EmptyResult { | 
				
			||||
 | 
					    TwoFactor::new(user_uuid.into(), TwoFactorType::U2f, serde_json::to_string(regs)?).save(&conn) | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					fn get_u2f_registrations(user_uuid: &str, conn: &DbConn) -> Result<(bool, Vec<U2FRegistration>), Error> { | 
				
			||||
 | 
					    let type_ = TwoFactorType::U2f as i32; | 
				
			||||
 | 
					    let (enabled, regs) = match TwoFactor::find_by_user_and_type(user_uuid, type_, conn) { | 
				
			||||
 | 
					        Some(tf) => (tf.enabled, tf.data), | 
				
			||||
 | 
					        None => return Ok((false, Vec::new())), // If no data, return empty list
 | 
				
			||||
 | 
					    }; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let data = match serde_json::from_str(®s) { | 
				
			||||
 | 
					        Ok(d) => d, | 
				
			||||
 | 
					        Err(_) => { | 
				
			||||
 | 
					            // If error, try old format
 | 
				
			||||
 | 
					            let mut old_regs = _old_parse_registrations(®s); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            if old_regs.len() != 1 { | 
				
			||||
 | 
					                err!("The old U2F format only allows one device") | 
				
			||||
 | 
					            } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            // Convert to new format
 | 
				
			||||
 | 
					            let new_regs = vec![U2FRegistration { | 
				
			||||
 | 
					                id: 1, | 
				
			||||
 | 
					                name: "Unnamed U2F key".into(), | 
				
			||||
 | 
					                reg: old_regs.remove(0), | 
				
			||||
 | 
					                compromised: false, | 
				
			||||
 | 
					                counter: 0, | 
				
			||||
 | 
					            }]; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            // Save new format
 | 
				
			||||
 | 
					            save_u2f_registrations(user_uuid, &new_regs, &conn)?; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					            new_regs | 
				
			||||
 | 
					        } | 
				
			||||
 | 
					    }; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    Ok((enabled, data)) | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					fn _old_parse_registrations(registations: &str) -> Vec<Registration> { | 
				
			||||
 | 
					    #[derive(Deserialize)] | 
				
			||||
 | 
					    struct Helper(#[serde(with = "RegistrationDef")] Registration); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let regs: Vec<Value> = serde_json::from_str(registations).expect("Can't parse Registration data"); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    regs.into_iter() | 
				
			||||
 | 
					        .map(|r| serde_json::from_value(r).unwrap()) | 
				
			||||
 | 
					        .map(|Helper(r)| r) | 
				
			||||
 | 
					        .collect() | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					pub fn generate_u2f_login(user_uuid: &str, conn: &DbConn) -> ApiResult<U2fSignRequest> { | 
				
			||||
 | 
					    let challenge = _create_u2f_challenge(user_uuid, TwoFactorType::U2fLoginChallenge, conn); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let registrations: Vec<_> = get_u2f_registrations(user_uuid, conn)? | 
				
			||||
 | 
					        .1 | 
				
			||||
 | 
					        .into_iter() | 
				
			||||
 | 
					        .map(|r| r.reg) | 
				
			||||
 | 
					        .collect(); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    if registrations.is_empty() { | 
				
			||||
 | 
					        err!("No U2F devices registered") | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    Ok(U2F.sign_request(challenge, registrations)) | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					pub fn validate_u2f_login(user_uuid: &str, response: &str, conn: &DbConn) -> EmptyResult { | 
				
			||||
 | 
					    let challenge_type = TwoFactorType::U2fLoginChallenge as i32; | 
				
			||||
 | 
					    let tf_challenge = TwoFactor::find_by_user_and_type(user_uuid, challenge_type, &conn); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let challenge = match tf_challenge { | 
				
			||||
 | 
					        Some(tf_challenge) => { | 
				
			||||
 | 
					            let challenge: Challenge = serde_json::from_str(&tf_challenge.data)?; | 
				
			||||
 | 
					            tf_challenge.delete(&conn)?; | 
				
			||||
 | 
					            challenge | 
				
			||||
 | 
					        } | 
				
			||||
 | 
					        None => err!("Can't recover login challenge"), | 
				
			||||
 | 
					    }; | 
				
			||||
 | 
					    let response: SignResponse = serde_json::from_str(response)?; | 
				
			||||
 | 
					    let mut registrations = get_u2f_registrations(user_uuid, conn)?.1; | 
				
			||||
 | 
					    if registrations.is_empty() { | 
				
			||||
 | 
					        err!("No U2F devices registered") | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    for reg in &mut registrations { | 
				
			||||
 | 
					        let response = U2F.sign_response(challenge.clone(), reg.reg.clone(), response.clone(), reg.counter); | 
				
			||||
 | 
					        match response { | 
				
			||||
 | 
					            Ok(new_counter) => { | 
				
			||||
 | 
					                reg.counter = new_counter; | 
				
			||||
 | 
					                save_u2f_registrations(user_uuid, ®istrations, &conn)?; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					                return Ok(()); | 
				
			||||
 | 
					            } | 
				
			||||
 | 
					            Err(u2f::u2ferror::U2fError::CounterTooLow) => { | 
				
			||||
 | 
					                reg.compromised = true; | 
				
			||||
 | 
					                save_u2f_registrations(user_uuid, ®istrations, &conn)?; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					                err!("This device might be compromised!"); | 
				
			||||
 | 
					            } | 
				
			||||
 | 
					            Err(e) => { | 
				
			||||
 | 
					                warn!("E {:#}", e); | 
				
			||||
 | 
					                // break;
 | 
				
			||||
 | 
					            } | 
				
			||||
 | 
					        } | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					    err!("error verifying response") | 
				
			||||
 | 
					} | 
				
			||||
@ -0,0 +1,194 @@ | 
				
			|||||
 | 
					use rocket::Route; | 
				
			||||
 | 
					use rocket_contrib::json::Json; | 
				
			||||
 | 
					use serde_json; | 
				
			||||
 | 
					use serde_json::Value; | 
				
			||||
 | 
					use yubico::config::Config; | 
				
			||||
 | 
					use yubico::verify; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					use crate::api::core::two_factor::_generate_recover_code; | 
				
			||||
 | 
					use crate::api::{EmptyResult, JsonResult, JsonUpcase, PasswordData}; | 
				
			||||
 | 
					use crate::auth::Headers; | 
				
			||||
 | 
					use crate::db::{ | 
				
			||||
 | 
					    models::{TwoFactor, TwoFactorType}, | 
				
			||||
 | 
					    DbConn, | 
				
			||||
 | 
					}; | 
				
			||||
 | 
					use crate::error::{Error, MapResult}; | 
				
			||||
 | 
					use crate::CONFIG; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					pub fn routes() -> Vec<Route> { | 
				
			||||
 | 
					    routes![ | 
				
			||||
 | 
					        generate_yubikey, | 
				
			||||
 | 
					        activate_yubikey, | 
				
			||||
 | 
					        activate_yubikey_put, | 
				
			||||
 | 
					    ] | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					#[derive(Deserialize, Debug)] | 
				
			||||
 | 
					#[allow(non_snake_case)] | 
				
			||||
 | 
					struct EnableYubikeyData { | 
				
			||||
 | 
					    MasterPasswordHash: String, | 
				
			||||
 | 
					    Key1: Option<String>, | 
				
			||||
 | 
					    Key2: Option<String>, | 
				
			||||
 | 
					    Key3: Option<String>, | 
				
			||||
 | 
					    Key4: Option<String>, | 
				
			||||
 | 
					    Key5: Option<String>, | 
				
			||||
 | 
					    Nfc: bool, | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					#[derive(Deserialize, Serialize, Debug)] | 
				
			||||
 | 
					#[allow(non_snake_case)] | 
				
			||||
 | 
					pub struct YubikeyMetadata { | 
				
			||||
 | 
					    Keys: Vec<String>, | 
				
			||||
 | 
					    pub Nfc: bool, | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					fn parse_yubikeys(data: &EnableYubikeyData) -> Vec<String> { | 
				
			||||
 | 
					    let data_keys = [&data.Key1, &data.Key2, &data.Key3, &data.Key4, &data.Key5]; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    data_keys.iter().filter_map(|e| e.as_ref().cloned()).collect() | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					fn jsonify_yubikeys(yubikeys: Vec<String>) -> serde_json::Value { | 
				
			||||
 | 
					    let mut result = json!({}); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    for (i, key) in yubikeys.into_iter().enumerate() { | 
				
			||||
 | 
					        result[format!("Key{}", i + 1)] = Value::String(key); | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    result | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					fn get_yubico_credentials() -> Result<(String, String), Error> { | 
				
			||||
 | 
					    match (CONFIG.yubico_client_id(), CONFIG.yubico_secret_key()) { | 
				
			||||
 | 
					        (Some(id), Some(secret)) => Ok((id, secret)), | 
				
			||||
 | 
					        _ => err!("`YUBICO_CLIENT_ID` or `YUBICO_SECRET_KEY` environment variable is not set. Yubikey OTP Disabled"), | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					fn verify_yubikey_otp(otp: String) -> EmptyResult { | 
				
			||||
 | 
					    let (yubico_id, yubico_secret) = get_yubico_credentials()?; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let config = Config::default().set_client_id(yubico_id).set_key(yubico_secret); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    match CONFIG.yubico_server() { | 
				
			||||
 | 
					        Some(server) => verify(otp, config.set_api_hosts(vec![server])), | 
				
			||||
 | 
					        None => verify(otp, config), | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					    .map_res("Failed to verify OTP") | 
				
			||||
 | 
					    .and(Ok(())) | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					#[post("/two-factor/get-yubikey", data = "<data>")] | 
				
			||||
 | 
					fn generate_yubikey(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> JsonResult { | 
				
			||||
 | 
					    // Make sure the credentials are set
 | 
				
			||||
 | 
					    get_yubico_credentials()?; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let data: PasswordData = data.into_inner().data; | 
				
			||||
 | 
					    let user = headers.user; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    if !user.check_valid_password(&data.MasterPasswordHash) { | 
				
			||||
 | 
					        err!("Invalid password"); | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let user_uuid = &user.uuid; | 
				
			||||
 | 
					    let yubikey_type = TwoFactorType::YubiKey as i32; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let r = TwoFactor::find_by_user_and_type(user_uuid, yubikey_type, &conn); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    if let Some(r) = r { | 
				
			||||
 | 
					        let yubikey_metadata: YubikeyMetadata = serde_json::from_str(&r.data)?; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        let mut result = jsonify_yubikeys(yubikey_metadata.Keys); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        result["Enabled"] = Value::Bool(true); | 
				
			||||
 | 
					        result["Nfc"] = Value::Bool(yubikey_metadata.Nfc); | 
				
			||||
 | 
					        result["Object"] = Value::String("twoFactorU2f".to_owned()); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        Ok(Json(result)) | 
				
			||||
 | 
					    } else { | 
				
			||||
 | 
					        Ok(Json(json!({ | 
				
			||||
 | 
					            "Enabled": false, | 
				
			||||
 | 
					            "Object": "twoFactorU2f", | 
				
			||||
 | 
					        }))) | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					#[post("/two-factor/yubikey", data = "<data>")] | 
				
			||||
 | 
					fn activate_yubikey(data: JsonUpcase<EnableYubikeyData>, headers: Headers, conn: DbConn) -> JsonResult { | 
				
			||||
 | 
					    let data: EnableYubikeyData = data.into_inner().data; | 
				
			||||
 | 
					    let mut user = headers.user; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    if !user.check_valid_password(&data.MasterPasswordHash) { | 
				
			||||
 | 
					        err!("Invalid password"); | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    // Check if we already have some data
 | 
				
			||||
 | 
					    let mut yubikey_data = match TwoFactor::find_by_user_and_type(&user.uuid, TwoFactorType::YubiKey as i32, &conn) { | 
				
			||||
 | 
					        Some(data) => data, | 
				
			||||
 | 
					        None => TwoFactor::new(user.uuid.clone(), TwoFactorType::YubiKey, String::new()), | 
				
			||||
 | 
					    }; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let yubikeys = parse_yubikeys(&data); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    if yubikeys.is_empty() { | 
				
			||||
 | 
					        return Ok(Json(json!({ | 
				
			||||
 | 
					            "Enabled": false, | 
				
			||||
 | 
					            "Object": "twoFactorU2f", | 
				
			||||
 | 
					        }))); | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    // Ensure they are valid OTPs
 | 
				
			||||
 | 
					    for yubikey in &yubikeys { | 
				
			||||
 | 
					        if yubikey.len() == 12 { | 
				
			||||
 | 
					            // YubiKey ID
 | 
				
			||||
 | 
					            continue; | 
				
			||||
 | 
					        } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					        verify_yubikey_otp(yubikey.to_owned()).map_res("Invalid Yubikey OTP provided")?; | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let yubikey_ids: Vec<String> = yubikeys.into_iter().map(|x| (&x[..12]).to_owned()).collect(); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let yubikey_metadata = YubikeyMetadata { | 
				
			||||
 | 
					        Keys: yubikey_ids, | 
				
			||||
 | 
					        Nfc: data.Nfc, | 
				
			||||
 | 
					    }; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    yubikey_data.data = serde_json::to_string(&yubikey_metadata).unwrap(); | 
				
			||||
 | 
					    yubikey_data.save(&conn)?; | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    _generate_recover_code(&mut user, &conn); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let mut result = jsonify_yubikeys(yubikey_metadata.Keys); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    result["Enabled"] = Value::Bool(true); | 
				
			||||
 | 
					    result["Nfc"] = Value::Bool(yubikey_metadata.Nfc); | 
				
			||||
 | 
					    result["Object"] = Value::String("twoFactorU2f".to_owned()); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    Ok(Json(result)) | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					#[put("/two-factor/yubikey", data = "<data>")] | 
				
			||||
 | 
					fn activate_yubikey_put(data: JsonUpcase<EnableYubikeyData>, headers: Headers, conn: DbConn) -> JsonResult { | 
				
			||||
 | 
					    activate_yubikey(data, headers, conn) | 
				
			||||
 | 
					} | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					pub fn validate_yubikey_login(response: &str, twofactor_data: &str) -> EmptyResult { | 
				
			||||
 | 
					    if response.len() != 44 { | 
				
			||||
 | 
					        err!("Invalid Yubikey OTP length"); | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    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()) { | 
				
			||||
 | 
					        err!("Given Yubikey is not registered"); | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    let result = verify_yubikey_otp(response.to_owned()); | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    match result { | 
				
			||||
 | 
					        Ok(_answer) => Ok(()), | 
				
			||||
 | 
					        Err(_e) => err!("Failed to verify Yubikey against OTP server"), | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					} | 
				
			||||
@ -0,0 +1,9 @@ | 
				
			|||||
 | 
					Your Two-step Login Verification Code | 
				
			||||
 | 
					<!----------------> | 
				
			||||
 | 
					<html> | 
				
			||||
 | 
					<p> | 
				
			||||
 | 
					    Your two-step verification code is: <b>{{token}}</b> | 
				
			||||
 | 
					
 | 
				
			||||
 | 
					    Use this code to complete logging in with Bitwarden. | 
				
			||||
 | 
					</p> | 
				
			||||
 | 
					</html> | 
				
			||||
@ -0,0 +1,129 @@ | 
				
			|||||
 | 
					Your Two-step Login Verification Code | 
				
			||||
 | 
					<!----------------> | 
				
			||||
 | 
					<html xmlns="http://www.w3.org/1999/xhtml" xmlns="http://www.w3.org/1999/xhtml" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;"> | 
				
			||||
 | 
					<head> | 
				
			||||
 | 
					    <meta name="viewport" content="width=device-width" /> | 
				
			||||
 | 
					    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> | 
				
			||||
 | 
					    <title>Bitwarden_rs</title> | 
				
			||||
 | 
					</head> | 
				
			||||
 | 
					<body style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; height: 100%; line-height: 25px; width: 100% !important;" bgcolor="#f6f6f6"> | 
				
			||||
 | 
					<style type="text/css"> | 
				
			||||
 | 
					     body { | 
				
			||||
 | 
					        margin: 0; | 
				
			||||
 | 
					        font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; | 
				
			||||
 | 
					        box-sizing: border-box; | 
				
			||||
 | 
					        font-size: 16px; | 
				
			||||
 | 
					        color: #333; | 
				
			||||
 | 
					        line-height: 25px; | 
				
			||||
 | 
					        -webkit-font-smoothing: antialiased; | 
				
			||||
 | 
					        -webkit-text-size-adjust: none; | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					    body * { | 
				
			||||
 | 
					        margin: 0; | 
				
			||||
 | 
					        font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; | 
				
			||||
 | 
					        box-sizing: border-box; | 
				
			||||
 | 
					        font-size: 16px; | 
				
			||||
 | 
					        color: #333; | 
				
			||||
 | 
					        line-height: 25px; | 
				
			||||
 | 
					        -webkit-font-smoothing: antialiased; | 
				
			||||
 | 
					        -webkit-text-size-adjust: none; | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					    img { | 
				
			||||
 | 
					        max-width: 100%; | 
				
			||||
 | 
					        border: none; | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					    body { | 
				
			||||
 | 
					        -webkit-font-smoothing: antialiased; | 
				
			||||
 | 
					        -webkit-text-size-adjust: none; | 
				
			||||
 | 
					        width: 100% !important; | 
				
			||||
 | 
					        height: 100%; | 
				
			||||
 | 
					        line-height: 25px; | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					    body { | 
				
			||||
 | 
					        background-color: #f6f6f6; | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					    @media only screen and (max-width: 600px) { | 
				
			||||
 | 
					        body { | 
				
			||||
 | 
					            padding: 0 !important; | 
				
			||||
 | 
					        } | 
				
			||||
 | 
					        .container { | 
				
			||||
 | 
					            padding: 0 !important; | 
				
			||||
 | 
					            width: 100% !important; | 
				
			||||
 | 
					        } | 
				
			||||
 | 
					        .container-table { | 
				
			||||
 | 
					            padding: 0 !important; | 
				
			||||
 | 
					            width: 100% !important; | 
				
			||||
 | 
					        } | 
				
			||||
 | 
					        .content { | 
				
			||||
 | 
					            padding: 0 0 10px 0 !important; | 
				
			||||
 | 
					        } | 
				
			||||
 | 
					        .content-wrap { | 
				
			||||
 | 
					            padding: 10px !important; | 
				
			||||
 | 
					        } | 
				
			||||
 | 
					        .invoice { | 
				
			||||
 | 
					            width: 100% !important; | 
				
			||||
 | 
					        } | 
				
			||||
 | 
					        .main { | 
				
			||||
 | 
					            border-right: none !important; | 
				
			||||
 | 
					            border-left: none !important; | 
				
			||||
 | 
					            border-radius: 0 !important; | 
				
			||||
 | 
					        } | 
				
			||||
 | 
					        .logo { | 
				
			||||
 | 
					            padding-top: 10px !important; | 
				
			||||
 | 
					        } | 
				
			||||
 | 
					        .footer { | 
				
			||||
 | 
					            margin-top: 10px !important; | 
				
			||||
 | 
					        } | 
				
			||||
 | 
					        .indented { | 
				
			||||
 | 
					            padding-left: 10px; | 
				
			||||
 | 
					        } | 
				
			||||
 | 
					    } | 
				
			||||
 | 
					</style> | 
				
			||||
 | 
					<table class="body-wrap" cellpadding="0" cellspacing="0" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; width: 100%;" bgcolor="#f6f6f6"> | 
				
			||||
 | 
					    <tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;"> | 
				
			||||
 | 
					        <td valign="middle" class="aligncenter middle logo" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; padding: 20px 0 10px;" align="center"> | 
				
			||||
 | 
					            <img src="{{url}}/bwrs_images/logo-gray.png" alt="" width="250" height="39" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; border: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; max-width: 100%;" /> | 
				
			||||
 | 
					        </td> | 
				
			||||
 | 
					    </tr> | 
				
			||||
 | 
					    <tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;"> | 
				
			||||
 | 
					        <td class="container" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both !important; color: #333; display: block !important; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto; max-width: 600px !important; width: 600px;" valign="top"> | 
				
			||||
 | 
					            <table cellpadding="0" cellspacing="0" class="container-table" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both !important; color: #333; display: block !important; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto; max-width: 600px !important; width: 600px;"> | 
				
			||||
 | 
					                <tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;"> | 
				
			||||
 | 
					                    <td class="content" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; display: block; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 0; line-height: 0; margin: 0 auto; max-width: 600px; padding-bottom: 20px;" valign="top"> | 
				
			||||
 | 
					                        <table class="main" width="100%" cellpadding="0" cellspacing="0" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; margin: 0; -webkit-text-size-adjust: none; border: 1px solid #e9e9e9; border-radius: 3px;" bgcolor="white"> | 
				
			||||
 | 
					                            <tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;"> | 
				
			||||
 | 
					                                <td class="content-wrap" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 20px; -webkit-text-size-adjust: none;" valign="top"> | 
				
			||||
 | 
					                                    <table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;"> | 
				
			||||
 | 
					                                        <tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;"> | 
				
			||||
 | 
					                                            <td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top"> | 
				
			||||
 | 
					                                                Your two-step verification code is: <b>{{token}}</b> | 
				
			||||
 | 
					                                            </td> | 
				
			||||
 | 
					                                        </tr> | 
				
			||||
 | 
					                                        <tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;"> | 
				
			||||
 | 
					                                            <td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top"> | 
				
			||||
 | 
					                                                Use this code to complete logging in with Bitwarden. | 
				
			||||
 | 
					                                            </td> | 
				
			||||
 | 
					                                        </tr> | 
				
			||||
 | 
					                                    </table> | 
				
			||||
 | 
					                                </td> | 
				
			||||
 | 
					                            </tr> | 
				
			||||
 | 
					                        </table> | 
				
			||||
 | 
					                        <table class="footer" cellpadding="0" cellspacing="0" width="100%" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; width: 100%;"> | 
				
			||||
 | 
					                            <tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;"> | 
				
			||||
 | 
					                                <td class="aligncenter social-icons" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; padding: 15px 0 0 0;" valign="top"> | 
				
			||||
 | 
					                                    <table cellpadding="0" cellspacing="0" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto;"> | 
				
			||||
 | 
					                                        <tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;"> | 
				
			||||
 | 
					                                            <td style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; padding: 0 10px;" valign="top"><a href="https://github.com/dani-garcia/bitwarden_rs" target="_blank" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; text-decoration: underline;"><img src="{{url}}/bwrs_images/mail-github.png" alt="GitHub" width="30" height="30" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; border: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; max-width: 100%;" /></a></td> | 
				
			||||
 | 
					                                        </tr> | 
				
			||||
 | 
					                                    </table> | 
				
			||||
 | 
					                                </td> | 
				
			||||
 | 
					                            </tr> | 
				
			||||
 | 
					                        </table> | 
				
			||||
 | 
					                    </td> | 
				
			||||
 | 
					                </tr> | 
				
			||||
 | 
					            </table> | 
				
			||||
 | 
					        </td> | 
				
			||||
 | 
					    </tr> | 
				
			||||
 | 
					</table> | 
				
			||||
 | 
					</body> | 
				
			||||
 | 
					</html> | 
				
			||||
					Loading…
					
					
				
		Reference in new issue