use capnp::capability::Promise; use opaque_ke::{ CredentialFinalization, CredentialRequest, RegistrationRequest, RegistrationUpload, ServerLogin, ServerRegistration, }; use quicproquo_core::opaque_auth::OpaqueSuite; use quicproquo_proto::node_capnp::node_service; use crate::auth::{coded_error, current_timestamp, PendingLogin, SESSION_TTL_SECS}; use crate::error_codes::*; use crate::metrics; use crate::storage::StorageError; use crate::hooks::AuthEvent; use super::NodeServiceImpl; // Audit events in this module must never include secrets (no session tokens, passwords, or raw keys). fn storage_err(err: StorageError) -> capnp::Error { coded_error(E009_STORAGE_ERROR, err) } /// Parse username from Cap'n Proto reader; requires valid UTF-8. fn parse_username_param( result: Result, capnp::Error>, ) -> Result { let reader = result.map_err(|e| coded_error(E020_BAD_PARAMS, e))?; reader .to_string() .map_err(|_| coded_error(E020_BAD_PARAMS, "username must be valid UTF-8")) } impl NodeServiceImpl { pub fn handle_opaque_login_start( &mut self, params: node_service::OpaqueLoginStartParams, mut results: node_service::OpaqueLoginStartResults, ) -> Promise<(), capnp::Error> { let p = match params.get() { Ok(p) => p, Err(e) => return Promise::err(coded_error(E020_BAD_PARAMS, e)), }; let username = match parse_username_param(p.get_username()) { Ok(s) => s, Err(e) => return Promise::err(e), }; let request_bytes = match p.get_request() { Ok(v) => v.to_vec(), Err(e) => return Promise::err(coded_error(E020_BAD_PARAMS, e)), }; if username.is_empty() { return Promise::err(coded_error(E011_USERNAME_EMPTY, "username must not be empty")); } // Check for existing recent pending login before expensive OPAQUE/storage work (DoS mitigation). if let Some(existing) = self.pending_logins.get(&username) { let age = current_timestamp().saturating_sub(existing.created_at); if age < 60 { return Promise::err(coded_error(E010_OPAQUE_ERROR, "login already in progress")); } } let credential_request = match CredentialRequest::::deserialize(&request_bytes) { Ok(r) => r, Err(e) => { return Promise::err(coded_error( E010_OPAQUE_ERROR, format!("invalid credential request: {e}"), )) } }; let password_file = match self.store.get_user_record(&username) { Ok(Some(bytes)) => match ServerRegistration::::deserialize(&bytes) { Ok(pf) => Some(pf), Err(e) => { return Promise::err(coded_error( E010_OPAQUE_ERROR, format!("corrupt user record: {e}"), )) } }, Ok(None) => None, Err(e) => return Promise::err(storage_err(e)), }; let mut rng = rand::rngs::OsRng; let result = match ServerLogin::::start( &mut rng, &self.opaque_setup, password_file, credential_request, username.as_bytes(), Default::default(), ) { Ok(r) => r, Err(e) => { return Promise::err(coded_error( E010_OPAQUE_ERROR, format!("OPAQUE login start failed: {e}"), )) } }; let state_bytes = result.state.serialize().to_vec(); self.pending_logins.insert( username.clone(), PendingLogin { state_bytes, created_at: current_timestamp(), }, ); let response_bytes = result.message.serialize(); results.get().set_response(&response_bytes); tracing::info!(user = %username, "OPAQUE login started"); Promise::ok(()) } pub fn handle_opaque_register_start( &mut self, params: node_service::OpaqueRegisterStartParams, mut results: node_service::OpaqueRegisterStartResults, ) -> Promise<(), capnp::Error> { let p = match params.get() { Ok(p) => p, Err(e) => return Promise::err(coded_error(E020_BAD_PARAMS, e)), }; let username = match parse_username_param(p.get_username()) { Ok(s) => s, Err(e) => return Promise::err(e), }; let request_bytes = match p.get_request() { Ok(v) => v.to_vec(), Err(e) => return Promise::err(coded_error(E020_BAD_PARAMS, e)), }; if username.is_empty() { return Promise::err(coded_error(E011_USERNAME_EMPTY, "username must not be empty")); } if let Ok(true) = self.store.has_user_record(&username) { return Promise::err(coded_error( E018_USER_EXISTS, format!("user '{}' already registered", username), )); } let registration_request = match RegistrationRequest::::deserialize(&request_bytes) { Ok(r) => r, Err(e) => { return Promise::err(coded_error( E010_OPAQUE_ERROR, format!("invalid registration request: {e}"), )) } }; let result = match ServerRegistration::::start( &self.opaque_setup, registration_request, username.as_bytes(), ) { Ok(r) => r, Err(e) => { return Promise::err(coded_error( E010_OPAQUE_ERROR, format!("OPAQUE registration start failed: {e}"), )) } }; let response_bytes = result.message.serialize(); results.get().set_response(&response_bytes); tracing::info!(user = %username, "OPAQUE registration started"); Promise::ok(()) } pub fn handle_opaque_login_finish( &mut self, params: node_service::OpaqueLoginFinishParams, mut results: node_service::OpaqueLoginFinishResults, ) -> Promise<(), capnp::Error> { let p = match params.get() { Ok(p) => p, Err(e) => return Promise::err(coded_error(E020_BAD_PARAMS, e)), }; let username = match parse_username_param(p.get_username()) { Ok(s) => s, Err(e) => return Promise::err(e), }; let finalization_bytes = match p.get_finalization() { Ok(v) => v.to_vec(), Err(e) => return Promise::err(coded_error(E020_BAD_PARAMS, e)), }; let identity_key = p.get_identity_key().unwrap_or_default().to_vec(); if username.is_empty() { return Promise::err(coded_error(E011_USERNAME_EMPTY, "username must not be empty")); } let pending = match self.pending_logins.remove(&username) { Some((_, pl)) => pl, None => { // Audit: login failure — do not log secrets (no token, no password). tracing::warn!(user = %username, "audit: auth login failure (no pending login)"); metrics::record_auth_login_failure_total(); self.hooks.on_auth(&AuthEvent { username: username.clone(), success: false, failure_reason: "no pending login".to_string(), }); return Promise::err(coded_error(E019_NO_PENDING_LOGIN, "no pending login for this username")) } }; let server_login = match ServerLogin::::deserialize(&pending.state_bytes) { Ok(s) => s, Err(e) => { return Promise::err(coded_error( E010_OPAQUE_ERROR, format!("corrupt login state: {e}"), )) } }; let finalization = match CredentialFinalization::::deserialize(&finalization_bytes) { Ok(f) => f, Err(e) => { return Promise::err(coded_error( E010_OPAQUE_ERROR, format!("invalid credential finalization: {e}"), )) } }; let _result = match server_login.finish(finalization, Default::default()) { Ok(r) => r, Err(e) => { tracing::warn!(user = %username, "audit: auth login failure (OPAQUE finish failed)"); metrics::record_auth_login_failure_total(); self.hooks.on_auth(&AuthEvent { username: username.clone(), success: false, failure_reason: format!("OPAQUE finish failed: {e}"), }); return Promise::err(coded_error( E010_OPAQUE_ERROR, format!("OPAQUE login finish failed (bad password?): {e}"), )) } }; if identity_key.is_empty() { metrics::record_auth_login_failure_total(); return Promise::err(coded_error( E016_IDENTITY_MISMATCH, "identity key required to bind session token", )); } if let Ok(Some(stored_ik)) = self.store.get_user_identity_key(&username) { if stored_ik != identity_key { tracing::warn!(user = %username, "audit: auth login failure (identity mismatch)"); metrics::record_auth_login_failure_total(); self.hooks.on_auth(&AuthEvent { username: username.clone(), success: false, failure_reason: "identity key mismatch".to_string(), }); return Promise::err(coded_error( E016_IDENTITY_MISMATCH, "identity key does not match registered key", )); } } let mut token = [0u8; 32]; rand::RngCore::fill_bytes(&mut rand::rngs::OsRng, &mut token); let token_vec = token.to_vec(); let now = current_timestamp(); self.sessions.insert( token_vec.clone(), crate::auth::SessionInfo { username: username.clone(), identity_key, created_at: now, expires_at: now + SESSION_TTL_SECS, }, ); results.get().set_session_token(&token_vec); // Hook: on_auth — fires after successful login. self.hooks.on_auth(&AuthEvent { username: username.clone(), success: true, failure_reason: String::new(), }); // Audit: login success — do not log session token or any secrets. metrics::record_auth_login_success_total(); tracing::info!(user = %username, "audit: auth login success — session token issued"); Promise::ok(()) } pub fn handle_opaque_register_finish( &mut self, params: node_service::OpaqueRegisterFinishParams, mut results: node_service::OpaqueRegisterFinishResults, ) -> Promise<(), capnp::Error> { let p = match params.get() { Ok(p) => p, Err(e) => return Promise::err(coded_error(E020_BAD_PARAMS, e)), }; let username = match parse_username_param(p.get_username()) { Ok(s) => s, Err(e) => return Promise::err(e), }; let upload_bytes = match p.get_upload() { Ok(v) => v.to_vec(), Err(e) => return Promise::err(coded_error(E020_BAD_PARAMS, e)), }; let identity_key = p.get_identity_key().unwrap_or_default().to_vec(); if username.is_empty() { return Promise::err(coded_error(E011_USERNAME_EMPTY, "username must not be empty")); } match self.store.has_user_record(&username) { Ok(true) => { return Promise::err(coded_error( E018_USER_EXISTS, format!("user '{}' already registered", username), )) } Err(e) => return Promise::err(storage_err(e)), _ => {} } let upload = match RegistrationUpload::::deserialize(&upload_bytes) { Ok(u) => u, Err(e) => { return Promise::err(coded_error( E010_OPAQUE_ERROR, format!("invalid registration upload: {e}"), )) } }; let password_file = ServerRegistration::::finish(upload); let record_bytes = password_file.serialize().to_vec(); match self .store .store_user_record(&username, record_bytes) { Ok(()) => {} Err(crate::storage::StorageError::DuplicateUser(_)) => { return Promise::err(coded_error( E018_USER_EXISTS, format!("user '{}' already registered", username), )) } Err(e) => return Promise::err(storage_err(e)), } // Hook: on_user_registered — fires after successful registration. self.hooks.on_user_registered(&username, &identity_key); if !identity_key.is_empty() { if let Err(e) = self .store .store_user_identity_key(&username, identity_key.clone()) .map_err(storage_err) { return Promise::err(e); } // Append (username, identity_key) to the Key Transparency Merkle log. match self.kt_log.lock() { Ok(mut log) => { log.append(&username, &identity_key); // Persist after each append (small extra cost, but ensures durability). match log.to_bytes() { Ok(bytes) => { if let Err(e) = self.store.save_kt_log(bytes) { tracing::warn!(user = %username, error = %e, "KT log persist failed"); } } Err(e) => { tracing::warn!(user = %username, error = %e, "KT log serialise failed"); } } tracing::info!(user = %username, tree_size = log.len(), "KT: appended identity binding"); } Err(e) => { tracing::warn!(user = %username, error = %e, "KT log lock poisoned; skipping append"); } } } results.get().set_success(true); tracing::info!(user = %username, "OPAQUE registration complete"); Promise::ok(()) } }