first commit

This commit is contained in:
gustavooth
2026-07-26 21:17:57 -03:00
commit 9de08ceef7
133 changed files with 38427 additions and 0 deletions
+296
View File
@@ -0,0 +1,296 @@
//! Construção do script de inicialização do Chromium.
//!
//! A função [`build_init_script`] retorna um JavaScript único, interpolado a
//! partir de uma [`Persona`], injetado via `Page.addScriptToEvaluateOnNewDocument`
//! antes de qualquer recurso da página. O script padroniza propriedades do
//! `navigator`, `screen` e os contextos de canvas/WebGL/áudio com os valores da
//! persona, aplicando perturbação determinística (seed fixa por sessão) sobre os
//! dados de canvas e áudio. O resultado é uma impressão digital estável dentro
//! da sessão e distinta entre sessões, sem bloquear as APIs correspondentes.
use crate::persona::Persona;
/// Constrói o script de inicialização para a sessão. A string resultante é
/// avaliada antes de qualquer outro script da página.
pub fn build_init_script(persona: &Persona) -> String {
let platform = persona.platform.js_platform();
let plugins = js_array_string(persona.plugins.iter().copied());
format!(
r###"(function() {{
'use strict';
const PERSONA = {{
platform: {platform_js},
languages: {languages},
locale: {locale_js},
hardwareConcurrency: {hw},
deviceMemory: {dm},
screenWidth: {sw},
screenHeight: {sh},
scale: {scale},
webglVendor: {webgl_vendor_js},
webglRenderer: {webgl_renderer_js},
audioSampleRate: {asr},
canvasSeed: {canvas_seed},
audioSeed: {audio_seed},
plugins: {plugins}
}};
// --- navigator ---
try {{
Object.defineProperty(navigator, 'webdriver', {{ get: () => false, configurable: true }});
}} catch (e) {{}}
try {{
Object.defineProperty(navigator, 'platform', {{ get: () => PERSONA.platform, configurable: true }});
}} catch (e) {{}}
try {{
Object.defineProperty(navigator, 'languages', {{ get: () => PERSONA.languages, configurable: true }});
}} catch (e) {{}}
try {{
Object.defineProperty(navigator, 'language', {{ get: () => PERSONA.locale, configurable: true }});
}} catch (e) {{}}
try {{
Object.defineProperty(navigator, 'hardwareConcurrency', {{ get: () => PERSONA.hardwareConcurrency, configurable: true }});
}} catch (e) {{}}
try {{
Object.defineProperty(navigator, 'deviceMemory', {{ get: () => PERSONA.deviceMemory, configurable: true }});
}} catch (e) {{}}
try {{
Object.defineProperty(navigator, 'plugins', {{ get: () => buildPlugins(PERSONA.plugins), configurable: true }});
}} catch (e) {{}}
try {{
Object.defineProperty(navigator, 'mimeTypes', {{ get: () => buildMimeTypes(), configurable: true }});
}} catch (e) {{}}
// --- screen ---
try {{
Object.defineProperty(screen, 'width', {{ get: () => PERSONA.screenWidth, configurable: true }});
Object.defineProperty(screen, 'height', {{ get: () => PERSONA.screenHeight, configurable: true }});
Object.defineProperty(screen, 'availWidth', {{ get: () => PERSONA.screenWidth, configurable: true }});
Object.defineProperty(screen, 'availHeight', {{ get: () => PERSONA.screenHeight - 40, configurable: true }});
Object.defineProperty(window, 'outerWidth', {{ get: () => PERSONA.screenWidth, configurable: true }});
Object.defineProperty(window, 'outerHeight', {{ get: () => PERSONA.screenHeight, configurable: true }});
}} catch (e) {{}}
// --- canvas: perturbação determinística de 1 bit por canal ---
function mulberry32(seed) {{
let a = seed >>> 0;
return function() {{
a = (a + 0x6D2B79F5) >>> 0;
let t = a;
t = Math.imul(t ^ (t >>> 15), t | 1);
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
}};
}}
const canvasRng = mulberry32(PERSONA.canvasSeed);
function perturbPixels(imageData) {{
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {{
const r = canvasRng();
if (r < 0.25) data[i] = data[i] ^ 1;
if (r > 0.33 && r < 0.58) data[i + 1] = data[i + 1] ^ 1;
if (r > 0.66 && r < 0.91) data[i + 2] = data[i + 2] ^ 1;
}}
return imageData;
}}
try {{
const origToDataURL = HTMLCanvasElement.prototype.toDataURL;
HTMLCanvasElement.prototype.toDataURL = function(...args) {{
const ctx = this.getContext('2d');
if (ctx) {{
try {{
const w = this.width, h = this.height;
const img = ctx.getImageData(0, 0, w, h);
perturbPixels(img);
ctx.putImageData(img, 0, 0);
}} catch (e) {{}}
}}
return origToDataURL.apply(this, args);
}};
}} catch (e) {{}}
try {{
const origGetImageData = CanvasRenderingContext2D.prototype.getImageData;
CanvasRenderingContext2D.prototype.getImageData = function(...args) {{
const img = origGetImageData.apply(this, args);
return perturbPixels(img);
}};
}} catch (e) {{}}
// --- WebGL: vendor e renderer coerentes com a persona ---
try {{
const overrideGetParameter = function(proto) {{
const orig = proto.getParameter;
proto.getParameter = function(parameter) {{
if (parameter === 37445) return PERSONA.webglVendor;
if (parameter === 37446) return PERSONA.webglRenderer;
return orig.apply(this, [parameter]);
}};
}};
if (typeof WebGLRenderingContext !== 'undefined') overrideGetParameter(WebGLRenderingContext.prototype);
if (typeof WebGL2RenderingContext !== 'undefined') overrideGetParameter(WebGL2RenderingContext.prototype);
}} catch (e) {{}}
// --- AudioContext: ruído determinístico de baixa amplitude ---
const audioRng = mulberry32(PERSONA.audioSeed);
try {{
const origGetByteFrequencyData = AnalyserNode.prototype.getByteFrequencyData;
AnalyserNode.prototype.getByteFrequencyData = function(array) {{
origGetByteFrequencyData.apply(this, [array]);
for (let i = 0; i < array.length; i++) {{
const n = (audioRng() - 0.5) * 2;
if (Math.abs(n) < 0.5) array[i] = Math.max(0, Math.min(255, array[i] + (n > 0 ? 1 : -1)));
}}
}};
}} catch (e) {{}}
try {{
const origGetChannelData = AudioBuffer.prototype.getChannelData;
AudioBuffer.prototype.getChannelData = function(channel) {{
const data = origGetChannelData.apply(this, [channel]);
const noise = new Float32Array(data.length);
for (let i = 0; i < data.length; i++) {{
noise[i] = (audioRng() - 0.5) * 1e-7;
}}
for (let i = 0; i < data.length; i++) {{
data[i] = data[i] + (i % 2 === 0 ? noise[i] : -noise[i]);
}}
return data;
}};
}} catch (e) {{}}
// --- helpers de plugins/mimeTypes ---
function makePlugin(name) {{
const plugin = Object.create(Plugin.prototype);
Object.defineProperties(plugin, {{
name: {{ value: name, enumerable: true }},
filename: {{ value: 'internal-pdf-viewer', enumerable: true }},
description: {{ value: 'Portable Document Format', enumerable: true }},
length: {{ value: 1, enumerable: true }},
0: {{ value: {{ type: 'application/pdf', suffixes: 'pdf', description: 'Portable Document Format' }}, enumerable: true }}
}});
return plugin;
}}
function buildPlugins(names) {{
const arr = Object.create(PluginArray.prototype);
names.forEach((name, idx) => {{
Object.defineProperty(arr, idx, {{ value: makePlugin(name), enumerable: true, configurable: true }});
}});
Object.defineProperty(arr, 'length', {{ value: names.length, enumerable: true }});
return arr;
}}
function makeMime() {{
const m = Object.create(MimeType.prototype);
Object.defineProperties(m, {{
type: {{ value: 'application/pdf', enumerable: true }},
suffixes: {{ value: 'pdf', enumerable: true }},
description: {{ value: 'Portable Document Format', enumerable: true }}
}});
return m;
}}
function buildMimeTypes() {{
const arr = Object.create(MimeTypeArray.prototype);
const m = makeMime();
Object.defineProperty(arr, 0, {{ value: m, enumerable: true, configurable: true }});
Object.defineProperty(arr, 'length', {{ value: 1, enumerable: true }});
return arr;
}}
}})();
"###,
platform_js = js_string(platform),
languages = js_array_string([persona.locale.as_str(), "en-US", "en"]),
locale_js = js_string(&persona.locale),
hw = persona.hardware_concurrency,
dm = persona.device_memory,
sw = persona.screen.0,
sh = persona.screen.1,
scale = persona.device_scale_factor,
webgl_vendor_js = js_string(persona.webgl_vendor),
webgl_renderer_js = js_string(persona.webgl_renderer),
asr = persona.audio_sample_rate,
canvas_seed = persona.canvas_seed,
audio_seed = persona.audio_seed,
plugins = plugins
)
}
fn js_string(value: &str) -> String {
let escaped = value
.replace('\\', "\\\\")
.replace('\'', "\\'")
.replace('\n', "\\n")
.replace('\r', "\\r");
format!("'{escaped}'")
}
fn js_array_string<I, S>(items: I) -> String
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
let parts: Vec<String> = items
.into_iter()
.map(|item| js_string(item.as_ref()))
.collect();
format!("[{}]", parts.join(", "))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::persona;
#[test]
fn build_init_script_is_non_empty_and_contains_persona_values() {
let persona = persona::generate("test", Some("br"));
let script = build_init_script(&persona);
assert!(script.len() > 1_000);
assert!(script.contains(&format!(
"hardwareConcurrency: {}",
persona.hardware_concurrency
)));
assert!(script.contains(&format!("deviceMemory: {}", persona.device_memory)));
assert!(script.contains(&format!("screenWidth: {}", persona.screen.0)));
assert!(script.contains(persona.webgl_vendor));
assert!(script.contains(persona.webgl_renderer));
assert!(script.contains(persona.platform.js_platform()));
}
/// Verifica sintaxe do JavaScript injetado via `node --check`. Ignorado
/// por padrão (exige node no PATH); rodar com `cargo test -- --ignored`.
#[test]
#[ignore]
fn build_init_script_is_valid_javascript_syntax() {
let persona = persona::generate("test", Some("br"));
let script = build_init_script(&persona);
let mut node = match std::process::Command::new("node")
.arg("--check")
.arg("-")
.stdin(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
{
Ok(child) => child,
Err(_) => {
eprintln!("node não disponível; pulando validação de sintaxe JS");
return;
}
};
use std::io::Write;
if let Some(stdin) = node.stdin.as_mut() {
stdin
.write_all(script.as_bytes())
.expect("escrita no stdin");
}
let output = node.wait_with_output().expect("esperar node terminar");
assert!(
output.status.success(),
"Sintaxe JS inválida:\n--- stderr ---\n{}\n--- início do script ---\n{}",
String::from_utf8_lossy(&output.stderr),
&script[..script.len().min(800)]
);
}
}