nautilus_binance/common/
credential.rs1#![allow(unused_assignments)] use std::fmt::{Debug, Display};
29
30use aws_lc_rs::hmac;
31use ed25519_dalek::{Signature, Signer, SigningKey};
32use ustr::Ustr;
33use zeroize::ZeroizeOnDrop;
34
35use super::enums::{BinanceEnvironment, BinanceProductType};
36
37pub fn resolve_credentials(
58 config_api_key: Option<String>,
59 config_api_secret: Option<String>,
60 environment: BinanceEnvironment,
61 product_type: BinanceProductType,
62) -> anyhow::Result<(String, String)> {
63 if let (Some(key), Some(secret)) = (config_api_key.clone(), config_api_secret.clone()) {
64 return Ok((key, secret));
65 }
66
67 let (deprecated_key_var, deprecated_secret_var, standard_key_var, standard_secret_var) =
68 match environment {
69 BinanceEnvironment::Testnet => match product_type {
70 BinanceProductType::Spot
71 | BinanceProductType::Margin
72 | BinanceProductType::Options => (
73 "BINANCE_TESTNET_ED25519_API_KEY",
74 "BINANCE_TESTNET_ED25519_API_SECRET",
75 "BINANCE_TESTNET_API_KEY",
76 "BINANCE_TESTNET_API_SECRET",
77 ),
78 BinanceProductType::UsdM | BinanceProductType::CoinM => (
79 "BINANCE_FUTURES_TESTNET_ED25519_API_KEY",
80 "BINANCE_FUTURES_TESTNET_ED25519_API_SECRET",
81 "BINANCE_FUTURES_TESTNET_API_KEY",
82 "BINANCE_FUTURES_TESTNET_API_SECRET",
83 ),
84 },
85
86 BinanceEnvironment::Demo => ("", "", "BINANCE_DEMO_API_KEY", "BINANCE_DEMO_API_SECRET"),
88 BinanceEnvironment::Mainnet => (
89 "BINANCE_ED25519_API_KEY",
90 "BINANCE_ED25519_API_SECRET",
91 "BINANCE_API_KEY",
92 "BINANCE_API_SECRET",
93 ),
94 };
95
96 let api_key = config_api_key
97 .or_else(|| std::env::var(standard_key_var).ok())
98 .or_else(|| {
99 std::env::var(deprecated_key_var).ok().inspect(|_| {
100 log::warn!(
101 "'{deprecated_key_var}' is deprecated, \
102 use '{standard_key_var}' instead"
103 );
104 })
105 })
106 .ok_or_else(|| anyhow::anyhow!("{standard_key_var} not found in config or environment"))?;
107
108 let api_secret = config_api_secret
109 .or_else(|| std::env::var(standard_secret_var).ok())
110 .or_else(|| {
111 std::env::var(deprecated_secret_var).ok().inspect(|_| {
112 log::warn!(
113 "'{deprecated_secret_var}' is deprecated, \
114 use '{standard_secret_var}' instead"
115 );
116 })
117 })
118 .ok_or_else(|| {
119 anyhow::anyhow!("{standard_secret_var} not found in config or environment")
120 })?;
121
122 Ok((api_key, api_secret))
123}
124
125#[derive(Clone, ZeroizeOnDrop)]
129pub struct Credential {
130 #[zeroize(skip)]
131 pub api_key: Ustr,
132 api_secret: Box<[u8]>,
133}
134
135#[derive(ZeroizeOnDrop)]
140pub struct Ed25519Credential {
141 #[zeroize(skip)]
142 pub api_key: Ustr,
143 signing_key: SigningKey,
144}
145
146impl Debug for Credential {
147 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
148 f.debug_struct(stringify!(Credential))
149 .field("api_key", &self.api_key)
150 .field("api_secret", &"<redacted>")
151 .finish()
152 }
153}
154
155impl Credential {
156 #[must_use]
158 pub fn new(api_key: String, api_secret: String) -> Self {
159 Self {
160 api_key: api_key.into(),
161 api_secret: api_secret.into_bytes().into_boxed_slice(),
162 }
163 }
164
165 #[must_use]
167 pub fn api_key(&self) -> &str {
168 self.api_key.as_str()
169 }
170
171 #[must_use]
173 pub fn sign(&self, message: &str) -> String {
174 let key = hmac::Key::new(hmac::HMAC_SHA256, &self.api_secret);
175 let tag = hmac::sign(&key, message.as_bytes());
176 hex::encode(tag.as_ref())
177 }
178}
179
180impl Debug for Ed25519Credential {
181 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
182 f.debug_struct(stringify!(Ed25519Credential))
183 .field("api_key", &self.api_key)
184 .field("signing_key", &"<redacted>")
185 .finish()
186 }
187}
188
189impl Ed25519Credential {
190 pub fn new(api_key: String, private_key_base64: &str) -> Result<Self, Ed25519CredentialError> {
204 let key_data: String = private_key_base64
206 .lines()
207 .filter(|line| !line.starts_with("-----"))
208 .collect();
209
210 let private_key_bytes =
211 base64::Engine::decode(&base64::engine::general_purpose::STANDARD, &key_data)
212 .map_err(|e| Ed25519CredentialError::InvalidBase64(e.to_string()))?;
213
214 if private_key_bytes.len() < 32 {
216 return Err(Ed25519CredentialError::InvalidKeyLength);
217 }
218 let seed_start = private_key_bytes.len() - 32;
219 let key_bytes: [u8; 32] = private_key_bytes[seed_start..]
220 .try_into()
221 .map_err(|_| Ed25519CredentialError::InvalidKeyLength)?;
222
223 let signing_key = SigningKey::from_bytes(&key_bytes);
224
225 Ok(Self {
226 api_key: api_key.into(),
227 signing_key,
228 })
229 }
230
231 #[must_use]
233 pub fn api_key(&self) -> &str {
234 self.api_key.as_str()
235 }
236
237 #[must_use]
239 pub fn sign(&self, message: &[u8]) -> String {
240 let signature: Signature = self.signing_key.sign(message);
241 base64::Engine::encode(
242 &base64::engine::general_purpose::STANDARD,
243 signature.to_bytes(),
244 )
245 }
246}
247
248#[derive(Debug, Clone)]
250pub enum Ed25519CredentialError {
251 InvalidBase64(String),
253 InvalidKeyLength,
255}
256
257impl Display for Ed25519CredentialError {
258 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
259 match self {
260 Self::InvalidBase64(e) => write!(f, "Invalid base64 encoding: {e}"),
261 Self::InvalidKeyLength => write!(f, "Ed25519 private key must be 32 bytes"),
262 }
263 }
264}
265
266impl std::error::Error for Ed25519CredentialError {}
267
268#[cfg(test)]
269mod tests {
270 use rstest::rstest;
271
272 use super::*;
273
274 const BINANCE_TEST_SECRET: &str =
277 "NhqPtmdSJYdKjVHjA7PZj4Mge3R5YNiP1e3UZjInClVN65XAbvqqM6A7H5fATj0j";
278
279 #[rstest]
280 fn test_sign_matches_binance_test_vector_simple() {
281 let cred = Credential::new("test_key".to_string(), BINANCE_TEST_SECRET.to_string());
282 let message = "timestamp=1578963600000";
283 let expected = "d84e6641b1e328e7b418fff030caed655c266299c9355e36ce801ed14631eed4";
284
285 assert_eq!(cred.sign(message), expected);
286 }
287
288 #[rstest]
289 fn test_sign_matches_binance_test_vector_order() {
290 let cred = Credential::new("test_key".to_string(), BINANCE_TEST_SECRET.to_string());
291 let message = "symbol=LTCBTC&side=BUY&type=LIMIT&timeInForce=GTC&quantity=1&price=0.1&recvWindow=5000×tamp=1499827319559";
292 let expected = "c8db56825ae71d6d79447849e617115f4a920fa2acdcab2b053c4b2838bd6b71";
293
294 assert_eq!(cred.sign(message), expected);
295 }
296}