use base64::Engine as _;
use base64::engine::general_purpose::{STANDARD, STANDARD_NO_PAD, URL_SAFE_NO_PAD};
const FRAGMENT: usize = 12;
struct Sentinel {
label: &'static str,
needles: Vec<(&'static str, String)>,
}
impl Sentinel {
fn new(label: &'static str, value: &str) -> Self {
let bytes = value.as_bytes();
let mut needles = vec![
("raw", value.to_owned()),
("lowercase", value.to_lowercase()),
("uppercase", value.to_uppercase()),
("base64", STANDARD.encode(bytes)),
("base64-unpadded", STANDARD_NO_PAD.encode(bytes)),
("base64url", URL_SAFE_NO_PAD.encode(bytes)),
("hex", hex(bytes, false)),
("hex-uppercase", hex(bytes, true)),
];
for window in value.as_bytes().windows(FRAGMENT.min(value.len())) {
let fragment = String::from_utf8(window.to_vec()).expect("sentinels are ASCII");
needles.push(("fragment", fragment));
}
needles.sort();
needles.dedup_by(|left, right| left.1 == right.1);
Self { label, needles }
}
fn found_in(&self, haystack: &str) -> Option<&'static str> {
self.whole_in(haystack).or_else(|| {
self.needles
.iter()
.find(|(_, needle)| haystack.contains(needle.as_str()))
.map(|(encoding, _)| *encoding)
})
}
fn whole_in(&self, haystack: &str) -> Option<&'static str> {
self.needles
.iter()
.filter(|(encoding, _)| *encoding != "fragment")
.find(|(_, needle)| haystack.contains(needle.as_str()))
.map(|(encoding, _)| *encoding)
}
}
fn hex(bytes: &[u8], upper: bool) -> String {
bytes
.iter()
.map(|byte| {
if upper {
format!("{byte:02X}")
} else {
format!("{byte:02x}")
}
})
.collect()
}
pub(crate) struct LeakSweep {
sentinels: Vec<Sentinel>,
}
impl LeakSweep {
pub(crate) fn of<'a>(materials: impl IntoIterator<Item = (&'static str, &'a str)>) -> Self {
Self {
sentinels: materials
.into_iter()
.map(|(label, value)| Sentinel::new(label, value))
.collect(),
}
}
pub(crate) fn assert_absent(&self, surface: &str, rendered: &str) {
let found = self
.sentinels
.iter()
.find_map(|sentinel| Some((sentinel.label, sentinel.whole_in(rendered)?)))
.or_else(|| {
self.sentinels
.iter()
.find_map(|sentinel| Some((sentinel.label, sentinel.found_in(rendered)?)))
});
if let Some((label, encoding)) = found {
panic!(
"{surface} discloses the `{label}` sentinel ({encoding} encoding); \
{} bytes of surface were swept and the material is deliberately not printed",
rendered.len()
);
}
}
pub(crate) fn assert_absent_bytes(&self, surface: &str, bytes: &[u8]) {
self.assert_absent(surface, &String::from_utf8_lossy(bytes));
for sentinel in &self.sentinels {
for (encoding, needle) in &sentinel.needles {
assert!(
!bytes
.windows(needle.len())
.any(|window| window == needle.as_bytes()),
"{surface} discloses the `{}` sentinel ({encoding} encoding) in its raw bytes",
sentinel.label
);
}
}
}
pub(crate) fn assert_present(&self, surface: &str, label: &str, rendered: &str) {
let sentinel = self
.sentinels
.iter()
.find(|sentinel| sentinel.label == label)
.unwrap_or_else(|| panic!("no `{label}` sentinel in this sweep"));
assert!(
sentinel.whole_in(rendered).is_some(),
"{surface} does not carry the `{label}` sentinel, so a redaction assertion \
against it would pass for the wrong reason"
);
}
}
#[cfg(test)]
mod tests {
use super::*;
const MATERIAL: &str = "sk-axond-sentinel-detector-90ab";
fn sweep() -> LeakSweep {
LeakSweep::of([("material", MATERIAL)])
}
#[test]
fn clean_text_sweeps_clean() {
sweep().assert_absent(
"a redacted surface",
"SecretMaterial(<redacted>) sk-… 30 bytes",
);
}
#[test]
fn every_encoding_of_the_material_is_detected() {
let bytes = MATERIAL.as_bytes();
for rendered in [
MATERIAL.to_owned(),
MATERIAL.to_uppercase(),
format!("token={}", STANDARD.encode(bytes)),
format!("token={}", STANDARD_NO_PAD.encode(bytes)),
format!("token={}", URL_SAFE_NO_PAD.encode(bytes)),
format!("\\x{}", hex(bytes, false)),
format!("\\X{}", hex(bytes, true)),
format!("credential={}…", &MATERIAL[..16]),
] {
let sweep = sweep();
let caught = std::panic::catch_unwind(move || {
sweep.assert_absent("a leaking surface", &rendered);
});
assert!(caught.is_err(), "an encoded leak went undetected");
}
}
#[test]
fn the_failure_report_does_not_reprint_the_material() {
let sweep = sweep();
let panic = std::panic::catch_unwind(move || {
sweep.assert_absent("the response body", MATERIAL);
})
.expect_err("the leak is detected");
let message = panic
.downcast_ref::<String>()
.expect("a formatted panic message");
assert!(message.contains("the response body"), "{message}");
assert!(!message.contains(&MATERIAL[..FRAGMENT]), "{message}");
}
#[test]
fn raw_bytes_are_swept_even_when_they_are_not_utf8() {
let mut bytes = vec![0xff, 0xfe];
bytes.extend_from_slice(MATERIAL.as_bytes());
let sweep = sweep();
let caught = std::panic::catch_unwind(move || {
sweep.assert_absent_bytes("a binary column", &bytes);
});
assert!(caught.is_err(), "a leak in non-UTF-8 bytes went undetected");
}
#[test]
fn the_tripwire_does_not_accept_a_sentinel_that_merely_looks_similar() {
let sweep = LeakSweep::of([
("provider", "sk-axond-sentinel-provider-6f21a9d0c7b4"),
("rotated", "sk-axond-sentinel-rotated-b48c37e1590a"),
]);
let caught = std::panic::catch_unwind(move || {
sweep.assert_present(
"an upstream presented the wrong key",
"provider",
"Bearer sk-axond-sentinel-rotated-b48c37e1590a",
);
});
assert!(
caught.is_err(),
"a shared prefix let the tripwire accept a different sentinel"
);
}
#[test]
fn the_tripwire_fires_when_the_material_never_entered_the_surface() {
let sweep = sweep();
let caught = std::panic::catch_unwind(move || {
sweep.assert_present("a fake upstream", "material", "Bearer something-else");
});
assert!(
caught.is_err(),
"a vacuous redaction test would not be caught"
);
}
}