1use std::borrow::Cow;
14use std::sync::LazyLock;
15use std::sync::atomic::{AtomicBool, Ordering};
16
17pub const PREFIX: &str = "DRE_SECRET_";
18pub const MASK: &str = "*****";
19pub const MASKED_SOURCE_ERROR: &str =
20 "source error details omitted because the request contains a masked secret";
21const MIN_LEN: usize = 4;
22
23static ENABLED: AtomicBool = AtomicBool::new(true);
24
25fn eligible(value: &str) -> bool {
26 value.chars().count() >= MIN_LEN
27}
28
29static SECRETS: LazyLock<Vec<String>> = LazyLock::new(|| {
31 let mut v: Vec<String> = std::env::vars()
32 .filter(|(k, v)| k.starts_with(PREFIX) && eligible(v))
33 .map(|(_, v)| v)
34 .collect();
35 v.sort_by_key(|s| std::cmp::Reverse(s.len()));
36 v.dedup();
37 v
38});
39
40pub fn set_enabled(on: bool) {
41 ENABLED.store(on, Ordering::Relaxed);
42}
43
44pub(crate) fn enabled() -> bool {
45 ENABLED.load(Ordering::Relaxed)
46}
47
48fn mask_with<'a>(s: &'a str, secrets: &[String]) -> Cow<'a, str> {
49 if !secrets.iter().any(|x| s.contains(x.as_str())) {
50 return Cow::Borrowed(s);
51 }
52 let mut out = s.to_string();
53 for x in secrets {
54 out = out.replace(x.as_str(), MASK);
55 }
56 Cow::Owned(out)
57}
58
59pub fn mask(s: &str) -> Cow<'_, str> {
61 if !ENABLED.load(Ordering::Relaxed) {
62 return Cow::Borrowed(s);
63 }
64 mask_with(s, &SECRETS)
65}
66
67pub(crate) fn mask_markdown(s: &str) -> Cow<'_, str> {
70 static VALUES: LazyLock<Vec<String>> = LazyLock::new(|| {
71 let mut values: Vec<String> = SECRETS
72 .iter()
73 .flat_map(|s| [s.clone(), dre_protocol::markdown::escape(s)])
74 .collect();
75 values.sort_by_key(|s| std::cmp::Reverse(s.len()));
76 values.dedup();
77 values
78 });
79 if !ENABLED.load(Ordering::Relaxed) {
80 return Cow::Borrowed(s);
81 }
82 mask_with(s, &VALUES)
83}
84
85pub(crate) fn mask_message_title(s: &str) -> Cow<'_, str> {
88 static VALUES: LazyLock<Vec<String>> = LazyLock::new(|| {
89 let mut values: Vec<String> = SECRETS
90 .iter()
91 .flat_map(|s| [s.clone(), s.split_whitespace().collect::<Vec<_>>().join(" ")])
92 .filter(|s| !s.is_empty())
93 .collect();
94 values.sort_by_key(|s| std::cmp::Reverse(s.len()));
95 values.dedup();
96 values
97 });
98 if !ENABLED.load(Ordering::Relaxed) {
99 return Cow::Borrowed(s);
100 }
101 mask_with(s, &VALUES)
102}
103
104pub fn contains_secret(s: &str) -> bool {
106 ENABLED.load(Ordering::Relaxed) && SECRETS.iter().any(|secret| s.contains(secret))
107}
108
109fn mask_source_error_with<'a>(error: &'a str, sensitive: bool, secrets: &[String]) -> Cow<'a, str> {
110 if sensitive {
111 Cow::Borrowed(MASKED_SOURCE_ERROR)
112 } else {
113 mask_with(error, secrets)
114 }
115}
116
117pub fn mask_source_error(error: &str, sensitive: bool) -> Cow<'_, str> {
121 if !ENABLED.load(Ordering::Relaxed) {
122 return Cow::Borrowed(error);
123 }
124 mask_source_error_with(error, sensitive, &SECRETS)
125}
126
127fn unique_masked_names_with(names: &[String], secrets: &[String]) -> Vec<String> {
128 use std::collections::BTreeSet;
129
130 let masked: Vec<String> = names
131 .iter()
132 .map(|name| mask_with(name, secrets).into_owned())
133 .collect();
134 if names == masked {
135 return names.to_vec();
136 }
137 let mut used = BTreeSet::new();
138 let mut output = vec![String::new(); names.len()];
139 let mut pending = Vec::new();
140
141 for (index, (original, protected)) in names.iter().zip(&masked).enumerate() {
143 if original == protected {
144 used.insert(original.clone());
145 output[index].clone_from(original);
146 } else {
147 pending.push((protected.clone(), original.clone(), index));
148 }
149 }
150
151 pending.sort();
153 for (base, _, index) in pending {
154 let mut candidate = base.clone();
155 let mut suffix = 2;
156 while !used.insert(candidate.clone()) {
157 candidate = format!("{base}#{suffix}");
158 suffix += 1;
159 }
160 output[index] = candidate;
161 }
162 output
163}
164
165fn redact_json_with(v: &mut serde_json::Value, secrets: &[String]) {
166 use serde_json::Value;
167 match v {
168 Value::String(s) => {
169 if let Cow::Owned(m) = mask_with(s, secrets) {
170 *s = m;
171 }
172 }
173 Value::Array(a) => a.iter_mut().for_each(|v| redact_json_with(v, secrets)),
174 Value::Object(o) => {
175 let entries = std::mem::take(o);
176 let keys: Vec<String> = entries.keys().cloned().collect();
177 let protected_keys = unique_masked_names_with(&keys, secrets);
178 for ((_, mut val), protected_key) in entries.into_iter().zip(protected_keys) {
179 redact_json_with(&mut val, secrets);
180 o.insert(protected_key, val);
181 }
182 }
183 _ => {}
184 }
185}
186
187pub fn redact_json(v: &mut serde_json::Value) {
190 if ENABLED.load(Ordering::Relaxed) {
191 redact_json_with(v, &SECRETS);
192 }
193}
194
195fn redacted<T: serde::Serialize>(v: &T) -> serde_json::Result<serde_json::Value> {
196 let mut v = serde_json::to_value(v)?;
197 redact_json(&mut v);
198 Ok(v)
199}
200
201pub fn to_json_pretty<T: serde::Serialize>(v: &T) -> serde_json::Result<String> {
203 serde_json::to_string_pretty(&redacted(v)?)
204}
205
206pub fn to_json_line<T: serde::Serialize>(v: &T) -> serde_json::Result<String> {
208 serde_json::to_string(&redacted(v)?)
209}
210
211#[cfg(test)]
212mod tests {
213 use super::*;
214 use serde_json::json;
215
216 fn secrets(values: &[&str]) -> Vec<String> {
217 let mut values: Vec<String> = values.iter().map(|v| (*v).to_string()).collect();
218 values.sort_by_key(|s| std::cmp::Reverse(s.len()));
219 values
220 }
221
222 #[test]
223 fn redacts_nested_values_longest_first() {
224 let secrets = secrets(&["nested-secret", "nested-secret-and-more"]);
225 let mut value = json!({"items": ["nested-secret-and-more", {"value": "x nested-secret y"}]});
226 redact_json_with(&mut value, &secrets);
227 assert_eq!(value, json!({"items": ["*****", {"value": "x ***** y"}]}));
228 }
229
230 #[test]
231 fn preserves_entries_when_masked_keys_collide() {
232 let secrets = secrets(&["alpha-secret-key", "bravo-secret-key"]);
233 let mut value = json!({
234 "alpha-secret-key": 1,
235 "bravo-secret-key": 2,
236 "*****": 3,
237 "*****#2": 4,
238 });
239 redact_json_with(&mut value, &secrets);
240 assert_eq!(
241 value,
242 json!({
243 "*****": 3,
244 "*****#2": 4,
245 "*****#3": 1,
246 "*****#4": 2,
247 })
248 );
249 }
250
251 #[test]
252 fn masking_one_name_does_not_rename_duplicate_literal_names() {
253 let secrets = secrets(&["secret-column"]);
254 let names = ["a", "a", "secret-column"].map(str::to_string);
255 assert_eq!(unique_masked_names_with(&names, &secrets), ["a", "a", "*****"]);
256 }
257
258 #[test]
259 fn suppresses_transformed_source_errors_only_for_secret_requests() {
260 let secrets = secrets(&["long-secret-value-that-a-source-truncated"]);
261 let error = "LINE 1: select * from long-secret-value-that-a-source...";
262 assert_eq!(mask_source_error_with(error, true, &secrets), MASKED_SOURCE_ERROR);
263 assert_eq!(
264 mask_source_error_with("ordinary syntax error", false, &secrets),
265 "ordinary syntax error"
266 );
267 }
268
269 #[test]
270 fn minimum_length_counts_unicode_characters() {
271 assert!(!eligible("ééé"));
272 assert!(eligible("éééé"));
273 }
274}