Skip to main content

sql_splitter/redactor/strategy/
hash.rs

1//! Hash strategy - one-way SHA256 hash.
2
3use super::{RedactValue, Strategy, StrategyKind};
4use sha2::{Digest, Sha256};
5
6/// Strategy that hashes values with SHA256
7#[derive(Debug, Clone)]
8pub struct HashStrategy {
9    /// Whether to preserve email domain
10    preserve_domain: bool,
11}
12
13impl HashStrategy {
14    pub fn new(preserve_domain: bool) -> Self {
15        Self { preserve_domain }
16    }
17
18    /// Hash a string value
19    fn hash_value(&self, value: &str) -> String {
20        if self.preserve_domain && value.contains('@') {
21            // Email: preserve domain
22            if let Some((local, domain)) = value.rsplit_once('@') {
23                let hash = self.compute_hash(local);
24                return format!("{}@{}", &hash[..8], domain);
25            }
26        }
27
28        // Regular hash: take first 16 chars of hex
29        let hash = self.compute_hash(value);
30        hash[..16].to_string()
31    }
32
33    /// Compute SHA256 hash and return hex string
34    fn compute_hash(&self, value: &str) -> String {
35        let mut hasher = Sha256::new();
36        hasher.update(value.as_bytes());
37        let result = hasher.finalize();
38        hex::encode(result)
39    }
40}
41
42impl Strategy for HashStrategy {
43    fn apply(&self, value: &RedactValue, _rng: &mut dyn rand::RngCore) -> RedactValue {
44        match value {
45            RedactValue::Null => RedactValue::Null,
46            RedactValue::String(s) => RedactValue::String(self.hash_value(s)),
47            RedactValue::Integer(i) => RedactValue::String(self.hash_value(&i.to_string())),
48            RedactValue::Bytes(b) => {
49                let s = String::from_utf8_lossy(b);
50                RedactValue::String(self.hash_value(&s))
51            }
52        }
53    }
54
55    fn kind(&self) -> StrategyKind {
56        StrategyKind::Hash {
57            preserve_domain: self.preserve_domain,
58        }
59    }
60}
61
62#[cfg(test)]
63mod tests {
64    use super::*;
65    use rand::SeedableRng;
66
67    #[test]
68    fn test_hash_strategy() {
69        let strategy = HashStrategy::new(false);
70        let mut rng = rand::rngs::StdRng::seed_from_u64(42);
71
72        let result = strategy.apply(&RedactValue::String("secret".to_string()), &mut rng);
73        match result {
74            RedactValue::String(s) => {
75                assert_eq!(s.len(), 16);
76                // Hash is deterministic
77                let result2 = strategy.apply(&RedactValue::String("secret".to_string()), &mut rng);
78                match result2 {
79                    RedactValue::String(s2) => assert_eq!(s, s2),
80                    _ => panic!("Expected String"),
81                }
82            }
83            _ => panic!("Expected String"),
84        }
85    }
86
87    #[test]
88    fn test_hash_preserve_domain() {
89        let strategy = HashStrategy::new(true);
90        let mut rng = rand::rngs::StdRng::seed_from_u64(42);
91
92        let result = strategy.apply(
93            &RedactValue::String("john.doe@example.com".to_string()),
94            &mut rng,
95        );
96        match result {
97            RedactValue::String(s) => {
98                assert!(s.ends_with("@example.com"));
99                assert!(s.len() > "@example.com".len());
100            }
101            _ => panic!("Expected String"),
102        }
103    }
104
105    #[test]
106    fn test_hash_deterministic() {
107        let strategy = HashStrategy::new(false);
108        let mut rng = rand::rngs::StdRng::seed_from_u64(42);
109
110        // Same input = same output (for referential integrity)
111        let result1 = strategy.apply(
112            &RedactValue::String("test@example.com".to_string()),
113            &mut rng,
114        );
115        let result2 = strategy.apply(
116            &RedactValue::String("test@example.com".to_string()),
117            &mut rng,
118        );
119
120        match (result1, result2) {
121            (RedactValue::String(s1), RedactValue::String(s2)) => assert_eq!(s1, s2),
122            _ => panic!("Expected Strings"),
123        }
124    }
125
126    #[test]
127    fn test_hash_null() {
128        let strategy = HashStrategy::new(false);
129        let mut rng = rand::rngs::StdRng::seed_from_u64(42);
130
131        let result = strategy.apply(&RedactValue::Null, &mut rng);
132        assert!(matches!(result, RedactValue::Null));
133    }
134}