From f7ffb81d9ec0c5deb159bae9064b7f5fb4231731 Mon Sep 17 00:00:00 2001 From: Jean-Christophe BEGUE Date: Mon, 13 Aug 2018 13:46:32 +0200 Subject: [PATCH 1/9] SMTP configuration parsing and checking --- src/main.rs | 55 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/src/main.rs b/src/main.rs index 4a42452d..e506e05e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -153,6 +153,56 @@ lazy_static! { static ref CONFIG: Config = Config::load(); } +#[derive(Debug)] +pub struct MailConfig { + reply_to_email: Option, + smtp_host: String, + smtp_port: u16, + smtp_ssl: bool, + smtp_username: String, + smtp_password: String, +} + +impl MailConfig { + fn load() -> Option { + let smtp_host = util::parse_option_string(env::var("SMTP_HOST").ok()); + + // When SMTP_HOST is absent, we assume the user does not want to enable it. + if smtp_host.is_none() { + return None + } + + let smtp_ssl = util::parse_option_string(env::var("SMTP_SSL").ok()).unwrap_or(false); + let smtp_port = util::parse_option_string(env::var("SMTP_PORT").ok()) + .unwrap_or_else(|| { + if smtp_ssl { + 465u16 + } else { + 25u16 + } + }); + + Some(MailConfig { + reply_to_email: util::parse_option_string(env::var("REPLY_TO_EMAIL").ok()), + smtp_host: smtp_host.unwrap(), + smtp_port: smtp_port, + smtp_ssl: smtp_ssl, + // If username or password is not specified, and SMTP support seems to be wanted, + // don't let the app start: the configuration is clearly incomplete. + smtp_username: util::parse_option_string(env::var("SMTP_USERNAME").ok()) + .unwrap_or_else(|| { + println!("Please specify SMTP_USERNAME to enable SMTP support."); + exit(1); + }), + smtp_password: util::parse_option_string(env::var("SMTP_PASSWORD").ok()) + .unwrap_or_else(|| { + println!("Please specify SMTP_PASSWORD to enable SMTP support."); + exit(1); + }), + }) + } +} + #[derive(Debug)] pub struct Config { database_url: String, @@ -170,8 +220,11 @@ pub struct Config { signups_allowed: bool, password_iterations: i32, show_password_hint: bool, + domain: String, domain_set: bool, + + mail: Option, } impl Config { @@ -202,6 +255,8 @@ impl Config { domain_set: domain.is_ok(), domain: domain.unwrap_or("http://localhost".into()), + + mail: MailConfig::load(), } } } From 812387e5860daefba2e0f57237cc65ae330e6196 Mon Sep 17 00:00:00 2001 From: Jean-Christophe BEGUE Date: Wed, 15 Aug 2018 08:32:19 +0200 Subject: [PATCH 2/9] SMTP integration, send password hint by email. --- .env | 7 ++++++ Cargo.toml | 4 ++++ src/api/core/accounts.rs | 25 ++++++++++++++------- src/mail.rs | 47 ++++++++++++++++++++++++++++++++++++++++ src/main.rs | 15 ++++++++----- 5 files changed, 85 insertions(+), 13 deletions(-) create mode 100644 src/mail.rs diff --git a/.env b/.env index 735724ee..475df852 100644 --- a/.env +++ b/.env @@ -41,3 +41,10 @@ # ROCKET_ADDRESS=0.0.0.0 # Enable this to test mobile app # ROCKET_PORT=8000 # ROCKET_TLS={certs="/path/to/certs.pem",key="/path/to/key.pem"} + +## Mail specific settings, if SMTP_HOST is specified, SMTP_USERNAME and SMTP_PASSWORD are mandatory +# SMTP_HOST=smtp.domain.tld +# SMTP_PORT=587 +# SMTP_SSL=true +# SMTP_USERNAME=username +# SMTP_PASSWORD=password \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index fb5adafb..537b4e1e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,6 +58,10 @@ lazy_static = "1.0.1" num-traits = "0.2.5" num-derive = "0.2.2" +lettre = "0.8.2" +lettre_email = "0.8.2" +native-tls = "0.1.5" + [patch.crates-io] # Make jwt use ring 0.11, to match rocket jsonwebtoken = { path = "libs/jsonwebtoken" } diff --git a/src/api/core/accounts.rs b/src/api/core/accounts.rs index e45c08dc..bc018f1e 100644 --- a/src/api/core/accounts.rs +++ b/src/api/core/accounts.rs @@ -5,6 +5,7 @@ use db::models::*; use api::{PasswordData, JsonResult, EmptyResult, JsonUpcase, NumberOrString}; use auth::Headers; +use mail; use CONFIG; @@ -258,15 +259,23 @@ struct PasswordHintData { fn password_hint(data: JsonUpcase, conn: DbConn) -> EmptyResult { let data: PasswordHintData = data.into_inner().data; - if !CONFIG.show_password_hint { - return Ok(()) + let user = User::find_by_mail(&data.Email, &conn); + if user.is_none() { + return Ok(()); } - match User::find_by_mail(&data.Email, &conn) { - Some(user) => { - let hint = user.password_hint.to_owned().unwrap_or_default(); - err!(format!("Your password hint is: {}", hint)) - }, - None => Ok(()), + let user = user.unwrap(); + let hint = user.password_hint.to_owned().unwrap_or("You don't have any...".to_string()); + + if let Some(ref mail_config) = CONFIG.mail { + if let Err(e) = mail::send_password_hint(&user.email, &hint, mail_config) { + err!(format!("There have been a problem sending the email: {}", e)); + } } + + if !CONFIG.show_password_hint { + err!(format!("Your password hint is: {}", &hint)); + } + + Ok(()) } diff --git a/src/mail.rs b/src/mail.rs new file mode 100644 index 00000000..7faa791f --- /dev/null +++ b/src/mail.rs @@ -0,0 +1,47 @@ +use std::error::Error; +use native_tls::TlsConnector; +use native_tls::{Protocol}; +use lettre::{EmailTransport, SmtpTransport, ClientTlsParameters, ClientSecurity}; +use lettre::smtp::{ConnectionReuseParameters, SmtpTransportBuilder}; +use lettre::smtp::authentication::{Credentials, Mechanism}; +use lettre_email::EmailBuilder; + +use MailConfig; + +fn mailer(config: &MailConfig) -> SmtpTransport { + let client_security = if config.smtp_ssl { + let mut tls_builder = TlsConnector::builder().unwrap(); + tls_builder.supported_protocols(&[ + Protocol::Tlsv10, Protocol::Tlsv11, Protocol::Tlsv12 + ]).unwrap(); + + ClientSecurity::Required( + ClientTlsParameters::new(config.smtp_host.to_owned(), tls_builder.build().unwrap()) + ) + } else { + ClientSecurity::None + }; + + SmtpTransportBuilder::new((config.smtp_host.to_owned().as_str(), config.smtp_port), client_security) + .unwrap() + .credentials(Credentials::new(config.smtp_username.to_owned(), config.smtp_password.to_owned())) + .authentication_mechanism(Mechanism::Login) + .smtp_utf8(true) + .connection_reuse(ConnectionReuseParameters::ReuseUnlimited) + .build() +} + +pub fn send_password_hint(address: &str, hint: &str, config: &MailConfig) -> Result<(), String> { + let email = EmailBuilder::new() + .to(address) + .from((config.smtp_from.to_owned(), "Bitwarden-rs")) + .subject("Your Master Password Hint") + .body(hint) + .build().unwrap(); + + match mailer(config).send(&email) { + Ok(_) => Ok(()), + Err(e) => Err(e.description().to_string()), + } +} + diff --git a/src/main.rs b/src/main.rs index e506e05e..8f39057f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -26,6 +26,9 @@ extern crate lazy_static; #[macro_use] extern crate num_derive; extern crate num_traits; +extern crate lettre; +extern crate lettre_email; +extern crate native_tls; use std::{env, path::Path, process::{exit, Command}}; use rocket::Rocket; @@ -37,6 +40,7 @@ mod api; mod db; mod crypto; mod auth; +mod mail; fn init_rocket() -> Rocket { rocket::ignite() @@ -155,10 +159,10 @@ lazy_static! { #[derive(Debug)] pub struct MailConfig { - reply_to_email: Option, smtp_host: String, smtp_port: u16, smtp_ssl: bool, + smtp_from: String, smtp_username: String, smtp_password: String, } @@ -172,22 +176,23 @@ impl MailConfig { return None } - let smtp_ssl = util::parse_option_string(env::var("SMTP_SSL").ok()).unwrap_or(false); + let smtp_ssl = util::parse_option_string(env::var("SMTP_SSL").ok()).unwrap_or(true); let smtp_port = util::parse_option_string(env::var("SMTP_PORT").ok()) .unwrap_or_else(|| { if smtp_ssl { - 465u16 + 587u16 } else { 25u16 } }); Some(MailConfig { - reply_to_email: util::parse_option_string(env::var("REPLY_TO_EMAIL").ok()), smtp_host: smtp_host.unwrap(), smtp_port: smtp_port, smtp_ssl: smtp_ssl, - // If username or password is not specified, and SMTP support seems to be wanted, + smtp_from: util::parse_option_string(env::var("SMTP_FROM").ok()) + .unwrap_or("bitwarden@localhost".to_string()), + // If username or password is not specified and SMTP support seems to be wanted, // don't let the app start: the configuration is clearly incomplete. smtp_username: util::parse_option_string(env::var("SMTP_USERNAME").ok()) .unwrap_or_else(|| { From 19e0605d30384e04b5bba4a3aa34d40619f61add Mon Sep 17 00:00:00 2001 From: Jean-Christophe BEGUE Date: Wed, 15 Aug 2018 10:17:05 +0200 Subject: [PATCH 3/9] Better message into the password hint email --- src/mail.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/mail.rs b/src/mail.rs index 7faa791f..0af83ce7 100644 --- a/src/mail.rs +++ b/src/mail.rs @@ -32,11 +32,17 @@ fn mailer(config: &MailConfig) -> SmtpTransport { } pub fn send_password_hint(address: &str, hint: &str, config: &MailConfig) -> Result<(), String> { + let body = format!( + "You (or someone) recently requested your master password hint.\n\n\ + Your hint is: \"{}\"\n\n\ + If you did not request your master password hint you can safely ignore this email.\n", + hint); + let email = EmailBuilder::new() .to(address) .from((config.smtp_from.to_owned(), "Bitwarden-rs")) .subject("Your Master Password Hint") - .body(hint) + .body(body) .build().unwrap(); match mailer(config).send(&email) { From d68f57cbba3e1d1c929fbb72b135ffbd88c3732a Mon Sep 17 00:00:00 2001 From: Jean-Christophe BEGUE Date: Wed, 15 Aug 2018 14:08:00 +0200 Subject: [PATCH 4/9] Fix password hint showing logic --- src/api/core/accounts.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/api/core/accounts.rs b/src/api/core/accounts.rs index bc018f1e..1d6469c2 100644 --- a/src/api/core/accounts.rs +++ b/src/api/core/accounts.rs @@ -265,15 +265,16 @@ fn password_hint(data: JsonUpcase, conn: DbConn) -> EmptyResul } let user = user.unwrap(); - let hint = user.password_hint.to_owned().unwrap_or("You don't have any...".to_string()); + let hint = match user.password_hint { + Some(hint) => hint, + None => return Ok(()), + }; if let Some(ref mail_config) = CONFIG.mail { if let Err(e) = mail::send_password_hint(&user.email, &hint, mail_config) { err!(format!("There have been a problem sending the email: {}", e)); } - } - - if !CONFIG.show_password_hint { + } else if CONFIG.show_password_hint { err!(format!("Your password hint is: {}", &hint)); } From 401aa7c699d87a7c6777893e0dc010a722bfba24 Mon Sep 17 00:00:00 2001 From: Jean-Christophe BEGUE Date: Wed, 15 Aug 2018 17:00:55 +0200 Subject: [PATCH 5/9] make SMTP authentication optionnal, let lettre pick the better auth mechanism --- src/mail.rs | 26 ++++++++++++++++---------- src/main.rs | 32 ++++++++++++++++---------------- 2 files changed, 32 insertions(+), 26 deletions(-) diff --git a/src/mail.rs b/src/mail.rs index 0af83ce7..fef03c25 100644 --- a/src/mail.rs +++ b/src/mail.rs @@ -3,7 +3,7 @@ use native_tls::TlsConnector; use native_tls::{Protocol}; use lettre::{EmailTransport, SmtpTransport, ClientTlsParameters, ClientSecurity}; use lettre::smtp::{ConnectionReuseParameters, SmtpTransportBuilder}; -use lettre::smtp::authentication::{Credentials, Mechanism}; +use lettre::smtp::authentication::Credentials; use lettre_email::EmailBuilder; use MailConfig; @@ -11,10 +11,7 @@ use MailConfig; fn mailer(config: &MailConfig) -> SmtpTransport { let client_security = if config.smtp_ssl { let mut tls_builder = TlsConnector::builder().unwrap(); - tls_builder.supported_protocols(&[ - Protocol::Tlsv10, Protocol::Tlsv11, Protocol::Tlsv12 - ]).unwrap(); - + tls_builder.supported_protocols(&[Protocol::Tlsv11, Protocol::Tlsv12]).unwrap(); ClientSecurity::Required( ClientTlsParameters::new(config.smtp_host.to_owned(), tls_builder.build().unwrap()) ) @@ -22,12 +19,21 @@ fn mailer(config: &MailConfig) -> SmtpTransport { ClientSecurity::None }; - SmtpTransportBuilder::new((config.smtp_host.to_owned().as_str(), config.smtp_port), client_security) - .unwrap() - .credentials(Credentials::new(config.smtp_username.to_owned(), config.smtp_password.to_owned())) - .authentication_mechanism(Mechanism::Login) + let smtp_transport = SmtpTransportBuilder::new( + (config.smtp_host.to_owned().as_str(), config.smtp_port), + client_security + ).unwrap(); + + let smtp_transport = match (&config.smtp_username, &config.smtp_password) { + (Some(username), Some(password)) => { + smtp_transport.credentials(Credentials::new(username.to_owned(), password.to_owned())) + }, + (_, _) => smtp_transport, + }; + + smtp_transport .smtp_utf8(true) - .connection_reuse(ConnectionReuseParameters::ReuseUnlimited) + .connection_reuse(ConnectionReuseParameters::NoReuse) .build() } diff --git a/src/main.rs b/src/main.rs index 8f39057f..9c0675e7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -163,13 +163,13 @@ pub struct MailConfig { smtp_port: u16, smtp_ssl: bool, smtp_from: String, - smtp_username: String, - smtp_password: String, + smtp_username: Option, + smtp_password: Option, } impl MailConfig { fn load() -> Option { - let smtp_host = util::parse_option_string(env::var("SMTP_HOST").ok()); + let smtp_host = env::var("SMTP_HOST").ok(); // When SMTP_HOST is absent, we assume the user does not want to enable it. if smtp_host.is_none() { @@ -186,24 +186,24 @@ impl MailConfig { } }); + let smtp_username = env::var("SMTP_USERNAME").ok(); + let smtp_password = env::var("SMTP_PASSWORD").ok().or_else(|| { + if smtp_username.as_ref().is_some() { + println!("Please specify SMTP_PASSWORD to enable SMTP support."); + exit(1); + } else { + None + } + }); + Some(MailConfig { smtp_host: smtp_host.unwrap(), smtp_port: smtp_port, smtp_ssl: smtp_ssl, smtp_from: util::parse_option_string(env::var("SMTP_FROM").ok()) - .unwrap_or("bitwarden@localhost".to_string()), - // If username or password is not specified and SMTP support seems to be wanted, - // don't let the app start: the configuration is clearly incomplete. - smtp_username: util::parse_option_string(env::var("SMTP_USERNAME").ok()) - .unwrap_or_else(|| { - println!("Please specify SMTP_USERNAME to enable SMTP support."); - exit(1); - }), - smtp_password: util::parse_option_string(env::var("SMTP_PASSWORD").ok()) - .unwrap_or_else(|| { - println!("Please specify SMTP_PASSWORD to enable SMTP support."); - exit(1); - }), + .unwrap_or("bitwarden-rs@localhost".to_string()), + smtp_username: smtp_username, + smtp_password: smtp_password, }) } } From 9e63985b284e4529825b8ac9a41a27eb42153396 Mon Sep 17 00:00:00 2001 From: Jean-Christophe BEGUE Date: Wed, 15 Aug 2018 17:25:59 +0200 Subject: [PATCH 6/9] Check email validity before using it for password hint sending --- Cargo.toml | 1 + src/api/core/accounts.rs | 5 +++++ src/mail.rs | 3 +-- src/main.rs | 1 + 4 files changed, 8 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 537b4e1e..3a514738 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,6 +61,7 @@ num-derive = "0.2.2" lettre = "0.8.2" lettre_email = "0.8.2" native-tls = "0.1.5" +fast_chemail = "0.9.5" [patch.crates-io] # Make jwt use ring 0.11, to match rocket diff --git a/src/api/core/accounts.rs b/src/api/core/accounts.rs index 1d6469c2..ffc76f3a 100644 --- a/src/api/core/accounts.rs +++ b/src/api/core/accounts.rs @@ -5,6 +5,7 @@ use db::models::*; use api::{PasswordData, JsonResult, EmptyResult, JsonUpcase, NumberOrString}; use auth::Headers; +use fast_chemail::is_valid_email; use mail; use CONFIG; @@ -259,6 +260,10 @@ struct PasswordHintData { fn password_hint(data: JsonUpcase, conn: DbConn) -> EmptyResult { let data: PasswordHintData = data.into_inner().data; + if !is_valid_email(&data.Email) { + return Ok(()); + } + let user = User::find_by_mail(&data.Email, &conn); if user.is_none() { return Ok(()); diff --git a/src/mail.rs b/src/mail.rs index fef03c25..09409e94 100644 --- a/src/mail.rs +++ b/src/mail.rs @@ -1,6 +1,5 @@ use std::error::Error; -use native_tls::TlsConnector; -use native_tls::{Protocol}; +use native_tls::{Protocol, TlsConnector}; use lettre::{EmailTransport, SmtpTransport, ClientTlsParameters, ClientSecurity}; use lettre::smtp::{ConnectionReuseParameters, SmtpTransportBuilder}; use lettre::smtp::authentication::Credentials; diff --git a/src/main.rs b/src/main.rs index 9c0675e7..5fbefb0b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -29,6 +29,7 @@ extern crate num_traits; extern crate lettre; extern crate lettre_email; extern crate native_tls; +extern crate fast_chemail; use std::{env, path::Path, process::{exit, Command}}; use rocket::Rocket; From e2ab2f7306a7adf31daf9f560f840be9174e583f Mon Sep 17 00:00:00 2001 From: Jean-Christophe BEGUE Date: Tue, 11 Sep 2018 13:00:59 +0200 Subject: [PATCH 7/9] Save None instead of empty password hint --- src/api/core/accounts.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/api/core/accounts.rs b/src/api/core/accounts.rs index ffc76f3a..cfec3e73 100644 --- a/src/api/core/accounts.rs +++ b/src/api/core/accounts.rs @@ -82,7 +82,10 @@ fn post_profile(data: JsonUpcase, headers: Headers, conn: DbConn) - let mut user = headers.user; user.name = data.Name; - user.password_hint = data.MasterPasswordHint; + user.password_hint = match data.MasterPasswordHint { + Some(ref h) if h.is_empty() => None, + _ => data.MasterPasswordHint, + }; user.save(&conn); Ok(Json(user.to_json(&conn))) From 1c641d7635d12c9fcb1efce6262c6f33606b61aa Mon Sep 17 00:00:00 2001 From: Jean-Christophe BEGUE Date: Tue, 11 Sep 2018 13:04:34 +0200 Subject: [PATCH 8/9] Special messages when user has no password hint --- src/api/core/accounts.rs | 13 ++++++------- src/mail.rs | 21 +++++++++++++-------- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/src/api/core/accounts.rs b/src/api/core/accounts.rs index cfec3e73..11b7700b 100644 --- a/src/api/core/accounts.rs +++ b/src/api/core/accounts.rs @@ -273,17 +273,16 @@ fn password_hint(data: JsonUpcase, conn: DbConn) -> EmptyResul } let user = user.unwrap(); - let hint = match user.password_hint { - Some(hint) => hint, - None => return Ok(()), - }; - if let Some(ref mail_config) = CONFIG.mail { - if let Err(e) = mail::send_password_hint(&user.email, &hint, mail_config) { + if let Err(e) = mail::send_password_hint(&user.email, user.password_hint, mail_config) { err!(format!("There have been a problem sending the email: {}", e)); } } else if CONFIG.show_password_hint { - err!(format!("Your password hint is: {}", &hint)); + if let Some(hint) = user.password_hint { + err!(format!("Your password hint is: {}", &hint)); + } else { + err!(format!("Sorry, you have no password hint...")); + } } Ok(()) diff --git a/src/mail.rs b/src/mail.rs index 09409e94..ccf83cca 100644 --- a/src/mail.rs +++ b/src/mail.rs @@ -36,17 +36,23 @@ fn mailer(config: &MailConfig) -> SmtpTransport { .build() } -pub fn send_password_hint(address: &str, hint: &str, config: &MailConfig) -> Result<(), String> { - let body = format!( - "You (or someone) recently requested your master password hint.\n\n\ - Your hint is: \"{}\"\n\n\ - If you did not request your master password hint you can safely ignore this email.\n", - hint); +pub fn send_password_hint(address: &str, hint: Option, config: &MailConfig) -> Result<(), String> { + let (subject, body) = if let Some(hint) = hint { + ("Your master password hint", + format!( + "You (or someone) recently requested your master password hint.\n\n\ + Your hint is: \"{}\"\n\n\ + If you did not request your master password hint you can safely ignore this email.\n", + hint)) + } else { + ("Sorry, you have no password hint...", + "Sorry, you have not specified any password hint...\n".to_string()) + }; let email = EmailBuilder::new() .to(address) .from((config.smtp_from.to_owned(), "Bitwarden-rs")) - .subject("Your Master Password Hint") + .subject(subject) .body(body) .build().unwrap(); @@ -55,4 +61,3 @@ pub fn send_password_hint(address: &str, hint: &str, config: &MailConfig) -> Res Err(e) => Err(e.description().to_string()), } } - From 37d88be2be484ab044d0954600049909876348ef Mon Sep 17 00:00:00 2001 From: Jean-Christophe BEGUE Date: Tue, 11 Sep 2018 13:12:24 +0200 Subject: [PATCH 9/9] return an error when email adress for password hint is not valid --- src/api/core/accounts.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/core/accounts.rs b/src/api/core/accounts.rs index 11b7700b..2f66b200 100644 --- a/src/api/core/accounts.rs +++ b/src/api/core/accounts.rs @@ -264,7 +264,7 @@ fn password_hint(data: JsonUpcase, conn: DbConn) -> EmptyResul let data: PasswordHintData = data.into_inner().data; if !is_valid_email(&data.Email) { - return Ok(()); + return err!("This email address is not valid..."); } let user = User::find_by_mail(&data.Email, &conn);