redaction 0.1.0

Layered data redaction controls: classification and redaction
Documentation
redaction-0.1.0 has been yanked.

Redaction

redaction helps you keep sensitive values (tokens, secrets, PII) out of places they don't belong by:

  • Deriving Sensitive on your types with #[derive(Sensitive)]
  • Marking sensitive fields with #[sensitive(Classification)]
  • Calling .redact() to produce a copy that is safe to log or serialize
  • Generating Debug output that prints "[REDACTED]" for sensitive fields (independent of policies)

Design (DDD / Clean Architecture friendly)

  • Classifications are domain concepts: marker types like Secret, Token, or your own InternalId represent what kind of data a field contains.
  • Policies belong in the application layer: policies are attached to classification types (impl RedactionPolicy for MyClassification) in the layer where you define “what is safe to expose”, typically close to logging/serialization boundaries.
  • Sinks are optional adapters: integrations like slog live behind feature flags; your domain types don’t depend on a logging framework.
  • Layering is optional: you can put classifications, policies, and redaction calls in a single crate if you prefer. The library supports both “clean architecture” layering and simple, pragmatic project layouts.

The Problem

Sensitive data ends up places it shouldn't:

  • Logging: Structured logs capture request/response bodies containing passwords, tokens, PII
  • Serialization: API responses, database exports, and message queues include fields that should be hidden
  • Error reporting: Stack traces and error contexts expose sensitive state
  • Debug output: #[derive(Debug)] prints everything, including secrets

Once sensitive data reaches these systems, it is often:

  • Stored long-term (retention policies, backups)
  • Indexed and searchable
  • Replicated across environments
  • Visible to anyone with access to logs/telemetry
#[derive(Debug, serde::Serialize)]
struct LoginRequest {
    username: String,
    password: String,
}

let request = LoginRequest {
    username: "alice".into(),
    password: "hunter2".into(),
};

// Debug output exposes the password
println!("{:?}", request);
// → LoginRequest { username: "alice", password: "hunter2" }

// Serialization also exposes the password
let json = serde_json::to_string(&request).unwrap();
// → {"username":"alice","password":"hunter2"}
//
// (This example uses `serde_json` to make the risk concrete. The same problem
// exists with any serializer that includes `password`.)

The Solution

Mark sensitive fields with a classification. This crate provides:

  • Safe Debug: sensitive fields print as [REDACTED]
  • Explicit redaction: call .redact() to get a copy safe for serialization and logging
  • Policy control: choose how each classification is redacted (full, keep, mask)
use redaction::{Redactable, Secret, Token, Sensitive};

#[derive(Clone, Sensitive)]
struct LoginRequest {
    username: String,
    #[sensitive(Secret)]
    password: String,
    #[sensitive(Token)]
    api_key: String,
}

let request = LoginRequest {
    username: "alice".into(),
    password: "hunter2".into(),
    api_key: "tok_live_abcdef".into(),
};

// Get a redacted copy for serialization, APIs, and logging
let safe = request.redact();
assert_eq!(safe.password, "[REDACTED]");       // Secret: fully redacted
assert_eq!(safe.api_key, "***********cdef");  // Token: only last 4 visible
assert_eq!(safe.username, "alice");            // Not sensitive: unchanged

// Debug output is also safe, but it does NOT apply policies:
// it always prints `"[REDACTED]"` for `#[sensitive(...)]` fields.
println!("{:?}", request);
// → LoginRequest { username: "alice", password: "[REDACTED]", api_key: "[REDACTED]" }

Installation

[dependencies]
redaction = "0.1"

Basic Usage

  • Add #[derive(Clone, Sensitive)] to your type
  • Mark sensitive fields with #[sensitive(Classification)]
  • Call .redact() before you log, serialize, return, or persist the value
use redaction::{Redactable, Secret, Token, Sensitive};

#[derive(Clone, Sensitive)]
struct ApiCredentials {
    #[sensitive(Secret)]
    password: String,
    #[sensitive(Token)]
    api_key: String,
    user_id: String,  // not sensitive, passed through unchanged
}

let creds = ApiCredentials {
    password: "super_secret".into(),
    api_key: "tok_live_abcdef".into(),
    user_id: "user_42".into(),
};

let redacted = creds.redact();
assert_eq!(redacted.password, "[REDACTED]");  // Secret → fully redacted
assert_eq!(redacted.api_key, "***********cdef"); // Token → only last 4 visible
assert_eq!(redacted.user_id, "user_42");      // unchanged

Built-in Classifications

Each classification has a default redaction policy. Use the one that matches your data:

Classification Use for Example output
Secret Passwords, private keys [REDACTED]
Token API keys, bearer tokens …abcd (last 4)
Email Email addresses jo… (first 2)
CreditCard Card numbers (PANs) …1234 (last 4)
Pii Generic PII …_doe (last 4)
PhoneNumber Phone numbers …12 (last 2)
NationalId SSN, passport numbers …6789 (last 4)
AccountId Account identifiers …abcd (last 4)
SessionId Session tokens …wxyz (last 4)
IpAddress IP addresses …1.1 (last 4 chars)
DateOfBirth Birth dates [REDACTED]
BlockchainAddress Wallet addresses …abc123 (last 6)

