From 27e0e41835a09d3e6e5a2d00ad606fc998dda72d Mon Sep 17 00:00:00 2001 From: vpl Date: Sun, 4 Aug 2019 16:56:39 +0200 Subject: [PATCH] Add email authenticator logic --- src/api/identity.rs | 11 +- src/api/two_factor.rs | 1208 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 1218 insertions(+), 1 deletion(-) create mode 100644 src/api/two_factor.rs diff --git a/src/api/identity.rs b/src/api/identity.rs index 0fbe77ed..eb0f761b 100644 --- a/src/api/identity.rs +++ b/src/api/identity.rs @@ -123,6 +123,7 @@ fn _password_login(data: ConnectData, conn: DbConn, ip: ClientIp) -> JsonResult "refresh_token": device.refresh_token, "Key": user.akey, "PrivateKey": user.private_key, + //"TwoFactorToken": "11122233333444555666777888999" }); if let Some(token) = twofactor_token { @@ -183,7 +184,7 @@ fn twofactor_auth( None => err_json!(_json_err_twofactor(&twofactor_ids, user_uuid, conn)?), }; - let selected_twofactor = twofactors.into_iter().filter(|tf| tf.atype == selected_id).nth(0); + let selected_twofactor = twofactors.into_iter().filter(|tf| tf.atype == selected_id && tf.enabled).nth(0); use crate::api::core::two_factor as _tf; use crate::crypto::ct_eq; @@ -196,6 +197,7 @@ fn twofactor_auth( Some(TwoFactorType::U2f) => _tf::validate_u2f_login(user_uuid, twofactor_code, conn)?, Some(TwoFactorType::YubiKey) => _tf::validate_yubikey_login(twofactor_code, &selected_data?)?, Some(TwoFactorType::Duo) => _tf::validate_duo_login(data.username.as_ref().unwrap(), twofactor_code, conn)?, + Some(TwoFactorType::Email) => _tf::validate_totp_code_str(twofactor_code, &selected_data?)?, Some(TwoFactorType::Remember) => { match device.twofactor_remember { @@ -286,6 +288,13 @@ fn _json_err_twofactor(providers: &[i32], user_uuid: &str, conn: &DbConn) -> Api }) } + Some(tf_type @ TwoFactorType::Email) => { + let twofactor = match TwoFactor::find_by_user_and_type(user_uuid, tf_type as i32, &conn) { + Some(tf) => tf, + None => err!("No twofactor email registered"), + }; + } + _ => {} } } diff --git a/src/api/two_factor.rs b/src/api/two_factor.rs new file mode 100644 index 00000000..70f43064 --- /dev/null +++ b/src/api/two_factor.rs @@ -0,0 +1,1208 @@ +use data_encoding::{BASE32, BASE64}; +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, mail}; +use crate::db::{ + models::{TwoFactor, TwoFactorType, User}, + DbConn, +}; +use crate::error::{Error, MapResult}; + +use rocket::Route; + +pub fn routes() -> Vec { + routes![ + get_twofactor, + get_recover, + recover, + disable_twofactor, + disable_twofactor_put, + generate_authenticator, + activate_authenticator, + activate_authenticator_put, + generate_u2f, + generate_u2f_challenge, + activate_u2f, + activate_u2f_put, + generate_yubikey, + activate_yubikey, + activate_yubikey_put, + get_duo, + activate_duo, + activate_duo_put, + get_email, + send_email_login, + send_email, + email, + ] +} + +#[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 = twofactors.iter().map(TwoFactor::to_json_list).collect(); + + Ok(Json(json!({ + "Data": twofactors_json, + "Object": "list", + "ContinuationToken": null, + }))) +} + +#[post("/two-factor/get-recover", data = "")] +fn get_recover(data: JsonUpcase, 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 = "")] +fn recover(data: JsonUpcase, 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 = "")] +fn disable_twofactor(data: JsonUpcase, 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 = "")] +fn disable_twofactor_put(data: JsonUpcase, headers: Headers, conn: DbConn) -> JsonResult { + disable_twofactor(data, headers, conn) +} + +#[post("/two-factor/get-authenticator", data = "")] +fn generate_authenticator(data: JsonUpcase, 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, +} + +fn validate_decode_key(key: &str) -> Result, crate::error::Error> { + // Validate key as base32 and 20 bytes length + let decoded_key: Vec = 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) +} + +#[post("/two-factor/authenticator", data = "")] +fn activate_authenticator(data: JsonUpcase, 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 = validate_decode_key(&key)?; + + 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 = "")] +fn activate_authenticator_put(data: JsonUpcase, 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(()) +} + +use u2f::messages::{RegisterResponse, SignResponse, U2fSignRequest}; +use u2f::protocol::{Challenge, U2f}; +use u2f::register::Registration; + +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()); +} + +#[post("/two-factor/get-u2f", data = "")] +fn generate_u2f(data: JsonUpcase, 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 = keys.iter().map(U2FRegistration::to_json).collect(); + + Ok(Json(json!({ + "Enabled": enabled, + "Keys": keys_json, + "Object": "twoFactorU2f" + }))) +} + +#[post("/two-factor/get-u2f-challenge", data = "")] +fn generate_u2f_challenge(data: JsonUpcase, 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, + pub_key: Vec, + attestation_cert: Option>, +} + +#[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, +} + +impl Into 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 = "")] +fn activate_u2f(data: JsonUpcase, 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 = regs.iter().map(U2FRegistration::to_json).collect(); + Ok(Json(json!({ + "Enabled": true, + "Keys": keys_json, + "Object": "twoFactorU2f" + }))) +} + +#[put("/two-factor/u2f", data = "")] +fn activate_u2f_put(data: JsonUpcase, 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), 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 { + #[derive(Deserialize)] + struct Helper(#[serde(with = "RegistrationDef")] Registration); + + let regs: Vec = 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 { + 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") +} + +#[derive(Deserialize, Debug)] +#[allow(non_snake_case)] +struct EnableYubikeyData { + MasterPasswordHash: String, + Key1: Option, + Key2: Option, + Key3: Option, + Key4: Option, + Key5: Option, + Nfc: bool, +} + +#[derive(Deserialize, Serialize, Debug)] +#[allow(non_snake_case)] +pub struct YubikeyMetadata { + Keys: Vec, + pub Nfc: bool, +} + +use yubico::config::Config; +use yubico::verify; + +fn parse_yubikeys(data: &EnableYubikeyData) -> Vec { + 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) -> 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 = "")] +fn generate_yubikey(data: JsonUpcase, 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 = "")] +fn activate_yubikey(data: JsonUpcase, 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 = 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 = "")] +fn activate_yubikey_put(data: JsonUpcase, 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"), + } +} + +#[derive(Serialize, Deserialize)] +struct DuoData { + host: String, + ik: String, + sk: String, +} + +impl DuoData { + fn global() -> Option { + 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("") + } + 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 { + match self { + DuoStatus::Global(data) => Some(data), + DuoStatus::User(data) => Some(data), + DuoStatus::Disabled(_) => None, + } + } +} +const DISABLED_MESSAGE_DEFAULT: &str = ""; + +#[post("/two-factor/get-duo", data = "")] +fn get_duo(data: JsonUpcase, 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 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 = "")] +fn activate_duo(data: JsonUpcase, 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 = "")] +fn activate_duo_put(data: JsonUpcase, 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"; + +use chrono::Utc; +use oath::{totp_raw_now, HashType}; + +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_email_login(twofactor_code: &str, secret: &str) -> EmptyResult { + + Ok(()) +} + +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 { + 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()) +} + +#[derive(Deserialize)] +#[allow(non_snake_case)] +struct SendEmailLoginData { + Email: String, + MasterPasswordHash: String, +} + +#[put("/two-factor/send-email-login", data = "")] // JsonResult +fn send_email_login(data: JsonUpcase, headers: Headers, 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 decoded_key = validate_decode_key(&twofactor.data)?; + + let generated_token = totp_raw_now(&decoded_key, 6, 0, 30, &HashType::SHA1); + let token_string = generated_token.to_string(); + + mail::send_token(&data.Email, &token_string)?; + +// activate_u2f(data, headers, conn) + Ok(()) +} + +#[post("/two-factor/get-email", data = "")] +fn get_email(data: JsonUpcase, 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 = "")] +fn send_email(data: JsonUpcase, 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 mut twofactor = TwoFactor::new(user.uuid, TwoFactorType::Email, base32_secret); + // Disable 2fa since it's not verified yet. + twofactor.enabled = false; + twofactor.save(&conn)?; + + let generated_token = totp_raw_now(&secret, 6, 0, 30, &HashType::SHA1); + let token_string = generated_token.to_string(); + + mail::send_token(&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 = "")] +fn email(data: JsonUpcase, 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::() { + Ok(token) => token, + _ => err!("Could not parse token"), + }; + + + let type_ = TwoFactorType::Email as i32; + let mut twofactor = TwoFactor::find_by_user_and_type(&user.uuid, type_, &conn)?; + + validate_totp_code(token_u64, &twofactor.data)?; + + twofactor.enabled = true; + twofactor.save(&conn)?; + + Ok(Json(json!({ + "Email": data.Email, // TODO Fix with actual email from db. + "Enabled": "true", + "Object": "twoFactorEmail" + }))) +} + +#[derive(Serialize, Deserialize)] +struct EmailTokenData { + email: String, + totp_secret: String, +} \ No newline at end of file