nautilus_binance/common/
credential.rs1#![allow(unused_assignments)] use std::fmt::Debug;
25
26use aws_lc_rs::hmac;
27use ed25519_dalek::{Signature, Signer, SigningKey};
28use ustr::Ustr;
29use zeroize::ZeroizeOnDrop;
30
31#[derive(Clone, ZeroizeOnDrop)]
35pub struct Credential {
36 #[zeroize(skip)]
37 pub api_key: Ustr,
38 api_secret: Box<[u8]>,
39}
40
41#[derive(ZeroizeOnDrop)]
46pub struct Ed25519Credential {
47 #[zeroize(skip)]
48 pub api_key: Ustr,
49 signing_key: SigningKey,
50}
51
52impl Debug for Credential {
53 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54 f.debug_struct(stringify!(Credential))
55 .field("api_key", &self.api_key)
56 .field("api_secret", &"<redacted>")
57 .finish()
58 }
59}
60
61impl Credential {
62 #[must_use]
64 pub fn new(api_key: String, api_secret: String) -> Self {
65 Self {
66 api_key: api_key.into(),
67 api_secret: api_secret.into_bytes().into_boxed_slice(),
68 }
69 }
70
71 #[must_use]
73 pub fn api_key(&self) -> &str {
74 self.api_key.as_str()
75 }
76
77 #[must_use]
79 pub fn sign(&self, message: &str) -> String {
80 let key = hmac::Key::new(hmac::HMAC_SHA256, &self.api_secret);
81 let tag = hmac::sign(&key, message.as_bytes());
82 hex::encode(tag.as_ref())
83 }
84}
85
86impl Debug for Ed25519Credential {
87 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
88 f.debug_struct(stringify!(Ed25519Credential))
89 .field("api_key", &self.api_key)
90 .field("signing_key", &"<redacted>")
91 .finish()
92 }
93}
94
95impl Ed25519Credential {
96 pub fn new(api_key: String, private_key_base64: &str) -> Result<Self, Ed25519CredentialError> {
110 let key_data: String = private_key_base64
112 .lines()
113 .filter(|line| !line.starts_with("-----"))
114 .collect();
115
116 let private_key_bytes =
117 base64::Engine::decode(&base64::engine::general_purpose::STANDARD, &key_data)
118 .map_err(|e| Ed25519CredentialError::InvalidBase64(e.to_string()))?;
119
120 if private_key_bytes.len() < 32 {
122 return Err(Ed25519CredentialError::InvalidKeyLength);
123 }
124 let seed_start = private_key_bytes.len() - 32;
125 let key_bytes: [u8; 32] = private_key_bytes[seed_start..]
126 .try_into()
127 .map_err(|_| Ed25519CredentialError::InvalidKeyLength)?;
128
129 let signing_key = SigningKey::from_bytes(&key_bytes);
130
131 Ok(Self {
132 api_key: api_key.into(),
133 signing_key,
134 })
135 }
136
137 #[must_use]
139 pub fn api_key(&self) -> &str {
140 self.api_key.as_str()
141 }
142
143 #[must_use]
145 pub fn sign(&self, message: &[u8]) -> String {
146 let signature: Signature = self.signing_key.sign(message);
147 base64::Engine::encode(
148 &base64::engine::general_purpose::STANDARD,
149 signature.to_bytes(),
150 )
151 }
152}
153
154#[derive(Debug, Clone)]
156pub enum Ed25519CredentialError {
157 InvalidBase64(String),
159 InvalidKeyLength,
161}
162
163impl std::fmt::Display for Ed25519CredentialError {
164 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
165 match self {
166 Self::InvalidBase64(e) => write!(f, "Invalid base64 encoding: {e}"),
167 Self::InvalidKeyLength => write!(f, "Ed25519 private key must be 32 bytes"),
168 }
169 }
170}
171
172impl std::error::Error for Ed25519CredentialError {}
173
174#[cfg(test)]
175mod tests {
176 use rstest::rstest;
177
178 use super::*;
179
180 const BINANCE_TEST_SECRET: &str =
183 "NhqPtmdSJYdKjVHjA7PZj4Mge3R5YNiP1e3UZjInClVN65XAbvqqM6A7H5fATj0j";
184
185 #[rstest]
186 fn test_sign_matches_binance_test_vector_simple() {
187 let cred = Credential::new("test_key".to_string(), BINANCE_TEST_SECRET.to_string());
188 let message = "timestamp=1578963600000";
189 let expected = "d84e6641b1e328e7b418fff030caed655c266299c9355e36ce801ed14631eed4";
190
191 assert_eq!(cred.sign(message), expected);
192 }
193
194 #[rstest]
195 fn test_sign_matches_binance_test_vector_order() {
196 let cred = Credential::new("test_key".to_string(), BINANCE_TEST_SECRET.to_string());
197 let message = "symbol=LTCBTC&side=BUY&type=LIMIT&timeInForce=GTC&quantity=1&price=0.1&recvWindow=5000×tamp=1499827319559";
198 let expected = "c8db56825ae71d6d79447849e617115f4a920fa2acdcab2b053c4b2838bd6b71";
199
200 assert_eq!(cred.sign(message), expected);
201 }
202}