OWASP MASVS-PRIVACY: data minimization, PII discovery, pseudonymization, consent checks, and retention status.
secure_privacy provides small policy primitives for privacy-sensitive applications. It does not store consent records, delete data, or render UI; those responsibilities stay in your application, while this crate gives you typed decisions and consistent security events.
[dependencies]
secure_privacy = "0.1.2"use secure_privacy::{PiiClassification, PiiClassifier};
let classifier = PiiClassifier::new();
let classification = classifier.classify("Contact sherif@example.com for access.");
assert_eq!(classification, PiiClassification::Email);The built-in classifier recognizes common privacy-sensitive values and can be extended with your own regex patterns:
use secure_privacy::{PiiClassification, PiiClassifier};
let mut classifier = PiiClassifier::new();
classifier.add_custom_pattern("customer_id", r"CUST-[0-9]{6}")?;
assert_eq!(classifier.classify("+44 20 7946 0958"), PiiClassification::PhoneNumber);
assert_eq!(classifier.classify("CUST-123456"), PiiClassification::Custom("customer_id".into()));
assert_eq!(classifier.classify("ordinary text"), PiiClassification::None);
# Ok::<(), secure_privacy::PrivacyError>(())Classification is intentionally conservative and deterministic. Use it to route records into review, minimization, or redaction workflows; do not treat a None result as proof that no personal data exists.
Pseudonymizer creates deterministic, non-reversible HMAC-SHA256 pseudonyms. Use a deployment-specific salt from your secret manager, not a literal in production code:
use secure_privacy::Pseudonymizer;
let pseudonymizer = Pseudonymizer::new(b"release-specific-salt")?;
let a = pseudonymizer.pseudonymize("user-123");
let b = pseudonymizer.pseudonymize("user-123");
assert_eq!(a, b);
assert_ne!(a.value, "user-123");
# Ok::<(), secure_privacy::PrivacyError>(())Batch processing is available when you need stable joins across an export:
use secure_privacy::Pseudonymizer;
let pseudonymizer = Pseudonymizer::new(b"analytics-export-salt")?;
let values = pseudonymizer.pseudonymize_batch(&["user-1", "user-2"]);
assert_eq!(values.len(), 2);
# Ok::<(), secure_privacy::PrivacyError>(())Changing the salt intentionally breaks linkability between old and new datasets.
Create one ConsentPolicy per purpose and deny by default until consent is granted:
use secure_privacy::{ConsentDecision, ConsentPolicy, ConsentPurpose};
use security_events::sink::InMemorySink;
let purpose = ConsentPurpose::new("analytics");
let mut policy = ConsentPolicy::new(purpose.clone());
let sink = InMemorySink::new();
assert_eq!(
policy.check_consent(&purpose, &sink),
ConsentDecision::NotCollected
);
policy.grant();
assert_eq!(policy.check_consent(&purpose, &sink), ConsentDecision::Allowed);
policy.withdraw();
assert_eq!(policy.check_consent(&purpose, &sink), ConsentDecision::Withdrawn);Purpose mismatch is blocked and emits a ConsentViolation event with both the consented and requested purpose labels.
Retention policies report whether data is still active or should be removed. They do not delete records themselves:
use secure_privacy::{RetentionPolicy, RetentionStatus};
use security_events::sink::InMemorySink;
use time::{Duration, OffsetDateTime};
let policy = RetentionPolicy::new(30, "support-transcripts");
let now = OffsetDateTime::now_utc();
let created_at = now - Duration::days(45);
let sink = InMemorySink::new();
assert_eq!(
policy.check_status(created_at, now, &sink),
RetentionStatus::Expired
);
assert_eq!(sink.events().len(), 1);When no policy exists, make that explicit:
use secure_privacy::{check_no_policy, RetentionStatus};
assert_eq!(check_no_policy(), RetentionStatus::NoPolicy);| Workflow | Recommended Use |
|---|---|
| User input or import scanning | PiiClassifier::classify() |
| Analytics joins without direct identifiers | Pseudonymizer::pseudonymize() |
| Purpose-scoped processing checks | ConsentPolicy::check_consent() |
| Scheduled retention sweeps | RetentionPolicy::check_status() |
Emit privacy decisions to security_events and keep storage/UI responsibilities in your application. That split gives auditors a clean policy trail without forcing a database model on downstream projects.