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}