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> {
99 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
100 where
101 S: Serializer,
102 {
103 self.value.serialize(serializer)
104 }
105}
106
107impl<'de, T: Deserialize<'de>> Deserialize<'de> for Sensitive<T> {
108 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
109 where
110 D: Deserializer<'de>,
111 {
112 T::deserialize(deserializer).map(Sensitive::new)
113 }
114}
115
116pub trait EncryptionHook: Send + Sync {
122 fn encrypt(&self, data: &[u8]) -> Vec<u8>;
124
125 fn decrypt(&self, data: &[u8]) -> Vec<u8>;
127}
128
129pub struct NoOpEncryption;
131
132impl EncryptionHook for NoOpEncryption {
133 fn encrypt(&self, data: &[u8]) -> Vec<u8> {
134 data.to_vec()
135 }
136
137 fn decrypt(&self, data: &[u8]) -> Vec<u8> {
138 data.to_vec()
139 }
140}
141
142pub struct XorEncryption {
145 key: u8,
146}
147
148impl XorEncryption {
149 pub fn new(key: u8) -> Self {
150 Self { key }
151 }
152}
153
154impl EncryptionHook for XorEncryption {
155 fn encrypt(&self, data: &[u8]) -> Vec<u8> {
156 data.iter().map(|b| b ^ self.key).collect()
157 }
158
159 fn decrypt(&self, data: &[u8]) -> Vec<u8> {
160 self.encrypt(data)
162 }
163}
164
165#[derive(Debug, Clone, Serialize, Deserialize)]
171pub struct PiiField {
172 pub field_name: String,
174 pub classification: ClassificationLevel,
176 pub category: String,
178}
179
180pub struct FieldNamePiiDetector {
182 patterns: Vec<(Vec<&'static str>, &'static str, ClassificationLevel)>,
183}
184
185impl Default for FieldNamePiiDetector {
186 fn default() -> Self {
187 Self::new()
188 }
189}
190
191impl FieldNamePiiDetector {
192 pub fn new() -> Self {
193 Self {
194 patterns: vec![
195 (
196 vec!["email", "e_mail", "email_address"],
197 "email",
198 ClassificationLevel::Confidential,
199 ),
200 (
201 vec!["phone", "phone_number", "mobile", "tel", "telephone"],
202 "phone",
203 ClassificationLevel::Confidential,
204 ),
205 (
206 vec!["ssn", "social_security", "social_security_number"],
207 "ssn",
208 ClassificationLevel::Restricted,
209 ),
210 (
211 vec![
212 "address", "street", "street_address", "home_address",
213 "postal_code", "zip_code", "zip",
214 ],
215 "address",
216 ClassificationLevel::Confidential,
217 ),
218 (
219 vec![
220 "first_name", "last_name", "full_name", "given_name",
221 "family_name", "surname",
222 ],
223 "name",
224 ClassificationLevel::Confidential,
225 ),
226 (
227 vec!["ip", "ip_address", "ipv4", "ipv6", "client_ip", "remote_addr"],
228 "ip_address",
229 ClassificationLevel::Internal,
230 ),
231 (
232 vec![
233 "credit_card", "card_number", "cc_number", "pan",
234 "payment_card",
235 ],
236 "credit_card",
237 ClassificationLevel::Restricted,
238 ),
239 (
240 vec!["password", "passwd", "secret", "api_key", "access_token"],
241 "credential",
242 ClassificationLevel::Restricted,
243 ),
244 (
245 vec!["date_of_birth", "dob", "birth_date", "birthday"],
246 "date_of_birth",
247 ClassificationLevel::Confidential,
248 ),
249 (
251 vec!["jumin", "jumin_number", "resident_number", "resident_registration"],
252 "kr_resident_number",
253 ClassificationLevel::Restricted,
254 ),
255 (
256 vec!["business_number", "saeopja", "business_registration"],
257 "kr_business_number",
258 ClassificationLevel::Confidential,
259 ),
260 (
261 vec!["passport", "passport_number", "yeokkwon"],
262 "passport",
263 ClassificationLevel::Restricted,
264 ),
265 (
266 vec!["drivers_license", "driver_license", "license_number", "myeonheo"],
267 "drivers_license",
268 ClassificationLevel::Restricted,
269 ),
270 ],
271 }
272 }
273
274 pub fn classify(&self, field_name: &str) -> Option<ClassificationLevel> {
276 let lower = field_name.to_lowercase();
277 for (patterns, _, level) in &self.patterns {
278 if patterns.iter().any(|p| lower == *p || lower.contains(p)) {
279 return Some(*level);
280 }
281 }
282 None
283 }
284
285 pub fn scan_value(&self, value: &serde_json::Value) -> Vec<PiiField> {
287 let mut results = Vec::new();
288 self.scan_recursive(value, "", &mut results);
289 results
290 }
291
292 fn scan_recursive(
293 &self,
294 value: &serde_json::Value,
295 path: &str,
296 results: &mut Vec<PiiField>,
297 ) {
298 match value {
299 serde_json::Value::Object(map) => {
300 for (key, val) in map {
301 let field_path = if path.is_empty() {
302 key.clone()
303 } else {
304 format!("{path}.{key}")
305 };
306
307 let lower = key.to_lowercase();
308 for (patterns, category, level) in &self.patterns {
309 if patterns.iter().any(|p| lower == *p || lower.contains(p)) {
310 results.push(PiiField {
311 field_name: field_path.clone(),
312 classification: *level,
313 category: category.to_string(),
314 });
315 break;
316 }
317 }
318
319 self.scan_recursive(val, &field_path, results);
320 }
321 }
322 serde_json::Value::Array(arr) => {
323 for (i, val) in arr.iter().enumerate() {
324 let item_path = format!("{path}[{i}]");
325 self.scan_recursive(val, &item_path, results);
326 }
327 }
328 _ => {}
329 }
330 }
331}
332
333pub trait PiiDetector {
335 fn contains_pii(&self, text: &str) -> bool;
337}
338
339impl PiiDetector for FieldNamePiiDetector {
340 fn contains_pii(&self, text: &str) -> bool {
341 self.classify(text).is_some()
342 }
343}
344
345#[derive(Debug, Clone, Serialize, Deserialize)]
351pub struct ErasureRequest {
352 pub id: String,
354 pub subject: String,
356 pub scope: Vec<String>,
358 pub timestamp: DateTime<Utc>,
360 pub reason: Option<String>,
362}
363
364impl ErasureRequest {
365 pub fn new(id: String, subject: String, scope: Vec<String>) -> Self {
366 Self {
367 id,
368 subject,
369 scope,
370 timestamp: Utc::now(),
371 reason: None,
372 }
373 }
374
375 pub fn with_reason(mut self, reason: impl Into<String>) -> Self {
376 self.reason = Some(reason.into());
377 self
378 }
379}
380
381#[derive(Debug, Clone, Serialize, Deserialize)]
383pub struct ErasureResult {
384 pub request_id: String,
386 pub success: bool,
388 pub records_erased: usize,
390 pub failed_scopes: Vec<String>,
392 pub completed_at: DateTime<Utc>,
394}
395
396pub trait ErasureSink: Send + Sync {
398 fn erase(&self, request: &ErasureRequest) -> ErasureResult;
400}
401
402pub struct InMemoryErasureSink {
404 records: std::sync::Mutex<std::collections::HashMap<String, Vec<String>>>,
405}
406
407impl Default for InMemoryErasureSink {
408 fn default() -> Self {
409 Self::new()
410 }
411}
412
413impl InMemoryErasureSink {
414 pub fn new() -> Self {
415 Self {
416 records: std::sync::Mutex::new(std::collections::HashMap::new()),
417 }
418 }
419
420 pub fn add_records(&self, subject: &str, data: Vec<String>) {
422 let mut records = self.records.lock().unwrap();
423 records.insert(subject.to_string(), data);
424 }
425}
426
427impl ErasureSink for InMemoryErasureSink {
428 fn erase(&self, request: &ErasureRequest) -> ErasureResult {
429 let mut records = self.records.lock().unwrap();
430 let count = if let Some(data) = records.remove(&request.subject) {
431 data.len()
432 } else {
433 0
434 };
435
436 ErasureResult {
437 request_id: request.id.clone(),
438 success: true,
439 records_erased: count,
440 failed_scopes: Vec::new(),
441 completed_at: Utc::now(),
442 }
443 }
444}
445
446#[cfg(test)]
447mod tests {
448 use super::*;
449
450 #[test]
451 fn test_sensitive_redaction() {
452 let email = Sensitive::new("user@example.com".to_string());
453
454 assert_eq!(format!("{:?}", email), "[REDACTED:Restricted]");
455 assert_eq!(format!("{}", email), "[REDACTED]");
456 assert_eq!(email.expose(), "user@example.com");
457 }
458
459 #[test]
460 fn test_sensitive_with_classification() {
461 let data = Sensitive::with_classification("internal doc", ClassificationLevel::Internal);
462 assert_eq!(data.classification, ClassificationLevel::Internal);
463 assert_eq!(format!("{:?}", data), "[REDACTED:Internal]");
464 }
465
466 #[test]
467 fn test_sensitive_serialization() {
468 let password = Sensitive::new("my_secret_pass".to_string());
469
470 let json = serde_json::to_string(&password).unwrap();
471 assert_eq!(json, "\"my_secret_pass\"");
472
473 let deserialized: Sensitive<String> = serde_json::from_str(&json).unwrap();
474 assert_eq!(deserialized.expose(), "my_secret_pass");
475 }
476
477 #[test]
478 fn classification_level_ordering() {
479 assert!(ClassificationLevel::Public < ClassificationLevel::Internal);
480 assert!(ClassificationLevel::Internal < ClassificationLevel::Confidential);
481 assert!(ClassificationLevel::Confidential < ClassificationLevel::Restricted);
482 }
483
484 #[test]
485 fn noop_encryption_passthrough() {
486 let hook = NoOpEncryption;
487 let data = b"hello world";
488 let encrypted = hook.encrypt(data);
489 let decrypted = hook.decrypt(&encrypted);
490 assert_eq!(decrypted, data);
491 }
492
493 #[test]
494 fn xor_encryption_roundtrip() {
495 let hook = XorEncryption::new(0x42);
496 let data = b"sensitive data";
497 let encrypted = hook.encrypt(data);
498 assert_ne!(encrypted, data, "Encrypted should differ from plaintext");
499 let decrypted = hook.decrypt(&encrypted);
500 assert_eq!(decrypted, data, "Decrypted should match original");
501 }
502
503 #[test]
504 fn pii_detector_classifies_email() {
505 let detector = FieldNamePiiDetector::new();
506 assert_eq!(
507 detector.classify("email"),
508 Some(ClassificationLevel::Confidential)
509 );
510 assert_eq!(
511 detector.classify("email_address"),
512 Some(ClassificationLevel::Confidential)
513 );
514 }
515
516 #[test]
517 fn pii_detector_classifies_ssn_as_restricted() {
518 let detector = FieldNamePiiDetector::new();
519 assert_eq!(
520 detector.classify("ssn"),
521 Some(ClassificationLevel::Restricted)
522 );
523 assert_eq!(
524 detector.classify("social_security_number"),
525 Some(ClassificationLevel::Restricted)
526 );
527 }
528
529 #[test]
530 fn pii_detector_no_match_for_regular_fields() {
531 let detector = FieldNamePiiDetector::new();
532 assert_eq!(detector.classify("created_at"), None);
533 assert_eq!(detector.classify("status"), None);
534 assert_eq!(detector.classify("quantity"), None);
535 }
536
537 #[test]
538 fn pii_detector_scan_json_value() {
539 let detector = FieldNamePiiDetector::new();
540 let json = serde_json::json!({
541 "id": 1,
542 "email": "test@example.com",
543 "profile": {
544 "first_name": "Alice",
545 "phone": "555-1234"
546 },
547 "status": "active"
548 });
549
550 let results = detector.scan_value(&json);
551 assert_eq!(results.len(), 3);
552
553 let field_names: Vec<&str> = results.iter().map(|f| f.field_name.as_str()).collect();
554 assert!(field_names.contains(&"email"));
555 assert!(field_names.contains(&"profile.first_name"));
556 assert!(field_names.contains(&"profile.phone"));
557 }
558
559 #[test]
560 fn pii_detector_trait_impl() {
561 let detector = FieldNamePiiDetector::new();
562 assert!(detector.contains_pii("email"));
563 assert!(!detector.contains_pii("status"));
564 }
565
566 #[test]
567 fn erasure_request_processing() {
568 let sink = InMemoryErasureSink::new();
569 sink.add_records(
570 "user_42",
571 vec![
572 "record1".into(),
573 "record2".into(),
574 "record3".into(),
575 ],
576 );
577
578 let request = ErasureRequest::new(
579 "req_001".into(),
580 "user_42".into(),
581 vec!["all".into()],
582 )
583 .with_reason("GDPR Article 17 request");
584
585 let result = sink.erase(&request);
586 assert!(result.success);
587 assert_eq!(result.records_erased, 3);
588 assert!(result.failed_scopes.is_empty());
589 }
590
591 #[test]
592 fn erasure_request_for_missing_subject() {
593 let sink = InMemoryErasureSink::new();
594 let request = ErasureRequest::new(
595 "req_002".into(),
596 "unknown_user".into(),
597 vec!["all".into()],
598 );
599
600 let result = sink.erase(&request);
601 assert!(result.success);
602 assert_eq!(result.records_erased, 0);
603 }
604
605 #[test]
608 fn sensitive_display_masking_all_levels() {
609 let public = Sensitive::with_classification("data", ClassificationLevel::Public);
610 let internal = Sensitive::with_classification("data", ClassificationLevel::Internal);
611 let confidential =
612 Sensitive::with_classification("data", ClassificationLevel::Confidential);
613 let restricted = Sensitive::with_classification("data", ClassificationLevel::Restricted);
614
615 assert_eq!(format!("{}", public), "[REDACTED]");
616 assert_eq!(format!("{}", internal), "[REDACTED]");
617 assert_eq!(format!("{}", confidential), "[REDACTED]");
618 assert_eq!(format!("{}", restricted), "[REDACTED]");
619 }
620
621 #[test]
622 fn sensitive_debug_includes_level() {
623 let public = Sensitive::with_classification("data", ClassificationLevel::Public);
624 let restricted = Sensitive::with_classification("data", ClassificationLevel::Restricted);
625
626 assert_eq!(format!("{:?}", public), "[REDACTED:Public]");
627 assert_eq!(format!("{:?}", restricted), "[REDACTED:Restricted]");
628 }
629
630 #[test]
631 fn pii_detector_korean_resident_number() {
632 let detector = FieldNamePiiDetector::new();
633 assert_eq!(
634 detector.classify("jumin_number"),
635 Some(ClassificationLevel::Restricted)
636 );
637 assert_eq!(
638 detector.classify("resident_registration"),
639 Some(ClassificationLevel::Restricted)
640 );
641 }
642
643 #[test]
644 fn pii_detector_korean_business_number() {
645 let detector = FieldNamePiiDetector::new();
646 assert_eq!(
647 detector.classify("business_number"),
648 Some(ClassificationLevel::Confidential)
649 );
650 assert_eq!(
651 detector.classify("business_registration"),
652 Some(ClassificationLevel::Confidential)
653 );
654 }
655
656 #[test]
657 fn pii_detector_passport() {
658 let detector = FieldNamePiiDetector::new();
659 assert_eq!(
660 detector.classify("passport_number"),
661 Some(ClassificationLevel::Restricted)
662 );
663 }
664
665 #[test]
666 fn pii_detector_drivers_license() {
667 let detector = FieldNamePiiDetector::new();
668 assert_eq!(
669 detector.classify("drivers_license"),
670 Some(ClassificationLevel::Restricted)
671 );
672 assert_eq!(
673 detector.classify("license_number"),
674 Some(ClassificationLevel::Restricted)
675 );
676 }
677
678 #[test]
679 fn classification_level_serde_roundtrip() {
680 let level = ClassificationLevel::Restricted;
681 let json = serde_json::to_string(&level).unwrap();
682 let deser: ClassificationLevel = serde_json::from_str(&json).unwrap();
683 assert_eq!(deser, level);
684 }
685
686 #[test]
687 fn erasure_request_with_reason() {
688 let req = ErasureRequest::new("r1".into(), "user1".into(), vec!["all".into()])
689 .with_reason("GDPR Article 17");
690 assert_eq!(req.reason.as_deref(), Some("GDPR Article 17"));
691 }
692
693 #[test]
694 fn in_memory_erasure_sink_verify_post_erasure() {
695 let sink = InMemoryErasureSink::new();
696 sink.add_records("user_1", vec!["rec1".into(), "rec2".into()]);
697
698 let req = ErasureRequest::new("r1".into(), "user_1".into(), vec!["all".into()]);
699 let result = sink.erase(&req);
700 assert_eq!(result.records_erased, 2);
701
702 let result2 = sink.erase(&req);
704 assert_eq!(result2.records_erased, 0);
705 }
706
707 #[test]
708 fn pii_detector_total_categories() {
709 let detector = FieldNamePiiDetector::new();
710 assert_eq!(detector.patterns.len(), 13);
712 }
713
714 #[test]
715 fn sensitive_into_inner_returns_value() {
716 let s = Sensitive::new(42);
717 assert_eq!(s.into_inner(), 42);
718 }
719
720 #[test]
721 fn xor_encryption_different_keys_differ() {
722 let hook1 = XorEncryption::new(0x42);
723 let hook2 = XorEncryption::new(0xFF);
724 let data = b"test";
725 assert_ne!(hook1.encrypt(data), hook2.encrypt(data));
726 }
727}