voided_core/obfuscation/
mod.rs

1//! Obfuscation module providing map-based character substitution.
2
3mod map;
4
5pub use map::{
6    generate_map, analyze_map, get_expansion_ratio,
7    GenerateMapOptions, TemperatureConfig, MapAnalysis,
8    get_temperature_profile, get_config_from_temperature,
9};
10
11use crate::Result;
12use alloc::{
13    collections::BTreeMap,
14    string::{String, ToString},
15    vec::Vec,
16};
17use serde::{Deserialize, Serialize};
18
19/// Obfuscation map: maps characters to arrays of possible substitutions
20pub type ObfuscationMap = BTreeMap<char, Vec<String>>;
21
22/// Result of an obfuscation operation
23#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct ObfuscationResult {
25    /// Obfuscated text
26    pub obfuscated: String,
27    /// Statistics about the obfuscation
28    pub stats: ObfuscationStats,
29}
30
31/// Statistics about an obfuscation operation
32#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct ObfuscationStats {
34    /// Original text length
35    pub original_length: usize,
36    /// Obfuscated text length
37    pub obfuscated_length: usize,
38    /// Expansion ratio
39    pub expansion_ratio: f64,
40    /// Number of unique characters obfuscated
41    pub unique_chars_obfuscated: usize,
42    /// Number of mappings used
43    pub mappings_used: usize,
44}
45
46/// Selection strategy for choosing among multiple mappings
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
48pub enum SelectionStrategy {
49    /// Random selection based on seed
50    #[default]
51    Random,
52    /// Round-robin through mappings
53    RoundRobin,
54    /// Always choose shortest mapping
55    Shortest,
56    /// Always choose longest mapping
57    Longest,
58}
59
60/// Options for obfuscation
61#[derive(Debug, Clone)]
62pub struct ObfuscationOptions {
63    /// Seed for deterministic random selection
64    pub seed: String,
65    /// Selection strategy
66    pub strategy: SelectionStrategy,
67}
68
69impl Default for ObfuscationOptions {
70    fn default() -> Self {
71        Self {
72            seed: "default-seed".to_string(),
73            strategy: SelectionStrategy::Random,
74        }
75    }
76}
77
78/// Delimiter used between obfuscated tokens
79/// Using a character that's NOT in the default charset to avoid conflicts
80/// U+001F (Unit Separator) is a control character not used in normal text
81pub const DELIMITER: char = '\x1F';
82
83/// Simple seeded random number generator
84struct SeededRandom {
85    state: u64,
86}
87
88impl SeededRandom {
89    fn new(seed: &str) -> Self {
90        let mut hash: u64 = 0;
91        for (i, byte) in seed.bytes().enumerate() {
92            hash = hash.wrapping_add((byte as u64).wrapping_mul(31u64.wrapping_pow(i as u32)));
93        }
94        Self { state: hash.max(1) }
95    }
96
97    fn next(&mut self) -> u64 {
98        self.state = self.state.wrapping_mul(9301).wrapping_add(49297) % 233280;
99        self.state
100    }
101
102    fn next_usize(&mut self, max: usize) -> usize {
103        (self.next() as usize) % max
104    }
105}
106
107/// Obfuscate text using the provided map
108pub fn obfuscate(
109    text: &str,
110    map: &ObfuscationMap,
111    options: Option<ObfuscationOptions>,
112) -> ObfuscationResult {
113    let opts = options.unwrap_or_default();
114    let _rng = SeededRandom::new(&opts.seed);
115    
116    let mut tokens: Vec<String> = Vec::with_capacity(text.len());
117    let mut unique_chars = std::collections::HashSet::new();
118    let mut mappings_used = 0;
119    
120    for (i, char) in text.chars().enumerate() {
121        if let Some(mappings) = map.get(&char) {
122            if !mappings.is_empty() {
123                unique_chars.insert(char);
124                
125                let selected = match opts.strategy {
126                    SelectionStrategy::Random => {
127                        // Use char code + position for additional entropy
128                        let entropy = (char as u64).wrapping_add(i as u64);
129                        let mut local_rng = SeededRandom::new(&format!("{}{}", opts.seed, entropy));
130                        &mappings[local_rng.next_usize(mappings.len())]
131                    }
132                    SelectionStrategy::RoundRobin => {
133                        &mappings[i % mappings.len()]
134                    }
135                    SelectionStrategy::Shortest => {
136                        mappings.iter().min_by_key(|s| s.len()).unwrap()
137                    }
138                    SelectionStrategy::Longest => {
139                        mappings.iter().max_by_key(|s| s.len()).unwrap()
140                    }
141                };
142                
143                tokens.push(selected.clone());
144                mappings_used += 1;
145                continue;
146            }
147        }
148        
149        // Character not in map, keep as-is
150        // But if it's the delimiter, we need to handle it specially
151        // Since delimiter is not in charset, this shouldn't happen, but be safe
152        if char == DELIMITER {
153            // This shouldn't happen if charset doesn't include delimiter
154            // But if it does, we need to escape it or use a placeholder
155            tokens.push("_DELIM_".to_string());
156        } else {
157            tokens.push(char.to_string());
158        }
159    }
160    
161    let obfuscated = tokens.join(&DELIMITER.to_string());
162    let original_length = text.len();
163    let obfuscated_length = obfuscated.len();
164    
165    ObfuscationResult {
166        obfuscated,
167        stats: ObfuscationStats {
168            original_length,
169            obfuscated_length,
170            expansion_ratio: obfuscated_length as f64 / original_length as f64,
171            unique_chars_obfuscated: unique_chars.len(),
172            mappings_used,
173        },
174    }
175}
176
177/// Deobfuscate text using the provided map
178pub fn deobfuscate(obfuscated: &str, map: &ObfuscationMap) -> String {
179    // Build reverse map
180    let mut reverse_map: BTreeMap<&str, char> = BTreeMap::new();
181    for (char, mappings) in map.iter() {
182        for mapping in mappings {
183            reverse_map.insert(mapping.as_str(), *char);
184        }
185    }
186    
187    // Split by delimiter and reverse each token
188    obfuscated
189        .split(DELIMITER)
190        .map(|token| {
191            // Handle delimiter placeholder
192            if token == "_DELIM_" {
193                return DELIMITER.to_string();
194            }
195            if let Some(&original) = reverse_map.get(token) {
196                original.to_string()
197            } else {
198                // Token not found in map, return as-is
199                token.to_string()
200            }
201        })
202        .collect()
203}
204
205/// Test obfuscation roundtrip
206pub fn test_roundtrip(
207    text: &str,
208    map: &ObfuscationMap,
209    options: Option<ObfuscationOptions>,
210) -> Result<bool> {
211    let result = obfuscate(text, map, options);
212    let deobfuscated = deobfuscate(&result.obfuscated, map);
213    Ok(text == deobfuscated)
214}
215
216#[cfg(test)]
217mod tests {
218    use super::*;
219
220    fn create_test_map() -> ObfuscationMap {
221        let mut map = ObfuscationMap::new();
222        map.insert('a', vec!["alpha".to_string(), "apple".to_string()]);
223        map.insert('b', vec!["bravo".to_string(), "banana".to_string()]);
224        map.insert('c', vec!["charlie".to_string()]);
225        map.insert(' ', vec!["_space_".to_string()]);
226        map
227    }
228
229    #[test]
230    fn test_obfuscate_deobfuscate() {
231        let map = create_test_map();
232        let text = "abc";
233        
234        let result = obfuscate(text, &map, None);
235        let deobfuscated = deobfuscate(&result.obfuscated, &map);
236        
237        assert_eq!(text, deobfuscated);
238    }
239
240    #[test]
241    fn test_deterministic_with_seed() {
242        let map = create_test_map();
243        let text = "abc";
244        let options = ObfuscationOptions {
245            seed: "test-seed".to_string(),
246            strategy: SelectionStrategy::Random,
247        };
248        
249        let result1 = obfuscate(text, &map, Some(options.clone()));
250        let result2 = obfuscate(text, &map, Some(options));
251        
252        // Same seed should produce same output
253        assert_eq!(result1.obfuscated, result2.obfuscated);
254    }
255
256    #[test]
257    fn test_different_seeds() {
258        let map = create_test_map();
259        let text = "aaa"; // Multiple 'a' chars with multiple mappings
260        
261        let result1 = obfuscate(text, &map, Some(ObfuscationOptions {
262            seed: "seed1".to_string(),
263            strategy: SelectionStrategy::Random,
264        }));
265        
266        let result2 = obfuscate(text, &map, Some(ObfuscationOptions {
267            seed: "seed2".to_string(),
268            strategy: SelectionStrategy::Random,
269        }));
270        
271        // Different seeds may produce different outputs (though not guaranteed)
272        // Both should deobfuscate correctly
273        assert_eq!(deobfuscate(&result1.obfuscated, &map), text);
274        assert_eq!(deobfuscate(&result2.obfuscated, &map), text);
275    }
276
277    #[test]
278    fn test_selection_strategies() {
279        let map = create_test_map();
280        let text = "a";
281        
282        let shortest = obfuscate(text, &map, Some(ObfuscationOptions {
283            seed: "test".to_string(),
284            strategy: SelectionStrategy::Shortest,
285        }));
286        
287        let longest = obfuscate(text, &map, Some(ObfuscationOptions {
288            seed: "test".to_string(),
289            strategy: SelectionStrategy::Longest,
290        }));
291        
292        // Shortest should be "alpha" (5 chars), longest "apple" (5 chars) - both same length
293        // Just verify they deobfuscate correctly
294        assert_eq!(deobfuscate(&shortest.obfuscated, &map), "a");
295        assert_eq!(deobfuscate(&longest.obfuscated, &map), "a");
296    }
297
298    #[test]
299    fn test_unmapped_characters() {
300        let map = create_test_map();
301        let text = "a1b2c"; // 1 and 2 not in map
302        
303        let result = obfuscate(text, &map, None);
304        let deobfuscated = deobfuscate(&result.obfuscated, &map);
305        
306        assert_eq!(text, deobfuscated);
307    }
308
309    #[test]
310    fn test_roundtrip_fn() {
311        let map = create_test_map();
312        let text = "abc test";
313        
314        assert!(super::test_roundtrip(text, &map, None).unwrap());
315    }
316}
317