diff --git a/crates/lib/Cargo.toml b/crates/lib/Cargo.toml index a855fcd7..df439514 100644 --- a/crates/lib/Cargo.toml +++ b/crates/lib/Cargo.toml @@ -83,6 +83,9 @@ regex = "1.11.1" [features] default = [] docs = [] +# WARNING: unsafe-debug enables verbose error/debug output that may expose sensitive data +# NEVER use this feature in production environments +unsafe-debug = [] [dev-dependencies] tempfile = "3.2" diff --git a/crates/lib/src/cache.rs b/crates/lib/src/cache.rs index 8a0ee459..d4711b4a 100644 --- a/crates/lib/src/cache.rs +++ b/crates/lib/src/cache.rs @@ -5,7 +5,7 @@ use solana_client::nonblocking::rpc_client::RpcClient; use solana_sdk::{account::Account, pubkey::Pubkey}; use tokio::sync::OnceCell; -use crate::error::KoraError; +use crate::{error::KoraError, sanitize_error}; #[cfg(not(test))] use crate::state::get_config; @@ -38,20 +38,29 @@ impl CacheUtil { let cfg = deadpool_redis::Config::from_url(redis_url); let pool = cfg.create_pool(Some(Runtime::Tokio1)).map_err(|e| { - KoraError::InternalServerError(format!("Failed to create cache pool: {e}")) + KoraError::InternalServerError(format!( + "Failed to create cache pool: {}", + sanitize_error!(e) + )) })?; // Test connection let mut conn = pool.get().await.map_err(|e| { - KoraError::InternalServerError(format!("Failed to connect to cache: {e}")) + KoraError::InternalServerError(format!( + "Failed to connect to cache: {}", + sanitize_error!(e) + )) })?; // Simple connection test - try to get a non-existent key let _: Option = conn.get("__connection_test__").await.map_err(|e| { - KoraError::InternalServerError(format!("Cache connection test failed: {e}")) + KoraError::InternalServerError(format!( + "Cache connection test failed: {}", + sanitize_error!(e) + )) })?; - log::info!("Cache initialized successfully with Redis at {redis_url}"); + log::info!("Cache initialized successfully"); Some(pool) } else { @@ -68,7 +77,10 @@ impl CacheUtil { async fn get_connection(pool: &Pool) -> Result { pool.get().await.map_err(|e| { - KoraError::InternalServerError(format!("Failed to get cache connection: {e}")) + KoraError::InternalServerError(format!( + "Failed to get cache connection: {}", + sanitize_error!(e) + )) }) } @@ -100,7 +112,10 @@ impl CacheUtil { let mut conn = Self::get_connection(pool).await?; let cached_data: Option = conn.get(key).await.map_err(|e| { - KoraError::InternalServerError(format!("Failed to get from cache: {e}")) + KoraError::InternalServerError(format!( + "Failed to get from cache: {}", + sanitize_error!(e) + )) })?; match cached_data { @@ -147,11 +162,17 @@ impl CacheUtil { let mut conn = Self::get_connection(pool).await?; let serialized = serde_json::to_string(data).map_err(|e| { - KoraError::InternalServerError(format!("Failed to serialize cache data: {e}")) + KoraError::InternalServerError(format!( + "Failed to serialize cache data: {}", + sanitize_error!(e) + )) })?; conn.set_ex::<_, _, ()>(key, serialized, ttl_seconds).await.map_err(|e| { - KoraError::InternalServerError(format!("Failed to set cache data: {e}")) + KoraError::InternalServerError(format!( + "Failed to set cache data: {}", + sanitize_error!(e) + )) })?; Ok(()) diff --git a/crates/lib/src/config.rs b/crates/lib/src/config.rs index 9f75cf4a..a6dd0725 100644 --- a/crates/lib/src/config.rs +++ b/crates/lib/src/config.rs @@ -16,9 +16,10 @@ use crate::{ error::KoraError, fee::price::{PriceConfig, PriceModel}, oracle::PriceSource, + sanitize_error, }; -#[derive(Debug, Clone, Deserialize)] +#[derive(Clone, Deserialize)] pub struct Config { pub validation: ValidationConfig, pub kora: KoraConfig, @@ -26,7 +27,7 @@ pub struct Config { pub metrics: MetricsConfig, } -#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[derive(Clone, Serialize, Deserialize, ToSchema)] pub struct MetricsConfig { pub enabled: bool, pub endpoint: String, @@ -48,7 +49,7 @@ impl Default for MetricsConfig { } } -#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[derive(Clone, Serialize, Deserialize, ToSchema)] pub struct FeePayerBalanceMetricsConfig { pub enabled: bool, pub expiry_seconds: u64, @@ -307,7 +308,7 @@ fn default_max_request_body_size() -> usize { DEFAULT_MAX_REQUEST_BODY_SIZE } -#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[derive(Clone, Serialize, Deserialize, ToSchema)] pub struct CacheConfig { /// Redis URL for caching (e.g., "redis://localhost:6379") pub url: Option, @@ -330,7 +331,7 @@ impl Default for CacheConfig { } } -#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[derive(Clone, Serialize, Deserialize, ToSchema)] pub struct KoraConfig { pub rate_limit: u64, #[serde(default = "default_max_request_body_size")] @@ -361,7 +362,7 @@ impl Default for KoraConfig { } } -#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[derive(Clone, Serialize, Deserialize, ToSchema)] pub struct UsageLimitConfig { /// Enable per-wallet usage limiting pub enabled: bool, @@ -384,7 +385,7 @@ impl Default for UsageLimitConfig { } } -#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[derive(Clone, Serialize, Deserialize, ToSchema)] pub struct AuthConfig { pub api_key: Option, pub hmac_secret: Option, @@ -401,16 +402,25 @@ impl Default for AuthConfig { impl Config { pub fn load_config>(path: P) -> Result { let contents = fs::read_to_string(path).map_err(|e| { - KoraError::InternalServerError(format!("Failed to read config file: {e}")) + KoraError::InternalServerError(format!( + "Failed to read config file: {}", + sanitize_error!(e) + )) })?; let mut config: Config = toml::from_str(&contents).map_err(|e| { - KoraError::InternalServerError(format!("Failed to parse config file: {e}")) + KoraError::InternalServerError(format!( + "Failed to parse config file: {}", + sanitize_error!(e) + )) })?; // Initialize Token2022Config to parse and cache extensions config.validation.token_2022.initialize().map_err(|e| { - KoraError::InternalServerError(format!("Failed to initialize Token2022 config: {e}")) + KoraError::InternalServerError(format!( + "Failed to initialize Token2022 config: {}", + sanitize_error!(e) + )) })?; Ok(config) @@ -422,9 +432,7 @@ impl KoraConfig { pub fn get_payment_address(&self, signer_pubkey: &Pubkey) -> Result { if let Some(payment_address_str) = &self.payment_address { let payment_address = Pubkey::from_str(payment_address_str).map_err(|_| { - KoraError::InternalServerError(format!( - "Invalid payment_address: {payment_address_str}" - )) + KoraError::InternalServerError("Invalid payment_address format".to_string()) })?; Ok(payment_address) } else { diff --git a/crates/lib/src/error.rs b/crates/lib/src/error.rs index 4a55079f..a291ee7a 100644 --- a/crates/lib/src/error.rs +++ b/crates/lib/src/error.rs @@ -1,3 +1,4 @@ +use crate::sanitize::sanitize_message; use jsonrpsee::{core::Error as RpcError, types::error::CallError}; use serde::{Deserialize, Serialize}; use solana_client::client_error::ClientError; @@ -69,61 +70,132 @@ pub enum KoraError { impl From for KoraError { fn from(e: ClientError) -> Self { let error_string = e.to_string(); + let sanitized_error_string = sanitize_message(&error_string); if error_string.contains("AccountNotFound") || error_string.contains("could not find account") { - KoraError::AccountNotFound(error_string) + #[cfg(feature = "unsafe-debug")] + { + KoraError::AccountNotFound(error_string) + } + #[cfg(not(feature = "unsafe-debug"))] + { + KoraError::AccountNotFound(sanitized_error_string) + } } else { - KoraError::RpcError(error_string) + #[cfg(feature = "unsafe-debug")] + { + KoraError::RpcError(error_string) + } + #[cfg(not(feature = "unsafe-debug"))] + { + KoraError::RpcError(sanitized_error_string) + } } } } impl From for KoraError { - fn from(e: SignerError) -> Self { - KoraError::SigningError(e.to_string()) + fn from(_e: SignerError) -> Self { + #[cfg(feature = "unsafe-debug")] + { + KoraError::SigningError(_e.to_string()) + } + #[cfg(not(feature = "unsafe-debug"))] + { + KoraError::SigningError(sanitize_message(&_e.to_string())) + } } } impl From for KoraError { - fn from(e: bincode::Error) -> Self { - KoraError::SerializationError(e.to_string()) + fn from(_e: bincode::Error) -> Self { + #[cfg(feature = "unsafe-debug")] + { + KoraError::SerializationError(_e.to_string()) + } + #[cfg(not(feature = "unsafe-debug"))] + { + KoraError::SerializationError(sanitize_message(&_e.to_string())) + } } } impl From for KoraError { - fn from(e: bs58::decode::Error) -> Self { - KoraError::SerializationError(e.to_string()) + fn from(_e: bs58::decode::Error) -> Self { + #[cfg(feature = "unsafe-debug")] + { + KoraError::SerializationError(_e.to_string()) + } + #[cfg(not(feature = "unsafe-debug"))] + { + KoraError::SerializationError(sanitize_message(&_e.to_string())) + } } } impl From for KoraError { - fn from(e: bs58::encode::Error) -> Self { - KoraError::SerializationError(e.to_string()) + fn from(_e: bs58::encode::Error) -> Self { + #[cfg(feature = "unsafe-debug")] + { + KoraError::SerializationError(_e.to_string()) + } + #[cfg(not(feature = "unsafe-debug"))] + { + KoraError::SerializationError(sanitize_message(&_e.to_string())) + } } } impl From for KoraError { - fn from(e: std::io::Error) -> Self { - KoraError::InternalServerError(e.to_string()) + fn from(_e: std::io::Error) -> Self { + #[cfg(feature = "unsafe-debug")] + { + KoraError::InternalServerError(_e.to_string()) + } + #[cfg(not(feature = "unsafe-debug"))] + { + KoraError::InternalServerError(sanitize_message(&_e.to_string())) + } } } impl From> for KoraError { - fn from(e: Box) -> Self { - KoraError::InternalServerError(e.to_string()) + fn from(_e: Box) -> Self { + #[cfg(feature = "unsafe-debug")] + { + KoraError::InternalServerError(_e.to_string()) + } + #[cfg(not(feature = "unsafe-debug"))] + { + KoraError::InternalServerError(sanitize_message(&_e.to_string())) + } } } impl From> for KoraError { - fn from(e: Box) -> Self { - KoraError::InternalServerError(e.to_string()) + fn from(_e: Box) -> Self { + #[cfg(feature = "unsafe-debug")] + { + KoraError::InternalServerError(_e.to_string()) + } + #[cfg(not(feature = "unsafe-debug"))] + { + KoraError::InternalServerError(sanitize_message(&_e.to_string())) + } } } impl From for KoraError { - fn from(err: ProgramError) -> Self { - KoraError::InvalidTransaction(err.to_string()) + fn from(_err: ProgramError) -> Self { + #[cfg(feature = "unsafe-debug")] + { + KoraError::InvalidTransaction(_err.to_string()) + } + #[cfg(not(feature = "unsafe-debug"))] + { + KoraError::InvalidTransaction(sanitize_message(&_err.to_string())) + } } } @@ -191,14 +263,28 @@ impl> IntoKoraResponse for Result { } impl From for KoraError { - fn from(err: anyhow::Error) -> Self { - KoraError::SigningError(err.to_string()) + fn from(_err: anyhow::Error) -> Self { + #[cfg(feature = "unsafe-debug")] + { + KoraError::SigningError(_err.to_string()) + } + #[cfg(not(feature = "unsafe-debug"))] + { + KoraError::SigningError(sanitize_message(&_err.to_string())) + } } } impl From for KoraError { - fn from(err: solana_signers::SignerError) -> Self { - KoraError::SigningError(err.to_string()) + fn from(_err: solana_signers::SignerError) -> Self { + #[cfg(feature = "unsafe-debug")] + { + KoraError::SigningError(_err.to_string()) + } + #[cfg(not(feature = "unsafe-debug"))] + { + KoraError::SigningError(sanitize_message(&_err.to_string())) + } } } @@ -254,6 +340,7 @@ mod tests { let client_error = ClientError::from(std::io::Error::other("test")); let kora_error: KoraError = client_error.into(); assert!(matches!(kora_error, KoraError::RpcError(_))); + // With sanitization, error message context is preserved unless it contains sensitive data if let KoraError::RpcError(msg) = kora_error { assert!(msg.contains("test")); } @@ -264,6 +351,7 @@ mod tests { let signer_error = SignerError::Custom("signing failed".to_string()); let kora_error: KoraError = signer_error.into(); assert!(matches!(kora_error, KoraError::SigningError(_))); + // With sanitization, error message context is preserved unless it contains sensitive data if let KoraError::SigningError(msg) = kora_error { assert!(msg.contains("signing failed")); } @@ -295,6 +383,7 @@ mod tests { let io_error = std::io::Error::other("file not found"); let kora_error: KoraError = io_error.into(); assert!(matches!(kora_error, KoraError::InternalServerError(_))); + // With sanitization, error message context is preserved unless it contains sensitive data if let KoraError::InternalServerError(msg) = kora_error { assert!(msg.contains("file not found")); } @@ -331,6 +420,7 @@ mod tests { let anyhow_error = anyhow::anyhow!("something went wrong"); let kora_error: KoraError = anyhow_error.into(); assert!(matches!(kora_error, KoraError::SigningError(_))); + // With sanitization, error message context is preserved unless it contains sensitive data if let KoraError::SigningError(msg) = kora_error { assert!(msg.contains("something went wrong")); } diff --git a/crates/lib/src/mod.rs b/crates/lib/src/mod.rs index dc688bf2..e94d36e8 100644 --- a/crates/lib/src/mod.rs +++ b/crates/lib/src/mod.rs @@ -13,6 +13,7 @@ pub mod metrics; pub mod oracle; pub mod rpc; pub mod rpc_server; +pub mod sanitize; pub mod signer; pub mod state; pub mod token; diff --git a/crates/lib/src/oracle/jupiter.rs b/crates/lib/src/oracle/jupiter.rs index 0bc6e79e..8aa09f28 100644 --- a/crates/lib/src/oracle/jupiter.rs +++ b/crates/lib/src/oracle/jupiter.rs @@ -2,6 +2,7 @@ use super::{PriceOracle, PriceSource, TokenPrice}; use crate::{ constant::{JUPITER_API_LITE_URL, JUPITER_API_PRO_URL, SOL_MINT}, error::KoraError, + sanitize_error, }; use once_cell::sync::Lazy; use parking_lot::RwLock; @@ -120,10 +121,9 @@ impl JupiterPriceOracle { request = request.header(JUPITER_AUTH_HEADER, key); } - let response = request - .send() - .await - .map_err(|e| KoraError::RpcError(format!("Jupiter API request failed: {e}")))?; + let response = request.send().await.map_err(|e| { + KoraError::RpcError(format!("Jupiter API request failed: {}", sanitize_error!(e))) + })?; if !response.status().is_success() { match response.status() { @@ -139,10 +139,9 @@ impl JupiterPriceOracle { } } - let jupiter_response: JupiterResponse = response - .json() - .await - .map_err(|e| KoraError::RpcError(format!("Failed to parse Jupiter response: {e}")))?; + let jupiter_response: JupiterResponse = response.json().await.map_err(|e| { + KoraError::RpcError(format!("Failed to parse Jupiter response: {}", sanitize_error!(e))) + })?; let sol_price = jupiter_response .get(SOL_MINT) diff --git a/crates/lib/src/rpc_server/args.rs b/crates/lib/src/rpc_server/args.rs index a68a9530..926aed27 100644 --- a/crates/lib/src/rpc_server/args.rs +++ b/crates/lib/src/rpc_server/args.rs @@ -3,7 +3,7 @@ use clap::Parser; use std::path::PathBuf; /// RPC server arguments -#[derive(Debug, Parser)] +#[derive(Parser)] pub struct RpcArgs { /// HTTP port to listen on for RPC requests #[arg(short = 'p', long, default_value = "8080")] @@ -26,7 +26,7 @@ pub struct RpcArgs { pub auth_args: AuthArgs, } -#[derive(Debug, Parser)] +#[derive(Parser)] pub struct AuthArgs { /// API key for authenticating requests to the Kora server (optional) - can be set in `kora.toml` #[arg(long, env = "KORA_API_KEY", help_heading = "Authentication")] diff --git a/crates/lib/src/rpc_server/auth.rs b/crates/lib/src/rpc_server/auth.rs index 4c0e2b4b..4fa36105 100644 --- a/crates/lib/src/rpc_server/auth.rs +++ b/crates/lib/src/rpc_server/auth.rs @@ -180,8 +180,8 @@ where let mut mac = match Hmac::::new_from_slice(secret.as_bytes()) { Ok(mac) => mac, - Err(e) => { - log::error!("Invalid HMAC secret: {e:?}"); + Err(_) => { + log::error!("HMAC authentication failed"); return Ok(unauthorized_response); } }; diff --git a/crates/lib/src/sanitize.rs b/crates/lib/src/sanitize.rs new file mode 100644 index 00000000..6b1b3a16 --- /dev/null +++ b/crates/lib/src/sanitize.rs @@ -0,0 +1,91 @@ +//! Security-focused logging and error message sanitization +//! +//! This module provides utilities to automatically redact sensitive information +//! from error messages and logs, including: +//! - URLs with embedded credentials (any protocol: redis://, postgres://, http://, etc.) +//! - Long hex strings (potential private keys) + +use regex::Regex; +use std::sync::LazyLock; + +/// Regex patterns for detecting sensitive data +static URL_WITH_CREDENTIALS_PATTERN: LazyLock = LazyLock::new(|| { + // Generic URL pattern with embedded credentials: protocol://user:password@host + // Matches any protocol (redis, http, https, postgres, mysql, mongodb, etc.) + Regex::new(r"[a-z][a-z0-9+.-]*://[^:@\s]+:[^@\s]+@[^\s]+").unwrap() +}); + +static HEX_PATTERN: LazyLock = LazyLock::new(|| { + // Long hex strings (likely keys/hashes) - 32+ chars, with optional 0x prefix + Regex::new(r"(?:0x)?[0-9a-fA-F]{32,}").unwrap() +}); + +/// Sanitizes a message by redacting sensitive information +pub fn sanitize_message(message: &str) -> String { + let mut result = message.to_string(); + + result = URL_WITH_CREDENTIALS_PATTERN.replace_all(&result, "[REDACTED_URL]").to_string(); + + result = HEX_PATTERN.replace_all(&result, "[REDACTED_HEX]").to_string(); + + result +} + +/// Sanitizes an error message based on the `unsafe-debug` feature flag +/// +/// - With `unsafe-debug`: Returns the original error message +/// - Without `unsafe-debug`: Returns a sanitized version with sensitive data redacted +#[macro_export] +macro_rules! sanitize_error { + ($error:expr) => {{ + #[cfg(feature = "unsafe-debug")] + { + format!("{}", $error) + } + #[cfg(not(feature = "unsafe-debug"))] + { + $crate::sanitize::sanitize_message(&format!("{}", $error)) + } + }}; +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_sanitize_url_with_credentials_redis() { + let msg = "Failed to connect to redis://user:password@localhost:6379"; + let sanitized = sanitize_message(msg); + assert!(sanitized.contains("[REDACTED_URL]")); + assert!(!sanitized.contains("password")); + assert!(!sanitized.contains("redis://user:")); + // Ensure the error message context remains + assert!(sanitized.contains("Failed to connect to")); + } + + #[test] + fn test_sanitize_url_with_credentials_http() { + let msg = "Request failed: https://user:token@api.example.com/endpoint"; + let sanitized = sanitize_message(msg); + assert!(sanitized.contains("[REDACTED_URL]")); + assert!(!sanitized.contains("token")); + assert!(!sanitized.contains("https://user:")); + } + + #[test] + fn test_sanitize_url_with_credentials_postgres() { + let msg = "DB error: postgres://admin:secret123@db.internal:5432/mydb"; + let sanitized = sanitize_message(msg); + assert!(sanitized.contains("[REDACTED_URL]")); + assert!(!sanitized.contains("admin")); + assert!(!sanitized.contains("secret123")); + } + + #[test] + fn test_sanitize_hex_string() { + let msg = "Key: 0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"; + let sanitized = sanitize_message(msg); + assert!(sanitized.contains("[REDACTED_HEX]")); + } +} diff --git a/crates/lib/src/signer/config.rs b/crates/lib/src/signer/config.rs index c52f28dc..f20ccd0d 100644 --- a/crates/lib/src/signer/config.rs +++ b/crates/lib/src/signer/config.rs @@ -1,10 +1,10 @@ -use crate::{error::KoraError, signer::utils::get_env_var_for_signer}; +use crate::{error::KoraError, sanitize_error, signer::utils::get_env_var_for_signer}; use serde::{Deserialize, Serialize}; use solana_signers::Signer; use std::{fmt, fs, path::Path}; /// Configuration for a pool of signers -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Clone, Serialize, Deserialize)] pub struct SignerPoolConfig { /// Signer pool configuration pub signer_pool: SignerPoolSettings, @@ -45,7 +45,7 @@ fn default_strategy() -> SelectionStrategy { } /// Configuration for an individual signer -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Clone, Serialize, Deserialize)] pub struct SignerConfig { /// Human-readable name for this signer pub name: String, @@ -58,13 +58,13 @@ pub struct SignerConfig { } /// Memory signer configuration (local keypair) -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Clone, Serialize, Deserialize)] pub struct MemorySignerConfig { pub private_key_env: String, } /// Turnkey signer configuration -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Clone, Serialize, Deserialize)] pub struct TurnkeySignerConfig { pub api_public_key_env: String, pub api_private_key_env: String, @@ -74,7 +74,7 @@ pub struct TurnkeySignerConfig { } /// Privy signer configuration -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Clone, Serialize, Deserialize)] pub struct PrivySignerConfig { pub app_id_env: String, pub app_secret_env: String, @@ -82,7 +82,7 @@ pub struct PrivySignerConfig { } /// Vault signer configuration -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Clone, Serialize, Deserialize)] pub struct VaultSignerConfig { pub vault_addr_env: String, pub vault_token_env: String, @@ -91,7 +91,7 @@ pub struct VaultSignerConfig { } /// Signer type-specific configuration -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Clone, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "snake_case")] pub enum SignerTypeConfig { /// Memory signer configuration @@ -120,11 +120,17 @@ impl SignerPoolConfig { /// Load signer pool configuration from TOML file pub fn load_config>(path: P) -> Result { let contents = fs::read_to_string(path).map_err(|e| { - KoraError::InternalServerError(format!("Failed to read config file: {e}")) + KoraError::InternalServerError(format!( + "Failed to read signer config file: {}", + sanitize_error!(e) + )) })?; let config: SignerPoolConfig = toml::from_str(&contents).map_err(|e| { - KoraError::ValidationError(format!("Failed to parse signers config TOML: {e}")) + KoraError::ValidationError(format!( + "Failed to parse signers config TOML: {}", + sanitize_error!(e) + )) })?; config.validate_signer_config()?; @@ -210,7 +216,10 @@ impl SignerConfig { ) -> Result { let private_key = get_env_var_for_signer(&config.private_key_env, signer_name)?; Signer::from_memory(&private_key).map_err(|e| { - KoraError::SigningError(format!("Failed to create memory signer '{signer_name}': {e}")) + KoraError::SigningError(format!( + "Failed to create memory signer '{signer_name}': {}", + sanitize_error!(e) + )) }) } @@ -232,7 +241,10 @@ impl SignerConfig { public_key, ) .map_err(|e| { - KoraError::SigningError(format!("Failed to create Turnkey signer '{signer_name}': {e}")) + KoraError::SigningError(format!( + "Failed to create Turnkey signer '{signer_name}': {}", + sanitize_error!(e) + )) }) } @@ -245,7 +257,10 @@ impl SignerConfig { let wallet_id = get_env_var_for_signer(&config.wallet_id_env, signer_name)?; Signer::from_privy(app_id, app_secret, wallet_id).await.map_err(|e| { - KoraError::SigningError(format!("Failed to create Privy signer '{signer_name}': {e}")) + KoraError::SigningError(format!( + "Failed to create Privy signer '{signer_name}': {}", + sanitize_error!(e) + )) }) } @@ -259,7 +274,10 @@ impl SignerConfig { let pubkey = get_env_var_for_signer(&config.pubkey_env, signer_name)?; Signer::from_vault(vault_addr, vault_token, key_name, pubkey).map_err(|e| { - KoraError::SigningError(format!("Failed to create Vault signer '{signer_name}': {e}")) + KoraError::SigningError(format!( + "Failed to create Vault signer '{signer_name}': {}", + sanitize_error!(e) + )) }) } diff --git a/crates/lib/src/signer/keypair_util.rs b/crates/lib/src/signer/keypair_util.rs index 5fb7faf3..1f8343b3 100644 --- a/crates/lib/src/signer/keypair_util.rs +++ b/crates/lib/src/signer/keypair_util.rs @@ -1,4 +1,4 @@ -use crate::error::KoraError; +use crate::{error::KoraError, sanitize_error}; use serde_json; use solana_sdk::signature::Keypair; use std::fs; @@ -29,9 +29,9 @@ impl KeypairUtil { /// Creates a new keypair from a base58-encoded private key string with proper error handling pub fn from_base58_safe(private_key: &str) -> Result { // Try to decode as base58 first - let decoded = bs58::decode(private_key) - .into_vec() - .map_err(|e| KoraError::SigningError(format!("Invalid base58 string: {e}")))?; + let decoded = bs58::decode(private_key).into_vec().map_err(|e| { + KoraError::SigningError(format!("Invalid base58 string: {}", sanitize_error!(e))) + })?; if decoded.len() != 64 { return Err(KoraError::SigningError(format!( @@ -40,8 +40,9 @@ impl KeypairUtil { ))); } - let keypair = Keypair::try_from(&decoded[..]) - .map_err(|e| KoraError::SigningError(format!("Invalid private key bytes: {e}")))?; + let keypair = Keypair::try_from(&decoded[..]).map_err(|e| { + KoraError::SigningError(format!("Invalid private key bytes: {}", sanitize_error!(e))) + })?; Ok(keypair) } @@ -72,10 +73,17 @@ impl KeypairUtil { byte_array.len() ))); } - Keypair::try_from(&byte_array[..]) - .map_err(|e| KoraError::SigningError(format!("Invalid private key bytes: {e}"))) + Keypair::try_from(&byte_array[..]).map_err(|e| { + KoraError::SigningError(format!( + "Invalid private key bytes: {}", + sanitize_error!(e) + )) + }) } - Err(e) => Err(KoraError::SigningError(format!("Failed to parse U8Array: {e}"))), + Err(e) => Err(KoraError::SigningError(format!( + "Failed to parse U8Array: {}", + sanitize_error!(e) + ))), } } @@ -89,8 +97,12 @@ impl KeypairUtil { byte_array.len() ))); } - return Keypair::try_from(&byte_array[..]) - .map_err(|e| KoraError::SigningError(format!("Invalid private key bytes: {e}"))); + return Keypair::try_from(&byte_array[..]).map_err(|e| { + KoraError::SigningError(format!( + "Invalid private key bytes: {}", + sanitize_error!(e) + )) + }); } Err(KoraError::SigningError( diff --git a/crates/lib/src/usage_limit/usage_store.rs b/crates/lib/src/usage_limit/usage_store.rs index 68ad0523..5f59a82f 100644 --- a/crates/lib/src/usage_limit/usage_store.rs +++ b/crates/lib/src/usage_limit/usage_store.rs @@ -4,7 +4,7 @@ use async_trait::async_trait; use deadpool_redis::{Connection, Pool}; use redis::AsyncCommands; -use crate::error::KoraError; +use crate::{error::KoraError, sanitize_error}; /// Trait for storing and retrieving usage counts #[async_trait] @@ -31,7 +31,10 @@ impl RedisUsageStore { async fn get_connection(&self) -> Result { self.pool.get().await.map_err(|e| { - KoraError::InternalServerError(format!("Failed to get Redis connection: {e}")) + KoraError::InternalServerError(sanitize_error!(format!( + "Failed to get Redis connection: {}", + e + ))) }) } } @@ -41,7 +44,10 @@ impl UsageStore for RedisUsageStore { async fn increment(&self, key: &str) -> Result { let mut conn = self.get_connection().await?; let count: u32 = conn.incr(key, 1).await.map_err(|e| { - KoraError::InternalServerError(format!("Failed to increment usage for {key}: {e}")) + KoraError::InternalServerError(sanitize_error!(format!( + "Failed to increment usage for {}: {}", + key, e + ))) })?; Ok(count) } @@ -49,17 +55,19 @@ impl UsageStore for RedisUsageStore { async fn get(&self, key: &str) -> Result { let mut conn = self.get_connection().await?; let count: Option = conn.get(key).await.map_err(|e| { - KoraError::InternalServerError(format!("Failed to get usage for {key}: {e}")) + KoraError::InternalServerError(sanitize_error!(format!( + "Failed to get usage for {}: {}", + key, e + ))) })?; Ok(count.unwrap_or(0)) } async fn clear(&self) -> Result<(), KoraError> { let mut conn = self.get_connection().await?; - let _: () = conn - .flushdb() - .await - .map_err(|e| KoraError::InternalServerError(format!("Failed to clear Redis: {e}")))?; + let _: () = conn.flushdb().await.map_err(|e| { + KoraError::InternalServerError(sanitize_error!(format!("Failed to clear Redis: {}", e))) + })?; Ok(()) } } @@ -85,7 +93,10 @@ impl Default for InMemoryUsageStore { impl UsageStore for InMemoryUsageStore { async fn increment(&self, key: &str) -> Result { let mut data = self.data.lock().map_err(|e| { - KoraError::InternalServerError(format!("Failed to lock usage store: {e}")) + KoraError::InternalServerError(sanitize_error!(format!( + "Failed to lock usage store: {}", + e + ))) })?; let count = data.entry(key.to_string()).or_insert(0); *count += 1; @@ -94,14 +105,20 @@ impl UsageStore for InMemoryUsageStore { async fn get(&self, key: &str) -> Result { let data = self.data.lock().map_err(|e| { - KoraError::InternalServerError(format!("Failed to lock usage store: {e}")) + KoraError::InternalServerError(sanitize_error!(format!( + "Failed to lock usage store: {}", + e + ))) })?; Ok(data.get(key).copied().unwrap_or(0)) } async fn clear(&self) -> Result<(), KoraError> { let mut data = self.data.lock().map_err(|e| { - KoraError::InternalServerError(format!("Failed to lock usage store: {e}")) + KoraError::InternalServerError(sanitize_error!(format!( + "Failed to lock usage store: {}", + e + ))) })?; data.clear(); Ok(()) diff --git a/crates/lib/src/usage_limit/usage_tracker.rs b/crates/lib/src/usage_limit/usage_tracker.rs index 2ed432a8..cc771666 100644 --- a/crates/lib/src/usage_limit/usage_tracker.rs +++ b/crates/lib/src/usage_limit/usage_tracker.rs @@ -7,7 +7,7 @@ use solana_signers::SolanaSigner; use tokio::sync::OnceCell; use super::usage_store::{RedisUsageStore, UsageStore}; -use crate::{error::KoraError, get_all_signers}; +use crate::{error::KoraError, get_all_signers, sanitize_error}; #[cfg(not(test))] use crate::state::get_config; @@ -152,22 +152,30 @@ impl UsageTracker { let usage_limiter = if let Some(cache_url) = &config.kora.usage_limit.cache_url { let cfg = deadpool_redis::Config::from_url(cache_url); let pool = cfg.create_pool(Some(Runtime::Tokio1)).map_err(|e| { - KoraError::InternalServerError(format!("Failed to create Redis pool: {e}")) + KoraError::InternalServerError(format!( + "Failed to create Redis pool: {}", + sanitize_error!(e) + )) })?; // Test Redis connection let mut conn = pool.get().await.map_err(|e| { - KoraError::InternalServerError(format!("Failed to connect to Redis: {e}")) + KoraError::InternalServerError(format!( + "Failed to connect to Redis: {}", + sanitize_error!(e) + )) })?; // Simple connection test let _: Option = conn.get("__usage_limiter_test__").await.map_err(|e| { - KoraError::InternalServerError(format!("Redis connection test failed: {e}")) + KoraError::InternalServerError(format!( + "Redis connection test failed: {}", + sanitize_error!(e) + )) })?; log::info!( - "Usage limiter initialized with Redis at {} (max: {} transactions)", - cache_url, + "Usage limiter initialized with max {} transactions", config.kora.usage_limit.max_transactions ); diff --git a/crates/lib/src/validator/config_validator.rs b/crates/lib/src/validator/config_validator.rs index ba6e6378..530f0108 100644 --- a/crates/lib/src/validator/config_validator.rs +++ b/crates/lib/src/validator/config_validator.rs @@ -288,8 +288,6 @@ impl ConfigValidator { println!("=== Configuration Validation ==="); if errors.is_empty() { println!("āœ“ Configuration validation successful!"); - println!("\n=== Current Configuration ==="); - println!("{config:#?}"); } else { println!("āœ— Configuration validation failed!"); println!("\nāŒ Errors:");