use crate::SqlBackendHandler; use async_trait::async_trait; use base64::Engine; use lldap_auth::opaque; use lldap_domain::types::UserId; use lldap_domain_handlers::handler::{BindRequest, LoginHandler, UserRequestFilter, UserListerBackendHandler}; use lldap_domain_model::{ error::{DomainError, Result}, model::{self, UserColumn}, }; use lldap_opaque_handler::{OpaqueHandler, login, registration}; use sea_orm::{ActiveModelTrait, ActiveValue, EntityTrait, QuerySelect}; use secstr::SecUtf8; use tracing::{debug, info, instrument, warn}; type SqlOpaqueHandler = SqlBackendHandler; #[instrument(skip_all, level = "debug", err, fields(username = %username.as_str()))] fn passwords_match( password_file_bytes: &[u8], clear_password: &str, opaque_setup: &opaque::server::ServerSetup, username: &UserId, ) -> Result<()> { use opaque::{client, server}; let mut rng = rand::rngs::OsRng; let client_login_start_result = client::login::start_login(clear_password, &mut rng)?; let password_file = server::ServerRegistration::deserialize(password_file_bytes) .map_err(opaque::AuthenticationError::ProtocolError)?; let server_login_start_result = server::login::start_login( &mut rng, opaque_setup, Some(password_file), client_login_start_result.message, username, )?; client::login::finish_login( client_login_start_result.state, server_login_start_result.message, )?; Ok(()) } impl SqlBackendHandler { fn get_orion_secret_key(&self) -> Result { Ok(orion::aead::SecretKey::from_slice( self.opaque_setup.keypair().private(), )?) } #[instrument(skip(self), level = "debug", err)] async fn get_password_file_for_user(&self, user_id: UserId) -> Result>> { // Fetch the previously registered password file from the DB. Ok(model::User::find_by_id(user_id) .select_only() .column(UserColumn::PasswordHash) .into_tuple::<(Option>,)>() .one(&self.sql_pool) .await? .and_then(|u| u.0)) } #[instrument(skip(self), level = "debug", err)] async fn find_user_id_by_email(&self, email: &str) -> Result> { // Find user ID by email address let users = self .list_users( Some(UserRequestFilter::Equality(UserColumn::Email, email.to_owned())), false, ) .await?; if users.len() > 1 { warn!("Multiple users found with email '{}', login ambiguous", email); return Ok(None); } Ok(users.first().map(|user_and_groups| user_and_groups.user.user_id.clone())) } } #[async_trait] impl LoginHandler for SqlBackendHandler { #[instrument(skip_all, level = "debug", err)] async fn bind(&self, request: BindRequest) -> Result<()> { if let Some(password_hash) = self .get_password_file_for_user(request.name.clone()) .await? { info!(r#"Login attempt for "{}""#, &request.name); if passwords_match( &password_hash, &request.password, &self.opaque_setup, &request.name, ) .is_ok() { return Ok(()); } } else { debug!( r#"User "{}" doesn't exist or has no password"#, &request.name ); } Err(DomainError::AuthenticationError(format!( r#"for user "{}""#, request.name ))) } } #[async_trait] impl OpaqueHandler for SqlOpaqueHandler { #[instrument(skip_all, level = "debug", err)] async fn login_start( &self, request: login::ClientLoginStartRequest, ) -> Result { // First try to authenticate with the provided name as a user ID let mut actual_user_id = request.username.clone(); let mut maybe_password_file = self .get_password_file_for_user(request.username.clone()) .await?; // If no user found by user ID, try to find by email for web UI login if maybe_password_file.is_none() { debug!(r#"User "{}" not found by user ID, trying email lookup for web login"#, &request.username); if let Some(user_id_by_email) = self .find_user_id_by_email(request.username.as_str()) .await? { debug!(r#"Found user by email: "{}""#, &user_id_by_email); actual_user_id = user_id_by_email; maybe_password_file = self .get_password_file_for_user(actual_user_id.clone()) .await?; } } info!(r#"OPAQUE login attempt for "{}" (input: "{}")"#, &actual_user_id, &request.username); let maybe_password_file = maybe_password_file .map(|bytes| { opaque::server::ServerRegistration::deserialize(&bytes).map_err(|_| { DomainError::InternalError(format!("Corrupted password file for {}", &actual_user_id)) }) }) .transpose()?; let mut rng = rand::rngs::OsRng; // Get the CredentialResponse for the user, or a dummy one if no user/no password. let start_response = opaque::server::login::start_login( &mut rng, &self.opaque_setup, maybe_password_file, request.login_start_request, &actual_user_id, )?; let secret_key = self.get_orion_secret_key()?; let server_data = login::ServerData { username: actual_user_id, server_login: start_response.state, }; let encrypted_state = orion::aead::seal(&secret_key, &bincode::serialize(&server_data)?)?; Ok(login::ServerLoginStartResponse { server_data: base64::engine::general_purpose::STANDARD.encode(encrypted_state), credential_response: start_response.message, }) } #[instrument(skip_all, level = "debug", err)] async fn login_finish(&self, request: login::ClientLoginFinishRequest) -> Result { let secret_key = self.get_orion_secret_key()?; let login::ServerData { username, server_login, } = bincode::deserialize(&orion::aead::open( &secret_key, &base64::engine::general_purpose::STANDARD.decode(&request.server_data)?, )?)?; // Finish the login: this makes sure the client data is correct, and gives a session key we // don't need. match opaque::server::login::finish_login(server_login, request.credential_finalization) { Ok(session) => { info!(r#"OPAQUE login successful for "{}""#, &username); let _ = session.session_key; } Err(e) => { warn!(r#"OPAQUE login attempt failed for "{}""#, &username); return Err(e.into()); } }; Ok(username) } #[instrument(skip_all, level = "debug", err)] async fn registration_start( &self, request: registration::ClientRegistrationStartRequest, ) -> Result { // Generate the server-side key and derive the data to send back. let start_response = opaque::server::registration::start_registration( &self.opaque_setup, request.registration_start_request, &request.username, )?; let secret_key = self.get_orion_secret_key()?; let server_data = registration::ServerData { username: request.username, }; let encrypted_state = orion::aead::seal(&secret_key, &bincode::serialize(&server_data)?)?; Ok(registration::ServerRegistrationStartResponse { server_data: base64::engine::general_purpose::STANDARD.encode(encrypted_state), registration_response: start_response.message, }) } #[instrument(skip_all, level = "debug", err)] async fn registration_finish( &self, request: registration::ClientRegistrationFinishRequest, ) -> Result<()> { let secret_key = self.get_orion_secret_key()?; let registration::ServerData { username } = bincode::deserialize(&orion::aead::open( &secret_key, &base64::engine::general_purpose::STANDARD.decode(&request.server_data)?, )?)?; let password_file = opaque::server::registration::get_password_file(request.registration_upload); // Set the user password to the new password. let now = chrono::Utc::now().naive_utc(); let user_update = model::users::ActiveModel { user_id: ActiveValue::Set(username.clone()), password_hash: ActiveValue::Set(Some(password_file.serialize())), password_modified_date: ActiveValue::Set(now), modified_date: ActiveValue::Set(now), ..Default::default() }; user_update.update(&self.sql_pool).await?; info!(r#"Successfully (re)set password for "{}""#, &username); Ok(()) } } /// Convenience function to set a user's password. #[instrument(skip_all, level = "debug", err, fields(username = %username.as_str()))] pub async fn register_password( opaque_handler: &SqlOpaqueHandler, username: UserId, password: &SecUtf8, ) -> Result<()> { let mut rng = rand::rngs::OsRng; use registration::*; let registration_start = opaque::client::registration::start_registration(password.unsecure().as_bytes(), &mut rng)?; let start_response = opaque_handler .registration_start(ClientRegistrationStartRequest { username, registration_start_request: registration_start.message, }) .await?; let registration_finish = opaque::client::registration::finish_registration( registration_start.state, start_response.registration_response, &mut rng, )?; opaque_handler .registration_finish(ClientRegistrationFinishRequest { server_data: start_response.server_data, registration_upload: registration_finish.message, }) .await } #[cfg(test)] mod tests { use self::opaque::server::generate_random_private_key; use super::*; use crate::sql_backend_handler::tests::{ get_initialized_db, insert_user, insert_user_no_password, }; async fn attempt_login( opaque_handler: &SqlOpaqueHandler, username: &str, password: &str, ) -> Result<()> { let mut rng = rand::rngs::OsRng; use login::*; let login_start = opaque::client::login::start_login(password, &mut rng)?; let start_response = opaque_handler .login_start(ClientLoginStartRequest { username: UserId::new(username), login_start_request: login_start.message, }) .await?; let login_finish = opaque::client::login::finish_login( login_start.state, start_response.credential_response, )?; opaque_handler .login_finish(ClientLoginFinishRequest { server_data: start_response.server_data, credential_finalization: login_finish.message, }) .await?; Ok(()) } #[tokio::test] async fn test_opaque_flow() -> Result<()> { let sql_pool = get_initialized_db().await; crate::logging::init_for_tests(); let backend_handler = SqlBackendHandler::new(generate_random_private_key(), sql_pool); insert_user_no_password(&backend_handler, "bob").await; insert_user_no_password(&backend_handler, "john").await; attempt_login(&backend_handler, "bob", "bob00") .await .unwrap_err(); register_password( &backend_handler, UserId::new("bob"), &secstr::SecUtf8::from("bob00"), ) .await?; attempt_login(&backend_handler, "bob", "wrong_password") .await .unwrap_err(); attempt_login(&backend_handler, "bob", "bob00").await?; Ok(()) } #[tokio::test] async fn test_bind_user() { let sql_pool = get_initialized_db().await; let handler = SqlOpaqueHandler::new(generate_random_private_key(), sql_pool.clone()); insert_user(&handler, "bob", "bob00").await; // Test login with username (should work) handler .bind(BindRequest { name: UserId::new("bob"), password: "bob00".to_string(), }) .await .unwrap(); // Test login with non-existent user handler .bind(BindRequest { name: UserId::new("andrew"), password: "bob00".to_string(), }) .await .unwrap_err(); // Test login with wrong password handler .bind(BindRequest { name: UserId::new("bob"), password: "wrong_password".to_string(), }) .await .unwrap_err(); // Test that email login is NOT supported for LDAP bind handler .bind(BindRequest { name: UserId::new("bob@bob.bob"), password: "bob00".to_string(), }) .await .unwrap_err(); } #[tokio::test] async fn test_opaque_login_with_email() { let sql_pool = get_initialized_db().await; crate::logging::init_for_tests(); let backend_handler = SqlBackendHandler::new(generate_random_private_key(), sql_pool); insert_user(&backend_handler, "bob", "bob00").await; // Test OPAQUE login with username (should work as before) attempt_login(&backend_handler, "bob", "bob00").await.unwrap(); // Test OPAQUE login with email (new functionality) attempt_login(&backend_handler, "bob@bob.bob", "bob00").await.unwrap(); // Test OPAQUE login with non-existent email attempt_login(&backend_handler, "nonexistent@bob.bob", "bob00") .await .unwrap_err(); // Test OPAQUE login with wrong password using email attempt_login(&backend_handler, "bob@bob.bob", "wrong_password") .await .unwrap_err(); } #[tokio::test] async fn test_user_no_password() { let sql_pool = get_initialized_db().await; let handler = SqlBackendHandler::new(generate_random_private_key(), sql_pool.clone()); insert_user_no_password(&handler, "bob").await; handler .bind(BindRequest { name: UserId::new("bob"), password: "bob00".to_string(), }) .await .unwrap_err(); } }