Redaction
redaction helps you keep sensitive values (tokens, secrets, PII) out of places they don't belong by:
- Deriving
Sensitiveon your types with#[derive(Sensitive)] - Marking sensitive fields with
#[sensitive]or#[sensitive(Classification)] - Calling
.redact()to produce a copy that is safe to log or serialize - Generating
Debugoutput 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 ownInternalIdrepresent 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
sloglive 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
let request = LoginRequest ;
// Debug output exposes the password
println!;
// → LoginRequest { username: "alice", password: "hunter2" }
// Serialization also exposes the password
let json = to_string.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 explicitly. 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)
- External type support: types you don't control (like
chrono::DateTime) just work
use ;
let request = LoginRequest ;
// Get a redacted copy for serialization, APIs, and logging
let safe = request.redact;
assert_eq!; // Secret: fully redacted
assert_eq!; // Token: only last 4 visible
assert_eq!; // Not sensitive: unchanged
// Debug output is also safe, but it does NOT apply policies:
// it always prints `"[REDACTED]"` for `#[sensitive(...)]` fields.
println!;
// → LoginRequest { username: "alice", password: "[REDACTED]", api_key: "[REDACTED]" }
Installation
[]
= "0.1"
Basic Usage
- Add
#[derive(Clone, Sensitive)]to your type - Mark sensitive fields with
#[sensitive]or#[sensitive(Classification)] - Call
.redact()before you log, serialize, return, or persist the value
use ;
let creds = ApiCredentials ;
let redacted = creds.redact;
assert_eq!; // Secret → fully redacted
assert_eq!; // Token → only last 4 visible
assert_eq!; // unchanged
Field Attributes
The #[sensitive(...)] attribute controls how each field is handled:
| Attribute | Use For | Behavior |
|---|---|---|
| (none) | Non-sensitive fields, external types | Pass through unchanged |
#[sensitive] |
Scalars OR nested Sensitive types |
Walk containers, or redact scalars to default |
#[sensitive(Class)] |
String-like leaf values | Apply classification's redaction policy |
Classifications are for string-like leaf values; the field type must implement SensitiveValue
and Classifiable.
Examples
use ;
External Types Just Work
Fields without #[sensitive] pass through unchanged. This means external types like chrono::DateTime,
rust_decimal::Decimal, uuid::Uuid, or any type you don't control work automatically. Do not
add #[sensitive] unless the type implements SensitiveType.
use ;
Nested Sensitive Types
When a field's type also derives Sensitive, use #[sensitive] to walk into it:
Important: Without #[sensitive], nested structs pass through unchanged (even if they derive Sensitive). This is by design - you explicitly choose what to redact.
Nested Wrapper Classifications
Classifications work on nested wrapper types like Option<Vec<String>> automatically:
The classification is applied recursively through any nesting depth of:
Option<T>Vec<T>Box<T>HashMap<K, V>(values only)BTreeMap<K, V>(values only)HashSet<T>/BTreeSet<T>Result<T, E>
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 ;
;
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:
Policies
Three policy types control how values are transformed:
- Full: replace the entire value with a placeholder
default_full // → "[REDACTED]"
full_with // → "<hidden>"
- Keep: keep specified characters visible, mask everything else
keep_first // "secret123" → "secr*****"
keep_last // "secret123" → "*****t123"
keep_with // "secret" → "se**et"
- Mask: mask specified characters, keep the rest visible
mask_first // "secret123" → "****et123"
mask_last // "secret123" → "secre****"
Logging with slog
With the slog feature, Sensitive types automatically redact when logged as
structured JSON:
[]
= { = "0.1", = ["slog"] }
// Redacts automatically (no explicit .redact() needed)
info!;
Structured JSON requirements (why they exist):
- Type must implement
Cloneso the redacted JSON payload can be built without consuming the original value. - Type must implement
serde::Serializebecause structured logging emits JSON derived from the redacted copy. - The slog adapter uses
IntoRedactedJson, which is auto-implemented forRedactable + Serialize.
If those bounds are too strict, use SensitiveError instead to log a redacted
string without requiring Serialize.
Logging errors without Serialize
For types that cannot or should not derive Serialize, use SensitiveError. It
emits the same redacted Debug output, but logs as a string using a redacted
display template rather than JSON:
use ;
error!;
This path does not require Serialize on the type. SensitiveError generates a
RedactedDisplay implementation used by the slog adapter, so your error’s
normal Display (from thiserror or displaydoc) can remain unchanged while
logs still use a redacted string.
Template rules and bounds:
- Template required:
#[error("...")]or doc comments (derive fails otherwise) - Pass-through
{field}usesDisplay;{field:?}usesDebug #[sensitive(Classification)]in template:Clone + Display(orDebugfor:?)#[sensitive]scalars use defaults (no extra bounds)#[sensitive]non-scalars in template must deriveSensitiveError
Feature flags
classification(default): built-in classification typespolicy(default): redaction policies and.redact()slog: structured logging adaptertesting: unredactedDebugoutput in tests
Reference
Trait Bound Summary
- Traversal:
#[sensitive]on non-scalars requiresSensitiveType - Classification:
#[sensitive(Classification)]requiresClassifiable - Debug: fields shown in
Debugoutput requireDebug slogJSON (Sensitive): the type itself requiresClone + Serialize + IntoRedactedJsonslogstring (SensitiveError): see template rules above (bounds are template-dependent)
Trait Concepts
The library uses these core traits, organized by layer:
Domain Layer (what is sensitive):
| Trait | Purpose | Implemented By |
|---|---|---|
SensitiveType |
Types that contain sensitive data | Structs/enums deriving Sensitive |
SensitiveValue |
Types that are sensitive data | String, Cow<str>, custom newtypes |
Policy Layer (how to redact):
| Trait | Purpose | Implemented By |
|---|---|---|
RedactionPolicy |
Maps classification → redaction strategy | Your custom classifications |
TextRedactionPolicy |
Concrete string transformations | Built-in (Full, Keep, Mask) |
Application Layer (redaction machinery):
| Trait | Purpose | Implemented By |
|---|---|---|
Classifiable |
Types that can have classifications applied | String, wrappers (Option, Vec, etc.) |
Redactable |
User-facing .redact() method |
Auto-implemented for SensitiveType |
RedactionMapper |
Internal traversal machinery | #[doc(hidden)] |
- Use
#[sensitive]on fields ofSensitiveTypetypes (to walk into them) - Use
#[sensitive(Classification)]on fields ofClassifiabletypes (supports nested wrappers)
Supported field types
String-like (SensitiveValue): Use #[sensitive(Classification)]:
StringCow<'_, str>(redaction returns an owned value)
Scalars: Use bare #[sensitive] (no classification):
- Integers:
i8-i128,u8-u128,isize,usize - Floats:
f32,f64 bool→ redacts tofalsechar→ redacts to'X'
Containers (SensitiveType): Use #[sensitive] to walk, or omit for pass-through:
Option<T>: redacts inner value if presentVec<T>: redacts all elementsBox<T>: redacts inner valueHashMap<K, V>,BTreeMap<K, V>: redacts values only (keys unchanged)HashSet<T>,BTreeSet<T>: redacts elementsResult<T, E>: redacts bothOkandErrsides
External types: No annotation needed (pass through):
chrono::DateTime<Tz>,rust_decimal::Decimal,uuid::Uuid, etc.- Any type that doesn't implement
SensitiveType
PhantomData: Automatically handled (pass through, no trait bounds added).
Compiler Error Messages
The library provides helpful error messages for common mistakes:
Using a classification on a struct:
error[E0277]: `Address` is not a `SensitiveValue`
= note: classifications like `#[sensitive(Secret)]` are for leaf values (String, etc.)
= note: if `Address` is a struct that derives `Sensitive`, use `#[sensitive]` instead
Using #[sensitive] on an external type:
error[E0277]: `DateTime<Utc>` does not implement `SensitiveType`
= note: use `#[derive(Sensitive)]` on the type definition
= note: or remove the #[sensitive] attribute to pass through unchanged
Policy Behavior
- Empty string (
""):- Keep/Mask: returns
"" - Full: returns the placeholder (default:
"[REDACTED]")
- Keep/Mask: returns
- Keep policies (
keep_first,keep_last,KeepConfig::both) operate on Unicode scalar values:- If
visible_prefix + visible_suffix >= length, the value is returned unchanged
- If
- Mask policies (
mask_first,mask_last,MaskConfig::both) operate on Unicode scalar values:- If
mask_prefix + mask_suffix >= length, the entire value is masked
- If
- 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 non-scalars and require #[sensitive(Classification)] or pass-through.
Boxed trait objects: The derive detects only the simple syntax Box<dyn Trait> and calls redact_boxed. It does not match std::boxed::Box<dyn Trait> or type aliases. The trait object must implement RedactableBoxed.
Foreign string types: For string-like types from other crates, wrap in a newtype:
use SensitiveValue;
;
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:
[]
= { = "0.1", = ["testing"] }
Security Considerations
- Length preservation: Keep/Mask policies preserve input length, which can leak information about value size. Use Full redaction for maximum privacy.
- Timing: Redaction is not constant-time. Do not use in cryptographic contexts.
- Memory: Original values may persist in memory until overwritten. Consider secure memory handling for highly sensitive data.
Documentation
Development
To set up git hooks for pre-commit checks (fmt, clippy, tests):
License
Licensed under the MIT license (LICENSE.md or opensource.org/licenses/MIT).