use capnp::capability::Promise; use opaque_ke::{ CredentialFinalization, CredentialRequest, RegistrationRequest, RegistrationUpload, ServerLogin, ServerRegistration, }; use quicnprotochat_core::opaque_auth::OpaqueSuite; use quicnprotochat_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 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) } 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 p.get_username() { Ok(v) => v.to_string().unwrap_or_default().to_string(), Err(e) => return Promise::err(coded_error(E020_BAD_PARAMS, 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")); } 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) => { return Promise::err(coded_error(E010_OPAQUE_ERROR, "user not registered")) } 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 p.get_username() { Ok(v) => v.to_string().unwrap_or_default().to_string(), Err(e) => return Promise::err(coded_error(E020_BAD_PARAMS, 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 p.get_username() { Ok(v) => v.to_string().unwrap_or_default().to_string(), Err(e) => return Promise::err(coded_error(E020_BAD_PARAMS, 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(); 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(); 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(); 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); // 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 p.get_username() { Ok(v) => v.to_string().unwrap_or_default().to_string(), Err(e) => return Promise::err(coded_error(E020_BAD_PARAMS, 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")); } let _request = match RegistrationRequest::::deserialize(&upload_bytes) { Ok(r) => r, Err(e) => { return Promise::err(coded_error( E010_OPAQUE_ERROR, format!("invalid registration upload: {e}"), )) } }; 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(); if let Err(e) = self .store .store_user_record(&username, record_bytes) .map_err(storage_err) { return Promise::err(e); } if !identity_key.is_empty() { if let Err(e) = self .store .store_user_identity_key(&username, identity_key) .map_err(storage_err) { return Promise::err(e); } } results.get().set_success(true); tracing::info!(user = %username, "OPAQUE registration complete"); Promise::ok(()) } }