From 0b60f20eb3d598acb0525a098445b56672b83985 Mon Sep 17 00:00:00 2001
From: vpl <vpl@vpl.me>
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
+<!---------------->
+<html>
+<p>
+    Your two-step verification code is: <b>{{token}}</b>
+
+    Use this code to complete logging in with Bitwarden.
+</p>
+</html>
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
+<!---------------->
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns="http://www.w3.org/1999/xhtml" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
+<head>
+    <meta name="viewport" content="width=device-width" />
+    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+    <title>Bitwarden_rs</title>
+</head>
+<body style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; height: 100%; line-height: 25px; width: 100% !important;" bgcolor="#f6f6f6">
+<style type="text/css">
+     body {
+        margin: 0;
+        font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+        box-sizing: border-box;
+        font-size: 16px;
+        color: #333;
+        line-height: 25px;
+        -webkit-font-smoothing: antialiased;
+        -webkit-text-size-adjust: none;
+    }
+    body * {
+        margin: 0;
+        font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+        box-sizing: border-box;
+        font-size: 16px;
+        color: #333;
+        line-height: 25px;
+        -webkit-font-smoothing: antialiased;
+        -webkit-text-size-adjust: none;
+    }
+    img {
+        max-width: 100%;
+        border: none;
+    }
+    body {
+        -webkit-font-smoothing: antialiased;
+        -webkit-text-size-adjust: none;
+        width: 100% !important;
+        height: 100%;
+        line-height: 25px;
+    }
+    body {
+        background-color: #f6f6f6;
+    }
+    @media only screen and (max-width: 600px) {
+        body {
+            padding: 0 !important;
+        }
+        .container {
+            padding: 0 !important;
+            width: 100% !important;
+        }
+        .container-table {
+            padding: 0 !important;
+            width: 100% !important;
+        }
+        .content {
+            padding: 0 0 10px 0 !important;
+        }
+        .content-wrap {
+            padding: 10px !important;
+        }
+        .invoice {
+            width: 100% !important;
+        }
+        .main {
+            border-right: none !important;
+            border-left: none !important;
+            border-radius: 0 !important;
+        }
+        .logo {
+            padding-top: 10px !important;
+        }
+        .footer {
+            margin-top: 10px !important;
+        }
+        .indented {
+            padding-left: 10px;
+        }
+    }
+</style>
+<table class="body-wrap" cellpadding="0" cellspacing="0" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; width: 100%;" bgcolor="#f6f6f6">
+    <tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
+        <td valign="middle" class="aligncenter middle logo" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; padding: 20px 0 10px;" align="center">
+            <img src="{{url}}/bwrs_images/logo-gray.png" alt="" width="250" height="39" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; border: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; max-width: 100%;" />
+        </td>
+    </tr>
+    <tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
+        <td class="container" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both !important; color: #333; display: block !important; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto; max-width: 600px !important; width: 600px;" valign="top">
+            <table cellpadding="0" cellspacing="0" class="container-table" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both !important; color: #333; display: block !important; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto; max-width: 600px !important; width: 600px;">
+                <tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
+                    <td class="content" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; display: block; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 0; line-height: 0; margin: 0 auto; max-width: 600px; padding-bottom: 20px;" valign="top">
+                        <table class="main" width="100%" cellpadding="0" cellspacing="0" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; margin: 0; -webkit-text-size-adjust: none; border: 1px solid #e9e9e9; border-radius: 3px;" bgcolor="white">
+                            <tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
+                                <td class="content-wrap" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 20px; -webkit-text-size-adjust: none;" valign="top">
+                                    <table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
+                                        <tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
+                                            <td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top">
+                                                Your two-step verification code is: <b>{{token}}</b>
+                                            </td>
+                                        </tr>
+                                        <tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
+                                            <td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top">
+                                                Use this code to complete logging in with Bitwarden.
+                                            </td>
+                                        </tr>
+                                    </table>
+                                </td>
+                            </tr>
+                        </table>
+                        <table class="footer" cellpadding="0" cellspacing="0" width="100%" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; clear: both; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; width: 100%;">
+                            <tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
+                                <td class="aligncenter social-icons" align="center" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; padding: 15px 0 0 0;" valign="top">
+                                    <table cellpadding="0" cellspacing="0" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0 auto;">
+                                        <tr style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0;">
+                                            <td style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; padding: 0 10px;" valign="top"><a href="https://github.com/dani-garcia/bitwarden_rs" target="_blank" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; box-sizing: border-box; color: #999; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 12px; line-height: 20px; margin: 0; text-decoration: underline;"><img src="{{url}}/bwrs_images/mail-github.png" alt="GitHub" width="30" height="30" style="-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; border: none; box-sizing: border-box; color: #333; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 16px; line-height: 25px; margin: 0; max-width: 100%;" /></a></td>
+                                        </tr>
+                                    </table>
+                                </td>
+                            </tr>
+                        </table>
+                    </td>
+                </tr>
+            </table>
+        </td>
+    </tr>
+</table>
+</body>
+</html>

