Update opaque and implement it without DB

This commit is contained in:
Valentin Tolmer 2021-06-23 20:33:36 +02:00 committed by nitnelave
parent f12abb35d3
commit 8b73de0df7
13 changed files with 209 additions and 343 deletions

65
Cargo.lock generated
View File

@ -728,6 +728,22 @@ dependencies = [
"subtle", "subtle",
] ]
[[package]]
name = "crypto-mac"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25fab6889090c8133f3deb8f73ba3c65a7f456f66436fc012a1b1e272b1e103e"
dependencies = [
"generic-array",
"subtle",
]
[[package]]
name = "ct-codecs"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3b7eb4404b8195a9abb6356f4ac07d8ba267045c8d6d220ac4dc992e6cc75df"
[[package]] [[package]]
name = "curve25519-dalek" name = "curve25519-dalek"
version = "3.1.0" version = "3.1.0"
@ -737,6 +753,7 @@ dependencies = [
"byteorder", "byteorder",
"digest", "digest",
"rand_core 0.5.1", "rand_core 0.5.1",
"serde",
"subtle", "subtle",
"zeroize", "zeroize",
] ]
@ -1056,6 +1073,7 @@ version = "0.14.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817" checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817"
dependencies = [ dependencies = [
"serde",
"typenum", "typenum",
"version_check", "version_check",
] ]
@ -1210,12 +1228,12 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]] [[package]]
name = "hkdf" name = "hkdf"
version = "0.10.0" version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51ab2f639c231793c5f6114bdb9bbe50a7dbbfcd7c7c6bd8475dec2d991e964f" checksum = "01706d578d5c281058480e673ae4086a9f4710d8df1ad80a5b03e39ece5f886b"
dependencies = [ dependencies = [
"digest", "digest",
"hmac", "hmac 0.11.0",
] ]
[[package]] [[package]]
@ -1224,7 +1242,17 @@ version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1441c6b1e930e2817404b5046f1f989899143a12bf92de603b69f4e0aee1e15" checksum = "c1441c6b1e930e2817404b5046f1f989899143a12bf92de603b69f4e0aee1e15"
dependencies = [ dependencies = [
"crypto-mac", "crypto-mac 0.10.0",
"digest",
]
[[package]]
name = "hmac"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b"
dependencies = [
"crypto-mac 0.11.0",
"digest", "digest",
] ]
@ -1324,9 +1352,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86e46349d67dc03bdbdb28da0337a355a53ca1d5156452722c36fe21d0e6389b" checksum = "86e46349d67dc03bdbdb28da0337a355a53ca1d5156452722c36fe21d0e6389b"
dependencies = [ dependencies = [
"base64", "base64",
"crypto-mac", "crypto-mac 0.10.0",
"digest", "digest",
"hmac", "hmac 0.10.1",
"serde", "serde",
"serde_json", "serde_json",
"sha2", "sha2",
@ -1419,6 +1447,8 @@ dependencies = [
"actix-web-httpauth", "actix-web-httpauth",
"anyhow", "anyhow",
"async-trait", "async-trait",
"base64",
"bincode",
"chrono", "chrono",
"clap", "clap",
"cron", "cron",
@ -1426,7 +1456,7 @@ dependencies = [
"figment", "figment",
"futures", "futures",
"futures-util", "futures-util",
"hmac", "hmac 0.10.1",
"http", "http",
"jwt", "jwt",
"ldap3_server", "ldap3_server",
@ -1434,6 +1464,7 @@ dependencies = [
"log", "log",
"mockall", "mockall",
"opaque-ke", "opaque-ke",
"orion",
"rand 0.8.3", "rand 0.8.3",
"sea-query", "sea-query",
"serde", "serde",
@ -1804,8 +1835,8 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
[[package]] [[package]]
name = "opaque-ke" name = "opaque-ke"
version = "0.5.1-pre.1" version = "0.6.0-pre.1"
source = "git+https://github.com/novifinancial/opaque-ke?rev=98f1821897cd2800e5bffb2a70541056145e99cc#98f1821897cd2800e5bffb2a70541056145e99cc" source = "git+https://github.com/novifinancial/opaque-ke?rev=eb59676a940b15f77871aefe1e46d7b5bf85f40a#eb59676a940b15f77871aefe1e46d7b5bf85f40a"
dependencies = [ dependencies = [
"base64", "base64",
"curve25519-dalek", "curve25519-dalek",
@ -1814,7 +1845,7 @@ dependencies = [
"generic-array", "generic-array",
"generic-bytes", "generic-bytes",
"hkdf", "hkdf",
"hmac", "hmac 0.11.0",
"rand 0.8.3", "rand 0.8.3",
"serde", "serde",
"subtle", "subtle",
@ -1871,6 +1902,18 @@ dependencies = [
"thiserror", "thiserror",
] ]
[[package]]
name = "orion"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "118f94b4ca56d1eb99466a26a216b87fad822a51af8b308264c88a9337eb0a15"
dependencies = [
"ct-codecs",
"getrandom 0.2.3",
"subtle",
"zeroize",
]
[[package]] [[package]]
name = "os_str_bytes" name = "os_str_bytes"
version = "2.4.0" version = "2.4.0"
@ -2525,7 +2568,7 @@ dependencies = [
"generic-array", "generic-array",
"hashlink", "hashlink",
"hex", "hex",
"hmac", "hmac 0.10.1",
"itoa", "itoa",
"libc", "libc",
"libsqlite3-sys", "libsqlite3-sys",

View File

@ -16,6 +16,8 @@ actix-service = "2.0.0"
actix-web = "4.0.0-beta.3" actix-web = "4.0.0-beta.3"
anyhow = "*" anyhow = "*"
async-trait = "0.1" async-trait = "0.1"
base64 = "0.13"
bincode = "1.3"
chrono = { version = "*", features = [ "serde" ]} chrono = { version = "*", features = [ "serde" ]}
clap = "3.0.0-beta.2" clap = "3.0.0-beta.2"
cron = "*" cron = "*"
@ -28,6 +30,7 @@ jwt = "0.13"
ldap3_server = "*" ldap3_server = "*"
lldap_model = { path = "model" } lldap_model = { path = "model" }
log = "*" log = "*"
orion = "0.16"
serde = "*" serde = "*"
serde_json = "1" serde_json = "1"
sha2 = "0.9" sha2 = "0.9"
@ -45,7 +48,7 @@ rand = { version = "0.8", features = ["small_rng", "getrandom"] }
# TODO: update to 0.6 when out. # TODO: update to 0.6 when out.
[dependencies.opaque-ke] [dependencies.opaque-ke]
git = "https://github.com/novifinancial/opaque-ke" git = "https://github.com/novifinancial/opaque-ke"
rev = "98f1821897cd2800e5bffb2a70541056145e99cc" rev = "eb59676a940b15f77871aefe1e46d7b5bf85f40a"
[dependencies.sqlx] [dependencies.sqlx]
version = "0.5.1" version = "0.5.1"

View File

@ -92,7 +92,7 @@ impl LoginForm {
Ok(l) => l, Ok(l) => l,
}; };
let req = login::ClientLoginFinishRequest { let req = login::ClientLoginFinishRequest {
login_key: res.login_key, server_data: res.server_data,
credential_finalization: login_finish.message, credential_finalization: login_finish.message,
}; };
self.call_backend( self.call_backend(

View File

@ -23,7 +23,7 @@ thiserror = "*"
# TODO: update to 0.6 when out. # TODO: update to 0.6 when out.
[dependencies.opaque-ke] [dependencies.opaque-ke]
git = "https://github.com/novifinancial/opaque-ke" git = "https://github.com/novifinancial/opaque-ke"
rev = "98f1821897cd2800e5bffb2a70541056145e99cc" rev = "eb59676a940b15f77871aefe1e46d7b5bf85f40a"
[dependencies.chrono] [dependencies.chrono]
version = "*" version = "*"

View File

@ -14,6 +14,12 @@ pub struct BindRequest {
pub mod login { pub mod login {
use super::*; use super::*;
#[derive(Serialize, Deserialize, Clone)]
pub struct ServerData {
pub username: String,
pub server_login: opaque::server::login::ServerLogin,
}
#[derive(Serialize, Deserialize, Clone)] #[derive(Serialize, Deserialize, Clone)]
pub struct ClientLoginStartRequest { pub struct ClientLoginStartRequest {
pub username: String, pub username: String,
@ -22,15 +28,15 @@ pub mod login {
#[derive(Serialize, Deserialize, Clone)] #[derive(Serialize, Deserialize, Clone)]
pub struct ServerLoginStartResponse { pub struct ServerLoginStartResponse {
/// A randomly-generated temporary key that corresponds to this login attempt. /// Base64, encrypted ServerData to be passed back to the server.
pub login_key: String, pub server_data: String,
pub credential_response: opaque::client::login::CredentialResponse, pub credential_response: opaque::client::login::CredentialResponse,
} }
#[derive(Serialize, Deserialize, Clone)] #[derive(Serialize, Deserialize, Clone)]
pub struct ClientLoginFinishRequest { pub struct ClientLoginFinishRequest {
/// The key returned by the server in the previous step. /// Encrypted ServerData from the previous step.
pub login_key: String, pub server_data: String,
pub credential_finalization: opaque::client::login::CredentialFinalization, pub credential_finalization: opaque::client::login::CredentialFinalization,
} }
} }
@ -39,6 +45,11 @@ pub mod login {
pub mod registration { pub mod registration {
use super::*; use super::*;
#[derive(Serialize, Deserialize, Clone)]
pub struct ServerData {
pub username: String,
}
#[derive(Serialize, Deserialize, Clone)] #[derive(Serialize, Deserialize, Clone)]
pub struct ClientRegistrationStartRequest { pub struct ClientRegistrationStartRequest {
pub username: String, pub username: String,
@ -47,15 +58,15 @@ pub mod registration {
#[derive(Serialize, Deserialize, Clone)] #[derive(Serialize, Deserialize, Clone)]
pub struct ServerRegistrationStartResponse { pub struct ServerRegistrationStartResponse {
/// A randomly-generated temporary key that corresponds to this registration attempt. /// Base64, encrypted ServerData to be passed back to the server.
pub registration_key: String, pub server_data: String,
pub registration_response: opaque::client::registration::RegistrationResponse, pub registration_response: opaque::client::registration::RegistrationResponse,
} }
#[derive(Serialize, Deserialize, Clone)] #[derive(Serialize, Deserialize, Clone)]
pub struct ClientRegistrationFinishRequest { pub struct ClientRegistrationFinishRequest {
/// The key returned by the server in the previous step. /// Encrypted ServerData from the previous step.
pub registration_key: String, pub server_data: String,
pub registration_upload: opaque::server::registration::RegistrationUpload, pub registration_upload: opaque::server::registration::RegistrationUpload,
} }
} }

View File

@ -100,18 +100,14 @@ pub mod client {
pub type ClientLoginStartResult = opaque_ke::ClientLoginStartResult<DefaultSuite>; pub type ClientLoginStartResult = opaque_ke::ClientLoginStartResult<DefaultSuite>;
pub type CredentialResponse = opaque_ke::CredentialResponse<DefaultSuite>; pub type CredentialResponse = opaque_ke::CredentialResponse<DefaultSuite>;
pub type CredentialFinalization = opaque_ke::CredentialFinalization<DefaultSuite>; pub type CredentialFinalization = opaque_ke::CredentialFinalization<DefaultSuite>;
pub use opaque_ke::{ClientLoginFinishParameters, ClientLoginStartParameters}; pub use opaque_ke::ClientLoginFinishParameters;
/// Initiate the login negotiation. /// Initiate the login negotiation.
pub fn start_login<R: RngCore + CryptoRng>( pub fn start_login<R: RngCore + CryptoRng>(
password: &str, password: &str,
rng: &mut R, rng: &mut R,
) -> AuthenticationResult<ClientLoginStartResult> { ) -> AuthenticationResult<ClientLoginStartResult> {
Ok(ClientLogin::start( Ok(ClientLogin::start(rng, password.as_bytes())?)
rng,
password.as_bytes(),
ClientLoginStartParameters::default(),
)?)
} }
/// Finalize the client login negotiation. /// Finalize the client login negotiation.
@ -130,6 +126,7 @@ pub mod client {
pub mod server { pub mod server {
pub use super::*; pub use super::*;
pub type ServerRegistration = opaque_ke::ServerRegistration<DefaultSuite>; pub type ServerRegistration = opaque_ke::ServerRegistration<DefaultSuite>;
pub type ServerSetup = opaque_ke::ServerSetup<DefaultSuite>;
/// Methods to register a new user, from the server side. /// Methods to register a new user, from the server side.
pub mod registration { pub mod registration {
pub use super::*; pub use super::*;
@ -140,24 +137,21 @@ pub mod server {
/// Start a registration process, from a request sent by the client. /// Start a registration process, from a request sent by the client.
/// ///
/// The result must be kept for the next step. /// The result must be kept for the next step.
pub fn start_registration<R: RngCore + CryptoRng>( pub fn start_registration(
rng: &mut R, server_setup: &ServerSetup,
registration_request: RegistrationRequest, registration_request: RegistrationRequest,
server_public_key: &PublicKey, username: &str,
) -> AuthenticationResult<ServerRegistrationStartResult> { ) -> AuthenticationResult<ServerRegistrationStartResult> {
Ok(ServerRegistration::start( Ok(ServerRegistration::start(
rng, server_setup,
registration_request, registration_request,
server_public_key, username.as_bytes(),
)?) )?)
} }
/// Finish to register a new user, and get the data to store in the database. /// Finish to register a new user, and get the data to store in the database.
pub fn get_password_file( pub fn get_password_file(registration_upload: RegistrationUpload) -> ServerRegistration {
registration_start: ServerRegistration, ServerRegistration::finish(registration_upload)
registration_upload: RegistrationUpload,
) -> AuthenticationResult<ServerRegistration> {
Ok(registration_start.finish(registration_upload)?)
} }
} }
@ -176,15 +170,17 @@ pub mod server {
/// The result must be kept for the next step. /// The result must be kept for the next step.
pub fn start_login<R: RngCore + CryptoRng>( pub fn start_login<R: RngCore + CryptoRng>(
rng: &mut R, rng: &mut R,
password_file: ServerRegistration, server_setup: &ServerSetup,
server_private_key: &PrivateKey, password_file: Option<ServerRegistration>,
credential_request: CredentialRequest, credential_request: CredentialRequest,
username: &str,
) -> AuthenticationResult<ServerLoginStartResult> { ) -> AuthenticationResult<ServerLoginStartResult> {
Ok(ServerLogin::start( Ok(ServerLogin::start(
rng, rng,
server_setup,
password_file, password_file,
server_private_key,
credential_request, credential_request,
username.as_bytes(),
ServerLoginStartParameters::default(), ServerLoginStartParameters::default(),
)?) )?)
} }

View File

@ -9,6 +9,12 @@ pub enum DomainError {
DatabaseError(#[from] sqlx::Error), DatabaseError(#[from] sqlx::Error),
#[error("Authentication protocol error for `{0}`")] #[error("Authentication protocol error for `{0}`")]
AuthenticationProtocolError(#[from] lldap_model::opaque::AuthenticationError), AuthenticationProtocolError(#[from] lldap_model::opaque::AuthenticationError),
#[error("Unknown crypto error: `{0}`")]
UnknownCryptoError(#[from] orion::errors::UnknownCryptoError),
#[error("Binary serialization error: `{0}`")]
BinarySerializationError(#[from] bincode::Error),
#[error("Invalid base64: `{0}`")]
Base64DecodeError(#[from] base64::DecodeError),
#[error("Internal error: `{0}`")] #[error("Internal error: `{0}`")]
InternalError(String), InternalError(String),
} }

View File

@ -22,7 +22,8 @@ impl SqlBackendHandler {
pub fn get_password_file( pub fn get_password_file(
clear_password: &str, clear_password: &str,
server_public_key: &opaque::PublicKey, server_setup: &opaque::server::ServerSetup,
username: &str,
) -> Result<opaque::server::ServerRegistration> { ) -> Result<opaque::server::ServerRegistration> {
use opaque::{client, server}; use opaque::{client, server};
let mut rng = rand::rngs::OsRng; let mut rng = rand::rngs::OsRng;
@ -30,9 +31,9 @@ pub fn get_password_file(
client::registration::start_registration(clear_password, &mut rng)?; client::registration::start_registration(clear_password, &mut rng)?;
let server_register_start_result = server::registration::start_registration( let server_register_start_result = server::registration::start_registration(
&mut rng, server_setup,
client_register_start_result.message, client_register_start_result.message,
server_public_key, username,
)?; )?;
let client_registration_result = client::registration::finish_registration( let client_registration_result = client::registration::finish_registration(
@ -42,9 +43,8 @@ pub fn get_password_file(
)?; )?;
Ok(server::registration::get_password_file( Ok(server::registration::get_password_file(
server_register_start_result.state,
client_registration_result.message, client_registration_result.message,
)?) ))
} }
fn get_filter_expr(filter: RequestFilter) -> SimpleExpr { fn get_filter_expr(filter: RequestFilter) -> SimpleExpr {
@ -187,7 +187,7 @@ impl BackendHandler for SqlBackendHandler {
Users::CreationDate, Users::CreationDate,
]; ];
let mut values = vec![ let mut values = vec![
request.user_id.into(), request.user_id.clone().into(),
request.email.into(), request.email.into(),
request.display_name.map(Into::into).unwrap_or(Value::Null), request.display_name.map(Into::into).unwrap_or(Value::Null),
request.first_name.map(Into::into).unwrap_or(Value::Null), request.first_name.map(Into::into).unwrap_or(Value::Null),
@ -197,7 +197,7 @@ impl BackendHandler for SqlBackendHandler {
if let Some(pass) = request.password { if let Some(pass) = request.password {
columns.push(Users::PasswordHash); columns.push(Users::PasswordHash);
values.push( values.push(
get_password_file(&pass, self.config.get_server_keys().public())? get_password_file(&pass, self.config.get_server_setup(), &request.user_id)?
.serialize() .serialize()
.into(), .into(),
); );

View File

@ -5,25 +5,16 @@ use super::{
use async_trait::async_trait; use async_trait::async_trait;
use lldap_model::{opaque, BindRequest}; use lldap_model::{opaque, BindRequest};
use log::*; use log::*;
use rand::{CryptoRng, RngCore};
use sea_query::{Expr, Iden, Query}; use sea_query::{Expr, Iden, Query};
use sqlx::Row; use sqlx::Row;
type SqlOpaqueHandler = SqlBackendHandler; type SqlOpaqueHandler = SqlBackendHandler;
fn generate_random_id<R: RngCore + CryptoRng>(rng: &mut R) -> String {
use rand::{distributions::Alphanumeric, Rng};
std::iter::repeat(())
.map(|()| rng.sample(Alphanumeric))
.map(char::from)
.take(32)
.collect()
}
fn passwords_match( fn passwords_match(
password_file_bytes: &[u8], password_file_bytes: &[u8],
clear_password: &str, clear_password: &str,
server_private_key: &opaque::PrivateKey, server_setup: &opaque::server::ServerSetup,
username: &str,
) -> Result<()> { ) -> Result<()> {
use opaque::{client, server}; use opaque::{client, server};
let mut rng = rand::rngs::OsRng; let mut rng = rand::rngs::OsRng;
@ -33,9 +24,10 @@ fn passwords_match(
.map_err(opaque::AuthenticationError::ProtocolError)?; .map_err(opaque::AuthenticationError::ProtocolError)?;
let server_login_start_result = server::login::start_login( let server_login_start_result = server::login::start_login(
&mut rng, &mut rng,
password_file, server_setup,
server_private_key, Some(password_file),
client_login_start_result.message, client_login_start_result.message,
username,
)?; )?;
client::login::finish_login( client::login::finish_login(
client_login_start_result.state, client_login_start_result.state,
@ -44,6 +36,40 @@ fn passwords_match(
Ok(()) Ok(())
} }
impl SqlBackendHandler {
fn get_orion_secret_key(&self) -> Result<orion::aead::SecretKey> {
Ok(orion::aead::SecretKey::from_slice(
self.config.get_server_keys().private(),
)?)
}
async fn get_password_file_for_user(
&self,
username: &str,
) -> Result<Option<opaque::server::ServerRegistration>> {
// Fetch the previously registered password file from the DB.
let password_file_bytes = {
let query = Query::select()
.column(Users::PasswordHash)
.from(Users::Table)
.and_where(Expr::col(Users::UserId).eq(username))
.to_string(DbQueryBuilder {});
if let Some(row) = sqlx::query(&query).fetch_optional(&self.sql_pool).await? {
row.get::<Option<Vec<u8>>, _>(&*Users::PasswordHash.to_string())
// If no password, always fail.
.ok_or_else(|| DomainError::AuthenticationError(username.to_string()))?
} else {
return Ok(None);
}
};
opaque::server::ServerRegistration::deserialize(&password_file_bytes)
.map(Option::Some)
.map_err(|_| {
DomainError::InternalError(format!("Corrupted password file for {}", username))
})
}
}
#[async_trait] #[async_trait]
impl LoginHandler for SqlBackendHandler { impl LoginHandler for SqlBackendHandler {
async fn bind(&self, request: BindRequest) -> Result<()> { async fn bind(&self, request: BindRequest) -> Result<()> {
@ -67,7 +93,8 @@ impl LoginHandler for SqlBackendHandler {
if let Err(e) = passwords_match( if let Err(e) = passwords_match(
&password_hash, &password_hash,
&request.password, &request.password,
self.config.get_server_keys().private(), self.config.get_server_setup(),
&request.name,
) { ) {
debug!(r#"Invalid password for "{}": {}"#, request.name, e); debug!(r#"Invalid password for "{}": {}"#, request.name, e);
} else { } else {
@ -89,99 +116,44 @@ impl OpaqueHandler for SqlOpaqueHandler {
&self, &self,
request: login::ClientLoginStartRequest, request: login::ClientLoginStartRequest,
) -> Result<login::ServerLoginStartResponse> { ) -> Result<login::ServerLoginStartResponse> {
// Fetch the previously registered password file from the DB. let maybe_password_file = self.get_password_file_for_user(&request.username).await?;
let password_file_bytes = {
let query = Query::select()
.column(Users::PasswordHash)
.from(Users::Table)
.and_where(Expr::col(Users::UserId).eq(request.username.as_str()))
.to_string(DbQueryBuilder {});
sqlx::query(&query)
.fetch_one(&self.sql_pool)
.await?
.get::<Option<Vec<u8>>, _>(&*Users::PasswordHash.to_string())
// If no password, always fail.
.ok_or_else(|| DomainError::AuthenticationError(request.username.clone()))?
};
let password_file = opaque::server::ServerRegistration::deserialize(&password_file_bytes)
.map_err(|_| {
DomainError::InternalError(format!("Corrupted password file for {}", request.username))
})?;
let mut rng = rand::rngs::OsRng; let mut rng = rand::rngs::OsRng;
let start_response = opaque::server::login::start_login( let start_response = opaque::server::login::start_login(
&mut rng, &mut rng,
password_file, self.config.get_server_setup(),
self.config.get_server_keys().private(), maybe_password_file,
request.login_start_request, request.login_start_request,
&request.username,
)?; )?;
let login_attempt_id = generate_random_id(&mut rng); let secret_key = self.get_orion_secret_key()?;
let server_data = login::ServerData {
{ username: request.username,
// Insert the current login attempt in the DB. server_login: start_response.state,
let query = Query::insert() };
.into_table(LoginAttempts::Table) let encrypted_state = orion::aead::seal(&secret_key, &bincode::serialize(&server_data)?)?;
.columns(vec![
LoginAttempts::RandomId,
LoginAttempts::UserId,
LoginAttempts::ServerLoginData,
LoginAttempts::Timestamp,
])
.values_panic(vec![
login_attempt_id.as_str().into(),
request.username.as_str().into(),
start_response.state.serialize().into(),
chrono::Utc::now().naive_utc().into(),
])
.to_string(DbQueryBuilder {});
sqlx::query(&query).execute(&self.sql_pool).await?;
}
Ok(login::ServerLoginStartResponse { Ok(login::ServerLoginStartResponse {
login_key: login_attempt_id, server_data: base64::encode(&encrypted_state),
credential_response: start_response.message, credential_response: start_response.message,
}) })
} }
async fn login_finish(&self, request: login::ClientLoginFinishRequest) -> Result<String> { async fn login_finish(&self, request: login::ClientLoginFinishRequest) -> Result<String> {
// Fetch the previous data from this login attempt. let secret_key = self.get_orion_secret_key()?;
let row = { let login::ServerData {
let query = Query::select() username,
.column(LoginAttempts::UserId) server_login,
.column(LoginAttempts::ServerLoginData) } = bincode::deserialize(&orion::aead::open(
.from(LoginAttempts::Table) &secret_key,
.and_where(Expr::col(LoginAttempts::RandomId).eq(request.login_key.as_str())) &base64::decode(&request.server_data)?,
.and_where( )?)?;
Expr::col(LoginAttempts::Timestamp)
.gt(chrono::Utc::now().naive_utc() - chrono::Duration::minutes(5)),
)
.to_string(DbQueryBuilder {});
sqlx::query(&query).fetch_one(&self.sql_pool).await?
};
let username = row.get::<String, _>(&*LoginAttempts::UserId.to_string());
let login_data = opaque::server::login::ServerLogin::deserialize(
&row.get::<Vec<u8>, _>(&*LoginAttempts::ServerLoginData.to_string()),
)
.map_err(|_| {
DomainError::InternalError(format!(
"Corrupted login data for user `{}` [id `{}`]",
username, request.login_key
))
})?;
// Finish the login: this makes sure the client data is correct, and gives a session key we // Finish the login: this makes sure the client data is correct, and gives a session key we
// don't need. // don't need.
let _session_key = let _session_key =
opaque::server::login::finish_login(login_data, request.credential_finalization)? opaque::server::login::finish_login(server_login, request.credential_finalization)?
.session_key; .session_key;
{
// Login was successful, we can delete the login attempt from the table.
let delete_query = Query::delete()
.from_table(LoginAttempts::Table)
.and_where(Expr::col(LoginAttempts::RandomId).eq(request.login_key))
.to_string(DbQueryBuilder {});
sqlx::query(&delete_query).execute(&self.sql_pool).await?;
}
Ok(username) Ok(username)
} }
@ -189,36 +161,19 @@ impl OpaqueHandler for SqlOpaqueHandler {
&self, &self,
request: registration::ClientRegistrationStartRequest, request: registration::ClientRegistrationStartRequest,
) -> Result<registration::ServerRegistrationStartResponse> { ) -> Result<registration::ServerRegistrationStartResponse> {
let mut rng = rand::rngs::OsRng;
// Generate the server-side key and derive the data to send back. // Generate the server-side key and derive the data to send back.
let start_response = opaque::server::registration::start_registration( let start_response = opaque::server::registration::start_registration(
&mut rng, self.config.get_server_setup(),
request.registration_start_request, request.registration_start_request,
self.config.get_server_keys().public(), &request.username,
)?; )?;
// Unique ID to identify the registration attempt. let secret_key = self.get_orion_secret_key()?;
let registration_attempt_id = generate_random_id(&mut rng); let server_data = registration::ServerData {
{ username: request.username,
// Write the registration attempt to the DB for the later turn. };
let query = Query::insert() let encrypted_state = orion::aead::seal(&secret_key, &bincode::serialize(&server_data)?)?;
.into_table(RegistrationAttempts::Table)
.columns(vec![
RegistrationAttempts::RandomId,
RegistrationAttempts::UserId,
RegistrationAttempts::ServerRegistrationData,
RegistrationAttempts::Timestamp,
])
.values_panic(vec![
registration_attempt_id.as_str().into(),
request.username.as_str().into(),
start_response.state.serialize().into(),
chrono::Utc::now().naive_utc().into(),
])
.to_string(DbQueryBuilder {});
sqlx::query(&query).execute(&self.sql_pool).await?;
}
Ok(registration::ServerRegistrationStartResponse { Ok(registration::ServerRegistrationStartResponse {
registration_key: registration_attempt_id, server_data: base64::encode(encrypted_state),
registration_response: start_response.message, registration_response: start_response.message,
}) })
} }
@ -227,37 +182,14 @@ impl OpaqueHandler for SqlOpaqueHandler {
&self, &self,
request: registration::ClientRegistrationFinishRequest, request: registration::ClientRegistrationFinishRequest,
) -> Result<()> { ) -> Result<()> {
// Fetch the previous state. let secret_key = self.get_orion_secret_key()?;
let row = { let registration::ServerData { username } = bincode::deserialize(&orion::aead::open(
let query = Query::select() &secret_key,
.column(RegistrationAttempts::UserId) &base64::decode(&request.server_data)?,
.column(RegistrationAttempts::ServerRegistrationData) )?)?;
.from(RegistrationAttempts::Table)
.and_where(
Expr::col(RegistrationAttempts::RandomId).eq(request.registration_key.as_str()),
)
.and_where(
Expr::col(RegistrationAttempts::Timestamp)
.gt(chrono::Utc::now().naive_utc() - chrono::Duration::minutes(5)),
)
.to_string(DbQueryBuilder {});
sqlx::query(&query).fetch_one(&self.sql_pool).await?
};
let username = row.get::<String, _>(&*RegistrationAttempts::UserId.to_string());
let registration_data = opaque::server::registration::ServerRegistration::deserialize(
&row.get::<Vec<u8>, _>(&*RegistrationAttempts::ServerRegistrationData.to_string()),
)
.map_err(|_| {
DomainError::InternalError(format!(
"Corrupted registration data for user `{}` [id `{}`]",
username, request.registration_key
))
})?;
let password_file = opaque::server::registration::get_password_file( let password_file =
registration_data, opaque::server::registration::get_password_file(request.registration_upload);
request.registration_upload,
)?;
{ {
// Set the user password to the new password. // Set the user password to the new password.
let update_query = Query::update() let update_query = Query::update()
@ -270,14 +202,6 @@ impl OpaqueHandler for SqlOpaqueHandler {
.to_string(DbQueryBuilder {}); .to_string(DbQueryBuilder {});
sqlx::query(&update_query).execute(&self.sql_pool).await?; sqlx::query(&update_query).execute(&self.sql_pool).await?;
} }
{
// Delete the registration attempt.
let delete_query = Query::delete()
.from_table(RegistrationAttempts::Table)
.and_where(Expr::col(RegistrationAttempts::RandomId).eq(request.registration_key))
.to_string(DbQueryBuilder {});
sqlx::query(&delete_query).execute(&self.sql_pool).await?;
}
Ok(()) Ok(())
} }
} }
@ -341,7 +265,7 @@ mod tests {
)?; )?;
opaque_handler opaque_handler
.login_finish(ClientLoginFinishRequest { .login_finish(ClientLoginFinishRequest {
login_key: start_response.login_key, server_data: start_response.server_data,
credential_finalization: login_finish.message, credential_finalization: login_finish.message,
}) })
.await?; .await?;
@ -370,7 +294,7 @@ mod tests {
)?; )?;
opaque_handler opaque_handler
.registration_finish(ClientRegistrationFinishRequest { .registration_finish(ClientRegistrationFinishRequest {
registration_key: start_response.registration_key, server_data: start_response.server_data,
registration_upload: registration_finish.message, registration_upload: registration_finish.message,
}) })
.await .await

View File

@ -34,27 +34,6 @@ pub enum Memberships {
GroupId, GroupId,
} }
/// Contains the temporary data that needs to be kept between the first and second message when
/// logging in with the OPAQUE protocol.
#[derive(Iden)]
pub enum LoginAttempts {
Table,
RandomId,
UserId,
ServerLoginData,
Timestamp,
}
/// Same for registration.
#[derive(Iden)]
pub enum RegistrationAttempts {
Table,
RandomId,
UserId,
ServerRegistrationData,
Timestamp,
}
pub async fn init_table(pool: &Pool) -> sqlx::Result<()> { pub async fn init_table(pool: &Pool) -> sqlx::Result<()> {
// SQLite needs this pragma to be turned on. Other DB might not understand this, so ignore the // SQLite needs this pragma to be turned on. Other DB might not understand this, so ignore the
// error. // error.
@ -135,80 +114,6 @@ pub async fn init_table(pool: &Pool) -> sqlx::Result<()> {
.execute(pool) .execute(pool)
.await?; .await?;
sqlx::query(
&Table::create()
.table(LoginAttempts::Table)
.if_not_exists()
.col(
ColumnDef::new(LoginAttempts::RandomId)
.string_len(32)
.primary_key(),
)
.col(
ColumnDef::new(LoginAttempts::UserId)
.string_len(255)
.not_null(),
)
.col(
ColumnDef::new(LoginAttempts::ServerLoginData)
.binary()
.not_null(),
)
.col(
ColumnDef::new(LoginAttempts::Timestamp)
.date_time()
.not_null(),
)
.foreign_key(
ForeignKey::create()
.name("LoginAttemptsUserIdForeignKey")
.table(LoginAttempts::Table, Users::Table)
.col(LoginAttempts::UserId, Users::UserId)
.on_delete(ForeignKeyAction::Cascade)
.on_update(ForeignKeyAction::Cascade),
)
.to_string(DbQueryBuilder {}),
)
.execute(pool)
.await?;
sqlx::query(
&Table::create()
.table(RegistrationAttempts::Table)
.if_not_exists()
.col(
ColumnDef::new(RegistrationAttempts::RandomId)
.string_len(32)
.primary_key(),
)
.col(
ColumnDef::new(RegistrationAttempts::UserId)
.string_len(255)
.not_null(),
)
.col(
ColumnDef::new(RegistrationAttempts::ServerRegistrationData)
.binary()
.not_null(),
)
.col(
ColumnDef::new(RegistrationAttempts::Timestamp)
.date_time()
.not_null(),
)
.foreign_key(
ForeignKey::create()
.name("RegistrationAttemptsUserIdForeignKey")
.table(RegistrationAttempts::Table, Users::Table)
.col(RegistrationAttempts::UserId, Users::UserId)
.on_delete(ForeignKeyAction::Cascade)
.on_update(ForeignKeyAction::Cascade),
)
.to_string(DbQueryBuilder {}),
)
.execute(pool)
.await?;
Ok(()) Ok(())
} }

View File

@ -3,7 +3,7 @@ use figment::{
providers::{Env, Format, Serialized, Toml}, providers::{Env, Format, Serialized, Toml},
Figment, Figment,
}; };
use lldap_model::{opaque, opaque::KeyPair}; use lldap_model::opaque::{server::ServerSetup, KeyPair};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::infra::cli::CLIOpts; use crate::infra::cli::CLIOpts;
@ -27,18 +27,18 @@ pub struct Configuration {
pub key_file: String, pub key_file: String,
#[serde(skip)] #[serde(skip)]
#[builder(field(private), setter(strip_option))] #[builder(field(private), setter(strip_option))]
server_keys: Option<KeyPair>, server_setup: Option<ServerSetup>,
} }
impl ConfigurationBuilder { impl ConfigurationBuilder {
#[cfg(test)] #[cfg(test)]
pub fn build(self) -> Result<Configuration> { pub fn build(self) -> Result<Configuration> {
let server_keys = get_server_keys(self.key_file.as_deref().unwrap_or("server_key"))?; let server_setup = get_server_setup(self.key_file.as_deref().unwrap_or("server_key"))?;
Ok(self.server_keys(server_keys).private_build()?) Ok(self.server_setup(server_setup).private_build()?)
} }
fn validate(&self) -> Result<(), String> { fn validate(&self) -> Result<(), String> {
if self.server_keys.is_none() { if self.server_setup.is_none() {
Err("Don't use `private_build`, use `build` instead".to_string()) Err("Don't use `private_build`, use `build` instead".to_string())
} else { } else {
Ok(()) Ok(())
@ -47,8 +47,12 @@ impl ConfigurationBuilder {
} }
impl Configuration { impl Configuration {
pub fn get_server_setup(&self) -> &ServerSetup {
self.server_setup.as_ref().unwrap()
}
pub fn get_server_keys(&self) -> &KeyPair { pub fn get_server_keys(&self) -> &KeyPair {
self.server_keys.as_ref().unwrap() self.get_server_setup().keypair()
} }
fn merge_with_cli(mut self: Configuration, cli_opts: CLIOpts) -> Configuration { fn merge_with_cli(mut self: Configuration, cli_opts: CLIOpts) -> Configuration {
@ -80,30 +84,29 @@ impl Configuration {
database_url: String::from("sqlite://users.db?mode=rwc"), database_url: String::from("sqlite://users.db?mode=rwc"),
verbose: false, verbose: false,
key_file: String::from("server_key"), key_file: String::from("server_key"),
server_keys: None, server_setup: None,
} }
} }
} }
fn get_server_keys(file_path: &str) -> Result<KeyPair> { fn get_server_setup(file_path: &str) -> Result<ServerSetup> {
use opaque_ke::ciphersuite::CipherSuite;
use std::path::Path; use std::path::Path;
let path = Path::new(file_path); let path = Path::new(file_path);
if path.exists() { if path.exists() {
let bytes = std::fs::read(file_path) let bytes = std::fs::read(file_path)
.map_err(|e| anyhow!("Could not read key file `{}`: {}", file_path, e))?; .map_err(|e| anyhow!("Could not read key file `{}`: {}", file_path, e))?;
Ok(KeyPair::from_private_key_slice(&bytes)?) Ok(ServerSetup::deserialize(&bytes)?)
} else { } else {
let mut rng = rand::rngs::OsRng; let mut rng = rand::rngs::OsRng;
let keypair = opaque::DefaultSuite::generate_random_keypair(&mut rng); let server_setup = ServerSetup::new(&mut rng);
std::fs::write(path, keypair.private().as_slice()).map_err(|e| { std::fs::write(path, server_setup.serialize()).map_err(|e| {
anyhow!( anyhow!(
"Could not write the generated server keys to file `{}`: {}", "Could not write the generated server setup to file `{}`: {}",
file_path, file_path,
e e
) )
})?; })?;
Ok(keypair) Ok(server_setup)
} }
} }
@ -116,6 +119,6 @@ pub fn init(cli_opts: CLIOpts) -> Result<Configuration> {
.extract()?; .extract()?;
let mut config = config.merge_with_cli(cli_opts); let mut config = config.merge_with_cli(cli_opts);
config.server_keys = Some(get_server_keys(&config.key_file)?); config.server_setup = Some(get_server_setup(&config.key_file)?);
Ok(config) Ok(config)
} }

View File

@ -1,5 +1,5 @@
use crate::{ use crate::{
domain::sql_tables::{DbQueryBuilder, LoginAttempts, Pool, RegistrationAttempts}, domain::sql_tables::{DbQueryBuilder, Pool},
infra::jwt_sql_tables::{JwtRefreshStorage, JwtStorage}, infra::jwt_sql_tables::{JwtRefreshStorage, JwtStorage},
}; };
use actix::prelude::*; use actix::prelude::*;
@ -70,34 +70,6 @@ impl Scheduler {
{ {
log::error!("DB error while cleaning up JWT storage: {}", e); log::error!("DB error while cleaning up JWT storage: {}", e);
}; };
if let Err(e) = sqlx::query(
&Query::delete()
.from_table(LoginAttempts::Table)
.and_where(
Expr::col(LoginAttempts::Timestamp)
.lt(Local::now().naive_utc() - chrono::Duration::minutes(5)),
)
.to_string(DbQueryBuilder {}),
)
.execute(&sql_pool)
.await
{
log::error!("DB error while cleaning up login attempts: {}", e);
};
if let Err(e) = sqlx::query(
&Query::delete()
.from_table(RegistrationAttempts::Table)
.and_where(
Expr::col(RegistrationAttempts::Timestamp)
.lt(Local::now().naive_utc() - chrono::Duration::minutes(5)),
)
.to_string(DbQueryBuilder {}),
)
.execute(&sql_pool)
.await
{
log::error!("DB error while cleaning up registration attempts: {}", e);
};
log::info!("DB cleaned!"); log::info!("DB cleaned!");
} }

View File

@ -32,8 +32,11 @@ pub(crate) fn error_to_http_response(error: DomainError) -> HttpResponse {
DomainError::AuthenticationError(_) | DomainError::AuthenticationProtocolError(_) => { DomainError::AuthenticationError(_) | DomainError::AuthenticationProtocolError(_) => {
HttpResponse::Unauthorized() HttpResponse::Unauthorized()
} }
DomainError::DatabaseError(_) | DomainError::InternalError(_) => { DomainError::DatabaseError(_)
HttpResponse::InternalServerError() | DomainError::InternalError(_)
| DomainError::UnknownCryptoError(_) => HttpResponse::InternalServerError(),
DomainError::Base64DecodeError(_) | DomainError::BinarySerializationError(_) => {
HttpResponse::BadRequest()
} }
} }
.body(error.to_string()) .body(error.to_string())