nautilus_coinbase_intx/common/
credential.rs1#![allow(unused_assignments)] use std::fmt::Debug;
19
20use aws_lc_rs::hmac;
21use base64::prelude::*;
22use ustr::Ustr;
23use zeroize::ZeroizeOnDrop;
24
25#[derive(Clone, ZeroizeOnDrop)]
30pub struct Credential {
31 #[zeroize(skip)]
32 pub api_key: Ustr,
33 #[zeroize(skip)]
34 pub api_passphrase: Ustr,
35 api_secret: Box<[u8]>,
36}
37
38impl Debug for Credential {
39 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40 f.debug_struct(stringify!(Credential))
41 .field("api_key", &self.api_key)
42 .field("api_passphrase", &self.api_passphrase)
43 .field("api_secret", &"<redacted>")
44 .finish()
45 }
46}
47
48impl Credential {
49 #[must_use]
55 pub fn new(api_key: String, api_secret: String, api_passphrase: String) -> Self {
56 let decoded_secret = BASE64_STANDARD
57 .decode(api_secret)
58 .expect("Invalid base64 secret key");
59
60 Self {
61 api_key: api_key.into(),
62 api_passphrase: api_passphrase.into(),
63 api_secret: decoded_secret.into_boxed_slice(),
64 }
65 }
66
67 #[must_use]
73 pub fn sign(&self, timestamp: &str, method: &str, endpoint: &str, body: &str) -> String {
74 let request_path = match endpoint.find('?') {
76 Some(index) => &endpoint[..index],
77 None => endpoint,
78 };
79
80 let message = format!("{timestamp}{method}{request_path}{body}");
81 let key = hmac::Key::new(hmac::HMAC_SHA256, &self.api_secret[..]);
82 let tag = hmac::sign(&key, message.as_bytes());
83 BASE64_STANDARD.encode(tag.as_ref())
84 }
85
86 #[must_use]
91 pub fn api_key_masked(&self) -> String {
92 nautilus_core::string::mask_api_key(self.api_key.as_str())
93 }
94
95 pub fn sign_ws(&self, timestamp: &str) -> String {
101 let message = format!("{timestamp}{}CBINTLMD{}", self.api_key, self.api_passphrase);
102 tracing::trace!("Signing message: {message}");
103
104 let key = hmac::Key::new(hmac::HMAC_SHA256, &self.api_secret[..]);
105 let tag = hmac::sign(&key, message.as_bytes());
106 BASE64_STANDARD.encode(tag.as_ref())
107 }
108}
109
110#[cfg(test)]
115mod tests {
116 use rstest::rstest;
117
118 use super::*;
119
120 const API_KEY: &str = "test_key_123";
121 const API_SECRET: &str = "dGVzdF9zZWNyZXRfYmFzZTY0"; const API_PASSPHRASE: &str = "test_pass";
123
124 #[rstest]
125 fn test_simple_get() {
126 let credential = Credential::new(
127 API_KEY.to_string(),
128 API_SECRET.to_string(),
129 API_PASSPHRASE.to_string(),
130 );
131 let timestamp = "1641890400"; let signature = credential.sign(timestamp, "GET", "/api/v1/fee-rate-tiers", "");
133
134 assert_eq!(signature, "h/9tnYzD/nsEbH1sV7dkB5uJ3Vygr4TjmOOxJNQB8ts=");
135 }
136
137 #[rstest]
138 fn test_debug_redacts_secret() {
139 let credential = Credential::new(
140 API_KEY.to_string(),
141 API_SECRET.to_string(),
142 API_PASSPHRASE.to_string(),
143 );
144 let dbg_out = format!("{credential:?}");
145 assert!(dbg_out.contains("api_secret: \"<redacted>\""));
146 assert!(!dbg_out.contains("dGVz")); let secret_bytes_dbg = format!("{:?}", BASE64_STANDARD.decode(API_SECRET).unwrap());
148 assert!(
149 !dbg_out.contains(&secret_bytes_dbg),
150 "Debug output must not contain raw secret bytes"
151 );
152 }
153}