diff --git a/Cargo.lock b/Cargo.lock index 8a889d8..002d4ae 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1803,6 +1803,7 @@ dependencies = [ "orion", "rand 0.8.4", "sea-query", + "secstr", "serde", "serde_json", "sha2", @@ -2757,6 +2758,16 @@ dependencies = [ "syn", ] +[[package]] +name = "secstr" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cce2c726741c320e5b8f1edd9a21b3c2c292ae94514afd001d41d81ba143dafc" +dependencies = [ + "libc", + "serde", +] + [[package]] name = "security-framework" version = "2.4.2" diff --git a/server/Cargo.toml b/server/Cargo.toml index 94452d2..b9bf089 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -83,5 +83,9 @@ features = ["with-chrono"] features = ["env", "toml"] version = "*" +[dependencies.secstr] +features = ["serde"] +version = "*" + [dev-dependencies] mockall = "0.9.1" diff --git a/server/src/domain/sql_backend_handler.rs b/server/src/domain/sql_backend_handler.rs index 23a76c8..c961073 100644 --- a/server/src/domain/sql_backend_handler.rs +++ b/server/src/domain/sql_backend_handler.rs @@ -437,7 +437,7 @@ mod tests { let sql_pool = get_in_memory_db().await; let config = ConfigurationBuilder::default() .ldap_user_dn("admin".to_string()) - .ldap_user_pass("test".to_string()) + .ldap_user_pass(secstr::SecUtf8::from("test")) .build() .unwrap(); let handler = SqlBackendHandler::new(config, sql_pool); diff --git a/server/src/domain/sql_opaque_handler.rs b/server/src/domain/sql_opaque_handler.rs index 93da601..39c07e4 100644 --- a/server/src/domain/sql_opaque_handler.rs +++ b/server/src/domain/sql_opaque_handler.rs @@ -9,6 +9,7 @@ use async_trait::async_trait; use lldap_auth::opaque; use log::*; use sea_query::{Expr, Iden, Query}; +use secstr::SecUtf8; use sqlx::Row; type SqlOpaqueHandler = SqlBackendHandler; @@ -83,7 +84,7 @@ impl SqlBackendHandler { impl LoginHandler for SqlBackendHandler { async fn bind(&self, request: BindRequest) -> Result<()> { if request.name == self.config.ldap_user_dn { - if request.password == self.config.ldap_user_pass { + if SecUtf8::from(request.password) == self.config.ldap_user_pass { return Ok(()); } else { debug!(r#"Invalid password for LDAP bind user"#); @@ -220,11 +221,12 @@ impl OpaqueHandler for SqlOpaqueHandler { pub(crate) async fn register_password( opaque_handler: &SqlOpaqueHandler, username: &str, - password: &str, + password: &SecUtf8, ) -> Result<()> { let mut rng = rand::rngs::OsRng; use registration::*; - let registration_start = opaque::client::registration::start_registration(password, &mut rng)?; + let registration_start = + opaque::client::registration::start_registration(password.unsecure(), &mut rng)?; let start_response = opaque_handler .registration_start(ClientRegistrationStartRequest { username: username.to_string(), @@ -321,7 +323,7 @@ mod tests { attempt_login(&opaque_handler, "bob", "bob00") .await .unwrap_err(); - register_password(&opaque_handler, "bob", "bob00").await?; + register_password(&opaque_handler, "bob", &secstr::SecUtf8::from("bob00")).await?; attempt_login(&opaque_handler, "bob", "wrong_password") .await .unwrap_err(); diff --git a/server/src/infra/configuration.rs b/server/src/infra/configuration.rs index cb5454b..3cee6c8 100644 --- a/server/src/infra/configuration.rs +++ b/server/src/infra/configuration.rs @@ -6,6 +6,7 @@ use figment::{ }; use lettre::message::Mailbox; use lldap_auth::opaque::{server::ServerSetup, KeyPair}; +use secstr::SecUtf8; use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, Deserialize, Serialize, derive_builder::Builder)] @@ -23,8 +24,8 @@ pub struct MailOptions { pub port: u16, #[builder(default = r#""admin".to_string()"#)] pub user: String, - #[builder(default = r#""".to_string()"#)] - pub password: String, + #[builder(default = r#"SecUtf8::from("")"#)] + pub password: SecUtf8, #[builder(default = "true")] pub tls_required: bool, } @@ -47,14 +48,14 @@ pub struct Configuration { pub ldaps_port: u16, #[builder(default = "17170")] pub http_port: u16, - #[builder(default = r#"String::from("secretjwtsecret")"#)] - pub jwt_secret: String, + #[builder(default = r#"SecUtf8::from("secretjwtsecret")"#)] + pub jwt_secret: SecUtf8, #[builder(default = r#"String::from("dc=example,dc=com")"#)] pub ldap_base_dn: String, #[builder(default = r#"String::from("admin")"#)] pub ldap_user_dn: String, - #[builder(default = r#"String::from("password")"#)] - pub ldap_user_pass: String, + #[builder(default = r#"SecUtf8::from("password")"#)] + pub ldap_user_pass: SecUtf8, #[builder(default = r#"String::from("sqlite://users.db?mode=rwc")"#)] pub database_url: String, #[builder(default = "false")] @@ -188,7 +189,7 @@ impl ConfigOverrider for SmtpOpts { config.smtp_options.user = user.clone(); } if let Some(password) = &self.smtp_password { - config.smtp_options.password = password.clone(); + config.smtp_options.password = SecUtf8::from(password.clone()); } if let Some(tls_required) = self.smtp_tls_required { config.smtp_options.tls_required = tls_required; @@ -219,10 +220,10 @@ where println!("Configuration: {:#?}", &config); } config.server_setup = Some(get_server_setup(&config.key_file)?); - if config.jwt_secret == "secretjwtsecret" { + if config.jwt_secret == SecUtf8::from("secretjwtsecret") { println!("WARNING: Default JWT secret used! This is highly unsafe and can allow attackers to log in as admin."); } - if config.ldap_user_pass == "password" { + if config.ldap_user_pass == SecUtf8::from("password") { println!("WARNING: Unsecure default admin password is used."); } Ok(config) diff --git a/server/src/infra/mail.rs b/server/src/infra/mail.rs index 969cd29..7ecb343 100644 --- a/server/src/infra/mail.rs +++ b/server/src/infra/mail.rs @@ -22,7 +22,10 @@ pub fn send_test_email(to: Mailbox, options: &MailOptions) -> Result<()> { .to(to) .subject("LLDAP test email") .body("The test is successful! You can send emails from LLDAP".to_string())?; - let creds = Credentials::new(options.user.clone(), options.password.clone()); + let creds = Credentials::new( + options.user.clone(), + options.password.unsecure().to_string(), + ); let mailer = SmtpTransport::relay(&options.server)? .credentials(creds) .build(); diff --git a/server/src/infra/tcp_server.rs b/server/src/infra/tcp_server.rs index ce9ab2c..1f45816 100644 --- a/server/src/infra/tcp_server.rs +++ b/server/src/infra/tcp_server.rs @@ -44,14 +44,14 @@ pub(crate) fn error_to_http_response(error: DomainError) -> HttpResponse { fn http_config( cfg: &mut web::ServiceConfig, backend_handler: Backend, - jwt_secret: String, + jwt_secret: secstr::SecUtf8, jwt_blacklist: HashSet, ) where Backend: TcpBackendHandler + BackendHandler + LoginHandler + OpaqueHandler + Sync + 'static, { cfg.app_data(web::Data::new(AppState:: { backend_handler, - jwt_key: Hmac::new_varkey(jwt_secret.as_bytes()).unwrap(), + jwt_key: Hmac::new_varkey(jwt_secret.unsecure().as_bytes()).unwrap(), jwt_blacklist: RwLock::new(jwt_blacklist), })) // Serve index.html and main.js, and default to index.html. diff --git a/server/src/main.rs b/server/src/main.rs index d11cd89..c628061 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -20,10 +20,11 @@ mod domain; mod infra; async fn create_admin_user(handler: &SqlBackendHandler, config: &Configuration) -> Result<()> { + let pass_length = config.ldap_user_pass.unsecure().len(); assert!( - config.ldap_user_pass.len() >= 8, + pass_length >= 8, "Minimum password length is 8 characters, got {} characters", - config.ldap_user_pass.len() + pass_length ); handler .create_user(CreateUserRequest {