rusty_ems/auth/
utils.rs

1//! Authentication utilities and helper functions
2
3use super::adapters::{
4    BinanceAuthAdapter, BithumbAuthAdapter, BybitAuthAdapter, CoinbaseAuthAdapter, UpbitAuthAdapter,
5};
6use super::traits::AuthenticationManager;
7use crate::error::{EMSError, Result};
8use rusty_common::SmartString;
9use rusty_common::auth::exchanges::{
10    binance::BinanceAuth, bithumb::BithumbAuth, bybit::BybitAuth, coinbase::CoinbaseAuth,
11    upbit::UpbitAuth,
12};
13use rusty_common::auth::pem::validate_pem_format;
14use std::str::FromStr;
15use std::sync::Arc;
16use thiserror::Error;
17
18/// Authentication-specific errors
19#[derive(Error, Debug)]
20pub enum AuthenticationError {
21    /// Invalid exchange name provided
22    #[error("Invalid exchange: {0}")]
23    InvalidExchange(SmartString),
24
25    /// Authentication method not supported by exchange
26    #[error("Authentication method not supported: {0}")]
27    UnsupportedMethod(SmartString),
28
29    /// Invalid credentials format or structure
30    #[error("Invalid credentials format: {0}")]
31    InvalidCredentials(SmartString),
32
33    /// Authentication token or credentials have expired
34    #[error("Authentication expired")]
35    Expired,
36
37    /// Failed to refresh authentication credentials
38    #[error("Authentication refresh failed: {0}")]
39    RefreshFailed(SmartString),
40}
41
42impl From<AuthenticationError> for EMSError {
43    fn from(err: AuthenticationError) -> Self {
44        Self::auth(err.to_string())
45    }
46}
47
48/// Exchange type enumeration for authentication adapter creation
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub enum ExchangeType {
51    /// Binance exchange
52    Binance,
53    /// Bybit exchange
54    Bybit,
55    /// Coinbase exchange
56    Coinbase,
57    /// Upbit exchange
58    Upbit,
59    /// Bithumb exchange
60    Bithumb,
61}
62
63impl ExchangeType {
64    /// Get exchange name as string
65    #[must_use]
66    pub const fn as_str(&self) -> &'static str {
67        match self {
68            Self::Binance => "binance",
69            Self::Bybit => "bybit",
70            Self::Coinbase => "coinbase",
71            Self::Upbit => "upbit",
72            Self::Bithumb => "bithumb",
73        }
74    }
75}
76
77impl FromStr for ExchangeType {
78    type Err = EMSError;
79
80    fn from_str(s: &str) -> Result<Self> {
81        match s.to_lowercase().as_str() {
82            "binance" => Ok(Self::Binance),
83            "bybit" => Ok(Self::Bybit),
84            "coinbase" => Ok(Self::Coinbase),
85            "upbit" => Ok(Self::Upbit),
86            "bithumb" => Ok(Self::Bithumb),
87            _ => Err(AuthenticationError::InvalidExchange(s.into()).into()),
88        }
89    }
90}
91
92/// Create an authentication adapter for the specified exchange type
93pub fn create_auth_adapter(
94    exchange_type: ExchangeType,
95    api_key: impl Into<SmartString>,
96    secret_key: impl Into<SmartString>,
97    passphrase: Option<impl Into<SmartString>>,
98) -> Result<Arc<dyn AuthenticationManager>> {
99    let api_key = api_key.into();
100    let secret_key = secret_key.into();
101
102    match exchange_type {
103        ExchangeType::Binance => {
104            let auth = BinanceAuth::new_hmac(api_key, secret_key);
105            Ok(Arc::new(BinanceAuthAdapter::new(auth)))
106        }
107
108        ExchangeType::Bybit => {
109            let auth = BybitAuth::new(api_key, secret_key);
110            Ok(Arc::new(BybitAuthAdapter::new(auth)))
111        }
112
113        ExchangeType::Coinbase => {
114            if let Some(_passphrase) = passphrase {
115                // HMAC authentication
116                let auth = CoinbaseAuth::new_hmac(api_key, secret_key);
117                Ok(Arc::new(CoinbaseAuthAdapter::new(auth)))
118            } else {
119                // Try ECDSA authentication (secret_key is PEM)
120                let auth = CoinbaseAuth::new_ecdsa(api_key, secret_key).map_err(|e| {
121                    EMSError::auth(format!("Failed to create Coinbase ECDSA auth: {e}"))
122                })?;
123                Ok(Arc::new(CoinbaseAuthAdapter::new(auth)))
124            }
125        }
126
127        ExchangeType::Upbit => {
128            let config = rusty_common::auth::exchanges::upbit::UpbitAuthConfig {
129                access_key: api_key,
130                secret_key,
131            };
132            let auth = UpbitAuth::new(config);
133            Ok(Arc::new(UpbitAuthAdapter::new(auth)))
134        }
135
136        ExchangeType::Bithumb => {
137            let auth = BithumbAuth::new(api_key, secret_key);
138            Ok(Arc::new(BithumbAuthAdapter::new(auth)))
139        }
140    }
141}
142
143/// Create a Binance Ed25519 authentication adapter
144pub fn create_binance_ed25519_adapter(
145    api_key: impl Into<SmartString>,
146    private_key_base64: impl Into<SmartString>,
147) -> Result<Arc<dyn AuthenticationManager>> {
148    let auth = BinanceAuth::new_ed25519(api_key.into(), private_key_base64.into())
149        .map_err(|e| EMSError::auth(format!("Failed to create Binance Ed25519 auth: {e}")))?;
150    Ok(Arc::new(BinanceAuthAdapter::new(auth)))
151}
152
153/// Authentication configuration builder
154pub struct AuthConfigBuilder {
155    exchange_type: Option<ExchangeType>,
156    api_key: Option<SmartString>,
157    secret_key: Option<SmartString>,
158    passphrase: Option<SmartString>,
159    use_ed25519: bool,
160}
161
162impl AuthConfigBuilder {
163    /// Create a new authentication configuration builder
164    #[must_use]
165    pub const fn new() -> Self {
166        Self {
167            exchange_type: None,
168            api_key: None,
169            secret_key: None,
170            passphrase: None,
171            use_ed25519: false,
172        }
173    }
174
175    /// Set exchange type
176    #[must_use]
177    pub const fn exchange(mut self, exchange_type: ExchangeType) -> Self {
178        self.exchange_type = Some(exchange_type);
179        self
180    }
181
182    /// Set exchange type from string
183    pub fn exchange_str(mut self, exchange: &str) -> Result<Self> {
184        self.exchange_type = Some(ExchangeType::from_str(exchange)?);
185        Ok(self)
186    }
187
188    /// Set API key
189    pub fn api_key(mut self, api_key: impl Into<SmartString>) -> Self {
190        self.api_key = Some(api_key.into());
191        self
192    }
193
194    /// Set secret key
195    pub fn secret_key(mut self, secret_key: impl Into<SmartString>) -> Self {
196        self.secret_key = Some(secret_key.into());
197        self
198    }
199
200    /// Set passphrase (for Coinbase HMAC)
201    pub fn passphrase(mut self, passphrase: impl Into<SmartString>) -> Self {
202        self.passphrase = Some(passphrase.into());
203        self
204    }
205
206    /// Use Ed25519 authentication (Binance only)
207    #[must_use]
208    pub const fn use_ed25519(mut self) -> Self {
209        self.use_ed25519 = true;
210        self
211    }
212
213    /// Build the authentication adapter
214    pub fn build(self) -> Result<Arc<dyn AuthenticationManager>> {
215        let exchange_type = self
216            .exchange_type
217            .ok_or_else(|| EMSError::invalid_params("Exchange type not specified"))?;
218        let api_key = self
219            .api_key
220            .ok_or_else(|| EMSError::invalid_params("API key not specified"))?;
221        let secret_key = self
222            .secret_key
223            .ok_or_else(|| EMSError::invalid_params("Secret key not specified"))?;
224
225        if self.use_ed25519 && exchange_type == ExchangeType::Binance {
226            create_binance_ed25519_adapter(api_key, secret_key)
227        } else {
228            create_auth_adapter(exchange_type, api_key, secret_key, self.passphrase)
229        }
230    }
231}
232
233impl Default for AuthConfigBuilder {
234    fn default() -> Self {
235        Self::new()
236    }
237}
238
239/// Utility function to extract authentication method from exchange adapter
240pub fn get_auth_method_info(adapter: &dyn AuthenticationManager) -> AuthMethodInfo {
241    let exchange = adapter.exchange_name();
242    let supports_ws_trading = adapter.supports_websocket_trading();
243    let requires_refresh = adapter.requires_refresh();
244    let refresh_interval = adapter.refresh_interval();
245
246    let method_type = match exchange.to_lowercase().as_str() {
247        "binance" => {
248            if supports_ws_trading {
249                "Ed25519".into()
250            } else {
251                "HMAC-SHA256".into()
252            }
253        }
254        "bybit" => "HMAC-SHA256".into(),
255        "coinbase" => "ECDSA/JWT".into(),
256        "upbit" | "bithumb" => "JWT-HMAC".into(),
257        _ => "Unknown".into(),
258    };
259
260    AuthMethodInfo {
261        exchange_name: exchange.into(),
262        method_type,
263        supports_websocket_trading: supports_ws_trading,
264        requires_refresh,
265        refresh_interval,
266    }
267}
268
269/// Information about authentication method
270#[derive(Debug, Clone)]
271pub struct AuthMethodInfo {
272    /// Name of the exchange
273    pub exchange_name: SmartString,
274    /// Type of authentication method used
275    pub method_type: SmartString,
276    /// Whether WebSocket trading is supported
277    pub supports_websocket_trading: bool,
278    /// Whether authentication requires periodic refresh
279    pub requires_refresh: bool,
280    /// Interval for refreshing authentication
281    pub refresh_interval: Option<std::time::Duration>,
282}
283
284/// Validate authentication configuration
285pub fn validate_auth_config(
286    exchange_type: ExchangeType,
287    api_key: &str,
288    secret_key: &str,
289    passphrase: Option<&str>,
290) -> Result<()> {
291    // Basic validation
292    if api_key.is_empty() {
293        return Err(EMSError::invalid_params("API key cannot be empty"));
294    }
295
296    if secret_key.is_empty() {
297        return Err(EMSError::invalid_params("Secret key cannot be empty"));
298    }
299
300    // Exchange-specific validation
301    match exchange_type {
302        ExchangeType::Coinbase => {
303            // Coinbase can use either HMAC (with passphrase) or ECDSA (PEM key)
304            if let Some(phrase) = passphrase {
305                // HMAC mode - validate passphrase is not empty
306                if phrase.is_empty() {
307                    return Err(EMSError::invalid_params(
308                        "Coinbase passphrase cannot be empty",
309                    ));
310                }
311            } else {
312                // ECDSA mode - validate PEM format using proper parser
313                validate_pem_format(secret_key)
314                    .map_err(|e| EMSError::invalid_params(format!("PEM validation failed: {e}")))?;
315            }
316        }
317
318        ExchangeType::Binance => {
319            // For Ed25519, secret key should be base64 encoded
320            if secret_key.len() < 32 {
321                return Err(EMSError::invalid_params(
322                    "Binance secret key appears too short",
323                ));
324            }
325        }
326
327        ExchangeType::Upbit | ExchangeType::Bithumb => {
328            // JWT-based exchanges
329            if api_key.len() < 10 {
330                return Err(EMSError::invalid_params(
331                    "Korean exchange API key appears too short",
332                ));
333            }
334        }
335
336        ExchangeType::Bybit => {
337            // Standard HMAC validation
338            if secret_key.len() < 20 {
339                return Err(EMSError::invalid_params(
340                    "Bybit secret key appears too short",
341                ));
342            }
343        }
344    }
345
346    Ok(())
347}
348
349#[cfg(test)]
350mod tests {
351    use super::*;
352
353    #[test]
354    fn test_exchange_type_parsing() {
355        assert_eq!(
356            ExchangeType::from_str("binance").unwrap(),
357            ExchangeType::Binance
358        );
359        assert_eq!(
360            ExchangeType::from_str("BYBIT").unwrap(),
361            ExchangeType::Bybit
362        );
363        assert_eq!(
364            ExchangeType::from_str("Coinbase").unwrap(),
365            ExchangeType::Coinbase
366        );
367
368        assert!(ExchangeType::from_str("invalid").is_err());
369    }
370
371    #[test]
372    fn test_auth_config_builder() {
373        // Test successful builder pattern
374        let builder = AuthConfigBuilder::new()
375            .exchange(ExchangeType::Binance)
376            .api_key("test_api_key")
377            .secret_key("test_secret_key_longer_than_32_chars_here");
378
379        // This should succeed for Binance with proper key length
380        assert!(builder.build().is_ok());
381
382        // Test invalid PEM format for Coinbase
383        let invalid_pem_builder = AuthConfigBuilder::new()
384            .exchange(ExchangeType::Coinbase)
385            .api_key("test_api_key")
386            .secret_key("invalid_pem_key");
387
388        // This should fail due to invalid PEM format
389        assert!(invalid_pem_builder.build().is_err());
390
391        // Test valid PEM format for Coinbase
392        let valid_pem_builder = AuthConfigBuilder::new()
393            .exchange(ExchangeType::Coinbase)
394            .api_key("test_api_key")
395            .secret_key(
396                "-----BEGIN PRIVATE KEY-----\nVEVTVCBLRVkgREFUQQ==\n-----END PRIVATE KEY-----",
397            );
398
399        // This should succeed with valid PEM format
400        let result = valid_pem_builder.build();
401        if let Err(e) = &result {
402            eprintln!("PEM validation failed: {e:?}");
403        }
404        assert!(result.is_ok());
405    }
406
407    #[test]
408    fn test_auth_validation() {
409        // Valid configurations
410        assert!(
411            validate_auth_config(
412                ExchangeType::Binance,
413                "test_api_key",
414                "test_secret_key_longer_than_32_chars_here",
415                None
416            )
417            .is_ok()
418        );
419
420        // Invalid configurations
421        assert!(validate_auth_config(ExchangeType::Binance, "", "test_secret", None).is_err());
422
423        assert!(
424            validate_auth_config(ExchangeType::Coinbase, "test_api", "short", Some("")).is_err()
425        );
426    }
427}