Skip to main content

nautilus_architect_ax/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//! AX Exchange API credential storage for bearer token authentication.
17
18use core::fmt::Debug;
19
20use zeroize::ZeroizeOnDrop;
21
22/// API credentials required for Ax bearer token authentication.
23///
24/// Ax uses bearer token authentication where the API key and secret
25/// are used to obtain a session token that is then used in the Authorization header.
26#[derive(Clone, ZeroizeOnDrop)]
27pub struct Credential {
28    api_key: Box<str>,
29    api_secret: Box<str>,
30}
31
32impl Debug for Credential {
33    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34        f.debug_struct(stringify!(Credential))
35            .field("api_key", &self.masked_api_key())
36            .field("api_secret", &"<redacted>")
37            .finish()
38    }
39}
40
41impl Credential {
42    /// Creates a new [`Credential`] instance from the API key and secret.
43    #[must_use]
44    pub fn new(api_key: impl Into<String>, api_secret: impl Into<String>) -> Self {
45        Self {
46            api_key: api_key.into().into_boxed_str(),
47            api_secret: api_secret.into().into_boxed_str(),
48        }
49    }
50
51    /// Returns the API key associated with this credential.
52    #[must_use]
53    pub fn api_key(&self) -> &str {
54        &self.api_key
55    }
56
57    /// Returns the API secret associated with this credential.
58    ///
59    /// # Safety
60    ///
61    /// The secret should be handled carefully and never logged or exposed.
62    #[must_use]
63    pub fn api_secret(&self) -> &str {
64        &self.api_secret
65    }
66
67    /// Returns a masked version of the API key for logging purposes.
68    ///
69    /// Shows first 4 and last 4 characters with ellipsis in between.
70    /// For keys shorter than 8 characters, shows asterisks only.
71    #[must_use]
72    pub fn masked_api_key(&self) -> String {
73        let key = self.api_key.as_ref();
74        let len = key.len();
75
76        if len <= 8 {
77            "*".repeat(len)
78        } else {
79            format!("{}...{}", &key[..4], &key[len - 4..])
80        }
81    }
82}
83
84#[cfg(test)]
85mod tests {
86    use rstest::rstest;
87
88    use super::*;
89
90    const API_KEY: &str = "test_api_key_123";
91    const API_SECRET: &str = "test_secret_456";
92
93    #[rstest]
94    fn test_credential_creation() {
95        let credential = Credential::new(API_KEY, API_SECRET);
96
97        assert_eq!(credential.api_key(), API_KEY);
98        assert_eq!(credential.api_secret(), API_SECRET);
99    }
100
101    #[rstest]
102    fn test_masked_api_key() {
103        let credential = Credential::new(API_KEY, API_SECRET);
104        let masked = credential.masked_api_key();
105
106        assert_eq!(masked, "test..._123");
107        assert!(!masked.contains("api_key"));
108    }
109
110    #[rstest]
111    fn test_masked_api_key_short() {
112        let credential = Credential::new("short", API_SECRET);
113        let masked = credential.masked_api_key();
114
115        assert_eq!(masked, "*****");
116    }
117
118    #[rstest]
119    fn test_debug_does_not_leak_secret() {
120        let credential = Credential::new(API_KEY, API_SECRET);
121        let debug_string = format!("{credential:?}");
122
123        assert!(!debug_string.contains(API_SECRET));
124        assert!(debug_string.contains("<redacted>"));
125        assert!(debug_string.contains("test..."));
126    }
127}