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}