Skip to main content

earl_core/
redact.rs

1use std::collections::BTreeSet;
2
3use base64::Engine;
4
5#[derive(Debug, Default, Clone)]
6pub struct Redactor {
7    secrets: Vec<String>,
8}
9
10impl Redactor {
11    pub fn new<I: IntoIterator<Item = String>>(secrets: I) -> Self {
12        let mut variants = BTreeSet::new();
13
14        for secret in secrets {
15            if secret.is_empty() {
16                continue;
17            }
18
19            variants.insert(secret.clone());
20
21            if secret.len() >= 6 {
22                let bytes = secret.as_bytes();
23                variants.insert(base64::engine::general_purpose::STANDARD.encode(bytes));
24                variants.insert(base64::engine::general_purpose::URL_SAFE.encode(bytes));
25                variants.insert(hex_encode(bytes, false));
26                variants.insert(hex_encode(bytes, true));
27
28                let url_encoded = url::form_urlencoded::byte_serialize(bytes).collect::<String>();
29                if !url_encoded.is_empty() {
30                    variants.insert(url_encoded);
31                }
32            }
33        }
34
35        let mut ordered: Vec<String> = variants.into_iter().collect();
36        ordered.sort_by(|left, right| right.len().cmp(&left.len()).then_with(|| left.cmp(right)));
37
38        Self { secrets: ordered }
39    }
40
41    pub fn redact(&self, input: &str) -> String {
42        let mut out = input.to_string();
43        for secret in &self.secrets {
44            out = out.replace(secret, "[REDACTED]");
45        }
46        out
47    }
48
49    pub fn redact_json(&self, value: &serde_json::Value) -> serde_json::Value {
50        match value {
51            serde_json::Value::Null => serde_json::Value::Null,
52            serde_json::Value::Bool(v) => serde_json::Value::Bool(*v),
53            serde_json::Value::Number(v) => serde_json::Value::Number(v.clone()),
54            serde_json::Value::String(v) => serde_json::Value::String(self.redact(v)),
55            serde_json::Value::Array(values) => {
56                serde_json::Value::Array(values.iter().map(|v| self.redact_json(v)).collect())
57            }
58            serde_json::Value::Object(map) => serde_json::Value::Object(
59                map.iter()
60                    .map(|(k, v)| (k.clone(), self.redact_json(v)))
61                    .collect(),
62            ),
63        }
64    }
65}
66
67fn hex_encode(bytes: &[u8], uppercase: bool) -> String {
68    let mut out = String::with_capacity(bytes.len() * 2);
69    for b in bytes {
70        if uppercase {
71            out.push_str(&format!("{:02X}", b));
72        } else {
73            out.push_str(&format!("{:02x}", b));
74        }
75    }
76    out
77}