1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Deserializer, Serialize, Serializer};
3use std::fmt;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
13pub enum ClassificationLevel {
14 Public,
16 Internal,
18 Confidential,
20 Restricted,
22}
23
24impl fmt::Display for ClassificationLevel {
25 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
26 match self {
27 ClassificationLevel::Public => write!(f, "Public"),
28 ClassificationLevel::Internal => write!(f, "Internal"),
29 ClassificationLevel::Confidential => write!(f, "Confidential"),
30 ClassificationLevel::Restricted => write!(f, "Restricted"),
31 }
32 }
33}
34
35pub trait Redact {
41 fn redact(&self) -> String;
42}
43
44#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
51pub struct Sensitive<T> {
52 value: T,
53 pub classification: ClassificationLevel,
55}
56
57impl<T> Sensitive<T> {
58 pub fn new(value: T) -> Self {
59 Self {
60 value,
61 classification: ClassificationLevel::Restricted,
62 }
63 }
64
65 pub fn with_classification(value: T, classification: ClassificationLevel) -> Self {
67 Self {
68 value,
69 classification,
70 }
71 }
72
73 pub fn expose(&self) -> &T {
75 &self.value
76 }
77
78 pub fn into_inner(self) -> T {
79 self.value
80 }
81}
82
83impl<T> fmt::Debug for Sensitive<T> {
85 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86 write!(f, "[REDACTED:{}]", self.classification)
87 }
88}
89
90impl<T> fmt::Display for Sensitive<T> {
92 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
93 write!(f, "[REDACTED]")
94 }
95}
96
97impl<T: Serialize> Serialize for Sensitive<T> {
107 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
108 where
109 S: Serializer,
110 {
111 if cfg!(debug_assertions) {
112 self.value.serialize(serializer)
113 } else {
114 serializer.serialize_str("[REDACTED]")
115 }
116 }
117}
118
119impl<'de, T: Deserialize<'de>> Deserialize<'de> for Sensitive<T> {
120 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
121 where
122 D: Deserializer<'de>,
123 {
124 T::deserialize(deserializer).map(Sensitive::new)
125 }
126}
127
128pub trait EncryptionHook: Send + Sync {
134 fn encrypt(&self, data: &[u8]) -> Vec<u8>;
136
137 fn decrypt(&self, data: &[u8]) -> Vec<u8>;
139}
140
141pub struct NoOpEncryption;
143
144impl EncryptionHook for NoOpEncryption {
145 fn encrypt(&self, data: &[u8]) -> Vec<u8> {
146 data.to_vec()
147 }
148
149 fn decrypt(&self, data: &[u8]) -> Vec<u8> {
150 data.to_vec()
151 }
152}
153
154#[cfg(feature = "xor-demo")]
165#[deprecated(
166 since = "0.32.0",
167 note = "XOR encryption is cryptographically broken. Use AES-GCM or ChaCha20Poly1305 for production."
168)]
169pub struct XorEncryption {
170 key: u8,
171}
172
173#[cfg(feature = "xor-demo")]
174#[allow(deprecated)]
175impl XorEncryption {
176 pub fn new(key: u8) -> Self {
177 Self { key }
178 }
179}
180
181#[cfg(feature = "xor-demo")]
182#[allow(deprecated)]
183impl EncryptionHook for XorEncryption {
184 fn encrypt(&self, data: &[u8]) -> Vec<u8> {
185 data.iter().map(|b| b ^ self.key).collect()
186 }
187
188 fn decrypt(&self, data: &[u8]) -> Vec<u8> {
189 self.encrypt(data)
191 }
192}
193
194#[derive(Debug, Clone, Serialize, Deserialize)]
200pub struct PiiField {
201 pub field_name: String,
203 pub classification: ClassificationLevel,
205 pub category: String,
207}
208
209pub struct FieldNamePiiDetector {
211 patterns: Vec<(Vec<&'static str>, &'static str, ClassificationLevel)>,
212}
213
214impl Default for FieldNamePiiDetector {
215 fn default() -> Self {
216 Self::new()
217 }
218}
219
220impl FieldNamePiiDetector {
221 pub fn new() -> Self {
222 Self {
223 patterns: vec![
224 (
225 vec!["email", "e_mail", "email_address"],
226 "email",
227 ClassificationLevel::Confidential,
228 ),
229 (
230 vec!["phone", "phone_number", "mobile", "tel", "telephone"],
231 "phone",
232 ClassificationLevel::Confidential,
233 ),
234 (
235 vec!["ssn", "social_security", "social_security_number"],
236 "ssn",
237 ClassificationLevel::Restricted,
238 ),
239 (
240 vec![
241 "address", "street", "street_address", "home_address",
242 "postal_code", "zip_code", "zip",
243 ],
244 "address",
245 ClassificationLevel::Confidential,
246 ),
247 (
248 vec![
249 "first_name", "last_name", "full_name", "given_name",
250 "family_name", "surname",
251 ],
252 "name",
253 ClassificationLevel::Confidential,
254 ),
255 (
256 vec!["ip", "ip_address", "ipv4", "ipv6", "client_ip", "remote_addr"],
257 "ip_address",
258 ClassificationLevel::Internal,
259 ),
260 (
261 vec![
262 "credit_card", "card_number", "cc_number", "pan",
263 "payment_card",
264 ],
265 "credit_card",
266 ClassificationLevel::Restricted,
267 ),
268 (
269 vec!["password", "passwd", "secret", "api_key", "access_token"],
270 "credential",
271 ClassificationLevel::Restricted,
272 ),
273 (
274 vec!["date_of_birth", "dob", "birth_date", "birthday"],
275 "date_of_birth",
276 ClassificationLevel::Confidential,
277 ),
278 (
280 vec!["jumin", "jumin_number", "resident_number", "resident_registration"],
281 "kr_resident_number",
282 ClassificationLevel::Restricted,
283 ),
284 (
285 vec!["business_number", "saeopja", "business_registration"],
286 "kr_business_number",
287 ClassificationLevel::Confidential,
288 ),
289 (
290 vec!["passport", "passport_number", "yeokkwon"],
291 "passport",
292 ClassificationLevel::Restricted,
293 ),
294 (
295 vec!["drivers_license", "driver_license", "license_number", "myeonheo"],
296 "drivers_license",
297 ClassificationLevel::Restricted,
298 ),
299 ],
300 }
301 }
302
303 pub fn classify(&self, field_name: &str) -> Option<ClassificationLevel> {
305 let lower = field_name.to_lowercase();
306 for (patterns, _, level) in &self.patterns {
307 if patterns.iter().any(|p| lower == *p || lower.contains(p)) {
308 return Some(*level);
309 }
310 }
311 None
312 }
313
314 pub fn scan_value(&self, value: &serde_json::Value) -> Vec<PiiField> {
316 let mut results = Vec::new();
317 self.scan_recursive(value, "", &mut results);
318 results
319 }
320
321 fn scan_recursive(
322 &self,
323 value: &serde_json::Value,
324 path: &str,
325 results: &mut Vec<PiiField>,
326 ) {
327 match value {
328 serde_json::Value::Object(map) => {
329 for (key, val) in map {
330 let field_path = if path.is_empty() {
331 key.clone()
332 } else {
333 format!("{path}.{key}")
334 };
335
336 let lower = key.to_lowercase();
337 for (patterns, category, level) in &self.patterns {
338 if patterns.iter().any(|p| lower == *p || lower.contains(p)) {
339 results.push(PiiField {
340 field_name: field_path.clone(),
341 classification: *level,
342 category: category.to_string(),
343 });
344 break;
345 }
346 }
347
348 self.scan_recursive(val, &field_path, results);
349 }
350 }
351 serde_json::Value::Array(arr) => {
352 for (i, val) in arr.iter().enumerate() {
353 let item_path = format!("{path}[{i}]");
354 self.scan_recursive(val, &item_path, results);
355 }
356 }
357 _ => {}
358 }
359 }
360}
361
362pub trait PiiDetector {
364 fn contains_pii(&self, text: &str) -> bool;
366}
367
368impl PiiDetector for FieldNamePiiDetector {
369 fn contains_pii(&self, text: &str) -> bool {
370 self.classify(text).is_some()
371 }
372}
373
374#[derive(Debug, Clone, Serialize, Deserialize)]
380pub struct ErasureRequest {
381 pub id: String,
383 pub subject: String,
385 pub scope: Vec<String>,
387 pub timestamp: DateTime<Utc>,
389 pub reason: Option<String>,
391}
392
393impl ErasureRequest {
394 pub fn new(id: String, subject: String, scope: Vec<String>) -> Self {
395 Self {
396 id,
397 subject,
398 scope,
399 timestamp: Utc::now(),
400 reason: None,
401 }
402 }
403
404 pub fn with_reason(mut self, reason: impl Into<String>) -> Self {
405 self.reason = Some(reason.into());
406 self
407 }
408}
409
410#[derive(Debug, Clone, Serialize, Deserialize)]
412pub struct ErasureResult {
413 pub request_id: String,
415 pub success: bool,
417 pub records_erased: usize,
419 pub failed_scopes: Vec<String>,
421 pub completed_at: DateTime<Utc>,
423}
424
425pub trait ErasureSink: Send + Sync {
427 fn erase(&self, request: &ErasureRequest) -> ErasureResult;
429}
430
431pub struct InMemoryErasureSink {
433 records: std::sync::Mutex<std::collections::HashMap<String, Vec<String>>>,
434}
435
436impl Default for InMemoryErasureSink {
437 fn default() -> Self {
438 Self::new()
439 }
440}
441
442impl InMemoryErasureSink {
443 pub fn new() -> Self {
444 Self {
445 records: std::sync::Mutex::new(std::collections::HashMap::new()),
446 }
447 }
448
449 pub fn add_records(&self, subject: &str, data: Vec<String>) {
451 let mut records = self.records.lock().unwrap();
452 records.insert(subject.to_string(), data);
453 }
454}
455
456impl ErasureSink for InMemoryErasureSink {
457 fn erase(&self, request: &ErasureRequest) -> ErasureResult {
458 let mut records = self.records.lock().unwrap();
459 let count = if let Some(data) = records.remove(&request.subject) {
460 data.len()
461 } else {
462 0
463 };
464
465 ErasureResult {
466 request_id: request.id.clone(),
467 success: true,
468 records_erased: count,
469 failed_scopes: Vec::new(),
470 completed_at: Utc::now(),
471 }
472 }
473}
474
475#[cfg(test)]
476mod tests {
477 use super::*;
478
479 #[test]
480 fn test_sensitive_redaction() {
481 let email = Sensitive::new("user@example.com".to_string());
482
483 assert_eq!(format!("{:?}", email), "[REDACTED:Restricted]");
484 assert_eq!(format!("{}", email), "[REDACTED]");
485 assert_eq!(email.expose(), "user@example.com");
486 }
487
488 #[test]
489 fn test_sensitive_with_classification() {
490 let data = Sensitive::with_classification("internal doc", ClassificationLevel::Internal);
491 assert_eq!(data.classification, ClassificationLevel::Internal);
492 assert_eq!(format!("{:?}", data), "[REDACTED:Internal]");
493 }
494
495 #[test]
496 fn test_sensitive_serialization() {
497 let password = Sensitive::new("my_secret_pass".to_string());
498
499 let json = serde_json::to_string(&password).unwrap();
500 assert_eq!(json, "\"my_secret_pass\"");
501
502 let deserialized: Sensitive<String> = serde_json::from_str(&json).unwrap();
503 assert_eq!(deserialized.expose(), "my_secret_pass");
504 }
505
506 #[test]
507 fn classification_level_ordering() {
508 assert!(ClassificationLevel::Public < ClassificationLevel::Internal);
509 assert!(ClassificationLevel::Internal < ClassificationLevel::Confidential);
510 assert!(ClassificationLevel::Confidential < ClassificationLevel::Restricted);
511 }
512
513 #[test]
514 fn noop_encryption_passthrough() {
515 let hook = NoOpEncryption;
516 let data = b"hello world";
517 let encrypted = hook.encrypt(data);
518 let decrypted = hook.decrypt(&encrypted);
519 assert_eq!(decrypted, data);
520 }
521
522 #[cfg(feature = "xor-demo")]
523 #[test]
524 #[allow(deprecated)]
525 fn xor_encryption_roundtrip() {
526 let hook = XorEncryption::new(0x42);
527 let data = b"sensitive data";
528 let encrypted = hook.encrypt(data);
529 assert_ne!(encrypted, data, "Encrypted should differ from plaintext");
530 let decrypted = hook.decrypt(&encrypted);
531 assert_eq!(decrypted, data, "Decrypted should match original");
532 }
533
534 #[test]
535 fn pii_detector_classifies_email() {
536 let detector = FieldNamePiiDetector::new();
537 assert_eq!(
538 detector.classify("email"),
539 Some(ClassificationLevel::Confidential)
540 );
541 assert_eq!(
542 detector.classify("email_address"),
543 Some(ClassificationLevel::Confidential)
544 );
545 }
546
547 #[test]
548 fn pii_detector_classifies_ssn_as_restricted() {
549 let detector = FieldNamePiiDetector::new();
550 assert_eq!(
551 detector.classify("ssn"),
552 Some(ClassificationLevel::Restricted)
553 );
554 assert_eq!(
555 detector.classify("social_security_number"),
556 Some(ClassificationLevel::Restricted)
557 );
558 }
559
560 #[test]
561 fn pii_detector_no_match_for_regular_fields() {
562 let detector = FieldNamePiiDetector::new();
563 assert_eq!(detector.classify("created_at"), None);
564 assert_eq!(detector.classify("status"), None);
565 assert_eq!(detector.classify("quantity"), None);
566 }
567
568 #[test]
569 fn pii_detector_scan_json_value() {
570 let detector = FieldNamePiiDetector::new();
571 let json = serde_json::json!({
572 "id": 1,
573 "email": "test@example.com",
574 "profile": {
575 "first_name": "Alice",
576 "phone": "555-1234"
577 },
578 "status": "active"
579 });
580
581 let results = detector.scan_value(&json);
582 assert_eq!(results.len(), 3);
583
584 let field_names: Vec<&str> = results.iter().map(|f| f.field_name.as_str()).collect();
585 assert!(field_names.contains(&"email"));
586 assert!(field_names.contains(&"profile.first_name"));
587 assert!(field_names.contains(&"profile.phone"));
588 }
589
590 #[test]
591 fn pii_detector_trait_impl() {
592 let detector = FieldNamePiiDetector::new();
593 assert!(detector.contains_pii("email"));
594 assert!(!detector.contains_pii("status"));
595 }
596
597 #[test]
598 fn erasure_request_processing() {
599 let sink = InMemoryErasureSink::new();
600 sink.add_records(
601 "user_42",
602 vec![
603 "record1".into(),
604 "record2".into(),
605 "record3".into(),
606 ],
607 );
608
609 let request = ErasureRequest::new(
610 "req_001".into(),
611 "user_42".into(),
612 vec!["all".into()],
613 )
614 .with_reason("GDPR Article 17 request");
615
616 let result = sink.erase(&request);
617 assert!(result.success);
618 assert_eq!(result.records_erased, 3);
619 assert!(result.failed_scopes.is_empty());
620 }
621
622 #[test]
623 fn erasure_request_for_missing_subject() {
624 let sink = InMemoryErasureSink::new();
625 let request = ErasureRequest::new(
626 "req_002".into(),
627 "unknown_user".into(),
628 vec!["all".into()],
629 );
630
631 let result = sink.erase(&request);
632 assert!(result.success);
633 assert_eq!(result.records_erased, 0);
634 }
635
636 #[test]
639 fn sensitive_display_masking_all_levels() {
640 let public = Sensitive::with_classification("data", ClassificationLevel::Public);
641 let internal = Sensitive::with_classification("data", ClassificationLevel::Internal);
642 let confidential =
643 Sensitive::with_classification("data", ClassificationLevel::Confidential);
644 let restricted = Sensitive::with_classification("data", ClassificationLevel::Restricted);
645
646 assert_eq!(format!("{}", public), "[REDACTED]");
647 assert_eq!(format!("{}", internal), "[REDACTED]");
648 assert_eq!(format!("{}", confidential), "[REDACTED]");
649 assert_eq!(format!("{}", restricted), "[REDACTED]");
650 }
651
652 #[test]
653 fn sensitive_debug_includes_level() {
654 let public = Sensitive::with_classification("data", ClassificationLevel::Public);
655 let restricted = Sensitive::with_classification("data", ClassificationLevel::Restricted);
656
657 assert_eq!(format!("{:?}", public), "[REDACTED:Public]");
658 assert_eq!(format!("{:?}", restricted), "[REDACTED:Restricted]");
659 }
660
661 #[test]
662 fn pii_detector_korean_resident_number() {
663 let detector = FieldNamePiiDetector::new();
664 assert_eq!(
665 detector.classify("jumin_number"),
666 Some(ClassificationLevel::Restricted)
667 );
668 assert_eq!(
669 detector.classify("resident_registration"),
670 Some(ClassificationLevel::Restricted)
671 );
672 }
673
674 #[test]
675 fn pii_detector_korean_business_number() {
676 let detector = FieldNamePiiDetector::new();
677 assert_eq!(
678 detector.classify("business_number"),
679 Some(ClassificationLevel::Confidential)
680 );
681 assert_eq!(
682 detector.classify("business_registration"),
683 Some(ClassificationLevel::Confidential)
684 );
685 }
686
687 #[test]
688 fn pii_detector_passport() {
689 let detector = FieldNamePiiDetector::new();
690 assert_eq!(
691 detector.classify("passport_number"),
692 Some(ClassificationLevel::Restricted)
693 );
694 }
695
696 #[test]
697 fn pii_detector_drivers_license() {
698 let detector = FieldNamePiiDetector::new();
699 assert_eq!(
700 detector.classify("drivers_license"),
701 Some(ClassificationLevel::Restricted)
702 );
703 assert_eq!(
704 detector.classify("license_number"),
705 Some(ClassificationLevel::Restricted)
706 );
707 }
708
709 #[test]
710 fn classification_level_serde_roundtrip() {
711 let level = ClassificationLevel::Restricted;
712 let json = serde_json::to_string(&level).unwrap();
713 let deser: ClassificationLevel = serde_json::from_str(&json).unwrap();
714 assert_eq!(deser, level);
715 }
716
717 #[test]
718 fn erasure_request_with_reason() {
719 let req = ErasureRequest::new("r1".into(), "user1".into(), vec!["all".into()])
720 .with_reason("GDPR Article 17");
721 assert_eq!(req.reason.as_deref(), Some("GDPR Article 17"));
722 }
723
724 #[test]
725 fn in_memory_erasure_sink_verify_post_erasure() {
726 let sink = InMemoryErasureSink::new();
727 sink.add_records("user_1", vec!["rec1".into(), "rec2".into()]);
728
729 let req = ErasureRequest::new("r1".into(), "user_1".into(), vec!["all".into()]);
730 let result = sink.erase(&req);
731 assert_eq!(result.records_erased, 2);
732
733 let result2 = sink.erase(&req);
735 assert_eq!(result2.records_erased, 0);
736 }
737
738 #[test]
739 fn pii_detector_total_categories() {
740 let detector = FieldNamePiiDetector::new();
741 assert_eq!(detector.patterns.len(), 13);
743 }
744
745 #[test]
746 fn sensitive_into_inner_returns_value() {
747 let s = Sensitive::new(42);
748 assert_eq!(s.into_inner(), 42);
749 }
750
751 #[cfg(feature = "xor-demo")]
752 #[test]
753 #[allow(deprecated)]
754 fn xor_encryption_different_keys_differ() {
755 let hook1 = XorEncryption::new(0x42);
756 let hook2 = XorEncryption::new(0xFF);
757 let data = b"test";
758 assert_ne!(hook1.encrypt(data), hook2.encrypt(data));
759 }
760}