diff --git a/src/lib.rs b/src/lib.rs index 9dd90ff..6af9df9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,7 +6,7 @@ //! The purpose of this macro is to allow for easy, configurable and efficient redaction of sensitive data in structs and enum variants. //! This can be used to hide sensitive data in logs or anywhere where personal data should not be exposed or stored. //! -//! Redaction is unicode-aware. Only alphanumeric characters are redacted. Whitespace, symbols and other characters are left as-is. +//! Redaction is unicode-aware by default. Unless opted out of, only alphanumeric characters are redacted. Whitespace, symbols and other characters are left as-is. //! //! # Controlling Redaction //! @@ -19,6 +19,7 @@ //! | **Modifier** | | **Effects** | | **Default** | //! |--------------------------------|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---|-----------------------------------------------| //! | `#[redact(partial)]` | | If the string is long enough, a small part of the
beginning and end will be exposed. If the string is too short to securely expose a portion of it, it will be redacted entirely. | | Disabled. The entire string will be redacted. | +//! | `#[redact(all_utf8)]` | | Overrides the redaction behavior to redact all characters instead of just alphanumeric characters. | | Disabled. | //! | `#[redact(with = 'X')]` | | Specifies the `char` the string will be redacted with. | | `'*'` | //! | `#[redact(fixed = )]` | | If this modifier is present, the length and contents of
the string are completely ignored and the string will always
be redacted as a fixed number of redaction characters. | | Disabled. | //! | `#[redact(display)]` | | Overrides the redaction behavior to use the type's [`Display`](std::fmt::Display) implementation instead of [`Debug`]. | | Disabled. | @@ -40,7 +41,7 @@ //! ```rust //! # use veil_macros::Redact; //! #[derive(Redact)] -//! #[redact(all, partial, with = 'X')] +//! #[redact(all, partial, with = 'X', all_utf8)] //! struct Foo { //! redact_me: String, //! also_redact_me: String, @@ -56,10 +57,10 @@ //! # use veil_macros::Redact; //! #[derive(Redact)] //! struct Foo { -//! #[redact(partial, with = 'X')] +//! #[redact(partial, with = 'X', all_utf8)] //! redact_me: String, //! -//! #[redact(partial, with = 'X')] +//! #[redact(partial, with = 'X', all_utf8)] //! also_redact_me: String, //! //! do_not_redact_me: String, diff --git a/src/private.rs b/src/private.rs index 73559ec..6734f54 100644 --- a/src/private.rs +++ b/src/private.rs @@ -35,6 +35,9 @@ pub enum RedactionLength { #[derive(Clone, Copy)] pub struct RedactFlags { + /// If we should skip non-alphanumeric characters when redacting + pub skip_non_alphanumeric: bool, + /// How much of the data to redact. pub redact_length: RedactionLength, @@ -54,7 +57,7 @@ impl RedactFlags { let count = to_redact.chars().filter(|char| char.is_alphanumeric()).count(); if count < Self::MIN_PARTIAL_CHARS { for char in to_redact.chars() { - if char.is_alphanumeric() { + if char.is_alphanumeric() && self.skip_non_alphanumeric { fmt.write_char(self.redact_char)?; } else { fmt.write_char(char)?; @@ -87,7 +90,7 @@ impl RedactFlags { pub(crate) fn redact_full(&self, fmt: &mut std::fmt::Formatter, to_redact: &str) -> std::fmt::Result { for char in to_redact.chars() { - if char.is_whitespace() || !char.is_alphanumeric() { + if (char.is_whitespace() || !char.is_alphanumeric()) && self.skip_non_alphanumeric { fmt.write_char(char)?; } else { fmt.write_char(self.redact_char)?; diff --git a/src/redactor.rs b/src/redactor.rs index 3d3944b..3c49d78 100644 --- a/src/redactor.rs +++ b/src/redactor.rs @@ -162,6 +162,7 @@ impl Redactor { pub struct RedactorBuilder { redact_char: Option, partial: bool, + skip_non_alphanumeric: bool, } impl RedactorBuilder { /// Initialize a new redaction flag builder. @@ -170,6 +171,7 @@ impl RedactorBuilder { Self { redact_char: None, partial: false, + skip_non_alphanumeric: true, } } @@ -191,26 +193,31 @@ impl RedactorBuilder { self } + /// Redact all utf8 characters, not just the alphanumeric ones. + /// + /// Equivalent to `#[redact(all_utf8)]` when deriving. + #[inline(always)] + pub const fn all_utf8(mut self) -> Self { + self.skip_non_alphanumeric = false; + self + } + /// Build the redaction flags. /// /// Returns an error if the state of the builder is invalid. /// The error will be optimised away by the compiler if the builder is valid at compile time, so it's safe and zero-cost to use `unwrap` on the result if you are constructing this at compile time. #[inline(always)] pub const fn build(self) -> Result { - let mut flags = RedactFlags { + let flags = RedactFlags { redact_length: if self.partial { RedactionLength::Partial } else { RedactionLength::Full }, - - redact_char: '*', + skip_non_alphanumeric: self.skip_non_alphanumeric, + redact_char: if let Some(ch) = self.redact_char { ch } else { '*' }, }; - if let Some(char) = self.redact_char { - flags.redact_char = char; - } - Ok(Redactor(flags)) } } diff --git a/veil-macros/src/flags.rs b/veil-macros/src/flags.rs index 7152e9d..11a4b79 100644 --- a/veil-macros/src/flags.rs +++ b/veil-macros/src/flags.rs @@ -105,12 +105,15 @@ pub struct RedactFlags { /// The character to use for redacting. Defaults to `*`. pub redact_char: char, + + pub all_utf8: bool, } impl Default for RedactFlags { fn default() -> Self { Self { redact_length: RedactionLength::Full, redact_char: '*', + all_utf8: false, } } } @@ -138,6 +141,8 @@ impl ExtractFlags for RedactFlags { NonZeroU8::new(int) .ok_or_else(|| syn::Error::new_spanned(int, "fixed redacting width must be greater than zero")) })?) + } else if meta.path.is_ident("all_utf8") { + self.all_utf8 = true; } else { return Ok(ParseMeta::Unrecognised); } @@ -149,12 +154,14 @@ impl quote::ToTokens for RedactFlags { let Self { redact_length, redact_char, + all_utf8, .. } = self; tokens.extend(quote! { redact_length: #redact_length, - redact_char: #redact_char + redact_char: #redact_char, + skip_non_alphanumeric: !#all_utf8, }); } } diff --git a/veil-tests/src/redaction_tests.rs b/veil-tests/src/redaction_tests.rs index bd73a05..1694da0 100644 --- a/veil-tests/src/redaction_tests.rs +++ b/veil-tests/src/redaction_tests.rs @@ -10,6 +10,7 @@ pub const SENSITIVE_DATA: &[&str] = &[ "039845734895", "10 Downing Street", "SensitiveVariant", + "password123!@#", ]; const DEBUGGY_PHRASE: &str = "Hello \"William\"!\nAnd here's the newline..."; @@ -280,6 +281,40 @@ fn test_derive_redactable_modifiers() { assert_eq!(buffer, "---"); } +#[test] +fn test_derive_redactable_all_utf8() { + #[derive(Redactable)] + #[redact(all_utf8)] + struct SensitiveString(String); + impl std::fmt::Display for SensitiveString { + fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(fmt) + } + } + + #[derive(Redactable)] + struct SensitiveStringLeaky(String); + impl std::fmt::Display for SensitiveStringLeaky { + fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(fmt) + } + } + + let sensitive = SensitiveString(SENSITIVE_DATA[5].to_string()); + let sensitive_leaky = SensitiveStringLeaky(SENSITIVE_DATA[5].to_string()); + + assert_eq!(sensitive.redact(), "**************"); + assert_eq!(sensitive_leaky.redact(), "***********!@#"); + + let mut buffer = String::new(); + sensitive.redact_into(&mut buffer).unwrap(); + assert_eq!(buffer, "**************"); + + buffer.clear(); + sensitive_leaky.redact_into(&mut buffer).unwrap(); + assert_eq!(buffer, "***********!@#"); +} + #[test] fn test_derive_redactable_dyn() { #[derive(Redactable)] @@ -316,6 +351,7 @@ fn test_derive_redactable_dyn() { fn test_enum_variant_names() { #[derive(Debug)] enum Control { + #[allow(dead_code)] Foo(String), Bar, }