11 changed files with 1462 additions and 2275 deletions
			
			
		
								
									
										File diff suppressed because it is too large
									
								
							
						
					| @ -0,0 +1,101 @@ | |||
| use data_encoding::{BASE32, BASE64}; | |||
| use rocket::Route; | |||
| use rocket_contrib::json::Json; | |||
| use serde_json; | |||
| use serde_json::Value; | |||
| 
 | |||
| use crate::api::{ApiResult, EmptyResult, JsonResult, JsonUpcase, NumberOrString, PasswordData}; | |||
| use crate::api::core::two_factor::{_generate_recover_code, totp}; | |||
| use crate::auth::Headers; | |||
| use crate::crypto; | |||
| use crate::db::{ | |||
|     DbConn, | |||
|     models::{TwoFactor, TwoFactorType, User}, | |||
| }; | |||
| use crate::error::{Error, MapResult}; | |||
| 
 | |||
| const TOTP_TIME_STEP: u64 = 30; | |||
| 
 | |||
| 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
 | |||
|     totp::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) | |||
| } | |||
| @ -0,0 +1,348 @@ | |||
| use chrono::Utc; | |||
| use data_encoding::{BASE32, BASE64}; | |||
| use oath::{HashType, totp_raw_now}; | |||
| use rocket::Route; | |||
| use rocket_contrib::json::Json; | |||
| use serde_json; | |||
| use serde_json::Value; | |||
| 
 | |||
| use crate::api::{ApiResult, EmptyResult, JsonResult, JsonUpcase, NumberOrString, PasswordData}; | |||
| use crate::auth::Headers; | |||
| use crate::CONFIG; | |||
| use crate::crypto; | |||
| use crate::db::{ | |||
|     DbConn, | |||
|     models::{TwoFactor, TwoFactorType, User}, | |||
| }; | |||
| use crate::error::{Error, MapResult}; | |||
| 
 | |||
| 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,272 @@ | |||
| use data_encoding::{BASE32, BASE64}; | |||
| use lettre_email::Email; | |||
| use oath::{totp_raw_now, HashType}; | |||
| use rocket::Route; | |||
| use rocket_contrib::json::Json; | |||
| use serde_json; | |||
| use serde_json::Value; | |||
| 
 | |||
| use crate::api::core::two_factor::totp; | |||
| use crate::api::core::two_factor::totp::validate_totp_code_with_time_step; | |||
| use crate::api::{ApiResult, EmptyResult, JsonResult, JsonUpcase, NumberOrString, PasswordData}; | |||
| use crate::auth::Headers; | |||
| use crate::db::{ | |||
|     models::{TwoFactor, TwoFactorType, User}, | |||
|     DbConn, | |||
| }; | |||
| use crate::error::{Error, MapResult}; | |||
| use crate::{crypto, mail}; | |||
| 
 | |||
| const TOTP_TIME_STEP: u64 = 120; | |||
| 
 | |||
| 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, | |||
| } | |||
| 
 | |||
| // 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 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.") | |||
|     } | |||
| 
 | |||
|     let type_ = TwoFactorType::Email as i32; | |||
|     let mut twofactor = TwoFactor::find_by_user_and_type(&user.uuid, type_, &conn)?; | |||
| 
 | |||
|     let twofactor_data = EmailTokenData::from_json(&twofactor.data)?; | |||
| 
 | |||
|     let decoded_key = totp::validate_decode_key(&twofactor_data.TotpSecret)?; | |||
| 
 | |||
|     let generated_token = totp_raw_now(&decoded_key, 6, 0, TOTP_TIME_STEP, &HashType::SHA1); | |||
|     let token_string = generated_token.to_string(); | |||
| 
 | |||
|     mail::send_token(&twofactor_data.Email, &token_string)?; | |||
| 
 | |||
|     Ok(()) | |||
| } | |||
| 
 | |||
| #[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!({// TODO check! FIX!
 | |||
|         "Email": user.email, | |||
|         "Enabled": enabled, | |||
|         "Object": "twoFactorEmail" | |||
|     }))) | |||
| } | |||
| 
 | |||
| #[derive(Deserialize)] | |||
| #[allow(non_snake_case)] | |||
| struct SendEmailData { | |||
|     Email: String, | |||
|     // Email where 2FA codes will be sent to, can be different than user email account.
 | |||
|     MasterPasswordHash: String, | |||
| } | |||
| 
 | |||
