ccxt_exchanges/binance/
auth.rs

1//! Binance authentication and signature module.
2//!
3//! Implements HMAC-SHA256 signature algorithm for API request authentication.
4
5use ccxt_core::credentials::SecretString;
6use ccxt_core::{Error, Result};
7use hmac::{Hmac, Mac};
8use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
9use sha2::Sha256;
10use std::collections::{BTreeMap, HashMap};
11
12type HmacSha256 = Hmac<Sha256>;
13
14/// Binance authenticator.
15///
16/// Credentials are automatically zeroed from memory when dropped.
17#[derive(Debug, Clone)]
18pub struct BinanceAuth {
19    /// API key (automatically zeroed on drop).
20    api_key: SecretString,
21    /// Secret key (automatically zeroed on drop).
22    secret: SecretString,
23}
24
25impl BinanceAuth {
26    /// Creates a new authenticator.
27    ///
28    /// # Arguments
29    ///
30    /// * `api_key` - API key
31    /// * `secret` - Secret key
32    ///
33    /// # Security
34    ///
35    /// Credentials are automatically zeroed from memory when the authenticator is dropped.
36    pub fn new(api_key: impl Into<String>, secret: impl Into<String>) -> Self {
37        Self {
38            api_key: SecretString::new(api_key),
39            secret: SecretString::new(secret),
40        }
41    }
42
43    /// Returns the API key.
44    pub fn api_key(&self) -> &str {
45        self.api_key.expose_secret()
46    }
47
48    /// Returns the secret key.
49    pub fn secret(&self) -> &str {
50        self.secret.expose_secret()
51    }
52
53    /// Signs a query string using HMAC-SHA256.
54    ///
55    /// # Arguments
56    ///
57    /// * `query_string` - Query string to sign
58    ///
59    /// # Returns
60    ///
61    /// Returns the HMAC-SHA256 signature as a hex string.
62    ///
63    /// # Errors
64    ///
65    /// Returns an error if the secret key is invalid.
66    pub fn sign(&self, query_string: &str) -> Result<String> {
67        let mut mac = HmacSha256::new_from_slice(self.secret.expose_secret_bytes())
68            .map_err(|e| Error::authentication(format!("Invalid secret key: {}", e)))?;
69
70        mac.update(query_string.as_bytes());
71        let result = mac.finalize();
72        let signature = hex::encode(result.into_bytes());
73
74        Ok(signature)
75    }
76
77    /// Signs a parameter map.
78    ///
79    /// # Arguments
80    ///
81    /// * `params` - Parameter map
82    ///
83    /// # Returns
84    ///
85    /// Returns a new parameter map containing the signature.
86    ///
87    /// # Errors
88    ///
89    /// Returns an error if signature generation fails.
90    pub fn sign_params(
91        &self,
92        params: &BTreeMap<String, String>,
93    ) -> Result<BTreeMap<String, String>> {
94        let query_string = self.build_query_string(params);
95        let signature = self.sign(&query_string)?;
96
97        let mut signed_params = params.clone();
98        signed_params.insert("signature".to_string(), signature);
99
100        Ok(signed_params)
101    }
102
103    /// Signs parameters with timestamp and optional receive window.
104    ///
105    /// # Arguments
106    ///
107    /// * `params` - Parameter map
108    /// * `timestamp` - Timestamp in milliseconds
109    /// * `recv_window` - Optional receive window in milliseconds
110    ///
111    /// # Returns
112    ///
113    /// Returns a new parameter map containing timestamp and signature.
114    ///
115    /// # Errors
116    ///
117    /// Returns an error if signature generation fails.
118    pub fn sign_with_timestamp(
119        &self,
120        params: &BTreeMap<String, String>,
121        timestamp: i64,
122        recv_window: Option<u64>,
123    ) -> Result<BTreeMap<String, String>> {
124        let mut params_with_time = params.clone();
125        params_with_time.insert("timestamp".to_string(), timestamp.to_string());
126
127        if let Some(window) = recv_window {
128            params_with_time.insert("recvWindow".to_string(), window.to_string());
129        }
130
131        self.sign_params(&params_with_time)
132    }
133
134    /// Builds a query string from parameters.
135    ///
136    /// # Arguments
137    ///
138    /// * `params` - Parameter map
139    ///
140    /// # Returns
141    ///
142    /// Returns a URL-encoded query string with parameters sorted by key.
143    #[allow(clippy::unused_self)]
144    pub(crate) fn build_query_string(&self, params: &BTreeMap<String, String>) -> String {
145        let mut pairs: Vec<_> = params.iter().collect();
146        pairs.sort_by_key(|(k, _)| *k);
147
148        pairs
149            .iter()
150            .map(|(k, v)| format!("{k}={v}"))
151            .collect::<Vec<_>>()
152            .join("&")
153    }
154
155    /// Adds authentication headers to the request.
156    ///
157    /// # Arguments
158    ///
159    /// * `headers` - Existing header map to modify
160    pub fn add_auth_headers(&self, headers: &mut HashMap<String, String>) {
161        headers.insert(
162            "X-MBX-APIKEY".to_string(),
163            self.api_key.expose_secret().to_string(),
164        );
165    }
166
167    /// Adds authentication headers to a `reqwest` request.
168    ///
169    /// # Arguments
170    ///
171    /// * `headers` - Reqwest `HeaderMap` to modify
172    pub fn add_auth_headers_reqwest(&self, headers: &mut HeaderMap) {
173        if let Ok(header_name) = HeaderName::from_bytes(b"X-MBX-APIKEY") {
174            if let Ok(header_value) = HeaderValue::from_str(self.api_key.expose_secret()) {
175                headers.insert(header_name, header_value);
176            }
177        }
178    }
179}
180
181/// Builds a signed URL with query parameters.
182///
183/// # Arguments
184///
185/// * `base_url` - Base URL
186/// * `endpoint` - API endpoint path
187/// * `params` - Parameter map (must include signature)
188///
189/// # Returns
190///
191/// Returns the complete URL with query parameters.
192pub fn build_signed_url<S: std::hash::BuildHasher>(
193    base_url: &str,
194    endpoint: &str,
195    params: &HashMap<String, String, S>,
196) -> String {
197    let query_string = params
198        .iter()
199        .map(|(k, v)| format!("{k}={v}"))
200        .collect::<Vec<_>>()
201        .join("&");
202
203    if query_string.is_empty() {
204        format!("{base_url}{endpoint}")
205    } else {
206        format!("{base_url}{endpoint}?{query_string}")
207    }
208}
209
210#[cfg(test)]
211mod tests {
212    use super::*;
213
214    #[test]
215    fn test_sign() {
216        let auth = BinanceAuth::new("test_key", "test_secret");
217        let query = "symbol=BTCUSDT&side=BUY&type=LIMIT&quantity=1&timestamp=1234567890";
218
219        let signature = auth.sign(query);
220        assert!(signature.is_ok());
221
222        let sig = signature.unwrap();
223        assert!(!sig.is_empty());
224        assert_eq!(sig.len(), 64); // HMAC-SHA256 produces 64 hex characters
225    }
226
227    #[test]
228    fn test_sign_params() {
229        let auth = BinanceAuth::new("test_key", "test_secret");
230        let mut params = BTreeMap::new();
231        params.insert("symbol".to_string(), "BTCUSDT".to_string());
232        params.insert("side".to_string(), "BUY".to_string());
233
234        let signed = auth.sign_params(&params);
235        assert!(signed.is_ok());
236
237        let signed_params = signed.unwrap();
238        assert!(signed_params.contains_key("signature"));
239        assert_eq!(signed_params.get("symbol").unwrap(), "BTCUSDT");
240    }
241
242    #[test]
243    fn test_sign_with_timestamp() {
244        let auth = BinanceAuth::new("test_key", "test_secret");
245        let params = BTreeMap::new();
246        let timestamp = 1234567890i64;
247
248        let signed = auth.sign_with_timestamp(&params, timestamp, Some(5000));
249        assert!(signed.is_ok());
250
251        let signed_params = signed.unwrap();
252        assert!(signed_params.contains_key("timestamp"));
253        assert!(signed_params.contains_key("recvWindow"));
254        assert!(signed_params.contains_key("signature"));
255        assert_eq!(signed_params.get("timestamp").unwrap(), "1234567890");
256        assert_eq!(signed_params.get("recvWindow").unwrap(), "5000");
257    }
258
259    #[test]
260    fn test_build_query_string() {
261        let auth = BinanceAuth::new("test_key", "test_secret");
262        let mut params = BTreeMap::new();
263        params.insert("symbol".to_string(), "BTCUSDT".to_string());
264        params.insert("side".to_string(), "BUY".to_string());
265
266        let query = auth.build_query_string(&params);
267        assert!(query == "side=BUY&symbol=BTCUSDT" || query == "symbol=BTCUSDT&side=BUY");
268    }
269
270    #[test]
271    fn test_add_auth_headers() {
272        let auth = BinanceAuth::new("my_api_key", "test_secret");
273        let mut headers = HashMap::new();
274
275        auth.add_auth_headers(&mut headers);
276
277        assert!(headers.contains_key("X-MBX-APIKEY"));
278        assert_eq!(headers.get("X-MBX-APIKEY").unwrap(), "my_api_key");
279    }
280
281    #[test]
282    fn test_build_signed_url() {
283        let mut params = HashMap::new();
284        params.insert("symbol".to_string(), "BTCUSDT".to_string());
285        params.insert("signature".to_string(), "abc123".to_string());
286
287        let url = build_signed_url("https://api.binance.com", "/api/v3/order", &params);
288
289        assert!(url.contains("https://api.binance.com/api/v3/order?"));
290        assert!(url.contains("symbol=BTCUSDT"));
291        assert!(url.contains("signature=abc123"));
292    }
293
294    #[test]
295    fn test_build_signed_url_empty_params() {
296        let params = HashMap::new();
297        let url = build_signed_url("https://api.binance.com", "/api/v3/time", &params);
298
299        assert_eq!(url, "https://api.binance.com/api/v3/time");
300    }
301}