wickra_exchange_core/
credentials.rs1use crate::error::{Error, Result};
13use std::fmt;
14use zeroize::ZeroizeOnDrop;
15
16#[derive(Clone, ZeroizeOnDrop)]
22pub struct Credentials {
23 pub(crate) api_key: String,
24 pub(crate) api_secret: String,
25 pub(crate) passphrase: Option<String>,
26 pub(crate) private_key: Option<String>,
27}
28
29impl Credentials {
30 pub fn new(api_key: impl Into<String>, api_secret: impl Into<String>) -> Self {
32 Self {
33 api_key: api_key.into(),
34 api_secret: api_secret.into(),
35 passphrase: None,
36 private_key: None,
37 }
38 }
39
40 #[must_use]
42 pub fn with_passphrase(mut self, passphrase: impl Into<String>) -> Self {
43 self.passphrase = Some(passphrase.into());
44 self
45 }
46
47 #[must_use]
49 pub fn with_private_key(mut self, private_key: impl Into<String>) -> Self {
50 self.private_key = Some(private_key.into());
51 self
52 }
53
54 pub fn validate(&self) -> Result<()> {
60 if self.api_key.is_empty() {
61 return Err(Error::InvalidCredentials("api key must not be empty"));
62 }
63 if self.api_secret.is_empty() {
64 return Err(Error::InvalidCredentials("api secret must not be empty"));
65 }
66 Ok(())
67 }
68
69 #[must_use]
71 pub fn has_passphrase(&self) -> bool {
72 self.passphrase.is_some()
73 }
74
75 #[must_use]
77 pub fn has_private_key(&self) -> bool {
78 self.private_key.is_some()
79 }
80}
81
82impl fmt::Debug for Credentials {
83 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86 f.debug_struct("Credentials")
87 .field("api_key", &"<redacted>")
88 .field("api_secret", &"<redacted>")
89 .field(
90 "passphrase",
91 &self.passphrase.as_ref().map(|_| "<redacted>"),
92 )
93 .field(
94 "private_key",
95 &self.private_key.as_ref().map(|_| "<redacted>"),
96 )
97 .finish()
98 }
99}
100
101#[cfg(test)]
102mod tests {
103 use super::*;
104
105 #[test]
106 fn builder_attaches_optional_fields() {
107 let creds = Credentials::new("key", "secret");
108 assert!(!creds.has_passphrase());
109 assert!(!creds.has_private_key());
110
111 let creds = creds.with_passphrase("pass").with_private_key("pem");
112 assert!(creds.has_passphrase());
113 assert!(creds.has_private_key());
114 }
115
116 #[test]
117 fn validate_rejects_empty_key_or_secret() {
118 assert!(Credentials::new("key", "secret").validate().is_ok());
119 assert_eq!(
120 Credentials::new("", "secret").validate().unwrap_err(),
121 Error::InvalidCredentials("api key must not be empty")
122 );
123 assert_eq!(
124 Credentials::new("key", "").validate().unwrap_err(),
125 Error::InvalidCredentials("api secret must not be empty")
126 );
127 }
128
129 #[test]
130 fn debug_never_leaks_secret_material() {
131 let creds = Credentials::new("AK_PUBLIC_VALUE", "SK_SECRET_VALUE")
132 .with_passphrase("PASS_SECRET")
133 .with_private_key("-----BEGIN EC PRIVATE KEY-----");
134 let rendered = format!("{creds:?}");
135 assert!(!rendered.contains("AK_PUBLIC_VALUE"));
136 assert!(!rendered.contains("SK_SECRET_VALUE"));
137 assert!(!rendered.contains("PASS_SECRET"));
138 assert!(!rendered.contains("BEGIN EC PRIVATE KEY"));
139 assert!(rendered.contains("api_key"));
141 assert!(rendered.contains("Some(\"<redacted>\")"));
142 }
143}