Skip to main content

ccxt_core/
credentials.rs

1//! Secure credential types with automatic memory zeroization.
2//!
3//! All sensitive data is automatically cleared from memory when dropped,
4//! preventing credential leakage through memory dumps or core files.
5//!
6//! # Security
7//!
8//! This module provides types that implement the `Zeroize` and `ZeroizeOnDrop`
9//! traits from the `zeroize` crate. When these types go out of scope, their
10//! memory is securely overwritten with zeros before being deallocated.
11//!
12//! # Example
13//!
14//! ```rust
15//! use ccxt_core::credentials::SecretString;
16//!
17//! let api_key = SecretString::new("my-api-key");
18//!
19//! // Access the secret when needed
20//! let key_value = api_key.expose_secret();
21//!
22//! // Debug output is redacted
23//! println!("{:?}", api_key); // Prints: [REDACTED]
24//!
25//! // When api_key goes out of scope, memory is automatically zeroed
26//! ```
27
28use std::fmt;
29use zeroize::{Zeroize, ZeroizeOnDrop};
30
31/// A secure string that is automatically zeroed when dropped.
32///
33/// Use this for API keys, secrets, passwords, and other sensitive data.
34/// The underlying memory is securely cleared when the value is dropped,
35/// preventing credential leakage through memory inspection.
36///
37/// # Security Features
38///
39/// - Memory is zeroed on drop using the `zeroize` crate
40/// - Debug and Display implementations are redacted to prevent accidental logging
41/// - Clone creates a new zeroed copy (original remains secure)
42///
43/// # Example
44///
45/// ```rust
46/// use ccxt_core::credentials::SecretString;
47///
48/// let secret = SecretString::new("my-secret-key");
49///
50/// // Use expose_secret() to access the value
51/// assert_eq!(secret.expose_secret(), "my-secret-key");
52///
53/// // Debug output is safe
54/// println!("{:?}", secret); // Prints: [REDACTED]
55/// ```
56#[derive(Clone, Zeroize, ZeroizeOnDrop, PartialEq, Eq)]
57pub struct SecretString(String);
58
59impl SecretString {
60    /// Creates a new secret string.
61    ///
62    /// # Arguments
63    ///
64    /// * `value` - The secret value to store
65    ///
66    /// # Example
67    ///
68    /// ```rust
69    /// use ccxt_core::credentials::SecretString;
70    ///
71    /// let secret = SecretString::new("api-key-12345");
72    /// ```
73    pub fn new(value: impl Into<String>) -> Self {
74        Self(value.into())
75    }
76
77    /// Returns the secret value.
78    ///
79    /// # Security
80    ///
81    /// Avoid storing the returned reference longer than necessary.
82    /// The reference should be used immediately and not persisted.
83    ///
84    /// # Example
85    ///
86    /// ```rust
87    /// use ccxt_core::credentials::SecretString;
88    ///
89    /// let secret = SecretString::new("my-key");
90    /// let value = secret.expose_secret();
91    /// // Use value immediately, don't store it
92    /// ```
93    #[inline]
94    pub fn expose_secret(&self) -> &str {
95        &self.0
96    }
97
98    /// Returns the secret as bytes.
99    ///
100    /// # Security
101    ///
102    /// Same security considerations as `expose_secret()` apply.
103    #[inline]
104    pub fn expose_secret_bytes(&self) -> &[u8] {
105        self.0.as_bytes()
106    }
107
108    /// Returns the length of the secret string.
109    #[inline]
110    pub fn len(&self) -> usize {
111        self.0.len()
112    }
113
114    /// Returns true if the secret string is empty.
115    #[inline]
116    pub fn is_empty(&self) -> bool {
117        self.0.is_empty()
118    }
119}
120
121// Prevent accidental logging of sensitive data
122impl fmt::Debug for SecretString {
123    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
124        f.write_str("[REDACTED]")
125    }
126}
127
128impl fmt::Display for SecretString {
129    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
130        f.write_str("[REDACTED]")
131    }
132}
133
134// Convenient conversions
135impl From<String> for SecretString {
136    fn from(s: String) -> Self {
137        Self::new(s)
138    }
139}
140
141impl From<&str> for SecretString {
142    fn from(s: &str) -> Self {
143        Self::new(s)
144    }
145}
146
147/// Secure bytes that are automatically zeroed when dropped.
148///
149/// Use this for private keys, binary secrets, and other sensitive byte data.
150///
151/// # Example
152///
153/// ```rust
154/// use ccxt_core::credentials::SecretBytes;
155///
156/// let private_key = SecretBytes::new(vec![0x01, 0x02, 0x03, 0x04]);
157/// let bytes = private_key.expose_secret();
158/// ```
159#[derive(Clone, Zeroize, ZeroizeOnDrop, PartialEq, Eq)]
160pub struct SecretBytes(Vec<u8>);
161
162impl SecretBytes {
163    /// Creates new secret bytes.
164    ///
165    /// # Arguments
166    ///
167    /// * `value` - The secret bytes to store
168    pub fn new(value: impl Into<Vec<u8>>) -> Self {
169        Self(value.into())
170    }
171
172    /// Creates secret bytes from a fixed-size array.
173    ///
174    /// # Arguments
175    ///
176    /// * `value` - The secret bytes array to store
177    pub fn from_array<const N: usize>(value: [u8; N]) -> Self {
178        Self(value.to_vec())
179    }
180
181    /// Returns the secret bytes.
182    ///
183    /// # Security
184    ///
185    /// Avoid storing the returned reference longer than necessary.
186    #[inline]
187    pub fn expose_secret(&self) -> &[u8] {
188        &self.0
189    }
190
191    /// Returns the length of the secret bytes.
192    #[inline]
193    pub fn len(&self) -> usize {
194        self.0.len()
195    }
196
197    /// Returns true if the secret bytes are empty.
198    #[inline]
199    pub fn is_empty(&self) -> bool {
200        self.0.is_empty()
201    }
202}
203
204impl fmt::Debug for SecretBytes {
205    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
206        write!(f, "[REDACTED {} bytes]", self.0.len())
207    }
208}
209
210impl<const N: usize> From<[u8; N]> for SecretBytes {
211    fn from(arr: [u8; N]) -> Self {
212        Self::from_array(arr)
213    }
214}
215
216impl From<Vec<u8>> for SecretBytes {
217    fn from(v: Vec<u8>) -> Self {
218        Self::new(v)
219    }
220}
221
222#[cfg(test)]
223#[allow(clippy::uninlined_format_args)] // format!("{}", x) is acceptable in tests
224mod tests {
225    use super::*;
226
227    #[test]
228    fn test_secret_string_debug_redacted() {
229        let secret = SecretString::new("my-api-key");
230        assert_eq!(format!("{secret:?}"), "[REDACTED]");
231    }
232
233    #[test]
234    fn test_secret_string_display_redacted() {
235        let secret = SecretString::new("my-api-key");
236        assert_eq!(format!("{secret}"), "[REDACTED]");
237    }
238
239    #[test]
240    fn test_secret_string_expose() {
241        let secret = SecretString::new("my-api-key");
242        assert_eq!(secret.expose_secret(), "my-api-key");
243    }
244
245    #[test]
246    fn test_secret_string_expose_bytes() {
247        let secret = SecretString::new("test");
248        assert_eq!(secret.expose_secret_bytes(), b"test");
249    }
250
251    #[test]
252    fn test_secret_string_len() {
253        let secret = SecretString::new("12345");
254        assert_eq!(secret.len(), 5);
255        assert!(!secret.is_empty());
256    }
257
258    #[test]
259    fn test_secret_string_empty() {
260        let secret = SecretString::new("");
261        assert!(secret.is_empty());
262    }
263
264    #[test]
265    fn test_secret_from_string() {
266        let secret: SecretString = String::from("test").into();
267        assert_eq!(secret.expose_secret(), "test");
268    }
269
270    #[test]
271    fn test_secret_from_str() {
272        let secret: SecretString = "test".into();
273        assert_eq!(secret.expose_secret(), "test");
274    }
275
276    #[test]
277    fn test_secret_bytes_debug_redacted() {
278        let secret = SecretBytes::new(vec![1, 2, 3, 4, 5]);
279        assert_eq!(format!("{secret:?}"), "[REDACTED 5 bytes]");
280    }
281
282    #[test]
283    fn test_secret_bytes_expose() {
284        let secret = SecretBytes::new(vec![1, 2, 3]);
285        assert_eq!(secret.expose_secret(), &[1, 2, 3]);
286    }
287
288    #[test]
289    fn test_secret_bytes_from_array() {
290        let secret = SecretBytes::from_array([1u8, 2, 3, 4]);
291        assert_eq!(secret.expose_secret(), &[1, 2, 3, 4]);
292    }
293
294    #[test]
295    fn test_secret_bytes_len() {
296        let secret = SecretBytes::new(vec![1, 2, 3]);
297        assert_eq!(secret.len(), 3);
298        assert!(!secret.is_empty());
299    }
300
301    #[test]
302    fn test_secret_clone() {
303        let secret1 = SecretString::new("test");
304        let secret2 = secret1.clone();
305        assert_eq!(secret1.expose_secret(), secret2.expose_secret());
306    }
307}