stackforge_core/anonymize/
crypto_pan.rs1use std::collections::HashMap;
14use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
15
16use aes::Aes128;
17use aes::cipher::{BlockEncrypt, KeyInit, generic_array::GenericArray};
18
19pub struct CryptoPan {
21 cipher: Aes128,
22 pad: [u8; 16],
23 cache: HashMap<IpAddr, IpAddr>,
25}
26
27impl std::fmt::Debug for CryptoPan {
28 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29 f.debug_struct("CryptoPan")
30 .field("cache_size", &self.cache.len())
31 .finish_non_exhaustive()
32 }
33}
34
35impl CryptoPan {
36 #[must_use]
41 pub fn new(key: &[u8; 32]) -> Self {
42 let cipher = Aes128::new(GenericArray::from_slice(&key[..16]));
43 let mut pad = [0u8; 16];
44 pad.copy_from_slice(&key[16..32]);
45 Self {
46 cipher,
47 pad,
48 cache: HashMap::new(),
49 }
50 }
51
52 fn encrypt_block(&self, block: &mut [u8; 16]) {
54 let mut ga = GenericArray::clone_from_slice(block);
55 self.cipher.encrypt_block(&mut ga);
56 block.copy_from_slice(ga.as_slice());
57 }
58
59 #[must_use]
61 pub fn anonymize_ipv4(&self, addr: Ipv4Addr) -> Ipv4Addr {
62 let orig = u32::from(addr);
63 let mut result: u32 = 0;
64
65 let mut input = self.pad;
67
68 let pad_first4 = u32::from_be_bytes([self.pad[0], self.pad[1], self.pad[2], self.pad[3]]);
69
70 for pos in 0..32u32 {
71 let first4 = if pos == 0 {
73 pad_first4
74 } else {
75 let mask = 0xFFFF_FFFFu32.wrapping_shl(32 - pos);
76 (orig & mask) | (pad_first4 & !mask)
77 };
78
79 input[0] = (first4 >> 24) as u8;
80 input[1] = (first4 >> 16) as u8;
81 input[2] = (first4 >> 8) as u8;
82 input[3] = first4 as u8;
83 let mut output = input;
86 self.encrypt_block(&mut output);
87
88 let bit = u32::from(output[0] >> 7);
90 result |= bit << (31 - pos);
91 }
92
93 Ipv4Addr::from(result ^ orig)
94 }
95
96 #[must_use]
98 pub fn anonymize_ipv6(&self, addr: Ipv6Addr) -> Ipv6Addr {
99 let orig = u128::from(addr);
100 let mut result: u128 = 0;
101
102 let pad128 =
103 u128::from_be_bytes(self.pad);
104
105 for pos in 0..128u32 {
106 let combined = if pos == 0 {
108 pad128
109 } else {
110 let mask = u128::MAX.wrapping_shl(128 - pos);
111 (orig & mask) | (pad128 & !mask)
112 };
113
114 let mut input = combined.to_be_bytes();
115 self.encrypt_block(&mut input);
116
117 let bit = u128::from(input[0] >> 7);
118 result |= bit << (127 - pos);
119 }
120
121 Ipv6Addr::from(result ^ orig)
122 }
123
124 pub fn anonymize_ip(&mut self, addr: IpAddr) -> IpAddr {
126 if let Some(&cached) = self.cache.get(&addr) {
127 return cached;
128 }
129 let anon = match addr {
130 IpAddr::V4(v4) => IpAddr::V4(self.anonymize_ipv4(v4)),
131 IpAddr::V6(v6) => IpAddr::V6(self.anonymize_ipv6(v6)),
132 };
133 self.cache.insert(addr, anon);
134 anon
135 }
136
137 #[must_use]
139 pub fn cache_size(&self) -> usize {
140 self.cache.len()
141 }
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147
148 fn test_key() -> [u8; 32] {
149 let mut key = [0u8; 32];
150 for (i, b) in key.iter_mut().enumerate() {
151 *b = i as u8;
152 }
153 key
154 }
155
156 #[test]
157 fn test_deterministic() {
158 let key = test_key();
159 let c1 = CryptoPan::new(&key);
160 let c2 = CryptoPan::new(&key);
161 let addr = Ipv4Addr::new(192, 168, 1, 100);
162 assert_eq!(c1.anonymize_ipv4(addr), c2.anonymize_ipv4(addr));
163 }
164
165 #[test]
166 fn test_different_from_original() {
167 let key = test_key();
168 let cp = CryptoPan::new(&key);
169 let addr = Ipv4Addr::new(192, 168, 1, 100);
170 let anon = cp.anonymize_ipv4(addr);
171 assert_ne!(addr, anon);
173 }
174
175 #[test]
176 fn test_prefix_preservation_ipv4() {
177 let key = test_key();
178 let cp = CryptoPan::new(&key);
179
180 let a1 = Ipv4Addr::new(10, 0, 1, 1);
182 let a2 = Ipv4Addr::new(10, 0, 1, 2);
183 let anon1 = u32::from(cp.anonymize_ipv4(a1));
184 let anon2 = u32::from(cp.anonymize_ipv4(a2));
185
186 let orig1 = u32::from(a1);
189 let orig2 = u32::from(a2);
190 let shared_bits = (orig1 ^ orig2).leading_zeros();
191
192 let anon_shared = (anon1 ^ anon2).leading_zeros();
193 assert!(
194 anon_shared >= shared_bits,
195 "Expected at least {shared_bits} shared prefix bits, got {anon_shared}"
196 );
197 }
198
199 #[test]
200 fn test_prefix_preservation_different_subnets() {
201 let key = test_key();
202 let cp = CryptoPan::new(&key);
203
204 let a1 = Ipv4Addr::new(10, 1, 0, 1);
206 let a2 = Ipv4Addr::new(10, 2, 0, 1);
207 let anon1 = u32::from(cp.anonymize_ipv4(a1));
208 let anon2 = u32::from(cp.anonymize_ipv4(a2));
209
210 let orig_shared = (u32::from(a1) ^ u32::from(a2)).leading_zeros();
211 let anon_shared = (anon1 ^ anon2).leading_zeros();
212 assert!(anon_shared >= orig_shared);
213 }
214
215 #[test]
216 fn test_ipv6_deterministic() {
217 let key = test_key();
218 let c1 = CryptoPan::new(&key);
219 let c2 = CryptoPan::new(&key);
220 let addr = Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1);
221 assert_eq!(c1.anonymize_ipv6(addr), c2.anonymize_ipv6(addr));
222 }
223
224 #[test]
225 fn test_ipv6_prefix_preservation() {
226 let key = test_key();
227 let cp = CryptoPan::new(&key);
228
229 let a1 = Ipv6Addr::new(0x2001, 0xdb8, 0, 1, 0, 0, 0, 1);
230 let a2 = Ipv6Addr::new(0x2001, 0xdb8, 0, 1, 0, 0, 0, 2);
231 let anon1 = u128::from(cp.anonymize_ipv6(a1));
232 let anon2 = u128::from(cp.anonymize_ipv6(a2));
233
234 let orig_shared = (u128::from(a1) ^ u128::from(a2)).leading_zeros();
235 let anon_shared = (anon1 ^ anon2).leading_zeros();
236 assert!(anon_shared >= orig_shared);
237 }
238
239 #[test]
240 fn test_cache() {
241 let key = test_key();
242 let mut cp = CryptoPan::new(&key);
243 let addr = IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1));
244
245 let first = cp.anonymize_ip(addr);
246 assert_eq!(cp.cache_size(), 1);
247
248 let second = cp.anonymize_ip(addr);
249 assert_eq!(first, second);
250 assert_eq!(cp.cache_size(), 1);
251 }
252
253 #[test]
254 fn test_different_keys_different_results() {
255 let key1 = test_key();
256 let mut key2 = test_key();
257 key2[0] = 0xFF;
258
259 let c1 = CryptoPan::new(&key1);
260 let c2 = CryptoPan::new(&key2);
261
262 let addr = Ipv4Addr::new(10, 0, 0, 1);
263 assert_ne!(c1.anonymize_ipv4(addr), c2.anonymize_ipv4(addr));
264 }
265}