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}