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> {
103 let private_key_bytes = base64::Engine::decode(
104 &base64::engine::general_purpose::STANDARD,
105 private_key_base64,
106 )
107 .map_err(|e| Ed25519CredentialError::InvalidBase64(e.to_string()))?;
108
109 let key_bytes: [u8; 32] = private_key_bytes
110 .try_into()
111 .map_err(|_| Ed25519CredentialError::InvalidKeyLength)?;
112
113 let signing_key = SigningKey::from_bytes(&key_bytes);
114
115 Ok(Self {
116 api_key: api_key.into(),
117 signing_key,
118 })
119 }
120
121 #[must_use]
123 pub fn api_key(&self) -> &str {
124 self.api_key.as_str()
125 }
126
127 #[must_use]
129 pub fn sign(&self, message: &[u8]) -> String {
130 let signature: Signature = self.signing_key.sign(message);
131 base64::Engine::encode(
132 &base64::engine::general_purpose::STANDARD,
133 signature.to_bytes(),
134 )
135 }
136}
137
138#[derive(Debug, Clone)]
140pub enum Ed25519CredentialError {
141 InvalidBase64(String),
143 InvalidKeyLength,
145}
146
147impl std::fmt::Display for Ed25519CredentialError {
148 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
149 match self {
150 Self::InvalidBase64(e) => write!(f, "Invalid base64 encoding: {e}"),
151 Self::InvalidKeyLength => write!(f, "Ed25519 private key must be 32 bytes"),
152 }
153 }
154}
155
156impl std::error::Error for Ed25519CredentialError {}
157
158#[cfg(test)]
159mod tests {
160 use rstest::rstest;
161
162 use super::*;
163
164 const BINANCE_TEST_SECRET: &str =
167 "NhqPtmdSJYdKjVHjA7PZj4Mge3R5YNiP1e3UZjInClVN65XAbvqqM6A7H5fATj0j";
168
169 #[rstest]
170 fn test_sign_matches_binance_test_vector_simple() {
171 let cred = Credential::new("test_key".to_string(), BINANCE_TEST_SECRET.to_string());
172 let message = "timestamp=1578963600000";
173 let expected = "d84e6641b1e328e7b418fff030caed655c266299c9355e36ce801ed14631eed4";
174
175 assert_eq!(cred.sign(message), expected);
176 }
177
178 #[rstest]
179 fn test_sign_matches_binance_test_vector_order() {
180 let cred = Credential::new("test_key".to_string(), BINANCE_TEST_SECRET.to_string());
181 let message = "symbol=LTCBTC&side=BUY&type=LIMIT&timeInForce=GTC&quantity=1&price=0.1&recvWindow=5000×tamp=1499827319559";
182 let expected = "c8db56825ae71d6d79447849e617115f4a920fa2acdcab2b053c4b2838bd6b71";
183
184 assert_eq!(cred.sign(message), expected);
185 }
186}