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#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
18pub enum SignatureType {
19 #[default]
21 HmacSha256,
22 RsaSha256,
24 Ed25519,
26}
27
28enum 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#[derive(Clone)]
77pub struct Credentials {
78 api_key: String,
79 signing_key: SigningKey_,
80 signature_type: SignatureType,
81}
82
83impl Credentials {
84 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 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 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 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 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 pub fn with_ed25519_pem(api_key: impl Into<String>, pem: &str) -> Result<Self> {
177 let der_bytes = extract_pem_der(pem, "PRIVATE KEY")?;
179 Self::with_ed25519_key(api_key, &der_bytes)
180 }
181
182 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 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 pub fn api_key(&self) -> &str {
203 &self.api_key
204 }
205
206 pub fn signature_type(&self) -> SignatureType {
208 self.signature_type
209 }
210
211 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
245fn 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
268pub fn get_timestamp() -> Result<u64> {
270 let duration = SystemTime::now().duration_since(UNIX_EPOCH)?;
271 Ok(duration.as_millis() as u64)
272}
273
274pub 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
289pub 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 let mut query_parts: Vec<String> = Vec::new();
304
305 if recv_window > 0 {
307 query_parts.push(format!("recvWindow={}", recv_window));
308 }
309
310 query_parts.push(format!("timestamp={}", timestamp));
312
313 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 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 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×tamp=1499827319559";
355 let signature = creds.sign(message);
356 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 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 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 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 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 assert!(BASE64.decode(&signature).is_ok());
429 }
430}