|
|
@ -1,23 +1,29 @@ |
|
|
|
use data_encoding::{BASE32}; |
|
|
|
use oath::{totp_raw_now, HashType}; |
|
|
|
use rocket::Route; |
|
|
|
use rocket_contrib::json::Json; |
|
|
|
use serde_json; |
|
|
|
|
|
|
|
use crate::api::core::two_factor::totp; |
|
|
|
use crate::api::{EmptyResult, JsonResult, JsonUpcase, PasswordData}; |
|
|
|
use crate::auth::Headers; |
|
|
|
use crate::db::{ |
|
|
|
models::{TwoFactor, TwoFactorType}, |
|
|
|
DbConn, |
|
|
|
}; |
|
|
|
use crate::error::{Error}; |
|
|
|
use crate::{crypto, mail}; |
|
|
|
use crate::error::Error; |
|
|
|
use crate::mail; |
|
|
|
use chrono::{Duration, NaiveDateTime, Utc}; |
|
|
|
use rand::Rng; |
|
|
|
use std::char; |
|
|
|
use std::ops::Add; |
|
|
|
|
|
|
|
const TOTP_TIME_STEP: u64 = 120; |
|
|
|
const MAX_TIME_DIFFERENCE: i64 = 600; |
|
|
|
|
|
|
|
pub fn routes() -> Vec<Route> { |
|
|
|
routes![get_email, send_email_login, send_email, email,] |
|
|
|
routes![ |
|
|
|
get_email, |
|
|
|
send_email_login, |
|
|
|
send_email, |
|
|
|
email, |
|
|
|
] |
|
|
|
} |
|
|
|
|
|
|
|
#[derive(Deserialize)] |
|
|
@ -27,7 +33,8 @@ struct SendEmailLoginData { |
|
|
|
MasterPasswordHash: String, |
|
|
|
} |
|
|
|
|
|
|
|
// Does not require Bearer token
|
|
|
|
/// User is trying to login and wants to use email 2FA.
|
|
|
|
/// Does not require Bearer token
|
|
|
|
#[post("/two-factor/send-email-login", data = "<data>")] // JsonResult
|
|
|
|
fn send_email_login(data: JsonUpcase<SendEmailLoginData>, conn: DbConn) -> EmptyResult { |
|
|
|
let data: SendEmailLoginData = data.into_inner().data; |
|
|
@ -46,16 +53,15 @@ fn send_email_login(data: JsonUpcase<SendEmailLoginData>, conn: DbConn) -> Empty |
|
|
|
} |
|
|
|
|
|
|
|
let type_ = TwoFactorType::Email as i32; |
|
|
|
let twofactor = TwoFactor::find_by_user_and_type(&user.uuid, type_, &conn)?; |
|
|
|
|
|
|
|
let twofactor_data = EmailTokenData::from_json(&twofactor.data)?; |
|
|
|
|
|
|
|
let decoded_key = totp::validate_decode_key(&twofactor_data.totp_secret)?; |
|
|
|
let mut twofactor = TwoFactor::find_by_user_and_type(&user.uuid, type_, &conn)?; |
|
|
|
|
|
|
|
let generated_token = totp_raw_now(&decoded_key, 6, 0, TOTP_TIME_STEP, &HashType::SHA1); |
|
|
|
let token_string = generated_token.to_string(); |
|
|
|
let generated_token = generate_token(); |
|
|
|
let mut twofactor_data = EmailTokenData::from_json(&twofactor.data)?; |
|
|
|
twofactor_data.set_token(generated_token); |
|
|
|
twofactor.data = twofactor_data.to_json(); |
|
|
|
twofactor.save(&conn)?; |
|
|
|
|
|
|
|
mail::send_token(&twofactor_data.email, &token_string)?; |
|
|
|
mail::send_token(&twofactor_data.email, &twofactor_data.last_token?)?; |
|
|
|
|
|
|
|
Ok(()) |
|
|
|
} |
|
|
@ -75,7 +81,7 @@ fn get_email(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> |
|
|
|
_ => false, |
|
|
|
}; |
|
|
|
|
|
|
|
Ok(Json(json!({// TODO check! FIX!
|
|
|
|
Ok(Json(json!({ |
|
|
|
"Email": user.email, |
|
|
|
"Enabled": enabled, |
|
|
|
"Object": "twoFactorEmail" |
|
|
@ -85,16 +91,26 @@ fn get_email(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> |
|
|
|
#[derive(Deserialize)] |
|
|
|
#[allow(non_snake_case)] |
|
|
|
struct SendEmailData { |
|
|
|
/// Email where 2FA codes will be sent to, can be different than user email account.
|
|
|
|
Email: String, |
|
|
|
// Email where 2FA codes will be sent to, can be different than user email account.
|
|
|
|
MasterPasswordHash: String, |
|
|
|
} |
|
|
|
|
|
|
|
// Send a verification email to the specified email address to check whether it exists/belongs to user.
|
|
|
|
fn generate_token() -> String { |
|
|
|
const TOKEN_LEN: usize = 6; |
|
|
|
let mut rng = rand::thread_rng(); |
|
|
|
|
|
|
|
(0..TOKEN_LEN) |
|
|
|
.map(|_| { |
|
|
|
let num = rng.gen_range(0, 9); |
|
|
|
char::from_digit(num, 10).unwrap() |
|
|
|
}) |
|
|
|
.collect() |
|
|
|
} |
|
|
|
|
|
|
|
/// Send a verification email to the specified email address to check whether it exists/belongs to user.
|
|
|
|
#[post("/two-factor/send-email", data = "<data>")] |
|
|
|
fn send_email(data: JsonUpcase<SendEmailData>, headers: Headers, conn: DbConn) -> EmptyResult { |
|
|
|
use oath::{totp_raw_now, HashType}; |
|
|
|
|
|
|
|
let data: SendEmailData = data.into_inner().data; |
|
|
|
let user = headers.user; |
|
|
|
|
|
|
@ -104,16 +120,12 @@ fn send_email(data: JsonUpcase<SendEmailData>, headers: Headers, conn: DbConn) - |
|
|
|
|
|
|
|
let type_ = TwoFactorType::Email as i32; |
|
|
|
|
|
|
|
// TODO: Delete previous email thing.
|
|
|
|
match TwoFactor::find_by_user_and_type(&user.uuid, type_, &conn) { |
|
|
|
Some(tf) => tf.delete(&conn), |
|
|
|
_ => Ok(()), |
|
|
|
}; |
|
|
|
|
|
|
|
let secret = crypto::get_random(vec![0u8; 20]); |
|
|
|
let base32_secret = BASE32.encode(&secret); |
|
|
|
if let Some(tf) = TwoFactor::find_by_user_and_type(&user.uuid, type_, &conn) { |
|
|
|
tf.delete(&conn)?; |
|
|
|
} |
|
|
|
|
|
|
|
let twofactor_data = EmailTokenData::new(data.Email, base32_secret); |
|
|
|
let generated_token = generate_token(); |
|
|
|
let twofactor_data = EmailTokenData::new(data.Email, generated_token); |
|
|
|
|
|
|
|
// Uses EmailVerificationChallenge as type to show that it's not verified yet.
|
|
|
|
let twofactor = TwoFactor::new( |
|
|
@ -123,10 +135,7 @@ fn send_email(data: JsonUpcase<SendEmailData>, headers: Headers, conn: DbConn) - |
|
|
|
); |
|
|
|
twofactor.save(&conn)?; |
|
|
|
|
|
|
|
let generated_token = totp_raw_now(&secret, 6, 0, TOTP_TIME_STEP, &HashType::SHA1); |
|
|
|
let token_string = generated_token.to_string(); |
|
|
|
|
|
|
|
mail::send_token(&twofactor_data.email, &token_string)?; |
|
|
|
mail::send_token(&twofactor_data.email, &twofactor_data.last_token?)?; |
|
|
|
|
|
|
|
Ok(()) |
|
|
|
} |
|
|
@ -139,7 +148,7 @@ struct EmailData { |
|
|
|
Token: String, |
|
|
|
} |
|
|
|
|
|
|
|
// Verify email used for 2FA email codes.
|
|
|
|
/// Verify email belongs to user and can be used for 2FA email codes.
|
|
|
|
#[put("/two-factor/email", data = "<data>")] |
|
|
|
fn email(data: JsonUpcase<EmailData>, headers: Headers, conn: DbConn) -> JsonResult { |
|
|
|
let data: EmailData = data.into_inner().data; |
|
|
@ -149,19 +158,23 @@ fn email(data: JsonUpcase<EmailData>, headers: Headers, conn: DbConn) -> JsonRes |
|
|
|
err!("Invalid password"); |
|
|
|
} |
|
|
|
|
|
|
|
let token_u64 = match data.Token.parse::<u64>() { |
|
|
|
Ok(token) => token, |
|
|
|
_ => err!("Could not parse token"), |
|
|
|
}; |
|
|
|
|
|
|
|
let type_ = TwoFactorType::EmailVerificationChallenge as i32; |
|
|
|
let mut twofactor = TwoFactor::find_by_user_and_type(&user.uuid, type_, &conn)?; |
|
|
|
|
|
|
|
let email_data = EmailTokenData::from_json(&twofactor.data)?; |
|
|
|
let mut email_data = EmailTokenData::from_json(&twofactor.data)?; |
|
|
|
|
|
|
|
totp::validate_totp_code_with_time_step(token_u64, &email_data.totp_secret, TOTP_TIME_STEP)?; |
|
|
|
let issued_token = match &email_data.last_token { |
|
|
|
Some(t) => t, |
|
|
|
_ => err!("No token available"), |
|
|
|
}; |
|
|
|
|
|
|
|
if issued_token != &data.Token { |
|
|
|
err!("Email token does not match") |
|
|
|
} |
|
|
|
|
|
|
|
email_data.reset_token(); |
|
|
|
twofactor.atype = TwoFactorType::Email as i32; |
|
|
|
twofactor.data = email_data.to_json(); |
|
|
|
twofactor.save(&conn)?; |
|
|
|
|
|
|
|
Ok(Json(json!({ |
|
|
@ -171,26 +184,26 @@ fn email(data: JsonUpcase<EmailData>, headers: Headers, conn: DbConn) -> JsonRes |
|
|
|
}))) |
|
|
|
} |
|
|
|
|
|
|
|
pub fn validate_email_code_str(code: &str, data: &str) -> EmptyResult { |
|
|
|
let totp_code: u64 = match code.parse() { |
|
|
|
Ok(code) => code, |
|
|
|
_ => err!("Email code is not a number"), |
|
|
|
/// Validate the email code when used as TwoFactor token mechanism
|
|
|
|
pub fn validate_email_code_str(user_uuid: &str, token: &str, data: &str, conn: &DbConn) -> EmptyResult { |
|
|
|
let mut email_data = EmailTokenData::from_json(&data)?; |
|
|
|
let mut twofactor = TwoFactor::find_by_user_and_type(&user_uuid, TwoFactorType::Email as i32, &conn)?; |
|
|
|
let issued_token = match &email_data.last_token { |
|
|
|
Some(t) => t, |
|
|
|
_ => err!("No token available"), |
|
|
|
}; |
|
|
|
|
|
|
|
validate_email_code(totp_code, data) |
|
|
|
if issued_token != &*token { |
|
|
|
err!("Email token does not match") |
|
|
|
} |
|
|
|
|
|
|
|
pub fn validate_email_code(code: u64, data: &str) -> EmptyResult { |
|
|
|
let email_data = EmailTokenData::from_json(&data)?; |
|
|
|
|
|
|
|
let decoded_secret = match BASE32.decode(email_data.totp_secret.as_bytes()) { |
|
|
|
Ok(s) => s, |
|
|
|
Err(_) => err!("Invalid email secret"), |
|
|
|
}; |
|
|
|
email_data.reset_token(); |
|
|
|
twofactor.data = email_data.to_json(); |
|
|
|
twofactor.save(&conn)?; |
|
|
|
|
|
|
|
let generated = totp_raw_now(&decoded_secret, 6, 0, TOTP_TIME_STEP, &HashType::SHA1); |
|
|
|
if generated != code { |
|
|
|
err!("Invalid email code"); |
|
|
|
let date = NaiveDateTime::from_timestamp(email_data.token_sent, 0); |
|
|
|
if date.add(Duration::seconds(MAX_TIME_DIFFERENCE)) < Utc::now().naive_utc() { |
|
|
|
err!("Email token too old") |
|
|
|
} |
|
|
|
|
|
|
|
Ok(()) |
|
|
@ -199,15 +212,26 @@ pub fn validate_email_code(code: u64, data: &str) -> EmptyResult { |
|
|
|
#[derive(Serialize, Deserialize)] |
|
|
|
pub struct EmailTokenData { |
|
|
|
pub email: String, |
|
|
|
pub totp_secret: String, |
|
|
|
pub last_token: Option<String>, |
|
|
|
pub token_sent: i64, |
|
|
|
} |
|
|
|
|
|
|
|
impl EmailTokenData { |
|
|
|
pub fn new(email: String, totp_secret: String) -> EmailTokenData { |
|
|
|
pub fn new(email: String, token: String) -> EmailTokenData { |
|
|
|
EmailTokenData { |
|
|
|
email, |
|
|
|
totp_secret, |
|
|
|
last_token: Some(token), |
|
|
|
token_sent: Utc::now().naive_utc().timestamp(), |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
pub fn set_token(&mut self, token: String) { |
|
|
|
self.last_token = Some(token); |
|
|
|
self.token_sent = Utc::now().naive_utc().timestamp(); |
|
|
|
} |
|
|
|
|
|
|
|
pub fn reset_token(&mut self) { |
|
|
|
self.last_token = None; |
|
|
|
} |
|
|
|
|
|
|
|
pub fn to_json(&self) -> String { |
|
|
|