apfsds_obfuscation/
padding.rs1const SIZE_DISTRIBUTION: &[(usize, f32)] = &[
5 (512, 0.40), (1024, 0.20), (2048, 0.15), (4096, 0.15), (8192, 0.07), (16384, 0.03), ];
12
13const JITTER_PERCENT: usize = 10;
15
16#[derive(Debug, Clone)]
18pub enum SizeDistribution {
19 Fixed(&'static [(usize, f32)]),
21 Learned(Vec<(usize, f32)>),
23 Random { min: usize, max: usize },
25 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#[derive(Debug, Clone)]
37pub struct PaddingStrategy {
38 pub jitter: bool,
40 pub min_size: usize,
42 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 pub fn new() -> Self {
59 Self::default()
60 }
61
62 pub fn no_jitter() -> Self {
64 Self {
65 jitter: false,
66 min_size: 64,
67 size_distribution: SizeDistribution::default(),
68 }
69 }
70
71 pub fn calculate_target_size(&self, payload_len: usize) -> usize {
73 let base_target = match &self.size_distribution {
74 SizeDistribution::Fixed(dist) => {
75 dist.iter()
77 .find(|(size, _)| *size > payload_len)
78 .map(|(size, _)| *size)
79 .unwrap_or(16384)
80 }
81 SizeDistribution::Learned(dist) => {
82 dist.iter()
84 .find(|(size, _)| *size > payload_len)
85 .map(|(size, _)| *size)
86 .unwrap_or(16384)
87 }
88 SizeDistribution::Random { min, max } => {
89 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 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 let target = base_target.max(self.min_size);
108
109 if self.jitter {
110 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 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 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; let mut result = Vec::with_capacity(total_len);
131
132 result.extend_from_slice(&(data.len() as u32).to_le_bytes());
134
135 result.extend_from_slice(data);
137
138 for _ in 0..padding_len {
140 result.push(fastrand::u8(..));
141 }
142
143 result
144 }
145
146 pub fn unpad(data: &[u8]) -> Option<Vec<u8>> {
148 if data.len() < 4 {
149 return None;
150 }
151
152 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
163pub 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 }
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 assert_eq!(strategy.calculate_target_size(100), 512);
200
201 assert_eq!(strategy.calculate_target_size(600), 1024);
203
204 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 let padding1 = &padded1[14..];
218 let padding2 = &padded2[14..];
219
220 assert_ne!(padding1, padding2);
222 }
223
224 #[test]
225 fn test_distributed_size() {
226 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 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}