2019-08-03 18:47:52 +02:00
|
|
|
use rocket::Route;
|
|
|
|
use rocket_contrib::json::Json;
|
|
|
|
use serde_json;
|
|
|
|
|
2019-11-02 18:31:50 +01:00
|
|
|
use crate::api::core::two_factor::_generate_recover_code;
|
2019-08-03 20:09:20 +02:00
|
|
|
use crate::api::{EmptyResult, JsonResult, JsonUpcase, PasswordData};
|
2019-08-03 18:47:52 +02:00
|
|
|
use crate::auth::Headers;
|
2019-08-10 22:33:39 +02:00
|
|
|
use crate::crypto;
|
2019-08-03 18:47:52 +02:00
|
|
|
use crate::db::{
|
2019-08-03 20:09:20 +02:00
|
|
|
models::{TwoFactor, TwoFactorType},
|
2019-08-03 18:47:52 +02:00
|
|
|
DbConn,
|
|
|
|
};
|
2019-08-04 16:55:43 +02:00
|
|
|
use crate::error::Error;
|
|
|
|
use crate::mail;
|
2019-08-10 22:33:39 +02:00
|
|
|
use crate::CONFIG;
|
2019-08-06 22:37:23 +02:00
|
|
|
|
2019-08-04 16:55:43 +02:00
|
|
|
use chrono::{Duration, NaiveDateTime, Utc};
|
|
|
|
use std::ops::Add;
|
2019-08-03 18:47:52 +02:00
|
|
|
|
|
|
|
pub fn routes() -> Vec<Route> {
|
2019-12-27 18:37:14 +01:00
|
|
|
routes![get_email, send_email_login, send_email, email,]
|
2019-08-03 18:47:52 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Deserialize)]
|
|
|
|
#[allow(non_snake_case)]
|
|
|
|
struct SendEmailLoginData {
|
|
|
|
Email: String,
|
|
|
|
MasterPasswordHash: String,
|
|
|
|
}
|
|
|
|
|
2019-08-04 16:55:43 +02:00
|
|
|
/// User is trying to login and wants to use email 2FA.
|
|
|
|
/// Does not require Bearer token
|
2019-08-03 18:47:52 +02:00
|
|
|
#[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
|
2019-08-03 20:09:20 +02:00
|
|
|
let user = match User::find_by_mail(&data.Email, &conn) {
|
2019-08-03 18:47:52 +02:00
|
|
|
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.")
|
|
|
|
}
|
|
|
|
|
2019-08-10 22:33:39 +02:00
|
|
|
if !CONFIG._enable_email_2fa() {
|
|
|
|
err!("Email 2FA is disabled")
|
|
|
|
}
|
|
|
|
|
2019-10-15 21:19:27 +02:00
|
|
|
send_token(&user.uuid, &conn)?;
|
2019-10-15 21:19:49 +02:00
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Generate the token, save the data for later verification and send email to user
|
2019-10-15 21:19:27 +02:00
|
|
|
pub fn send_token(user_uuid: &str, conn: &DbConn) -> EmptyResult {
|
|
|
|
let type_ = TwoFactorType::Email as i32;
|
|
|
|
let mut twofactor = TwoFactor::find_by_user_and_type(user_uuid, type_, &conn)?;
|
|
|
|
|
2019-11-25 06:28:49 +01:00
|
|
|
let generated_token = crypto::generate_token(CONFIG.email_token_size())?;
|
2019-10-15 21:19:27 +02:00
|
|
|
|
2019-08-04 16:55:43 +02:00
|
|
|
let mut twofactor_data = EmailTokenData::from_json(&twofactor.data)?;
|
|
|
|
twofactor_data.set_token(generated_token);
|
|
|
|
twofactor.data = twofactor_data.to_json();
|
|
|
|
twofactor.save(&conn)?;
|
2019-08-03 18:47:52 +02:00
|
|
|
|
2019-08-04 16:55:43 +02:00
|
|
|
mail::send_token(&twofactor_data.email, &twofactor_data.last_token?)?;
|
2019-08-03 18:47:52 +02:00
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2019-08-10 22:33:39 +02:00
|
|
|
/// When user clicks on Manage email 2FA show the user the related information
|
2019-08-03 18:47:52 +02:00
|
|
|
#[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,
|
|
|
|
};
|
|
|
|
|
2019-08-04 16:55:43 +02:00
|
|
|
Ok(Json(json!({
|
2019-08-03 18:47:52 +02:00
|
|
|
"Email": user.email,
|
|
|
|
"Enabled": enabled,
|
|
|
|
"Object": "twoFactorEmail"
|
|
|
|
})))
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Deserialize)]
|
|
|
|
#[allow(non_snake_case)]
|
|
|
|
struct SendEmailData {
|
2019-08-04 16:55:43 +02:00
|
|
|
/// Email where 2FA codes will be sent to, can be different than user email account.
|
2019-08-03 18:47:52 +02:00
|
|
|
Email: String,
|
|
|
|
MasterPasswordHash: String,
|
|
|
|
}
|
|
|
|
|
2019-08-04 16:55:43 +02:00
|
|
|
/// Send a verification email to the specified email address to check whether it exists/belongs to user.
|
2019-08-03 18:47:52 +02:00
|
|
|
#[post("/two-factor/send-email", data = "<data>")]
|
|
|
|
fn send_email(data: JsonUpcase<SendEmailData>, headers: Headers, conn: DbConn) -> EmptyResult {
|
|
|
|
let data: SendEmailData = data.into_inner().data;
|
|
|
|
let user = headers.user;
|
|
|
|
|
|
|
|
if !user.check_valid_password(&data.MasterPasswordHash) {
|
|
|
|
err!("Invalid password");
|
|
|
|
}
|
|
|
|
|
2019-08-10 22:33:39 +02:00
|
|
|
if !CONFIG._enable_email_2fa() {
|
|
|
|
err!("Email 2FA is disabled")
|
|
|
|
}
|
|
|
|
|
2019-08-03 18:47:52 +02:00
|
|
|
let type_ = TwoFactorType::Email as i32;
|
|
|
|
|
2019-08-04 16:55:43 +02:00
|
|
|
if let Some(tf) = TwoFactor::find_by_user_and_type(&user.uuid, type_, &conn) {
|
|
|
|
tf.delete(&conn)?;
|
|
|
|
}
|
2019-08-03 18:47:52 +02:00
|
|
|
|
2019-11-25 06:28:49 +01:00
|
|
|
let generated_token = crypto::generate_token(CONFIG.email_token_size())?;
|
2019-08-04 16:55:43 +02:00
|
|
|
let twofactor_data = EmailTokenData::new(data.Email, generated_token);
|
2019-08-03 18:47:52 +02:00
|
|
|
|
|
|
|
// Uses EmailVerificationChallenge as type to show that it's not verified yet.
|
2019-08-03 20:09:20 +02:00
|
|
|
let twofactor = TwoFactor::new(
|
2019-08-03 18:47:52 +02:00
|
|
|
user.uuid,
|
|
|
|
TwoFactorType::EmailVerificationChallenge,
|
|
|
|
twofactor_data.to_json(),
|
|
|
|
);
|
|
|
|
twofactor.save(&conn)?;
|
|
|
|
|
2019-08-04 16:55:43 +02:00
|
|
|
mail::send_token(&twofactor_data.email, &twofactor_data.last_token?)?;
|
2019-08-03 18:47:52 +02:00
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Deserialize, Serialize)]
|
|
|
|
#[allow(non_snake_case)]
|
|
|
|
struct EmailData {
|
|
|
|
Email: String,
|
|
|
|
MasterPasswordHash: String,
|
|
|
|
Token: String,
|
|
|
|
}
|
|
|
|
|
2019-08-04 16:55:43 +02:00
|
|
|
/// Verify email belongs to user and can be used for 2FA email codes.
|
2019-08-03 18:47:52 +02:00
|
|
|
#[put("/two-factor/email", data = "<data>")]
|
|
|
|
fn email(data: JsonUpcase<EmailData>, headers: Headers, conn: DbConn) -> JsonResult {
|
|
|
|
let data: EmailData = data.into_inner().data;
|
2019-11-02 18:31:50 +01:00
|
|
|
let mut user = headers.user;
|
2019-08-03 18:47:52 +02:00
|
|
|
|
|
|
|
if !user.check_valid_password(&data.MasterPasswordHash) {
|
|
|
|
err!("Invalid password");
|
|
|
|
}
|
|
|
|
|
|
|
|
let type_ = TwoFactorType::EmailVerificationChallenge as i32;
|
|
|
|
let mut twofactor = TwoFactor::find_by_user_and_type(&user.uuid, type_, &conn)?;
|
|
|
|
|
2019-08-04 16:55:43 +02:00
|
|
|
let mut email_data = EmailTokenData::from_json(&twofactor.data)?;
|
|
|
|
|
|
|
|
let issued_token = match &email_data.last_token {
|
|
|
|
Some(t) => t,
|
|
|
|
_ => err!("No token available"),
|
|
|
|
};
|
2019-08-03 18:47:52 +02:00
|
|
|
|
2019-08-26 20:22:04 +02:00
|
|
|
if !crypto::ct_eq(issued_token, data.Token) {
|
2019-08-10 22:33:39 +02:00
|
|
|
err!("Token is invalid")
|
2019-08-04 16:55:43 +02:00
|
|
|
}
|
2019-08-03 18:47:52 +02:00
|
|
|
|
2019-08-04 16:55:43 +02:00
|
|
|
email_data.reset_token();
|
2019-08-03 18:47:52 +02:00
|
|
|
twofactor.atype = TwoFactorType::Email as i32;
|
2019-08-04 16:55:43 +02:00
|
|
|
twofactor.data = email_data.to_json();
|
2019-08-03 18:47:52 +02:00
|
|
|
twofactor.save(&conn)?;
|
|
|
|
|
2019-11-02 18:31:50 +01:00
|
|
|
_generate_recover_code(&mut user, &conn);
|
|
|
|
|
2019-08-03 18:47:52 +02:00
|
|
|
Ok(Json(json!({
|
2019-08-03 20:09:20 +02:00
|
|
|
"Email": email_data.email,
|
2019-08-03 18:47:52 +02:00
|
|
|
"Enabled": "true",
|
|
|
|
"Object": "twoFactorEmail"
|
|
|
|
})))
|
|
|
|
}
|
|
|
|
|
2019-08-04 16:55:43 +02:00
|
|
|
/// 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"),
|
2019-08-03 18:47:52 +02:00
|
|
|
};
|
|
|
|
|
2019-08-26 20:22:04 +02:00
|
|
|
if !crypto::ct_eq(issued_token, token) {
|
2019-08-10 22:33:39 +02:00
|
|
|
email_data.add_attempt();
|
|
|
|
if email_data.attempts >= CONFIG.email_attempts_limit() {
|
|
|
|
email_data.reset_token();
|
|
|
|
}
|
|
|
|
twofactor.data = email_data.to_json();
|
|
|
|
twofactor.save(&conn)?;
|
|
|
|
|
|
|
|
err!("Token is invalid")
|
2019-08-04 16:55:43 +02:00
|
|
|
}
|
2019-08-03 18:47:52 +02:00
|
|
|
|
2019-08-04 16:55:43 +02:00
|
|
|
email_data.reset_token();
|
|
|
|
twofactor.data = email_data.to_json();
|
|
|
|
twofactor.save(&conn)?;
|
2019-08-03 18:47:52 +02:00
|
|
|
|
2019-08-04 16:55:43 +02:00
|
|
|
let date = NaiveDateTime::from_timestamp(email_data.token_sent, 0);
|
2019-08-10 22:33:39 +02:00
|
|
|
let max_time = CONFIG.email_expiration_time() as i64;
|
|
|
|
if date.add(Duration::seconds(max_time)) < Utc::now().naive_utc() {
|
|
|
|
err!("Token has expired")
|
2019-08-03 18:47:52 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
2019-08-10 22:33:39 +02:00
|
|
|
/// Data stored in the TwoFactor table in the db
|
2019-08-03 18:47:52 +02:00
|
|
|
#[derive(Serialize, Deserialize)]
|
|
|
|
pub struct EmailTokenData {
|
2019-08-10 22:33:39 +02:00
|
|
|
/// Email address where the token will be sent to. Can be different from account email.
|
2019-08-03 20:09:20 +02:00
|
|
|
pub email: String,
|
2019-08-10 22:33:39 +02:00
|
|
|
/// Some(token): last valid token issued that has not been entered.
|
|
|
|
/// None: valid token was used and removed.
|
2019-08-04 16:55:43 +02:00
|
|
|
pub last_token: Option<String>,
|
2019-08-10 22:33:39 +02:00
|
|
|
/// UNIX timestamp of token issue.
|
2019-08-04 16:55:43 +02:00
|
|
|
pub token_sent: i64,
|
2019-08-10 22:33:39 +02:00
|
|
|
/// Amount of token entry attempts for last_token.
|
|
|
|
pub attempts: u64,
|
2019-08-03 18:47:52 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
impl EmailTokenData {
|
2019-08-04 16:55:43 +02:00
|
|
|
pub fn new(email: String, token: String) -> EmailTokenData {
|
2019-08-03 18:47:52 +02:00
|
|
|
EmailTokenData {
|
2019-08-03 20:09:20 +02:00
|
|
|
email,
|
2019-08-04 16:55:43 +02:00
|
|
|
last_token: Some(token),
|
|
|
|
token_sent: Utc::now().naive_utc().timestamp(),
|
2019-08-10 22:33:39 +02:00
|
|
|
attempts: 0,
|
2019-08-03 18:47:52 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-08-04 16:55:43 +02:00
|
|
|
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;
|
2019-08-10 22:33:39 +02:00
|
|
|
self.attempts = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn add_attempt(&mut self) {
|
2019-09-05 21:56:12 +02:00
|
|
|
self.attempts += 1;
|
2019-08-04 16:55:43 +02:00
|
|
|
}
|
|
|
|
|
2019-08-03 18:47:52 +02:00
|
|
|
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"),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-08-03 20:09:20 +02:00
|
|
|
/// Takes an email address and obscures it by replacing it with asterisks except two characters.
|
2019-08-03 18:47:52 +02:00
|
|
|
pub fn obscure_email(email: &str) -> String {
|
2019-09-05 21:56:12 +02:00
|
|
|
let split: Vec<&str> = email.split('@').collect();
|
2019-08-03 18:47:52 +02:00
|
|
|
|
|
|
|
let mut name = split[0].to_string();
|
|
|
|
let domain = &split[1];
|
|
|
|
|
|
|
|
let name_size = name.chars().count();
|
|
|
|
|
|
|
|
let new_name = match name_size {
|
2019-08-03 20:09:20 +02:00
|
|
|
1..=3 => "*".repeat(name_size),
|
2019-08-03 18:47:52 +02:00
|
|
|
_ => {
|
2019-08-04 16:55:43 +02:00
|
|
|
let stars = "*".repeat(name_size - 2);
|
2019-08-03 18:47:52 +02:00
|
|
|
name.truncate(2);
|
|
|
|
format!("{}{}", name, stars)
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
format!("{}@{}", new_name, &domain)
|
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_obscure_email_long() {
|
|
|
|
let email = "bytes@example.ext";
|
|
|
|
|
|
|
|
let result = obscure_email(&email);
|
|
|
|
|
|
|
|
// Only first two characters should be visible.
|
|
|
|
assert_eq!(result, "by***@example.ext");
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_obscure_email_short() {
|
2019-08-03 20:09:20 +02:00
|
|
|
let email = "byt@example.ext";
|
2019-08-03 18:47:52 +02:00
|
|
|
|
|
|
|
let result = obscure_email(&email);
|
|
|
|
|
|
|
|
// If it's smaller than 3 characters it should only show asterisks.
|
2019-08-03 20:09:20 +02:00
|
|
|
assert_eq!(result, "***@example.ext");
|
2019-08-03 18:47:52 +02:00
|
|
|
}
|
2019-08-06 22:37:23 +02:00
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_token() {
|
2019-11-30 23:30:35 +01:00
|
|
|
let result = crypto::generate_token(19).unwrap();
|
2019-08-26 20:26:54 +02:00
|
|
|
|
|
|
|
assert_eq!(result.chars().count(), 19);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_token_too_large() {
|
2019-11-30 23:30:35 +01:00
|
|
|
let result = crypto::generate_token(20);
|
2019-08-06 22:37:23 +02:00
|
|
|
|
2019-08-26 20:26:54 +02:00
|
|
|
assert!(result.is_err(), "too large token should give an error");
|
2019-08-06 22:37:23 +02:00
|
|
|
}
|
2019-08-03 18:47:52 +02:00
|
|
|
}
|