#![allow(unused_assignments)]
use std::fmt::{Debug, Display};
use aws_lc_rs::hmac;
use ed25519_dalek::{Signature, Signer, SigningKey};
use nautilus_core::{hex, string::REDACTED};
use zeroize::ZeroizeOnDrop;
use super::enums::{BinanceEnvironment, BinanceProductType};
pub fn resolve_credentials(
config_api_key: Option<String>,
config_api_secret: Option<String>,
environment: BinanceEnvironment,
product_type: BinanceProductType,
) -> anyhow::Result<(String, String)> {
if let (Some(key), Some(secret)) = (config_api_key.clone(), config_api_secret.clone()) {
return Ok((key, secret));
}
let (deprecated_key_var, deprecated_secret_var, standard_key_var, standard_secret_var) =
match environment {
BinanceEnvironment::Testnet => match product_type {
BinanceProductType::Spot
| BinanceProductType::Margin
| BinanceProductType::Options => (
"BINANCE_TESTNET_ED25519_API_KEY",
"BINANCE_TESTNET_ED25519_API_SECRET",
"BINANCE_TESTNET_API_KEY",
"BINANCE_TESTNET_API_SECRET",
),
BinanceProductType::UsdM | BinanceProductType::CoinM => (
"BINANCE_FUTURES_TESTNET_ED25519_API_KEY",
"BINANCE_FUTURES_TESTNET_ED25519_API_SECRET",
"BINANCE_FUTURES_TESTNET_API_KEY",
"BINANCE_FUTURES_TESTNET_API_SECRET",
),
},
BinanceEnvironment::Demo => ("", "", "BINANCE_DEMO_API_KEY", "BINANCE_DEMO_API_SECRET"),
BinanceEnvironment::Mainnet => (
"BINANCE_ED25519_API_KEY",
"BINANCE_ED25519_API_SECRET",
"BINANCE_API_KEY",
"BINANCE_API_SECRET",
),
};
let is_futures = matches!(
product_type,
BinanceProductType::UsdM | BinanceProductType::CoinM
);
let api_key = config_api_key
.or_else(|| std::env::var(standard_key_var).ok())
.or_else(|| resolve_deprecated_var(deprecated_key_var, standard_key_var, is_futures))
.ok_or_else(|| anyhow::anyhow!("{standard_key_var} not found in config or environment"))?;
let api_secret = config_api_secret
.or_else(|| std::env::var(standard_secret_var).ok())
.or_else(|| resolve_deprecated_var(deprecated_secret_var, standard_secret_var, is_futures))
.ok_or_else(|| {
anyhow::anyhow!("{standard_secret_var} not found in config or environment")
})?;
Ok((api_key, api_secret))
}
fn resolve_deprecated_var(
deprecated_var: &str,
standard_var: &str,
allow_fallback: bool,
) -> Option<String> {
if deprecated_var.is_empty() {
return None;
}
let value = std::env::var(deprecated_var).ok()?;
if allow_fallback {
log::warn!(
"'{deprecated_var}' is deprecated and will be removed in a future version. \
Rename it to '{standard_var}' (Ed25519 keys are now auto-detected)"
);
Some(value)
} else {
log::error!(
"'{deprecated_var}' has been removed. \
Rename it to '{standard_var}' (Ed25519 keys are now auto-detected)"
);
None
}
}
#[derive(Clone, ZeroizeOnDrop)]
pub struct Credential {
api_key: Box<str>,
api_secret: Box<[u8]>,
}
#[derive(ZeroizeOnDrop)]
pub struct Ed25519Credential {
api_key: Box<str>,
signing_key: SigningKey,
}
impl Debug for Credential {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct(stringify!(Credential))
.field("api_key", &self.api_key)
.field("api_secret", &REDACTED)
.finish()
}
}
impl Credential {
#[must_use]
pub fn new(api_key: String, api_secret: String) -> Self {
Self {
api_key: api_key.into_boxed_str(),
api_secret: api_secret.into_bytes().into_boxed_slice(),
}
}
#[must_use]
pub fn api_key(&self) -> &str {
&self.api_key
}
#[must_use]
pub fn sign(&self, message: &str) -> String {
let key = hmac::Key::new(hmac::HMAC_SHA256, &self.api_secret);
let tag = hmac::sign(&key, message.as_bytes());
hex::encode(tag.as_ref())
}
}
impl Debug for Ed25519Credential {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct(stringify!(Ed25519Credential))
.field("api_key", &self.api_key)
.field("signing_key", &REDACTED)
.finish()
}
}
impl Ed25519Credential {
pub fn new(api_key: String, private_key_base64: &str) -> Result<Self, Ed25519CredentialError> {
let key_data: String = private_key_base64
.lines()
.filter(|line| !line.starts_with("-----"))
.collect();
let private_key_bytes =
base64::Engine::decode(&base64::engine::general_purpose::STANDARD, &key_data)
.map_err(|e| Ed25519CredentialError::InvalidBase64(e.to_string()))?;
if private_key_bytes.len() < 32 {
return Err(Ed25519CredentialError::InvalidKeyLength);
}
let seed_start = private_key_bytes.len() - 32;
let key_bytes: [u8; 32] = private_key_bytes[seed_start..]
.try_into()
.map_err(|_| Ed25519CredentialError::InvalidKeyLength)?;
let signing_key = SigningKey::from_bytes(&key_bytes);
Ok(Self {
api_key: api_key.into_boxed_str(),
signing_key,
})
}
#[must_use]
pub fn api_key(&self) -> &str {
&self.api_key
}
#[must_use]
pub fn sign(&self, message: &[u8]) -> String {
let signature: Signature = self.signing_key.sign(message);
base64::Engine::encode(
&base64::engine::general_purpose::STANDARD,
signature.to_bytes(),
)
}
}
#[derive(Debug, Clone)]
pub enum Ed25519CredentialError {
InvalidBase64(String),
InvalidKeyLength,
}
impl Display for Ed25519CredentialError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::InvalidBase64(e) => write!(f, "Invalid base64 encoding: {e}"),
Self::InvalidKeyLength => write!(f, "Ed25519 private key must be 32 bytes"),
}
}
}
impl std::error::Error for Ed25519CredentialError {}
#[derive(Clone)]
pub enum SigningCredential {
Hmac(Credential),
Ed25519(Box<Ed25519Credential>),
}
impl Debug for SigningCredential {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Hmac(c) => f.debug_tuple("Hmac").field(c).finish(),
Self::Ed25519(c) => f.debug_tuple("Ed25519").field(c).finish(),
}
}
}
impl SigningCredential {
#[must_use]
pub fn new(api_key: String, api_secret: String) -> Self {
match Ed25519Credential::new(api_key.clone(), &api_secret) {
Ok(ed25519) => {
log::info!("Auto-detected Ed25519 API key");
Self::Ed25519(Box::new(ed25519))
}
Err(_) => {
log::info!("Using HMAC SHA256 API key");
Self::Hmac(Credential::new(api_key, api_secret))
}
}
}
#[must_use]
pub fn api_key(&self) -> &str {
match self {
Self::Hmac(c) => c.api_key(),
Self::Ed25519(c) => c.api_key(),
}
}
#[must_use]
pub fn sign(&self, message: &str) -> String {
match self {
Self::Hmac(c) => c.sign(message),
Self::Ed25519(c) => c.sign(message.as_bytes()),
}
}
#[must_use]
pub fn is_ed25519(&self) -> bool {
matches!(self, Self::Ed25519(_))
}
}
impl Clone for Ed25519Credential {
fn clone(&self) -> Self {
let key_bytes = self.signing_key.to_bytes();
Self {
api_key: self.api_key.clone(),
signing_key: SigningKey::from_bytes(&key_bytes),
}
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
const BINANCE_TEST_SECRET: &str =
"NhqPtmdSJYdKjVHjA7PZj4Mge3R5YNiP1e3UZjInClVN65XAbvqqM6A7H5fATj0j";
#[rstest]
fn test_sign_matches_binance_test_vector_simple() {
let cred = Credential::new("test_key".to_string(), BINANCE_TEST_SECRET.to_string());
let message = "timestamp=1578963600000";
let expected = "d84e6641b1e328e7b418fff030caed655c266299c9355e36ce801ed14631eed4";
assert_eq!(cred.sign(message), expected);
}
#[rstest]
fn test_sign_matches_binance_test_vector_order() {
let cred = Credential::new("test_key".to_string(), BINANCE_TEST_SECRET.to_string());
let message = "symbol=LTCBTC&side=BUY&type=LIMIT&timeInForce=GTC&quantity=1&price=0.1&recvWindow=5000×tamp=1499827319559";
let expected = "c8db56825ae71d6d79447849e617115f4a920fa2acdcab2b053c4b2838bd6b71";
assert_eq!(cred.sign(message), expected);
}
#[rstest]
fn test_debug_redacts_secret() {
let cred = Credential::new("test_key".to_string(), BINANCE_TEST_SECRET.to_string());
let dbg_out = format!("{cred:?}");
assert!(dbg_out.contains(REDACTED));
assert!(!dbg_out.contains("NhqPtmdSJYdKjVHjA7PZj4"));
}
#[rstest]
fn test_ed25519_debug_redacts_secret() {
let seed = base64::Engine::encode(&base64::engine::general_purpose::STANDARD, [0xABu8; 32]);
let cred = Ed25519Credential::new("test_key".to_string(), &seed).unwrap();
let dbg_out = format!("{cred:?}");
assert!(dbg_out.contains(REDACTED));
assert!(!dbg_out.contains(&seed));
}
}