mirror of
https://github.com/lldap/lldap.git
synced 2026-06-24 05:54:47 +00:00
server: extract the sql backend handler to a separate crate
This commit is contained in:
committed by
nitnelave
parent
ee21d83056
commit
55de3ac329
343
crates/sql-backend-handler/src/sql_opaque_handler.rs
Normal file
343
crates/sql-backend-handler/src/sql_opaque_handler.rs
Normal file
@@ -0,0 +1,343 @@
|
||||
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};
|
||||
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<orion::aead::SecretKey> {
|
||||
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<Option<Vec<u8>>> {
|
||||
// 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<Vec<u8>>,)>()
|
||||
.one(&self.sql_pool)
|
||||
.await?
|
||||
.and_then(|u| u.0))
|
||||
}
|
||||
}
|
||||
|
||||
#[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<login::ServerLoginStartResponse> {
|
||||
let user_id = request.username;
|
||||
info!(r#"OPAQUE login attempt for "{}""#, &user_id);
|
||||
let maybe_password_file = self
|
||||
.get_password_file_for_user(user_id.clone())
|
||||
.await?
|
||||
.map(|bytes| {
|
||||
opaque::server::ServerRegistration::deserialize(&bytes).map_err(|_| {
|
||||
DomainError::InternalError(format!("Corrupted password file for {}", &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,
|
||||
&user_id,
|
||||
)?;
|
||||
let secret_key = self.get_orion_secret_key()?;
|
||||
let server_data = login::ServerData {
|
||||
username: 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<UserId> {
|
||||
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<registration::ServerRegistrationStartResponse> {
|
||||
// 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 user_update = model::users::ActiveModel {
|
||||
user_id: ActiveValue::Set(username.clone()),
|
||||
password_hash: ActiveValue::Set(Some(password_file.serialize())),
|
||||
..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;
|
||||
|
||||
handler
|
||||
.bind(BindRequest {
|
||||
name: UserId::new("bob"),
|
||||
password: "bob00".to_string(),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
handler
|
||||
.bind(BindRequest {
|
||||
name: UserId::new("andrew"),
|
||||
password: "bob00".to_string(),
|
||||
})
|
||||
.await
|
||||
.unwrap_err();
|
||||
handler
|
||||
.bind(BindRequest {
|
||||
name: UserId::new("bob"),
|
||||
password: "wrong_password".to_string(),
|
||||
})
|
||||
.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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user