#![cfg_attr(not(feature = "std"), no_std)]
#![forbid(unsafe_code)]
#![warn(missing_docs)]
extern crate alloc;
use alloc::borrow::Cow;
use alloc::boxed::Box;
use alloc::format;
use alloc::string::String;
use alloc::vec::Vec;
pub mod detectors;
mod types;
pub use detectors::{Detector, FnDetector};
pub use types::{Kind, Match};
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub enum Mask {
#[default]
Label,
Fixed(Cow<'static, str>),
Char(char),
Partial {
keep_last: usize,
ch: char,
},
Hash,
}
impl Mask {
pub fn fixed<S>(s: S) -> Self
where
S: Into<Cow<'static, str>>,
{
Self::Fixed(s.into())
}
}
pub struct Redactor {
detectors: Vec<Box<dyn Detector>>,
mask: Mask,
}
impl Default for Redactor {
fn default() -> Self {
Self::new()
}
}
impl Redactor {
pub fn new() -> Self {
Self {
detectors: default_detectors(),
mask: Mask::Label,
}
}
pub fn empty() -> Self {
Self {
detectors: Vec::new(),
mask: Mask::Label,
}
}
pub fn only(kinds: &[Kind]) -> Self {
let detectors = default_detectors()
.into_iter()
.filter(|d| kinds.contains(&d.kind()))
.collect();
Self {
detectors,
mask: Mask::Label,
}
}
pub fn mask(mut self, mask: Mask) -> Self {
self.mask = mask;
self
}
pub fn with_detector<D: Detector + 'static>(mut self, detector: D) -> Self {
self.detectors.push(Box::new(detector));
self
}
pub fn without(mut self, kind: &Kind) -> Self {
self.detectors.retain(|d| &d.kind() != kind);
self
}
pub fn find(&self, input: &str) -> Vec<Match> {
let mut raw = Vec::new();
for d in &self.detectors {
d.detect(input, &mut raw);
}
resolve_overlaps(raw)
}
pub fn is_dirty(&self, input: &str) -> bool {
self.detectors.iter().any(|d| {
let mut v = Vec::new();
d.detect(input, &mut v);
!v.is_empty()
})
}
pub fn clean(&self, input: &str) -> String {
let matches = self.find(input);
if matches.is_empty() {
return String::from(input);
}
let mut out = String::with_capacity(input.len());
let mut cursor = 0;
for m in &matches {
if m.start > cursor {
out.push_str(&input[cursor..m.start]);
}
out.push_str(&self.render(m, &input[m.start..m.end]));
cursor = m.end;
}
if cursor < input.len() {
out.push_str(&input[cursor..]);
}
out
}
fn render(&self, m: &Match, original: &str) -> String {
match &self.mask {
Mask::Label => format!("[REDACTED:{}]", m.kind.label()),
Mask::Fixed(s) => String::from(s.as_ref()),
Mask::Char(c) => core::iter::repeat(*c)
.take(original.chars().count())
.collect(),
Mask::Partial { keep_last, ch } => {
let total = original.chars().count();
let keep = (*keep_last).min(total);
let masked = total - keep;
let mut s = String::with_capacity(total);
for _ in 0..masked {
s.push(*ch);
}
s.extend(original.chars().skip(masked));
s
}
Mask::Hash => format!("[{}:{:08x}]", m.kind.label(), fnv1a(original.as_bytes())),
}
}
}
fn default_detectors() -> Vec<Box<dyn Detector>> {
use detectors::*;
alloc::vec![
Box::new(PrivateKey) as Box<dyn Detector>,
Box::new(Jwt),
Box::new(GitHubToken),
Box::new(SlackToken),
Box::new(StripeKey),
Box::new(OpenAiKey),
Box::new(GoogleApiKey),
Box::new(AwsAccessKey),
Box::new(UrlCredentials),
Box::new(Email),
Box::new(Iban),
Box::new(CreditCard),
Box::new(IpV6),
Box::new(IpV4),
Box::new(MacAddress),
Box::new(UsSsn),
Box::new(PhoneNumber),
]
}
fn resolve_overlaps(mut matches: Vec<Match>) -> Vec<Match> {
matches.sort_by(|a, b| a.start.cmp(&b.start).then_with(|| b.len().cmp(&a.len())));
let mut kept: Vec<Match> = Vec::with_capacity(matches.len());
let mut last_end = 0usize;
for m in matches {
if m.start >= last_end {
last_end = m.end;
kept.push(m);
}
}
kept
}
fn fnv1a(bytes: &[u8]) -> u32 {
let mut hash: u32 = 0x811c_9dc5;
for &b in bytes {
hash ^= b as u32;
hash = hash.wrapping_mul(0x0100_0193);
}
hash
}