Skip to main content

secret_fmt/
lib.rs

1#![no_std]
2#![doc = include_str!("../README.md")]
3#![forbid(unsafe_code)]
4#![warn(missing_docs, clippy::pedantic, clippy::cargo)]
5
6//! A zero-dependency wrapper type that prevents accidental logging of sensitive data.
7//!
8//! `Secret<T>` wraps any value and explicitly overrides its `Debug` and `Display`
9//! (and optionally `Serialize`) implementations to emit `"[REDACTED]"`.
10//!
11//! Unlike other secrecy crates, `Secret` has **zero trait bounds**. You can wrap
12//! any type instantly without implementing boilerplate traits like `Zeroize`.
13//!
14//! Note: `Secret` intentionally does not implement `Deref` to prevent implicit
15//! coercions that could bypass the redaction formatting.
16
17use core::fmt;
18
19/// A wrapper type that redacts its contents when logged or formatted.
20///
21/// Implements standard traits like `Clone`, `PartialEq`, and `Hash` transparently,
22/// so it can be freely used in standard collections without exposing the secret via formatting.
23#[derive(Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
24#[must_use = "wrapping a value in a secret_fmt is useless if the secret_fmt is immediately dropped"]
25pub struct Secret<T>(T);
26
27impl<T> Secret<T> {
28    /// Creates a new `Secret` wrapping the provided value.
29    ///
30    /// # Examples
31    /// ```
32    /// use secret_fmt::Secret;
33    /// let secret = Secret::new("super_secret_password");
34    /// assert_eq!(format!("{:?}", secret), "[REDACTED]");
35    /// ```
36    #[inline]
37    pub const fn new(value: T) -> Self {
38        Self(value)
39    }
40
41    /// Consumes the `Secret`, returning the wrapped value.
42    ///
43    /// # Examples
44    /// ```
45    /// use secret_fmt::Secret;
46    /// let secret = Secret::new(12345);
47    /// assert_eq!(secret.into_inner(), 12345);
48    /// ```
49    #[inline]
50    #[must_use = "consuming the secret_fmt to discard the inner value is likely a mistake"]
51    pub fn into_inner(self) -> T {
52        self.0
53    }
54
55    /// Returns a shared reference to the wrapped value.
56    ///
57    /// # Examples
58    /// ```
59    /// use secret_fmt::Secret;
60    /// let secret = Secret::new(String::from("test"));
61    /// assert_eq!(secret.as_inner(), "test");
62    /// ```
63    #[inline]
64    #[must_use = "getting the inner reference without using it is likely a mistake"]
65    pub const fn as_inner(&self) -> &T {
66        &self.0
67    }
68
69    /// Returns a mutable reference to the wrapped value.
70    ///
71    /// # Examples
72    /// ```
73    /// use secret_fmt::Secret;
74    /// let mut secret = Secret::new(String::from("test"));
75    /// secret.as_inner_mut().push_str("ing");
76    /// assert_eq!(secret.into_inner(), "testing");
77    /// ```
78    #[inline]
79    #[must_use = "getting the inner mutable reference without using it is likely a mistake"]
80    pub fn as_inner_mut(&mut self) -> &mut T {
81        &mut self.0
82    }
83}
84
85impl<T> fmt::Debug for Secret<T> {
86    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87        f.write_str("[REDACTED]")
88    }
89}
90
91impl<T> fmt::Display for Secret<T> {
92    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
93        f.write_str("[REDACTED]")
94    }
95}
96
97impl<T> From<T> for Secret<T> {
98    #[inline]
99    fn from(value: T) -> Self {
100        Self(value)
101    }
102}
103
104impl<T> AsRef<T> for Secret<T> {
105    #[inline]
106    fn as_ref(&self) -> &T {
107        &self.0
108    }
109}
110
111/// Wraps a reference or value in a `Secret` on the fly.
112///
113/// Useful for inline logging without taking ownership or altering structs.
114///
115/// # Examples
116/// ```
117/// use secret_fmt::redact;
118/// let token = "sensitive_data";
119/// assert_eq!(format!("{}", redact!(&token)), "[REDACTED]");
120/// ```
121#[macro_export]
122macro_rules! redact {
123    ($val:expr) => {
124        $crate::Secret::new($val)
125    };
126}
127
128#[cfg(feature = "serde")]
129pub mod serialize_redacted {
130    //! Helper module to serialize a `Secret` as `"[REDACTED]"`.
131    //!
132    //! # Examples
133    //! ```
134    //! use serde::Serialize;
135    //! use secret_fmt::{Secret, serialize_redacted};
136    //!
137    //! #[derive(Serialize)]
138    //! struct LogPayload {
139    //!     #[serde(serialize_with = "serialize_redacted::serialize")]
140    //!     api_key: Secret<String>,
141    //! }
142    //! ```
143    use super::Secret;
144    use serde::Serializer;
145
146    /// Serializes the wrapped value as the literal string `"[REDACTED]"`.
147    ///
148    /// # Errors
149    /// Returns an error if the underlying serializer fails.
150    pub fn serialize<T, S>(_: &Secret<T>, serializer: S) -> Result<S::Ok, S::Error>
151    where
152        S: Serializer,
153    {
154        serializer.serialize_str("[REDACTED]")
155    }
156}
157
158#[cfg(feature = "serde")]
159pub mod serialize_actual {
160    //! Helper module to serialize the actual underlying value of a `Secret`.
161    //!
162    //! # Examples
163    //! ```
164    //! use serde::Serialize;
165    //! use secret_fmt::{Secret, serialize_actual};
166    //!
167    //! #[derive(Serialize)]
168    //! struct UpstreamRequest {
169    //!     #[serde(serialize_with = "serialize_actual::serialize")]
170    //!     api_key: Secret<String>,
171    //! }
172    //! ```
173    use super::Secret;
174    use serde::{Serialize, Serializer};
175
176    /// Serializes the actual underlying value, ignoring the redaction.
177    ///
178    /// # Errors
179    /// Returns an error if the underlying serializer fails.
180    pub fn serialize<T: Serialize, S>(val: &Secret<T>, serializer: S) -> Result<S::Ok, S::Error>
181    where
182        S: Serializer,
183    {
184        val.as_inner().serialize(serializer)
185    }
186}
187
188#[cfg(feature = "serde")]
189impl<'de, T> serde::Deserialize<'de> for Secret<T>
190where
191    T: serde::Deserialize<'de>,
192{
193    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
194    where
195        D: serde::Deserializer<'de>,
196    {
197        // Allow incoming JSON to parse the secret properly.
198        T::deserialize(deserializer).map(Self)
199    }
200}
201
202#[cfg(test)]
203mod tests {
204    use super::*;
205    // Using std only for testing string formatting and collections
206    extern crate std;
207    use std::collections::hash_map::DefaultHasher;
208    use std::format;
209    use std::hash::{Hash, Hasher};
210    use std::string::String;
211
212    #[test]
213    fn test_debug_redacted() {
214        let secret = Secret::new("super_secret_password");
215        assert_eq!(format!("{:?}", secret), "[REDACTED]");
216    }
217
218    #[test]
219    fn test_display_redacted() {
220        let secret = Secret::new(12345);
221        assert_eq!(format!("{}", secret), "[REDACTED]");
222    }
223
224    #[test]
225    fn test_as_inner() {
226        let mut secret = Secret::new(String::from("test"));
227        assert_eq!(secret.as_inner(), "test");
228
229        secret.as_inner_mut().push_str("ing");
230        assert_eq!(secret.into_inner(), "testing");
231    }
232
233    #[test]
234    fn test_derived_traits() {
235        // Test that traits like Eq, Ord, Hash transparently pass through to the inner value
236        let a = Secret::new(10);
237        let a_clone = a.clone();
238        let b = Secret::new(20);
239
240        assert_eq!(a, a_clone);
241        assert_ne!(a, b);
242        assert!(a < b);
243
244        let mut hasher_a = DefaultHasher::new();
245        a.hash(&mut hasher_a);
246        let mut hasher_inner = DefaultHasher::new();
247        10.hash(&mut hasher_inner);
248        assert_eq!(hasher_a.finish(), hasher_inner.finish());
249    }
250
251    #[test]
252    fn test_macro_and_conversions() {
253        let original = "sensitive_data";
254        let wrapped: Secret<&str> = original.into();
255
256        assert_eq!(wrapped.as_ref(), &"sensitive_data");
257        assert_eq!(format!("{}", redact!(&original)), "[REDACTED]");
258        assert_eq!(format!("{:?}", redact!(original)), "[REDACTED]");
259    }
260
261    #[cfg(feature = "serde")]
262    #[test]
263    fn test_serde() {
264        #[derive(serde::Serialize, serde::Deserialize, PartialEq, core::fmt::Debug)]
265        struct User {
266            id: u32,
267            #[serde(serialize_with = "serialize_redacted::serialize")]
268            api_key: Secret<String>,
269            #[serde(serialize_with = "serialize_actual::serialize")]
270            pass_through: Secret<String>,
271        }
272
273        let json = r#"{"id":123,"api_key":"secret_token_abc","pass_through":"sent_to_stripe"}"#;
274        let user: User = serde_json::from_str(json).unwrap();
275        assert_eq!(user.id, 123);
276        assert_eq!(user.api_key.as_inner(), "secret_token_abc");
277        assert_eq!(user.pass_through.as_inner(), "sent_to_stripe");
278
279        let serialized = serde_json::to_string(&user).unwrap();
280        assert_eq!(
281            serialized,
282            r#"{"id":123,"api_key":"[REDACTED]","pass_through":"sent_to_stripe"}"#
283        );
284    }
285}