Skip to main content

dre_core/
secrets.rs

1//! Secret masking. The value of every environment variable named `DRE_SECRET_*` is replaced by
2//! `*****` in everything DRE writes for people to read: the console, `logs/dre.log`, JSON events,
3//! `run_results.json` and the compiled SQL in `target/compiled/`. The SQL actually sent to the
4//! database is unchanged. `mask_secrets: false` in `dre_project.yml` turns masking off.
5//!
6//! JSON is redacted structurally, before it is serialized: serializing escapes `"`, `\` and
7//! control characters, so masking the finished text would miss a secret that contains them. Use
8//! [`to_json_pretty`] / [`to_json_line`] for every JSON surface, never `mask` on JSON text.
9//!
10//! Values shorter than 4 characters aren't masked: replacing every `a` or `12` in a log would
11//! make it unreadable, and a value that short isn't a secret anyway.
12
13use std::borrow::Cow;
14use std::sync::LazyLock;
15use std::sync::atomic::{AtomicBool, Ordering};
16
17pub const PREFIX: &str = "DRE_SECRET_";
18pub const MASK: &str = "*****";
19const MIN_LEN: usize = 4;
20
21static ENABLED: AtomicBool = AtomicBool::new(true);
22
23/// Secret values, longest first so a secret containing another is masked whole.
24static SECRETS: LazyLock<Vec<String>> = LazyLock::new(|| {
25    let mut v: Vec<String> = std::env::vars()
26        .filter(|(k, v)| k.starts_with(PREFIX) && v.chars().count() >= MIN_LEN)
27        .map(|(_, v)| v)
28        .collect();
29    v.sort_by_key(|s| std::cmp::Reverse(s.len()));
30    v.dedup();
31    v
32});
33
34pub fn set_enabled(on: bool) {
35    ENABLED.store(on, Ordering::Relaxed);
36}
37
38/// `s` with every secret value replaced by `*****`.
39pub fn mask(s: &str) -> Cow<'_, str> {
40    if !ENABLED.load(Ordering::Relaxed) || !SECRETS.iter().any(|x| s.contains(x.as_str())) {
41        return Cow::Borrowed(s);
42    }
43    let mut out = s.to_string();
44    for x in SECRETS.iter() {
45        out = out.replace(x.as_str(), MASK);
46    }
47    Cow::Owned(out)
48}
49
50/// `v` with every secret value replaced by `*****` in all strings and object keys.
51pub fn redact_json(v: &mut serde_json::Value) {
52    use serde_json::Value;
53    match v {
54        Value::String(s) => {
55            if let Cow::Owned(m) = mask(s) {
56                *s = m;
57            }
58        }
59        Value::Array(a) => a.iter_mut().for_each(redact_json),
60        Value::Object(o) => {
61            let entries = std::mem::take(o);
62            for (k, mut val) in entries {
63                redact_json(&mut val);
64                o.insert(mask(&k).into_owned(), val);
65            }
66        }
67        _ => {}
68    }
69}
70
71fn redacted<T: serde::Serialize>(v: &T) -> serde_json::Result<serde_json::Value> {
72    let mut v = serde_json::to_value(v)?;
73    redact_json(&mut v);
74    Ok(v)
75}
76
77/// Pretty JSON of `v` with secrets redacted first.
78pub fn to_json_pretty<T: serde::Serialize>(v: &T) -> serde_json::Result<String> {
79    serde_json::to_string_pretty(&redacted(v)?)
80}
81
82/// One-line JSON of `v` with secrets redacted first.
83pub fn to_json_line<T: serde::Serialize>(v: &T) -> serde_json::Result<String> {
84    serde_json::to_string(&redacted(v)?)
85}