Skip to main content

binance_api_client/
credentials.rs

1use base64::{Engine, engine::general_purpose::STANDARD as BASE64};
2use ring::{hmac, signature as ring_sig};
3use rsa::{
4    RsaPrivateKey,
5    pkcs1v15::SigningKey,
6    pkcs8::DecodePrivateKey,
7    signature::{RandomizedSigner, SignatureEncoding},
8};
9use secrecy::{ExposeSecret, SecretString};
10use sha2::Sha256;
11use std::sync::Arc;
12use std::time::{SystemTime, UNIX_EPOCH};
13
14use crate::error::Result;
15
16/// Signature algorithm type for API authentication.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
18pub enum SignatureType {
19    /// HMAC-SHA256 (default, most common)
20    #[default]
21    HmacSha256,
22    /// RSA with SHA256 (for institutional/enterprise keys)
23    RsaSha256,
24    /// Ed25519 (modern, fast alternative)
25    Ed25519,
26}
27
28/// Internal key storage for different signature types.
29enum SigningKey_ {
30    Hmac(SecretString),
31    Rsa(Arc<RsaPrivateKey>),
32    Ed25519(Arc<ring_sig::Ed25519KeyPair>),
33}
34
35impl Clone for SigningKey_ {
36    fn clone(&self) -> Self {
37        match self {
38            Self::Hmac(s) => Self::Hmac(s.clone()),
39            Self::Rsa(k) => Self::Rsa(Arc::clone(k)),
40            Self::Ed25519(k) => Self::Ed25519(Arc::clone(k)),
41        }
42    }
43}
44
45/// API credentials for authenticated Binance endpoints.
46///
47/// Supports three authentication methods:
48/// - HMAC-SHA256 (default): Most common, uses API secret key
49/// - RSA-SHA256: For institutional accounts with RSA key pairs
50/// - Ed25519: Modern, fast signature algorithm
51///
52/// # Examples
53///
54/// ## HMAC-SHA256 (Default)
55/// ```rust
56/// use binance_api_client::Credentials;
57///
58/// let creds = Credentials::new("api_key", "secret_key");
59/// ```
60///
61/// ## RSA-SHA256
62/// ```rust,ignore
63/// use binance_api_client::Credentials;
64///
65/// let pem = std::fs::read_to_string("private_key.pem")?;
66/// let creds = Credentials::with_rsa_key("api_key", &pem)?;
67/// ```
68///
69/// ## Ed25519
70/// ```rust,ignore
71/// use binance_api_client::Credentials;
72///
73/// let private_key_bytes = std::fs::read("ed25519_private_key.der")?;
74/// let creds = Credentials::with_ed25519_key("api_key", &private_key_bytes)?;
75/// ```
76#[derive(Clone)]
77pub struct Credentials {
78    api_key: String,
79    signing_key: SigningKey_,
80    signature_type: SignatureType,
81}
82
83impl Credentials {
84    /// Create new credentials with HMAC-SHA256 signing.
85    ///
86    /// This is the default and most common authentication method.
87    pub fn new(api_key: impl Into<String>, secret_key: impl Into<String>) -> Self {
88        Self {
89            api_key: api_key.into(),
90            signing_key: SigningKey_::Hmac(SecretString::from(secret_key.into())),
91            signature_type: SignatureType::HmacSha256,
92        }
93    }
94
95    /// Create credentials with an RSA private key for RSA-SHA256 signing.
96    ///
97    /// RSA signatures are commonly used for institutional/enterprise API keys.
98    /// The private key should be in PKCS#8 PEM format.
99    ///
100    /// # Arguments
101    ///
102    /// * `api_key` - The API key
103    /// * `private_key_pem` - RSA private key in PKCS#8 PEM format
104    ///
105    /// # Example
106    ///
107    /// ```rust,ignore
108    /// let pem = r#"-----BEGIN PRIVATE KEY-----
109    /// MIIEvQIBADANBg...
110    /// -----END PRIVATE KEY-----"#;
111    ///
112    /// let creds = Credentials::with_rsa_key("api_key", pem)?;
113    /// ```
114    pub fn with_rsa_key(api_key: impl Into<String>, private_key_pem: &str) -> Result<Self> {
115        let private_key = RsaPrivateKey::from_pkcs8_pem(private_key_pem).map_err(|e| {
116            crate::error::Error::InvalidCredentials(format!("Invalid RSA key: {}", e))
117        })?;
118
119        Ok(Self {
120            api_key: api_key.into(),
121            signing_key: SigningKey_::Rsa(Arc::new(private_key)),
122            signature_type: SignatureType::RsaSha256,
123        })
124    }
125
126    /// Create credentials with an Ed25519 private key.
127    ///
128    /// Ed25519 is a modern, fast signature algorithm.
129    /// The private key should be the raw 32-byte seed or a PKCS#8 DER-encoded key.
130    ///
131    /// # Arguments
132    ///
133    /// * `api_key` - The API key
134    /// * `private_key_bytes` - Ed25519 private key bytes (seed or PKCS#8 DER)
135    ///
136    /// # Example
137    ///
138    /// ```rust,ignore
139    /// // From raw 32-byte seed
140    /// let seed: [u8; 32] = [...];
141    /// let creds = Credentials::with_ed25519_key("api_key", &seed)?;
142    ///
143    /// // From PKCS#8 DER file
144    /// let der_bytes = std::fs::read("private_key.der")?;
145    /// let creds = Credentials::with_ed25519_key("api_key", &der_bytes)?;
146    /// ```
147    pub fn with_ed25519_key(api_key: impl Into<String>, private_key_bytes: &[u8]) -> Result<Self> {
148        let key_pair = if private_key_bytes.len() == 32 {
149            // Raw 32-byte seed
150            ring_sig::Ed25519KeyPair::from_seed_unchecked(private_key_bytes).map_err(|e| {
151                crate::error::Error::InvalidCredentials(format!("Invalid Ed25519 seed: {}", e))
152            })?
153        } else {
154            // PKCS#8 DER-encoded key
155            ring_sig::Ed25519KeyPair::from_pkcs8(private_key_bytes).map_err(|e| {
156                crate::error::Error::InvalidCredentials(format!(
157                    "Invalid Ed25519 PKCS#8 key: {}",
158                    e
159                ))
160            })?
161        };
162
163        Ok(Self {
164            api_key: api_key.into(),
165            signing_key: SigningKey_::Ed25519(Arc::new(key_pair)),
166            signature_type: SignatureType::Ed25519,
167        })
168    }
169
170    /// Create credentials with an Ed25519 private key from a PEM file.
171    ///
172    /// # Arguments
173    ///
174    /// * `api_key` - The API key
175    /// * `pem` - Ed25519 private key in PKCS#8 PEM format
176    pub fn with_ed25519_pem(api_key: impl Into<String>, pem: &str) -> Result<Self> {
177        // Extract the base64-encoded key from PEM format
178        let der_bytes = extract_pem_der(pem, "PRIVATE KEY")?;
179        Self::with_ed25519_key(api_key, &der_bytes)
180    }
181
182    /// Load credentials from environment variables.
183    ///
184    /// Expects `BINANCE_API_KEY` and `BINANCE_SECRET_KEY` environment variables.
185    /// Uses HMAC-SHA256 signing.
186    pub fn from_env() -> Result<Self> {
187        let api_key = std::env::var("BINANCE_API_KEY")?;
188        let secret_key = std::env::var("BINANCE_SECRET_KEY")?;
189        Ok(Self::new(api_key, secret_key))
190    }
191
192    /// Load credentials from environment variables with custom names.
193    ///
194    /// Uses HMAC-SHA256 signing.
195    pub fn from_env_with_prefix(prefix: &str) -> Result<Self> {
196        let api_key = std::env::var(format!("{}_API_KEY", prefix))?;
197        let secret_key = std::env::var(format!("{}_SECRET_KEY", prefix))?;
198        Ok(Self::new(api_key, secret_key))
199    }
200
201    /// Get the API key.
202    pub fn api_key(&self) -> &str {
203        &self.api_key
204    }
205
206    /// Get the signature type being used.
207    pub fn signature_type(&self) -> SignatureType {
208        self.signature_type
209    }
210
211    /// Sign a message using the configured signing method.
212    ///
213    /// Returns the signature as a hex string for HMAC, or base64 for RSA/Ed25519.
214    pub fn sign(&self, message: &str) -> String {
215        match &self.signing_key {
216            SigningKey_::Hmac(secret) => {
217                let key = hmac::Key::new(hmac::HMAC_SHA256, secret.expose_secret().as_bytes());
218                let signature = hmac::sign(&key, message.as_bytes());
219                hex::encode(signature.as_ref())
220            }
221            SigningKey_::Rsa(private_key) => {
222                let signing_key = SigningKey::<Sha256>::new((**private_key).clone());
223                let mut rng = rand::thread_rng();
224                let signature = signing_key.sign_with_rng(&mut rng, message.as_bytes());
225                BASE64.encode(signature.to_bytes())
226            }
227            SigningKey_::Ed25519(key_pair) => {
228                let signature = key_pair.sign(message.as_bytes());
229                BASE64.encode(signature.as_ref())
230            }
231        }
232    }
233}
234
235impl std::fmt::Debug for Credentials {
236    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
237        f.debug_struct("Credentials")
238            .field("api_key", &self.api_key)
239            .field("signature_type", &self.signature_type)
240            .field("secret_key", &"[REDACTED]")
241            .finish()
242    }
243}
244
245/// Extract DER bytes from a PEM-encoded string.
246fn extract_pem_der(pem: &str, expected_label: &str) -> Result<Vec<u8>> {
247    let begin_marker = format!("-----BEGIN {}-----", expected_label);
248    let end_marker = format!("-----END {}-----", expected_label);
249
250    let start = pem.find(&begin_marker).ok_or_else(|| {
251        crate::error::Error::InvalidCredentials(format!("Missing {} begin marker", expected_label))
252    })? + begin_marker.len();
253
254    let end = pem.find(&end_marker).ok_or_else(|| {
255        crate::error::Error::InvalidCredentials(format!("Missing {} end marker", expected_label))
256    })?;
257
258    let base64_content: String = pem[start..end]
259        .chars()
260        .filter(|c| !c.is_whitespace())
261        .collect();
262
263    BASE64
264        .decode(&base64_content)
265        .map_err(|e| crate::error::Error::InvalidCredentials(format!("Invalid PEM base64: {}", e)))
266}
267
268/// Get the current timestamp in milliseconds since Unix epoch.
269pub fn get_timestamp() -> Result<u64> {
270    let duration = SystemTime::now().duration_since(UNIX_EPOCH)?;
271    Ok(duration.as_millis() as u64)
272}
273
274/// Build a query string from key-value pairs.
275pub fn build_query_string<I, K, V>(params: I) -> String
276where
277    I: IntoIterator<Item = (K, V)>,
278    K: AsRef<str>,
279    V: AsRef<str>,
280{
281    params
282        .into_iter()
283        .filter(|(k, _)| !k.as_ref().is_empty())
284        .map(|(k, v)| format!("{}={}", k.as_ref(), v.as_ref()))
285        .collect::<Vec<_>>()
286        .join("&")
287}
288
289/// Build a signed query string with timestamp and signature.
290pub fn build_signed_query_string<I, K, V>(
291    params: I,
292    credentials: &Credentials,
293    recv_window: u64,
294) -> Result<String>
295where
296    I: IntoIterator<Item = (K, V)>,
297    K: AsRef<str>,
298    V: AsRef<str>,
299{
300    let timestamp = get_timestamp()?;
301
302    // Build the base query string
303    let mut query_parts: Vec<String> = Vec::new();
304
305    // Add recv_window if specified
306    if recv_window > 0 {
307        query_parts.push(format!("recvWindow={}", recv_window));
308    }
309
310    // Add timestamp
311    query_parts.push(format!("timestamp={}", timestamp));
312
313    // Add user params
314    for (k, v) in params {
315        if !k.as_ref().is_empty() {
316            query_parts.push(format!("{}={}", k.as_ref(), v.as_ref()));
317        }
318    }
319
320    let query_string = query_parts.join("&");
321
322    // Sign and append signature
323    let signature = credentials.sign(&query_string);
324    Ok(format!("{}&signature={}", query_string, signature))
325}
326
327#[cfg(test)]
328mod tests {
329    use super::*;
330
331    #[test]
332    fn test_credentials_new() {
333        let creds = Credentials::new("my_api_key", "my_secret_key");
334        assert_eq!(creds.api_key(), "my_api_key");
335        assert_eq!(creds.signature_type(), SignatureType::HmacSha256);
336    }
337
338    #[test]
339    fn test_credentials_debug_redacts_secret() {
340        let creds = Credentials::new("my_api_key", "my_secret_key");
341        let debug_output = format!("{:?}", creds);
342        assert!(debug_output.contains("my_api_key"));
343        assert!(debug_output.contains("[REDACTED]"));
344        assert!(!debug_output.contains("my_secret_key"));
345    }
346
347    #[test]
348    fn test_sign_hmac() {
349        // Test vector: known key and message should produce known signature
350        let creds = Credentials::new(
351            "api_key",
352            "NhqPtmdSJYdKjVHjA7PZj4Mge3R5YNiP1e3UZjInClVN65XAbvqqM6A7H5fATj0j",
353        );
354        let message = "symbol=LTCBTC&side=BUY&type=LIMIT&timeInForce=GTC&quantity=1&price=0.1&recvWindow=5000&timestamp=1499827319559";
355        let signature = creds.sign(message);
356        // This is the expected signature from Binance's documentation
357        assert_eq!(
358            signature,
359            "c8db56825ae71d6d79447849e617115f4a920fa2acdcab2b053c4b2838bd6b71"
360        );
361    }
362
363    #[test]
364    fn test_signature_type_default() {
365        assert_eq!(SignatureType::default(), SignatureType::HmacSha256);
366    }
367
368    #[test]
369    fn test_build_query_string() {
370        let params = [("symbol", "BTCUSDT"), ("limit", "100")];
371        let query = build_query_string(params);
372        assert_eq!(query, "symbol=BTCUSDT&limit=100");
373    }
374
375    #[test]
376    fn test_build_query_string_empty_key_filtered() {
377        let params = [("symbol", "BTCUSDT"), ("", "ignored"), ("limit", "100")];
378        let query = build_query_string(params);
379        assert_eq!(query, "symbol=BTCUSDT&limit=100");
380    }
381
382    #[test]
383    fn test_get_timestamp() {
384        let ts = get_timestamp().unwrap();
385        // Timestamp should be reasonable (after Jan 1, 2020 in milliseconds)
386        assert!(ts > 1577836800000);
387    }
388
389    #[test]
390    fn test_build_signed_query_string() {
391        let creds = Credentials::new("api_key", "secret_key");
392        let params = [("symbol", "BTCUSDT")];
393        let query = build_signed_query_string(params, &creds, 5000).unwrap();
394
395        // Should contain recvWindow, timestamp, symbol, and signature
396        assert!(query.contains("recvWindow=5000"));
397        assert!(query.contains("timestamp="));
398        assert!(query.contains("symbol=BTCUSDT"));
399        assert!(query.contains("signature="));
400    }
401
402    #[test]
403    fn test_build_signed_query_string_no_recv_window() {
404        let creds = Credentials::new("api_key", "secret_key");
405        let params = [("symbol", "BTCUSDT")];
406        let query = build_signed_query_string(params, &creds, 0).unwrap();
407
408        // Should NOT contain recvWindow when set to 0
409        assert!(!query.contains("recvWindow="));
410        assert!(query.contains("timestamp="));
411        assert!(query.contains("symbol=BTCUSDT"));
412        assert!(query.contains("signature="));
413    }
414
415    #[test]
416    fn test_ed25519_signing() {
417        // Generate a test Ed25519 key pair using ring
418        let rng = ring::rand::SystemRandom::new();
419        let pkcs8_bytes = ring_sig::Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
420
421        let creds = Credentials::with_ed25519_key("api_key", pkcs8_bytes.as_ref()).unwrap();
422        assert_eq!(creds.signature_type(), SignatureType::Ed25519);
423
424        let message = "test message";
425        let signature = creds.sign(message);
426
427        // Ed25519 signatures should be base64 encoded
428        assert!(BASE64.decode(&signature).is_ok());
429    }
430}