nautilus_okx/common/
credential.rs1use std::fmt::Debug;
17
18use aws_lc_rs::hmac;
19use base64::prelude::*;
20use ustr::Ustr;
21use zeroize::ZeroizeOnDrop;
22
23#[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 #[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 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#[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}