nautilus_okx/common/
credential.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 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
16use std::fmt::Debug;
17
18use aws_lc_rs::hmac;
19use base64::prelude::*;
20use ustr::Ustr;
21use zeroize::ZeroizeOnDrop;
22
23/// OKX API credentials for signing requests.
24///
25/// Uses HMAC SHA256 for request signing as per OKX API specifications.
26/// Secrets are automatically zeroized on drop for security.
27#[derive(Clone, ZeroizeOnDrop)]
28pub struct Credential {
29    #[zeroize(skip)]
30    pub api_key: Ustr,
31    pub api_passphrase: String,
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("Credential")
38            .field("api_key", &self.api_key)
39            .field("api_passphrase", &self.api_passphrase)
40            .field("api_secret", &"<redacted>")
41            .finish()
42    }
43}
44
45impl Credential {
46    /// Creates a new [`Credential`] instance.
47    #[must_use]
48    pub fn new(api_key: String, api_secret: String, api_passphrase: String) -> Self {
49        Self {
50            api_key: api_key.into(),
51            api_passphrase,
52            api_secret: api_secret.into_bytes().into_boxed_slice(),
53        }
54    }
55
56    /// Signs a request message according to the OKX authentication scheme.
57    pub fn sign(&self, timestamp: &str, method: &str, endpoint: &str, body: &str) -> String {
58        let message = format!("{timestamp}{method}{endpoint}{body}");
59        let key = hmac::Key::new(hmac::HMAC_SHA256, &self.api_secret[..]);
60        let tag = hmac::sign(&key, message.as_bytes());
61        BASE64_STANDARD.encode(tag.as_ref())
62    }
63}
64
65////////////////////////////////////////////////////////////////////////////////
66// Tests
67////////////////////////////////////////////////////////////////////////////////
68
69#[cfg(test)]
70mod tests {
71    use rstest::rstest;
72
73    use super::*;
74
75    const API_KEY: &str = "985d5b66-57ce-40fb-b714-afc0b9787083";
76    const API_SECRET: &str = "chNOOS4KvNXR_Xq4k4c9qsfoKWvnDecLATCRlcBwyKDYnWgO";
77    const API_PASSPHRASE: &str = "1234567890";
78
79    #[rstest]
80    fn test_simple_get() {
81        let credential = Credential::new(
82            API_KEY.to_string(),
83            API_SECRET.to_string(),
84            API_PASSPHRASE.to_string(),
85        );
86
87        let signature = credential.sign(
88            "2020-12-08T09:08:57.715Z",
89            "GET",
90            "/api/v5/account/balance",
91            "",
92        );
93
94        assert_eq!(signature, "PJ61e1nb2F2Qd7D8SPiaIcx2gjdELc+o0ygzre9z33k=");
95    }
96
97    #[rstest]
98    fn test_debug_redacts_secret() {
99        let credential = Credential::new(
100            API_KEY.to_string(),
101            API_SECRET.to_string(),
102            API_PASSPHRASE.to_string(),
103        );
104        let dbg_out = format!("{:?}", credential);
105        assert!(dbg_out.contains("api_secret: \"<redacted>\""));
106        assert!(!dbg_out.contains("chNOO"));
107        let secret_bytes_dbg = format!("{:?}", API_SECRET.as_bytes());
108        assert!(
109            !dbg_out.contains(&secret_bytes_dbg),
110            "Debug output must not contain raw secret bytes"
111        );
112    }
113}