From 0b60f20eb3d598acb0525a098445b56672b83985 Mon Sep 17 00:00:00 2001 From: vpl Date: Sat, 3 Aug 2019 08:07:14 +0200 Subject: [PATCH 1/9] Add email message for twofactor email codes --- src/config.rs | 1 + src/mail.rs | 13 ++ .../templates/email/twofactor_email.hbs | 9 ++ .../templates/email/twofactor_email.html.hbs | 129 ++++++++++++++++++ 4 files changed, 152 insertions(+) create mode 100644 src/static/templates/email/twofactor_email.hbs create mode 100644 src/static/templates/email/twofactor_email.html.hbs diff --git a/src/config.rs b/src/config.rs index 85d97511..6b5d3de6 100644 --- a/src/config.rs +++ b/src/config.rs @@ -530,6 +530,7 @@ fn load_templates(path: &str) -> Handlebars { reg!("email/pw_hint_none", ".html"); reg!("email/pw_hint_some", ".html"); reg!("email/send_org_invite", ".html"); + reg!("email/twofactor_email", ".html"); reg!("admin/base"); reg!("admin/login"); diff --git a/src/mail.rs b/src/mail.rs index c729efad..735a5ecd 100644 --- a/src/mail.rs +++ b/src/mail.rs @@ -156,6 +156,19 @@ pub fn send_new_device_logged_in(address: &str, ip: &str, dt: &NaiveDateTime, de send_email(&address, &subject, &body_html, &body_text) } +pub fn send_token(address: &str, token: &str) -> EmptyResult { + + let (subject, body_html, body_text) = get_text( + "email/twofactor_email", + json!({ + "url": CONFIG.domain(), + "token": token, + }), + )?; + + send_email(&address, &subject, &body_html, &body_text) +} + fn send_email(address: &str, subject: &str, body_html: &str, body_text: &str) -> EmptyResult { let html = PartBuilder::new() .body(encode_to_str(body_html)) diff --git a/src/static/templates/email/twofactor_email.hbs b/src/static/templates/email/twofactor_email.hbs new file mode 100644 index 00000000..b2cb1611 --- /dev/null +++ b/src/static/templates/email/twofactor_email.hbs @@ -0,0 +1,9 @@ +Your Two-step Login Verification Code + + +

+ Your two-step verification code is: {{token}} + + Use this code to complete logging in with Bitwarden. +

+ diff --git a/src/static/templates/email/twofactor_email.html.hbs b/src/static/templates/email/twofactor_email.html.hbs new file mode 100644 index 00000000..32898047 --- /dev/null +++ b/src/static/templates/email/twofactor_email.html.hbs @@ -0,0 +1,129 @@ +Your Two-step Login Verification Code + + + + + + Bitwarden_rs + + + + + + + + + + +
+ + + + +
+ + + + +
+ + + + + + + +
+ Your two-step verification code is: {{token}} +
+ Use this code to complete logging in with Bitwarden. +
+
+ + + + + +
+
+ + From 27e0e41835a09d3e6e5a2d00ad606fc998dda72d Mon Sep 17 00:00:00 2001 From: vpl Date: Sun, 4 Aug 2019 16:56:39 +0200 Subject: [PATCH 2/9] 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 From 29aedd388e0ff6558c4ee57db8c8e74b4d899be8 Mon Sep 17 00:00:00 2001 From: vpl Date: Sat, 3 Aug 2019 18:47:52 +0200 Subject: [PATCH 3/9] Add email code logic and move two_factor into separate modules --- src/api/core/two_factor.rs | 1039 ------------------- src/api/core/two_factor/authenticator.rs | 101 ++ src/api/core/two_factor/duo.rs | 348 +++++++ src/api/core/two_factor/email.rs | 272 +++++ src/api/core/two_factor/mod.rs | 152 +++ src/api/core/two_factor/totp.rs | 46 + src/api/core/two_factor/u2f.rs | 317 ++++++ src/api/core/two_factor/yubikey.rs | 196 ++++ src/api/identity.rs | 41 +- src/api/two_factor.rs | 1208 ---------------------- src/db/models/two_factor.rs | 17 +- 11 files changed, 1462 insertions(+), 2275 deletions(-) delete mode 100644 src/api/core/two_factor.rs create mode 100644 src/api/core/two_factor/authenticator.rs create mode 100644 src/api/core/two_factor/duo.rs create mode 100644 src/api/core/two_factor/email.rs create mode 100644 src/api/core/two_factor/mod.rs create mode 100644 src/api/core/two_factor/totp.rs create mode 100644 src/api/core/two_factor/u2f.rs create mode 100644 src/api/core/two_factor/yubikey.rs delete mode 100644 src/api/two_factor.rs diff --git a/src/api/core/two_factor.rs b/src/api/core/two_factor.rs deleted file mode 100644 index dea5a735..00000000 --- a/src/api/core/two_factor.rs +++ /dev/null @@ -1,1039 +0,0 @@ -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; -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("/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, -} - -#[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: Vec = 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 = "")] -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; - -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 { - 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()) -} diff --git a/src/api/core/two_factor/authenticator.rs b/src/api/core/two_factor/authenticator.rs new file mode 100644 index 00000000..02eff3bf --- /dev/null +++ b/src/api/core/two_factor/authenticator.rs @@ -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 { + routes![ + generate_authenticator, + activate_authenticator, + activate_authenticator_put, + ] +} + +#[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, +} + +#[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: Vec = 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 = "")] +fn activate_authenticator_put(data: JsonUpcase, headers: Headers, conn: DbConn) -> JsonResult { + activate_authenticator(data, headers, conn) +} diff --git a/src/api/core/two_factor/duo.rs b/src/api/core/two_factor/duo.rs new file mode 100644 index 00000000..647cb257 --- /dev/null +++ b/src/api/core/two_factor/duo.rs @@ -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 { + routes![ + get_duo, + activate_duo, + activate_duo_put, + ] +} + +#[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"; + +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 { + 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()) +} \ No newline at end of file diff --git a/src/api/core/two_factor/email.rs b/src/api/core/two_factor/email.rs new file mode 100644 index 00000000..afdf09dc --- /dev/null +++ b/src/api/core/two_factor/email.rs @@ -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 { + 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 = "")] // JsonResult +fn send_email_login(data: JsonUpcase, 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 = "")] +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 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 = "")] +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::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 { + let res: Result = 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"); + } +} diff --git a/src/api/core/two_factor/mod.rs b/src/api/core/two_factor/mod.rs new file mode 100644 index 00000000..85d44f8b --- /dev/null +++ b/src/api/core/two_factor/mod.rs @@ -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 { + 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 = 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) +} diff --git a/src/api/core/two_factor/totp.rs b/src/api/core/two_factor/totp.rs new file mode 100644 index 00000000..1e2ce361 --- /dev/null +++ b/src/api/core/two_factor/totp.rs @@ -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, 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) +} \ No newline at end of file diff --git a/src/api/core/two_factor/u2f.rs b/src/api/core/two_factor/u2f.rs new file mode 100644 index 00000000..1d1d495c --- /dev/null +++ b/src/api/core/two_factor/u2f.rs @@ -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 { + routes![ + generate_u2f, + generate_u2f_challenge, + activate_u2f, + activate_u2f_put, + ] +} + +#[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") +} diff --git a/src/api/core/two_factor/yubikey.rs b/src/api/core/two_factor/yubikey.rs new file mode 100644 index 00000000..fc2c4229 --- /dev/null +++ b/src/api/core/two_factor/yubikey.rs @@ -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 { + routes![ + generate_yubikey, + activate_yubikey, + activate_yubikey_put, + ] +} + +#[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, +} + +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"), + } +} diff --git a/src/api/identity.rs b/src/api/identity.rs index eb0f761b..d3437478 100644 --- a/src/api/identity.rs +++ b/src/api/identity.rs @@ -1,23 +1,18 @@ +use num_traits::FromPrimitive; use rocket::request::{Form, FormItems, FromForm}; use rocket::Route; - use rocket_contrib::json::Json; use serde_json::Value; -use num_traits::FromPrimitive; - -use crate::db::models::*; -use crate::db::DbConn; - -use crate::util; - use crate::api::{ApiResult, EmptyResult, JsonResult}; - +use crate::api::core::two_factor::{duo, email, yubikey}; +use crate::api::core::two_factor::email::EmailTokenData; use crate::auth::ClientIp; - -use crate::mail; - use crate::CONFIG; +use crate::db::DbConn; +use crate::db::models::*; +use crate::mail; +use crate::util; pub fn routes() -> Vec { routes![login] @@ -193,11 +188,11 @@ fn twofactor_auth( let mut remember = data.two_factor_remember.unwrap_or(0); match TwoFactorType::from_i32(selected_id) { - Some(TwoFactorType::Authenticator) => _tf::validate_totp_code_str(twofactor_code, &selected_data?)?, - Some(TwoFactorType::U2f) => _tf::validate_u2f_login(user_uuid, twofactor_code, conn)?, - Some(TwoFactorType::YubiKey) => _tf::validate_yubikey_login(twofactor_code, &selected_data?)?, - 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::Authenticator) => _tf::totp::validate_totp_code_str(twofactor_code, &selected_data?)?, + Some(TwoFactorType::U2f) => _tf::u2f::validate_u2f_login(user_uuid, twofactor_code, conn)?, + Some(TwoFactorType::YubiKey) => _tf::yubikey::validate_yubikey_login(twofactor_code, &selected_data?)?, + Some(TwoFactorType::Duo) => _tf::duo::validate_duo_login(data.username.as_ref().unwrap(), twofactor_code, conn)?, + Some(TwoFactorType::Email) => _tf::email::validate_email_code_str(twofactor_code, &selected_data?)?, Some(TwoFactorType::Remember) => { match device.twofactor_remember { @@ -242,7 +237,7 @@ fn _json_err_twofactor(providers: &[i32], user_uuid: &str, conn: &DbConn) -> Api Some(TwoFactorType::Authenticator) => { /* Nothing to do for TOTP */ } Some(TwoFactorType::U2f) if CONFIG.domain_set() => { - let request = two_factor::generate_u2f_login(user_uuid, conn)?; + let request = two_factor::u2f::generate_u2f_login(user_uuid, conn)?; let mut challenge_list = Vec::new(); for key in request.registered_keys { @@ -267,7 +262,7 @@ fn _json_err_twofactor(providers: &[i32], user_uuid: &str, conn: &DbConn) -> Api None => err!("User does not exist"), }; - let (signature, host) = two_factor::generate_duo_signature(&email, conn)?; + let (signature, host) = duo::generate_duo_signature(&email, conn)?; result["TwoFactorProviders2"][provider.to_string()] = json!({ "Host": host, @@ -281,7 +276,7 @@ fn _json_err_twofactor(providers: &[i32], user_uuid: &str, conn: &DbConn) -> Api None => err!("No YubiKey devices registered"), }; - let yubikey_metadata: two_factor::YubikeyMetadata = serde_json::from_str(&twofactor.data)?; + let yubikey_metadata: yubikey::YubikeyMetadata = serde_json::from_str(&twofactor.data)?; result["TwoFactorProviders2"][provider.to_string()] = json!({ "Nfc": yubikey_metadata.Nfc, @@ -293,6 +288,12 @@ fn _json_err_twofactor(providers: &[i32], user_uuid: &str, conn: &DbConn) -> Api Some(tf) => tf, None => err!("No twofactor email registered"), }; + + let email_data = EmailTokenData::from_json(&twofactor.data)?; + + result["TwoFactorProviders2"][provider.to_string()] = json!({ + "Email": email::obscure_email(&email_data.Email), + }) } _ => {} diff --git a/src/api/two_factor.rs b/src/api/two_factor.rs deleted file mode 100644 index 70f43064..00000000 --- a/src/api/two_factor.rs +++ /dev/null @@ -1,1208 +0,0 @@ -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 diff --git a/src/db/models/two_factor.rs b/src/db/models/two_factor.rs index 30a7b475..a390e219 100644 --- a/src/db/models/two_factor.rs +++ b/src/db/models/two_factor.rs @@ -1,5 +1,12 @@ +use diesel; +use diesel::prelude::*; use serde_json::Value; +use crate::api::EmptyResult; +use crate::db::DbConn; +use crate::db::schema::twofactor; +use crate::error::MapResult; + use super::User; #[derive(Debug, Identifiable, Queryable, Insertable, Associations)] @@ -28,6 +35,8 @@ pub enum TwoFactorType { // These are implementation details U2fRegisterChallenge = 1000, U2fLoginChallenge = 1001, + EmailVerificationChallenge = 1002, + } /// Local methods @@ -59,14 +68,6 @@ impl TwoFactor { } } -use crate::db::schema::twofactor; -use crate::db::DbConn; -use diesel; -use diesel::prelude::*; - -use crate::api::EmptyResult; -use crate::error::MapResult; - /// Database methods impl TwoFactor { pub fn save(&self, conn: &DbConn) -> EmptyResult { From efd8d9f5284abb8a9710b80bee2d863458a096dd Mon Sep 17 00:00:00 2001 From: vpl Date: Sat, 3 Aug 2019 20:09:20 +0200 Subject: [PATCH 4/9] Remove some unused imports, unneeded mut variables --- src/api/core/two_factor/authenticator.rs | 9 ++--- src/api/core/two_factor/duo.rs | 8 ++--- src/api/core/two_factor/email.rs | 46 ++++++++++++------------ src/api/core/two_factor/mod.rs | 9 ++--- src/api/core/two_factor/u2f.rs | 6 ++-- src/api/core/two_factor/yubikey.rs | 18 ++++------ src/api/identity.rs | 2 +- 7 files changed, 40 insertions(+), 58 deletions(-) diff --git a/src/api/core/two_factor/authenticator.rs b/src/api/core/two_factor/authenticator.rs index 02eff3bf..5ea2308e 100644 --- a/src/api/core/two_factor/authenticator.rs +++ b/src/api/core/two_factor/authenticator.rs @@ -1,18 +1,15 @@ -use data_encoding::{BASE32, BASE64}; +use data_encoding::{BASE32}; 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::{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}, + models::{TwoFactor, TwoFactorType}, }; -use crate::error::{Error, MapResult}; const TOTP_TIME_STEP: u64 = 30; diff --git a/src/api/core/two_factor/duo.rs b/src/api/core/two_factor/duo.rs index 647cb257..80d1411f 100644 --- a/src/api/core/two_factor/duo.rs +++ b/src/api/core/two_factor/duo.rs @@ -1,12 +1,10 @@ use chrono::Utc; -use data_encoding::{BASE32, BASE64}; -use oath::{HashType, totp_raw_now}; +use data_encoding::{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::{ApiResult, EmptyResult, JsonResult, JsonUpcase, PasswordData}; use crate::auth::Headers; use crate::CONFIG; use crate::crypto; @@ -14,7 +12,7 @@ use crate::db::{ DbConn, models::{TwoFactor, TwoFactorType, User}, }; -use crate::error::{Error, MapResult}; +use crate::error::{MapResult}; pub fn routes() -> Vec { routes![ diff --git a/src/api/core/two_factor/email.rs b/src/api/core/two_factor/email.rs index afdf09dc..16d61832 100644 --- a/src/api/core/two_factor/email.rs +++ b/src/api/core/two_factor/email.rs @@ -1,20 +1,17 @@ -use data_encoding::{BASE32, BASE64}; -use lettre_email::Email; +use data_encoding::{BASE32}; 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::api::{EmptyResult, JsonResult, JsonUpcase, PasswordData}; use crate::auth::Headers; use crate::db::{ - models::{TwoFactor, TwoFactorType, User}, + models::{TwoFactor, TwoFactorType}, DbConn, }; -use crate::error::{Error, MapResult}; +use crate::error::{Error}; use crate::{crypto, mail}; const TOTP_TIME_STEP: u64 = 120; @@ -38,7 +35,7 @@ fn send_email_login(data: JsonUpcase, conn: DbConn) -> Empty use crate::db::models::User; // Get the user - let mut user = match User::find_by_mail(&data.Email, &conn) { + let user = match User::find_by_mail(&data.Email, &conn) { Some(user) => user, None => err!("Username or password is incorrect. Try again."), }; @@ -49,16 +46,16 @@ fn send_email_login(data: JsonUpcase, conn: DbConn) -> Empty } let type_ = TwoFactorType::Email as i32; - let mut twofactor = TwoFactor::find_by_user_and_type(&user.uuid, type_, &conn)?; + let 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 decoded_key = totp::validate_decode_key(&twofactor_data.totp_secret)?; 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)?; + mail::send_token(&twofactor_data.email, &token_string)?; Ok(()) } @@ -119,7 +116,7 @@ fn send_email(data: JsonUpcase, headers: Headers, conn: DbConn) - 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( + let twofactor = TwoFactor::new( user.uuid, TwoFactorType::EmailVerificationChallenge, twofactor_data.to_json(), @@ -129,7 +126,7 @@ fn send_email(data: JsonUpcase, headers: Headers, conn: DbConn) - 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)?; + mail::send_token(&twofactor_data.email, &token_string)?; Ok(()) } @@ -162,13 +159,13 @@ fn email(data: JsonUpcase, headers: Headers, conn: DbConn) -> JsonRes let email_data = EmailTokenData::from_json(&twofactor.data)?; - totp::validate_totp_code_with_time_step(token_u64, &email_data.TotpSecret, TOTP_TIME_STEP)?; + totp::validate_totp_code_with_time_step(token_u64, &email_data.totp_secret, TOTP_TIME_STEP)?; twofactor.atype = TwoFactorType::Email as i32; twofactor.save(&conn)?; Ok(Json(json!({ - "Email": email_data.Email, + "Email": email_data.email, "Enabled": "true", "Object": "twoFactorEmail" }))) @@ -186,7 +183,7 @@ pub fn validate_email_code_str(code: &str, data: &str) -> EmptyResult { 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()) { + let decoded_secret = match BASE32.decode(email_data.totp_secret.as_bytes()) { Ok(s) => s, Err(_) => err!("Invalid email secret"), }; @@ -201,15 +198,15 @@ pub fn validate_email_code(code: u64, data: &str) -> EmptyResult { #[derive(Serialize, Deserialize)] pub struct EmailTokenData { - pub Email: String, - pub TotpSecret: String, + pub email: String, + pub totp_secret: String, } impl EmailTokenData { - pub fn new(email: String, secret: String) -> EmailTokenData { + pub fn new(email: String, totp_secret: String) -> EmailTokenData { EmailTokenData { - Email: email, - TotpSecret: secret, + email, + totp_secret, } } @@ -226,6 +223,7 @@ impl EmailTokenData { } } +/// 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(); @@ -235,7 +233,7 @@ pub fn obscure_email(email: &str) -> String { let name_size = name.chars().count(); let new_name = match name_size { - 1..=2 => "*".repeat(name_size), + 1..=3 => "*".repeat(name_size), _ => { let stars = "*".repeat(name_size-2); name.truncate(2); @@ -262,11 +260,11 @@ mod tests { #[test] fn test_obscure_email_short() { - let email = "by@example.ext"; + 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"); + assert_eq!(result, "***@example.ext"); } } diff --git a/src/api/core/two_factor/mod.rs b/src/api/core/two_factor/mod.rs index 85d44f8b..6190fc20 100644 --- a/src/api/core/two_factor/mod.rs +++ b/src/api/core/two_factor/mod.rs @@ -1,19 +1,16 @@ -use chrono::Utc; -use data_encoding::{BASE32, BASE64}; -use oath::{HashType, totp_raw_now}; +use data_encoding::{BASE32}; 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::{JsonResult, JsonUpcase, NumberOrString, PasswordData}; use crate::auth::Headers; use crate::crypto; use crate::db::{ DbConn, - models::{TwoFactor, TwoFactorType, User}, + models::{TwoFactor, User}, }; -use crate::error::{Error, MapResult}; pub(crate) mod authenticator; pub(crate) mod duo; diff --git a/src/api/core/two_factor/u2f.rs b/src/api/core/two_factor/u2f.rs index 1d1d495c..0bf9cfb7 100644 --- a/src/api/core/two_factor/u2f.rs +++ b/src/api/core/two_factor/u2f.rs @@ -1,4 +1,3 @@ -use data_encoding::{BASE32, BASE64}; use rocket::Route; use rocket_contrib::json::Json; use serde_json; @@ -11,12 +10,11 @@ use crate::api::{ApiResult, EmptyResult, JsonResult, JsonUpcase, NumberOrString, 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}, + models::{TwoFactor, TwoFactorType}, }; -use crate::error::{Error, MapResult}; +use crate::error::{Error}; const U2F_VERSION: &str = "U2F_V2"; diff --git a/src/api/core/two_factor/yubikey.rs b/src/api/core/two_factor/yubikey.rs index fc2c4229..a7b91de3 100644 --- a/src/api/core/two_factor/yubikey.rs +++ b/src/api/core/two_factor/yubikey.rs @@ -1,4 +1,3 @@ -use data_encoding::{BASE32, BASE64}; use rocket::Route; use rocket_contrib::json::Json; use serde_json; @@ -6,23 +5,18 @@ 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::api::{EmptyResult, JsonResult, JsonUpcase, PasswordData}; use crate::auth::Headers; -use crate::CONFIG; -use crate::crypto; use crate::db::{ + models::{TwoFactor, TwoFactorType}, DbConn, - models::{TwoFactor, TwoFactorType, User}, }; use crate::error::{Error, MapResult}; +use crate::CONFIG; pub fn routes() -> Vec { - routes![ - generate_yubikey, - activate_yubikey, - activate_yubikey_put, - ] + routes![generate_yubikey, activate_yubikey, activate_yubikey_put,] } #[derive(Deserialize, Debug)] @@ -76,8 +70,8 @@ fn verify_yubikey_otp(otp: String) -> EmptyResult { Some(server) => verify(otp, config.set_api_hosts(vec![server])), None => verify(otp, config), } - .map_res("Failed to verify OTP") - .and(Ok(())) + .map_res("Failed to verify OTP") + .and(Ok(())) } #[post("/two-factor/get-yubikey", data = "")] diff --git a/src/api/identity.rs b/src/api/identity.rs index d3437478..b6163625 100644 --- a/src/api/identity.rs +++ b/src/api/identity.rs @@ -292,7 +292,7 @@ fn _json_err_twofactor(providers: &[i32], user_uuid: &str, conn: &DbConn) -> Api let email_data = EmailTokenData::from_json(&twofactor.data)?; result["TwoFactorProviders2"][provider.to_string()] = json!({ - "Email": email::obscure_email(&email_data.Email), + "Email": email::obscure_email(&email_data.email), }) } From 6d460b44b0484f7c191a807d8608f958cd1a7a23 Mon Sep 17 00:00:00 2001 From: vpl Date: Sun, 4 Aug 2019 16:55:43 +0200 Subject: [PATCH 5/9] Use saved token for email 2fa codes --- Cargo.lock | 1 + Cargo.toml | 3 + src/api/core/two_factor/authenticator.rs | 38 ++++-- src/api/core/two_factor/duo.rs | 12 +- src/api/core/two_factor/email.rs | 148 +++++++++++++---------- src/api/core/two_factor/mod.rs | 5 +- src/api/core/two_factor/totp.rs | 46 ------- src/api/core/two_factor/u2f.rs | 8 +- src/api/core/two_factor/yubikey.rs | 6 +- src/api/identity.rs | 17 +-- src/db/models/two_factor.rs | 3 +- 11 files changed, 148 insertions(+), 139 deletions(-) delete mode 100644 src/api/core/two_factor/totp.rs diff --git a/Cargo.lock b/Cargo.lock index 1a677d4f..f616897f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -118,6 +118,7 @@ dependencies = [ "oath 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)", "percent-encoding 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "quoted_printable 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", "regex 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "reqwest 0.9.19 (registry+https://github.com/rust-lang/crates.io-index)", "ring 0.14.6 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/Cargo.toml b/Cargo.toml index 231c1c2f..bac6eb4b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -108,6 +108,9 @@ regex = "1.2.0" # URL encoding library percent-encoding = "2.0.0" +# Random +rand = "0.7.0" + [patch.crates-io] # Add support for Timestamp type rmp = { git = 'https://github.com/dani-garcia/msgpack-rust' } diff --git a/src/api/core/two_factor/authenticator.rs b/src/api/core/two_factor/authenticator.rs index 5ea2308e..eeb91c46 100644 --- a/src/api/core/two_factor/authenticator.rs +++ b/src/api/core/two_factor/authenticator.rs @@ -1,18 +1,16 @@ -use data_encoding::{BASE32}; +use data_encoding::BASE32; use rocket::Route; use rocket_contrib::json::Json; -use crate::api::{JsonResult, JsonUpcase, NumberOrString, PasswordData}; -use crate::api::core::two_factor::{_generate_recover_code, totp}; +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::{ - DbConn, models::{TwoFactor, TwoFactorType}, + DbConn, }; -const TOTP_TIME_STEP: u64 = 30; - pub fn routes() -> Vec { routes![ generate_authenticator, @@ -20,7 +18,6 @@ pub fn routes() -> Vec { activate_authenticator_put, ] } - #[post("/two-factor/get-authenticator", data = "")] fn generate_authenticator(data: JsonUpcase, headers: Headers, conn: DbConn) -> JsonResult { let data: PasswordData = data.into_inner().data; @@ -80,7 +77,7 @@ fn activate_authenticator(data: JsonUpcase, headers: He let twofactor = TwoFactor::new(user.uuid.clone(), type_, key.to_uppercase()); // Validate the token provided with the key - totp::validate_totp_code(token, &twofactor.data)?; + validate_totp_code(token, &twofactor.data)?; _generate_recover_code(&mut user, &conn); twofactor.save(&conn)?; @@ -96,3 +93,28 @@ fn activate_authenticator(data: JsonUpcase, headers: He 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(()) +} diff --git a/src/api/core/two_factor/duo.rs b/src/api/core/two_factor/duo.rs index 80d1411f..b291bd11 100644 --- a/src/api/core/two_factor/duo.rs +++ b/src/api/core/two_factor/duo.rs @@ -1,18 +1,18 @@ use chrono::Utc; -use data_encoding::{BASE64}; +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::CONFIG; use crate::crypto; use crate::db::{ - DbConn, models::{TwoFactor, TwoFactorType, User}, + DbConn, }; -use crate::error::{MapResult}; +use crate::error::MapResult; +use crate::CONFIG; pub fn routes() -> Vec { routes![ @@ -71,7 +71,7 @@ enum DuoStatus { // Using the global duo config User(DuoData), // Using the user's config - Disabled(bool), // True if there is a global setting + Disabled(bool), // True if there is a global setting } impl DuoStatus { @@ -343,4 +343,4 @@ fn parse_duo_values(key: &str, val: &str, ikey: &str, prefix: &str, time: i64) - } Ok(username.into()) -} \ No newline at end of file +} diff --git a/src/api/core/two_factor/email.rs b/src/api/core/two_factor/email.rs index 16d61832..ae5900e9 100644 --- a/src/api/core/two_factor/email.rs +++ b/src/api/core/two_factor/email.rs @@ -1,23 +1,29 @@ -use data_encoding::{BASE32}; -use oath::{totp_raw_now, HashType}; use rocket::Route; use rocket_contrib::json::Json; use serde_json; -use crate::api::core::two_factor::totp; use crate::api::{EmptyResult, JsonResult, JsonUpcase, PasswordData}; use crate::auth::Headers; use crate::db::{ models::{TwoFactor, TwoFactorType}, DbConn, }; -use crate::error::{Error}; -use crate::{crypto, mail}; +use crate::error::Error; +use crate::mail; +use chrono::{Duration, NaiveDateTime, Utc}; +use rand::Rng; +use std::char; +use std::ops::Add; -const TOTP_TIME_STEP: u64 = 120; +const MAX_TIME_DIFFERENCE: i64 = 600; pub fn routes() -> Vec { - routes![get_email, send_email_login, send_email, email,] + routes![ + get_email, + send_email_login, + send_email, + email, + ] } #[derive(Deserialize)] @@ -27,7 +33,8 @@ struct SendEmailLoginData { MasterPasswordHash: String, } -// Does not require Bearer token +/// User is trying to login and wants to use email 2FA. +/// Does not require Bearer token #[post("/two-factor/send-email-login", data = "")] // JsonResult fn send_email_login(data: JsonUpcase, conn: DbConn) -> EmptyResult { let data: SendEmailLoginData = data.into_inner().data; @@ -46,16 +53,15 @@ fn send_email_login(data: JsonUpcase, conn: DbConn) -> Empty } let type_ = TwoFactorType::Email as i32; - let twofactor = TwoFactor::find_by_user_and_type(&user.uuid, type_, &conn)?; + let mut twofactor = TwoFactor::find_by_user_and_type(&user.uuid, type_, &conn)?; - let twofactor_data = EmailTokenData::from_json(&twofactor.data)?; + let generated_token = generate_token(); + let mut twofactor_data = EmailTokenData::from_json(&twofactor.data)?; + twofactor_data.set_token(generated_token); + twofactor.data = twofactor_data.to_json(); + twofactor.save(&conn)?; - let decoded_key = totp::validate_decode_key(&twofactor_data.totp_secret)?; - - 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)?; + mail::send_token(&twofactor_data.email, &twofactor_data.last_token?)?; Ok(()) } @@ -75,7 +81,7 @@ fn get_email(data: JsonUpcase, headers: Headers, conn: DbConn) -> _ => false, }; - Ok(Json(json!({// TODO check! FIX! + Ok(Json(json!({ "Email": user.email, "Enabled": enabled, "Object": "twoFactorEmail" @@ -85,16 +91,26 @@ fn get_email(data: JsonUpcase, headers: Headers, conn: DbConn) -> #[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, - // 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. +fn generate_token() -> String { + const TOKEN_LEN: usize = 6; + let mut rng = rand::thread_rng(); + + (0..TOKEN_LEN) + .map(|_| { + let num = rng.gen_range(0, 9); + char::from_digit(num, 10).unwrap() + }) + .collect() +} + +/// 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; @@ -104,16 +120,12 @@ fn send_email(data: JsonUpcase, headers: Headers, conn: DbConn) - 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(()), - }; + if let Some(tf) = TwoFactor::find_by_user_and_type(&user.uuid, type_, &conn) { + tf.delete(&conn)?; + } - let secret = crypto::get_random(vec![0u8; 20]); - let base32_secret = BASE32.encode(&secret); - - let twofactor_data = EmailTokenData::new(data.Email, base32_secret); + let generated_token = generate_token(); + 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( @@ -123,10 +135,7 @@ fn send_email(data: JsonUpcase, headers: Headers, conn: DbConn) - ); 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)?; + mail::send_token(&twofactor_data.email, &twofactor_data.last_token?)?; Ok(()) } @@ -139,7 +148,7 @@ struct EmailData { Token: String, } -// Verify email used for 2FA email codes. +/// Verify email belongs to user and can be 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; @@ -149,19 +158,23 @@ fn email(data: JsonUpcase, headers: Headers, conn: DbConn) -> JsonRes err!("Invalid password"); } - let token_u64 = match data.Token.parse::() { - 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)?; + let mut email_data = EmailTokenData::from_json(&twofactor.data)?; - totp::validate_totp_code_with_time_step(token_u64, &email_data.totp_secret, TOTP_TIME_STEP)?; + let issued_token = match &email_data.last_token { + Some(t) => t, + _ => err!("No token available"), + }; + if issued_token != &data.Token { + err!("Email token does not match") + } + + email_data.reset_token(); twofactor.atype = TwoFactorType::Email as i32; + twofactor.data = email_data.to_json(); twofactor.save(&conn)?; Ok(Json(json!({ @@ -171,26 +184,26 @@ fn email(data: JsonUpcase, headers: Headers, conn: DbConn) -> JsonRes }))) } -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 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"), }; - validate_email_code(totp_code, data) -} + if issued_token != &*token { + err!("Email token does not match") + } -pub fn validate_email_code(code: u64, data: &str) -> EmptyResult { - let email_data = EmailTokenData::from_json(&data)?; + email_data.reset_token(); + twofactor.data = email_data.to_json(); + twofactor.save(&conn)?; - let decoded_secret = match BASE32.decode(email_data.totp_secret.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"); + let date = NaiveDateTime::from_timestamp(email_data.token_sent, 0); + if date.add(Duration::seconds(MAX_TIME_DIFFERENCE)) < Utc::now().naive_utc() { + err!("Email token too old") } Ok(()) @@ -199,17 +212,28 @@ pub fn validate_email_code(code: u64, data: &str) -> EmptyResult { #[derive(Serialize, Deserialize)] pub struct EmailTokenData { pub email: String, - pub totp_secret: String, + pub last_token: Option, + pub token_sent: i64, } impl EmailTokenData { - pub fn new(email: String, totp_secret: String) -> EmailTokenData { + pub fn new(email: String, token: String) -> EmailTokenData { EmailTokenData { email, - totp_secret, + last_token: Some(token), + token_sent: Utc::now().naive_utc().timestamp(), } } + 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; + } + pub fn to_json(&self) -> String { serde_json::to_string(&self).unwrap() } @@ -235,7 +259,7 @@ pub fn obscure_email(email: &str) -> String { let new_name = match name_size { 1..=3 => "*".repeat(name_size), _ => { - let stars = "*".repeat(name_size-2); + let stars = "*".repeat(name_size - 2); name.truncate(2); format!("{}{}", name, stars) } diff --git a/src/api/core/two_factor/mod.rs b/src/api/core/two_factor/mod.rs index 6190fc20..7a53d9c8 100644 --- a/src/api/core/two_factor/mod.rs +++ b/src/api/core/two_factor/mod.rs @@ -1,4 +1,4 @@ -use data_encoding::{BASE32}; +use data_encoding::BASE32; use rocket::Route; use rocket_contrib::json::Json; use serde_json; @@ -8,8 +8,8 @@ use crate::api::{JsonResult, JsonUpcase, NumberOrString, PasswordData}; use crate::auth::Headers; use crate::crypto; use crate::db::{ - DbConn, models::{TwoFactor, User}, + DbConn, }; pub(crate) mod authenticator; @@ -17,7 +17,6 @@ pub(crate) mod duo; pub(crate) mod email; pub(crate) mod u2f; pub(crate) mod yubikey; -pub(crate) mod totp; pub fn routes() -> Vec { let mut routes = routes![ diff --git a/src/api/core/two_factor/totp.rs b/src/api/core/two_factor/totp.rs deleted file mode 100644 index 1e2ce361..00000000 --- a/src/api/core/two_factor/totp.rs +++ /dev/null @@ -1,46 +0,0 @@ -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, 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) -} \ No newline at end of file diff --git a/src/api/core/two_factor/u2f.rs b/src/api/core/two_factor/u2f.rs index 0bf9cfb7..5ccf2914 100644 --- a/src/api/core/two_factor/u2f.rs +++ b/src/api/core/two_factor/u2f.rs @@ -6,15 +6,15 @@ 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::api::{ApiResult, EmptyResult, JsonResult, JsonUpcase, NumberOrString, PasswordData}; use crate::auth::Headers; -use crate::CONFIG; use crate::db::{ - DbConn, models::{TwoFactor, TwoFactorType}, + DbConn, }; -use crate::error::{Error}; +use crate::error::Error; +use crate::CONFIG; const U2F_VERSION: &str = "U2F_V2"; diff --git a/src/api/core/two_factor/yubikey.rs b/src/api/core/two_factor/yubikey.rs index a7b91de3..3fef249d 100644 --- a/src/api/core/two_factor/yubikey.rs +++ b/src/api/core/two_factor/yubikey.rs @@ -16,7 +16,11 @@ use crate::error::{Error, MapResult}; use crate::CONFIG; pub fn routes() -> Vec { - routes![generate_yubikey, activate_yubikey, activate_yubikey_put,] + routes![ + generate_yubikey, + activate_yubikey, + activate_yubikey_put, + ] } #[derive(Deserialize, Debug)] diff --git a/src/api/identity.rs b/src/api/identity.rs index b6163625..7e60eb3b 100644 --- a/src/api/identity.rs +++ b/src/api/identity.rs @@ -4,15 +4,15 @@ use rocket::Route; use rocket_contrib::json::Json; use serde_json::Value; -use crate::api::{ApiResult, EmptyResult, JsonResult}; -use crate::api::core::two_factor::{duo, email, yubikey}; use crate::api::core::two_factor::email::EmailTokenData; +use crate::api::core::two_factor::{duo, email, yubikey}; +use crate::api::{ApiResult, EmptyResult, JsonResult}; use crate::auth::ClientIp; -use crate::CONFIG; -use crate::db::DbConn; use crate::db::models::*; +use crate::db::DbConn; use crate::mail; use crate::util; +use crate::CONFIG; pub fn routes() -> Vec { routes![login] @@ -179,7 +179,10 @@ 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 && tf.enabled).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; @@ -188,11 +191,11 @@ fn twofactor_auth( let mut remember = data.two_factor_remember.unwrap_or(0); match TwoFactorType::from_i32(selected_id) { - Some(TwoFactorType::Authenticator) => _tf::totp::validate_totp_code_str(twofactor_code, &selected_data?)?, + Some(TwoFactorType::Authenticator) => _tf::authenticator::validate_totp_code_str(twofactor_code, &selected_data?)?, Some(TwoFactorType::U2f) => _tf::u2f::validate_u2f_login(user_uuid, twofactor_code, conn)?, Some(TwoFactorType::YubiKey) => _tf::yubikey::validate_yubikey_login(twofactor_code, &selected_data?)?, Some(TwoFactorType::Duo) => _tf::duo::validate_duo_login(data.username.as_ref().unwrap(), twofactor_code, conn)?, - Some(TwoFactorType::Email) => _tf::email::validate_email_code_str(twofactor_code, &selected_data?)?, + Some(TwoFactorType::Email) => _tf::email::validate_email_code_str(user_uuid, twofactor_code, &selected_data?, conn)?, Some(TwoFactorType::Remember) => { match device.twofactor_remember { diff --git a/src/db/models/two_factor.rs b/src/db/models/two_factor.rs index a390e219..233d5312 100644 --- a/src/db/models/two_factor.rs +++ b/src/db/models/two_factor.rs @@ -3,8 +3,8 @@ use diesel::prelude::*; use serde_json::Value; use crate::api::EmptyResult; -use crate::db::DbConn; use crate::db::schema::twofactor; +use crate::db::DbConn; use crate::error::MapResult; use super::User; @@ -36,7 +36,6 @@ pub enum TwoFactorType { U2fRegisterChallenge = 1000, U2fLoginChallenge = 1001, EmailVerificationChallenge = 1002, - } /// Local methods From 5609103a97f44c7dc4aa7269d80f2df30d50e276 Mon Sep 17 00:00:00 2001 From: vpl Date: Tue, 6 Aug 2019 22:37:23 +0200 Subject: [PATCH 6/9] Use ring to generate email token --- Cargo.lock | 1 - Cargo.toml | 3 --- src/api/core/two_factor/email.rs | 24 ++++++++++++++++-------- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f616897f..1a677d4f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -118,7 +118,6 @@ dependencies = [ "oath 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)", "percent-encoding 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "quoted_printable 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", - "rand 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", "regex 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "reqwest 0.9.19 (registry+https://github.com/rust-lang/crates.io-index)", "ring 0.14.6 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/Cargo.toml b/Cargo.toml index bac6eb4b..231c1c2f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -108,9 +108,6 @@ regex = "1.2.0" # URL encoding library percent-encoding = "2.0.0" -# Random -rand = "0.7.0" - [patch.crates-io] # Add support for Timestamp type rmp = { git = 'https://github.com/dani-garcia/msgpack-rust' } diff --git a/src/api/core/two_factor/email.rs b/src/api/core/two_factor/email.rs index ae5900e9..93e89b1f 100644 --- a/src/api/core/two_factor/email.rs +++ b/src/api/core/two_factor/email.rs @@ -10,12 +10,14 @@ use crate::db::{ }; use crate::error::Error; use crate::mail; +use crate::crypto; + use chrono::{Duration, NaiveDateTime, Utc}; -use rand::Rng; use std::char; use std::ops::Add; const MAX_TIME_DIFFERENCE: i64 = 600; +const TOKEN_LEN: usize = 6; pub fn routes() -> Vec { routes![ @@ -97,13 +99,12 @@ struct SendEmailData { } fn generate_token() -> String { - const TOKEN_LEN: usize = 6; - let mut rng = rand::thread_rng(); - - (0..TOKEN_LEN) - .map(|_| { - let num = rng.gen_range(0, 9); - char::from_digit(num, 10).unwrap() + crypto::get_random(vec![0; TOKEN_LEN]) + .iter() + .map(|byte| { (byte % 10)}) + .map(|num| { + dbg!(num); + char::from_digit(num as u32, 10).unwrap() }) .collect() } @@ -291,4 +292,11 @@ mod tests { // 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(); + + assert_eq!(result.chars().count(), 6); + } } From ad2225b6e58bcaf2f6fd9058a316c028b0b3ec8f Mon Sep 17 00:00:00 2001 From: vpl Date: Sat, 10 Aug 2019 22:33:39 +0200 Subject: [PATCH 7/9] Add configuration options for Email 2FA --- src/api/core/two_factor/email.rs | 58 +++++++++++++++++++++++--------- src/config.rs | 12 +++++++ 2 files changed, 54 insertions(+), 16 deletions(-) diff --git a/src/api/core/two_factor/email.rs b/src/api/core/two_factor/email.rs index 93e89b1f..c96ed94d 100644 --- a/src/api/core/two_factor/email.rs +++ b/src/api/core/two_factor/email.rs @@ -4,21 +4,19 @@ 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::crypto; +use crate::CONFIG; use chrono::{Duration, NaiveDateTime, Utc}; use std::char; use std::ops::Add; -const MAX_TIME_DIFFERENCE: i64 = 600; -const TOKEN_LEN: usize = 6; - pub fn routes() -> Vec { routes![ get_email, @@ -54,10 +52,14 @@ fn send_email_login(data: JsonUpcase, conn: DbConn) -> Empty 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(); + 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(); @@ -68,6 +70,7 @@ fn send_email_login(data: JsonUpcase, conn: DbConn) -> Empty Ok(()) } +/// When user clicks on Manage email 2FA show the user the related information #[post("/two-factor/get-email", data = "")] fn get_email(data: JsonUpcase, headers: Headers, conn: DbConn) -> JsonResult { let data: PasswordData = data.into_inner().data; @@ -98,12 +101,11 @@ struct SendEmailData { MasterPasswordHash: String, } -fn generate_token() -> String { - crypto::get_random(vec![0; TOKEN_LEN]) +fn generate_token(token_size: u64) -> String { + crypto::get_random(vec![0; token_size as usize]) .iter() .map(|byte| { (byte % 10)}) .map(|num| { - dbg!(num); char::from_digit(num as u32, 10).unwrap() }) .collect() @@ -119,13 +121,17 @@ fn send_email(data: JsonUpcase, headers: Headers, conn: DbConn) - 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(); + 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. @@ -170,7 +176,7 @@ fn email(data: JsonUpcase, headers: Headers, conn: DbConn) -> JsonRes }; if issued_token != &data.Token { - err!("Email token does not match") + err!("Token is invalid") } email_data.reset_token(); @@ -195,7 +201,14 @@ pub fn validate_email_code_str(user_uuid: &str, token: &str, data: &str, conn: & }; if issued_token != &*token { - err!("Email token does not match") + 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(); @@ -203,18 +216,25 @@ pub fn validate_email_code_str(user_uuid: &str, token: &str, data: &str, conn: & twofactor.save(&conn)?; let date = NaiveDateTime::from_timestamp(email_data.token_sent, 0); - if date.add(Duration::seconds(MAX_TIME_DIFFERENCE)) < Utc::now().naive_utc() { - err!("Email token too old") + 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, + /// UNIX timestamp of token issue. pub token_sent: i64, + /// Amount of token entry attempts for last_token. + pub attempts: u64, } impl EmailTokenData { @@ -223,6 +243,7 @@ impl EmailTokenData { email, last_token: Some(token), token_sent: Utc::now().naive_utc().timestamp(), + attempts: 0, } } @@ -233,6 +254,11 @@ impl EmailTokenData { 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 { @@ -295,8 +321,8 @@ mod tests { #[test] fn test_token() { - let result = generate_token(); + let result = generate_token(100); - assert_eq!(result.chars().count(), 6); + assert_eq!(result.chars().count(), 100); } } diff --git a/src/config.rs b/src/config.rs index 6b5d3de6..31754e07 100644 --- a/src/config.rs +++ b/src/config.rs @@ -318,6 +318,18 @@ make_config! { _duo_akey: Pass, false, option; }, + /// Email 2FA Settings + email_2fa: _enable_email_2fa { + /// Enabled |> Disabling will prevent users from setting up new email 2FA and using existing email 2FA configured + _enable_email_2fa: bool, true, def, true; + /// Token number length |> Length of the numbers in an email token + email_token_size: u64, true, def, 6; + /// Token expiration time |> Maximum time in seconds a token is valid. The time the user has to open email client and copy token. + email_expiration_time: u64, true, def, 600; + /// Maximum attempts |> Maximum attempts before an email token is reset and a new email will need to be sent + email_attempts_limit: u64, true, def, 3; + }, + /// SMTP Email Settings smtp: _enable_smtp { /// Enabled From 591ae101448a19555fce5bca4f5caed0ff82457c Mon Sep 17 00:00:00 2001 From: vpl Date: Mon, 26 Aug 2019 20:26:54 +0200 Subject: [PATCH 8/9] Get token from single u64 --- src/api/core/two_factor/email.rs | 37 +++++++++++++++++++++----------- src/config.rs | 12 +++++++++-- 2 files changed, 35 insertions(+), 14 deletions(-) diff --git a/src/api/core/two_factor/email.rs b/src/api/core/two_factor/email.rs index c96ed94d..1986f816 100644 --- a/src/api/core/two_factor/email.rs +++ b/src/api/core/two_factor/email.rs @@ -59,7 +59,7 @@ fn send_email_login(data: JsonUpcase, conn: DbConn) -> Empty 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 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(); @@ -101,14 +101,20 @@ struct SendEmailData { MasterPasswordHash: String, } -fn generate_token(token_size: u64) -> String { - crypto::get_random(vec![0; token_size as usize]) - .iter() - .map(|byte| { (byte % 10)}) - .map(|num| { - char::from_digit(num as u32, 10).unwrap() - }) - .collect() + +fn generate_token(token_size: u32) -> Result { + 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. @@ -131,7 +137,7 @@ fn send_email(data: JsonUpcase, headers: Headers, conn: DbConn) - tf.delete(&conn)?; } - let generated_token = generate_token(CONFIG.email_token_size()); + 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. @@ -321,8 +327,15 @@ mod tests { #[test] fn test_token() { - let result = generate_token(100); + let result = generate_token(19).unwrap(); - assert_eq!(result.chars().count(), 100); + 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"); } } diff --git a/src/config.rs b/src/config.rs index 31754e07..49f41bee 100644 --- a/src/config.rs +++ b/src/config.rs @@ -322,8 +322,8 @@ make_config! { email_2fa: _enable_email_2fa { /// Enabled |> Disabling will prevent users from setting up new email 2FA and using existing email 2FA configured _enable_email_2fa: bool, true, def, true; - /// Token number length |> Length of the numbers in an email token - email_token_size: u64, true, def, 6; + /// Token number length |> Length of the numbers in an email token. Minimum of 6. Maximum is 19. + email_token_size: u32, true, def, 6; /// Token expiration time |> Maximum time in seconds a token is valid. The time the user has to open email client and copy token. email_expiration_time: u64, true, def, 600; /// Maximum attempts |> Maximum attempts before an email token is reset and a new email will need to be sent @@ -378,6 +378,14 @@ fn validate_config(cfg: &ConfigItems) -> Result<(), Error> { err!("Both `SMTP_USERNAME` and `SMTP_PASSWORD` need to be set to enable email authentication") } + if cfg.email_token_size < 6 { + err!("`EMAIL_TOKEN_SIZE` has a minimum size of 6") + } + + if cfg.email_token_size > 19 { + err!("`EMAIL_TOKEN_SIZE` has a maximum size of 19") + } + Ok(()) } From c99df1c310d7ba7a9fbe9026646f8a951b1596c9 Mon Sep 17 00:00:00 2001 From: vpl Date: Mon, 26 Aug 2019 20:22:04 +0200 Subject: [PATCH 9/9] Compare token using crypto::ct_eq --- src/api/core/two_factor/email.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/api/core/two_factor/email.rs b/src/api/core/two_factor/email.rs index 1986f816..8491a5b9 100644 --- a/src/api/core/two_factor/email.rs +++ b/src/api/core/two_factor/email.rs @@ -181,7 +181,7 @@ fn email(data: JsonUpcase, headers: Headers, conn: DbConn) -> JsonRes _ => err!("No token available"), }; - if issued_token != &data.Token { + if !crypto::ct_eq(issued_token, data.Token) { err!("Token is invalid") } @@ -206,7 +206,7 @@ pub fn validate_email_code_str(user_uuid: &str, token: &str, data: &str, conn: & _ => err!("No token available"), }; - if issued_token != &*token { + if !crypto::ct_eq(issued_token, token) { email_data.add_attempt(); if email_data.attempts >= CONFIG.email_attempts_limit() { email_data.reset_token();