| // 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 { | |||
|     use oath::{totp_raw_now, HashType}; | |||
| 
 | |||
|     let data: SendEmailData = data.into_inner().data; | |||
|     let user = headers.user; | |||
| 
 | |||
|     if !user.check_valid_password(&data.MasterPasswordHash) { | |||
|         err!("Invalid password"); | |||
|     } | |||
| 
 | |||
|     let type_ = TwoFactorType::Email as i32; | |||
| 
 | |||
|     // TODO: Delete previous email thing.
 | |||
|     match TwoFactor::find_by_user_and_type(&user.uuid, type_, &conn) { | |||
|         Some(tf) => tf.delete(&conn), | |||
|         _ => Ok(()), | |||
|     }; | |||
| 
 | |||
|     let secret = crypto::get_random(vec![0u8; 20]); | |||
|     let base32_secret = BASE32.encode(&secret); | |||
| 
 | |||
|     let twofactor_data = EmailTokenData::new(data.Email, base32_secret); | |||
| 
 | |||
|     // Uses EmailVerificationChallenge as type to show that it's not verified yet.
 | |||
|     let mut twofactor = TwoFactor::new( | |||
|         user.uuid, | |||
|         TwoFactorType::EmailVerificationChallenge, | |||
|         twofactor_data.to_json(), | |||
|     ); | |||
|     twofactor.save(&conn)?; | |||
| 
 | |||
|     let generated_token = totp_raw_now(&secret, 6, 0, TOTP_TIME_STEP, &HashType::SHA1); | |||
|     let token_string = generated_token.to_string(); | |||
| 
 | |||
|     mail::send_token(&twofactor_data.Email, &token_string)?; | |||
| 
 | |||
|     Ok(()) | |||
| } | |||
| 
 | |||
| #[derive(Deserialize, Serialize)] | |||
| #[allow(non_snake_case)] | |||
| struct EmailData { | |||
|     Email: String, | |||
|     MasterPasswordHash: String, | |||
|     Token: String, | |||
| } | |||
| 
 | |||
| // Verify email 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 token_u64 = match data.Token.parse::<u64>() { | |||
|         Ok(token) => token, | |||
|         _ => err!("Could not parse token"), | |||
|     }; | |||
| 
 | |||
|     let type_ = TwoFactorType::EmailVerificationChallenge as i32; | |||
|     let mut twofactor = TwoFactor::find_by_user_and_type(&user.uuid, type_, &conn)?; | |||
| 
 | |||
|     let email_data = EmailTokenData::from_json(&twofactor.data)?; | |||
| 
 | |||
|     totp::validate_totp_code_with_time_step(token_u64, &email_data.TotpSecret, TOTP_TIME_STEP)?; | |||
| 
 | |||
|     twofactor.atype = TwoFactorType::Email as i32; | |||
|     twofactor.save(&conn)?; | |||
| 
 | |||
|     Ok(Json(json!({ | |||
|         "Email": email_data.Email, | |||
|         "Enabled": "true", | |||
|         "Object": "twoFactorEmail" | |||
|     }))) | |||
| } | |||
| 
 | |||
| pub fn validate_email_code_str(code: &str, data: &str) -> EmptyResult { | |||
|     let totp_code: u64 = match code.parse() { | |||
|         Ok(code) => code, | |||
|         _ => err!("Email code is not a number"), | |||
|     }; | |||
| 
 | |||
|     validate_email_code(totp_code, data) | |||
| } | |||
| 
 | |||
| pub fn validate_email_code(code: u64, data: &str) -> EmptyResult { | |||
|     let email_data = EmailTokenData::from_json(&data)?; | |||
| 
 | |||
|     let decoded_secret = match BASE32.decode(email_data.TotpSecret.as_bytes()) { | |||
|         Ok(s) => s, | |||
|         Err(_) => err!("Invalid email secret"), | |||
|     }; | |||
| 
 | |||
|     let generated = totp_raw_now(&decoded_secret, 6, 0, TOTP_TIME_STEP, &HashType::SHA1); | |||
|     if generated != code { | |||
|         err!("Invalid email code"); | |||
|     } | |||
| 
 | |||
|     Ok(()) | |||
| } | |||
| 
 | |||
