api_keys_simplified/secure.rs
1//! Secure memory handling for sensitive data
2
3use secrecy::{ExposeSecret, SecretString};
4use subtle::ConstantTimeEq;
5
6/// A secure string that automatically zeros its memory on drop.
7///
8/// This is a type alias for `secrecy::SecretString`, which provides:
9/// - Automatic memory zeroing on drop
10/// - Prevention of accidental logging via Debug/Display
11/// - Industry-standard security practices
12///
13/// # Security
14///
15/// The contained data is automatically zeroed when the value is dropped,
16/// using the `zeroize` crate which provides compiler-fence-backed guarantees
17/// that the zeroing operation won't be optimized away.
18///
19/// # Usage
20///
21/// Access the underlying string using `.expose_secret()`:
22///
23/// ```rust
24/// use api_keys_simplified::SecureString;
25/// use api_keys_simplified::ExposeSecret;
26///
27/// let secret = SecureString::from("my_secret".to_string());
28/// let value: &str = secret.expose_secret();
29/// ```
30///
31/// # Design: Why No `Deref<Target=str>`?
32///
33/// This type **intentionally does NOT implement `Deref`** to maintain security:
34///
35/// - **Explicit access**: Requires `.expose_secret()` call, making code auditable
36/// - **Prevents silent leakage**: No implicit coercion to `&str` in logs/errors
37/// - **Grep-able security**: Easy to audit with `git grep "\.expose_secret\(\)"`
38/// - **Industry stan`dard**: Uses the battle-tested `secrecy` crate
39pub type SecureString = SecretString;
40
41/// Extension trait to add convenience methods to SecureString
42pub trait SecureStringExt {
43 /// Returns the length of the string in bytes.
44 fn len(&self) -> usize;
45
46 /// Returns true if the string is empty.
47 fn is_empty(&self) -> bool;
48
49 /// Constant time eq
50 // FIXME: we can add wrapper to secure
51 // string and impl PartialEq
52 fn eq(&self, other: &Self) -> bool;
53}
54
55impl SecureStringExt for SecureString {
56 fn len(&self) -> usize {
57 self.expose_secret().len()
58 }
59
60 fn is_empty(&self) -> bool {
61 self.expose_secret().is_empty()
62 }
63
64 fn eq(&self, other: &Self) -> bool {
65 self.expose_secret()
66 .as_bytes()
67 .ct_eq(other.expose_secret().as_bytes())
68 .into()
69 }
70}
71
72#[cfg(test)]
73mod tests {
74 use super::*;
75
76 #[test]
77 fn test_secure_string_creation() {
78 let secret = SecretString::from("my_secret");
79 assert_eq!(secret.expose_secret(), "my_secret");
80 assert_eq!(secret.len(), 9);
81 assert!(!secret.is_empty());
82 }
83
84 #[test]
85 fn test_secure_string_redaction() {
86 let secret = SecretString::from("sensitive_data");
87
88 // Debug output should be redacted by secrecy crate
89 let debug_output = format!("{:?}", secret);
90 assert!(!debug_output.contains("sensitive_data"));
91 assert!(debug_output.contains("Secret"));
92 }
93
94 #[test]
95 fn test_secure_string_empty() {
96 let empty = SecretString::from("");
97 assert!(empty.is_empty());
98 assert_eq!(empty.len(), 0);
99 }
100
101 #[test]
102 fn test_expose_secret() {
103 let secret = SecretString::from("test".to_string());
104 let reference: &str = secret.expose_secret();
105 assert_eq!(reference, "test");
106 }
107}