Skip to main content

stackforge_core/anonymize/
crypto_pan.rs

1//! Crypto-PAn prefix-preserving IP address anonymization.
2//!
3//! Implements the algorithm by Xu, Fan, Ammar & Moon (2002) using AES-128
4//! as the underlying block cipher. Two addresses sharing a *k*-bit network
5//! prefix are guaranteed to share a *k*-bit prefix after anonymization.
6//!
7//! # Key format
8//!
9//! The 32-byte key is split into:
10//! - bytes `[0..16]`: AES-128 encryption key
11//! - bytes `[16..32]`: padding material used as the initial cipher input
12
13use std::collections::HashMap;
14use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
15
16use aes::Aes128;
17use aes::cipher::{BlockEncrypt, KeyInit, generic_array::GenericArray};
18
19/// Crypto-PAn anonymizer backed by AES-128.
20pub struct CryptoPan {
21    cipher: Aes128,
22    pad: [u8; 16],
23    /// Cache mapping original IPs to their anonymized counterparts.
24    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    /// Create a new Crypto-PAn instance from a 32-byte key.
37    ///
38    /// - `key[0..16]` is the AES-128 key.
39    /// - `key[16..32]` is the padding material.
40    #[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    /// Encrypt a single 128-bit block in-place.
53    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    /// Anonymize an IPv4 address (prefix-preserving).
60    #[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        // Base input block: start with pad, replace first 4 bytes as we go
66        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            // Build input: first `pos` bits from original address, rest from pad
72            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            // input[4..16] stays as pad[4..16]
84
85            let mut output = input;
86            self.encrypt_block(&mut output);
87
88            // Use the MSB of the encrypted output
89            let bit = u32::from(output[0] >> 7);
90            result |= bit << (31 - pos);
91        }
92
93        Ipv4Addr::from(result ^ orig)
94    }
95
96    /// Anonymize an IPv6 address (prefix-preserving).
97    #[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            // Build input: first `pos` bits from original, rest from pad
107            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    /// Anonymize an IP address, using the cache for repeated lookups.
125    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    /// Number of cached address mappings.
138    #[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        // Extremely unlikely to map to itself
172        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        // Two addresses on the same /24 subnet
181        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        // They originally share a 30-bit prefix (differ only in last 2 bits).
187        // After Crypto-PAn, they must share at least a 30-bit prefix.
188        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        // Two addresses on different /16 subnets
205        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}