Skip to main content

secure_serialize/
lib.rs

1//! # secure-serialize
2//!
3//! A proc-macro crate that automatically redacts sensitive fields during serialization.
4//!
5//! When a struct is derived with `#[derive(SecureSerialize)]`, all fields marked with
6//! `#[redact]` will be replaced with `"<redacted>"` (or a custom string) when serialized via
7//! `serde::Serialize`. For cases where you need the real values (internal operations like config
8//! hot-reloading), the `to_json_unredacted()` method is available.
9//!
10//! ## Example
11//!
12//! ```
13//! use secure_serialize::SecureSerialize;
14//! use serde::Deserialize;
15//!
16//! #[derive(Deserialize, SecureSerialize)]
17//! struct Config {
18//!     pub host: String,
19//!
20//!     /// This field will be redacted to "<redacted>" when serialized
21//!     #[redact]
22//!     pub api_key: String,
23//!
24//!     /// This field will be redacted to "***" when serialized
25//!     #[redact(with = "***")]
26//!     pub password: String,
27//! }
28//!
29//! let config = Config {
30//!     host: "localhost".to_string(),
31//!     api_key: "secret123".to_string(),
32//!     password: "my_password".to_string(),
33//! };
34//!
35//! // Serialized version has redacted fields
36//! let serialized = serde_json::to_value(&config).unwrap();
37//! assert_eq!(serialized["api_key"], "<redacted>");
38//! assert_eq!(serialized["password"], "***");
39//! assert_eq!(serialized["host"], "localhost");
40//!
41//! // Unredacted version has all real values (internal use only!)
42//! let unredacted = config.to_json_unredacted().unwrap();
43//! assert_eq!(unredacted["api_key"], "secret123");
44//! assert_eq!(unredacted["password"], "my_password");
45//! ```
46//!
47//! ## Attributes
48//!
49//! ### `#[redact]`
50//!
51//! Mark a field as sensitive. When serialized, it will be replaced with `"<redacted>"`.
52//!
53//! ```ignore
54//! #[derive(SecureSerialize)]
55//! struct Config {
56//!     #[redact]
57//!     pub secret: String,
58//! }
59//! ```
60//!
61//! ### `#[redact(with = "...")]`
62//!
63//! Mark a field as sensitive and specify a custom redaction string.
64//!
65//! ```ignore
66//! #[derive(SecureSerialize)]
67//! struct Config {
68//!     #[redact(with = "***")]
69//!     pub password: String,
70//! }
71//! ```
72//!
73//! ### `#[secure_serialize(debug)]` and `#[secure_serialize(display)]`
74//!
75//! Optional struct-level attributes (place them on the struct, next to `derive`):
76//!
77//! - **`debug`** — generates `impl std::fmt::Debug` where `#[redact]` fields show the redaction
78//!   string instead of real values. Use this for `{:?}`, `dbg!`, and typical logging.
79//! - **`display`** — generates `impl std::fmt::Display` as compact JSON with the same redaction as
80//!   `serde_json::to_string` (requires `serde_json` in your crate’s dependency graph, same as
81//!   `to_json_unredacted`).
82//!
83//! You can combine them: `#[secure_serialize(debug, display)]`.
84//!
85//! If you omit these, behavior stays as before: only `Serialize` redacts. `#[derive(Debug)]` alone
86//! still prints real secrets — opt in to `#[secure_serialize(debug)]` when you want safe `Debug`.
87//!
88//! ```ignore
89//! #[derive(Deserialize, SecureSerialize)]
90//! #[secure_serialize(debug, display)]
91//! struct Config {
92//!     pub host: String,
93//!     #[redact]
94//!     pub api_key: String,
95//! }
96//! ```
97//!
98//! ## Trait Methods
99//!
100//! - `redacted_keys()` — Returns a static slice of all redacted field names.
101//! - `to_json_unredacted()` — Returns a JSON value with all real values (no redaction).
102//!   Use this only for internal operations where you need actual values.
103//! - `to_json_with_revealed_fields()` — Same as normal JSON serialization, but you pass a list of
104//!   redacted field names to expose with real values; all other redacted fields stay redacted.
105//!
106//! ⚠️ **Warning**: `to_json_unredacted()` exposes all sensitive data. Use it only internally,
107//! never expose its output to logs, APIs, or external systems.
108//!
109//! ⚠️ **`to_json_with_revealed_fields`** still exposes real values for every field you list. Use
110//! only in controlled contexts (for example internal tooling or selective debugging).
111
112pub use secure_serialize_derive::SecureSerialize;
113
114/// Constant string used for default redaction.
115pub const REDACTED: &str = "<redacted>";
116
117/// Trait for types that support secure serialization with automatic redaction of sensitive fields.
118///
119/// Implementors should derive `#[derive(SecureSerialize)]` to automatically generate implementations.
120/// The trait requires `serde::Serialize`, so all redactable types can be serialized.
121///
122/// When a struct is serialized via `serde::Serialize`, fields marked with `#[redact]` are replaced
123/// with redaction strings. For redacted `Debug` / JSON `Display`, add
124/// `#[secure_serialize(debug)]` or `#[secure_serialize(display)]` on the struct.
125///
126/// For internal operations where you need all real values, use `to_json_unredacted()`. To expose
127/// only a subset of redacted fields, use `to_json_with_revealed_fields()`.
128pub trait SecureSerialize: serde::Serialize {
129    /// Returns the names of all redacted fields in this struct.
130    ///
131    /// These are the field names that will be redacted when the struct is serialized.
132    /// Names are in snake_case.
133    fn redacted_keys() -> &'static [&'static str];
134
135    /// Serializes this struct to a JSON value with all real values exposed (no redaction).
136    ///
137    /// ⚠️ **Use only for internal operations** where you actually need the real sensitive values,
138    /// such as config hot-reloading or merging. Never use this for display, logging, or API responses.
139    ///
140    /// # Example
141    ///
142    /// ```ignore
143    /// let config = load_config();
144    /// // Safe: this is internal config merging logic
145    /// let full_values = config.to_json_unredacted()?;
146    /// ```
147    fn to_json_unredacted(&self) -> Result<serde_json::Value, serde_json::Error>;
148
149    /// Serializes this value to JSON like [`serde::Serialize`], then replaces listed redacted
150    /// fields with their real values from [`Self::to_json_unredacted`].
151    ///
152    /// Only names that appear in [`Self::redacted_keys()`] are affected. Other keys in `reveal`
153    /// are ignored (non-redacted fields are already serialized normally). Unknown or misspelled
154    /// redacted names leave the redaction placeholder in place if the key is missing from the
155    /// unredacted map.
156    ///
157    /// This runs two JSON serializations (redacted snapshot plus full unredacted). Prefer
158    /// [`Self::to_json_unredacted`] when you need every secret, or plain `serde_json::to_value`
159    /// when you need none.
160    ///
161    /// ⚠️ **Security**: Each revealed field exposes real sensitive data. Use only in trusted,
162    /// internal code paths.
163    ///
164    /// # Example
165    ///
166    /// ```ignore
167    /// let json = config.to_json_with_revealed_fields(&["api_key"])?;
168    /// // api_key is real; other #[redact] fields stay redacted
169    /// ```
170    fn to_json_with_revealed_fields(
171        &self,
172        reveal: &[&str],
173    ) -> Result<serde_json::Value, serde_json::Error> {
174        let mut v = serde_json::to_value(self)?;
175        let full = self.to_json_unredacted()?;
176        let reds = Self::redacted_keys();
177        if let (serde_json::Value::Object(map), serde_json::Value::Object(full_map)) =
178            (&mut v, full)
179        {
180            for &key in reveal {
181                if reds.iter().any(|&k| k == key) {
182                    if let Some(val) = full_map.get(key) {
183                        map.insert(key.to_string(), val.clone());
184                    }
185                }
186            }
187        }
188        Ok(v)
189    }
190}