geforkt von mirrored/vaultwarden
Use saved token for email 2fa codes
Dieser Commit ist enthalten in:
Ursprung
efd8d9f528
Commit
6d460b44b0
11 geänderte Dateien mit 148 neuen und 139 gelöschten Zeilen
1
Cargo.lock
generiert
1
Cargo.lock
generiert
|
@ -118,6 +118,7 @@ dependencies = [
|
||||||
"oath 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
"oath 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"percent-encoding 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
"percent-encoding 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"quoted_printable 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
"quoted_printable 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"rand 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"regex 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
"regex 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"reqwest 0.9.19 (registry+https://github.com/rust-lang/crates.io-index)",
|
"reqwest 0.9.19 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"ring 0.14.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
"ring 0.14.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
|
|
@ -108,6 +108,9 @@ regex = "1.2.0"
|
||||||
# URL encoding library
|
# URL encoding library
|
||||||
percent-encoding = "2.0.0"
|
percent-encoding = "2.0.0"
|
||||||
|
|
||||||
|
# Random
|
||||||
|
rand = "0.7.0"
|
||||||
|
|
||||||
[patch.crates-io]
|
[patch.crates-io]
|
||||||
# Add support for Timestamp type
|
# Add support for Timestamp type
|
||||||
rmp = { git = 'https://github.com/dani-garcia/msgpack-rust' }
|
rmp = { git = 'https://github.com/dani-garcia/msgpack-rust' }
|
||||||
|
|
|
@ -1,18 +1,16 @@
|
||||||
use data_encoding::{BASE32};
|
use data_encoding::BASE32;
|
||||||
use rocket::Route;
|
use rocket::Route;
|
||||||
use rocket_contrib::json::Json;
|
use rocket_contrib::json::Json;
|
||||||
|
|
||||||
use crate::api::{JsonResult, JsonUpcase, NumberOrString, PasswordData};
|
use crate::api::core::two_factor::_generate_recover_code;
|
||||||
use crate::api::core::two_factor::{_generate_recover_code, totp};
|
use crate::api::{EmptyResult, JsonResult, JsonUpcase, NumberOrString, PasswordData};
|
||||||
use crate::auth::Headers;
|
use crate::auth::Headers;
|
||||||
use crate::crypto;
|
use crate::crypto;
|
||||||
use crate::db::{
|
use crate::db::{
|
||||||
DbConn,
|
|
||||||
models::{TwoFactor, TwoFactorType},
|
models::{TwoFactor, TwoFactorType},
|
||||||
|
DbConn,
|
||||||
};
|
};
|
||||||
|
|
||||||
const TOTP_TIME_STEP: u64 = 30;
|
|
||||||
|
|
||||||
pub fn routes() -> Vec<Route> {
|
pub fn routes() -> Vec<Route> {
|
||||||
routes![
|
routes![
|
||||||
generate_authenticator,
|
generate_authenticator,
|
||||||
|
@ -20,7 +18,6 @@ pub fn routes() -> Vec<Route> {
|
||||||
activate_authenticator_put,
|
activate_authenticator_put,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/two-factor/get-authenticator", data = "<data>")]
|
#[post("/two-factor/get-authenticator", data = "<data>")]
|
||||||
fn generate_authenticator(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> JsonResult {
|
fn generate_authenticator(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> JsonResult {
|
||||||
let data: PasswordData = data.into_inner().data;
|
let data: PasswordData = data.into_inner().data;
|
||||||
|
@ -80,7 +77,7 @@ fn activate_authenticator(data: JsonUpcase<EnableAuthenticatorData>, headers: He
|
||||||
let twofactor = TwoFactor::new(user.uuid.clone(), type_, key.to_uppercase());
|
let twofactor = TwoFactor::new(user.uuid.clone(), type_, key.to_uppercase());
|
||||||
|
|
||||||
// Validate the token provided with the key
|
// Validate the token provided with the key
|
||||||
totp::validate_totp_code(token, &twofactor.data)?;
|
validate_totp_code(token, &twofactor.data)?;
|
||||||
|
|
||||||
_generate_recover_code(&mut user, &conn);
|
_generate_recover_code(&mut user, &conn);
|
||||||
twofactor.save(&conn)?;
|
twofactor.save(&conn)?;
|
||||||
|
@ -96,3 +93,28 @@ fn activate_authenticator(data: JsonUpcase<EnableAuthenticatorData>, headers: He
|
||||||
fn activate_authenticator_put(data: JsonUpcase<EnableAuthenticatorData>, headers: Headers, conn: DbConn) -> JsonResult {
|
fn activate_authenticator_put(data: JsonUpcase<EnableAuthenticatorData>, headers: Headers, conn: DbConn) -> JsonResult {
|
||||||
activate_authenticator(data, headers, conn)
|
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(())
|
||||||
|
}
|
||||||
|
|
|
@ -1,18 +1,18 @@
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use data_encoding::{BASE64};
|
use data_encoding::BASE64;
|
||||||
use rocket::Route;
|
use rocket::Route;
|
||||||
use rocket_contrib::json::Json;
|
use rocket_contrib::json::Json;
|
||||||
use serde_json;
|
use serde_json;
|
||||||
|
|
||||||
use crate::api::{ApiResult, EmptyResult, JsonResult, JsonUpcase, PasswordData};
|
use crate::api::{ApiResult, EmptyResult, JsonResult, JsonUpcase, PasswordData};
|
||||||
use crate::auth::Headers;
|
use crate::auth::Headers;
|
||||||
use crate::CONFIG;
|
|
||||||
use crate::crypto;
|
use crate::crypto;
|
||||||
use crate::db::{
|
use crate::db::{
|
||||||
DbConn,
|
|
||||||
models::{TwoFactor, TwoFactorType, User},
|
models::{TwoFactor, TwoFactorType, User},
|
||||||
|
DbConn,
|
||||||
};
|
};
|
||||||
use crate::error::{MapResult};
|
use crate::error::MapResult;
|
||||||
|
use crate::CONFIG;
|
||||||
|
|
||||||
pub fn routes() -> Vec<Route> {
|
pub fn routes() -> Vec<Route> {
|
||||||
routes![
|
routes![
|
||||||
|
@ -71,7 +71,7 @@ enum DuoStatus {
|
||||||
// Using the global duo config
|
// Using the global duo config
|
||||||
User(DuoData),
|
User(DuoData),
|
||||||
// Using the user's config
|
// Using the user's config
|
||||||
Disabled(bool), // True if there is a global setting
|
Disabled(bool), // True if there is a global setting
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DuoStatus {
|
impl DuoStatus {
|
||||||
|
|
|
@ -1,23 +1,29 @@
|
||||||
use data_encoding::{BASE32};
|
|
||||||
use oath::{totp_raw_now, HashType};
|
|
||||||
use rocket::Route;
|
use rocket::Route;
|
||||||
use rocket_contrib::json::Json;
|
use rocket_contrib::json::Json;
|
||||||
use serde_json;
|
use serde_json;
|
||||||
|
|
||||||
use crate::api::core::two_factor::totp;
|
|
||||||
use crate::api::{EmptyResult, JsonResult, JsonUpcase, PasswordData};
|
use crate::api::{EmptyResult, JsonResult, JsonUpcase, PasswordData};
|
||||||
use crate::auth::Headers;
|
use crate::auth::Headers;
|
||||||
use crate::db::{
|
use crate::db::{
|
||||||
models::{TwoFactor, TwoFactorType},
|
models::{TwoFactor, TwoFactorType},
|
||||||
DbConn,
|
DbConn,
|
||||||
};
|
};
|
||||||
use crate::error::{Error};
|
use crate::error::Error;
|
||||||
use crate::{crypto, mail};
|
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> {
|
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)]
|
#[derive(Deserialize)]
|
||||||
|
@ -27,7 +33,8 @@ struct SendEmailLoginData {
|
||||||
MasterPasswordHash: String,
|
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
|
#[post("/two-factor/send-email-login", data = "<data>")] // JsonResult
|
||||||
fn send_email_login(data: JsonUpcase<SendEmailLoginData>, conn: DbConn) -> EmptyResult {
|
fn send_email_login(data: JsonUpcase<SendEmailLoginData>, conn: DbConn) -> EmptyResult {
|
||||||
let data: SendEmailLoginData = data.into_inner().data;
|
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 type_ = TwoFactorType::Email as i32;
|
||||||
let twofactor = TwoFactor::find_by_user_and_type(&user.uuid, type_, &conn)?;
|
let mut twofactor = TwoFactor::find_by_user_and_type(&user.uuid, type_, &conn)?;
|
||||||
|
|
||||||
let twofactor_data = EmailTokenData::from_json(&twofactor.data)?;
|
let generated_token = generate_token();
|
||||||
|
let mut twofactor_data = EmailTokenData::from_json(&twofactor.data)?;
|
||||||
|
twofactor_data.set_token(generated_token);
|
||||||
|
twofactor.data = twofactor_data.to_json();
|
||||||
|
twofactor.save(&conn)?;
|
||||||
|
|
||||||
let decoded_key = totp::validate_decode_key(&twofactor_data.totp_secret)?;
|
mail::send_token(&twofactor_data.email, &twofactor_data.last_token?)?;
|
||||||
|
|
||||||
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -75,7 +81,7 @@ fn get_email(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) ->
|
||||||
_ => false,
|
_ => false,
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Json(json!({// TODO check! FIX!
|
Ok(Json(json!({
|
||||||
"Email": user.email,
|
"Email": user.email,
|
||||||
"Enabled": enabled,
|
"Enabled": enabled,
|
||||||
"Object": "twoFactorEmail"
|
"Object": "twoFactorEmail"
|
||||||
|
@ -85,16 +91,26 @@ fn get_email(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) ->
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
struct SendEmailData {
|
struct SendEmailData {
|
||||||
|
/// Email where 2FA codes will be sent to, can be different than user email account.
|
||||||
Email: String,
|
Email: String,
|
||||||
// Email where 2FA codes will be sent to, can be different than user email account.
|
|
||||||
MasterPasswordHash: String,
|
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>")]
|
#[post("/two-factor/send-email", data = "<data>")]
|
||||||
fn send_email(data: JsonUpcase<SendEmailData>, headers: Headers, conn: DbConn) -> EmptyResult {
|
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 data: SendEmailData = data.into_inner().data;
|
||||||
let user = headers.user;
|
let user = headers.user;
|
||||||
|
|
||||||
|
@ -104,16 +120,12 @@ fn send_email(data: JsonUpcase<SendEmailData>, headers: Headers, conn: DbConn) -
|
||||||
|
|
||||||
let type_ = TwoFactorType::Email as i32;
|
let type_ = TwoFactorType::Email as i32;
|
||||||
|
|
||||||
// TODO: Delete previous email thing.
|
if let Some(tf) = TwoFactor::find_by_user_and_type(&user.uuid, type_, &conn) {
|
||||||
match TwoFactor::find_by_user_and_type(&user.uuid, type_, &conn) {
|
tf.delete(&conn)?;
|
||||||
Some(tf) => tf.delete(&conn),
|
}
|
||||||
_ => Ok(()),
|
|
||||||
};
|
|
||||||
|
|
||||||
let secret = crypto::get_random(vec![0u8; 20]);
|
let generated_token = generate_token();
|
||||||
let base32_secret = BASE32.encode(&secret);
|
let twofactor_data = EmailTokenData::new(data.Email, generated_token);
|
||||||
|
|
||||||
let twofactor_data = EmailTokenData::new(data.Email, base32_secret);
|
|
||||||
|
|
||||||
// Uses EmailVerificationChallenge as type to show that it's not verified yet.
|
// Uses EmailVerificationChallenge as type to show that it's not verified yet.
|
||||||
let twofactor = TwoFactor::new(
|
let twofactor = TwoFactor::new(
|
||||||
|
@ -123,10 +135,7 @@ fn send_email(data: JsonUpcase<SendEmailData>, headers: Headers, conn: DbConn) -
|
||||||
);
|
);
|
||||||
twofactor.save(&conn)?;
|
twofactor.save(&conn)?;
|
||||||
|
|
||||||
let generated_token = totp_raw_now(&secret, 6, 0, TOTP_TIME_STEP, &HashType::SHA1);
|
mail::send_token(&twofactor_data.email, &twofactor_data.last_token?)?;
|
||||||
let token_string = generated_token.to_string();
|
|
||||||
|
|
||||||
mail::send_token(&twofactor_data.email, &token_string)?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -139,7 +148,7 @@ struct EmailData {
|
||||||
Token: String,
|
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>")]
|
#[put("/two-factor/email", data = "<data>")]
|
||||||
fn email(data: JsonUpcase<EmailData>, headers: Headers, conn: DbConn) -> JsonResult {
|
fn email(data: JsonUpcase<EmailData>, headers: Headers, conn: DbConn) -> JsonResult {
|
||||||
let data: EmailData = data.into_inner().data;
|
let data: EmailData = data.into_inner().data;
|
||||||
|
@ -149,19 +158,23 @@ fn email(data: JsonUpcase<EmailData>, headers: Headers, conn: DbConn) -> JsonRes
|
||||||
err!("Invalid password");
|
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 type_ = TwoFactorType::EmailVerificationChallenge as i32;
|
||||||
let mut twofactor = TwoFactor::find_by_user_and_type(&user.uuid, type_, &conn)?;
|
let mut twofactor = TwoFactor::find_by_user_and_type(&user.uuid, type_, &conn)?;
|
||||||
|
|
||||||
let 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.atype = TwoFactorType::Email as i32;
|
||||||
|
twofactor.data = email_data.to_json();
|
||||||
twofactor.save(&conn)?;
|
twofactor.save(&conn)?;
|
||||||
|
|
||||||
Ok(Json(json!({
|
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 {
|
/// Validate the email code when used as TwoFactor token mechanism
|
||||||
let totp_code: u64 = match code.parse() {
|
pub fn validate_email_code_str(user_uuid: &str, token: &str, data: &str, conn: &DbConn) -> EmptyResult {
|
||||||
Ok(code) => code,
|
let mut email_data = EmailTokenData::from_json(&data)?;
|
||||||
_ => err!("Email code is not a number"),
|
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 {
|
email_data.reset_token();
|
||||||
let email_data = EmailTokenData::from_json(&data)?;
|
twofactor.data = email_data.to_json();
|
||||||
|
twofactor.save(&conn)?;
|
||||||
|
|
||||||
let decoded_secret = match BASE32.decode(email_data.totp_secret.as_bytes()) {
|
let date = NaiveDateTime::from_timestamp(email_data.token_sent, 0);
|
||||||
Ok(s) => s,
|
if date.add(Duration::seconds(MAX_TIME_DIFFERENCE)) < Utc::now().naive_utc() {
|
||||||
Err(_) => err!("Invalid email secret"),
|
err!("Email token too old")
|
||||||
};
|
|
||||||
|
|
||||||
let generated = totp_raw_now(&decoded_secret, 6, 0, TOTP_TIME_STEP, &HashType::SHA1);
|
|
||||||
if generated != code {
|
|
||||||
err!("Invalid email code");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -199,17 +212,28 @@ pub fn validate_email_code(code: u64, data: &str) -> EmptyResult {
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct EmailTokenData {
|
pub struct EmailTokenData {
|
||||||
pub email: String,
|
pub email: String,
|
||||||
pub totp_secret: String,
|
pub last_token: Option<String>,
|
||||||
|
pub token_sent: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EmailTokenData {
|
impl EmailTokenData {
|
||||||
pub fn new(email: String, totp_secret: String) -> EmailTokenData {
|
pub fn new(email: String, token: String) -> EmailTokenData {
|
||||||
EmailTokenData {
|
EmailTokenData {
|
||||||
email,
|
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 {
|
pub fn to_json(&self) -> String {
|
||||||
serde_json::to_string(&self).unwrap()
|
serde_json::to_string(&self).unwrap()
|
||||||
}
|
}
|
||||||
|
@ -235,7 +259,7 @@ pub fn obscure_email(email: &str) -> String {
|
||||||
let new_name = match name_size {
|
let new_name = match name_size {
|
||||||
1..=3 => "*".repeat(name_size),
|
1..=3 => "*".repeat(name_size),
|
||||||
_ => {
|
_ => {
|
||||||
let stars = "*".repeat(name_size-2);
|
let stars = "*".repeat(name_size - 2);
|
||||||
name.truncate(2);
|
name.truncate(2);
|
||||||
format!("{}{}", name, stars)
|
format!("{}{}", name, stars)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use data_encoding::{BASE32};
|
use data_encoding::BASE32;
|
||||||
use rocket::Route;
|
use rocket::Route;
|
||||||
use rocket_contrib::json::Json;
|
use rocket_contrib::json::Json;
|
||||||
use serde_json;
|
use serde_json;
|
||||||
|
@ -8,8 +8,8 @@ use crate::api::{JsonResult, JsonUpcase, NumberOrString, PasswordData};
|
||||||
use crate::auth::Headers;
|
use crate::auth::Headers;
|
||||||
use crate::crypto;
|
use crate::crypto;
|
||||||
use crate::db::{
|
use crate::db::{
|
||||||
DbConn,
|
|
||||||
models::{TwoFactor, User},
|
models::{TwoFactor, User},
|
||||||
|
DbConn,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub(crate) mod authenticator;
|
pub(crate) mod authenticator;
|
||||||
|
@ -17,7 +17,6 @@ pub(crate) mod duo;
|
||||||
pub(crate) mod email;
|
pub(crate) mod email;
|
||||||
pub(crate) mod u2f;
|
pub(crate) mod u2f;
|
||||||
pub(crate) mod yubikey;
|
pub(crate) mod yubikey;
|
||||||
pub(crate) mod totp;
|
|
||||||
|
|
||||||
pub fn routes() -> Vec<Route> {
|
pub fn routes() -> Vec<Route> {
|
||||||
let mut routes = routes![
|
let mut routes = routes![
|
||||||
|
|
|
@ -1,46 +0,0 @@
|
||||||
use data_encoding::BASE32;
|
|
||||||
|
|
||||||
use crate::api::EmptyResult;
|
|
||||||
|
|
||||||
pub fn validate_totp_code_str(totp_code: &str, secret: &str) -> EmptyResult {
|
|
||||||
let totp_code: u64 = match totp_code.parse() {
|
|
||||||
Ok(code) => code,
|
|
||||||
_ => err!("TOTP code is not a number"),
|
|
||||||
};
|
|
||||||
|
|
||||||
validate_totp_code(totp_code, secret)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn validate_totp_code(totp_code: u64, secret: &str) -> EmptyResult {
|
|
||||||
validate_totp_code_with_time_step(totp_code, &secret, 30)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn validate_totp_code_with_time_step(totp_code: u64, secret: &str, time_step: u64) -> EmptyResult {
|
|
||||||
use oath::{totp_raw_now, HashType};
|
|
||||||
|
|
||||||
let decoded_secret = match BASE32.decode(secret.as_bytes()) {
|
|
||||||
Ok(s) => s,
|
|
||||||
Err(_) => err!("Invalid TOTP secret"),
|
|
||||||
};
|
|
||||||
|
|
||||||
let generated = totp_raw_now(&decoded_secret, 6, 0, time_step, &HashType::SHA1);
|
|
||||||
if generated != totp_code {
|
|
||||||
err!("Invalid TOTP code");
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn validate_decode_key(key: &str) -> Result<Vec<u8>, crate::error::Error> {
|
|
||||||
// Validate key as base32 and 20 bytes length
|
|
||||||
let decoded_key: Vec<u8> = match BASE32.decode(key.as_bytes()) {
|
|
||||||
Ok(decoded) => decoded,
|
|
||||||
_ => err!("Invalid totp secret"),
|
|
||||||
};
|
|
||||||
|
|
||||||
if decoded_key.len() != 20 {
|
|
||||||
err!("Invalid key length")
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(decoded_key)
|
|
||||||
}
|
|
|
@ -6,15 +6,15 @@ use u2f::messages::{RegisterResponse, SignResponse, U2fSignRequest};
|
||||||
use u2f::protocol::{Challenge, U2f};
|
use u2f::protocol::{Challenge, U2f};
|
||||||
use u2f::register::Registration;
|
use u2f::register::Registration;
|
||||||
|
|
||||||
use crate::api::{ApiResult, EmptyResult, JsonResult, JsonUpcase, NumberOrString, PasswordData};
|
|
||||||
use crate::api::core::two_factor::_generate_recover_code;
|
use crate::api::core::two_factor::_generate_recover_code;
|
||||||
|
use crate::api::{ApiResult, EmptyResult, JsonResult, JsonUpcase, NumberOrString, PasswordData};
|
||||||
use crate::auth::Headers;
|
use crate::auth::Headers;
|
||||||
use crate::CONFIG;
|
|
||||||
use crate::db::{
|
use crate::db::{
|
||||||
DbConn,
|
|
||||||
models::{TwoFactor, TwoFactorType},
|
models::{TwoFactor, TwoFactorType},
|
||||||
|
DbConn,
|
||||||
};
|
};
|
||||||
use crate::error::{Error};
|
use crate::error::Error;
|
||||||
|
use crate::CONFIG;
|
||||||
|
|
||||||
const U2F_VERSION: &str = "U2F_V2";
|
const U2F_VERSION: &str = "U2F_V2";
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,11 @@ use crate::error::{Error, MapResult};
|
||||||
use crate::CONFIG;
|
use crate::CONFIG;
|
||||||
|
|
||||||
pub fn routes() -> Vec<Route> {
|
pub fn routes() -> Vec<Route> {
|
||||||
routes![generate_yubikey, activate_yubikey, activate_yubikey_put,]
|
routes![
|
||||||
|
generate_yubikey,
|
||||||
|
activate_yubikey,
|
||||||
|
activate_yubikey_put,
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
|
|
|
@ -4,15 +4,15 @@ use rocket::Route;
|
||||||
use rocket_contrib::json::Json;
|
use rocket_contrib::json::Json;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
use crate::api::{ApiResult, EmptyResult, JsonResult};
|
|
||||||
use crate::api::core::two_factor::{duo, email, yubikey};
|
|
||||||
use crate::api::core::two_factor::email::EmailTokenData;
|
use crate::api::core::two_factor::email::EmailTokenData;
|
||||||
|
use crate::api::core::two_factor::{duo, email, yubikey};
|
||||||
|
use crate::api::{ApiResult, EmptyResult, JsonResult};
|
||||||
use crate::auth::ClientIp;
|
use crate::auth::ClientIp;
|
||||||
use crate::CONFIG;
|
|
||||||
use crate::db::DbConn;
|
|
||||||
use crate::db::models::*;
|
use crate::db::models::*;
|
||||||
|
use crate::db::DbConn;
|
||||||
use crate::mail;
|
use crate::mail;
|
||||||
use crate::util;
|
use crate::util;
|
||||||
|
use crate::CONFIG;
|
||||||
|
|
||||||
pub fn routes() -> Vec<Route> {
|
pub fn routes() -> Vec<Route> {
|
||||||
routes![login]
|
routes![login]
|
||||||
|
@ -179,7 +179,10 @@ fn twofactor_auth(
|
||||||
None => err_json!(_json_err_twofactor(&twofactor_ids, user_uuid, conn)?),
|
None => err_json!(_json_err_twofactor(&twofactor_ids, user_uuid, conn)?),
|
||||||
};
|
};
|
||||||
|
|
||||||
let selected_twofactor = twofactors.into_iter().filter(|tf| tf.atype == selected_id && tf.enabled).nth(0);
|
let selected_twofactor = twofactors
|
||||||
|
.into_iter()
|
||||||
|
.filter(|tf| tf.atype == selected_id && tf.enabled)
|
||||||
|
.nth(0);
|
||||||
|
|
||||||
use crate::api::core::two_factor as _tf;
|
use crate::api::core::two_factor as _tf;
|
||||||
use crate::crypto::ct_eq;
|
use crate::crypto::ct_eq;
|
||||||
|
@ -188,11 +191,11 @@ fn twofactor_auth(
|
||||||
let mut remember = data.two_factor_remember.unwrap_or(0);
|
let mut remember = data.two_factor_remember.unwrap_or(0);
|
||||||
|
|
||||||
match TwoFactorType::from_i32(selected_id) {
|
match TwoFactorType::from_i32(selected_id) {
|
||||||
Some(TwoFactorType::Authenticator) => _tf::totp::validate_totp_code_str(twofactor_code, &selected_data?)?,
|
Some(TwoFactorType::Authenticator) => _tf::authenticator::validate_totp_code_str(twofactor_code, &selected_data?)?,
|
||||||
Some(TwoFactorType::U2f) => _tf::u2f::validate_u2f_login(user_uuid, twofactor_code, conn)?,
|
Some(TwoFactorType::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::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::Duo) => _tf::duo::validate_duo_login(data.username.as_ref().unwrap(), twofactor_code, conn)?,
|
||||||
Some(TwoFactorType::Email) => _tf::email::validate_email_code_str(twofactor_code, &selected_data?)?,
|
Some(TwoFactorType::Email) => _tf::email::validate_email_code_str(user_uuid, twofactor_code, &selected_data?, conn)?,
|
||||||
|
|
||||||
Some(TwoFactorType::Remember) => {
|
Some(TwoFactorType::Remember) => {
|
||||||
match device.twofactor_remember {
|
match device.twofactor_remember {
|
||||||
|
|
|
@ -3,8 +3,8 @@ use diesel::prelude::*;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
use crate::api::EmptyResult;
|
use crate::api::EmptyResult;
|
||||||
use crate::db::DbConn;
|
|
||||||
use crate::db::schema::twofactor;
|
use crate::db::schema::twofactor;
|
||||||
|
use crate::db::DbConn;
|
||||||
use crate::error::MapResult;
|
use crate::error::MapResult;
|
||||||
|
|
||||||
use super::User;
|
use super::User;
|
||||||
|
@ -36,7 +36,6 @@ pub enum TwoFactorType {
|
||||||
U2fRegisterChallenge = 1000,
|
U2fRegisterChallenge = 1000,
|
||||||
U2fLoginChallenge = 1001,
|
U2fLoginChallenge = 1001,
|
||||||
EmailVerificationChallenge = 1002,
|
EmailVerificationChallenge = 1002,
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Local methods
|
/// Local methods
|
||||||
|
|
Laden …
In neuem Issue referenzieren