浏览代码

Add email code logic and move two_factor into separate modules

vpl 6 年之前
父节点
当前提交
29aedd388e

+ 0 - 1039
src/api/core/two_factor.rs

@@ -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())
-}

+ 101 - 0
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)
+}

+ 348 - 0
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())
+}

+ 272 - 0
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 = "[email protected]";
+
+        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 = "[email protected]";
+
+        let result = obscure_email(&email);
+
+        // If it's smaller than 3 characters it should only show asterisks.
+        assert_eq!(result, "**@example.ext");
+    }
+}

+ 152 - 0
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)
+}

+ 46 - 0
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)
+}

+ 317 - 0
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")
+}

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

+ 21 - 20
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),
+                })
             }
 
             _ => {}

+ 0 - 1208
src/api/two_factor.rs

@@ -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,
-}

+ 9 - 8
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 {