nautilus_coinbase_intx/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 #[zeroize(skip)]
32 pub api_passphrase: Ustr,
33 api_secret: Box<[u8]>,
34}
35
36impl Debug for Credential {
37 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38 f.debug_struct("Credential")
39 .field("api_key", &self.api_key)
40 .field("api_passphrase", &self.api_passphrase)
41 .field("api_secret", &"<redacted>")
42 .finish()
43 }
44}
45
46impl Credential {
47 #[must_use]
53 pub fn new(api_key: String, api_secret: String, api_passphrase: String) -> Self {
54 let decoded_secret = BASE64_STANDARD
55 .decode(api_secret)
56 .expect("Invalid base64 secret key");
57
58 Self {
59 api_key: api_key.into(),
60 api_passphrase: api_passphrase.into(),
61 api_secret: decoded_secret.into_boxed_slice(),
62 }
63 }
64
65 #[must_use]
71 pub fn sign(&self, timestamp: &str, method: &str, endpoint: &str, body: &str) -> String {
72 let request_path = match endpoint.find('?') {
74 Some(index) => &endpoint[..index],
75 None => endpoint,
76 };
77
78 let message = format!("{timestamp}{method}{request_path}{body}");
79 let key = hmac::Key::new(hmac::HMAC_SHA256, &self.api_secret[..]);
80 let tag = hmac::sign(&key, message.as_bytes());
81 BASE64_STANDARD.encode(tag.as_ref())
82 }
83
84 pub fn sign_ws(&self, timestamp: &str) -> String {
90 let message = format!("{timestamp}{}CBINTLMD{}", self.api_key, self.api_passphrase);
91 tracing::trace!("Signing message: {message}");
92
93 let key = hmac::Key::new(hmac::HMAC_SHA256, &self.api_secret[..]);
94 let tag = hmac::sign(&key, message.as_bytes());
95 BASE64_STANDARD.encode(tag.as_ref())
96 }
97}
98
99#[cfg(test)]
104mod tests {
105 use rstest::rstest;
106
107 use super::*;
108
109 const API_KEY: &str = "test_key_123";
110 const API_SECRET: &str = "dGVzdF9zZWNyZXRfYmFzZTY0"; const API_PASSPHRASE: &str = "test_pass";
112
113 #[rstest]
114 fn test_simple_get() {
115 let credential = Credential::new(
116 API_KEY.to_string(),
117 API_SECRET.to_string(),
118 API_PASSPHRASE.to_string(),
119 );
120 let timestamp = "1641890400"; let signature = credential.sign(timestamp, "GET", "/api/v1/fee-rate-tiers", "");
122
123 assert_eq!(signature, "h/9tnYzD/nsEbH1sV7dkB5uJ3Vygr4TjmOOxJNQB8ts=");
124 }
125
126 #[rstest]
127 fn test_debug_redacts_secret() {
128 let credential = Credential::new(
129 API_KEY.to_string(),
130 API_SECRET.to_string(),
131 API_PASSPHRASE.to_string(),
132 );
133 let dbg_out = format!("{credential:?}");
134 assert!(dbg_out.contains("api_secret: \"<redacted>\""));
135 assert!(!dbg_out.contains("dGVz")); let secret_bytes_dbg = format!("{:?}", BASE64_STANDARD.decode(API_SECRET).unwrap());
137 assert!(
138 !dbg_out.contains(&secret_bytes_dbg),
139 "Debug output must not contain raw secret bytes"
140 );
141 }
142}