1#![allow(unused_assignments)] use std::fmt::{Debug, Display};
29
30use aws_lc_rs::hmac;
31use ed25519_dalek::{Signature, Signer, SigningKey};
32use nautilus_core::{hex, string::secret::REDACTED};
33use zeroize::ZeroizeOnDrop;
34
35use super::enums::{BinanceEnvironment, BinanceProductType};
36
37pub fn resolve_credentials(
52 config_api_key: Option<String>,
53 config_api_secret: Option<String>,
54 environment: BinanceEnvironment,
55 product_type: BinanceProductType,
56) -> anyhow::Result<(String, String)> {
57 if let (Some(key), Some(secret)) = (config_api_key.clone(), config_api_secret.clone()) {
58 return Ok((key, secret));
59 }
60
61 let (deprecated_key_var, deprecated_secret_var, standard_key_var, standard_secret_var) =
62 match environment {
63 BinanceEnvironment::Testnet => match product_type {
64 BinanceProductType::Spot
65 | BinanceProductType::Margin
66 | BinanceProductType::Options => (
67 "BINANCE_TESTNET_ED25519_API_KEY",
68 "BINANCE_TESTNET_ED25519_API_SECRET",
69 "BINANCE_TESTNET_API_KEY",
70 "BINANCE_TESTNET_API_SECRET",
71 ),
72 BinanceProductType::UsdM | BinanceProductType::CoinM => (
73 "BINANCE_FUTURES_TESTNET_ED25519_API_KEY",
74 "BINANCE_FUTURES_TESTNET_ED25519_API_SECRET",
75 "BINANCE_FUTURES_TESTNET_API_KEY",
76 "BINANCE_FUTURES_TESTNET_API_SECRET",
77 ),
78 },
79
80 BinanceEnvironment::Demo => ("", "", "BINANCE_DEMO_API_KEY", "BINANCE_DEMO_API_SECRET"),
82 BinanceEnvironment::Live => (
83 "BINANCE_ED25519_API_KEY",
84 "BINANCE_ED25519_API_SECRET",
85 "BINANCE_API_KEY",
86 "BINANCE_API_SECRET",
87 ),
88 };
89
90 let is_futures = matches!(
93 product_type,
94 BinanceProductType::UsdM | BinanceProductType::CoinM
95 );
96
97 let api_key = config_api_key
98 .or_else(|| std::env::var(standard_key_var).ok())
99 .or_else(|| resolve_deprecated_var(deprecated_key_var, standard_key_var, is_futures))
100 .ok_or_else(|| anyhow::anyhow!("{standard_key_var} not found in config or environment"))?;
101
102 let api_secret = config_api_secret
103 .or_else(|| std::env::var(standard_secret_var).ok())
104 .or_else(|| resolve_deprecated_var(deprecated_secret_var, standard_secret_var, is_futures))
105 .ok_or_else(|| {
106 anyhow::anyhow!("{standard_secret_var} not found in config or environment")
107 })?;
108
109 Ok((api_key, api_secret))
110}
111
112fn resolve_deprecated_var(
113 deprecated_var: &str,
114 standard_var: &str,
115 allow_fallback: bool,
116) -> Option<String> {
117 if deprecated_var.is_empty() {
118 return None;
119 }
120
121 let value = std::env::var(deprecated_var).ok()?;
122
123 if allow_fallback {
124 log::warn!(
125 "'{deprecated_var}' is deprecated and will be removed in a future version. \
126 Rename it to '{standard_var}' (Ed25519 keys are now auto-detected)"
127 );
128 Some(value)
129 } else {
130 log::error!(
131 "'{deprecated_var}' has been removed. \
132 Rename it to '{standard_var}' (Ed25519 keys are now auto-detected)"
133 );
134 None
135 }
136}
137
138#[derive(Clone, ZeroizeOnDrop)]
142pub struct Credential {
143 api_key: Box<str>,
144 api_secret: Box<[u8]>,
145}
146
147#[derive(ZeroizeOnDrop)]
152pub struct Ed25519Credential {
153 api_key: Box<str>,
154 signing_key: SigningKey,
155}
156
157impl Debug for Credential {
158 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
159 f.debug_struct(stringify!(Credential))
160 .field("api_key", &self.api_key)
161 .field("api_secret", &REDACTED)
162 .finish()
163 }
164}
165
166impl Credential {
167 #[must_use]
169 pub fn new(api_key: String, api_secret: String) -> Self {
170 Self {
171 api_key: api_key.into_boxed_str(),
172 api_secret: api_secret.into_bytes().into_boxed_slice(),
173 }
174 }
175
176 #[must_use]
178 pub fn api_key(&self) -> &str {
179 &self.api_key
180 }
181
182 #[must_use]
184 pub fn sign(&self, message: &str) -> String {
185 let key = hmac::Key::new(hmac::HMAC_SHA256, &self.api_secret);
186 let tag = hmac::sign(&key, message.as_bytes());
187 hex::encode(tag.as_ref())
188 }
189}
190
191impl Debug for Ed25519Credential {
192 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
193 f.debug_struct(stringify!(Ed25519Credential))
194 .field("api_key", &self.api_key)
195 .field("signing_key", &REDACTED)
196 .finish()
197 }
198}
199
200const ED25519_OID: [u8; 5] = [0x06, 0x03, 0x2B, 0x65, 0x70];
207
208impl Ed25519Credential {
209 pub fn new(api_key: String, private_key_base64: &str) -> Result<Self, Ed25519CredentialError> {
226 let key_data: String = private_key_base64
228 .lines()
229 .filter(|line| !line.starts_with("-----"))
230 .collect();
231
232 let private_key_bytes =
233 base64::Engine::decode(&base64::engine::general_purpose::STANDARD, &key_data)
234 .map_err(|e| Ed25519CredentialError::InvalidBase64(e.to_string()))?;
235
236 if !contains_subslice(&private_key_bytes, &ED25519_OID) {
237 return Err(Ed25519CredentialError::NotEd25519);
238 }
239
240 if private_key_bytes.len() < 32 {
241 return Err(Ed25519CredentialError::InvalidKeyLength);
242 }
243 let seed_start = private_key_bytes.len() - 32;
244 let key_bytes: [u8; 32] = private_key_bytes[seed_start..]
245 .try_into()
246 .map_err(|_| Ed25519CredentialError::InvalidKeyLength)?;
247
248 let signing_key = SigningKey::from_bytes(&key_bytes);
249
250 Ok(Self {
251 api_key: api_key.into_boxed_str(),
252 signing_key,
253 })
254 }
255
256 #[must_use]
258 pub fn api_key(&self) -> &str {
259 &self.api_key
260 }
261
262 #[must_use]
264 pub fn sign(&self, message: &[u8]) -> String {
265 let signature: Signature = self.signing_key.sign(message);
266 base64::Engine::encode(
267 &base64::engine::general_purpose::STANDARD,
268 signature.to_bytes(),
269 )
270 }
271}
272
273#[derive(Debug, Clone)]
275pub enum Ed25519CredentialError {
276 InvalidBase64(String),
278 NotEd25519,
280 InvalidKeyLength,
282}
283
284impl Display for Ed25519CredentialError {
285 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
286 match self {
287 Self::InvalidBase64(e) => write!(f, "Invalid base64 encoding: {e}"),
288 Self::NotEd25519 => write!(f, "Decoded key does not carry the Ed25519 PKCS#8 OID"),
289 Self::InvalidKeyLength => write!(f, "Ed25519 private key must be 32 bytes"),
290 }
291 }
292}
293
294impl std::error::Error for Ed25519CredentialError {}
295
296#[derive(Clone)]
306pub enum SigningCredential {
307 Hmac(Credential),
309 Ed25519(Box<Ed25519Credential>),
311}
312
313impl Debug for SigningCredential {
314 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
315 match self {
316 Self::Hmac(c) => f.debug_tuple("Hmac").field(c).finish(),
317 Self::Ed25519(c) => f.debug_tuple("Ed25519").field(c).finish(),
318 }
319 }
320}
321
322impl SigningCredential {
323 #[must_use]
328 pub fn new(api_key: String, api_secret: String) -> Self {
329 match Ed25519Credential::new(api_key.clone(), &api_secret) {
330 Ok(ed25519) => {
331 log::debug!("Auto-detected Ed25519 API key");
332 Self::Ed25519(Box::new(ed25519))
333 }
334 Err(_) => {
335 log::debug!("Using HMAC SHA256 API key");
336 Self::Hmac(Credential::new(api_key, api_secret))
337 }
338 }
339 }
340
341 #[must_use]
343 pub fn api_key(&self) -> &str {
344 match self {
345 Self::Hmac(c) => c.api_key(),
346 Self::Ed25519(c) => c.api_key(),
347 }
348 }
349
350 #[must_use]
355 pub fn sign(&self, message: &str) -> String {
356 match self {
357 Self::Hmac(c) => c.sign(message),
358 Self::Ed25519(c) => c.sign(message.as_bytes()),
359 }
360 }
361
362 #[must_use]
364 pub fn is_ed25519(&self) -> bool {
365 matches!(self, Self::Ed25519(_))
366 }
367}
368
369impl Clone for Ed25519Credential {
372 fn clone(&self) -> Self {
373 let key_bytes = self.signing_key.to_bytes();
375 Self {
376 api_key: self.api_key.clone(),
377 signing_key: SigningKey::from_bytes(&key_bytes),
378 }
379 }
380}
381
382fn contains_subslice(haystack: &[u8], needle: &[u8]) -> bool {
383 if needle.is_empty() || needle.len() > haystack.len() {
384 return false;
385 }
386 haystack.windows(needle.len()).any(|w| w == needle)
387}
388
389pub(crate) fn canonical_ws_query_string<'a, I>(
409 params: I,
410) -> Result<String, serde_urlencoded::ser::Error>
411where
412 I: IntoIterator<Item = (&'a str, &'a serde_json::Value)>,
413{
414 let sorted: std::collections::BTreeMap<&str, &serde_json::Value> = params.into_iter().collect();
418 serde_urlencoded::to_string(sorted)
419}
420
421#[cfg(test)]
422mod tests {
423 use rstest::rstest;
424
425 use super::*;
426
427 const BINANCE_TEST_SECRET: &str =
430 "NhqPtmdSJYdKjVHjA7PZj4Mge3R5YNiP1e3UZjInClVN65XAbvqqM6A7H5fATj0j";
431
432 #[rstest]
433 fn test_sign_matches_binance_test_vector_simple() {
434 let cred = Credential::new("test_key".to_string(), BINANCE_TEST_SECRET.to_string());
435 let message = "timestamp=1578963600000";
436 let expected = "d84e6641b1e328e7b418fff030caed655c266299c9355e36ce801ed14631eed4";
437
438 assert_eq!(cred.sign(message), expected);
439 }
440
441 #[rstest]
442 fn test_sign_matches_binance_test_vector_order() {
443 let cred = Credential::new("test_key".to_string(), BINANCE_TEST_SECRET.to_string());
444 let message = "symbol=LTCBTC&side=BUY&type=LIMIT&timeInForce=GTC&quantity=1&price=0.1&recvWindow=5000×tamp=1499827319559";
445 let expected = "c8db56825ae71d6d79447849e617115f4a920fa2acdcab2b053c4b2838bd6b71";
446
447 assert_eq!(cred.sign(message), expected);
448 }
449
450 #[rstest]
451 fn test_canonical_ws_query_string_is_key_sorted_regardless_of_input_order() {
452 let symbol = serde_json::json!("LTCBTC");
458 let side = serde_json::json!("BUY");
459 let quantity = serde_json::json!("1");
460 let timestamp = serde_json::json!(1_499_827_319_559i64);
461 let api_key = serde_json::json!("mykey");
462 let unsorted = [
463 ("symbol", &symbol),
464 ("side", &side),
465 ("quantity", &quantity),
466 ("timestamp", ×tamp),
467 ("apiKey", &api_key),
468 ];
469
470 let query = canonical_ws_query_string(unsorted).unwrap();
471
472 assert_eq!(
473 query,
474 "apiKey=mykey&quantity=1&side=BUY&symbol=LTCBTC×tamp=1499827319559"
475 );
476 }
477
478 #[rstest]
479 fn test_canonical_ws_query_string_preserves_urlencoding() {
480 let symbol = serde_json::json!("LTCBTC");
481 let new_client_order_id = serde_json::json!("desk alpha");
482 let unsorted = [
483 ("symbol", &symbol),
484 ("newClientOrderId", &new_client_order_id),
485 ];
486
487 let query = canonical_ws_query_string(unsorted).unwrap();
488
489 assert_eq!(query, "newClientOrderId=desk+alpha&symbol=LTCBTC");
490 }
491
492 #[rstest]
493 fn test_debug_redacts_secret() {
494 let cred = Credential::new("test_key".to_string(), BINANCE_TEST_SECRET.to_string());
495 let dbg_out = format!("{cred:?}");
496
497 assert!(dbg_out.contains(REDACTED));
498 assert!(!dbg_out.contains("NhqPtmdSJYdKjVHjA7PZj4"));
499 }
500
501 const ED25519_PKCS8_TEST_VECTOR: [u8; 48] = [
506 0x30, 0x2e, 0x02, 0x01, 0x00, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x04, 0x22, 0x04,
507 0x20, 0x9d, 0x61, 0xb1, 0x9d, 0xef, 0xfd, 0x5a, 0x60, 0xba, 0x84, 0x4a, 0xf4, 0x92, 0xec,
508 0x2c, 0xc4, 0x44, 0x49, 0xc5, 0x69, 0x7b, 0x32, 0x69, 0x19, 0x70, 0x3b, 0xac, 0x03, 0x1c,
509 0xae, 0x7f, 0x60,
510 ];
511
512 #[rstest]
513 fn test_ed25519_matches_rfc_8032_vector() {
514 let key_b64 = base64::Engine::encode(
515 &base64::engine::general_purpose::STANDARD,
516 ED25519_PKCS8_TEST_VECTOR,
517 );
518
519 let cred = Ed25519Credential::new("test_key".to_string(), &key_b64).unwrap();
520
521 let signature = cred.sign(b"");
522
523 assert_eq!(
524 signature,
525 "5VZDAMNgrHKQhuLMgG6CioSHfx645dl02HPgZSJJAVVfuIIVkKM7rMYeOXAc+bRr0lv18FlbviRlUUFDjnoQCw=="
526 );
527 }
528
529 #[rstest]
530 fn test_ed25519_rejects_raw_32_byte_seed() {
531 let seed = base64::Engine::encode(&base64::engine::general_purpose::STANDARD, [0xABu8; 32]);
535
536 let result = Ed25519Credential::new("test_key".to_string(), &seed);
537
538 assert!(matches!(result, Err(Ed25519CredentialError::NotEd25519)));
539 }
540
541 #[rstest]
542 fn test_ed25519_rejects_binance_hmac_secret() {
543 let result = Ed25519Credential::new("test_key".to_string(), BINANCE_TEST_SECRET);
547
548 assert!(matches!(result, Err(Ed25519CredentialError::NotEd25519)));
549 }
550
551 #[rstest]
552 fn test_signing_credential_autodetect_falls_back_to_hmac_on_binance_secret() {
553 let cred = SigningCredential::new("test_key".to_string(), BINANCE_TEST_SECRET.to_string());
557
558 assert!(matches!(cred, SigningCredential::Hmac(_)));
559 }
560
561 #[rstest]
562 fn test_ed25519_debug_redacts_secret() {
563 let key_b64 = base64::Engine::encode(
564 &base64::engine::general_purpose::STANDARD,
565 ED25519_PKCS8_TEST_VECTOR,
566 );
567
568 let cred = Ed25519Credential::new("test_key".to_string(), &key_b64).unwrap();
569 let dbg_out = format!("{cred:?}");
570
571 assert!(dbg_out.contains(REDACTED));
572 assert!(!dbg_out.contains(&key_b64));
573 }
574}