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::Mainnet => (
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::info!("Auto-detected Ed25519 API key");
332 Self::Ed25519(Box::new(ed25519))
333 }
334 Err(_) => {
335 log::info!("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
389#[cfg(test)]
390mod tests {
391 use rstest::rstest;
392
393 use super::*;
394
395 const BINANCE_TEST_SECRET: &str =
398 "NhqPtmdSJYdKjVHjA7PZj4Mge3R5YNiP1e3UZjInClVN65XAbvqqM6A7H5fATj0j";
399
400 #[rstest]
401 fn test_sign_matches_binance_test_vector_simple() {
402 let cred = Credential::new("test_key".to_string(), BINANCE_TEST_SECRET.to_string());
403 let message = "timestamp=1578963600000";
404 let expected = "d84e6641b1e328e7b418fff030caed655c266299c9355e36ce801ed14631eed4";
405
406 assert_eq!(cred.sign(message), expected);
407 }
408
409 #[rstest]
410 fn test_sign_matches_binance_test_vector_order() {
411 let cred = Credential::new("test_key".to_string(), BINANCE_TEST_SECRET.to_string());
412 let message = "symbol=LTCBTC&side=BUY&type=LIMIT&timeInForce=GTC&quantity=1&price=0.1&recvWindow=5000×tamp=1499827319559";
413 let expected = "c8db56825ae71d6d79447849e617115f4a920fa2acdcab2b053c4b2838bd6b71";
414
415 assert_eq!(cred.sign(message), expected);
416 }
417
418 #[rstest]
419 fn test_debug_redacts_secret() {
420 let cred = Credential::new("test_key".to_string(), BINANCE_TEST_SECRET.to_string());
421 let dbg_out = format!("{cred:?}");
422
423 assert!(dbg_out.contains(REDACTED));
424 assert!(!dbg_out.contains("NhqPtmdSJYdKjVHjA7PZj4"));
425 }
426
427 const ED25519_PKCS8_TEST_VECTOR: [u8; 48] = [
432 0x30, 0x2e, 0x02, 0x01, 0x00, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x04, 0x22, 0x04,
433 0x20, 0x9d, 0x61, 0xb1, 0x9d, 0xef, 0xfd, 0x5a, 0x60, 0xba, 0x84, 0x4a, 0xf4, 0x92, 0xec,
434 0x2c, 0xc4, 0x44, 0x49, 0xc5, 0x69, 0x7b, 0x32, 0x69, 0x19, 0x70, 0x3b, 0xac, 0x03, 0x1c,
435 0xae, 0x7f, 0x60,
436 ];
437
438 #[rstest]
439 fn test_ed25519_accepts_pkcs8_wrapped_key() {
440 let key_b64 = base64::Engine::encode(
441 &base64::engine::general_purpose::STANDARD,
442 ED25519_PKCS8_TEST_VECTOR,
443 );
444
445 let cred = Ed25519Credential::new("test_key".to_string(), &key_b64).unwrap();
446
447 let signature = cred.sign(b"hello");
448 assert!(!signature.is_empty());
449 }
450
451 #[rstest]
452 fn test_ed25519_rejects_raw_32_byte_seed() {
453 let seed = base64::Engine::encode(&base64::engine::general_purpose::STANDARD, [0xABu8; 32]);
457
458 let result = Ed25519Credential::new("test_key".to_string(), &seed);
459
460 assert!(matches!(result, Err(Ed25519CredentialError::NotEd25519)));
461 }
462
463 #[rstest]
464 fn test_ed25519_rejects_binance_hmac_secret() {
465 let result = Ed25519Credential::new("test_key".to_string(), BINANCE_TEST_SECRET);
469
470 assert!(matches!(result, Err(Ed25519CredentialError::NotEd25519)));
471 }
472
473 #[rstest]
474 fn test_signing_credential_autodetect_falls_back_to_hmac_on_binance_secret() {
475 let cred = SigningCredential::new("test_key".to_string(), BINANCE_TEST_SECRET.to_string());
479
480 assert!(matches!(cred, SigningCredential::Hmac(_)));
481 }
482
483 #[rstest]
484 fn test_ed25519_debug_redacts_secret() {
485 let key_b64 = base64::Engine::encode(
486 &base64::engine::general_purpose::STANDARD,
487 ED25519_PKCS8_TEST_VECTOR,
488 );
489
490 let cred = Ed25519Credential::new("test_key".to_string(), &key_b64).unwrap();
491 let dbg_out = format!("{cred:?}");
492
493 assert!(dbg_out.contains(REDACTED));
494 assert!(!dbg_out.contains(&key_b64));
495 }
496}