| #[derive(Serialize, Deserialize)] | |||
| pub struct EmailTokenData { | |||
|     pub Email: String, | |||
|     pub TotpSecret: String, | |||
| } | |||
| 
 | |||
| impl EmailTokenData { | |||
|     pub fn new(email: String, secret: String) -> EmailTokenData { | |||
|         EmailTokenData { | |||
|             Email: email, | |||
|             TotpSecret: secret, | |||
|         } | |||
|     } | |||
| 
 | |||
|     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"), | |||
|         } | |||
|     } | |||
| } | |||
| 
 | |||
| 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..=2 => "*".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 = "by@example.ext"; | |||
| 
 | |||
|         let result = obscure_email(&email); | |||
| 
 | |||
|         // If it's smaller than 3 characters it should only show asterisks.
 | |||
|         assert_eq!(result, "**@example.ext"); | |||
|     } | |||
| } | |||
| @ -0,0 +1,152 @@ | |||
| use chrono::Utc; | |||
| use data_encoding::{BASE32, BASE64}; | |||
| use oath::{HashType, totp_raw_now}; | |||
| use rocket::Route; | |||
| use rocket_contrib::json::Json; | |||
| use serde_json; | |||
| use serde_json::Value; | |||
| 
 | |||
| use crate::api::{ApiResult, EmptyResult, JsonResult, JsonUpcase, NumberOrString, PasswordData}; | |||
| use crate::auth::Headers; | |||
| use crate::crypto; | |||
| use crate::db::{ | |||
|     DbConn, | |||
|     models::{TwoFactor, TwoFactorType, User}, | |||
| }; | |||
| use crate::error::{Error, MapResult}; | |||
| 
 | |||
| pub(crate) mod authenticator; | |||
| pub(crate) mod duo; | |||
| pub(crate) mod email; | |||
| pub(crate) mod u2f; | |||
| pub(crate) mod yubikey; | |||
| pub(crate) mod totp; | |||
| 
 | |||
| 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
 | |||
|     for twofactor in TwoFactor::find_by_user(&user.uuid, &conn) { | |||
|         twofactor.delete(&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,46 @@ | |||
| use data_encoding::BASE32; | |||
| 
 | |||
| use crate::api::EmptyResult; | |||
| 
 | |||
| 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 { | |||
|     validate_totp_code_with_time_step(totp_code, &secret, 30) | |||
| } | |||
| 
 | |||
| pub fn validate_totp_code_with_time_step(totp_code: u64, secret: &str, time_step: u64) -> 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, time_step, &HashType::SHA1); | |||
|     if generated != totp_code { | |||
|         err!("Invalid TOTP code"); | |||
|     } | |||
| 
 | |||
|     Ok(()) | |||
| } | |||
| 
 | |||
| pub fn validate_decode_key(key: &str) -> Result<Vec<u8>, crate::error::Error> { | |||
|     // 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") | |||
|     } | |||
| 
 | |||
|     Ok(decoded_key) | |||
| } | |||
| @ -0,0 +1,317 @@ | |||
| use data_encoding::{BASE32, BASE64}; | |||
| 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::{ApiResult, EmptyResult, JsonResult, JsonUpcase, NumberOrString, PasswordData}; | |||
| use crate::api::core::two_factor::_generate_recover_code; | |||
| use crate::auth::Headers; | |||
| use crate::CONFIG; | |||
| use crate::crypto; | |||
| use crate::db::{ | |||
|     DbConn, | |||
|     models::{TwoFactor, TwoFactorType, User}, | |||
| }; | |||
| use crate::error::{Error, MapResult}; | |||
| 
 | |||
| 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,196 @@ | |||
| use data_encoding::{BASE32, BASE64}; | |||
| 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::{ApiResult, EmptyResult, JsonResult, JsonUpcase, NumberOrString, PasswordData}; | |||
| use crate::api::core::two_factor::_generate_recover_code; | |||
| use crate::auth::Headers; | |||
| use crate::CONFIG; | |||
| use crate::crypto; | |||
| use crate::db::{ | |||
|     DbConn, | |||
|     models::{TwoFactor, TwoFactorType, User}, | |||
| }; | |||
| use crate::error::{Error, MapResult}; | |||
| 
 | |||
| 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"), | |||
|     } | |||
| } | |||
								
									
										File diff suppressed because it is too large
									
								
							
						
					
					Loading…
					
					
				
		Reference in new issue