Skip to main content

palisade_errors/
obfuscation.rs

1//! Error code obfuscation (always on).
2//!
3//! Makes systematic error code fingerprinting harder by adding per-session
4//! offsets to error codes. The same semantic error will have different codes
5//! across sessions, making it harder for attackers to build a code map.
6//!
7//! # Security Model
8//!
9//! - **Namespace preserved**: Still "CFG", "IO", etc. (needed for Display)
10//! - **Category preserved**: Still Configuration, I/O, etc.
11//! - **Numeric code obfuscated**: E-CFG-100 becomes E-CFG-103, E-CFG-107, etc.
12//! - **Session-specific**: Different salt per connection/session
13//! - **Deterministic within session**: Same error = same obfuscated code
14//!
15//! # Threat Mitigation
16//!
17//! **Without obfuscation:**
18//! ```text
19//! Attacker triggers 100 errors, sees:
20//! E-CFG-100 (repeated 50x)
21//! E-CFG-101 (repeated 30x)
22//! E-CFG-104 (repeated 20x)
23//!
24//! Maps to source code, identifies:
25//! - 100 = parser.rs:42
26//! - 101 = validator.rs:89
27//! - 104 = permissions.rs:156
28//! ```
29//!
30//! **With obfuscation:**
31//! ```text
32//! Session 1: E-CFG-103, E-CFG-104, E-CFG-107
33//! Session 2: E-CFG-101, E-CFG-102, E-CFG-105
34//! Session 3: E-CFG-106, E-CFG-107, E-CFG-110
35//!
36//! Attacker cannot correlate codes across sessions.
37//! Fingerprinting requires compromising a session to learn its salt.
38//! ```
39//!
40//! # Performance
41//!
42//! Overhead:
43//! Initialize session salt:  352 ps  (2.8T ops/sec)
44//! Obfuscate error code:      14 ns  (71.4M ops/sec)
45//! Generate random salt:      72 ns  (13.9M ops/sec)
46//! Error with obfuscation:   243 ns  (4.1M errors/sec)
47
48use crate::ErrorCode;
49use std::sync::atomic::{AtomicU32, Ordering};
50
51/// Thread-local session salt for error code obfuscation.
52///
53/// Uses relaxed ordering since we don't need synchronization -
54/// each thread/session has its own salt and doesn't care about others.
55static SESSION_SALT: AtomicU32 = AtomicU32::new(0);
56
57/// Initialize session-specific error code salt.
58///
59/// Call this once per session/connection to enable per-session obfuscation.
60/// The salt affects all errors created in this thread until re-initialized.
61///
62/// # Arguments
63///
64/// * `seed` - Any u32 value (session ID, connection hash, random number)
65///
66/// # Implementation Note
67///
68/// We use only the lower 3 bits (0-7 range) to keep codes within
69/// their namespace ranges and avoid collisions.
70#[inline]
71pub fn init_session_salt(seed: u32) {
72    // Use lower 3 bits: gives us 8 different offsets (0-7)
73    // This keeps codes well within their 100-range namespaces
74    let salt = seed & 0b111;
75    SESSION_SALT.store(salt, Ordering::Relaxed);
76}
77
78/// Get current session salt value.
79///
80/// Useful for debugging or logging which salt is active.
81#[inline]
82pub fn get_session_salt() -> u32 {
83    SESSION_SALT.load(Ordering::Relaxed)
84}
85
86/// Clear session salt (revert to no obfuscation).
87///
88/// Useful for testing or when switching contexts.
89#[inline]
90pub fn clear_session_salt() {
91    SESSION_SALT.store(0, Ordering::Relaxed);
92}
93
94/// Apply obfuscation to an error code using current session salt.
95///
96/// Creates a new ErrorCode with:
97/// - Same namespace (e.g., "CFG")
98/// - Same category (e.g., Configuration)
99/// - Offset numeric code (e.g., 100 → 103)
100///
101/// The offset wraps within the namespace's range to avoid collisions.
102///
103/// # Example
104///
105/// ```rust
106/// use palisade_errors::{obfuscation, definitions};
107///
108/// // Base: E-CFG-100
109/// obfuscation::init_session_salt(3);
110/// let obfuscated = obfuscation::obfuscate_code(&definitions::CFG_PARSE_FAILED);
111/// // Result: E-CFG-103
112///
113/// obfuscation::init_session_salt(7);
114/// let obfuscated = obfuscation::obfuscate_code(&definitions::CFG_PARSE_FAILED);
115/// // Result: E-CFG-107
116/// ```
117///
118/// # Namespace Safety
119///
120/// The obfuscation ensures codes stay within their namespace:
121/// - CFG (100-199): offsets wrap within 100-199
122/// - IO (800-899): offsets wrap within 800-899
123/// - etc.
124#[inline]
125pub fn obfuscate_code(base: &ErrorCode) -> ErrorCode {
126    let salt = get_session_salt();
127    let base_code = base.code();
128    
129    // Calculate namespace boundaries
130    // E.g., for 150: namespace_base = 100, offset = 50
131    let namespace_base = (base_code / 100) * 100;
132    let offset = base_code % 100;
133    
134    // Add salt and wrap within namespace (0-99 range per namespace)
135    let new_offset = (offset + salt as u16) % 100;
136    let new_code = namespace_base + new_offset;
137    
138    // Create new code with same namespace and category
139    ErrorCode::const_new(base.namespace(), new_code, base.category(), base.impact())
140}
141
142/// Generate a random session salt using system entropy.
143///
144/// Useful for automatically initializing sessions without manual seed management.
145///
146/// # Example
147///
148/// ```rust
149/// use palisade_errors::obfuscation;
150///
151/// // Auto-generate salt for this session
152/// let salt = obfuscation::generate_random_salt();
153/// obfuscation::init_session_salt(salt);
154/// ```
155#[inline]
156pub fn generate_random_salt() -> u32 {
157    use std::collections::hash_map::RandomState;
158    use std::hash::{BuildHasher, Hasher};
159    
160    let random_state = RandomState::new();
161    let mut hasher = random_state.build_hasher();
162    
163    // Hash current time + thread ID for entropy
164    let now = std::time::SystemTime::now()
165        .duration_since(std::time::UNIX_EPOCH)
166        .unwrap()
167        .as_nanos();
168    
169    hasher.write_u128(now);
170    
171    // Also mix in thread ID if available
172    #[cfg(not(target_arch = "wasm32"))]
173    {
174        use std::hash::Hash;
175        let thread_id = std::thread::current().id();
176        thread_id.hash(&mut hasher);
177    }
178    
179    hasher.finish() as u32
180}
181
182#[cfg(test)]
183mod tests {
184    use super::*;
185    use crate::{ErrorCode, ImpactScore, OperationCategory};
186
187    #[test]
188    fn obfuscation_stays_within_namespace() {
189        let base = ErrorCode::const_new(
190            &crate::codes::namespaces::CFG,
191            150,
192            OperationCategory::Configuration,
193            ImpactScore::new(100),
194        );
195        
196        for salt in 0..8 {
197            init_session_salt(salt);
198            let obfuscated = obfuscate_code(&base);
199            
200            // Should stay in 100-199 range
201            assert!(obfuscated.code() >= 100, "Code {} below namespace", obfuscated.code());
202            assert!(obfuscated.code() <= 199, "Code {} above namespace", obfuscated.code());
203            
204            // Namespace and category unchanged
205            assert_eq!(obfuscated.namespace().as_str(), "CFG");
206            assert_eq!(obfuscated.category(), OperationCategory::Configuration);
207        }
208    }
209
210    #[test]
211    fn different_salts_produce_different_codes() {
212        let base = ErrorCode::const_new(
213            &crate::codes::namespaces::CFG,
214            100,
215            OperationCategory::Configuration,
216            ImpactScore::new(100),
217        );
218        
219        init_session_salt(0);
220        let code1 = obfuscate_code(&base);
221        
222        init_session_salt(5);
223        let code2 = obfuscate_code(&base);
224        
225        assert_ne!(code1.code(), code2.code());
226    }
227
228    #[test]
229    fn same_salt_produces_same_code() {
230        let base = ErrorCode::const_new(
231            &crate::codes::namespaces::CFG,
232            100,
233            OperationCategory::Configuration,
234            ImpactScore::new(100),
235        );
236        
237        init_session_salt(3);
238        let code1 = obfuscate_code(&base);
239        let code2 = obfuscate_code(&base);
240        
241        assert_eq!(code1.code(), code2.code());
242    }
243
244    #[test]
245    fn obfuscation_at_namespace_boundary() {
246        // Test edge cases at namespace boundaries
247        let base_low = ErrorCode::const_new(
248            &crate::codes::namespaces::CFG,
249            100,
250            OperationCategory::Configuration,
251            ImpactScore::new(100),
252        );
253        let base_high = ErrorCode::const_new(
254            &crate::codes::namespaces::CFG,
255            199,
256            OperationCategory::Configuration,
257            ImpactScore::new(100),
258        );
259        
260        init_session_salt(7);
261        let obf_low = obfuscate_code(&base_low);
262        let obf_high = obfuscate_code(&base_high);
263        
264        assert!(obf_low.code() >= 100 && obf_low.code() <= 199);
265        assert!(obf_high.code() >= 100 && obf_high.code() <= 199);
266    }
267
268    #[test]
269    fn wrapping_behavior() {
270        // Code at 195 + salt 7 = should wrap to 102
271        let base = ErrorCode::const_new(
272            &crate::codes::namespaces::CFG,
273            195,
274            OperationCategory::Configuration,
275            ImpactScore::new(100),
276        );
277        init_session_salt(7);
278        let obfuscated = obfuscate_code(&base);
279        
280        // 195 % 100 = 95, (95 + 7) % 100 = 2, 100 + 2 = 102
281        assert_eq!(obfuscated.code(), 102);
282    }
283
284    #[test]
285    fn clear_salt_resets_to_zero() {
286        init_session_salt(5);
287        assert_eq!(get_session_salt(), 5);
288        
289        clear_session_salt();
290        assert_eq!(get_session_salt(), 0);
291    }
292
293    #[test]
294    fn random_salt_generation() {
295        let salt1 = generate_random_salt();
296        let salt2 = generate_random_salt();
297        
298        // Should be different (extremely high probability)
299        assert_ne!(salt1, salt2);
300        
301        // Should be valid when used
302        init_session_salt(salt1);
303        assert_eq!(get_session_salt(), salt1 & 0b111);
304    }
305
306    #[test]
307    fn obfuscation_formatting() {
308        let base = ErrorCode::const_new(
309            &crate::codes::namespaces::CFG,
310            100,
311            OperationCategory::Configuration,
312            ImpactScore::new(100),
313        );
314        init_session_salt(3);
315        let obfuscated = obfuscate_code(&base);
316        
317        assert_eq!(obfuscated.to_string(), "E-CFG-103");
318    }
319
320    #[test]
321    fn multiple_namespaces() {
322        init_session_salt(4);
323        
324        let cfg = ErrorCode::const_new(
325            &crate::codes::namespaces::CFG,
326            100,
327            OperationCategory::Configuration,
328            ImpactScore::new(100),
329        );
330        let io = ErrorCode::const_new(
331            &crate::codes::namespaces::IO,
332            800,
333            OperationCategory::IO,
334            ImpactScore::new(100),
335        );
336        
337        let cfg_obf = obfuscate_code(&cfg);
338        let io_obf = obfuscate_code(&io);
339        
340        // Each stays in its namespace
341        assert_eq!(cfg_obf.code(), 104);  // 100 + 4
342        assert_eq!(io_obf.code(), 804);   // 800 + 4
343    }
344}