From 27e0e41835a09d3e6e5a2d00ad606fc998dda72d Mon Sep 17 00:00:00 2001
From: vpl <vpl@vpl.me>
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<Route> {
+    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<Value> = twofactors.iter().map(TwoFactor::to_json_list).collect();
+
+    Ok(Json(json!({
+        "Data": twofactors_json,
+        "Object": "list",
+        "ContinuationToken": null,
+    })))
+}
+
+#[post("/two-factor/get-recover", data = "<data>")]
+fn get_recover(data: JsonUpcase<PasswordData>, headers: Headers) -> JsonResult {
+    let data: PasswordData = data.into_inner().data;
+    let user = headers.user;
+
+    if !user.check_valid_password(&data.MasterPasswordHash) {
+        err!("Invalid password");
+    }
+
+    Ok(Json(json!({
+        "Code": user.totp_recover,
+        "Object": "twoFactorRecover"
+    })))
+}
+
+#[derive(Deserialize)]
+#[allow(non_snake_case)]
+struct RecoverTwoFactor {
+    MasterPasswordHash: String,
+    Email: String,
+    RecoveryCode: String,
+}
+
+#[post("/two-factor/recover", data = "<data>")]
+fn recover(data: JsonUpcase<RecoverTwoFactor>, conn: DbConn) -> JsonResult {
+    let data: RecoverTwoFactor = data.into_inner().data;
+
+    use crate::db::models::User;
+
+    // Get the user
+    let mut user = match User::find_by_mail(&data.Email, &conn) {
+        Some(user) => user,
+        None => err!("Username or password is incorrect. Try again."),
+    };
+
+    // Check password
+    if !user.check_valid_password(&data.MasterPasswordHash) {
+        err!("Username or password is incorrect. Try again.")
+    }
+
+    // Check if recovery code is correct
+    if !user.check_valid_recovery_code(&data.RecoveryCode) {
+        err!("Recovery code is incorrect. Try again.")
+    }
+
+    // Remove all twofactors from the user
+    for twofactor in TwoFactor::find_by_user(&user.uuid, &conn) {
+        twofactor.delete(&conn)?;
+    }
+
+    // Remove the recovery code, not needed without twofactors
+    user.totp_recover = None;
+    user.save(&conn)?;
+    Ok(Json(json!({})))
+}
+
+fn _generate_recover_code(user: &mut User, conn: &DbConn) {
+    if user.totp_recover.is_none() {
+        let totp_recover = BASE32.encode(&crypto::get_random(vec![0u8; 20]));
+        user.totp_recover = Some(totp_recover);
+        user.save(conn).ok();
+    }
+}
+
+#[derive(Deserialize)]
+#[allow(non_snake_case)]
+struct DisableTwoFactorData {
+    MasterPasswordHash: String,
+    Type: NumberOrString,
+}
+
+#[post("/two-factor/disable", data = "<data>")]
+fn disable_twofactor(data: JsonUpcase<DisableTwoFactorData>, headers: Headers, conn: DbConn) -> JsonResult {
+    let data: DisableTwoFactorData = data.into_inner().data;
+    let password_hash = data.MasterPasswordHash;
+    let user = headers.user;
+
+    if !user.check_valid_password(&password_hash) {
+        err!("Invalid password");
+    }
+
+    let type_ = data.Type.into_i32()?;
+
+    if let Some(twofactor) = TwoFactor::find_by_user_and_type(&user.uuid, type_, &conn) {
+        twofactor.delete(&conn)?;
+    }
+
+    Ok(Json(json!({
+        "Enabled": false,
+        "Type": type_,
+        "Object": "twoFactorProvider"
+    })))
+}
+
+#[put("/two-factor/disable", data = "<data>")]
+fn disable_twofactor_put(data: JsonUpcase<DisableTwoFactorData>, headers: Headers, conn: DbConn) -> JsonResult {
+    disable_twofactor(data, headers, conn)
+}
+
+#[post("/two-factor/get-authenticator", data = "<data>")]
+fn generate_authenticator(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> JsonResult {
+    let data: PasswordData = data.into_inner().data;
+    let user = headers.user;
+
+    if !user.check_valid_password(&data.MasterPasswordHash) {
+        err!("Invalid password");
+    }
+
+    let type_ = TwoFactorType::Authenticator as i32;
+    let twofactor = TwoFactor::find_by_user_and_type(&user.uuid, type_, &conn);
+
+    let (enabled, key) = match twofactor {
+        Some(tf) => (true, tf.data),
+        _ => (false, BASE32.encode(&crypto::get_random(vec![0u8; 20]))),
+    };
+
+    Ok(Json(json!({
+        "Enabled": enabled,
+        "Key": key,
+        "Object": "twoFactorAuthenticator"
+    })))
+}
+
+#[derive(Deserialize, Debug)]
+#[allow(non_snake_case)]
+struct EnableAuthenticatorData {
+    MasterPasswordHash: String,
+    Key: String,
+    Token: NumberOrString,
+}
+
+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)
+}
+
+#[post("/two-factor/authenticator", data = "<data>")]
+fn activate_authenticator(data: JsonUpcase<EnableAuthenticatorData>, headers: Headers, conn: DbConn) -> JsonResult {
+    let data: EnableAuthenticatorData = data.into_inner().data;
+    let password_hash = data.MasterPasswordHash;
+    let key = data.Key;
+    let token = data.Token.into_i32()? as u64;
+
+    let mut user = headers.user;
+
+    if !user.check_valid_password(&password_hash) {
+        err!("Invalid password");
+    }
+
+    // Validate key as base32 and 20 bytes length
+    let decoded_key = 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 = "<data>")]
+fn activate_authenticator_put(data: JsonUpcase<EnableAuthenticatorData>, headers: Headers, conn: DbConn) -> JsonResult {
+    activate_authenticator(data, headers, conn)
+}
+
+pub fn validate_totp_code_str(totp_code: &str, secret: &str) -> EmptyResult {
+    let totp_code: u64 = match totp_code.parse() {
+        Ok(code) => code,
+        _ => err!("TOTP code is not a number"),
+    };
+
+    validate_totp_code(totp_code, secret)
+}
+
+pub fn validate_totp_code(totp_code: u64, secret: &str) -> EmptyResult {
+    use oath::{totp_raw_now, HashType};
+
+    let decoded_secret = match BASE32.decode(secret.as_bytes()) {
+        Ok(s) => s,
+        Err(_) => err!("Invalid TOTP secret"),
+    };
+
+    let generated = totp_raw_now(&decoded_secret, 6, 0, 30, &HashType::SHA1);
+    if generated != totp_code {
+        err!("Invalid TOTP code");
+    }
+
+    Ok(())
+}
+
+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 = "<data>")]
+fn generate_u2f(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> JsonResult {
+    if !CONFIG.domain_set() {
+        err!("`DOMAIN` environment variable is not set. U2F disabled")
+    }
+    let data: PasswordData = data.into_inner().data;
+
+    if !headers.user.check_valid_password(&data.MasterPasswordHash) {
+        err!("Invalid password");
+    }
+
+    let (enabled, keys) = get_u2f_registrations(&headers.user.uuid, &conn)?;
+    let keys_json: Vec<Value> = keys.iter().map(U2FRegistration::to_json).collect();
+
+    Ok(Json(json!({
+        "Enabled": enabled,
+        "Keys": keys_json,
+        "Object": "twoFactorU2f"
+    })))
+}
+
+#[post("/two-factor/get-u2f-challenge", data = "<data>")]
+fn generate_u2f_challenge(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> JsonResult {
+    let data: PasswordData = data.into_inner().data;
+
+    if !headers.user.check_valid_password(&data.MasterPasswordHash) {
+        err!("Invalid password");
+    }
+
+    let _type = TwoFactorType::U2fRegisterChallenge;
+    let challenge = _create_u2f_challenge(&headers.user.uuid, _type, &conn).challenge;
+
+    Ok(Json(json!({
+        "UserId": headers.user.uuid,
+        "AppId": APP_ID.to_string(),
+        "Challenge": challenge,
+        "Version": U2F_VERSION,
+    })))
+}
+
+#[derive(Deserialize, Debug)]
+#[allow(non_snake_case)]
+struct EnableU2FData {
+    Id: NumberOrString, // 1..5
+    Name: String,
+    MasterPasswordHash: String,
+    DeviceResponse: String,
+}
+
+// This struct is referenced from the U2F lib
+// because it doesn't implement Deserialize
+#[derive(Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+#[serde(remote = "Registration")]
+struct RegistrationDef {
+    key_handle: Vec<u8>,
+    pub_key: Vec<u8>,
+    attestation_cert: Option<Vec<u8>>,
+}
+
+#[derive(Serialize, Deserialize)]
+struct U2FRegistration {
+    id: i32,
+    name: String,
+    #[serde(with = "RegistrationDef")]
+    reg: Registration,
+    counter: u32,
+    compromised: bool,
+}
+
+impl U2FRegistration {
+    fn to_json(&self) -> Value {
+        json!({
+            "Id": self.id,
+            "Name": self.name,
+            "Compromised": self.compromised,
+        })
+    }
+}
+
+// This struct is copied from the U2F lib
+// to add an optional error code
+#[derive(Deserialize)]
+#[serde(rename_all = "camelCase")]
+struct RegisterResponseCopy {
+    pub registration_data: String,
+    pub version: String,
+    pub client_data: String,
+
+    pub error_code: Option<NumberOrString>,
+}
+
+impl Into<RegisterResponse> for RegisterResponseCopy {
+    fn into(self) -> RegisterResponse {
+        RegisterResponse {
+            registration_data: self.registration_data,
+            version: self.version,
+            client_data: self.client_data,
+        }
+    }
+}
+
+#[post("/two-factor/u2f", data = "<data>")]
+fn activate_u2f(data: JsonUpcase<EnableU2FData>, headers: Headers, conn: DbConn) -> JsonResult {
+    let data: EnableU2FData = data.into_inner().data;
+    let mut user = headers.user;
+
+    if !user.check_valid_password(&data.MasterPasswordHash) {
+        err!("Invalid password");
+    }
+
+    let tf_type = TwoFactorType::U2fRegisterChallenge as i32;
+    let tf_challenge = match TwoFactor::find_by_user_and_type(&user.uuid, tf_type, &conn) {
+        Some(c) => c,
+        None => err!("Can't recover challenge"),
+    };
+
+    let challenge: Challenge = serde_json::from_str(&tf_challenge.data)?;
+    tf_challenge.delete(&conn)?;
+
+    let response: RegisterResponseCopy = serde_json::from_str(&data.DeviceResponse)?;
+
+    let error_code = response
+        .error_code
+        .clone()
+        .map_or("0".into(), NumberOrString::into_string);
+
+    if error_code != "0" {
+        err!("Error registering U2F token")
+    }
+
+    let registration = U2F.register_response(challenge.clone(), response.into())?;
+    let full_registration = U2FRegistration {
+        id: data.Id.into_i32()?,
+        name: data.Name,
+        reg: registration,
+        compromised: false,
+        counter: 0,
+    };
+
+    let mut regs = get_u2f_registrations(&user.uuid, &conn)?.1;
+
+    // TODO: Check that there is no repeat Id
+    regs.push(full_registration);
+    save_u2f_registrations(&user.uuid, &regs, &conn)?;
+
+    _generate_recover_code(&mut user, &conn);
+
+    let keys_json: Vec<Value> = regs.iter().map(U2FRegistration::to_json).collect();
+    Ok(Json(json!({
+        "Enabled": true,
+        "Keys": keys_json,
+        "Object": "twoFactorU2f"
+    })))
+}
+
+#[put("/two-factor/u2f", data = "<data>")]
+fn activate_u2f_put(data: JsonUpcase<EnableU2FData>, headers: Headers, conn: DbConn) -> JsonResult {
+    activate_u2f(data, headers, conn)
+}
+
+fn _create_u2f_challenge(user_uuid: &str, type_: TwoFactorType, conn: &DbConn) -> Challenge {
+    let challenge = U2F.generate_challenge().unwrap();
+
+    TwoFactor::new(user_uuid.into(), type_, serde_json::to_string(&challenge).unwrap())
+        .save(conn)
+        .expect("Error saving challenge");
+
+    challenge
+}
+
+fn save_u2f_registrations(user_uuid: &str, regs: &[U2FRegistration], conn: &DbConn) -> EmptyResult {
+    TwoFactor::new(user_uuid.into(), TwoFactorType::U2f, serde_json::to_string(regs)?).save(&conn)
+}
+
+fn get_u2f_registrations(user_uuid: &str, conn: &DbConn) -> Result<(bool, Vec<U2FRegistration>), Error> {
+    let type_ = TwoFactorType::U2f as i32;
+    let (enabled, regs) = match TwoFactor::find_by_user_and_type(user_uuid, type_, conn) {
+        Some(tf) => (tf.enabled, tf.data),
+        None => return Ok((false, Vec::new())), // If no data, return empty list
+    };
+
+    let data = match serde_json::from_str(&regs) {
+        Ok(d) => d,
+        Err(_) => {
+            // If error, try old format
+            let mut old_regs = _old_parse_registrations(&regs);
+
+            if old_regs.len() != 1 {
+                err!("The old U2F format only allows one device")
+            }
+
+            // Convert to new format
+            let new_regs = vec![U2FRegistration {
+                id: 1,
+                name: "Unnamed U2F key".into(),
+                reg: old_regs.remove(0),
+                compromised: false,
+                counter: 0,
+            }];
+
+            // Save new format
+            save_u2f_registrations(user_uuid, &new_regs, &conn)?;
+
+            new_regs
+        }
+    };
+
+    Ok((enabled, data))
+}
+
+fn _old_parse_registrations(registations: &str) -> Vec<Registration> {
+    #[derive(Deserialize)]
+    struct Helper(#[serde(with = "RegistrationDef")] Registration);
+
+    let regs: Vec<Value> = serde_json::from_str(registations).expect("Can't parse Registration data");
+
+    regs.into_iter()
+        .map(|r| serde_json::from_value(r).unwrap())
+        .map(|Helper(r)| r)
+        .collect()
+}
+
+pub fn generate_u2f_login(user_uuid: &str, conn: &DbConn) -> ApiResult<U2fSignRequest> {
+    let challenge = _create_u2f_challenge(user_uuid, TwoFactorType::U2fLoginChallenge, conn);
+
+    let registrations: Vec<_> = get_u2f_registrations(user_uuid, conn)?
+        .1
+        .into_iter()
+        .map(|r| r.reg)
+        .collect();
+
+    if registrations.is_empty() {
+        err!("No U2F devices registered")
+    }
+
+    Ok(U2F.sign_request(challenge, registrations))
+}
+
+pub fn validate_u2f_login(user_uuid: &str, response: &str, conn: &DbConn) -> EmptyResult {
+    let challenge_type = TwoFactorType::U2fLoginChallenge as i32;
+    let tf_challenge = TwoFactor::find_by_user_and_type(user_uuid, challenge_type, &conn);
+
+    let challenge = match tf_challenge {
+        Some(tf_challenge) => {
+            let challenge: Challenge = serde_json::from_str(&tf_challenge.data)?;
+            tf_challenge.delete(&conn)?;
+            challenge
+        }
+        None => err!("Can't recover login challenge"),
+    };
+    let response: SignResponse = serde_json::from_str(response)?;
+    let mut registrations = get_u2f_registrations(user_uuid, conn)?.1;
+    if registrations.is_empty() {
+        err!("No U2F devices registered")
+    }
+
+    for reg in &mut registrations {
+        let response = U2F.sign_response(challenge.clone(), reg.reg.clone(), response.clone(), reg.counter);
+        match response {
+            Ok(new_counter) => {
+                reg.counter = new_counter;
+                save_u2f_registrations(user_uuid, &registrations, &conn)?;
+
+                return Ok(());
+            }
+            Err(u2f::u2ferror::U2fError::CounterTooLow) => {
+                reg.compromised = true;
+                save_u2f_registrations(user_uuid, &registrations, &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<String>,
+    Key2: Option<String>,
+    Key3: Option<String>,
+    Key4: Option<String>,
+    Key5: Option<String>,
+    Nfc: bool,
+}
+
+#[derive(Deserialize, Serialize, Debug)]
+#[allow(non_snake_case)]
+pub struct YubikeyMetadata {
+    Keys: Vec<String>,
+    pub Nfc: bool,
+}
+
+use yubico::config::Config;
+use yubico::verify;
+
+fn parse_yubikeys(data: &EnableYubikeyData) -> Vec<String> {
+    let data_keys = [&data.Key1, &data.Key2, &data.Key3, &data.Key4, &data.Key5];
+
+    data_keys.iter().filter_map(|e| e.as_ref().cloned()).collect()
+}
+
+fn jsonify_yubikeys(yubikeys: Vec<String>) -> serde_json::Value {
+    let mut result = json!({});
+
+    for (i, key) in yubikeys.into_iter().enumerate() {
+        result[format!("Key{}", i + 1)] = Value::String(key);
+    }
+
+    result
+}
+
+fn get_yubico_credentials() -> Result<(String, String), Error> {
+    match (CONFIG.yubico_client_id(), CONFIG.yubico_secret_key()) {
+        (Some(id), Some(secret)) => Ok((id, secret)),
+        _ => err!("`YUBICO_CLIENT_ID` or `YUBICO_SECRET_KEY` environment variable is not set. Yubikey OTP Disabled"),
+    }
+}
+
+fn verify_yubikey_otp(otp: String) -> EmptyResult {
+    let (yubico_id, yubico_secret) = get_yubico_credentials()?;
+
+    let config = Config::default().set_client_id(yubico_id).set_key(yubico_secret);
+
+    match CONFIG.yubico_server() {
+        Some(server) => verify(otp, config.set_api_hosts(vec![server])),
+        None => verify(otp, config),
+    }
+    .map_res("Failed to verify OTP")
+    .and(Ok(()))
+}
+
+#[post("/two-factor/get-yubikey", data = "<data>")]
+fn generate_yubikey(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> JsonResult {
+    // Make sure the credentials are set
+    get_yubico_credentials()?;
+
+    let data: PasswordData = data.into_inner().data;
+    let user = headers.user;
+
+    if !user.check_valid_password(&data.MasterPasswordHash) {
+        err!("Invalid password");
+    }
+
+    let user_uuid = &user.uuid;
+    let yubikey_type = TwoFactorType::YubiKey as i32;
+
+    let r = TwoFactor::find_by_user_and_type(user_uuid, yubikey_type, &conn);
+
+    if let Some(r) = r {
+        let yubikey_metadata: YubikeyMetadata = serde_json::from_str(&r.data)?;
+
+        let mut result = jsonify_yubikeys(yubikey_metadata.Keys);
+
+        result["Enabled"] = Value::Bool(true);
+        result["Nfc"] = Value::Bool(yubikey_metadata.Nfc);
+        result["Object"] = Value::String("twoFactorU2f".to_owned());
+
+        Ok(Json(result))
+    } else {
+        Ok(Json(json!({
+            "Enabled": false,
+            "Object": "twoFactorU2f",
+        })))
+    }
+}
+
+#[post("/two-factor/yubikey", data = "<data>")]
+fn activate_yubikey(data: JsonUpcase<EnableYubikeyData>, headers: Headers, conn: DbConn) -> JsonResult {
+    let data: EnableYubikeyData = data.into_inner().data;
+    let mut user = headers.user;
+
+    if !user.check_valid_password(&data.MasterPasswordHash) {
+        err!("Invalid password");
+    }
+
+    // Check if we already have some data
+    let mut yubikey_data = match TwoFactor::find_by_user_and_type(&user.uuid, TwoFactorType::YubiKey as i32, &conn) {
+        Some(data) => data,
+        None => TwoFactor::new(user.uuid.clone(), TwoFactorType::YubiKey, String::new()),
+    };
+
+    let yubikeys = parse_yubikeys(&data);
+
+    if yubikeys.is_empty() {
+        return Ok(Json(json!({
+            "Enabled": false,
+            "Object": "twoFactorU2f",
+        })));
+    }
+
+    // Ensure they are valid OTPs
+    for yubikey in &yubikeys {
+        if yubikey.len() == 12 {
+            // YubiKey ID
+            continue;
+        }
+
+        verify_yubikey_otp(yubikey.to_owned()).map_res("Invalid Yubikey OTP provided")?;
+    }
+
+    let yubikey_ids: Vec<String> = yubikeys.into_iter().map(|x| (&x[..12]).to_owned()).collect();
+
+    let yubikey_metadata = YubikeyMetadata {
+        Keys: yubikey_ids,
+        Nfc: data.Nfc,
+    };
+
+    yubikey_data.data = serde_json::to_string(&yubikey_metadata).unwrap();
+    yubikey_data.save(&conn)?;
+
+    _generate_recover_code(&mut user, &conn);
+
+    let mut result = jsonify_yubikeys(yubikey_metadata.Keys);
+
+    result["Enabled"] = Value::Bool(true);
+    result["Nfc"] = Value::Bool(yubikey_metadata.Nfc);
+    result["Object"] = Value::String("twoFactorU2f".to_owned());
+
+    Ok(Json(result))
+}
+
+#[put("/two-factor/yubikey", data = "<data>")]
+fn activate_yubikey_put(data: JsonUpcase<EnableYubikeyData>, headers: Headers, conn: DbConn) -> JsonResult {
+    activate_yubikey(data, headers, conn)
+}
+
+pub fn validate_yubikey_login(response: &str, twofactor_data: &str) -> EmptyResult {
+    if response.len() != 44 {
+        err!("Invalid Yubikey OTP length");
+    }
+
+    let yubikey_metadata: YubikeyMetadata = serde_json::from_str(twofactor_data).expect("Can't parse Yubikey Metadata");
+    let response_id = &response[..12];
+
+    if !yubikey_metadata.Keys.contains(&response_id.to_owned()) {
+        err!("Given Yubikey is not registered");
+    }
+
+    let result = verify_yubikey_otp(response.to_owned());
+
+    match result {
+        Ok(_answer) => Ok(()),
+        Err(_e) => err!("Failed to verify Yubikey against OTP server"),
+    }
+}
+
+#[derive(Serialize, Deserialize)]
+struct DuoData {
+    host: String,
+    ik: String,
+    sk: String,
+}
+
+impl DuoData {
+    fn global() -> Option<Self> {
+        match CONFIG.duo_host() {
+            Some(host) => Some(Self {
+                host,
+                ik: CONFIG.duo_ikey().unwrap(),
+                sk: CONFIG.duo_skey().unwrap(),
+            }),
+            None => None,
+        }
+    }
+    fn msg(s: &str) -> Self {
+        Self {
+            host: s.into(),
+            ik: s.into(),
+            sk: s.into(),
+        }
+    }
+    fn secret() -> Self {
+        Self::msg("<global_secret>")
+    }
+    fn obscure(self) -> Self {
+        let mut host = self.host;
+        let mut ik = self.ik;
+        let mut sk = self.sk;
+
+        let digits = 4;
+        let replaced = "************";
+
+        host.replace_range(digits.., replaced);
+        ik.replace_range(digits.., replaced);
+        sk.replace_range(digits.., replaced);
+
+        Self { host, ik, sk }
+    }
+}
+
+enum DuoStatus {
+    Global(DuoData), // Using the global duo config
+    User(DuoData),   // Using the user's config
+    Disabled(bool),  // True if there is a global setting
+}
+
+impl DuoStatus {
+    fn data(self) -> Option<DuoData> {
+        match self {
+            DuoStatus::Global(data) => Some(data),
+            DuoStatus::User(data) => Some(data),
+            DuoStatus::Disabled(_) => None,
+        }
+    }
+}
+const DISABLED_MESSAGE_DEFAULT: &str = "<To use the global Duo keys, please leave these fields untouched>";
+
+#[post("/two-factor/get-duo", data = "<data>")]
+fn get_duo(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> JsonResult {
+    let data: PasswordData = data.into_inner().data;
+
+    if !headers.user.check_valid_password(&data.MasterPasswordHash) {
+        err!("Invalid password");
+    }
+
+    let data = get_user_duo_data(&headers.user.uuid, &conn);
+
+    let (enabled, data) = match data {
+        DuoStatus::Global(_) => (true, Some(DuoData::secret())),
+        DuoStatus::User(data) => (true, Some(data.obscure())),
+        DuoStatus::Disabled(true) => (false, Some(DuoData::msg(DISABLED_MESSAGE_DEFAULT))),
+        DuoStatus::Disabled(false) => (false, None),
+    };
+
+    let json = if let Some(data) = data {
+        json!({
+            "Enabled": enabled,
+            "Host": data.host,
+            "SecretKey": data.sk,
+            "IntegrationKey": data.ik,
+            "Object": "twoFactorDuo"
+        })
+    } else {
+        json!({
+            "Enabled": enabled,
+            "Object": "twoFactorDuo"
+        })
+    };
+
+    Ok(Json(json))
+}
+
+#[derive(Deserialize)]
+#[allow(non_snake_case, dead_code)]
+struct EnableDuoData {
+    MasterPasswordHash: String,
+    Host: String,
+    SecretKey: String,
+    IntegrationKey: String,
+}
+
+impl From<EnableDuoData> for DuoData {
+    fn from(d: EnableDuoData) -> Self {
+        Self {
+            host: d.Host,
+            ik: d.IntegrationKey,
+            sk: d.SecretKey,
+        }
+    }
+}
+
+fn check_duo_fields_custom(data: &EnableDuoData) -> bool {
+    fn empty_or_default(s: &str) -> bool {
+        let st = s.trim();
+        st.is_empty() || s == DISABLED_MESSAGE_DEFAULT
+    }
+
+    !empty_or_default(&data.Host) && !empty_or_default(&data.SecretKey) && !empty_or_default(&data.IntegrationKey)
+}
+
+#[post("/two-factor/duo", data = "<data>")]
+fn activate_duo(data: JsonUpcase<EnableDuoData>, headers: Headers, conn: DbConn) -> JsonResult {
+    let data: EnableDuoData = data.into_inner().data;
+
+    if !headers.user.check_valid_password(&data.MasterPasswordHash) {
+        err!("Invalid password");
+    }
+
+    let (data, data_str) = if check_duo_fields_custom(&data) {
+        let data_req: DuoData = data.into();
+        let data_str = serde_json::to_string(&data_req)?;
+        duo_api_request("GET", "/auth/v2/check", "", &data_req).map_res("Failed to validate Duo credentials")?;
+        (data_req.obscure(), data_str)
+    } else {
+        (DuoData::secret(), String::new())
+    };
+
+    let type_ = TwoFactorType::Duo;
+    let twofactor = TwoFactor::new(headers.user.uuid.clone(), type_, data_str);
+    twofactor.save(&conn)?;
+
+    Ok(Json(json!({
+        "Enabled": true,
+        "Host": data.host,
+        "SecretKey": data.sk,
+        "IntegrationKey": data.ik,
+        "Object": "twoFactorDuo"
+    })))
+}
+
+#[put("/two-factor/duo", data = "<data>")]
+fn activate_duo_put(data: JsonUpcase<EnableDuoData>, headers: Headers, conn: DbConn) -> JsonResult {
+    activate_duo(data, headers, conn)
+}
+
+fn duo_api_request(method: &str, path: &str, params: &str, data: &DuoData) -> EmptyResult {
+    const AGENT: &str = "bitwarden_rs:Duo/1.0 (Rust)";
+
+    use reqwest::{header::*, Client, Method};
+    use std::str::FromStr;
+
+    let url = format!("https://{}{}", &data.host, path);
+    let date = Utc::now().to_rfc2822();
+    let username = &data.ik;
+    let fields = [&date, method, &data.host, path, params];
+    let password = crypto::hmac_sign(&data.sk, &fields.join("\n"));
+
+    let m = Method::from_str(method).unwrap_or_default();
+
+    Client::new()
+        .request(m, &url)
+        .basic_auth(username, Some(password))
+        .header(USER_AGENT, AGENT)
+        .header(DATE, date)
+        .send()?
+        .error_for_status()?;
+
+    Ok(())
+}
+
+const DUO_EXPIRE: i64 = 300;
+const APP_EXPIRE: i64 = 3600;
+
+const AUTH_PREFIX: &str = "AUTH";
+const DUO_PREFIX: &str = "TX";
+const APP_PREFIX: &str = "APP";
+
+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<String> {
+    let split: Vec<&str> = val.split('|').collect();
+    if split.len() != 3 {
+        err!("Invalid value length")
+    }
+
+    let u_prefix = split[0];
+    let u_b64 = split[1];
+    let u_sig = split[2];
+
+    let sig = crypto::hmac_sign(key, &format!("{}|{}", u_prefix, u_b64));
+
+    if !crypto::ct_eq(crypto::hmac_sign(key, &sig), crypto::hmac_sign(key, u_sig)) {
+        err!("Duo signatures don't match")
+    }
+
+    if u_prefix != prefix {
+        err!("Prefixes don't match")
+    }
+
+    let cookie_vec = match BASE64.decode(u_b64.as_bytes()) {
+        Ok(c) => c,
+        Err(_) => err!("Invalid Duo cookie encoding"),
+    };
+
+    let cookie = match String::from_utf8(cookie_vec) {
+        Ok(c) => c,
+        Err(_) => err!("Invalid Duo cookie encoding"),
+    };
+
+    let cookie_split: Vec<&str> = cookie.split('|').collect();
+    if cookie_split.len() != 3 {
+        err!("Invalid cookie length")
+    }
+
+    let username = cookie_split[0];
+    let u_ikey = cookie_split[1];
+    let expire = cookie_split[2];
+
+    if !crypto::ct_eq(ikey, u_ikey) {
+        err!("Invalid ikey")
+    }
+
+    let expire = match expire.parse() {
+        Ok(e) => e,
+        Err(_) => err!("Invalid expire time"),
+    };
+
+    if time >= expire {
+        err!("Expired authorization")
+    }
+
+    Ok(username.into())
+}
+
+#[derive(Deserialize)]
+#[allow(non_snake_case)]
+struct SendEmailLoginData {
+    Email: String,
+    MasterPasswordHash: String,
+}
+
+#[put("/two-factor/send-email-login", data = "<data>")] // JsonResult
+fn send_email_login(data: JsonUpcase<SendEmailLoginData>, 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 = "<data>")]
+fn get_email(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> JsonResult {
+    let data: PasswordData = data.into_inner().data;
+    let user = headers.user;
+
+    if !user.check_valid_password(&data.MasterPasswordHash) {
+        err!("Invalid password");
+    }
+
+    let type_ = TwoFactorType::Email as i32;
+    let enabled = match TwoFactor::find_by_user_and_type(&user.uuid, type_, &conn) {
+        Some(x) => x.enabled,
+        _ => false,
+    };
+
+
+    Ok(Json(json!({// TODO check! FIX!
+        "Email": user.email,
+        "Enabled": enabled,
+        "Object": "twoFactorEmail"
+    })))
+}
+
+#[derive(Deserialize)]
+#[allow(non_snake_case)]
+struct SendEmailData {
+    Email: String, // Email where 2FA codes will be sent to, can be different than user email account.
+    MasterPasswordHash: String,
+}
+
+// Send a verification email to the specified email address to check whether it exists/belongs to user.
+#[post("/two-factor/send-email", data = "<data>")]
+fn send_email(data: JsonUpcase<SendEmailData>, headers: Headers, conn: DbConn) -> EmptyResult {
+    use oath::{totp_raw_now, HashType};
+
+    let data: SendEmailData = data.into_inner().data;
+    let user = headers.user;
+
+    if !user.check_valid_password(&data.MasterPasswordHash) {
+        err!("Invalid password");
+    }
+
+    let type_ = TwoFactorType::Email as i32;
+
+    // TODO: Delete previous email thing.
+    match TwoFactor::find_by_user_and_type(&user.uuid, type_, &conn) {
+        Some(tf) => tf.delete(&conn),
+        _ => Ok(()),
+    };
+
+    let secret = crypto::get_random(vec![0u8; 20]);
+    let base32_secret = BASE32.encode(&secret);
+
+    let 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 = "<data>")]
+fn email(data: JsonUpcase<EmailData>, headers: Headers, conn: DbConn) -> JsonResult {
+
+    let data: EmailData = data.into_inner().data;
+    let user = headers.user;
+
+    if !user.check_valid_password(&data.MasterPasswordHash) {
+        err!("Invalid password");
+    }
+
+    let token_u64 = match data.Token.parse::<u64>() {
+        Ok(token) => token,
+        _ => err!("Could not parse token"),
+    };
+
+
+    let type_ = TwoFactorType::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 <vpl@vpl.me>
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<Route> {
-    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<Value> = twofactors.iter().map(TwoFactor::to_json_list).collect();
-
-    Ok(Json(json!({
-        "Data": twofactors_json,
-        "Object": "list",
-        "ContinuationToken": null,
-    })))
-}
-
-#[post("/two-factor/get-recover", data = "<data>")]
-fn get_recover(data: JsonUpcase<PasswordData>, headers: Headers) -> JsonResult {
-    let data: PasswordData = data.into_inner().data;
-    let user = headers.user;
-
-    if !user.check_valid_password(&data.MasterPasswordHash) {
-        err!("Invalid password");
-    }
-
-    Ok(Json(json!({
-        "Code": user.totp_recover,
-        "Object": "twoFactorRecover"
-    })))
-}
-
-#[derive(Deserialize)]
-#[allow(non_snake_case)]
-struct RecoverTwoFactor {
-    MasterPasswordHash: String,
-    Email: String,
-    RecoveryCode: String,
-}
-
-#[post("/two-factor/recover", data = "<data>")]
-fn recover(data: JsonUpcase<RecoverTwoFactor>, conn: DbConn) -> JsonResult {
-    let data: RecoverTwoFactor = data.into_inner().data;
-
-    use crate::db::models::User;
-
-    // Get the user
-    let mut user = match User::find_by_mail(&data.Email, &conn) {
-        Some(user) => user,
-        None => err!("Username or password is incorrect. Try again."),
-    };
-
-    // Check password
-    if !user.check_valid_password(&data.MasterPasswordHash) {
-        err!("Username or password is incorrect. Try again.")
-    }
-
-    // Check if recovery code is correct
-    if !user.check_valid_recovery_code(&data.RecoveryCode) {
-        err!("Recovery code is incorrect. Try again.")
-    }
-
-    // Remove all twofactors from the user
-    for twofactor in TwoFactor::find_by_user(&user.uuid, &conn) {
-        twofactor.delete(&conn)?;
-    }
-
-    // Remove the recovery code, not needed without twofactors
-    user.totp_recover = None;
-    user.save(&conn)?;
-    Ok(Json(json!({})))
-}
-
-fn _generate_recover_code(user: &mut User, conn: &DbConn) {
-    if user.totp_recover.is_none() {
-        let totp_recover = BASE32.encode(&crypto::get_random(vec![0u8; 20]));
-        user.totp_recover = Some(totp_recover);
-        user.save(conn).ok();
-    }
-}
-
-#[derive(Deserialize)]
-#[allow(non_snake_case)]
-struct DisableTwoFactorData {
-    MasterPasswordHash: String,
-    Type: NumberOrString,
-}
-
-#[post("/two-factor/disable", data = "<data>")]
-fn disable_twofactor(data: JsonUpcase<DisableTwoFactorData>, headers: Headers, conn: DbConn) -> JsonResult {
-    let data: DisableTwoFactorData = data.into_inner().data;
-    let password_hash = data.MasterPasswordHash;
-    let user = headers.user;
-
-    if !user.check_valid_password(&password_hash) {
-        err!("Invalid password");
-    }
-
-    let type_ = data.Type.into_i32()?;
-
-    if let Some(twofactor) = TwoFactor::find_by_user_and_type(&user.uuid, type_, &conn) {
-        twofactor.delete(&conn)?;
-    }
-
-    Ok(Json(json!({
-        "Enabled": false,
-        "Type": type_,
-        "Object": "twoFactorProvider"
-    })))
-}
-
-#[put("/two-factor/disable", data = "<data>")]
-fn disable_twofactor_put(data: JsonUpcase<DisableTwoFactorData>, headers: Headers, conn: DbConn) -> JsonResult {
-    disable_twofactor(data, headers, conn)
-}
-
-#[post("/two-factor/get-authenticator", data = "<data>")]
-fn generate_authenticator(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> JsonResult {
-    let data: PasswordData = data.into_inner().data;
-    let user = headers.user;
-
-    if !user.check_valid_password(&data.MasterPasswordHash) {
-        err!("Invalid password");
-    }
-
-    let type_ = TwoFactorType::Authenticator as i32;
-    let twofactor = TwoFactor::find_by_user_and_type(&user.uuid, type_, &conn);
-
-    let (enabled, key) = match twofactor {
-        Some(tf) => (true, tf.data),
-        _ => (false, BASE32.encode(&crypto::get_random(vec![0u8; 20]))),
-    };
-
-    Ok(Json(json!({
-        "Enabled": enabled,
-        "Key": key,
-        "Object": "twoFactorAuthenticator"
-    })))
-}
-
-#[derive(Deserialize, Debug)]
-#[allow(non_snake_case)]
-struct EnableAuthenticatorData {
-    MasterPasswordHash: String,
-    Key: String,
-    Token: NumberOrString,
-}
-
-#[post("/two-factor/authenticator", data = "<data>")]
-fn activate_authenticator(data: JsonUpcase<EnableAuthenticatorData>, headers: Headers, conn: DbConn) -> JsonResult {
-    let data: EnableAuthenticatorData = data.into_inner().data;
-    let password_hash = data.MasterPasswordHash;
-    let key = data.Key;
-    let token = data.Token.into_i32()? as u64;
-
-    let mut user = headers.user;
-
-    if !user.check_valid_password(&password_hash) {
-        err!("Invalid password");
-    }
-
-    // Validate key as base32 and 20 bytes length
-    let decoded_key: Vec<u8> = match BASE32.decode(key.as_bytes()) {
-        Ok(decoded) => decoded,
-        _ => err!("Invalid totp secret"),
-    };
-
-    if decoded_key.len() != 20 {
-        err!("Invalid key length")
-    }
-
-    let type_ = TwoFactorType::Authenticator;
-    let twofactor = TwoFactor::new(user.uuid.clone(), type_, key.to_uppercase());
-
-    // Validate the token provided with the key
-    validate_totp_code(token, &twofactor.data)?;
-
-    _generate_recover_code(&mut user, &conn);
-    twofactor.save(&conn)?;
-
-    Ok(Json(json!({
-        "Enabled": true,
-        "Key": key,
-        "Object": "twoFactorAuthenticator"
-    })))
-}
-
-#[put("/two-factor/authenticator", data = "<data>")]
-fn activate_authenticator_put(data: JsonUpcase<EnableAuthenticatorData>, headers: Headers, conn: DbConn) -> JsonResult {
-    activate_authenticator(data, headers, conn)
-}
-
-pub fn validate_totp_code_str(totp_code: &str, secret: &str) -> EmptyResult {
-    let totp_code: u64 = match totp_code.parse() {
-        Ok(code) => code,
-        _ => err!("TOTP code is not a number"),
-    };
-
-    validate_totp_code(totp_code, secret)
-}
-
-pub fn validate_totp_code(totp_code: u64, secret: &str) -> EmptyResult {
-    use oath::{totp_raw_now, HashType};
-
-    let decoded_secret = match BASE32.decode(secret.as_bytes()) {
-        Ok(s) => s,
-        Err(_) => err!("Invalid TOTP secret"),
-    };
-
-    let generated = totp_raw_now(&decoded_secret, 6, 0, 30, &HashType::SHA1);
-    if generated != totp_code {
-        err!("Invalid TOTP code");
-    }
-
-    Ok(())
-}
-
-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 = "<data>")]
-fn generate_u2f(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> JsonResult {
-    if !CONFIG.domain_set() {
-        err!("`DOMAIN` environment variable is not set. U2F disabled")
-    }
-    let data: PasswordData = data.into_inner().data;
-
-    if !headers.user.check_valid_password(&data.MasterPasswordHash) {
-        err!("Invalid password");
-    }
-
-    let (enabled, keys) = get_u2f_registrations(&headers.user.uuid, &conn)?;
-    let keys_json: Vec<Value> = keys.iter().map(U2FRegistration::to_json).collect();
-
-    Ok(Json(json!({
-        "Enabled": enabled,
-        "Keys": keys_json,
-        "Object": "twoFactorU2f"
-    })))
-}
-
-#[post("/two-factor/get-u2f-challenge", data = "<data>")]
-fn generate_u2f_challenge(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> JsonResult {
-    let data: PasswordData = data.into_inner().data;
-
-    if !headers.user.check_valid_password(&data.MasterPasswordHash) {
-        err!("Invalid password");
-    }
-
-    let _type = TwoFactorType::U2fRegisterChallenge;
-    let challenge = _create_u2f_challenge(&headers.user.uuid, _type, &conn).challenge;
-
-    Ok(Json(json!({
-        "UserId": headers.user.uuid,
-        "AppId": APP_ID.to_string(),
-        "Challenge": challenge,
-        "Version": U2F_VERSION,
-    })))
-}
-
-#[derive(Deserialize, Debug)]
-#[allow(non_snake_case)]
-struct EnableU2FData {
-    Id: NumberOrString, // 1..5
-    Name: String,
-    MasterPasswordHash: String,
-    DeviceResponse: String,
-}
-
-// This struct is referenced from the U2F lib
-// because it doesn't implement Deserialize
-#[derive(Serialize, Deserialize)]
-#[serde(rename_all = "camelCase")]
-#[serde(remote = "Registration")]
-struct RegistrationDef {
-    key_handle: Vec<u8>,
-    pub_key: Vec<u8>,
-    attestation_cert: Option<Vec<u8>>,
-}
-
-#[derive(Serialize, Deserialize)]
-struct U2FRegistration {
-    id: i32,
-    name: String,
-    #[serde(with = "RegistrationDef")]
-    reg: Registration,
-    counter: u32,
-    compromised: bool,
-}
-
-impl U2FRegistration {
-    fn to_json(&self) -> Value {
-        json!({
-            "Id": self.id,
-            "Name": self.name,
-            "Compromised": self.compromised,
-        })
-    }
-}
-
-// This struct is copied from the U2F lib
-// to add an optional error code
-#[derive(Deserialize)]
-#[serde(rename_all = "camelCase")]
-struct RegisterResponseCopy {
-    pub registration_data: String,
-    pub version: String,
-    pub client_data: String,
-
-    pub error_code: Option<NumberOrString>,
-}
-
-impl Into<RegisterResponse> for RegisterResponseCopy {
-    fn into(self) -> RegisterResponse {
-        RegisterResponse {
-            registration_data: self.registration_data,
-            version: self.version,
-            client_data: self.client_data,
-        }
-    }
-}
-
-#[post("/two-factor/u2f", data = "<data>")]
-fn activate_u2f(data: JsonUpcase<EnableU2FData>, headers: Headers, conn: DbConn) -> JsonResult {
-    let data: EnableU2FData = data.into_inner().data;
-    let mut user = headers.user;
-
-    if !user.check_valid_password(&data.MasterPasswordHash) {
-        err!("Invalid password");
-    }
-
-    let tf_type = TwoFactorType::U2fRegisterChallenge as i32;
-    let tf_challenge = match TwoFactor::find_by_user_and_type(&user.uuid, tf_type, &conn) {
-        Some(c) => c,
-        None => err!("Can't recover challenge"),
-    };
-
-    let challenge: Challenge = serde_json::from_str(&tf_challenge.data)?;
-    tf_challenge.delete(&conn)?;
-
-    let response: RegisterResponseCopy = serde_json::from_str(&data.DeviceResponse)?;
-
-    let error_code = response
-        .error_code
-        .clone()
-        .map_or("0".into(), NumberOrString::into_string);
-
-    if error_code != "0" {
-        err!("Error registering U2F token")
-    }
-
-    let registration = U2F.register_response(challenge.clone(), response.into())?;
-    let full_registration = U2FRegistration {
-        id: data.Id.into_i32()?,
-        name: data.Name,
-        reg: registration,
-        compromised: false,
-        counter: 0,
-    };
-
-    let mut regs = get_u2f_registrations(&user.uuid, &conn)?.1;
-
-    // TODO: Check that there is no repeat Id
-    regs.push(full_registration);
-    save_u2f_registrations(&user.uuid, &regs, &conn)?;
-
-    _generate_recover_code(&mut user, &conn);
-
-    let keys_json: Vec<Value> = regs.iter().map(U2FRegistration::to_json).collect();
-    Ok(Json(json!({
-        "Enabled": true,
-        "Keys": keys_json,
-        "Object": "twoFactorU2f"
-    })))
-}
-
-#[put("/two-factor/u2f", data = "<data>")]
-fn activate_u2f_put(data: JsonUpcase<EnableU2FData>, headers: Headers, conn: DbConn) -> JsonResult {
-    activate_u2f(data, headers, conn)
-}
-
-fn _create_u2f_challenge(user_uuid: &str, type_: TwoFactorType, conn: &DbConn) -> Challenge {
-    let challenge = U2F.generate_challenge().unwrap();
-
-    TwoFactor::new(user_uuid.into(), type_, serde_json::to_string(&challenge).unwrap())
-        .save(conn)
-        .expect("Error saving challenge");
-
-    challenge
-}
-
-fn save_u2f_registrations(user_uuid: &str, regs: &[U2FRegistration], conn: &DbConn) -> EmptyResult {
-    TwoFactor::new(user_uuid.into(), TwoFactorType::U2f, serde_json::to_string(regs)?).save(&conn)
-}
-
-fn get_u2f_registrations(user_uuid: &str, conn: &DbConn) -> Result<(bool, Vec<U2FRegistration>), Error> {
-    let type_ = TwoFactorType::U2f as i32;
-    let (enabled, regs) = match TwoFactor::find_by_user_and_type(user_uuid, type_, conn) {
-        Some(tf) => (tf.enabled, tf.data),
-        None => return Ok((false, Vec::new())), // If no data, return empty list
-    };
-
-    let data = match serde_json::from_str(&regs) {
-        Ok(d) => d,
-        Err(_) => {
-            // If error, try old format
-            let mut old_regs = _old_parse_registrations(&regs);
-
-            if old_regs.len() != 1 {
-                err!("The old U2F format only allows one device")
-            }
-
-            // Convert to new format
-            let new_regs = vec![U2FRegistration {
-                id: 1,
-                name: "Unnamed U2F key".into(),
-                reg: old_regs.remove(0),
-                compromised: false,
-                counter: 0,
-            }];
-
-            // Save new format
-            save_u2f_registrations(user_uuid, &new_regs, &conn)?;
-
-            new_regs
-        }
-    };
-
-    Ok((enabled, data))
-}
-
-fn _old_parse_registrations(registations: &str) -> Vec<Registration> {
-    #[derive(Deserialize)]
-    struct Helper(#[serde(with = "RegistrationDef")] Registration);
-
-    let regs: Vec<Value> = serde_json::from_str(registations).expect("Can't parse Registration data");
-
-    regs.into_iter()
-        .map(|r| serde_json::from_value(r).unwrap())
-        .map(|Helper(r)| r)
-        .collect()
-}
-
-pub fn generate_u2f_login(user_uuid: &str, conn: &DbConn) -> ApiResult<U2fSignRequest> {
-    let challenge = _create_u2f_challenge(user_uuid, TwoFactorType::U2fLoginChallenge, conn);
-
-    let registrations: Vec<_> = get_u2f_registrations(user_uuid, conn)?
-        .1
-        .into_iter()
-        .map(|r| r.reg)
-        .collect();
-
-    if registrations.is_empty() {
-        err!("No U2F devices registered")
-    }
-
-    Ok(U2F.sign_request(challenge, registrations))
-}
-
-pub fn validate_u2f_login(user_uuid: &str, response: &str, conn: &DbConn) -> EmptyResult {
-    let challenge_type = TwoFactorType::U2fLoginChallenge as i32;
-    let tf_challenge = TwoFactor::find_by_user_and_type(user_uuid, challenge_type, &conn);
-
-    let challenge = match tf_challenge {
-        Some(tf_challenge) => {
-            let challenge: Challenge = serde_json::from_str(&tf_challenge.data)?;
-            tf_challenge.delete(&conn)?;
-            challenge
-        }
-        None => err!("Can't recover login challenge"),
-    };
-    let response: SignResponse = serde_json::from_str(response)?;
-    let mut registrations = get_u2f_registrations(user_uuid, conn)?.1;
-    if registrations.is_empty() {
-        err!("No U2F devices registered")
-    }
-
-    for reg in &mut registrations {
-        let response = U2F.sign_response(challenge.clone(), reg.reg.clone(), response.clone(), reg.counter);
-        match response {
-            Ok(new_counter) => {
-                reg.counter = new_counter;
-                save_u2f_registrations(user_uuid, &registrations, &conn)?;
-
-                return Ok(());
-            }
-            Err(u2f::u2ferror::U2fError::CounterTooLow) => {
-                reg.compromised = true;
-                save_u2f_registrations(user_uuid, &registrations, &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<String>,
-    Key2: Option<String>,
-    Key3: Option<String>,
-    Key4: Option<String>,
-    Key5: Option<String>,
-    Nfc: bool,
-}
-
-#[derive(Deserialize, Serialize, Debug)]
-#[allow(non_snake_case)]
-pub struct YubikeyMetadata {
-    Keys: Vec<String>,
-    pub Nfc: bool,
-}
-
-use yubico::config::Config;
-use yubico::verify;
-
-fn parse_yubikeys(data: &EnableYubikeyData) -> Vec<String> {
-    let data_keys = [&data.Key1, &data.Key2, &data.Key3, &data.Key4, &data.Key5];
-
-    data_keys.iter().filter_map(|e| e.as_ref().cloned()).collect()
-}
-
-fn jsonify_yubikeys(yubikeys: Vec<String>) -> serde_json::Value {
-    let mut result = json!({});
-
-    for (i, key) in yubikeys.into_iter().enumerate() {
-        result[format!("Key{}", i + 1)] = Value::String(key);
-    }
-
-    result
-}
-
-fn get_yubico_credentials() -> Result<(String, String), Error> {
-    match (CONFIG.yubico_client_id(), CONFIG.yubico_secret_key()) {
-        (Some(id), Some(secret)) => Ok((id, secret)),
-        _ => err!("`YUBICO_CLIENT_ID` or `YUBICO_SECRET_KEY` environment variable is not set. Yubikey OTP Disabled"),
-    }
-}
-
-fn verify_yubikey_otp(otp: String) -> EmptyResult {
-    let (yubico_id, yubico_secret) = get_yubico_credentials()?;
-
-    let config = Config::default().set_client_id(yubico_id).set_key(yubico_secret);
-
-    match CONFIG.yubico_server() {
-        Some(server) => verify(otp, config.set_api_hosts(vec![server])),
-        None => verify(otp, config),
-    }
-    .map_res("Failed to verify OTP")
-    .and(Ok(()))
-}
-
-#[post("/two-factor/get-yubikey", data = "<data>")]
-fn generate_yubikey(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> JsonResult {
-    // Make sure the credentials are set
-    get_yubico_credentials()?;
-
-    let data: PasswordData = data.into_inner().data;
-    let user = headers.user;
-
-    if !user.check_valid_password(&data.MasterPasswordHash) {
-        err!("Invalid password");
-    }
-
-    let user_uuid = &user.uuid;
-    let yubikey_type = TwoFactorType::YubiKey as i32;
-
-    let r = TwoFactor::find_by_user_and_type(user_uuid, yubikey_type, &conn);
-
-    if let Some(r) = r {
-        let yubikey_metadata: YubikeyMetadata = serde_json::from_str(&r.data)?;
-
-        let mut result = jsonify_yubikeys(yubikey_metadata.Keys);
-
-        result["Enabled"] = Value::Bool(true);
-        result["Nfc"] = Value::Bool(yubikey_metadata.Nfc);
-        result["Object"] = Value::String("twoFactorU2f".to_owned());
-
-        Ok(Json(result))
-    } else {
-        Ok(Json(json!({
-            "Enabled": false,
-            "Object": "twoFactorU2f",
-        })))
-    }
-}
-
-#[post("/two-factor/yubikey", data = "<data>")]
-fn activate_yubikey(data: JsonUpcase<EnableYubikeyData>, headers: Headers, conn: DbConn) -> JsonResult {
-    let data: EnableYubikeyData = data.into_inner().data;
-    let mut user = headers.user;
-
-    if !user.check_valid_password(&data.MasterPasswordHash) {
-        err!("Invalid password");
-    }
-
-    // Check if we already have some data
-    let mut yubikey_data = match TwoFactor::find_by_user_and_type(&user.uuid, TwoFactorType::YubiKey as i32, &conn) {
-        Some(data) => data,
-        None => TwoFactor::new(user.uuid.clone(), TwoFactorType::YubiKey, String::new()),
-    };
-
-    let yubikeys = parse_yubikeys(&data);
-
-    if yubikeys.is_empty() {
-        return Ok(Json(json!({
-            "Enabled": false,
-            "Object": "twoFactorU2f",
-        })));
-    }
-
-    // Ensure they are valid OTPs
-    for yubikey in &yubikeys {
-        if yubikey.len() == 12 {
-            // YubiKey ID
-            continue;
-        }
-
-        verify_yubikey_otp(yubikey.to_owned()).map_res("Invalid Yubikey OTP provided")?;
-    }
-
-    let yubikey_ids: Vec<String> = yubikeys.into_iter().map(|x| (&x[..12]).to_owned()).collect();
-
-    let yubikey_metadata = YubikeyMetadata {
-        Keys: yubikey_ids,
-        Nfc: data.Nfc,
-    };
-
-    yubikey_data.data = serde_json::to_string(&yubikey_metadata).unwrap();
-    yubikey_data.save(&conn)?;
-
-    _generate_recover_code(&mut user, &conn);
-
-    let mut result = jsonify_yubikeys(yubikey_metadata.Keys);
-
-    result["Enabled"] = Value::Bool(true);
-    result["Nfc"] = Value::Bool(yubikey_metadata.Nfc);
-    result["Object"] = Value::String("twoFactorU2f".to_owned());
-
-    Ok(Json(result))
-}
-
-#[put("/two-factor/yubikey", data = "<data>")]
-fn activate_yubikey_put(data: JsonUpcase<EnableYubikeyData>, headers: Headers, conn: DbConn) -> JsonResult {
-    activate_yubikey(data, headers, conn)
-}
-
-pub fn validate_yubikey_login(response: &str, twofactor_data: &str) -> EmptyResult {
-    if response.len() != 44 {
-        err!("Invalid Yubikey OTP length");
-    }
-
-    let yubikey_metadata: YubikeyMetadata = serde_json::from_str(twofactor_data).expect("Can't parse Yubikey Metadata");
-    let response_id = &response[..12];
-
-    if !yubikey_metadata.Keys.contains(&response_id.to_owned()) {
-        err!("Given Yubikey is not registered");
-    }
-
-    let result = verify_yubikey_otp(response.to_owned());
-
-    match result {
-        Ok(_answer) => Ok(()),
-        Err(_e) => err!("Failed to verify Yubikey against OTP server"),
-    }
-}
-
-#[derive(Serialize, Deserialize)]
-struct DuoData {
-    host: String,
-    ik: String,
-    sk: String,
-}
-
-impl DuoData {
-    fn global() -> Option<Self> {
-        match CONFIG.duo_host() {
-            Some(host) => Some(Self {
-                host,
-                ik: CONFIG.duo_ikey().unwrap(),
-                sk: CONFIG.duo_skey().unwrap(),
-            }),
-            None => None,
-        }
-    }
-    fn msg(s: &str) -> Self {
-        Self {
-            host: s.into(),
-            ik: s.into(),
-            sk: s.into(),
-        }
-    }
-    fn secret() -> Self {
-        Self::msg("<global_secret>")
-    }
-    fn obscure(self) -> Self {
-        let mut host = self.host;
-        let mut ik = self.ik;
-        let mut sk = self.sk;
-
-        let digits = 4;
-        let replaced = "************";
-
-        host.replace_range(digits.., replaced);
-        ik.replace_range(digits.., replaced);
-        sk.replace_range(digits.., replaced);
-
-        Self { host, ik, sk }
-    }
-}
-
-enum DuoStatus {
-    Global(DuoData), // Using the global duo config
-    User(DuoData),   // Using the user's config
-    Disabled(bool),  // True if there is a global setting
-}
-
-impl DuoStatus {
-    fn data(self) -> Option<DuoData> {
-        match self {
-            DuoStatus::Global(data) => Some(data),
-            DuoStatus::User(data) => Some(data),
-            DuoStatus::Disabled(_) => None,
-        }
-    }
-}
-const DISABLED_MESSAGE_DEFAULT: &str = "<To use the global Duo keys, please leave these fields untouched>";
-
-#[post("/two-factor/get-duo", data = "<data>")]
-fn get_duo(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> JsonResult {
-    let data: PasswordData = data.into_inner().data;
-
-    if !headers.user.check_valid_password(&data.MasterPasswordHash) {
-        err!("Invalid password");
-    }
-
-    let data = get_user_duo_data(&headers.user.uuid, &conn);
-
-    let (enabled, data) = match data {
-        DuoStatus::Global(_) => (true, Some(DuoData::secret())),
-        DuoStatus::User(data) => (true, Some(data.obscure())),
-        DuoStatus::Disabled(true) => (false, Some(DuoData::msg(DISABLED_MESSAGE_DEFAULT))),
-        DuoStatus::Disabled(false) => (false, None),
-    };
-
-    let json = if let Some(data) = data {
-        json!({
-            "Enabled": enabled,
-            "Host": data.host,
-            "SecretKey": data.sk,
-            "IntegrationKey": data.ik,
-            "Object": "twoFactorDuo"
-        })
-    } else {
-        json!({
-            "Enabled": enabled,
-            "Object": "twoFactorDuo"
-        })
-    };
-
-    Ok(Json(json))
-}
-
-#[derive(Deserialize)]
-#[allow(non_snake_case, dead_code)]
-struct EnableDuoData {
-    MasterPasswordHash: String,
-    Host: String,
-    SecretKey: String,
-    IntegrationKey: String,
-}
-
-impl From<EnableDuoData> for DuoData {
-    fn from(d: EnableDuoData) -> Self {
-        Self {
-            host: d.Host,
-            ik: d.IntegrationKey,
-            sk: d.SecretKey,
-        }
-    }
-}
-
-fn check_duo_fields_custom(data: &EnableDuoData) -> bool {
-    fn empty_or_default(s: &str) -> bool {
-        let st = s.trim();
-        st.is_empty() || s == DISABLED_MESSAGE_DEFAULT
-    }
-
-    !empty_or_default(&data.Host) && !empty_or_default(&data.SecretKey) && !empty_or_default(&data.IntegrationKey)
-}
-
-#[post("/two-factor/duo", data = "<data>")]
-fn activate_duo(data: JsonUpcase<EnableDuoData>, headers: Headers, conn: DbConn) -> JsonResult {
-    let data: EnableDuoData = data.into_inner().data;
-
-    if !headers.user.check_valid_password(&data.MasterPasswordHash) {
-        err!("Invalid password");
-    }
-
-    let (data, data_str) = if check_duo_fields_custom(&data) {
-        let data_req: DuoData = data.into();
-        let data_str = serde_json::to_string(&data_req)?;
-        duo_api_request("GET", "/auth/v2/check", "", &data_req).map_res("Failed to validate Duo credentials")?;
-        (data_req.obscure(), data_str)
-    } else {
-        (DuoData::secret(), String::new())
-    };
-
-    let type_ = TwoFactorType::Duo;
-    let twofactor = TwoFactor::new(headers.user.uuid.clone(), type_, data_str);
-    twofactor.save(&conn)?;
-
-    Ok(Json(json!({
-        "Enabled": true,
-        "Host": data.host,
-        "SecretKey": data.sk,
-        "IntegrationKey": data.ik,
-        "Object": "twoFactorDuo"
-    })))
-}
-
-#[put("/two-factor/duo", data = "<data>")]
-fn activate_duo_put(data: JsonUpcase<EnableDuoData>, headers: Headers, conn: DbConn) -> JsonResult {
-    activate_duo(data, headers, conn)
-}
-
-fn duo_api_request(method: &str, path: &str, params: &str, data: &DuoData) -> EmptyResult {
-    const AGENT: &str = "bitwarden_rs:Duo/1.0 (Rust)";
-
-    use reqwest::{header::*, Client, Method};
-    use std::str::FromStr;
-
-    let url = format!("https://{}{}", &data.host, path);
-    let date = Utc::now().to_rfc2822();
-    let username = &data.ik;
-    let fields = [&date, method, &data.host, path, params];
-    let password = crypto::hmac_sign(&data.sk, &fields.join("\n"));
-
-    let m = Method::from_str(method).unwrap_or_default();
-
-    Client::new()
-        .request(m, &url)
-        .basic_auth(username, Some(password))
-        .header(USER_AGENT, AGENT)
-        .header(DATE, date)
-        .send()?
-        .error_for_status()?;
-
-    Ok(())
-}
-
-const DUO_EXPIRE: i64 = 300;
-const APP_EXPIRE: i64 = 3600;
-
-const AUTH_PREFIX: &str = "AUTH";
-const DUO_PREFIX: &str = "TX";
-const APP_PREFIX: &str = "APP";
-
-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<String> {
-    let split: Vec<&str> = val.split('|').collect();
-    if split.len() != 3 {
-        err!("Invalid value length")
-    }
-
-    let u_prefix = split[0];
-    let u_b64 = split[1];
-    let u_sig = split[2];
-
-    let sig = crypto::hmac_sign(key, &format!("{}|{}", u_prefix, u_b64));
-
-    if !crypto::ct_eq(crypto::hmac_sign(key, &sig), crypto::hmac_sign(key, u_sig)) {
-        err!("Duo signatures don't match")
-    }
-
-    if u_prefix != prefix {
-        err!("Prefixes don't match")
-    }
-
-    let cookie_vec = match BASE64.decode(u_b64.as_bytes()) {
-        Ok(c) => c,
-        Err(_) => err!("Invalid Duo cookie encoding"),
-    };
-
-    let cookie = match String::from_utf8(cookie_vec) {
-        Ok(c) => c,
-        Err(_) => err!("Invalid Duo cookie encoding"),
-    };
-
-    let cookie_split: Vec<&str> = cookie.split('|').collect();
-    if cookie_split.len() != 3 {
-        err!("Invalid cookie length")
-    }
-
-    let username = cookie_split[0];
-    let u_ikey = cookie_split[1];
-    let expire = cookie_split[2];
-
-    if !crypto::ct_eq(ikey, u_ikey) {
-        err!("Invalid ikey")
-    }
-
-    let expire = match expire.parse() {
-        Ok(e) => e,
-        Err(_) => err!("Invalid expire time"),
-    };
-
-    if time >= expire {
-        err!("Expired authorization")
-    }
-
-    Ok(username.into())
-}
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<Route> {
+    routes![
+        generate_authenticator,
+        activate_authenticator,
+        activate_authenticator_put,
+    ]
+}
+
+#[post("/two-factor/get-authenticator", data = "<data>")]
+fn generate_authenticator(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> JsonResult {
+    let data: PasswordData = data.into_inner().data;
+    let user = headers.user;
+
+    if !user.check_valid_password(&data.MasterPasswordHash) {
+        err!("Invalid password");
+    }
+
+    let type_ = TwoFactorType::Authenticator as i32;
+    let twofactor = TwoFactor::find_by_user_and_type(&user.uuid, type_, &conn);
+
+    let (enabled, key) = match twofactor {
+        Some(tf) => (true, tf.data),
+        _ => (false, BASE32.encode(&crypto::get_random(vec![0u8; 20]))),
+    };
+
+    Ok(Json(json!({
+        "Enabled": enabled,
+        "Key": key,
+        "Object": "twoFactorAuthenticator"
+    })))
+}
+
+#[derive(Deserialize, Debug)]
+#[allow(non_snake_case)]
+struct EnableAuthenticatorData {
+    MasterPasswordHash: String,
+    Key: String,
+    Token: NumberOrString,
+}
+
+#[post("/two-factor/authenticator", data = "<data>")]
+fn activate_authenticator(data: JsonUpcase<EnableAuthenticatorData>, headers: Headers, conn: DbConn) -> JsonResult {
+    let data: EnableAuthenticatorData = data.into_inner().data;
+    let password_hash = data.MasterPasswordHash;
+    let key = data.Key;
+    let token = data.Token.into_i32()? as u64;
+
+    let mut user = headers.user;
+
+    if !user.check_valid_password(&password_hash) {
+        err!("Invalid password");
+    }
+
+    // Validate key as base32 and 20 bytes length
+    let decoded_key: Vec<u8> = match BASE32.decode(key.as_bytes()) {
+        Ok(decoded) => decoded,
+        _ => err!("Invalid totp secret"),
+    };
+
+    if decoded_key.len() != 20 {
+        err!("Invalid key length")
+    }
+
+    let type_ = TwoFactorType::Authenticator;
+    let twofactor = TwoFactor::new(user.uuid.clone(), type_, key.to_uppercase());
+
+    // Validate the token provided with the key
+    totp::validate_totp_code(token, &twofactor.data)?;
+
+    _generate_recover_code(&mut user, &conn);
+    twofactor.save(&conn)?;
+
+    Ok(Json(json!({
+        "Enabled": true,
+        "Key": key,
+        "Object": "twoFactorAuthenticator"
+    })))
+}
+
+#[put("/two-factor/authenticator", data = "<data>")]
+fn activate_authenticator_put(data: JsonUpcase<EnableAuthenticatorData>, headers: Headers, conn: DbConn) -> JsonResult {
+    activate_authenticator(data, headers, conn)
+}
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<Route> {
+    routes![
+        get_duo,
+        activate_duo,
+        activate_duo_put,
+    ]
+}
+
+#[derive(Serialize, Deserialize)]
+struct DuoData {
+    host: String,
+    ik: String,
+    sk: String,
+}
+
+impl DuoData {
+    fn global() -> Option<Self> {
+        match CONFIG.duo_host() {
+            Some(host) => Some(Self {
+                host,
+                ik: CONFIG.duo_ikey().unwrap(),
+                sk: CONFIG.duo_skey().unwrap(),
+            }),
+            None => None,
+        }
+    }
+    fn msg(s: &str) -> Self {
+        Self {
+            host: s.into(),
+            ik: s.into(),
+            sk: s.into(),
+        }
+    }
+    fn secret() -> Self {
+        Self::msg("<global_secret>")
+    }
+    fn obscure(self) -> Self {
+        let mut host = self.host;
+        let mut ik = self.ik;
+        let mut sk = self.sk;
+
+        let digits = 4;
+        let replaced = "************";
+
+        host.replace_range(digits.., replaced);
+        ik.replace_range(digits.., replaced);
+        sk.replace_range(digits.., replaced);
+
+        Self { host, ik, sk }
+    }
+}
+
+enum DuoStatus {
+    Global(DuoData),
+    // Using the global duo config
+    User(DuoData),
+    // Using the user's config
+    Disabled(bool),  // True if there is a global setting
+}
+
+impl DuoStatus {
+    fn data(self) -> Option<DuoData> {
+        match self {
+            DuoStatus::Global(data) => Some(data),
+            DuoStatus::User(data) => Some(data),
+            DuoStatus::Disabled(_) => None,
+        }
+    }
+}
+
+const DISABLED_MESSAGE_DEFAULT: &str = "<To use the global Duo keys, please leave these fields untouched>";
+
+#[post("/two-factor/get-duo", data = "<data>")]
+fn get_duo(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> JsonResult {
+    let data: PasswordData = data.into_inner().data;
+
+    if !headers.user.check_valid_password(&data.MasterPasswordHash) {
+        err!("Invalid password");
+    }
+
+    let data = get_user_duo_data(&headers.user.uuid, &conn);
+
+    let (enabled, data) = match data {
+        DuoStatus::Global(_) => (true, Some(DuoData::secret())),
+        DuoStatus::User(data) => (true, Some(data.obscure())),
+        DuoStatus::Disabled(true) => (false, Some(DuoData::msg(DISABLED_MESSAGE_DEFAULT))),
+        DuoStatus::Disabled(false) => (false, None),
+    };
+
+    let json = if let Some(data) = data {
+        json!({
+            "Enabled": enabled,
+            "Host": data.host,
+            "SecretKey": data.sk,
+            "IntegrationKey": data.ik,
+            "Object": "twoFactorDuo"
+        })
+    } else {
+        json!({
+            "Enabled": enabled,
+            "Object": "twoFactorDuo"
+        })
+    };
+
+    Ok(Json(json))
+}
+
+#[derive(Deserialize)]
+#[allow(non_snake_case, dead_code)]
+struct EnableDuoData {
+    MasterPasswordHash: String,
+    Host: String,
+    SecretKey: String,
+    IntegrationKey: String,
+}
+
+impl From<EnableDuoData> for DuoData {
+    fn from(d: EnableDuoData) -> Self {
+        Self {
+            host: d.Host,
+            ik: d.IntegrationKey,
+            sk: d.SecretKey,
+        }
+    }
+}
+
+fn check_duo_fields_custom(data: &EnableDuoData) -> bool {
+    fn empty_or_default(s: &str) -> bool {
+        let st = s.trim();
+        st.is_empty() || s == DISABLED_MESSAGE_DEFAULT
+    }
+
+    !empty_or_default(&data.Host) && !empty_or_default(&data.SecretKey) && !empty_or_default(&data.IntegrationKey)
+}
+
+#[post("/two-factor/duo", data = "<data>")]
+fn activate_duo(data: JsonUpcase<EnableDuoData>, headers: Headers, conn: DbConn) -> JsonResult {
+    let data: EnableDuoData = data.into_inner().data;
+
+    if !headers.user.check_valid_password(&data.MasterPasswordHash) {
+        err!("Invalid password");
+    }
+
+    let (data, data_str) = if check_duo_fields_custom(&data) {
+        let data_req: DuoData = data.into();
+        let data_str = serde_json::to_string(&data_req)?;
+        duo_api_request("GET", "/auth/v2/check", "", &data_req).map_res("Failed to validate Duo credentials")?;
+        (data_req.obscure(), data_str)
+    } else {
+        (DuoData::secret(), String::new())
+    };
+
+    let type_ = TwoFactorType::Duo;
+    let twofactor = TwoFactor::new(headers.user.uuid.clone(), type_, data_str);
+    twofactor.save(&conn)?;
+
+    Ok(Json(json!({
+        "Enabled": true,
+        "Host": data.host,
+        "SecretKey": data.sk,
+        "IntegrationKey": data.ik,
+        "Object": "twoFactorDuo"
+    })))
+}
+
+#[put("/two-factor/duo", data = "<data>")]
+fn activate_duo_put(data: JsonUpcase<EnableDuoData>, headers: Headers, conn: DbConn) -> JsonResult {
+    activate_duo(data, headers, conn)
+}
+
+fn duo_api_request(method: &str, path: &str, params: &str, data: &DuoData) -> EmptyResult {
+    const AGENT: &str = "bitwarden_rs:Duo/1.0 (Rust)";
+
+    use reqwest::{header::*, Client, Method};
+    use std::str::FromStr;
+
+    let url = format!("https://{}{}", &data.host, path);
+    let date = Utc::now().to_rfc2822();
+    let username = &data.ik;
+    let fields = [&date, method, &data.host, path, params];
+    let password = crypto::hmac_sign(&data.sk, &fields.join("\n"));
+
+    let m = Method::from_str(method).unwrap_or_default();
+
+    Client::new()
+        .request(m, &url)
+        .basic_auth(username, Some(password))
+        .header(USER_AGENT, AGENT)
+        .header(DATE, date)
+        .send()?
+        .error_for_status()?;
+
+    Ok(())
+}
+
+const DUO_EXPIRE: i64 = 300;
+const APP_EXPIRE: i64 = 3600;
+
+const AUTH_PREFIX: &str = "AUTH";
+const DUO_PREFIX: &str = "TX";
+const APP_PREFIX: &str = "APP";
+
+fn get_user_duo_data(uuid: &str, conn: &DbConn) -> DuoStatus {
+    let type_ = TwoFactorType::Duo as i32;
+
+    // If the user doesn't have an entry, disabled
+    let twofactor = match TwoFactor::find_by_user_and_type(uuid, type_, &conn) {
+        Some(t) => t,
+        None => return DuoStatus::Disabled(DuoData::global().is_some()),
+    };
+
+    // If the user has the required values, we use those
+    if let Ok(data) = serde_json::from_str(&twofactor.data) {
+        return DuoStatus::User(data);
+    }
+
+    // Otherwise, we try to use the globals
+    if let Some(global) = DuoData::global() {
+        return DuoStatus::Global(global);
+    }
+
+    // If there are no globals configured, just disable it
+    DuoStatus::Disabled(false)
+}
+
+// let (ik, sk, ak, host) = get_duo_keys();
+fn get_duo_keys_email(email: &str, conn: &DbConn) -> ApiResult<(String, String, String, String)> {
+    let data = User::find_by_mail(email, &conn)
+        .and_then(|u| get_user_duo_data(&u.uuid, &conn).data())
+        .or_else(DuoData::global)
+        .map_res("Can't fetch Duo keys")?;
+
+    Ok((data.ik, data.sk, CONFIG.get_duo_akey(), data.host))
+}
+
+pub fn generate_duo_signature(email: &str, conn: &DbConn) -> ApiResult<(String, String)> {
+    let now = Utc::now().timestamp();
+
+    let (ik, sk, ak, host) = get_duo_keys_email(email, conn)?;
+
+    let duo_sign = sign_duo_values(&sk, email, &ik, DUO_PREFIX, now + DUO_EXPIRE);
+    let app_sign = sign_duo_values(&ak, email, &ik, APP_PREFIX, now + APP_EXPIRE);
+
+    Ok((format!("{}:{}", duo_sign, app_sign), host))
+}
+
+fn sign_duo_values(key: &str, email: &str, ikey: &str, prefix: &str, expire: i64) -> String {
+    let val = format!("{}|{}|{}", email, ikey, expire);
+    let cookie = format!("{}|{}", prefix, BASE64.encode(val.as_bytes()));
+
+    format!("{}|{}", cookie, crypto::hmac_sign(key, &cookie))
+}
+
+pub fn validate_duo_login(email: &str, response: &str, conn: &DbConn) -> EmptyResult {
+    let split: Vec<&str> = response.split(':').collect();
+    if split.len() != 2 {
+        err!("Invalid response length");
+    }
+
+    let auth_sig = split[0];
+    let app_sig = split[1];
+
+    let now = Utc::now().timestamp();
+
+    let (ik, sk, ak, _host) = get_duo_keys_email(email, conn)?;
+
+    let auth_user = parse_duo_values(&sk, auth_sig, &ik, AUTH_PREFIX, now)?;
+    let app_user = parse_duo_values(&ak, app_sig, &ik, APP_PREFIX, now)?;
+
+    if !crypto::ct_eq(&auth_user, app_user) || !crypto::ct_eq(&auth_user, email) {
+        err!("Error validating duo authentication")
+    }
+
+    Ok(())
+}
+
+fn parse_duo_values(key: &str, val: &str, ikey: &str, prefix: &str, time: i64) -> ApiResult<String> {
+    let split: Vec<&str> = val.split('|').collect();
+    if split.len() != 3 {
+        err!("Invalid value length")
+    }
+
+    let u_prefix = split[0];
+    let u_b64 = split[1];
+    let u_sig = split[2];
+
+    let sig = crypto::hmac_sign(key, &format!("{}|{}", u_prefix, u_b64));
+
+    if !crypto::ct_eq(crypto::hmac_sign(key, &sig), crypto::hmac_sign(key, u_sig)) {
+        err!("Duo signatures don't match")
+    }
+
+    if u_prefix != prefix {
+        err!("Prefixes don't match")
+    }
+
+    let cookie_vec = match BASE64.decode(u_b64.as_bytes()) {
+        Ok(c) => c,
+        Err(_) => err!("Invalid Duo cookie encoding"),
+    };
+
+    let cookie = match String::from_utf8(cookie_vec) {
+        Ok(c) => c,
+        Err(_) => err!("Invalid Duo cookie encoding"),
+    };
+
+    let cookie_split: Vec<&str> = cookie.split('|').collect();
+    if cookie_split.len() != 3 {
+        err!("Invalid cookie length")
+    }
+
+    let username = cookie_split[0];
+    let u_ikey = cookie_split[1];
+    let expire = cookie_split[2];
+
+    if !crypto::ct_eq(ikey, u_ikey) {
+        err!("Invalid ikey")
+    }
+
+    let expire = match expire.parse() {
+        Ok(e) => e,
+        Err(_) => err!("Invalid expire time"),
+    };
+
+    if time >= expire {
+        err!("Expired authorization")
+    }
+
+    Ok(username.into())
+}
\ 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<Route> {
+    routes![get_email, send_email_login, send_email, email,]
+}
+
+#[derive(Deserialize)]
+#[allow(non_snake_case)]
+struct SendEmailLoginData {
+    Email: String,
+    MasterPasswordHash: String,
+}
+
+// Does not require Bearer token
+#[post("/two-factor/send-email-login", data = "<data>")] // JsonResult
+fn send_email_login(data: JsonUpcase<SendEmailLoginData>, conn: DbConn) -> EmptyResult {
+    let data: SendEmailLoginData = data.into_inner().data;
+
+    use crate::db::models::User;
+
+    // Get the user
+    let mut user = match User::find_by_mail(&data.Email, &conn) {
+        Some(user) => user,
+        None => err!("Username or password is incorrect. Try again."),
+    };
+
+    // Check password
+    if !user.check_valid_password(&data.MasterPasswordHash) {
+        err!("Username or password is incorrect. Try again.")
+    }
+
+    let type_ = TwoFactorType::Email as i32;
+    let mut twofactor = TwoFactor::find_by_user_and_type(&user.uuid, type_, &conn)?;
+
+    let twofactor_data = EmailTokenData::from_json(&twofactor.data)?;
+
+    let decoded_key = totp::validate_decode_key(&twofactor_data.TotpSecret)?;
+
+    let generated_token = totp_raw_now(&decoded_key, 6, 0, TOTP_TIME_STEP, &HashType::SHA1);
+    let token_string = generated_token.to_string();
+
+    mail::send_token(&twofactor_data.Email, &token_string)?;
+
+    Ok(())
+}
+
+#[post("/two-factor/get-email", data = "<data>")]
+fn get_email(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> JsonResult {
+    let data: PasswordData = data.into_inner().data;
+    let user = headers.user;
+
+    if !user.check_valid_password(&data.MasterPasswordHash) {
+        err!("Invalid password");
+    }
+
+    let type_ = TwoFactorType::Email as i32;
+    let enabled = match TwoFactor::find_by_user_and_type(&user.uuid, type_, &conn) {
+        Some(x) => x.enabled,
+        _ => false,
+    };
+
+    Ok(Json(json!({// TODO check! FIX!
+        "Email": user.email,
+        "Enabled": enabled,
+        "Object": "twoFactorEmail"
+    })))
+}
+
+#[derive(Deserialize)]
+#[allow(non_snake_case)]
+struct SendEmailData {
+    Email: String,
+    // Email where 2FA codes will be sent to, can be different than user email account.
+    MasterPasswordHash: String,
+}
+
+// Send a verification email to the specified email address to check whether it exists/belongs to user.
+#[post("/two-factor/send-email", data = "<data>")]
+fn send_email(data: JsonUpcase<SendEmailData>, headers: Headers, conn: DbConn) -> EmptyResult {
+    use oath::{totp_raw_now, HashType};
+
+    let data: SendEmailData = data.into_inner().data;
+    let user = headers.user;
+
+    if !user.check_valid_password(&data.MasterPasswordHash) {
+        err!("Invalid password");
+    }
+
+    let type_ = TwoFactorType::Email as i32;
+
+    // TODO: Delete previous email thing.
+    match TwoFactor::find_by_user_and_type(&user.uuid, type_, &conn) {
+        Some(tf) => tf.delete(&conn),
+        _ => Ok(()),
+    };
+
+    let secret = crypto::get_random(vec![0u8; 20]);
+    let base32_secret = BASE32.encode(&secret);
+
+    let twofactor_data = EmailTokenData::new(data.Email, base32_secret);
+
+    // Uses EmailVerificationChallenge as type to show that it's not verified yet.
+    let mut twofactor = TwoFactor::new(
+        user.uuid,
+        TwoFactorType::EmailVerificationChallenge,
+        twofactor_data.to_json(),
+    );
+    twofactor.save(&conn)?;
+
+    let generated_token = totp_raw_now(&secret, 6, 0, TOTP_TIME_STEP, &HashType::SHA1);
+    let token_string = generated_token.to_string();
+
+    mail::send_token(&twofactor_data.Email, &token_string)?;
+
+    Ok(())
+}
+
+#[derive(Deserialize, Serialize)]
+#[allow(non_snake_case)]
+struct EmailData {
+    Email: String,
+    MasterPasswordHash: String,
+    Token: String,
+}
+
+// Verify email used for 2FA email codes.
+#[put("/two-factor/email", data = "<data>")]
+fn email(data: JsonUpcase<EmailData>, headers: Headers, conn: DbConn) -> JsonResult {
+    let data: EmailData = data.into_inner().data;
+    let user = headers.user;
+
+    if !user.check_valid_password(&data.MasterPasswordHash) {
+        err!("Invalid password");
+    }
+
+    let token_u64 = match data.Token.parse::<u64>() {
+        Ok(token) => token,
+        _ => err!("Could not parse token"),
+    };
+
+    let type_ = TwoFactorType::EmailVerificationChallenge as i32;
+    let mut twofactor = TwoFactor::find_by_user_and_type(&user.uuid, type_, &conn)?;
+
+    let email_data = EmailTokenData::from_json(&twofactor.data)?;
+
+    totp::validate_totp_code_with_time_step(token_u64, &email_data.TotpSecret, TOTP_TIME_STEP)?;
+
+    twofactor.atype = TwoFactorType::Email as i32;
+    twofactor.save(&conn)?;
+
+    Ok(Json(json!({
+        "Email": email_data.Email,
+        "Enabled": "true",
+        "Object": "twoFactorEmail"
+    })))
+}
+
+pub fn validate_email_code_str(code: &str, data: &str) -> EmptyResult {
+    let totp_code: u64 = match code.parse() {
+        Ok(code) => code,
+        _ => err!("Email code is not a number"),
+    };
+
+    validate_email_code(totp_code, data)
+}
+
+pub fn validate_email_code(code: u64, data: &str) -> EmptyResult {
+    let email_data = EmailTokenData::from_json(&data)?;
+
+    let decoded_secret = match BASE32.decode(email_data.TotpSecret.as_bytes()) {
+        Ok(s) => s,
+        Err(_) => err!("Invalid email secret"),
+    };
+
+    let generated = totp_raw_now(&decoded_secret, 6, 0, TOTP_TIME_STEP, &HashType::SHA1);
+    if generated != code {
+        err!("Invalid email code");
+    }
+
+    Ok(())
+}
+
+#[derive(Serialize, Deserialize)]
+pub struct EmailTokenData {
+    pub Email: String,
+    pub TotpSecret: String,
+}
+
+impl EmailTokenData {
+    pub fn new(email: String, secret: String) -> EmailTokenData {
+        EmailTokenData {
+            Email: email,
+            TotpSecret: secret,
+        }
+    }
+
+    pub fn to_json(&self) -> String {
+        serde_json::to_string(&self).unwrap()
+    }
+
+    pub fn from_json(string: &str) -> Result<EmailTokenData, Error> {
+        let res: Result<EmailTokenData, crate::serde_json::Error> = serde_json::from_str(&string);
+        match res {
+            Ok(x) => Ok(x),
+            Err(_) => err!("Could not decode EmailTokenData from string"),
+        }
+    }
+}
+
+pub fn obscure_email(email: &str) -> String {
+    let split: Vec<&str> = email.split("@").collect();
+
+    let mut name = split[0].to_string();
+    let domain = &split[1];
+
+    let name_size = name.chars().count();
+
+    let new_name = match name_size {
+        1..=2 => "*".repeat(name_size),
+        _ => {
+            let stars = "*".repeat(name_size-2);
+            name.truncate(2);
+            format!("{}{}", name, stars)
+        }
+    };
+
+    format!("{}@{}", new_name, &domain)
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_obscure_email_long() {
+        let email = "bytes@example.ext";
+
+        let result = obscure_email(&email);
+
+        // Only first two characters should be visible.
+        assert_eq!(result, "by***@example.ext");
+    }
+
+    #[test]
+    fn test_obscure_email_short() {
+        let email = "by@example.ext";
+
+        let result = obscure_email(&email);
+
+        // If it's smaller than 3 characters it should only show asterisks.
+        assert_eq!(result, "**@example.ext");
+    }
+}
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<Route> {
+    let mut routes = routes![
+        get_twofactor,
+        get_recover,
+        recover,
+        disable_twofactor,
+        disable_twofactor_put,
+    ];
+
+    routes.append(&mut authenticator::routes());
+    routes.append(&mut duo::routes());
+    routes.append(&mut email::routes());
+    routes.append(&mut u2f::routes());
+    routes.append(&mut yubikey::routes());
+
+    routes
+}
+
+#[get("/two-factor")]
+fn get_twofactor(headers: Headers, conn: DbConn) -> JsonResult {
+    let twofactors = TwoFactor::find_by_user(&headers.user.uuid, &conn);
+    let twofactors_json: Vec<Value> = twofactors.iter().map(TwoFactor::to_json_list).collect();
+
+    Ok(Json(json!({
+        "Data": twofactors_json,
+        "Object": "list",
+        "ContinuationToken": null,
+    })))
+}
+
+#[post("/two-factor/get-recover", data = "<data>")]
+fn get_recover(data: JsonUpcase<PasswordData>, headers: Headers) -> JsonResult {
+    let data: PasswordData = data.into_inner().data;
+    let user = headers.user;
+
+    if !user.check_valid_password(&data.MasterPasswordHash) {
+        err!("Invalid password");
+    }
+
+    Ok(Json(json!({
+        "Code": user.totp_recover,
+        "Object": "twoFactorRecover"
+    })))
+}
+
+#[derive(Deserialize)]
+#[allow(non_snake_case)]
+struct RecoverTwoFactor {
+    MasterPasswordHash: String,
+    Email: String,
+    RecoveryCode: String,
+}
+
+#[post("/two-factor/recover", data = "<data>")]
+fn recover(data: JsonUpcase<RecoverTwoFactor>, conn: DbConn) -> JsonResult {
+    let data: RecoverTwoFactor = data.into_inner().data;
+
+    use crate::db::models::User;
+
+    // Get the user
+    let mut user = match User::find_by_mail(&data.Email, &conn) {
+        Some(user) => user,
+        None => err!("Username or password is incorrect. Try again."),
+    };
+
+    // Check password
+    if !user.check_valid_password(&data.MasterPasswordHash) {
+        err!("Username or password is incorrect. Try again.")
+    }
+
+    // Check if recovery code is correct
+    if !user.check_valid_recovery_code(&data.RecoveryCode) {
+        err!("Recovery code is incorrect. Try again.")
+    }
+
+    // Remove all twofactors from the user
+    for twofactor in TwoFactor::find_by_user(&user.uuid, &conn) {
+        twofactor.delete(&conn)?;
+    }
+
+    // Remove the recovery code, not needed without twofactors
+    user.totp_recover = None;
+    user.save(&conn)?;
+    Ok(Json(json!({})))
+}
+
+fn _generate_recover_code(user: &mut User, conn: &DbConn) {
+    if user.totp_recover.is_none() {
+        let totp_recover = BASE32.encode(&crypto::get_random(vec![0u8; 20]));
+        user.totp_recover = Some(totp_recover);
+        user.save(conn).ok();
+    }
+}
+
+#[derive(Deserialize)]
+#[allow(non_snake_case)]
+struct DisableTwoFactorData {
+    MasterPasswordHash: String,
+    Type: NumberOrString,
+}
+
+#[post("/two-factor/disable", data = "<data>")]
+fn disable_twofactor(data: JsonUpcase<DisableTwoFactorData>, headers: Headers, conn: DbConn) -> JsonResult {
+    let data: DisableTwoFactorData = data.into_inner().data;
+    let password_hash = data.MasterPasswordHash;
+    let user = headers.user;
+
+    if !user.check_valid_password(&password_hash) {
+        err!("Invalid password");
+    }
+
+    let type_ = data.Type.into_i32()?;
+
+    if let Some(twofactor) = TwoFactor::find_by_user_and_type(&user.uuid, type_, &conn) {
+        twofactor.delete(&conn)?;
+    }
+
+    Ok(Json(json!({
+        "Enabled": false,
+        "Type": type_,
+        "Object": "twoFactorProvider"
+    })))
+}
+
+#[put("/two-factor/disable", data = "<data>")]
+fn disable_twofactor_put(data: JsonUpcase<DisableTwoFactorData>, headers: Headers, conn: DbConn) -> JsonResult {
+    disable_twofactor(data, headers, conn)
+}
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<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)
+}
\ 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<Route> {
+    routes![
+        generate_u2f,
+        generate_u2f_challenge,
+        activate_u2f,
+        activate_u2f_put,
+    ]
+}
+
+#[post("/two-factor/get-u2f", data = "<data>")]
+fn generate_u2f(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> JsonResult {
+    if !CONFIG.domain_set() {
+        err!("`DOMAIN` environment variable is not set. U2F disabled")
+    }
+    let data: PasswordData = data.into_inner().data;
+
+    if !headers.user.check_valid_password(&data.MasterPasswordHash) {
+        err!("Invalid password");
+    }
+
+    let (enabled, keys) = get_u2f_registrations(&headers.user.uuid, &conn)?;
+    let keys_json: Vec<Value> = keys.iter().map(U2FRegistration::to_json).collect();
+
+    Ok(Json(json!({
+        "Enabled": enabled,
+        "Keys": keys_json,
+        "Object": "twoFactorU2f"
+    })))
+}
+
+#[post("/two-factor/get-u2f-challenge", data = "<data>")]
+fn generate_u2f_challenge(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> JsonResult {
+    let data: PasswordData = data.into_inner().data;
+
+    if !headers.user.check_valid_password(&data.MasterPasswordHash) {
+        err!("Invalid password");
+    }
+
+    let _type = TwoFactorType::U2fRegisterChallenge;
+    let challenge = _create_u2f_challenge(&headers.user.uuid, _type, &conn).challenge;
+
+    Ok(Json(json!({
+        "UserId": headers.user.uuid,
+        "AppId": APP_ID.to_string(),
+        "Challenge": challenge,
+        "Version": U2F_VERSION,
+    })))
+}
+
+#[derive(Deserialize, Debug)]
+#[allow(non_snake_case)]
+struct EnableU2FData {
+    Id: NumberOrString,
+    // 1..5
+    Name: String,
+    MasterPasswordHash: String,
+    DeviceResponse: String,
+}
+
+// This struct is referenced from the U2F lib
+// because it doesn't implement Deserialize
+#[derive(Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+#[serde(remote = "Registration")]
+struct RegistrationDef {
+    key_handle: Vec<u8>,
+    pub_key: Vec<u8>,
+    attestation_cert: Option<Vec<u8>>,
+}
+
+#[derive(Serialize, Deserialize)]
+struct U2FRegistration {
+    id: i32,
+    name: String,
+    #[serde(with = "RegistrationDef")]
+    reg: Registration,
+    counter: u32,
+    compromised: bool,
+}
+
+impl U2FRegistration {
+    fn to_json(&self) -> Value {
+        json!({
+            "Id": self.id,
+            "Name": self.name,
+            "Compromised": self.compromised,
+        })
+    }
+}
+
+// This struct is copied from the U2F lib
+// to add an optional error code
+#[derive(Deserialize)]
+#[serde(rename_all = "camelCase")]
+struct RegisterResponseCopy {
+    pub registration_data: String,
+    pub version: String,
+    pub client_data: String,
+
+    pub error_code: Option<NumberOrString>,
+}
+
+impl Into<RegisterResponse> for RegisterResponseCopy {
+    fn into(self) -> RegisterResponse {
+        RegisterResponse {
+            registration_data: self.registration_data,
+            version: self.version,
+            client_data: self.client_data,
+        }
+    }
+}
+
+#[post("/two-factor/u2f", data = "<data>")]
+fn activate_u2f(data: JsonUpcase<EnableU2FData>, headers: Headers, conn: DbConn) -> JsonResult {
+    let data: EnableU2FData = data.into_inner().data;
+    let mut user = headers.user;
+
+    if !user.check_valid_password(&data.MasterPasswordHash) {
+        err!("Invalid password");
+    }
+
+    let tf_type = TwoFactorType::U2fRegisterChallenge as i32;
+    let tf_challenge = match TwoFactor::find_by_user_and_type(&user.uuid, tf_type, &conn) {
+        Some(c) => c,
+        None => err!("Can't recover challenge"),
+    };
+
+    let challenge: Challenge = serde_json::from_str(&tf_challenge.data)?;
+    tf_challenge.delete(&conn)?;
+
+    let response: RegisterResponseCopy = serde_json::from_str(&data.DeviceResponse)?;
+
+    let error_code = response
+        .error_code
+        .clone()
+        .map_or("0".into(), NumberOrString::into_string);
+
+    if error_code != "0" {
+        err!("Error registering U2F token")
+    }
+
+    let registration = U2F.register_response(challenge.clone(), response.into())?;
+    let full_registration = U2FRegistration {
+        id: data.Id.into_i32()?,
+        name: data.Name,
+        reg: registration,
+        compromised: false,
+        counter: 0,
+    };
+
+    let mut regs = get_u2f_registrations(&user.uuid, &conn)?.1;
+
+    // TODO: Check that there is no repeat Id
+    regs.push(full_registration);
+    save_u2f_registrations(&user.uuid, &regs, &conn)?;
+
+    _generate_recover_code(&mut user, &conn);
+
+    let keys_json: Vec<Value> = regs.iter().map(U2FRegistration::to_json).collect();
+    Ok(Json(json!({
+        "Enabled": true,
+        "Keys": keys_json,
+        "Object": "twoFactorU2f"
+    })))
+}
+
+#[put("/two-factor/u2f", data = "<data>")]
+fn activate_u2f_put(data: JsonUpcase<EnableU2FData>, headers: Headers, conn: DbConn) -> JsonResult {
+    activate_u2f(data, headers, conn)
+}
+
+fn _create_u2f_challenge(user_uuid: &str, type_: TwoFactorType, conn: &DbConn) -> Challenge {
+    let challenge = U2F.generate_challenge().unwrap();
+
+    TwoFactor::new(user_uuid.into(), type_, serde_json::to_string(&challenge).unwrap())
+        .save(conn)
+        .expect("Error saving challenge");
+
+    challenge
+}
+
+fn save_u2f_registrations(user_uuid: &str, regs: &[U2FRegistration], conn: &DbConn) -> EmptyResult {
+    TwoFactor::new(user_uuid.into(), TwoFactorType::U2f, serde_json::to_string(regs)?).save(&conn)
+}
+
+fn get_u2f_registrations(user_uuid: &str, conn: &DbConn) -> Result<(bool, Vec<U2FRegistration>), Error> {
+    let type_ = TwoFactorType::U2f as i32;
+    let (enabled, regs) = match TwoFactor::find_by_user_and_type(user_uuid, type_, conn) {
+        Some(tf) => (tf.enabled, tf.data),
+        None => return Ok((false, Vec::new())), // If no data, return empty list
+    };
+
+    let data = match serde_json::from_str(&regs) {
+        Ok(d) => d,
+        Err(_) => {
+            // If error, try old format
+            let mut old_regs = _old_parse_registrations(&regs);
+
+            if old_regs.len() != 1 {
+                err!("The old U2F format only allows one device")
+            }
+
+            // Convert to new format
+            let new_regs = vec![U2FRegistration {
+                id: 1,
+                name: "Unnamed U2F key".into(),
+                reg: old_regs.remove(0),
+                compromised: false,
+                counter: 0,
+            }];
+
+            // Save new format
+            save_u2f_registrations(user_uuid, &new_regs, &conn)?;
+
+            new_regs
+        }
+    };
+
+    Ok((enabled, data))
+}
+
+fn _old_parse_registrations(registations: &str) -> Vec<Registration> {
+    #[derive(Deserialize)]
+    struct Helper(#[serde(with = "RegistrationDef")] Registration);
+
+    let regs: Vec<Value> = serde_json::from_str(registations).expect("Can't parse Registration data");
+
+    regs.into_iter()
+        .map(|r| serde_json::from_value(r).unwrap())
+        .map(|Helper(r)| r)
+        .collect()
+}
+
+pub fn generate_u2f_login(user_uuid: &str, conn: &DbConn) -> ApiResult<U2fSignRequest> {
+    let challenge = _create_u2f_challenge(user_uuid, TwoFactorType::U2fLoginChallenge, conn);
+
+    let registrations: Vec<_> = get_u2f_registrations(user_uuid, conn)?
+        .1
+        .into_iter()
+        .map(|r| r.reg)
+        .collect();
+
+    if registrations.is_empty() {
+        err!("No U2F devices registered")
+    }
+
+    Ok(U2F.sign_request(challenge, registrations))
+}
+
+pub fn validate_u2f_login(user_uuid: &str, response: &str, conn: &DbConn) -> EmptyResult {
+    let challenge_type = TwoFactorType::U2fLoginChallenge as i32;
+    let tf_challenge = TwoFactor::find_by_user_and_type(user_uuid, challenge_type, &conn);
+
+    let challenge = match tf_challenge {
+        Some(tf_challenge) => {
+            let challenge: Challenge = serde_json::from_str(&tf_challenge.data)?;
+            tf_challenge.delete(&conn)?;
+            challenge
+        }
+        None => err!("Can't recover login challenge"),
+    };
+    let response: SignResponse = serde_json::from_str(response)?;
+    let mut registrations = get_u2f_registrations(user_uuid, conn)?.1;
+    if registrations.is_empty() {
+        err!("No U2F devices registered")
+    }
+
+    for reg in &mut registrations {
+        let response = U2F.sign_response(challenge.clone(), reg.reg.clone(), response.clone(), reg.counter);
+        match response {
+            Ok(new_counter) => {
+                reg.counter = new_counter;
+                save_u2f_registrations(user_uuid, &registrations, &conn)?;
+
+                return Ok(());
+            }
+            Err(u2f::u2ferror::U2fError::CounterTooLow) => {
+                reg.compromised = true;
+                save_u2f_registrations(user_uuid, &registrations, &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<Route> {
+    routes![
+        generate_yubikey,
+        activate_yubikey,
+        activate_yubikey_put,
+    ]
+}
+
+#[derive(Deserialize, Debug)]
+#[allow(non_snake_case)]
+struct EnableYubikeyData {
+    MasterPasswordHash: String,
+    Key1: Option<String>,
+    Key2: Option<String>,
+    Key3: Option<String>,
+    Key4: Option<String>,
+    Key5: Option<String>,
+    Nfc: bool,
+}
+
+#[derive(Deserialize, Serialize, Debug)]
+#[allow(non_snake_case)]
+pub struct YubikeyMetadata {
+    Keys: Vec<String>,
+    pub Nfc: bool,
+}
+
+fn parse_yubikeys(data: &EnableYubikeyData) -> Vec<String> {
+    let data_keys = [&data.Key1, &data.Key2, &data.Key3, &data.Key4, &data.Key5];
+
+    data_keys.iter().filter_map(|e| e.as_ref().cloned()).collect()
+}
+
+fn jsonify_yubikeys(yubikeys: Vec<String>) -> serde_json::Value {
+    let mut result = json!({});
+
+    for (i, key) in yubikeys.into_iter().enumerate() {
+        result[format!("Key{}", i + 1)] = Value::String(key);
+    }
+
+    result
+}
+
+fn get_yubico_credentials() -> Result<(String, String), Error> {
+    match (CONFIG.yubico_client_id(), CONFIG.yubico_secret_key()) {
+        (Some(id), Some(secret)) => Ok((id, secret)),
+        _ => err!("`YUBICO_CLIENT_ID` or `YUBICO_SECRET_KEY` environment variable is not set. Yubikey OTP Disabled"),
+    }
+}
+
+fn verify_yubikey_otp(otp: String) -> EmptyResult {
+    let (yubico_id, yubico_secret) = get_yubico_credentials()?;
+
+    let config = Config::default().set_client_id(yubico_id).set_key(yubico_secret);
+
+    match CONFIG.yubico_server() {
+        Some(server) => verify(otp, config.set_api_hosts(vec![server])),
+        None => verify(otp, config),
+    }
+        .map_res("Failed to verify OTP")
+        .and(Ok(()))
+}
+
+#[post("/two-factor/get-yubikey", data = "<data>")]
+fn generate_yubikey(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> JsonResult {
+    // Make sure the credentials are set
+    get_yubico_credentials()?;
+
+    let data: PasswordData = data.into_inner().data;
+    let user = headers.user;
+
+    if !user.check_valid_password(&data.MasterPasswordHash) {
+        err!("Invalid password");
+    }
+
+    let user_uuid = &user.uuid;
+    let yubikey_type = TwoFactorType::YubiKey as i32;
+
+    let r = TwoFactor::find_by_user_and_type(user_uuid, yubikey_type, &conn);
+
+    if let Some(r) = r {
+        let yubikey_metadata: YubikeyMetadata = serde_json::from_str(&r.data)?;
+
+        let mut result = jsonify_yubikeys(yubikey_metadata.Keys);
+
+        result["Enabled"] = Value::Bool(true);
+        result["Nfc"] = Value::Bool(yubikey_metadata.Nfc);
+        result["Object"] = Value::String("twoFactorU2f".to_owned());
+
+        Ok(Json(result))
+    } else {
+        Ok(Json(json!({
+            "Enabled": false,
+            "Object": "twoFactorU2f",
+        })))
+    }
+}
+
+#[post("/two-factor/yubikey", data = "<data>")]
+fn activate_yubikey(data: JsonUpcase<EnableYubikeyData>, headers: Headers, conn: DbConn) -> JsonResult {
+    let data: EnableYubikeyData = data.into_inner().data;
+    let mut user = headers.user;
+
+    if !user.check_valid_password(&data.MasterPasswordHash) {
+        err!("Invalid password");
+    }
+
+    // Check if we already have some data
+    let mut yubikey_data = match TwoFactor::find_by_user_and_type(&user.uuid, TwoFactorType::YubiKey as i32, &conn) {
+        Some(data) => data,
+        None => TwoFactor::new(user.uuid.clone(), TwoFactorType::YubiKey, String::new()),
+    };
+
+    let yubikeys = parse_yubikeys(&data);
+
+    if yubikeys.is_empty() {
+        return Ok(Json(json!({
+            "Enabled": false,
+            "Object": "twoFactorU2f",
+        })));
+    }
+
+    // Ensure they are valid OTPs
+    for yubikey in &yubikeys {
+        if yubikey.len() == 12 {
+            // YubiKey ID
+            continue;
+        }
+
+        verify_yubikey_otp(yubikey.to_owned()).map_res("Invalid Yubikey OTP provided")?;
+    }
+
+    let yubikey_ids: Vec<String> = yubikeys.into_iter().map(|x| (&x[..12]).to_owned()).collect();
+
+    let yubikey_metadata = YubikeyMetadata {
+        Keys: yubikey_ids,
+        Nfc: data.Nfc,
+    };
+
+    yubikey_data.data = serde_json::to_string(&yubikey_metadata).unwrap();
+    yubikey_data.save(&conn)?;
+
+    _generate_recover_code(&mut user, &conn);
+
+    let mut result = jsonify_yubikeys(yubikey_metadata.Keys);
+
+    result["Enabled"] = Value::Bool(true);
+    result["Nfc"] = Value::Bool(yubikey_metadata.Nfc);
+    result["Object"] = Value::String("twoFactorU2f".to_owned());
+
+    Ok(Json(result))
+}
+
+#[put("/two-factor/yubikey", data = "<data>")]
+fn activate_yubikey_put(data: JsonUpcase<EnableYubikeyData>, headers: Headers, conn: DbConn) -> JsonResult {
+    activate_yubikey(data, headers, conn)
+}
+
+pub fn validate_yubikey_login(response: &str, twofactor_data: &str) -> EmptyResult {
+    if response.len() != 44 {
+        err!("Invalid Yubikey OTP length");
+    }
+
+    let yubikey_metadata: YubikeyMetadata = serde_json::from_str(twofactor_data).expect("Can't parse Yubikey Metadata");
+    let response_id = &response[..12];
+
+    if !yubikey_metadata.Keys.contains(&response_id.to_owned()) {
+        err!("Given Yubikey is not registered");
+    }
+
+    let result = verify_yubikey_otp(response.to_owned());
+
+    match result {
+        Ok(_answer) => Ok(()),
+        Err(_e) => err!("Failed to verify Yubikey against OTP server"),
+    }
+}
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<Route> {
     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<Route> {
-    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<Value> = twofactors.iter().map(TwoFactor::to_json_list).collect();
-
-    Ok(Json(json!({
-        "Data": twofactors_json,
-        "Object": "list",
-        "ContinuationToken": null,
-    })))
-}
-
-#[post("/two-factor/get-recover", data = "<data>")]
-fn get_recover(data: JsonUpcase<PasswordData>, headers: Headers) -> JsonResult {
-    let data: PasswordData = data.into_inner().data;
-    let user = headers.user;
-
-    if !user.check_valid_password(&data.MasterPasswordHash) {
-        err!("Invalid password");
-    }
-
-    Ok(Json(json!({
-        "Code": user.totp_recover,
-        "Object": "twoFactorRecover"
-    })))
-}
-
-#[derive(Deserialize)]
-#[allow(non_snake_case)]
-struct RecoverTwoFactor {
-    MasterPasswordHash: String,
-    Email: String,
-    RecoveryCode: String,
-}
-
-#[post("/two-factor/recover", data = "<data>")]
-fn recover(data: JsonUpcase<RecoverTwoFactor>, conn: DbConn) -> JsonResult {
-    let data: RecoverTwoFactor = data.into_inner().data;
-
-    use crate::db::models::User;
-
-    // Get the user
-    let mut user = match User::find_by_mail(&data.Email, &conn) {
-        Some(user) => user,
-        None => err!("Username or password is incorrect. Try again."),
-    };
-
-    // Check password
-    if !user.check_valid_password(&data.MasterPasswordHash) {
-        err!("Username or password is incorrect. Try again.")
-    }
-
-    // Check if recovery code is correct
-    if !user.check_valid_recovery_code(&data.RecoveryCode) {
-        err!("Recovery code is incorrect. Try again.")
-    }
-
-    // Remove all twofactors from the user
-    for twofactor in TwoFactor::find_by_user(&user.uuid, &conn) {
-        twofactor.delete(&conn)?;
-    }
-
-    // Remove the recovery code, not needed without twofactors
-    user.totp_recover = None;
-    user.save(&conn)?;
-    Ok(Json(json!({})))
-}
-
-fn _generate_recover_code(user: &mut User, conn: &DbConn) {
-    if user.totp_recover.is_none() {
-        let totp_recover = BASE32.encode(&crypto::get_random(vec![0u8; 20]));
-        user.totp_recover = Some(totp_recover);
-        user.save(conn).ok();
-    }
-}
-
-#[derive(Deserialize)]
-#[allow(non_snake_case)]
-struct DisableTwoFactorData {
-    MasterPasswordHash: String,
-    Type: NumberOrString,
-}
-
-#[post("/two-factor/disable", data = "<data>")]
-fn disable_twofactor(data: JsonUpcase<DisableTwoFactorData>, headers: Headers, conn: DbConn) -> JsonResult {
-    let data: DisableTwoFactorData = data.into_inner().data;
-    let password_hash = data.MasterPasswordHash;
-    let user = headers.user;
-
-    if !user.check_valid_password(&password_hash) {
-        err!("Invalid password");
-    }
-
-    let type_ = data.Type.into_i32()?;
-
-    if let Some(twofactor) = TwoFactor::find_by_user_and_type(&user.uuid, type_, &conn) {
-        twofactor.delete(&conn)?;
-    }
-
-    Ok(Json(json!({
-        "Enabled": false,
-        "Type": type_,
-        "Object": "twoFactorProvider"
-    })))
-}
-
-#[put("/two-factor/disable", data = "<data>")]
-fn disable_twofactor_put(data: JsonUpcase<DisableTwoFactorData>, headers: Headers, conn: DbConn) -> JsonResult {
-    disable_twofactor(data, headers, conn)
-}
-
-#[post("/two-factor/get-authenticator", data = "<data>")]
-fn generate_authenticator(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> JsonResult {
-    let data: PasswordData = data.into_inner().data;
-    let user = headers.user;
-
-    if !user.check_valid_password(&data.MasterPasswordHash) {
-        err!("Invalid password");
-    }
-
-    let type_ = TwoFactorType::Authenticator as i32;
-    let twofactor = TwoFactor::find_by_user_and_type(&user.uuid, type_, &conn);
-
-    let (enabled, key) = match twofactor {
-        Some(tf) => (true, tf.data),
-        _ => (false, BASE32.encode(&crypto::get_random(vec![0u8; 20]))),
-    };
-
-    Ok(Json(json!({
-        "Enabled": enabled,
-        "Key": key,
-        "Object": "twoFactorAuthenticator"
-    })))
-}
-
-#[derive(Deserialize, Debug)]
-#[allow(non_snake_case)]
-struct EnableAuthenticatorData {
-    MasterPasswordHash: String,
-    Key: String,
-    Token: NumberOrString,
-}
-
-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)
-}
-
-#[post("/two-factor/authenticator", data = "<data>")]
-fn activate_authenticator(data: JsonUpcase<EnableAuthenticatorData>, headers: Headers, conn: DbConn) -> JsonResult {
-    let data: EnableAuthenticatorData = data.into_inner().data;
-    let password_hash = data.MasterPasswordHash;
-    let key = data.Key;
-    let token = data.Token.into_i32()? as u64;
-
-    let mut user = headers.user;
-
-    if !user.check_valid_password(&password_hash) {
-        err!("Invalid password");
-    }
-
-    // Validate key as base32 and 20 bytes length
-    let decoded_key = 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 = "<data>")]
-fn activate_authenticator_put(data: JsonUpcase<EnableAuthenticatorData>, headers: Headers, conn: DbConn) -> JsonResult {
-    activate_authenticator(data, headers, conn)
-}
-
-pub fn validate_totp_code_str(totp_code: &str, secret: &str) -> EmptyResult {
-    let totp_code: u64 = match totp_code.parse() {
-        Ok(code) => code,
-        _ => err!("TOTP code is not a number"),
-    };
-
-    validate_totp_code(totp_code, secret)
-}
-
-pub fn validate_totp_code(totp_code: u64, secret: &str) -> EmptyResult {
-    use oath::{totp_raw_now, HashType};
-
-    let decoded_secret = match BASE32.decode(secret.as_bytes()) {
-        Ok(s) => s,
-        Err(_) => err!("Invalid TOTP secret"),
-    };
-
-    let generated = totp_raw_now(&decoded_secret, 6, 0, 30, &HashType::SHA1);
-    if generated != totp_code {
-        err!("Invalid TOTP code");
-    }
-
-    Ok(())
-}
-
-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 = "<data>")]
-fn generate_u2f(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> JsonResult {
-    if !CONFIG.domain_set() {
-        err!("`DOMAIN` environment variable is not set. U2F disabled")
-    }
-    let data: PasswordData = data.into_inner().data;
-
-    if !headers.user.check_valid_password(&data.MasterPasswordHash) {
-        err!("Invalid password");
-    }
-
-    let (enabled, keys) = get_u2f_registrations(&headers.user.uuid, &conn)?;
-    let keys_json: Vec<Value> = keys.iter().map(U2FRegistration::to_json).collect();
-
-    Ok(Json(json!({
-        "Enabled": enabled,
-        "Keys": keys_json,
-        "Object": "twoFactorU2f"
-    })))
-}
-
-#[post("/two-factor/get-u2f-challenge", data = "<data>")]
-fn generate_u2f_challenge(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> JsonResult {
-    let data: PasswordData = data.into_inner().data;
-
-    if !headers.user.check_valid_password(&data.MasterPasswordHash) {
-        err!("Invalid password");
-    }
-
-    let _type = TwoFactorType::U2fRegisterChallenge;
-    let challenge = _create_u2f_challenge(&headers.user.uuid, _type, &conn).challenge;
-
-    Ok(Json(json!({
-        "UserId": headers.user.uuid,
-        "AppId": APP_ID.to_string(),
-        "Challenge": challenge,
-        "Version": U2F_VERSION,
-    })))
-}
-
-#[derive(Deserialize, Debug)]
-#[allow(non_snake_case)]
-struct EnableU2FData {
-    Id: NumberOrString, // 1..5
-    Name: String,
-    MasterPasswordHash: String,
-    DeviceResponse: String,
-}
-
-// This struct is referenced from the U2F lib
-// because it doesn't implement Deserialize
-#[derive(Serialize, Deserialize)]
-#[serde(rename_all = "camelCase")]
-#[serde(remote = "Registration")]
-struct RegistrationDef {
-    key_handle: Vec<u8>,
-    pub_key: Vec<u8>,
-    attestation_cert: Option<Vec<u8>>,
-}
-
-#[derive(Serialize, Deserialize)]
-struct U2FRegistration {
-    id: i32,
-    name: String,
-    #[serde(with = "RegistrationDef")]
-    reg: Registration,
-    counter: u32,
-    compromised: bool,
-}
-
-impl U2FRegistration {
-    fn to_json(&self) -> Value {
-        json!({
-            "Id": self.id,
-            "Name": self.name,
-            "Compromised": self.compromised,
-        })
-    }
-}
-
-// This struct is copied from the U2F lib
-// to add an optional error code
-#[derive(Deserialize)]
-#[serde(rename_all = "camelCase")]
-struct RegisterResponseCopy {
-    pub registration_data: String,
-    pub version: String,
-    pub client_data: String,
-
-    pub error_code: Option<NumberOrString>,
-}
-
-impl Into<RegisterResponse> for RegisterResponseCopy {
-    fn into(self) -> RegisterResponse {
-        RegisterResponse {
-            registration_data: self.registration_data,
-            version: self.version,
-            client_data: self.client_data,
-        }
-    }
-}
-
-#[post("/two-factor/u2f", data = "<data>")]
-fn activate_u2f(data: JsonUpcase<EnableU2FData>, headers: Headers, conn: DbConn) -> JsonResult {
-    let data: EnableU2FData = data.into_inner().data;
-    let mut user = headers.user;
-
-    if !user.check_valid_password(&data.MasterPasswordHash) {
-        err!("Invalid password");
-    }
-
-    let tf_type = TwoFactorType::U2fRegisterChallenge as i32;
-    let tf_challenge = match TwoFactor::find_by_user_and_type(&user.uuid, tf_type, &conn) {
-        Some(c) => c,
-        None => err!("Can't recover challenge"),
-    };
-
-    let challenge: Challenge = serde_json::from_str(&tf_challenge.data)?;
-    tf_challenge.delete(&conn)?;
-
-    let response: RegisterResponseCopy = serde_json::from_str(&data.DeviceResponse)?;
-
-    let error_code = response
-        .error_code
-        .clone()
-        .map_or("0".into(), NumberOrString::into_string);
-
-    if error_code != "0" {
-        err!("Error registering U2F token")
-    }
-
-    let registration = U2F.register_response(challenge.clone(), response.into())?;
-    let full_registration = U2FRegistration {
-        id: data.Id.into_i32()?,
-        name: data.Name,
-        reg: registration,
-        compromised: false,
-        counter: 0,
-    };
-
-    let mut regs = get_u2f_registrations(&user.uuid, &conn)?.1;
-
-    // TODO: Check that there is no repeat Id
-    regs.push(full_registration);
-    save_u2f_registrations(&user.uuid, &regs, &conn)?;
-
-    _generate_recover_code(&mut user, &conn);
-
-    let keys_json: Vec<Value> = regs.iter().map(U2FRegistration::to_json).collect();
-    Ok(Json(json!({
-        "Enabled": true,
-        "Keys": keys_json,
-        "Object": "twoFactorU2f"
-    })))
-}
-
-#[put("/two-factor/u2f", data = "<data>")]
-fn activate_u2f_put(data: JsonUpcase<EnableU2FData>, headers: Headers, conn: DbConn) -> JsonResult {
-    activate_u2f(data, headers, conn)
-}
-
-fn _create_u2f_challenge(user_uuid: &str, type_: TwoFactorType, conn: &DbConn) -> Challenge {
-    let challenge = U2F.generate_challenge().unwrap();
-
-    TwoFactor::new(user_uuid.into(), type_, serde_json::to_string(&challenge).unwrap())
-        .save(conn)
-        .expect("Error saving challenge");
-
-    challenge
-}
-
-fn save_u2f_registrations(user_uuid: &str, regs: &[U2FRegistration], conn: &DbConn) -> EmptyResult {
-    TwoFactor::new(user_uuid.into(), TwoFactorType::U2f, serde_json::to_string(regs)?).save(&conn)
-}
-
-fn get_u2f_registrations(user_uuid: &str, conn: &DbConn) -> Result<(bool, Vec<U2FRegistration>), Error> {
-    let type_ = TwoFactorType::U2f as i32;
-    let (enabled, regs) = match TwoFactor::find_by_user_and_type(user_uuid, type_, conn) {
-        Some(tf) => (tf.enabled, tf.data),
-        None => return Ok((false, Vec::new())), // If no data, return empty list
-    };
-
-    let data = match serde_json::from_str(&regs) {
-        Ok(d) => d,
-        Err(_) => {
-            // If error, try old format
-            let mut old_regs = _old_parse_registrations(&regs);
-
-            if old_regs.len() != 1 {
-                err!("The old U2F format only allows one device")
-            }
-
-            // Convert to new format
-            let new_regs = vec![U2FRegistration {
-                id: 1,
-                name: "Unnamed U2F key".into(),
-                reg: old_regs.remove(0),
-                compromised: false,
-                counter: 0,
-            }];
-
-            // Save new format
-            save_u2f_registrations(user_uuid, &new_regs, &conn)?;
-
-            new_regs
-        }
-    };
-
-    Ok((enabled, data))
-}
-
-fn _old_parse_registrations(registations: &str) -> Vec<Registration> {
-    #[derive(Deserialize)]
-    struct Helper(#[serde(with = "RegistrationDef")] Registration);
-
-    let regs: Vec<Value> = serde_json::from_str(registations).expect("Can't parse Registration data");
-
-    regs.into_iter()
-        .map(|r| serde_json::from_value(r).unwrap())
-        .map(|Helper(r)| r)
-        .collect()
-}
-
-pub fn generate_u2f_login(user_uuid: &str, conn: &DbConn) -> ApiResult<U2fSignRequest> {
-    let challenge = _create_u2f_challenge(user_uuid, TwoFactorType::U2fLoginChallenge, conn);
-
-    let registrations: Vec<_> = get_u2f_registrations(user_uuid, conn)?
-        .1
-        .into_iter()
-        .map(|r| r.reg)
-        .collect();
-
-    if registrations.is_empty() {
-        err!("No U2F devices registered")
-    }
-
-    Ok(U2F.sign_request(challenge, registrations))
-}
-
-pub fn validate_u2f_login(user_uuid: &str, response: &str, conn: &DbConn) -> EmptyResult {
-    let challenge_type = TwoFactorType::U2fLoginChallenge as i32;
-    let tf_challenge = TwoFactor::find_by_user_and_type(user_uuid, challenge_type, &conn);
-
-    let challenge = match tf_challenge {
-        Some(tf_challenge) => {
-            let challenge: Challenge = serde_json::from_str(&tf_challenge.data)?;
-            tf_challenge.delete(&conn)?;
-            challenge
-        }
-        None => err!("Can't recover login challenge"),
-    };
-    let response: SignResponse = serde_json::from_str(response)?;
-    let mut registrations = get_u2f_registrations(user_uuid, conn)?.1;
-    if registrations.is_empty() {
-        err!("No U2F devices registered")
-    }
-
-    for reg in &mut registrations {
-        let response = U2F.sign_response(challenge.clone(), reg.reg.clone(), response.clone(), reg.counter);
-        match response {
-            Ok(new_counter) => {
-                reg.counter = new_counter;
-                save_u2f_registrations(user_uuid, &registrations, &conn)?;
-
-                return Ok(());
-            }
-            Err(u2f::u2ferror::U2fError::CounterTooLow) => {
-                reg.compromised = true;
-                save_u2f_registrations(user_uuid, &registrations, &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<String>,
-    Key2: Option<String>,
-    Key3: Option<String>,
-    Key4: Option<String>,
-    Key5: Option<String>,
-    Nfc: bool,
-}
-
-#[derive(Deserialize, Serialize, Debug)]
-#[allow(non_snake_case)]
-pub struct YubikeyMetadata {
-    Keys: Vec<String>,
-    pub Nfc: bool,
-}
-
-use yubico::config::Config;
-use yubico::verify;
-
-fn parse_yubikeys(data: &EnableYubikeyData) -> Vec<String> {
-    let data_keys = [&data.Key1, &data.Key2, &data.Key3, &data.Key4, &data.Key5];
-
-    data_keys.iter().filter_map(|e| e.as_ref().cloned()).collect()
-}
-
-fn jsonify_yubikeys(yubikeys: Vec<String>) -> serde_json::Value {
-    let mut result = json!({});
-
-    for (i, key) in yubikeys.into_iter().enumerate() {
-        result[format!("Key{}", i + 1)] = Value::String(key);
-    }
-
-    result
-}
-
-fn get_yubico_credentials() -> Result<(String, String), Error> {
-    match (CONFIG.yubico_client_id(), CONFIG.yubico_secret_key()) {
-        (Some(id), Some(secret)) => Ok((id, secret)),
-        _ => err!("`YUBICO_CLIENT_ID` or `YUBICO_SECRET_KEY` environment variable is not set. Yubikey OTP Disabled"),
-    }
-}
-
-fn verify_yubikey_otp(otp: String) -> EmptyResult {
-    let (yubico_id, yubico_secret) = get_yubico_credentials()?;
-
-    let config = Config::default().set_client_id(yubico_id).set_key(yubico_secret);
-
-    match CONFIG.yubico_server() {
-        Some(server) => verify(otp, config.set_api_hosts(vec![server])),
-        None => verify(otp, config),
-    }
-    .map_res("Failed to verify OTP")
-    .and(Ok(()))
-}
-
-#[post("/two-factor/get-yubikey", data = "<data>")]
-fn generate_yubikey(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> JsonResult {
-    // Make sure the credentials are set
-    get_yubico_credentials()?;
-
-    let data: PasswordData = data.into_inner().data;
-    let user = headers.user;
-
-    if !user.check_valid_password(&data.MasterPasswordHash) {
-        err!("Invalid password");
-    }
-
-    let user_uuid = &user.uuid;
-    let yubikey_type = TwoFactorType::YubiKey as i32;
-
-    let r = TwoFactor::find_by_user_and_type(user_uuid, yubikey_type, &conn);
-
-    if let Some(r) = r {
-        let yubikey_metadata: YubikeyMetadata = serde_json::from_str(&r.data)?;
-
-        let mut result = jsonify_yubikeys(yubikey_metadata.Keys);
-
-        result["Enabled"] = Value::Bool(true);
-        result["Nfc"] = Value::Bool(yubikey_metadata.Nfc);
-        result["Object"] = Value::String("twoFactorU2f".to_owned());
-
-        Ok(Json(result))
-    } else {
-        Ok(Json(json!({
-            "Enabled": false,
-            "Object": "twoFactorU2f",
-        })))
-    }
-}
-
-#[post("/two-factor/yubikey", data = "<data>")]
-fn activate_yubikey(data: JsonUpcase<EnableYubikeyData>, headers: Headers, conn: DbConn) -> JsonResult {
-    let data: EnableYubikeyData = data.into_inner().data;
-    let mut user = headers.user;
-
-    if !user.check_valid_password(&data.MasterPasswordHash) {
-        err!("Invalid password");
-    }
-
-    // Check if we already have some data
-    let mut yubikey_data = match TwoFactor::find_by_user_and_type(&user.uuid, TwoFactorType::YubiKey as i32, &conn) {
-        Some(data) => data,
-        None => TwoFactor::new(user.uuid.clone(), TwoFactorType::YubiKey, String::new()),
-    };
-
-    let yubikeys = parse_yubikeys(&data);
-
-    if yubikeys.is_empty() {
-        return Ok(Json(json!({
-            "Enabled": false,
-            "Object": "twoFactorU2f",
-        })));
-    }
-
-    // Ensure they are valid OTPs
-    for yubikey in &yubikeys {
-        if yubikey.len() == 12 {
-            // YubiKey ID
-            continue;
-        }
-
-        verify_yubikey_otp(yubikey.to_owned()).map_res("Invalid Yubikey OTP provided")?;
-    }
-
-    let yubikey_ids: Vec<String> = yubikeys.into_iter().map(|x| (&x[..12]).to_owned()).collect();
-
-    let yubikey_metadata = YubikeyMetadata {
-        Keys: yubikey_ids,
-        Nfc: data.Nfc,
-    };
-
-    yubikey_data.data = serde_json::to_string(&yubikey_metadata).unwrap();
-    yubikey_data.save(&conn)?;
-
-    _generate_recover_code(&mut user, &conn);
-
-    let mut result = jsonify_yubikeys(yubikey_metadata.Keys);
-
-    result["Enabled"] = Value::Bool(true);
-    result["Nfc"] = Value::Bool(yubikey_metadata.Nfc);
-    result["Object"] = Value::String("twoFactorU2f".to_owned());
-
-    Ok(Json(result))
-}
-
-#[put("/two-factor/yubikey", data = "<data>")]
-fn activate_yubikey_put(data: JsonUpcase<EnableYubikeyData>, headers: Headers, conn: DbConn) -> JsonResult {
-    activate_yubikey(data, headers, conn)
-}
-
-pub fn validate_yubikey_login(response: &str, twofactor_data: &str) -> EmptyResult {
-    if response.len() != 44 {
-        err!("Invalid Yubikey OTP length");
-    }
-
-    let yubikey_metadata: YubikeyMetadata = serde_json::from_str(twofactor_data).expect("Can't parse Yubikey Metadata");
-    let response_id = &response[..12];
-
-    if !yubikey_metadata.Keys.contains(&response_id.to_owned()) {
-        err!("Given Yubikey is not registered");
-    }
-
-    let result = verify_yubikey_otp(response.to_owned());
-
-    match result {
-        Ok(_answer) => Ok(()),
-        Err(_e) => err!("Failed to verify Yubikey against OTP server"),
-    }
-}
-
-#[derive(Serialize, Deserialize)]
-struct DuoData {
-    host: String,
-    ik: String,
-    sk: String,
-}
-
-impl DuoData {
-    fn global() -> Option<Self> {
-        match CONFIG.duo_host() {
-            Some(host) => Some(Self {
-                host,
-                ik: CONFIG.duo_ikey().unwrap(),
-                sk: CONFIG.duo_skey().unwrap(),
-            }),
-            None => None,
-        }
-    }
-    fn msg(s: &str) -> Self {
-        Self {
-            host: s.into(),
-            ik: s.into(),
-            sk: s.into(),
-        }
-    }
-    fn secret() -> Self {
-        Self::msg("<global_secret>")
-    }
-    fn obscure(self) -> Self {
-        let mut host = self.host;
-        let mut ik = self.ik;
-        let mut sk = self.sk;
-
-        let digits = 4;
-        let replaced = "************";
-
-        host.replace_range(digits.., replaced);
-        ik.replace_range(digits.., replaced);
-        sk.replace_range(digits.., replaced);
-
-        Self { host, ik, sk }
-    }
-}
-
-enum DuoStatus {
-    Global(DuoData), // Using the global duo config
-    User(DuoData),   // Using the user's config
-    Disabled(bool),  // True if there is a global setting
-}
-
-impl DuoStatus {
-    fn data(self) -> Option<DuoData> {
-        match self {
-            DuoStatus::Global(data) => Some(data),
-            DuoStatus::User(data) => Some(data),
-            DuoStatus::Disabled(_) => None,
-        }
-    }
-}
-const DISABLED_MESSAGE_DEFAULT: &str = "<To use the global Duo keys, please leave these fields untouched>";
-
-#[post("/two-factor/get-duo", data = "<data>")]
-fn get_duo(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> JsonResult {
-    let data: PasswordData = data.into_inner().data;
-
-    if !headers.user.check_valid_password(&data.MasterPasswordHash) {
-        err!("Invalid password");
-    }
-
-    let data = get_user_duo_data(&headers.user.uuid, &conn);
-
-    let (enabled, data) = match data {
-        DuoStatus::Global(_) => (true, Some(DuoData::secret())),
-        DuoStatus::User(data) => (true, Some(data.obscure())),
-        DuoStatus::Disabled(true) => (false, Some(DuoData::msg(DISABLED_MESSAGE_DEFAULT))),
-        DuoStatus::Disabled(false) => (false, None),
-    };
-
-    let json = if let Some(data) = data {
-        json!({
-            "Enabled": enabled,
-            "Host": data.host,
-            "SecretKey": data.sk,
-            "IntegrationKey": data.ik,
-            "Object": "twoFactorDuo"
-        })
-    } else {
-        json!({
-            "Enabled": enabled,
-            "Object": "twoFactorDuo"
-        })
-    };
-
-    Ok(Json(json))
-}
-
-#[derive(Deserialize)]
-#[allow(non_snake_case, dead_code)]
-struct EnableDuoData {
-    MasterPasswordHash: String,
-    Host: String,
-    SecretKey: String,
-    IntegrationKey: String,
-}
-
-impl From<EnableDuoData> for DuoData {
-    fn from(d: EnableDuoData) -> Self {
-        Self {
-            host: d.Host,
-            ik: d.IntegrationKey,
-            sk: d.SecretKey,
-        }
-    }
-}
-
-fn check_duo_fields_custom(data: &EnableDuoData) -> bool {
-    fn empty_or_default(s: &str) -> bool {
-        let st = s.trim();
-        st.is_empty() || s == DISABLED_MESSAGE_DEFAULT
-    }
-
-    !empty_or_default(&data.Host) && !empty_or_default(&data.SecretKey) && !empty_or_default(&data.IntegrationKey)
-}
-
-#[post("/two-factor/duo", data = "<data>")]
-fn activate_duo(data: JsonUpcase<EnableDuoData>, headers: Headers, conn: DbConn) -> JsonResult {
-    let data: EnableDuoData = data.into_inner().data;
-
-    if !headers.user.check_valid_password(&data.MasterPasswordHash) {
-        err!("Invalid password");
-    }
-
-    let (data, data_str) = if check_duo_fields_custom(&data) {
-        let data_req: DuoData = data.into();
-        let data_str = serde_json::to_string(&data_req)?;
-        duo_api_request("GET", "/auth/v2/check", "", &data_req).map_res("Failed to validate Duo credentials")?;
-        (data_req.obscure(), data_str)
-    } else {
-        (DuoData::secret(), String::new())
-    };
-
-    let type_ = TwoFactorType::Duo;
-    let twofactor = TwoFactor::new(headers.user.uuid.clone(), type_, data_str);
-    twofactor.save(&conn)?;
-
-    Ok(Json(json!({
-        "Enabled": true,
-        "Host": data.host,
-        "SecretKey": data.sk,
-        "IntegrationKey": data.ik,
-        "Object": "twoFactorDuo"
-    })))
-}
-
-#[put("/two-factor/duo", data = "<data>")]
-fn activate_duo_put(data: JsonUpcase<EnableDuoData>, headers: Headers, conn: DbConn) -> JsonResult {
-    activate_duo(data, headers, conn)
-}
-
-fn duo_api_request(method: &str, path: &str, params: &str, data: &DuoData) -> EmptyResult {
-    const AGENT: &str = "bitwarden_rs:Duo/1.0 (Rust)";
-
-    use reqwest::{header::*, Client, Method};
-    use std::str::FromStr;
-
-    let url = format!("https://{}{}", &data.host, path);
-    let date = Utc::now().to_rfc2822();
-    let username = &data.ik;
-    let fields = [&date, method, &data.host, path, params];
-    let password = crypto::hmac_sign(&data.sk, &fields.join("\n"));
-
-    let m = Method::from_str(method).unwrap_or_default();
-
-    Client::new()
-        .request(m, &url)
-        .basic_auth(username, Some(password))
-        .header(USER_AGENT, AGENT)
-        .header(DATE, date)
-        .send()?
-        .error_for_status()?;
-
-    Ok(())
-}
-
-const DUO_EXPIRE: i64 = 300;
-const APP_EXPIRE: i64 = 3600;
-
-const AUTH_PREFIX: &str = "AUTH";
-const DUO_PREFIX: &str = "TX";
-const APP_PREFIX: &str = "APP";
-
-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<String> {
-    let split: Vec<&str> = val.split('|').collect();
-    if split.len() != 3 {
-        err!("Invalid value length")
-    }
-
-    let u_prefix = split[0];
-    let u_b64 = split[1];
-    let u_sig = split[2];
-
-    let sig = crypto::hmac_sign(key, &format!("{}|{}", u_prefix, u_b64));
-
-    if !crypto::ct_eq(crypto::hmac_sign(key, &sig), crypto::hmac_sign(key, u_sig)) {
-        err!("Duo signatures don't match")
-    }
-
-    if u_prefix != prefix {
-        err!("Prefixes don't match")
-    }
-
-    let cookie_vec = match BASE64.decode(u_b64.as_bytes()) {
-        Ok(c) => c,
-        Err(_) => err!("Invalid Duo cookie encoding"),
-    };
-
-    let cookie = match String::from_utf8(cookie_vec) {
-        Ok(c) => c,
-        Err(_) => err!("Invalid Duo cookie encoding"),
-    };
-
-    let cookie_split: Vec<&str> = cookie.split('|').collect();
-    if cookie_split.len() != 3 {
-        err!("Invalid cookie length")
-    }
-
-    let username = cookie_split[0];
-    let u_ikey = cookie_split[1];
-    let expire = cookie_split[2];
-
-    if !crypto::ct_eq(ikey, u_ikey) {
-        err!("Invalid ikey")
-    }
-
-    let expire = match expire.parse() {
-        Ok(e) => e,
-        Err(_) => err!("Invalid expire time"),
-    };
-
-    if time >= expire {
-        err!("Expired authorization")
-    }
-
-    Ok(username.into())
-}
-
-#[derive(Deserialize)]
-#[allow(non_snake_case)]
-struct SendEmailLoginData {
-    Email: String,
-    MasterPasswordHash: String,
-}
-
-#[put("/two-factor/send-email-login", data = "<data>")] // JsonResult
-fn send_email_login(data: JsonUpcase<SendEmailLoginData>, 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 = "<data>")]
-fn get_email(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> JsonResult {
-    let data: PasswordData = data.into_inner().data;
-    let user = headers.user;
-
-    if !user.check_valid_password(&data.MasterPasswordHash) {
-        err!("Invalid password");
-    }
-
-    let type_ = TwoFactorType::Email as i32;
-    let enabled = match TwoFactor::find_by_user_and_type(&user.uuid, type_, &conn) {
-        Some(x) => x.enabled,
-        _ => false,
-    };
-
-
-    Ok(Json(json!({// TODO check! FIX!
-        "Email": user.email,
-        "Enabled": enabled,
-        "Object": "twoFactorEmail"
-    })))
-}
-
-#[derive(Deserialize)]
-#[allow(non_snake_case)]
-struct SendEmailData {
-    Email: String, // Email where 2FA codes will be sent to, can be different than user email account.
-    MasterPasswordHash: String,
-}
-
-// Send a verification email to the specified email address to check whether it exists/belongs to user.
-#[post("/two-factor/send-email", data = "<data>")]
-fn send_email(data: JsonUpcase<SendEmailData>, headers: Headers, conn: DbConn) -> EmptyResult {
-    use oath::{totp_raw_now, HashType};
-
-    let data: SendEmailData = data.into_inner().data;
-    let user = headers.user;
-
-    if !user.check_valid_password(&data.MasterPasswordHash) {
-        err!("Invalid password");
-    }
-
-    let type_ = TwoFactorType::Email as i32;
-
-    // TODO: Delete previous email thing.
-    match TwoFactor::find_by_user_and_type(&user.uuid, type_, &conn) {
-        Some(tf) => tf.delete(&conn),
-        _ => Ok(()),
-    };
-
-    let secret = crypto::get_random(vec![0u8; 20]);
-    let base32_secret = BASE32.encode(&secret);
-
-    let 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 = "<data>")]
-fn email(data: JsonUpcase<EmailData>, headers: Headers, conn: DbConn) -> JsonResult {
-
-    let data: EmailData = data.into_inner().data;
-    let user = headers.user;
-
-    if !user.check_valid_password(&data.MasterPasswordHash) {
-        err!("Invalid password");
-    }
-
-    let token_u64 = match data.Token.parse::<u64>() {
-        Ok(token) => token,
-        _ => err!("Could not parse token"),
-    };
-
-
-    let type_ = TwoFactorType::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 <vpl@vpl.me>
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<Route> {
     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<SendEmailLoginData>, 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<SendEmailLoginData>, 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<SendEmailData>, 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<SendEmailData>, 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<EmailData>, 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<Route> {
-    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 = "<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 <vpl@vpl.me>
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<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(())
+}
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<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())
-}
\ 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<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 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<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(()),
-    };
+    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<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)?;
 
-    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<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)
-}
+    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<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)
         }
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<Route> {
     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<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)
-}
\ 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<Route> {
-    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<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 {
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 <vpl@vpl.me>
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<Route> {
     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 <vpl@vpl.me>
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<Route> {
     routes![
         get_email,
@@ -54,10 +52,14 @@ fn send_email_login(data: JsonUpcase<SendEmailLoginData>, 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<SendEmailLoginData>, conn: DbConn) -> Empty
     Ok(())
 }
 
+/// When user clicks on Manage email 2FA show the user the related information
 #[post("/two-factor/get-email", data = "<data>")]
 fn get_email(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> JsonResult {
     let data: PasswordData = data.into_inner().data;
@@ -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<SendEmailData>, 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<EmailData>, 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<String>,
+    /// 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 <vpl@vpl.me>
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<SendEmailLoginData>, 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<String, Error> {
+    if token_size > 19 {
+        err!("Generating token failed")
+    }
+
+    // 8 bytes to create an u64 for up to 19 token digits
+    let bytes = crypto::get_random(vec![0; 8]);
+    let mut bytes_array = [0u8; 8];
+    bytes_array.copy_from_slice(&bytes);
+
+    let number = u64::from_be_bytes(bytes_array) % 10u64.pow(token_size);
+    let token = format!("{:0size$}", number, size = token_size as usize);
+    Ok(token)
 }
 
 /// Send a verification email to the specified email address to check whether it exists/belongs to user.
@@ -131,7 +137,7 @@ fn send_email(data: JsonUpcase<SendEmailData>, 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 <vpl@vpl.me>
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<EmailData>, 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();