Skip to main content

apfsds_obfuscation/
padding.rs

1//! Smart padding to disguise packet sizes
2
3/// Target packet sizes mimicking real API traffic
4const SIZE_DISTRIBUTION: &[(usize, f32)] = &[
5    (512, 0.40),   // 40% small packets
6    (1024, 0.20),  // 20%
7    (2048, 0.15),  // 15%
8    (4096, 0.15),  // 15%
9    (8192, 0.07),  // 7%
10    (16384, 0.03), // 3% large packets
11];
12
13/// Maximum jitter percentage (±10%)
14const JITTER_PERCENT: usize = 10;
15
16/// Size distribution strategy
17#[derive(Debug, Clone)]
18pub enum SizeDistribution {
19    /// Fixed distribution (default)
20    Fixed(&'static [(usize, f32)]),
21    /// Learned from real traffic
22    Learned(Vec<(usize, f32)>),
23    /// Random within range
24    Random { min: usize, max: usize },
25    /// Normal distribution
26    Normal { mean: usize, std_dev: usize },
27}
28
29impl Default for SizeDistribution {
30    fn default() -> Self {
31        Self::Fixed(SIZE_DISTRIBUTION)
32    }
33}
34
35/// Padding strategy configuration
36#[derive(Debug, Clone)]
37pub struct PaddingStrategy {
38    /// Enable random jitter
39    pub jitter: bool,
40    /// Minimum output size
41    pub min_size: usize,
42    /// Size distribution strategy
43    pub size_distribution: SizeDistribution,
44}
45
46impl Default for PaddingStrategy {
47    fn default() -> Self {
48        Self {
49            jitter: true,
50            min_size: 64,
51            size_distribution: SizeDistribution::default(),
52        }
53    }
54}
55
56impl PaddingStrategy {
57    /// Create a new padding strategy
58    pub fn new() -> Self {
59        Self::default()
60    }
61
62    /// Without jitter (for testing)
63    pub fn no_jitter() -> Self {
64        Self {
65            jitter: false,
66            min_size: 64,
67            size_distribution: SizeDistribution::default(),
68        }
69    }
70
71    /// Calculate target size for padding
72    pub fn calculate_target_size(&self, payload_len: usize) -> usize {
73        let base_target = match &self.size_distribution {
74            SizeDistribution::Fixed(dist) => {
75                // Find the next target size from fixed distribution
76                dist.iter()
77                    .find(|(size, _)| *size > payload_len)
78                    .map(|(size, _)| *size)
79                    .unwrap_or(16384)
80            }
81            SizeDistribution::Learned(dist) => {
82                // Use learned distribution (same logic as Fixed)
83                dist.iter()
84                    .find(|(size, _)| *size > payload_len)
85                    .map(|(size, _)| *size)
86                    .unwrap_or(16384)
87            }
88            SizeDistribution::Random { min, max } => {
89                // Random size within range
90                if payload_len >= *max {
91                    *max
92                } else {
93                    fastrand::usize(payload_len.max(*min)..*max)
94                }
95            }
96            SizeDistribution::Normal { mean, std_dev } => {
97                // Approximate normal distribution using Box-Muller transform
98                let u1 = fastrand::f64();
99                let u2 = fastrand::f64();
100                let z = (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos();
101                let size = (*mean as f64 + z * (*std_dev as f64)).max(0.0) as usize;
102                size.max(payload_len)
103            }
104        };
105
106        // Ensure minimum size
107        let target = base_target.max(self.min_size);
108
109        if self.jitter {
110            // Add jitter (±10%)
111            let jitter_range = target / JITTER_PERCENT;
112            let jitter = fastrand::usize(0..=jitter_range * 2);
113            target.saturating_sub(jitter_range) + jitter
114        } else {
115            target
116        }
117    }
118
119    /// Calculate how much padding to add
120    pub fn calculate_padding_len(&self, payload_len: usize) -> usize {
121        let target = self.calculate_target_size(payload_len);
122        target.saturating_sub(payload_len)
123    }
124
125    /// Add padding to data
126    pub fn pad(&self, data: &[u8]) -> Vec<u8> {
127        let padding_len = self.calculate_padding_len(data.len());
128        let total_len = data.len() + padding_len + 4; // 4 bytes for original length
129
130        let mut result = Vec::with_capacity(total_len);
131
132        // Store original length as u32 LE
133        result.extend_from_slice(&(data.len() as u32).to_le_bytes());
134
135        // Original data
136        result.extend_from_slice(data);
137
138        // Random padding (not zeros to avoid patterns)
139        for _ in 0..padding_len {
140            result.push(fastrand::u8(..));
141        }
142
143        result
144    }
145
146    /// Remove padding from data
147    pub fn unpad(data: &[u8]) -> Option<Vec<u8>> {
148        if data.len() < 4 {
149            return None;
150        }
151
152        // Read original length
153        let original_len = u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize;
154
155        if data.len() < 4 + original_len {
156            return None;
157        }
158
159        Some(data[4..4 + original_len].to_vec())
160    }
161}
162
163/// Select a target size based on distribution
164pub fn select_distributed_size() -> usize {
165    let r = fastrand::f32();
166    let mut cumulative = 0.0;
167
168    for &(size, prob) in SIZE_DISTRIBUTION {
169        cumulative += prob;
170        if r < cumulative {
171            return size;
172        }
173    }
174
175    8192 // fallback
176}
177
178#[cfg(test)]
179mod tests {
180    use super::*;
181
182    #[test]
183    fn test_pad_unpad_roundtrip() {
184        let strategy = PaddingStrategy::no_jitter();
185        let original = vec![1, 2, 3, 4, 5];
186
187        let padded = strategy.pad(&original);
188        assert!(padded.len() > original.len());
189
190        let unpadded = PaddingStrategy::unpad(&padded).unwrap();
191        assert_eq!(original, unpadded);
192    }
193
194    #[test]
195    fn test_target_size_selection() {
196        let strategy = PaddingStrategy::no_jitter();
197
198        // Small data should be padded to 512
199        assert_eq!(strategy.calculate_target_size(100), 512);
200
201        // Medium data should be padded to 1024
202        assert_eq!(strategy.calculate_target_size(600), 1024);
203
204        // Large data should be padded to 16384
205        assert_eq!(strategy.calculate_target_size(10000), 16384);
206    }
207
208    #[test]
209    fn test_padding_is_random() {
210        let strategy = PaddingStrategy::default();
211        let data = vec![0u8; 10];
212
213        let padded1 = strategy.pad(&data);
214        let padded2 = strategy.pad(&data);
215
216        // Padding should be different due to random bytes
217        let padding1 = &padded1[14..];
218        let padding2 = &padded2[14..];
219
220        // Note: This could theoretically fail with very low probability
221        assert_ne!(padding1, padding2);
222    }
223
224    #[test]
225    fn test_distributed_size() {
226        // Run multiple times to verify distribution
227        let mut counts = [0usize; 6];
228        let n = 10000;
229
230        for _ in 0..n {
231            let size = select_distributed_size();
232            match size {
233                512 => counts[0] += 1,
234                1024 => counts[1] += 1,
235                2048 => counts[2] += 1,
236                4096 => counts[3] += 1,
237                8192 => counts[4] += 1,
238                16384 => counts[5] += 1,
239                _ => {}
240            }
241        }
242
243        // 512 should be most common (~40%)
244        assert!(counts[0] > counts[1]);
245        assert!(counts[0] > counts[5]);
246    }
247
248    #[test]
249    fn test_empty_data() {
250        let strategy = PaddingStrategy::no_jitter();
251        let data: Vec<u8> = vec![];
252
253        let padded = strategy.pad(&data);
254        let unpadded = PaddingStrategy::unpad(&padded).unwrap();
255
256        assert!(unpadded.is_empty());
257    }
258}