/// Secret — a typed value that never leaks via Display/Debug. /// /// The wrapper ensures that accidental logging or serialization of a secret /// never reveals the actual value. You must explicitly call `.expose()` to /// read it, which creates a visible opt-in point in the code. use serde::{Deserialize, Serialize, Serializer}; /// A secret value. Never prints the inner value via Display or Debug. /// /// Always displays as `[REDACTED]`. To access the inner value: /// ``` /// use el_secrets::Secret; /// let key = Secret::new("my-secret-key".to_string()); /// let actual: &str = key.expose(); // explicit opt-in /// ``` #[derive(Clone)] pub struct Secret(T); impl Secret { /// Wrap a value in a Secret. pub fn new(value: T) -> Self { Self(value) } /// Access the inner value. /// /// This is the ONLY way to get the actual secret value out. /// Name it `expose` so it's searchable in code review. pub fn expose(&self) -> &T { &self.0 } /// Consume the Secret and return the inner value. pub fn into_inner(self) -> T { self.0 } /// Map the inner value to a new type, wrapping in a new Secret. pub fn map U>(self, f: F) -> Secret { Secret(f(self.0)) } } /// Debug never reveals the secret value. impl std::fmt::Debug for Secret { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "Secret([REDACTED])") } } /// Display never reveals the secret value. impl std::fmt::Display for Secret { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "[REDACTED]") } } /// Serialize writes [REDACTED], never the actual value. /// This prevents secrets from appearing in JSON logs, API responses, etc. impl Serialize for Secret { fn serialize(&self, serializer: S) -> Result { serializer.serialize_str("[REDACTED]") } } /// Deserialize from a string — used when loading secrets from files/env. /// Only implemented for Secret since we always load as strings. impl<'de> Deserialize<'de> for Secret { fn deserialize>(deserializer: D) -> Result { let s = String::deserialize(deserializer)?; Ok(Secret::new(s)) } } /// A guard that prevents a value from being accidentally exposed. /// /// Use this on struct fields that should never be serialized or logged. #[derive(Clone)] pub struct SecretGuard { inner: Secret, /// A hint shown in Debug output (not the value itself). label: &'static str, } impl SecretGuard { pub fn new(value: T, label: &'static str) -> Self { Self { inner: Secret::new(value), label, } } pub fn expose(&self) -> &T { self.inner.expose() } } impl std::fmt::Debug for SecretGuard { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "SecretGuard({}: [REDACTED])", self.label) } } #[cfg(test)] mod tests { use super::*; #[test] fn secret_expose() { let s = Secret::new("my-api-key".to_string()); assert_eq!(s.expose(), "my-api-key"); } #[test] fn secret_debug_redacted() { let s = Secret::new("super-secret".to_string()); let debug = format!("{:?}", s); assert_eq!(debug, "Secret([REDACTED])"); assert!(!debug.contains("super-secret")); } #[test] fn secret_display_redacted() { let s = Secret::new(12345u32); let display = format!("{}", s); assert_eq!(display, "[REDACTED]"); assert!(!display.contains("12345")); } #[test] fn secret_serialize_redacted() { let s = Secret::new("should-not-appear".to_string()); let json = serde_json::to_string(&s).unwrap(); assert_eq!(json, r#""[REDACTED]""#); assert!(!json.contains("should-not-appear")); } #[test] fn secret_deserialize() { let s: Secret = serde_json::from_str(r#""my-secret""#).unwrap(); assert_eq!(s.expose(), "my-secret"); } #[test] fn secret_map() { let s = Secret::new("42".to_string()); let n: Secret = s.map(|v| v.parse().unwrap()); assert_eq!(*n.expose(), 42u32); } #[test] fn secret_into_inner() { let s = Secret::new("value".to_string()); assert_eq!(s.into_inner(), "value"); } #[test] fn secret_guard_debug() { let g = SecretGuard::new("token".to_string(), "jwt_token"); let debug = format!("{:?}", g); assert!(debug.contains("jwt_token")); assert!(!debug.contains("token\"")); } #[test] fn secret_guard_expose() { let g = SecretGuard::new("secret-value".to_string(), "api_key"); assert_eq!(g.expose(), "secret-value"); } #[test] fn secret_clone_does_not_expose() { let s1 = Secret::new("clone-me".to_string()); let s2 = s1.clone(); assert_eq!(s2.expose(), "clone-me"); assert!(!format!("{:?}", s2).contains("clone-me")); } }