Browse Source

Use saved token for email 2fa codes

vpl 6 years ago
parent
commit
6d460b44b0

+ 1 - 0
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)",

+ 3 - 0
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' }

+ 30 - 8
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<Route> {
     routes![
         generate_authenticator,
@@ -20,7 +18,6 @@ pub fn routes() -> Vec<Route> {
         activate_authenticator_put,
     ]
 }
-
 #[post("/two-factor/get-authenticator", data = "<data>")]
 fn generate_authenticator(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> JsonResult {
     let data: PasswordData = data.into_inner().data;
@@ -80,7 +77,7 @@ fn activate_authenticator(data: JsonUpcase<EnableAuthenticatorData>, 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<EnableAuthenticatorData>, headers: He
 fn activate_authenticator_put(data: JsonUpcase<EnableAuthenticatorData>, headers: Headers, conn: DbConn) -> JsonResult {
     activate_authenticator(data, headers, conn)
 }
+
+pub fn validate_totp_code_str(totp_code: &str, secret: &str) -> EmptyResult {
+    let totp_code: u64 = match totp_code.parse() {
+        Ok(code) => code,
+        _ => err!("TOTP code is not a number"),
+    };
+
+    validate_totp_code(totp_code, secret)
+}
+
+pub fn validate_totp_code(totp_code: u64, secret: &str) -> EmptyResult {
+    use oath::{totp_raw_now, HashType};
+
+    let decoded_secret = match BASE32.decode(secret.as_bytes()) {
+        Ok(s) => s,
+        Err(_) => err!("Invalid TOTP secret"),
+    };
+
+    let generated = totp_raw_now(&decoded_secret, 6, 0, 30, &HashType::SHA1);
+    if generated != totp_code {
+        err!("Invalid TOTP code");
+    }
+
+    Ok(())
+}

+ 6 - 6
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<Route> {
     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())
-}
+}

+ 86 - 62
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<Route> {
-    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 = "<data>")] // JsonResult
 fn send_email_login(data: JsonUpcase<SendEmailLoginData>, conn: DbConn) -> EmptyResult {
     let data: SendEmailLoginData = data.into_inner().data;
@@ -46,16 +53,15 @@ fn send_email_login(data: JsonUpcase<SendEmailLoginData>, conn: DbConn) -> Empty
     }
 
     let type_ = TwoFactorType::Email as i32;
-    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.totp_secret)?;
+    let mut twofactor = TwoFactor::find_by_user_and_type(&user.uuid, type_, &conn)?;
 
-    let generated_token = totp_raw_now(&decoded_key, 6, 0, TOTP_TIME_STEP, &HashType::SHA1);
-    let token_string = generated_token.to_string();
+    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)?;
 
-    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<PasswordData>, 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<PasswordData>, 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 = "<data>")]
 fn send_email(data: JsonUpcase<SendEmailData>, headers: Headers, conn: DbConn) -> EmptyResult {
-    use oath::{totp_raw_now, HashType};
-
     let data: SendEmailData = data.into_inner().data;
     let user = headers.user;
 
@@ -104,16 +120,12 @@ fn send_email(data: JsonUpcase<SendEmailData>, 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(()),
-    };
-
-    let secret = crypto::get_random(vec![0u8; 20]);
-    let base32_secret = BASE32.encode(&secret);
+    if let Some(tf) = TwoFactor::find_by_user_and_type(&user.uuid, type_, &conn) {
+        tf.delete(&conn)?;
+    }
 
-    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<SendEmailData>, 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 = "<data>")]
 fn email(data: JsonUpcase<EmailData>, headers: Headers, conn: DbConn) -> JsonResult {
     let data: EmailData = data.into_inner().data;
@@ -149,19 +158,23 @@ fn email(data: JsonUpcase<EmailData>, headers: Headers, conn: DbConn) -> JsonRes
         err!("Invalid password");
     }
 
-    let token_u64 = match data.Token.parse::<u64>() {
-        Ok(token) => token,
-        _ => err!("Could not parse token"),
-    };
-
     let type_ = TwoFactorType::EmailVerificationChallenge as i32;
     let mut twofactor = TwoFactor::find_by_user_and_type(&user.uuid, type_, &conn)?;
 
-    let email_data = EmailTokenData::from_json(&twofactor.data)?;
+    let mut email_data = EmailTokenData::from_json(&twofactor.data)?;
+
+    let issued_token = match &email_data.last_token {
+        Some(t) => t,
+        _ => err!("No token available"),
+    };
 
-    totp::validate_totp_code_with_time_step(token_u64, &email_data.totp_secret, TOTP_TIME_STEP)?;
+    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<EmailData>, 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)
-}
-
-pub fn validate_email_code(code: u64, data: &str) -> EmptyResult {
-    let email_data = EmailTokenData::from_json(&data)?;
+    if issued_token != &*token {
+        err!("Email token does not match")
+    }
 
-    let decoded_secret = match BASE32.decode(email_data.totp_secret.as_bytes()) {
-        Ok(s) => s,
-        Err(_) => err!("Invalid email secret"),
-    };
+    email_data.reset_token();
+    twofactor.data = email_data.to_json();
+    twofactor.save(&conn)?;
 
-    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<String>,
+    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)
         }

+ 2 - 3
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<Route> {
     let mut routes = routes![

+ 0 - 46
src/api/core/two_factor/totp.rs

@@ -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<Vec<u8>, crate::error::Error> {
-    // Validate key as base32 and 20 bytes length
-    let decoded_key: Vec<u8> = match BASE32.decode(key.as_bytes()) {
-        Ok(decoded) => decoded,
-        _ => err!("Invalid totp secret"),
-    };
-
-    if decoded_key.len() != 20 {
-        err!("Invalid key length")
-    }
-
-    Ok(decoded_key)
-}

+ 4 - 4
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";
 

+ 5 - 1
src/api/core/two_factor/yubikey.rs

@@ -16,7 +16,11 @@ use crate::error::{Error, MapResult};
 use crate::CONFIG;
 
 pub fn routes() -> Vec<Route> {
-    routes![generate_yubikey, activate_yubikey, activate_yubikey_put,]
+    routes![
+        generate_yubikey,
+        activate_yubikey,
+        activate_yubikey_put,
+    ]
 }
 
 #[derive(Deserialize, Debug)]

+ 10 - 7
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<Route> {
     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 {

+ 1 - 2
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