Skip to main content

nautilus_bitmex/common/
credential.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! API credential utilities for signing BitMEX requests.
17
18#![allow(unused_assignments)] // Fields are accessed externally, false positive from nightly
19
20use std::fmt::Debug;
21
22use aws_lc_rs::hmac;
23use nautilus_core::{
24    env::resolve_env_var_pair,
25    hex,
26    string::secret::{REDACTED, mask_api_key},
27};
28use zeroize::ZeroizeOnDrop;
29
30use crate::common::enums::BitmexEnvironment;
31
32/// Returns the environment variable names for API credentials,
33/// based on the environment.
34#[must_use]
35pub fn credential_env_vars(environment: BitmexEnvironment) -> (&'static str, &'static str) {
36    match environment {
37        BitmexEnvironment::Testnet => ("BITMEX_TESTNET_API_KEY", "BITMEX_TESTNET_API_SECRET"),
38        BitmexEnvironment::Mainnet => ("BITMEX_API_KEY", "BITMEX_API_SECRET"),
39    }
40}
41
42/// BitMEX API credentials for signing requests.
43///
44/// Uses HMAC SHA256 for request signing as per BitMEX API specifications.
45/// Secrets are automatically zeroized on drop for security.
46#[derive(Clone, ZeroizeOnDrop)]
47pub struct Credential {
48    api_key: Box<str>,
49    api_secret: Box<[u8]>,
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    /// Creates a new [`Credential`] instance.
63    #[must_use]
64    pub fn new(api_key: String, api_secret: String) -> Self {
65        Self {
66            api_key: api_key.into_boxed_str(),
67            api_secret: api_secret.into_bytes().into_boxed_slice(),
68        }
69    }
70
71    /// Resolves credentials from provided values or environment variables.
72    #[must_use]
73    pub fn resolve(
74        api_key: Option<String>,
75        api_secret: Option<String>,
76        environment: BitmexEnvironment,
77    ) -> Option<Self> {
78        let (key_var, secret_var) = credential_env_vars(environment);
79        let (k, s) = resolve_env_var_pair(api_key, api_secret, key_var, secret_var)?;
80        Some(Self::new(k, s))
81    }
82
83    /// Returns the API key.
84    #[must_use]
85    pub fn api_key(&self) -> &str {
86        &self.api_key
87    }
88
89    /// Signs a request message according to the BitMEX authentication scheme.
90    #[must_use]
91    pub fn sign(&self, verb: &str, endpoint: &str, expires: i64, data: &str) -> String {
92        let sign_message = format!("{verb}{endpoint}{expires}{data}");
93        let key = hmac::Key::new(hmac::HMAC_SHA256, &self.api_secret[..]);
94        let signature = hmac::sign(&key, sign_message.as_bytes());
95        hex::encode(signature.as_ref())
96    }
97
98    /// Returns a masked version of the API key for logging purposes.
99    ///
100    /// Shows first 4 and last 4 characters with ellipsis in between.
101    /// For keys shorter than 8 characters, shows asterisks only.
102    #[must_use]
103    pub fn api_key_masked(&self) -> String {
104        mask_api_key(&self.api_key)
105    }
106}
107
108/// Tests use examples from <https://www.bitmex.com/app/apiKeysUsage>.
109#[cfg(test)]
110mod tests {
111    use rstest::rstest;
112
113    use super::*;
114    use crate::common::testing::load_test_json;
115
116    const API_KEY: &str = "LAqUlngMIQkIUjXMUreyu3qn";
117    const API_SECRET: &str = "chNOOS4KvNXR_Xq4k4c9qsfoKWvnDecLATCRlcBwyKDYnWgO";
118
119    #[rstest]
120    fn test_simple_get() {
121        let credential = Credential::new(API_KEY.to_string(), API_SECRET.to_string());
122
123        let signature = credential.sign("GET", "/api/v1/instrument", 1518064236, "");
124
125        assert_eq!(
126            signature,
127            "c7682d435d0cfe87c16098df34ef2eb5a549d4c5a3c2b1f0f77b8af73423bf00"
128        );
129    }
130
131    #[rstest]
132    fn test_get_with_query() {
133        let credential = Credential::new(API_KEY.to_string(), API_SECRET.to_string());
134
135        let signature = credential.sign(
136            "GET",
137            "/api/v1/instrument?filter=%7B%22symbol%22%3A+%22XBTM15%22%7D",
138            1518064237,
139            "",
140        );
141
142        assert_eq!(
143            signature,
144            "e2f422547eecb5b3cb29ade2127e21b858b235b386bfa45e1c1756eb3383919f"
145        );
146    }
147
148    #[rstest]
149    fn test_post_with_data() {
150        let credential = Credential::new(API_KEY.to_string(), API_SECRET.to_string());
151
152        let data = load_test_json("credential_post_order.json");
153
154        let signature = credential.sign("POST", "/api/v1/order", 1518064238, data.trim_end());
155
156        assert_eq!(
157            signature,
158            "1749cd2ccae4aa49048ae09f0b95110cee706e0944e6a14ad0b3a8cb45bd336b"
159        );
160    }
161
162    #[rstest]
163    fn test_debug_redacts_secret() {
164        let credential = Credential::new(API_KEY.to_string(), API_SECRET.to_string());
165        let dbg_out = format!("{credential:?}");
166        assert!(dbg_out.contains("api_secret: \"<redacted>\""));
167        assert!(!dbg_out.contains("chNOO"));
168        let secret_bytes_dbg = format!("{:?}", API_SECRET.as_bytes());
169        assert!(
170            !dbg_out.contains(&secret_bytes_dbg),
171            "Debug output must not contain raw secret bytes"
172        );
173    }
174
175    use crate::common::enums::BitmexEnvironment;
176
177    #[rstest]
178    fn test_resolve_with_both_args() {
179        let result = Credential::resolve(
180            Some("my_key".to_string()),
181            Some("my_secret".to_string()),
182            BitmexEnvironment::Mainnet,
183        );
184
185        assert!(result.is_some());
186        assert_eq!(result.unwrap().api_key(), "my_key");
187    }
188
189    #[rstest]
190    fn test_resolve_with_no_args_no_env() {
191        let (key_var, secret_var) = credential_env_vars(BitmexEnvironment::Mainnet);
192        if std::env::var(key_var).is_ok() || std::env::var(secret_var).is_ok() {
193            return;
194        }
195
196        let result = Credential::resolve(None, None, BitmexEnvironment::Mainnet);
197
198        assert!(result.is_none());
199    }
200}