Skip to main content

stygian_browser/
webgl_noise.rs

1//! WebGL parameter spoofing and readPixels noise injection.
2//!
3//! Overrides `WebGL1` and `WebGL2` APIs to present a coherent, session-unique GPU
4//! identity and apply deterministic noise to `readPixels()` output.
5//!
6//! # Example
7//!
8//! ```
9//! use stygian_browser::webgl_noise::{webgl_noise_script, WebGlProfile};
10//! use stygian_browser::noise::{NoiseEngine, NoiseSeed};
11//!
12//! let engine = NoiseEngine::new(NoiseSeed::from(42_u64));
13//! let js = webgl_noise_script(&WebGlProfile::nvidia_rtx_3060(), &engine);
14//! assert!(js.contains("RTX 3060"));
15//! assert!(js.contains("__stygian_webgl_noise"));
16//! ```
17
18use serde::{Deserialize, Serialize};
19
20use crate::noise::NoiseEngine;
21
22// ---------------------------------------------------------------------------
23// ShaderPrecisionProfile
24// ---------------------------------------------------------------------------
25
26/// Shader precision format values for a GPU profile.
27///
28/// Matches the structure returned by `getShaderPrecisionFormat()`.
29#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
30pub struct ShaderPrecisionProfile {
31    /// High-float range exponent bits.
32    pub high_float_range_min: i32,
33    /// High-float range exponent bits.
34    pub high_float_range_max: i32,
35    /// High-float precision bits.
36    pub high_float_precision: i32,
37    /// Medium-float precision bits.
38    pub medium_float_precision: i32,
39    /// Low-float precision bits.
40    pub low_float_precision: i32,
41    /// High-int precision bits.
42    pub high_int_precision: i32,
43}
44
45impl Default for ShaderPrecisionProfile {
46    fn default() -> Self {
47        Self {
48            high_float_range_min: 127,
49            high_float_range_max: 127,
50            high_float_precision: 23,
51            medium_float_precision: 23,
52            low_float_precision: 23,
53            high_int_precision: 31,
54        }
55    }
56}
57
58// ---------------------------------------------------------------------------
59// ContextAttributes
60// ---------------------------------------------------------------------------
61
62/// WebGL context attributes returned by `getContextAttributes()`.
63#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
64pub struct ContextAttributes {
65    /// Alpha channel enabled.
66    pub alpha: bool,
67    /// Anti-aliasing enabled.
68    pub antialias: bool,
69    /// Depth buffer enabled.
70    pub depth: bool,
71    /// Fail if major performance caveat.
72    pub fail_if_major_performance_caveat: bool,
73    /// Power preference.
74    pub power_preference: String,
75    /// Premultiplied alpha.
76    pub premultiplied_alpha: bool,
77    /// Preserve drawing buffer.
78    pub preserve_drawing_buffer: bool,
79    /// Stencil buffer.
80    pub stencil: bool,
81    /// Desynchronized.
82    pub desynchronized: bool,
83}
84
85impl Default for ContextAttributes {
86    fn default() -> Self {
87        Self {
88            alpha: true,
89            antialias: true,
90            depth: true,
91            fail_if_major_performance_caveat: false,
92            power_preference: "default".to_string(),
93            premultiplied_alpha: true,
94            preserve_drawing_buffer: false,
95            stencil: false,
96            desynchronized: false,
97        }
98    }
99}
100
101// ---------------------------------------------------------------------------
102// WebGlProfile
103// ---------------------------------------------------------------------------
104
105/// A complete WebGL device identity profile.
106///
107/// Used to present a consistent, plausible GPU identity to fingerprinting scripts.
108///
109/// # Example
110///
111/// ```
112/// use stygian_browser::webgl_noise::WebGlProfile;
113///
114/// let profile = WebGlProfile::nvidia_rtx_3060();
115/// assert!(profile.renderer.contains("RTX 3060"));
116/// assert!(profile.max_texture_size >= 16384);
117/// ```
118#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
119pub struct WebGlProfile {
120    /// `UNMASKED_VENDOR_WEBGL` / `getParameter(GL_VENDOR)` value.
121    pub vendor: String,
122    /// `UNMASKED_RENDERER_WEBGL` / `getParameter(GL_RENDERER)` value.
123    pub renderer: String,
124    /// `MAX_TEXTURE_SIZE` in pixels.
125    pub max_texture_size: u32,
126    /// `MAX_VIEWPORT_DIMS` as `[width, height]`.
127    pub max_viewport_dims: (u32, u32),
128    /// `MAX_RENDERBUFFER_SIZE`.
129    pub max_renderbuffer_size: u32,
130    /// `MAX_VERTEX_ATTRIBS`.
131    pub max_vertex_attribs: u32,
132    /// `MAX_VARYING_VECTORS`.
133    pub max_varying_vectors: u32,
134    /// `MAX_FRAGMENT_UNIFORM_VECTORS`.
135    pub max_fragment_uniform_vectors: u32,
136    /// `MAX_VERTEX_UNIFORM_VECTORS`.
137    pub max_vertex_uniform_vectors: u32,
138    /// Ordered list of supported WebGL extensions.
139    pub extensions: Vec<String>,
140    /// Shader precision format.
141    pub shader_precision: ShaderPrecisionProfile,
142    /// Context attributes.
143    pub context_attributes: ContextAttributes,
144}
145
146impl WebGlProfile {
147    /// Return the NVIDIA RTX 3060 profile with all fields populated.
148    ///
149    /// # Example
150    ///
151    /// ```
152    /// use stygian_browser::webgl_noise::WebGlProfile;
153    /// let p = WebGlProfile::nvidia_rtx_3060();
154    /// assert!(p.renderer.contains("RTX 3060"));
155    /// assert_eq!(p.max_texture_size, 16384);
156    /// ```
157    #[must_use]
158    pub fn nvidia_rtx_3060() -> Self {
159        Self {
160            vendor: "Google Inc. (NVIDIA)".to_string(),
161            renderer: "ANGLE (NVIDIA, NVIDIA GeForce RTX 3060 Direct3D11 vs_5_0 ps_5_0, D3D11)"
162                .to_string(),
163            max_texture_size: 16384,
164            max_viewport_dims: (32768, 32768),
165            max_renderbuffer_size: 16384,
166            max_vertex_attribs: 16,
167            max_varying_vectors: 15,
168            max_fragment_uniform_vectors: 1024,
169            max_vertex_uniform_vectors: 4096,
170            extensions: default_extensions(),
171            shader_precision: ShaderPrecisionProfile::default(),
172            context_attributes: ContextAttributes::default(),
173        }
174    }
175
176    /// Return the NVIDIA GTX 1660 profile.
177    ///
178    /// # Example
179    ///
180    /// ```
181    /// use stygian_browser::webgl_noise::WebGlProfile;
182    /// let p = WebGlProfile::nvidia_gtx_1660();
183    /// assert!(p.renderer.contains("GTX 1660"));
184    /// ```
185    #[must_use]
186    pub fn nvidia_gtx_1660() -> Self {
187        Self {
188            vendor: "Google Inc. (NVIDIA)".to_string(),
189            renderer:
190                "ANGLE (NVIDIA, NVIDIA GeForce GTX 1660 SUPER Direct3D11 vs_5_0 ps_5_0, D3D11)"
191                    .to_string(),
192            max_texture_size: 16384,
193            max_viewport_dims: (32768, 32768),
194            max_renderbuffer_size: 16384,
195            max_vertex_attribs: 16,
196            max_varying_vectors: 15,
197            max_fragment_uniform_vectors: 1024,
198            max_vertex_uniform_vectors: 4096,
199            extensions: default_extensions(),
200            shader_precision: ShaderPrecisionProfile::default(),
201            context_attributes: ContextAttributes::default(),
202        }
203    }
204
205    /// Return the AMD RX 6700 profile.
206    ///
207    /// # Example
208    ///
209    /// ```
210    /// use stygian_browser::webgl_noise::WebGlProfile;
211    /// let p = WebGlProfile::amd_rx_6700();
212    /// assert!(p.renderer.contains("RX 6700"));
213    /// ```
214    #[must_use]
215    pub fn amd_rx_6700() -> Self {
216        Self {
217            vendor: "Google Inc. (AMD)".to_string(),
218            renderer: "ANGLE (AMD, AMD Radeon RX 6700 XT Direct3D11 vs_5_0 ps_5_0, D3D11)"
219                .to_string(),
220            max_texture_size: 16384,
221            max_viewport_dims: (32768, 32768),
222            max_renderbuffer_size: 16384,
223            max_vertex_attribs: 16,
224            max_varying_vectors: 15,
225            max_fragment_uniform_vectors: 1024,
226            max_vertex_uniform_vectors: 4096,
227            extensions: default_extensions(),
228            shader_precision: ShaderPrecisionProfile::default(),
229            context_attributes: ContextAttributes::default(),
230        }
231    }
232
233    /// Return the Intel UHD 630 profile (integrated graphics).
234    ///
235    /// # Example
236    ///
237    /// ```
238    /// use stygian_browser::webgl_noise::WebGlProfile;
239    /// let p = WebGlProfile::intel_uhd_630();
240    /// assert!(p.renderer.contains("UHD Graphics 630"));
241    /// ```
242    #[must_use]
243    pub fn intel_uhd_630() -> Self {
244        Self {
245            vendor: "Google Inc. (Intel)".to_string(),
246            renderer: "ANGLE (Intel, Intel(R) UHD Graphics 630 Direct3D11 vs_5_0 ps_5_0, D3D11)"
247                .to_string(),
248            max_texture_size: 8192,
249            max_viewport_dims: (16384, 16384),
250            max_renderbuffer_size: 8192,
251            max_vertex_attribs: 16,
252            max_varying_vectors: 15,
253            max_fragment_uniform_vectors: 1024,
254            max_vertex_uniform_vectors: 4096,
255            extensions: default_extensions(),
256            shader_precision: ShaderPrecisionProfile::default(),
257            context_attributes: ContextAttributes::default(),
258        }
259    }
260
261    /// Assert basic internal consistency: texture size ≤ viewport dims, etc.
262    ///
263    /// # Example
264    ///
265    /// ```
266    /// use stygian_browser::webgl_noise::WebGlProfile;
267    /// let p = WebGlProfile::nvidia_rtx_3060();
268    /// p.assert_consistent();
269    /// ```
270    pub fn assert_consistent(&self) {
271        assert!(
272            self.max_texture_size <= self.max_viewport_dims.0,
273            "max_texture_size must be <= max_viewport_dims.0"
274        );
275        assert!(
276            self.max_texture_size <= self.max_viewport_dims.1,
277            "max_texture_size must be <= max_viewport_dims.1"
278        );
279        assert!(
280            self.max_renderbuffer_size <= self.max_texture_size,
281            "max_renderbuffer_size must be <= max_texture_size"
282        );
283    }
284}
285
286fn default_extensions() -> Vec<String> {
287    [
288        "ANGLE_instanced_arrays",
289        "EXT_blend_minmax",
290        "EXT_clip_control",
291        "EXT_color_buffer_half_float",
292        "EXT_depth_clamp",
293        "EXT_disjoint_timer_query",
294        "EXT_float_blend",
295        "EXT_frag_depth",
296        "EXT_shader_texture_lod",
297        "EXT_texture_compression_bptc",
298        "EXT_texture_compression_rgtc",
299        "EXT_texture_filter_anisotropic",
300        "EXT_sRGB",
301        "KHR_parallel_shader_compile",
302        "OES_element_index_uint",
303        "OES_fbo_render_mipmap",
304        "OES_standard_derivatives",
305        "OES_texture_float",
306        "OES_texture_float_linear",
307        "OES_texture_half_float",
308        "OES_texture_half_float_linear",
309        "OES_vertex_array_object",
310        "WEBGL_color_buffer_float",
311        "WEBGL_compressed_texture_s3tc",
312        "WEBGL_compressed_texture_s3tc_srgb",
313        "WEBGL_debug_renderer_info",
314        "WEBGL_debug_shaders",
315        "WEBGL_depth_texture",
316        "WEBGL_draw_buffers",
317        "WEBGL_lose_context",
318        "WEBGL_multi_draw",
319        "WEBGL_polygon_mode",
320    ]
321    .iter()
322    .map(|s| (*s).to_string())
323    .collect()
324}
325
326// ---------------------------------------------------------------------------
327// Script generation
328// ---------------------------------------------------------------------------
329
330/// Generate the WebGL noise injection script for a given profile and engine.
331///
332/// # Example
333///
334/// ```
335/// use stygian_browser::webgl_noise::{webgl_noise_script, WebGlProfile};
336/// use stygian_browser::noise::{NoiseEngine, NoiseSeed};
337///
338/// let js = webgl_noise_script(&WebGlProfile::nvidia_rtx_3060(), &NoiseEngine::new(NoiseSeed::from(1)));
339/// assert!(js.contains("getParameter"));
340/// assert!(js.contains("readPixels"));
341/// ```
342#[must_use]
343#[allow(clippy::too_many_lines)]
344pub fn webgl_noise_script(profile: &WebGlProfile, engine: &NoiseEngine) -> String {
345    let noise_fn = engine.js_noise_fn();
346    let vendor = &profile.vendor;
347    let renderer = &profile.renderer;
348    let max_tex = profile.max_texture_size;
349    let vp_w = profile.max_viewport_dims.0;
350    let vp_h = profile.max_viewport_dims.1;
351    let max_rb = profile.max_renderbuffer_size;
352    let max_va = profile.max_vertex_attribs;
353    let max_varying_vectors = profile.max_varying_vectors;
354    let max_fragment_uniform_vectors = profile.max_fragment_uniform_vectors;
355    let max_vertex_uniform_vectors = profile.max_vertex_uniform_vectors;
356
357    let exts_json = {
358        let items: Vec<String> = profile
359            .extensions
360            .iter()
361            .map(|e| format!("{e:?}"))
362            .collect();
363        format!("[{}]", items.join(", "))
364    };
365
366    let sp = &profile.shader_precision;
367    let ca = &profile.context_attributes;
368    let ca_power = &ca.power_preference;
369
370    format!(
371        r"(function() {{
372  'use strict';
373
374  // ── Noise helpers ──────────────────────────────────────────────────────
375  {noise_fn}
376
377  // ── WebGL constants ────────────────────────────────────────────────────
378  const _VENDOR   = 0x1F00;
379  const _RENDERER = 0x1F01;
380  const _UNMASKED_VENDOR   = 0x9245;
381  const _UNMASKED_RENDERER = 0x9246;
382  const _MAX_TEXTURE_SIZE            = 0x0D33;
383  const _MAX_VIEWPORT_DIMS           = 0x0D3A;
384  const _MAX_RENDERBUFFER_SIZE       = 0x84E8;
385  const _MAX_VERTEX_ATTRIBS          = 0x8869;
386  const _MAX_VARYING_VECTORS         = 0x8DFC;
387  const _MAX_FRAGMENT_UNIFORM_VECTORS = 0x8DFD;
388  const _MAX_VERTEX_UNIFORM_VECTORS  = 0x8DFB;
389
390  const _PROFILE_VENDOR   = {vendor:?};
391  const _PROFILE_RENDERER = {renderer:?};
392  const _EXTENSIONS = {exts_json};
393
394  // ── Spoof toString ─────────────────────────────────────────────────────
395  function _nts(name) {{ return function toString() {{ return 'function ' + name + '() {{ [native code] }}'; }}; }}
396  function _def(obj, prop, fn) {{
397    fn.toString = _nts(prop);
398    Object.defineProperty(obj, prop, {{ value: fn, writable: false, configurable: false, enumerable: false }});
399  }}
400
401  // ── Patch both WebGL1 and WebGL2 ────────────────────────────────────────
402  [WebGLRenderingContext, (typeof WebGL2RenderingContext !== 'undefined' ? WebGL2RenderingContext : null)]
403    .filter(Boolean)
404    .forEach(function(Ctx) {{
405      const proto = Ctx.prototype;
406
407      // getParameter
408      const _origGP = proto.getParameter;
409      _def(proto, 'getParameter', function getParameter(pname) {{
410        switch (pname) {{
411          case _VENDOR:             return _PROFILE_VENDOR;
412          case _RENDERER:           return _PROFILE_RENDERER;
413          case _UNMASKED_VENDOR:    return _PROFILE_VENDOR;
414          case _UNMASKED_RENDERER:  return _PROFILE_RENDERER;
415          case _MAX_TEXTURE_SIZE:   return {max_tex};
416          case _MAX_VIEWPORT_DIMS:  return new Int32Array([{vp_w}, {vp_h}]);
417          case _MAX_RENDERBUFFER_SIZE: return {max_rb};
418          case _MAX_VERTEX_ATTRIBS: return {max_va};
419          case _MAX_VARYING_VECTORS: return {max_varying_vectors};
420          case _MAX_FRAGMENT_UNIFORM_VECTORS: return {max_fragment_uniform_vectors};
421          case _MAX_VERTEX_UNIFORM_VECTORS: return {max_vertex_uniform_vectors};
422          default: return _origGP.call(this, pname);
423        }}
424      }});
425
426      // getSupportedExtensions
427      _def(proto, 'getSupportedExtensions', function getSupportedExtensions() {{
428        return _EXTENSIONS.slice();
429      }});
430
431      // getExtension
432      const _origGE = proto.getExtension;
433      _def(proto, 'getExtension', function getExtension(name) {{
434        if (!_EXTENSIONS.includes(name)) return null;
435        return _origGE.call(this, name);
436      }});
437
438      // getShaderPrecisionFormat
439      _def(proto, 'getShaderPrecisionFormat', function getShaderPrecisionFormat(shaderType, precisionType) {{
440        // HIGH_FLOAT = 0x8DF2, MEDIUM_FLOAT = 0x8DF1, LOW_FLOAT = 0x8DF0
441        // HIGH_INT = 0x8DF5, MEDIUM_INT = 0x8DF4, LOW_INT = 0x8DF3
442        const HIGH_FLOAT = 0x8DF2, MEDIUM_FLOAT = 0x8DF1, HIGH_INT = 0x8DF5;
443        if (precisionType === HIGH_FLOAT) {{
444          return {{ rangeMin: {sp_hfrm}, rangeMax: {sp_hfrx}, precision: {sp_hfp} }};
445        }} else if (precisionType === MEDIUM_FLOAT) {{
446          return {{ rangeMin: 127, rangeMax: 127, precision: {sp_mfp} }};
447        }} else if (precisionType === HIGH_INT) {{
448          return {{ rangeMin: 31, rangeMax: 30, precision: {sp_hip} }};
449        }}
450        return {{ rangeMin: 1, rangeMax: 1, precision: 8 }};
451      }});
452
453      // getContextAttributes
454      _def(proto, 'getContextAttributes', function getContextAttributes() {{
455        return {{
456          alpha: {ca_alpha},
457          antialias: {ca_antialias},
458          depth: {ca_depth},
459          failIfMajorPerformanceCaveat: {ca_fail},
460          powerPreference: {ca_power:?},
461          premultipliedAlpha: {ca_pma},
462          preserveDrawingBuffer: {ca_pdb},
463          stencil: {ca_stencil},
464          desynchronized: {ca_desync},
465        }};
466      }});
467
468      // readPixels — apply webgl noise to output
469      const _origRP = proto.readPixels;
470      _def(proto, 'readPixels', function readPixels(x, y, width, height, format, type, pixels) {{
471        _origRP.call(this, x, y, width, height, format, type, pixels);
472        if (pixels instanceof Uint8Array || pixels instanceof Uint8ClampedArray) {{
473          for (let i = 0; i < pixels.length; i += 4) {{
474            const px = (x + ((i / 4) % width)) >>> 0;
475            const py = (y + (((i / 4) / width) | 0)) >>> 0;
476            if (pixels[i] === 0 && pixels[i+1] === 0 && pixels[i+2] === 0 && pixels[i+3] === 0) continue;
477            const [dr, dg, db, da] = __stygian_webgl_noise('readPixels', px, py);
478            pixels[i]   = Math.max(0, Math.min(255, pixels[i]   + dr));
479            pixels[i+1] = Math.max(0, Math.min(255, pixels[i+1] + dg));
480            pixels[i+2] = Math.max(0, Math.min(255, pixels[i+2] + db));
481            pixels[i+3] = Math.max(0, Math.min(255, pixels[i+3] + da));
482          }}
483        }}
484      }});
485    }});
486}})();
487",
488        noise_fn = noise_fn,
489        vendor = vendor,
490        renderer = renderer,
491        exts_json = exts_json,
492        max_tex = max_tex,
493        vp_w = vp_w,
494        vp_h = vp_h,
495        max_rb = max_rb,
496        max_va = max_va,
497        max_varying_vectors = max_varying_vectors,
498        max_fragment_uniform_vectors = max_fragment_uniform_vectors,
499        max_vertex_uniform_vectors = max_vertex_uniform_vectors,
500        sp_hfrm = sp.high_float_range_min,
501        sp_hfrx = sp.high_float_range_max,
502        sp_hfp = sp.high_float_precision,
503        sp_mfp = sp.medium_float_precision,
504        sp_hip = sp.high_int_precision,
505        ca_alpha = ca.alpha,
506        ca_antialias = ca.antialias,
507        ca_depth = ca.depth,
508        ca_fail = ca.fail_if_major_performance_caveat,
509        ca_power = ca_power,
510        ca_pma = ca.premultiplied_alpha,
511        ca_pdb = ca.preserve_drawing_buffer,
512        ca_stencil = ca.stencil,
513        ca_desync = ca.desynchronized,
514    )
515}
516
517// ---------------------------------------------------------------------------
518// Tests
519// ---------------------------------------------------------------------------
520
521#[cfg(test)]
522mod tests {
523    use super::*;
524    use crate::noise::{NoiseEngine, NoiseSeed};
525
526    fn eng() -> NoiseEngine {
527        NoiseEngine::new(NoiseSeed::from(1_u64))
528    }
529
530    #[test]
531    fn all_profiles_consistent() {
532        WebGlProfile::nvidia_rtx_3060().assert_consistent();
533        WebGlProfile::nvidia_gtx_1660().assert_consistent();
534        WebGlProfile::amd_rx_6700().assert_consistent();
535        WebGlProfile::intel_uhd_630().assert_consistent();
536    }
537
538    #[test]
539    fn script_contains_webgl_overrides() {
540        let js = webgl_noise_script(&WebGlProfile::nvidia_rtx_3060(), &eng());
541        assert!(js.contains("getParameter"), "missing getParameter");
542        assert!(
543            js.contains("getSupportedExtensions"),
544            "missing getSupportedExtensions"
545        );
546        assert!(js.contains("getExtension"), "missing getExtension");
547        assert!(
548            js.contains("getShaderPrecisionFormat"),
549            "missing getShaderPrecisionFormat"
550        );
551        assert!(
552            js.contains("getContextAttributes"),
553            "missing getContextAttributes"
554        );
555        assert!(js.contains("readPixels"), "missing readPixels");
556    }
557
558    #[test]
559    fn script_contains_noise_reference() {
560        let js = webgl_noise_script(&WebGlProfile::nvidia_rtx_3060(), &eng());
561        assert!(
562            js.contains("__stygian_webgl_noise"),
563            "missing webgl noise fn"
564        );
565    }
566
567    #[test]
568    fn script_contains_native_tostring() {
569        let js = webgl_noise_script(&WebGlProfile::nvidia_rtx_3060(), &eng());
570        assert!(js.contains("[native code]"), "missing toString spoof");
571    }
572
573    #[test]
574    fn profile_serde_round_trip() {
575        let p = WebGlProfile::nvidia_rtx_3060();
576        let json_result = serde_json::to_string(&p);
577        assert!(json_result.is_ok(), "serialize failed: {json_result:?}");
578        let Ok(json) = json_result else {
579            return;
580        };
581        let back_result: Result<WebGlProfile, _> = serde_json::from_str(&json);
582        assert!(back_result.is_ok(), "deserialize failed: {back_result:?}");
583        let Ok(back) = back_result else {
584            return;
585        };
586        assert_eq!(back.vendor, p.vendor);
587        assert_eq!(back.renderer, p.renderer);
588        assert_eq!(back.max_texture_size, p.max_texture_size);
589        assert_eq!(back.extensions.len(), p.extensions.len());
590    }
591
592    #[test]
593    fn script_contains_renderer_string() {
594        let p = WebGlProfile::nvidia_rtx_3060();
595        let js = webgl_noise_script(&p, &eng());
596        assert!(js.contains("RTX 3060"), "renderer not in script");
597    }
598}