api_keys_simplified/
secure.rs

1//! Secure memory handling for sensitive data
2//!
3//! This module provides types that automatically zero their memory on drop,
4//! preventing sensitive data from lingering in memory after use.
5
6use std::fmt;
7use zeroize::{Zeroize, ZeroizeOnDrop};
8
9/// A secure string that automatically zeros its memory on drop.
10///
11/// This type should be used for any sensitive data like API keys, tokens,
12/// or passwords to prevent potential memory disclosure through:
13/// - Core dumps
14/// - Swap files
15/// - Memory scanning tools
16/// - Debuggers
17///
18/// # Security
19///
20/// The contained data is automatically zeroed when the value is dropped,
21/// using the `zeroize` crate which provides compiler-fence-backed guarantees
22/// that the zeroing operation won't be optimized away.
23///
24/// # Design: Why No `Deref<Target=str>`?
25///
26/// This type **intentionally does NOT implement `Deref`** to maintain security:
27///
28/// - **Explicit access**: Requires `.as_ref()` call, making code auditable
29/// - **Prevents silent leakage**: No implicit coercion to `&str` in logs/errors  
30/// - **Grep-able security**: Easy to audit with `git grep "\.as_ref\(\)"`
31/// - **Industry standard**: Aligns with `secrecy` crate's proven approach
32///
33/// The slight ergonomic cost of typing `.as_ref()` is a worthwhile
34/// security trade-off that prevents accidental secret exposure.
35///
36/// # Example
37///
38/// ```
39/// use api_keys_simplified::SecureString;
40///
41/// let sensitive = SecureString::from("my_secret_api_key");
42///
43/// // Explicit access (good - auditable)
44/// let key = sensitive.as_ref();
45///
46/// // Debug output is automatically redacted (safe)
47/// println!("{:?}", sensitive);  // Output: "SecureString([REDACTED])"
48/// ```
49///
50/// // Memory is automatically zeroed when `sensitive` goes out of scope
51/// ```
52#[derive(Clone, Zeroize, ZeroizeOnDrop)]
53pub struct SecureString(String);
54
55impl SecureString {
56    /// Creates a new SecureString from a String.
57    ///
58    /// The original string is moved and will be zeroed when this
59    /// SecureString is dropped.
60    pub fn new(s: String) -> Self {
61        Self(s)
62    }
63
64    /// Returns the length of the string in bytes.
65    pub fn len(&self) -> usize {
66        self.0.len()
67    }
68
69    /// Returns true if the string is empty.
70    pub fn is_empty(&self) -> bool {
71        self.0.is_empty()
72    }
73}
74
75impl From<String> for SecureString {
76    fn from(s: String) -> Self {
77        Self::new(s)
78    }
79}
80
81impl From<&str> for SecureString {
82    fn from(s: &str) -> Self {
83        Self::new(s.to_string())
84    }
85}
86
87impl AsRef<str> for SecureString {
88    fn as_ref(&self) -> &str {
89        &self.0
90    }
91}
92
93// Prevent accidental logging of sensitive data
94impl fmt::Debug for SecureString {
95    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
96        f.write_str("SecureString([REDACTED])")
97    }
98}
99
100// Prevent accidental display of sensitive data
101impl fmt::Display for SecureString {
102    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
103        f.write_str("[REDACTED]")
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110
111    #[test]
112    fn test_secure_string_creation() {
113        let secret = SecureString::from("my_secret");
114        assert_eq!(secret.as_ref(), "my_secret");
115        assert_eq!(secret.len(), 9);
116        assert!(!secret.is_empty());
117    }
118
119    #[test]
120    fn test_secure_string_redaction() {
121        let secret = SecureString::from("sensitive_data");
122
123        // Debug output should be redacted
124        let debug_output = format!("{:?}", secret);
125        assert_eq!(debug_output, "SecureString([REDACTED])");
126        assert!(!debug_output.contains("sensitive_data"));
127
128        // Display output should be redacted
129        let display_output = format!("{}", secret);
130        assert_eq!(display_output, "[REDACTED]");
131        assert!(!display_output.contains("sensitive_data"));
132    }
133
134    #[test]
135    fn test_secure_string_empty() {
136        let empty = SecureString::from("");
137        assert!(empty.is_empty());
138        assert_eq!(empty.len(), 0);
139    }
140
141    #[test]
142    fn test_secure_string_clone() {
143        let secret1 = SecureString::from("original");
144        let secret2 = secret1.clone();
145
146        assert_eq!(secret1.as_ref(), "original");
147        assert_eq!(secret2.as_ref(), "original");
148    }
149
150    #[test]
151    fn test_as_ref() {
152        let secret = SecureString::from("test");
153        let reference: &str = secret.as_ref();
154        assert_eq!(reference, "test");
155    }
156}