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}