Skip to main content

bao_stealth/
webgl_audio.rs

1// REQ-STL-005: WebGL/Audio fingerprint protection  @trace REQ-STL-005
2#[derive(Debug, Clone)]
3pub struct WebGLProfile {
4    pub vendor: String,
5    pub renderer: String,
6    pub extensions: Vec<String>,
7    pub max_texture_size: u32,
8    pub max_renderbuffer_size: u32,
9    pub max_viewport_dims: [u32; 2],
10}
11
12impl WebGLProfile {
13    pub fn firefox() -> Self {
14        WebGLProfile {
15            vendor: "Mozilla".into(),
16            renderer: "WebGL 1.0 (OpenGL ES 2.0 Chromium)".into(),
17            extensions: vec![
18                "ANGLE_instanced_arrays".into(),
19                "EXT_blend_minmax".into(),
20                "EXT_color_buffer_half_float".into(),
21                "EXT_float_blend".into(),
22                "EXT_frag_depth".into(),
23                "EXT_shader_texture_lod".into(),
24                "EXT_texture_compression_bptc".into(),
25                "EXT_texture_filter_anisotropic".into(),
26                "OES_element_index_uint".into(),
27                "OES_fbo_render_mipmap".into(),
28                "OES_standard_derivatives".into(),
29                "OES_texture_float".into(),
30                "OES_texture_float_linear".into(),
31                "OES_texture_half_float".into(),
32                "OES_texture_half_float_linear".into(),
33                "OES_vertex_array_object".into(),
34                "WEBGL_color_buffer_float".into(),
35                "WEBGL_compressed_texture_etc".into(),
36                "WEBGL_compressed_texture_s3tc".into(),
37                "WEBGL_debug_renderer_info".into(),
38                "WEBGL_debug_shaders".into(),
39                "WEBGL_depth_texture".into(),
40                "WEBGL_draw_buffers".into(),
41                "WEBGL_lose_context".into(),
42            ],
43            max_texture_size: 16384,
44            max_renderbuffer_size: 16384,
45            max_viewport_dims: [16384, 16384],
46        }
47    }
48
49    pub fn chrome() -> Self {
50        WebGLProfile {
51            vendor: "Google Inc. (NVIDIA)".into(),
52            renderer: "ANGLE (NVIDIA, NVIDIA GeForce GTX 1060, OpenGL 4.5)".into(),
53            extensions: vec![
54                "ANGLE_instanced_arrays".into(),
55                "EXT_blend_minmax".into(),
56                "EXT_color_buffer_half_float".into(),
57                "EXT_float_blend".into(),
58                "EXT_texture_filter_anisotropic".into(),
59                "OES_element_index_uint".into(),
60                "OES_standard_derivatives".into(),
61                "OES_texture_float".into(),
62                "OES_texture_float_linear".into(),
63                "OES_texture_half_float".into(),
64                "OES_texture_half_float_linear".into(),
65                "OES_vertex_array_object".into(),
66                "WEBGL_color_buffer_float".into(),
67                "WEBGL_compressed_texture_s3tc".into(),
68                "WEBGL_debug_renderer_info".into(),
69                "WEBGL_depth_texture".into(),
70                "WEBGL_draw_buffers".into(),
71                "WEBGL_lose_context".into(),
72            ],
73            max_texture_size: 16384,
74            max_renderbuffer_size: 16384,
75            max_viewport_dims: [16384, 16384],
76        }
77    }
78}
79
80#[derive(Debug, Clone)]
81pub struct AudioProfile {
82    seed: u64,
83    noise_amplitude: f64,
84    sample_rate: u32,
85}
86
87impl AudioProfile {
88    pub fn new(seed: u64) -> Self {
89        AudioProfile {
90            seed,
91            noise_amplitude: 1e-7,
92            sample_rate: 44100,
93        }
94    }
95
96    pub fn seed(&self) -> u64 {
97        self.seed
98    }
99
100    pub fn noise_amplitude(&self) -> f64 {
101        self.noise_amplitude
102    }
103
104    pub fn sample_rate(&self) -> u32 {
105        self.sample_rate
106    }
107
108    pub fn apply_noise(&self, sample: f64, index: u32) -> f64 {
109        let noise = self.deterministic_noise(index);
110        sample + noise * self.noise_amplitude
111    }
112
113    fn deterministic_noise(&self, index: u32) -> f64 {
114        let mut state = self.seed;
115        state ^= (index as u64).wrapping_mul(0x517CC1B727220A95);
116        state = state.wrapping_mul(0x2545F4914F6CDD1D);
117        state ^= state >> 33;
118        (state as f64) / (u64::MAX as f64) - 0.5
119    }
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125
126    // ─── AudioProfile ──────────────────────────────────────────────
127    // @trace REQ-STL-005 [req:REQ-STL-005] [level:unit]
128
129    #[test]
130    fn test_audio_profile_new() {
131        let ap = AudioProfile::new(42);
132        assert_eq!(ap.seed(), 42);
133        assert_eq!(ap.noise_amplitude(), 1e-7);
134        assert_eq!(ap.sample_rate(), 44100);
135    }
136
137    #[test]
138    fn test_audio_profile_different_seeds() {
139        let ap1 = AudioProfile::new(0);
140        let ap2 = AudioProfile::new(999);
141        assert_ne!(ap1.seed(), ap2.seed());
142    }
143
144    #[test]
145    fn test_deterministic_noise_same_seed_same_result() {
146        let ap = AudioProfile::new(12345);
147        let n1 = ap.deterministic_noise(100);
148        let n2 = ap.deterministic_noise(100);
149        assert_eq!(n1, n2);
150    }
151
152    #[test]
153    fn test_deterministic_noise_range() {
154        let ap = AudioProfile::new(42);
155        for i in 0..1000u32 {
156            let n = ap.deterministic_noise(i);
157            assert!(n >= -0.5 && n <= 0.5, "noise at index {} is {}", i, n);
158        }
159    }
160
161    #[test]
162    fn test_deterministic_noise_different_indices() {
163        let ap = AudioProfile::new(42);
164        let n0 = ap.deterministic_noise(0);
165        let n1 = ap.deterministic_noise(1);
166        // Different indices almost always produce different noise
167        assert_ne!(n0, n1);
168    }
169
170    #[test]
171    fn test_deterministic_noise_different_seeds() {
172        let ap1 = AudioProfile::new(100);
173        let ap2 = AudioProfile::new(200);
174        let n1 = ap1.deterministic_noise(50);
175        let n2 = ap2.deterministic_noise(50);
176        assert_ne!(n1, n2);
177    }
178
179    #[test]
180    fn test_apply_noise_adds_deterministic_offset() {
181        let ap = AudioProfile::new(42);
182        let sample = 1.0;
183        let index = 10u32;
184        let result = ap.apply_noise(sample, index);
185        let noise = ap.deterministic_noise(index);
186        // Result should be sample + noise * amplitude
187        let expected = sample + noise * ap.noise_amplitude();
188        assert!((result - expected).abs() < 1e-15);
189    }
190
191    #[test]
192    fn test_apply_noise_preserves_signal() {
193        let ap = AudioProfile::new(42);
194        let sample = 0.5;
195        let result = ap.apply_noise(sample, 0);
196        // Noise amplitude is 1e-7, so result is within ±1e-7 of sample
197        assert!((result - sample).abs() < 1e-6);
198    }
199
200    #[test]
201    fn test_apply_noise_different_indices_different_results() {
202        let ap = AudioProfile::new(42);
203        let r0 = ap.apply_noise(1.0, 0);
204        let r1 = ap.apply_noise(1.0, 1);
205        assert_ne!(r0, r1);
206    }
207
208    #[test]
209    fn test_audio_profile_clone() {
210        let ap = AudioProfile::new(42);
211        let cloned = ap.clone();
212        assert_eq!(ap.seed(), cloned.seed());
213        assert_eq!(ap.noise_amplitude(), cloned.noise_amplitude());
214        assert_eq!(ap.sample_rate(), cloned.sample_rate());
215    }
216
217    #[test]
218    fn test_audio_profile_debug_format() {
219        let ap = AudioProfile::new(42);
220        let debug_str = format!("{:?}", ap);
221        assert!(debug_str.contains("AudioProfile"));
222    }
223
224    // ─── WebGLProfile ──────────────────────────────────────────────
225    // @trace REQ-STL-005 [req:REQ-STL-005] [level:unit]
226
227    #[test]
228    fn test_webgl_firefox_vendor() {
229        let p = WebGLProfile::firefox();
230        assert_eq!(p.vendor, "Mozilla");
231    }
232
233    #[test]
234    fn test_webgl_firefox_extensions_nonempty() {
235        let p = WebGLProfile::firefox();
236        assert!(!p.extensions.is_empty());
237        assert!(p
238            .extensions
239            .contains(&"WEBGL_debug_renderer_info".to_string()));
240    }
241
242    #[test]
243    fn test_webgl_firefox_max_texture_size() {
244        let p = WebGLProfile::firefox();
245        assert_eq!(p.max_texture_size, 16384);
246    }
247
248    #[test]
249    fn test_webgl_chrome_vendor() {
250        let p = WebGLProfile::chrome();
251        assert_eq!(p.vendor, "Google Inc. (NVIDIA)");
252    }
253
254    #[test]
255    fn test_webgl_chrome_extensions_nonempty() {
256        let p = WebGLProfile::chrome();
257        assert!(!p.extensions.is_empty());
258        assert!(p
259            .extensions
260            .contains(&"WEBGL_debug_renderer_info".to_string()));
261    }
262
263    #[test]
264    fn test_webgl_firefox_more_extensions_than_chrome() {
265        let ff = WebGLProfile::firefox();
266        let ch = WebGLProfile::chrome();
267        assert!(ff.extensions.len() > ch.extensions.len());
268    }
269
270    #[test]
271    fn test_webgl_same_max_viewport_dims() {
272        let ff = WebGLProfile::firefox();
273        let ch = WebGLProfile::chrome();
274        assert_eq!(ff.max_viewport_dims, ch.max_viewport_dims);
275        assert_eq!(ff.max_viewport_dims, [16384, 16384]);
276    }
277
278    #[test]
279    fn test_webgl_profile_clone() {
280        let p = WebGLProfile::firefox();
281        let cloned = p.clone();
282        assert_eq!(p.vendor, cloned.vendor);
283        assert_eq!(p.extensions, cloned.extensions);
284    }
285
286    #[test]
287    fn test_webgl_profile_debug_format() {
288        let p = WebGLProfile::chrome();
289        let debug = format!("{:?}", p);
290        assert!(debug.contains("WebGLProfile"));
291        assert!(debug.contains("vendor"));
292    }
293}