//! resolveUser / resolveIdentity RPCs: bidirectional username ↔ identity key lookup. use capnp::capability::Promise; use quicproquo_proto::node_capnp::node_service; use std::time::Duration; use tokio::time::Instant; use crate::auth::{check_rate_limit, coded_error, validate_auth_context}; use crate::error_codes::*; use crate::metrics; use crate::storage::StorageError; use super::NodeServiceImpl; /// Minimum response time for resolveUser to mask DB lookup timing differences. const RESOLVE_TIMING_FLOOR: Duration = Duration::from_millis(5); fn storage_err(err: StorageError) -> capnp::Error { coded_error(E009_STORAGE_ERROR, err) } impl NodeServiceImpl { pub fn handle_resolve_user( &mut self, params: node_service::ResolveUserParams, mut results: node_service::ResolveUserResults, ) -> 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(u) => u, Err(e) => return Promise::err(coded_error(E020_BAD_PARAMS, e)), }; let auth_ctx = match validate_auth_context(&self.auth_cfg, &self.sessions, p.get_auth()) { Ok(ctx) => ctx, Err(e) => return Promise::err(e), }; let username_str = match username.to_str() { Ok(s) => s, Err(e) => return Promise::err(coded_error(E020_BAD_PARAMS, e)), }; if username_str.is_empty() { return Promise::err(coded_error(E020_BAD_PARAMS, "username must not be empty")); } // Federation: parse user@domain format. let addr = crate::federation::address::FederatedAddress::parse(username_str); let is_remote = match (&addr.domain, &self.local_domain) { (Some(d), Some(ld)) => d != ld, (Some(_), None) => true, _ => false, }; if is_remote { // Proxy to remote server via federation. if let (Some(ref fed_client), Some(ref domain)) = (&self.federation_client, &addr.domain) { if fed_client.has_peer(domain) { let fed = fed_client.clone(); let user = addr.username.clone(); let dom = domain.clone(); return Promise::from_future(async move { match fed.proxy_resolve_user(&dom, &user).await { Ok(Some(key)) => { results.get().set_identity_key(&key); } Ok(None) => { // Not found on remote — return empty. } Err(e) => { tracing::warn!(error = %e, "federation proxy_resolve_user failed"); // Fall through — return empty (not found). } } Ok(()) }); } } // No federation client or unknown peer — return empty (not found). return Promise::ok(()); } // Rate-limit resolve requests to prevent bulk enumeration. if let Err(e) = check_rate_limit(&self.rate_limits, &auth_ctx.token) { tracing::warn!("rate_limit_hit"); metrics::record_rate_limit_hit_total(); return Promise::err(e); } // Timing floor: record the start time so we can pad the response to a // fixed minimum duration, masking DB-lookup timing differences between // existing and non-existing usernames. let deadline = Instant::now() + RESOLVE_TIMING_FLOOR; // Local resolution. let identity_key = match self.store.get_user_identity_key(&addr.username) { Ok(Some(key)) => key, Ok(None) => { // Return empty Data — caller checks length to detect "not found". // Pad to timing floor before responding. return Promise::from_future(async move { tokio::time::sleep_until(deadline).await; Ok(()) }); } Err(e) => return Promise::err(storage_err(e)), }; let mut r = results.get(); r.set_identity_key(&identity_key); // Attempt to include a KT Merkle inclusion proof. // Non-fatal: if the log is unavailable or has no entry, return just the key. if let Ok(log) = self.kt_log.lock() { if let Some(leaf_idx) = log.find(&addr.username, &identity_key) { match log.inclusion_proof(leaf_idx) { Ok(proof) => match proof.to_bytes() { Ok(bytes) => { r.set_inclusion_proof(&bytes); } Err(e) => { tracing::warn!(error = %e, "KT proof serialise failed"); } }, Err(e) => { tracing::warn!(error = %e, "KT inclusion_proof failed"); } } } } // Pad to timing floor before responding. Promise::from_future(async move { tokio::time::sleep_until(deadline).await; Ok(()) }) } pub fn handle_resolve_identity( &mut self, params: node_service::ResolveIdentityParams, mut results: node_service::ResolveIdentityResults, ) -> Promise<(), capnp::Error> { let p = match params.get() { Ok(p) => p, Err(e) => return Promise::err(coded_error(E020_BAD_PARAMS, e)), }; let identity_key = match p.get_identity_key() { Ok(v) => v, Err(e) => return Promise::err(coded_error(E020_BAD_PARAMS, e)), }; let _auth_ctx = match validate_auth_context(&self.auth_cfg, &self.sessions, p.get_auth()) { Ok(ctx) => ctx, Err(e) => return Promise::err(e), }; if identity_key.len() != 32 { return Promise::err(coded_error( E004_IDENTITY_KEY_LENGTH, format!("identityKey must be exactly 32 bytes, got {}", identity_key.len()), )); } match self.store.resolve_identity_key(identity_key) { Ok(Some(username)) => { results.get().set_username(&username); } Ok(None) => { // Return empty string — caller checks length to detect "not found". } Err(e) => return Promise::err(storage_err(e)), } Promise::ok(()) } }