nautilus_bitmex/common/
credential.rs1use std::fmt::Debug;
19
20use aws_lc_rs::hmac;
21use ustr::Ustr;
22use zeroize::ZeroizeOnDrop;
23
24#[derive(Clone, ZeroizeOnDrop)]
29pub struct Credential {
30 #[zeroize(skip)]
31 pub api_key: Ustr,
32 api_secret: Box<[u8]>,
33}
34
35impl Debug for Credential {
36 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37 f.debug_struct(stringify!(Credential))
38 .field("api_key", &self.api_key)
39 .field("api_secret", &"<redacted>")
40 .finish()
41 }
42}
43
44impl Credential {
45 #[must_use]
47 pub fn new(api_key: String, api_secret: String) -> Self {
48 let boxed: Box<[u8]> = api_secret.into_bytes().into_boxed_slice();
49
50 Self {
51 api_key: api_key.into(),
52 api_secret: boxed,
53 }
54 }
55
56 #[must_use]
58 pub fn sign(&self, verb: &str, endpoint: &str, expires: i64, data: &str) -> String {
59 let sign_message = format!("{verb}{endpoint}{expires}{data}");
60 let key = hmac::Key::new(hmac::HMAC_SHA256, &self.api_secret[..]);
61 let signature = hmac::sign(&key, sign_message.as_bytes());
62 hex::encode(signature.as_ref())
63 }
64}
65
66#[cfg(test)]
72mod tests {
73 use rstest::rstest;
74
75 use super::*;
76 use crate::common::testing::load_test_json;
77
78 const API_KEY: &str = "LAqUlngMIQkIUjXMUreyu3qn";
79 const API_SECRET: &str = "chNOOS4KvNXR_Xq4k4c9qsfoKWvnDecLATCRlcBwyKDYnWgO";
80
81 #[rstest]
82 fn test_simple_get() {
83 let credential = Credential::new(API_KEY.to_string(), API_SECRET.to_string());
84
85 let signature = credential.sign("GET", "/api/v1/instrument", 1518064236, "");
86
87 assert_eq!(
88 signature,
89 "c7682d435d0cfe87c16098df34ef2eb5a549d4c5a3c2b1f0f77b8af73423bf00"
90 );
91 }
92
93 #[rstest]
94 fn test_get_with_query() {
95 let credential = Credential::new(API_KEY.to_string(), API_SECRET.to_string());
96
97 let signature = credential.sign(
98 "GET",
99 "/api/v1/instrument?filter=%7B%22symbol%22%3A+%22XBTM15%22%7D",
100 1518064237,
101 "",
102 );
103
104 assert_eq!(
105 signature,
106 "e2f422547eecb5b3cb29ade2127e21b858b235b386bfa45e1c1756eb3383919f"
107 );
108 }
109
110 #[rstest]
111 fn test_post_with_data() {
112 let credential = Credential::new(API_KEY.to_string(), API_SECRET.to_string());
113
114 let data = load_test_json("credential_post_order.json");
115
116 let signature = credential.sign("POST", "/api/v1/order", 1518064238, data.trim_end());
117
118 assert_eq!(
119 signature,
120 "1749cd2ccae4aa49048ae09f0b95110cee706e0944e6a14ad0b3a8cb45bd336b"
121 );
122 }
123
124 #[rstest]
125 fn test_debug_redacts_secret() {
126 let credential = Credential::new(API_KEY.to_string(), API_SECRET.to_string());
127 let dbg_out = format!("{credential:?}");
128 assert!(dbg_out.contains("api_secret: \"<redacted>\""));
129 assert!(!dbg_out.contains("chNOO"));
130 let secret_bytes_dbg = format!("{:?}", API_SECRET.as_bytes());
131 assert!(
132 !dbg_out.contains(&secret_bytes_dbg),
133 "Debug output must not contain raw secret bytes"
134 );
135 }
136}