Custom Classifications

When built-in classifications don't fit, create your own:

use redaction::{Classification, RedactionPolicy, TextRedactionPolicy};

#[derive(Clone, Copy)]
struct InternalId;
impl Classification for InternalId {}

impl RedactionPolicy for InternalId {
    fn policy() -> TextRedactionPolicy {
        TextRedactionPolicy::keep_last(2)  // Show only last 2 characters
    }
}

Clean architecture note:

  • Put the classification type (InternalId) in your domain crate/module.
  • Put the policy implementation (impl RedactionPolicy for InternalId) in your application or infrastructure layer (where you define what is safe to expose and where logging/serialization happens).

Then use it like any built-in:

#[derive(Clone, Sensitive)]
struct Record {
    #[sensitive(InternalId)]
    id: String,
}

Policies

Three policy types control how values are transformed:

  • Full: replace the entire value with a placeholder
TextRedactionPolicy::default_full()           // → "[REDACTED]"
TextRedactionPolicy::full_with("<hidden>")    // → "<hidden>"
  • Keep: keep specified characters visible, mask everything else
TextRedactionPolicy::keep_first(4)            // "secret123" → "secr*****"
TextRedactionPolicy::keep_last(4)             // "secret123" → "*****t123"
TextRedactionPolicy::keep_with(KeepConfig::both(2, 2))  // "secret" → "se**et"
  • Mask: mask specified characters, keep the rest visible
TextRedactionPolicy::mask_first(4)            // "secret123" → "****et123"
TextRedactionPolicy::mask_last(4)             // "secret123" → "secre****"

Logging with slog

With the slog feature, Sensitive types automatically redact when logged:

[dependencies]
redaction = { version = "0.1", features = ["slog"] }
#[derive(Clone, Sensitive)]
#[cfg_attr(feature = "slog", derive(serde::Serialize))]
struct LoginEvent {
    #[sensitive(Secret)]
    password: String,
    username: String,
}

// Redacts automatically (no explicit .redact() needed)
slog::info!(logger, "login"; "event" => event);

Requirements:

  • Type must implement Clone
  • Type must implement serde::Serialize (for the redacted output)

Reference

Supported Field Types

String-like: Use #[sensitive(Classification)]:

  • String
  • Cow<'_, str> (redaction returns an owned value)

Scalars: Use bare #[sensitive] (no classification):

  • Integers: i8-i128, u8-u128, isize, usize
  • Floats: f32, f64
  • bool → redacts to false
  • char → redacts to 'X'

Containers: Traversed automatically:

  • Option<T>: redacts inner value if present
  • Vec<T>: redacts all elements
  • Box<T>: redacts inner value
  • HashMap<K, V>, BTreeMap<K, V>: redacts values only (keys unchanged)
  • HashSet<T>, BTreeSet<T>: redacts elements
  • Result<T, E>: redacts both Ok and Err sides

Policy Behavior

  • Empty string (""):
    • Keep/Mask: returns ""
    • Full: returns the placeholder (default: "[REDACTED]")
  • Keep policies (keep_first, keep_last, KeepConfig::both) operate on Unicode scalar values:
    • If visible_prefix + visible_suffix >= length, the value is returned unchanged
  • Mask policies (mask_first, mask_last, MaskConfig::both) operate on Unicode scalar values:
    • If mask_prefix + mask_suffix >= length, the entire value is masked
  • Length: keep/mask policies preserve the input length (full does not)

Edge Cases

Scalar type aliases: Only bare primitive names (i32, bool) are recognized as scalars. Type aliases like type MyInt = i32 or qualified paths like std::primitive::i32 are treated as string-like and require a classification.

Foreign string types: For string-like types from other crates, wrap in a newtype:

use redaction::SensitiveValue;

struct WrappedId(external_crate::Id);

impl SensitiveValue for WrappedId {
    fn as_str(&self) -> &str { self.0.as_str() }
    fn from_redacted(s: String) -> Self { WrappedId(external_crate::Id::from(s)) }
}

Map keys: Never redacted. Move sensitive data into values.

Debug vs redact(): The derived Debug formats the type normally, but replaces the values of #[sensitive(...)] fields with the string \"[REDACTED]\". It does not apply the field's policy. Use .redact() when you need policy-based output.

Testing: Enable the testing feature to get unredacted Debug output in tests:

[dev-dependencies]
redaction = { version = "0.1", features = ["testing"] }

Security Considerations

See SECURITY.md for:

  • Information leakage (length preservation, timing)
  • Memory safety considerations
  • Compliance notes (GDPR, HIPAA, PCI DSS)

Documentation

License

Licensed under the MIT license (LICENSE.md or opensource.org/licenses/MIT).