1119 Zeilen
52 KiB
Python
1119 Zeilen
52 KiB
Python
# browser/fingerprint_protection.py
|
|
|
|
"""
|
|
Schutz vor Browser-Fingerprinting - Erweiterte Methoden zum Schutz vor verschiedenen Fingerprinting-Techniken
|
|
"""
|
|
|
|
import random
|
|
import logging
|
|
import json
|
|
import hashlib
|
|
from datetime import datetime
|
|
from typing import Dict, List, Any, Optional, Tuple
|
|
|
|
logger = logging.getLogger("fingerprint_protection")
|
|
|
|
class FingerprintProtection:
|
|
"""
|
|
Bietet erweiterte Schutzmaßnahmen gegen verschiedene Browser-Fingerprinting-Techniken.
|
|
Kann mit dem PlaywrightManager integriert werden, um die Anonymität zu verbessern.
|
|
"""
|
|
|
|
def __init__(self, context=None, stealth_config=None, fingerprint_config=None):
|
|
"""
|
|
Initialisiert den Fingerprint-Schutz.
|
|
|
|
Args:
|
|
context: Der Browser-Kontext, auf den die Schutzmaßnahmen angewendet werden sollen
|
|
stealth_config: Konfigurationseinstellungen für das Stealth-Verhalten
|
|
fingerprint_config: Optional - BrowserFingerprint object for account-bound fingerprints
|
|
"""
|
|
self.context = context
|
|
self.stealth_config = stealth_config or {}
|
|
self.fingerprint_config = fingerprint_config
|
|
self.scripts = []
|
|
self.noise_level = self.stealth_config.get("noise_level", 0.5) # 0.0-1.0
|
|
|
|
# Use fingerprint config if provided, otherwise use defaults
|
|
if self.fingerprint_config:
|
|
self._init_from_fingerprint_config()
|
|
else:
|
|
self._init_from_defaults()
|
|
|
|
# Schutzmaßnahmen initialisieren
|
|
self._init_protections()
|
|
|
|
def _init_from_fingerprint_config(self):
|
|
"""Initialize values from BrowserFingerprint object"""
|
|
fp = self.fingerprint_config
|
|
|
|
# Use static components if available
|
|
if fp.static_components:
|
|
sc = fp.static_components
|
|
self.defaults = {
|
|
"webgl_vendor": sc.gpu_vendor,
|
|
"webgl_renderer": sc.gpu_model,
|
|
"canvas_noise": False, # Disabled for video compatibility
|
|
"audio_noise": False, # Disabled for video compatibility
|
|
"webgl_noise": False, # Disabled for video compatibility
|
|
"hardware_concurrency": fp.hardware_config.hardware_concurrency,
|
|
"device_memory": fp.hardware_config.device_memory,
|
|
"timezone_id": fp.timezone,
|
|
"platform": fp.navigator_props.platform,
|
|
"user_agent": fp.navigator_props.user_agent,
|
|
"languages": fp.navigator_props.languages,
|
|
"screen_resolution": fp.hardware_config.screen_resolution,
|
|
"fonts": fp.font_list,
|
|
"rotation_seed": fp.rotation_seed
|
|
}
|
|
else:
|
|
# Fallback to direct fingerprint values
|
|
self.defaults = {
|
|
"webgl_vendor": fp.webgl_vendor,
|
|
"webgl_renderer": fp.webgl_renderer,
|
|
"canvas_noise": False, # Disabled for video compatibility
|
|
"audio_noise": False, # Disabled for video compatibility
|
|
"webgl_noise": False, # Disabled for video compatibility
|
|
"hardware_concurrency": fp.hardware_config.hardware_concurrency,
|
|
"device_memory": fp.hardware_config.device_memory,
|
|
"timezone_id": fp.timezone,
|
|
"platform": fp.navigator_props.platform,
|
|
"user_agent": fp.navigator_props.user_agent,
|
|
"languages": fp.navigator_props.languages,
|
|
"screen_resolution": fp.hardware_config.screen_resolution,
|
|
"fonts": fp.font_list,
|
|
"rotation_seed": fp.rotation_seed
|
|
}
|
|
|
|
# Override with stealth config if provided
|
|
for key, value in self.stealth_config.items():
|
|
if key in self.defaults:
|
|
self.defaults[key] = value
|
|
|
|
def _init_from_defaults(self):
|
|
"""Initialize with default values"""
|
|
# Standardwerte für Fingerprinting-Schutz
|
|
self.defaults = {
|
|
"webgl_vendor": "Google Inc. (Intel)",
|
|
"webgl_renderer": "Intel Iris OpenGL Engine",
|
|
"canvas_noise": False, # Disabled for video compatibility
|
|
"audio_noise": False, # Disabled for video compatibility
|
|
"webgl_noise": False, # Disabled for video compatibility
|
|
"hardware_concurrency": 8,
|
|
"device_memory": 8,
|
|
"timezone_id": "Europe/Berlin",
|
|
"rotation_seed": None
|
|
}
|
|
|
|
# Einstellungen mit benutzerdefinierten Werten überschreiben
|
|
for key, value in self.stealth_config.items():
|
|
if key in self.defaults:
|
|
self.defaults[key] = value
|
|
|
|
def set_context(self, context):
|
|
"""Setzt den Browser-Kontext nach der Initialisierung."""
|
|
self.context = context
|
|
|
|
def _init_protections(self):
|
|
"""Initialisiert alle Fingerprint-Schutzmaßnahmen."""
|
|
self._init_canvas_protection()
|
|
self._init_webgl_protection()
|
|
self._init_audio_protection()
|
|
self._init_navigator_protection()
|
|
self._init_misc_protections()
|
|
self._init_font_protection()
|
|
|
|
def _init_canvas_protection(self):
|
|
"""
|
|
Initialisiert den Schutz gegen Canvas-Fingerprinting.
|
|
Dies modifiziert das Canvas-Element, um leicht abweichende Werte zurückzugeben.
|
|
"""
|
|
# Get deterministic seed for canvas noise
|
|
canvas_seed = self._get_deterministic_noise_seed('canvas')
|
|
|
|
script = f"""
|
|
() => {{
|
|
// Originalmethoden speichern
|
|
const originalGetImageData = CanvasRenderingContext2D.prototype.getImageData;
|
|
const originalToDataURL = HTMLCanvasElement.prototype.toDataURL;
|
|
const originalToBlob = HTMLCanvasElement.prototype.toBlob;
|
|
|
|
// Deterministic random generator
|
|
let seed = {canvas_seed};
|
|
const deterministicRandom = () => {{
|
|
seed = (seed * 9301 + 49297) % 233280;
|
|
return seed / 233280;
|
|
}};
|
|
|
|
// Funktion zum Hinzufügen von Rauschen zu Bilddaten
|
|
const addNoise = (data, noise) => {{
|
|
const noiseFactor = noise || 0.03; // Standardwert für das Rauschlevel
|
|
|
|
// Nur auf sehr große Canvas anwenden, um Video-Canvas nicht zu stören
|
|
if (data.width > 200 && data.height > 200) {{
|
|
const pixelCount = data.width * data.height * 4; // RGBA-Kanäle
|
|
|
|
// Präzise, aber wenig wahrnehmbare Änderungen
|
|
const minPixels = Math.max(10, Math.floor(pixelCount * 0.005)); // Mindestens 10 Pixel
|
|
const maxPixels = Math.min(100, Math.floor(pixelCount * 0.01)); // Höchstens 100 Pixel
|
|
|
|
// Deterministische Anzahl an Pixeln auswählen
|
|
const pixelsToModify = Math.floor(deterministicRandom() * (maxPixels - minPixels)) + minPixels;
|
|
|
|
// Pixel modifizieren
|
|
for (let i = 0; i < pixelsToModify; i++) {{
|
|
// Deterministischen Pixel auswählen
|
|
const offset = Math.floor(deterministicRandom() * pixelCount / 4) * 4;
|
|
|
|
// Nur RGB modifizieren, Alpha-Kanal unverändert lassen
|
|
for (let j = 0; j < 3; j++) {{
|
|
// Subtile Änderungen hinzufügen (±1-2)
|
|
const mod = Math.floor(deterministicRandom() * 3) - 1;
|
|
data.data[offset + j] = Math.max(0, Math.min(255, data.data[offset + j] + mod));
|
|
}}
|
|
}}
|
|
}}
|
|
|
|
return data;
|
|
}};
|
|
|
|
// getImageData überschreiben
|
|
CanvasRenderingContext2D.prototype.getImageData = function() {{
|
|
const imageData = originalGetImageData.apply(this, arguments);
|
|
return addNoise(imageData, {self.noise_level * 0.1});
|
|
}};
|
|
|
|
// toDataURL überschreiben
|
|
HTMLCanvasElement.prototype.toDataURL = function() {{
|
|
// Temporäre Modifikation für getImageData
|
|
const tempGetImageData = CanvasRenderingContext2D.prototype.getImageData;
|
|
CanvasRenderingContext2D.prototype.getImageData = originalGetImageData;
|
|
|
|
// toDataURL aufrufen
|
|
let dataURL = originalToDataURL.apply(this, arguments);
|
|
|
|
// Nur bei sehr großen Canvas und nicht bei Video-Canvas
|
|
if (this.width > 200 && this.height > 200 && !this.hasAttribute('data-fingerprint-protect-ignore') && !this.hasAttribute('data-video-canvas')) {{
|
|
// Rauschen in binären Teil des DataURL einfügen
|
|
const parts = dataURL.split(',');
|
|
if (parts.length === 2) {{
|
|
// Binäre Daten dekodieren
|
|
const binary = atob(parts[1]);
|
|
// Geringfügige Änderungen bei etwa 0,1% der Bytes
|
|
let modifiedBinary = '';
|
|
for (let i = 0; i < binary.length; i++) {{
|
|
if (Math.random() < 0.001 * {self.noise_level}) {{
|
|
// Byte leicht ändern
|
|
const byte = binary.charCodeAt(i);
|
|
const mod = Math.floor(Math.random() * 3) - 1;
|
|
modifiedBinary += String.fromCharCode(Math.max(0, Math.min(255, byte + mod)));
|
|
}} else {{
|
|
modifiedBinary += binary[i];
|
|
}}
|
|
}}
|
|
// Binäre Daten wieder kodieren
|
|
dataURL = parts[0] + ',' + btoa(modifiedBinary);
|
|
}}
|
|
}}
|
|
|
|
// getImageData wiederherstellen
|
|
CanvasRenderingContext2D.prototype.getImageData = tempGetImageData;
|
|
|
|
return dataURL;
|
|
}};
|
|
|
|
// toBlob überschreiben
|
|
HTMLCanvasElement.prototype.toBlob = function(callback) {{
|
|
// Original toBlob aufrufen
|
|
originalToBlob.apply(this, [function(blob) {{
|
|
// Nur bei sehr großen Canvas und nicht bei Video-Canvas
|
|
if (this.width > 200 && this.height > 200 && !this.hasAttribute('data-fingerprint-protect-ignore') && !this.hasAttribute('data-video-canvas')) {{
|
|
// Blob zu ArrayBuffer konvertieren
|
|
const reader = new FileReader();
|
|
reader.onload = function() {{
|
|
const arrayBuffer = reader.result;
|
|
const array = new Uint8Array(arrayBuffer);
|
|
|
|
// Geringfügige Änderungen bei etwa 0,1% der Bytes
|
|
for (let i = 0; i < array.length; i++) {{
|
|
if (Math.random() < 0.001 * {self.noise_level}) {{
|
|
// Byte leicht ändern
|
|
const mod = Math.floor(Math.random() * 3) - 1;
|
|
array[i] = Math.max(0, Math.min(255, array[i] + mod));
|
|
}}
|
|
}}
|
|
|
|
// Neuen Blob erstellen
|
|
const modifiedBlob = new Blob([array], {{type: blob.type}});
|
|
callback(modifiedBlob);
|
|
}};
|
|
reader.readAsArrayBuffer(blob);
|
|
}} else {{
|
|
// Unveränderten Blob zurückgeben
|
|
callback(blob);
|
|
}}
|
|
}}.bind(this)]);
|
|
}};
|
|
}}
|
|
"""
|
|
|
|
self.scripts.append(script)
|
|
|
|
def _init_webgl_protection(self):
|
|
"""
|
|
Initialisiert den Schutz gegen WebGL-Fingerprinting.
|
|
Dies modifiziert WebGL-spezifische Werte, die für Fingerprinting verwendet werden.
|
|
"""
|
|
webgl_vendor = self.defaults["webgl_vendor"]
|
|
webgl_renderer = self.defaults["webgl_renderer"]
|
|
|
|
script = f"""
|
|
() => {{
|
|
// WebGL Vendor und Renderer spoofen
|
|
const getParameterProxies = [
|
|
WebGLRenderingContext.prototype,
|
|
WebGL2RenderingContext.prototype
|
|
];
|
|
|
|
getParameterProxies.forEach(contextPrototype => {{
|
|
if (!contextPrototype) return;
|
|
|
|
const originalGetParameter = contextPrototype.getParameter;
|
|
contextPrototype.getParameter = function(parameter) {{
|
|
// WebGL Vendor (VENDOR)
|
|
if (parameter === 0x1F00) {{
|
|
return "{webgl_vendor}";
|
|
}}
|
|
|
|
// WebGL Renderer (RENDERER)
|
|
if (parameter === 0x1F01) {{
|
|
return "{webgl_renderer}";
|
|
}}
|
|
|
|
// Unshaded Language Version (SHADING_LANGUAGE_VERSION)
|
|
if (parameter === 0x8B8C) {{
|
|
const originalValue = originalGetParameter.call(this, parameter);
|
|
|
|
// Subtile Änderungen an der Version
|
|
const versionMatch = originalValue.match(/^WebGL GLSL ES ([0-9]\\.[0-9][0-9])/);
|
|
if (versionMatch) {{
|
|
return originalValue.replace(versionMatch[1],
|
|
(parseFloat(versionMatch[1]) + (Math.random() * 0.01 - 0.005)).toFixed(2));
|
|
}}
|
|
}}
|
|
|
|
// VERSION
|
|
if (parameter === 0x1F02) {{
|
|
const originalValue = originalGetParameter.call(this, parameter);
|
|
|
|
// Subtile Änderungen an der Version
|
|
const versionMatch = originalValue.match(/^WebGL ([0-9]\\.[0-9])/);
|
|
if (versionMatch) {{
|
|
return originalValue.replace(versionMatch[1],
|
|
(parseFloat(versionMatch[1]) + (Math.random() * 0.01 - 0.005)).toFixed(1));
|
|
}}
|
|
}}
|
|
|
|
return originalGetParameter.apply(this, arguments);
|
|
}};
|
|
}});
|
|
|
|
// WebGL Vertex und Fragment Shader spoofen
|
|
const shaderSourceProxies = [
|
|
WebGLRenderingContext.prototype,
|
|
WebGL2RenderingContext.prototype
|
|
];
|
|
|
|
shaderSourceProxies.forEach(contextPrototype => {{
|
|
if (!contextPrototype) return;
|
|
|
|
const originalShaderSource = contextPrototype.shaderSource;
|
|
contextPrototype.shaderSource = function(shader, source) {{
|
|
// Füge geringfügige Unterschiede in Kommentaren ein, ohne die Funktionalität zu beeinträchtigen
|
|
if (source.indexOf('//') !== -1) {{
|
|
// Zufälligen Kommentar leicht modifizieren
|
|
source = source.replace(/\\/\\/(.*?)\\n/g, (match, comment) => {{
|
|
if (Math.random() < 0.1) {{
|
|
// Füge ein Leerzeichen oder einen Bindestrich hinzu oder entferne eines
|
|
const modifications = [
|
|
' ', '-', '', ' '
|
|
];
|
|
const mod = modifications[Math.floor(Math.random() * modifications.length)];
|
|
return `//` + comment + mod + `\\n`;
|
|
}}
|
|
return match;
|
|
}});
|
|
}}
|
|
|
|
// Ersetze bestimmte Whitespace-Muster
|
|
source = source.replace(/\\s{2,}/g, match => {{
|
|
if (Math.random() < 0.05) {{
|
|
return ' '.repeat(match.length + (Math.random() < 0.5 ? 1 : -1));
|
|
}}
|
|
return match;
|
|
}});
|
|
|
|
return originalShaderSource.call(this, shader, source);
|
|
}};
|
|
}});
|
|
|
|
// Canvas-Kontext spoofen
|
|
const getContextProxies = [
|
|
HTMLCanvasElement.prototype
|
|
];
|
|
|
|
getContextProxies.forEach(canvasPrototype => {{
|
|
const originalGetContext = canvasPrototype.getContext;
|
|
canvasPrototype.getContext = function(contextType, contextAttributes) {{
|
|
const context = originalGetContext.apply(this, arguments);
|
|
|
|
if (context && (contextType === 'webgl' || contextType === 'experimental-webgl' || contextType === 'webgl2')) {{
|
|
// WebGL-Noise nur wenn explizit aktiviert (für Video-Kompatibilität deaktiviert)
|
|
if ({str(self.defaults["webgl_noise"]).lower()}) {{
|
|
// Zufällige Modifikation für MAX_VERTEX_UNIFORM_VECTORS
|
|
const MAX_VERTEX_UNIFORM_VECTORS = context.getParameter(context.MAX_VERTEX_UNIFORM_VECTORS);
|
|
Object.defineProperty(context, 'MAX_VERTEX_UNIFORM_VECTORS', {{
|
|
get: () => MAX_VERTEX_UNIFORM_VECTORS + Math.floor(Math.random() * 3) - 1
|
|
}});
|
|
|
|
// Zufällige Modifikation für MAX_FRAGMENT_UNIFORM_VECTORS
|
|
const MAX_FRAGMENT_UNIFORM_VECTORS = context.getParameter(context.MAX_FRAGMENT_UNIFORM_VECTORS);
|
|
Object.defineProperty(context, 'MAX_FRAGMENT_UNIFORM_VECTORS', {{
|
|
get: () => MAX_FRAGMENT_UNIFORM_VECTORS + Math.floor(Math.random() * 3) - 1
|
|
}});
|
|
}}
|
|
}}
|
|
|
|
return context;
|
|
}};
|
|
}});
|
|
}}
|
|
"""
|
|
|
|
self.scripts.append(script)
|
|
|
|
def _init_audio_protection(self):
|
|
"""
|
|
Initialisiert den Schutz gegen Audio-Fingerprinting.
|
|
Dies modifiziert die Audio-API-Funktionen, die für Fingerprinting verwendet werden.
|
|
"""
|
|
script = f"""
|
|
() => {{
|
|
// Audio-Kontext spoofen
|
|
if (window.AudioContext || window.webkitAudioContext) {{
|
|
const AudioContextProxy = window.AudioContext || window.webkitAudioContext;
|
|
const originalAudioContext = AudioContextProxy;
|
|
|
|
// AudioContext überschreiben
|
|
window.AudioContext = window.webkitAudioContext = function() {{
|
|
const context = new originalAudioContext();
|
|
|
|
// createOscillator überschreiben
|
|
const originalCreateOscillator = context.createOscillator;
|
|
context.createOscillator = function() {{
|
|
const oscillator = originalCreateOscillator.apply(this, arguments);
|
|
|
|
// Frequenz leicht modifizieren
|
|
const originalFrequency = oscillator.frequency;
|
|
Object.defineProperty(oscillator, 'frequency', {{
|
|
get: function() {{
|
|
return originalFrequency;
|
|
}},
|
|
set: function(value) {{
|
|
if (typeof value === 'number') {{
|
|
// Leichte Änderung hinzufügen
|
|
const noise = (Math.random() * 0.02 - 0.01) * value;
|
|
originalFrequency.value = value + noise;
|
|
}} else {{
|
|
originalFrequency.value = value;
|
|
}}
|
|
}}
|
|
}});
|
|
|
|
return oscillator;
|
|
}};
|
|
|
|
// getChannelData überschreiben
|
|
const originalGetChannelData = context.createBuffer.prototype?.getChannelData || OfflineAudioContext.prototype?.getChannelData;
|
|
if (originalGetChannelData) {{
|
|
context.createBuffer.prototype.getChannelData = function(channel) {{
|
|
const array = originalGetChannelData.call(this, channel);
|
|
|
|
if ({str(self.defaults["audio_noise"]).lower()} && this.length > 20) {{
|
|
// Erweiterte Audio Noise Patterns
|
|
const noise = {self.noise_level} * 0.0001;
|
|
|
|
// Verschiedene Noise-Patterns
|
|
const patterns = ['gaussian', 'pink', 'brown', 'white'];
|
|
const pattern = patterns[Math.floor(Math.random() * patterns.length)];
|
|
|
|
switch(pattern) {{
|
|
case 'gaussian':
|
|
// Gaussian Noise
|
|
const samples = Math.min(200, Math.floor(array.length * 0.001));
|
|
for (let i = 0; i < samples; i++) {{
|
|
const idx = Math.floor(Math.random() * array.length);
|
|
// Box-Muller Transform für Gaussian Distribution
|
|
const u1 = Math.random();
|
|
const u2 = Math.random();
|
|
const gaussian = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
|
|
array[idx] += gaussian * noise;
|
|
}}
|
|
break;
|
|
|
|
case 'pink':
|
|
// Pink Noise (1/f)
|
|
let b0 = 0, b1 = 0, b2 = 0;
|
|
for (let i = 0; i < array.length; i += 100) {{
|
|
const white = Math.random() * 2 - 1;
|
|
b0 = 0.99765 * b0 + white * 0.0990460;
|
|
b1 = 0.96300 * b1 + white * 0.2965164;
|
|
b2 = 0.57000 * b2 + white * 1.0526913;
|
|
const pink = (b0 + b1 + b2 + white * 0.1848) * noise;
|
|
if (i < array.length) {{
|
|
array[i] += pink * 0.1;
|
|
}}
|
|
}}
|
|
break;
|
|
|
|
case 'brown':
|
|
// Brown Noise (1/f²)
|
|
let lastOut = 0;
|
|
for (let i = 0; i < array.length; i += 200) {{
|
|
const white = Math.random() * 2 - 1;
|
|
const brown = (lastOut + (0.02 * white)) / 1.02;
|
|
lastOut = brown;
|
|
if (i < array.length) {{
|
|
array[i] += brown * noise * 3;
|
|
}}
|
|
}}
|
|
break;
|
|
|
|
case 'white':
|
|
default:
|
|
// White Noise
|
|
const whiteSamples = Math.min(150, Math.floor(array.length * 0.0008));
|
|
for (let i = 0; i < whiteSamples; i++) {{
|
|
const idx = Math.floor(Math.random() * array.length);
|
|
array[idx] += (Math.random() * 2 - 1) * noise;
|
|
}}
|
|
}}
|
|
}}
|
|
|
|
return array;
|
|
}};
|
|
}}
|
|
|
|
// AnalyserNode.getFloatFrequencyData überschreiben
|
|
if (context.createAnalyser) {{
|
|
const originalCreateAnalyser = context.createAnalyser;
|
|
context.createAnalyser = function() {{
|
|
const analyser = originalCreateAnalyser.apply(this, arguments);
|
|
const originalGetFloatFrequencyData = analyser.getFloatFrequencyData;
|
|
|
|
analyser.getFloatFrequencyData = function(array) {{
|
|
originalGetFloatFrequencyData.call(this, array);
|
|
|
|
if ({str(self.defaults["audio_noise"]).lower()} && array.length > 20) {{
|
|
// Erweiterte Frequency Domain Noise
|
|
const noise = {self.noise_level} * 0.001;
|
|
|
|
// Frequency-aware noise injection
|
|
// Weniger Noise bei niedrigen Frequenzen, mehr bei hohen
|
|
for (let i = 0; i < array.length; i++) {{
|
|
const frequencyFactor = i / array.length; // 0 bis 1
|
|
const adaptiveNoise = noise * (0.5 + frequencyFactor * 0.5);
|
|
|
|
// Nur bei etwa 1% der Frequenzen Noise hinzufügen
|
|
if (Math.random() < 0.01) {{
|
|
array[i] += (Math.random() * 2 - 1) * adaptiveNoise;
|
|
}}
|
|
}}
|
|
}}
|
|
|
|
return array;
|
|
}};
|
|
|
|
return analyser;
|
|
}};
|
|
}}
|
|
|
|
// Weitere Audio-Properties für besseren Schutz
|
|
|
|
// Sample Rate Spoofing
|
|
Object.defineProperty(context, 'sampleRate', {{
|
|
get: function() {{
|
|
// Gängige Sample Rates: 44100 oder 48000
|
|
const rates = [44100, 48000];
|
|
return rates[Math.floor(Math.random() * rates.length)];
|
|
}}
|
|
}});
|
|
|
|
// Destination Properties
|
|
if (context.destination) {{
|
|
Object.defineProperty(context.destination, 'maxChannelCount', {{
|
|
get: function() {{
|
|
// Typische Werte: 2 (Stereo), 6 (5.1), 8 (7.1)
|
|
const counts = [2, 2, 2, 6, 8]; // Mehr Gewicht auf Stereo
|
|
return counts[Math.floor(Math.random() * counts.length)];
|
|
}}
|
|
}});
|
|
}}
|
|
|
|
// createDynamicsCompressor mit Variation
|
|
if (context.createDynamicsCompressor) {{
|
|
const originalCreateDynamicsCompressor = context.createDynamicsCompressor;
|
|
context.createDynamicsCompressor = function() {{
|
|
const compressor = originalCreateDynamicsCompressor.apply(this, arguments);
|
|
|
|
// Leichte Variationen in den Default-Werten
|
|
const properties = ['threshold', 'knee', 'ratio', 'attack', 'release'];
|
|
properties.forEach(prop => {{
|
|
if (compressor[prop] && compressor[prop].value !== undefined) {{
|
|
const original = compressor[prop].value;
|
|
const variation = 1 + (Math.random() - 0.5) * 0.02; // ±1% Variation
|
|
compressor[prop].value = original * variation;
|
|
}}
|
|
}});
|
|
|
|
return compressor;
|
|
}};
|
|
}}
|
|
|
|
return context;
|
|
}};
|
|
}}
|
|
}}
|
|
"""
|
|
|
|
self.scripts.append(script)
|
|
|
|
def _init_navigator_protection(self):
|
|
"""
|
|
Initialisiert den Schutz gegen Navigator-Objekt-Fingerprinting.
|
|
Dies modifiziert verschiedene Navigator-Eigenschaften, die für Fingerprinting verwendet werden.
|
|
"""
|
|
hardware_concurrency = self.defaults["hardware_concurrency"]
|
|
device_memory = self.defaults["device_memory"]
|
|
|
|
script = f"""
|
|
() => {{
|
|
// Navigator-Eigenschaften überschreiben
|
|
|
|
// hardwareConcurrency (CPU-Kerne)
|
|
if (navigator.hardwareConcurrency) {{
|
|
Object.defineProperty(navigator, 'hardwareConcurrency', {{
|
|
get: () => {hardware_concurrency}
|
|
}});
|
|
}}
|
|
|
|
// deviceMemory (RAM)
|
|
if (navigator.deviceMemory) {{
|
|
Object.defineProperty(navigator, 'deviceMemory', {{
|
|
get: () => {device_memory}
|
|
}});
|
|
}}
|
|
|
|
// language und languages
|
|
if (navigator.language) {{
|
|
const originalLanguage = navigator.language;
|
|
const originalLanguages = navigator.languages;
|
|
|
|
Object.defineProperty(navigator, 'language', {{
|
|
get: () => originalLanguage
|
|
}});
|
|
|
|
Object.defineProperty(navigator, 'languages', {{
|
|
get: () => originalLanguages
|
|
}});
|
|
}}
|
|
|
|
// userAgent-Konsistenz sicherstellen
|
|
if (navigator.userAgent) {{
|
|
const userAgent = navigator.userAgent;
|
|
|
|
// Wenn der userAgent bereits überschrieben wurde, stellen wir
|
|
// sicher, dass die appVersion und platform konsistent sind
|
|
const browserInfo = {{
|
|
chrome: /Chrome\\/(\\d+)/.exec(userAgent),
|
|
firefox: /Firefox\\/(\\d+)/.exec(userAgent),
|
|
safari: /Safari\\/(\\d+)/.exec(userAgent),
|
|
edge: /Edg(e|)\\/(\\d+)/.exec(userAgent)
|
|
}};
|
|
|
|
// Platform basierend auf userAgent bestimmen
|
|
let platform = '{self.defaults.get("platform", "Win32")}';
|
|
if (/Windows/.test(userAgent)) platform = 'Win32';
|
|
else if (/Macintosh/.test(userAgent)) platform = 'MacIntel';
|
|
else if (/Linux/.test(userAgent)) platform = 'Linux x86_64';
|
|
else if (/Android/.test(userAgent)) platform = 'Linux armv8l';
|
|
else if (/iPhone|iPad/.test(userAgent)) platform = 'iPhone';
|
|
|
|
Object.defineProperty(navigator, 'platform', {{
|
|
get: () => platform
|
|
}});
|
|
|
|
// appVersion konsistent machen
|
|
if (navigator.appVersion) {{
|
|
Object.defineProperty(navigator, 'appVersion', {{
|
|
get: () => userAgent.substring(8)
|
|
}});
|
|
}}
|
|
|
|
// vendor basierend auf Browser setzen
|
|
let vendor = '{self.defaults.get("vendor", "Google Inc.")}';
|
|
if (browserInfo.safari) vendor = 'Apple Computer, Inc.';
|
|
else if (browserInfo.firefox) vendor = '';
|
|
|
|
Object.defineProperty(navigator, 'vendor', {{
|
|
get: () => vendor
|
|
}});
|
|
}}
|
|
|
|
"""
|
|
self.scripts.append(script)
|
|
|
|
def _init_misc_protections(self):
|
|
"""
|
|
Initialisiert verschiedene weitere Schutzmaßnahmen gegen Fingerprinting.
|
|
"""
|
|
timezone_id = self.defaults["timezone_id"]
|
|
|
|
script = f"""
|
|
() => {{
|
|
// Date.prototype.getTimezoneOffset überschreiben
|
|
const originalGetTimezoneOffset = Date.prototype.getTimezoneOffset;
|
|
Date.prototype.getTimezoneOffset = function() {{
|
|
// Zeitzonendaten für '{timezone_id}'
|
|
// Mitteleuropäische Zeit (CET/CEST): UTC+1 / UTC+2 (Sommerzeit)
|
|
const date = new Date(this);
|
|
|
|
// Prüfen, ob Sommerzeit
|
|
const jan = new Date(date.getFullYear(), 0, 1);
|
|
const jul = new Date(date.getFullYear(), 6, 1);
|
|
const standardOffset = Math.max(jan.getTimezoneOffset(), jul.getTimezoneOffset());
|
|
|
|
// Sommerzeit in Europa: Ende März bis Ende Oktober
|
|
const isDST = date.getMonth() > 2 && date.getMonth() < 10;
|
|
|
|
// Offset in Minuten: CET = -60, CEST = -120
|
|
return isDST ? -120 : -60;
|
|
}};
|
|
|
|
// Plugins und MimeTypes Schutz
|
|
if (navigator.plugins) {{
|
|
// Leere oder gefälschte Plugins
|
|
Object.defineProperty(navigator, 'plugins', {{
|
|
get: () => {{
|
|
// Plugins-Array-Eigenschaften simulieren
|
|
const plugins = {{
|
|
length: 0,
|
|
item: () => null,
|
|
namedItem: () => null,
|
|
refresh: () => {{}},
|
|
[Symbol.iterator]: function* () {{}}
|
|
}};
|
|
|
|
return plugins;
|
|
}}
|
|
}});
|
|
|
|
// MimeTypes ebenfalls leeren
|
|
if (navigator.mimeTypes) {{
|
|
Object.defineProperty(navigator, 'mimeTypes', {{
|
|
get: () => {{
|
|
// MimeTypes-Array-Eigenschaften simulieren
|
|
const mimeTypes = {{
|
|
length: 0,
|
|
item: () => null,
|
|
namedItem: () => null,
|
|
[Symbol.iterator]: function* () {{}}
|
|
}};
|
|
|
|
return mimeTypes;
|
|
}}
|
|
}});
|
|
}}
|
|
}}
|
|
|
|
// Performance.now() und Date.now() - Schutz gegen Timing-Angriffe
|
|
if (window.performance && performance.now) {{
|
|
const originalNow = performance.now;
|
|
|
|
performance.now = function() {{
|
|
const value = originalNow.call(performance);
|
|
// Subtile Abweichung hinzufügen
|
|
return value + (Math.random() * 0.01);
|
|
}};
|
|
}}
|
|
|
|
// Date.now() ebenfalls mit subtiler Abweichung
|
|
const originalDateNow = Date.now;
|
|
Date.now = function() {{
|
|
const value = originalDateNow.call(Date);
|
|
// Subtile Abweichung hinzufügen (±1ms)
|
|
return value + (Math.random() < 0.5 ? 1 : 0);
|
|
}};
|
|
|
|
// screen-Eigenschaften konsistent machen
|
|
if (window.screen) {{
|
|
const originalWidth = screen.width;
|
|
const originalHeight = screen.height;
|
|
const originalColorDepth = screen.colorDepth;
|
|
const originalPixelDepth = screen.pixelDepth;
|
|
|
|
// Abweichungen verhindern - konsistente Werte liefern
|
|
Object.defineProperties(screen, {{
|
|
'width': {{ get: () => originalWidth }},
|
|
'height': {{ get: () => originalHeight }},
|
|
'availWidth': {{ get: () => originalWidth }},
|
|
'availHeight': {{ get: () => originalHeight - 40 }}, // Taskleiste simulieren
|
|
'colorDepth': {{ get: () => originalColorDepth }},
|
|
'pixelDepth': {{ get: () => originalPixelDepth }}
|
|
}});
|
|
}}
|
|
|
|
// Battery API Spoofing
|
|
if (navigator.getBattery) {{
|
|
// Override getBattery mit realistischen Werten
|
|
const batteryLevel = Math.random() * 0.7 + 0.3; // 30-100%
|
|
const charging = Math.random() > 0.3; // 70% Chance nicht am Laden
|
|
|
|
navigator.getBattery = function() {{
|
|
return Promise.resolve({{
|
|
charging: charging,
|
|
chargingTime: charging ? Math.floor(Math.random() * 3600) : Infinity,
|
|
dischargingTime: !charging ? Math.floor(Math.random() * 10800) + 3600 : Infinity, // 1-4 Stunden
|
|
level: batteryLevel,
|
|
|
|
// Event Handlers
|
|
onchargingchange: null,
|
|
onchargingtimechange: null,
|
|
ondischargingtimechange: null,
|
|
onlevelchange: null,
|
|
|
|
// Event Target Methoden
|
|
addEventListener: function() {{}},
|
|
removeEventListener: function() {{}},
|
|
dispatchEvent: function() {{ return true; }}
|
|
}});
|
|
}};
|
|
}}
|
|
|
|
// Network Information API Spoofing
|
|
if (navigator.connection || navigator.mozConnection || navigator.webkitConnection) {{
|
|
const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
|
|
const connectionTypes = ['wifi', 'cellular', 'ethernet'];
|
|
const effectiveTypes = ['4g', '3g'];
|
|
|
|
// Realistische Werte basierend auf Connection Type
|
|
const connectionType = connectionTypes[Math.floor(Math.random() * connectionTypes.length)];
|
|
const effectiveType = effectiveTypes[Math.floor(Math.random() * effectiveTypes.length)];
|
|
|
|
Object.defineProperties(connection, {{
|
|
'type': {{
|
|
get: () => connectionType,
|
|
configurable: true
|
|
}},
|
|
'effectiveType': {{
|
|
get: () => effectiveType,
|
|
configurable: true
|
|
}},
|
|
'downlink': {{
|
|
get: () => effectiveType === '4g' ? Math.random() * 10 + 5 : Math.random() * 5 + 1,
|
|
configurable: true
|
|
}},
|
|
'downlinkMax': {{
|
|
get: () => connectionType === 'wifi' ? 100 : 50,
|
|
configurable: true
|
|
}},
|
|
'rtt': {{
|
|
get: () => effectiveType === '4g' ? Math.floor(Math.random() * 50) + 20 : Math.floor(Math.random() * 100) + 50,
|
|
configurable: true
|
|
}},
|
|
'saveData': {{
|
|
get: () => false,
|
|
configurable: true
|
|
}}
|
|
}});
|
|
}}
|
|
}}
|
|
"""
|
|
|
|
self.scripts.append(script)
|
|
|
|
def _init_font_protection(self):
|
|
"""
|
|
Initialisiert Schutz gegen Font Fingerprinting.
|
|
"""
|
|
# Realistische Font-Listen für verschiedene Betriebssysteme
|
|
windows_fonts = [
|
|
"Arial", "Arial Black", "Comic Sans MS", "Courier New",
|
|
"Georgia", "Impact", "Times New Roman", "Trebuchet MS",
|
|
"Verdana", "Calibri", "Cambria", "Consolas", "Segoe UI"
|
|
]
|
|
|
|
mac_fonts = [
|
|
"Arial", "Arial Black", "Comic Sans MS", "Courier New",
|
|
"Georgia", "Helvetica", "Helvetica Neue", "Times New Roman",
|
|
"Trebuchet MS", "Verdana", "American Typewriter", "Avenir"
|
|
]
|
|
|
|
linux_fonts = [
|
|
"Arial", "Courier New", "Times New Roman", "DejaVu Sans",
|
|
"DejaVu Serif", "DejaVu Sans Mono", "Liberation Sans",
|
|
"Liberation Serif", "Ubuntu", "Noto Sans"
|
|
]
|
|
|
|
# Wähle Font-Liste basierend auf User Agent
|
|
import random
|
|
fonts_list = random.choice([windows_fonts, mac_fonts, linux_fonts])
|
|
fonts_json = json.dumps(fonts_list)
|
|
|
|
script = f"""
|
|
() => {{
|
|
// Font Fingerprinting Protection
|
|
const fonts = {fonts_json};
|
|
|
|
// Override document.fonts.check()
|
|
if (document.fonts && document.fonts.check) {{
|
|
const originalCheck = document.fonts.check.bind(document.fonts);
|
|
document.fonts.check = function(font, text) {{
|
|
// Parse font family aus dem font string
|
|
const fontMatch = font.match(/["']([^"']+)["']/);
|
|
if (fontMatch) {{
|
|
const fontFamily = fontMatch[1];
|
|
// Nur true zurückgeben wenn Font in unserer Liste
|
|
if (fonts.includes(fontFamily)) {{
|
|
return originalCheck(font, text);
|
|
}}
|
|
return false;
|
|
}}
|
|
return originalCheck(font, text);
|
|
}};
|
|
}}
|
|
|
|
// Override CSS Font Loading API
|
|
if (window.FontFace) {{
|
|
const originalFontFace = window.FontFace;
|
|
window.FontFace = function(family, source, descriptors) {{
|
|
// Nur erlauben wenn Font in unserer Liste
|
|
if (!fonts.includes(family)) {{
|
|
throw new Error('Font not available');
|
|
}}
|
|
return new originalFontFace(family, source, descriptors);
|
|
}};
|
|
window.FontFace.prototype = originalFontFace.prototype;
|
|
}}
|
|
|
|
// Override getComputedStyle für font-family
|
|
const originalGetComputedStyle = window.getComputedStyle;
|
|
window.getComputedStyle = function(element, pseudoElt) {{
|
|
const style = originalGetComputedStyle(element, pseudoElt);
|
|
const originalGetter = Object.getOwnPropertyDescriptor(CSSStyleDeclaration.prototype, 'fontFamily').get;
|
|
|
|
Object.defineProperty(style, 'fontFamily', {{
|
|
get: function() {{
|
|
const fontFamily = originalGetter.call(this);
|
|
// Filtere Fonts die nicht in unserer Liste sind
|
|
const fontList = fontFamily.split(',').map(f => f.trim().replace(/['"]/g, ''));
|
|
const filteredFonts = fontList.filter(f => fonts.includes(f));
|
|
|
|
if (filteredFonts.length === 0) {{
|
|
return '"Arial"'; // Fallback
|
|
}}
|
|
|
|
return filteredFonts.map(f => `"${{f}}"`).join(', ');
|
|
}}
|
|
}});
|
|
|
|
return style;
|
|
}};
|
|
|
|
// Canvas measureText Protection
|
|
if (CanvasRenderingContext2D.prototype.measureText) {{
|
|
const originalMeasureText = CanvasRenderingContext2D.prototype.measureText;
|
|
CanvasRenderingContext2D.prototype.measureText = function(text) {{
|
|
// Füge kleine Variation zu den Messungen hinzu
|
|
const metrics = originalMeasureText.call(this, text);
|
|
const variation = 1 + (Math.random() - 0.5) * 0.01; // ±0.5% Variation
|
|
|
|
// Override width getter
|
|
Object.defineProperty(metrics, 'width', {{
|
|
get: function() {{
|
|
return metrics.width * variation;
|
|
}}
|
|
}});
|
|
|
|
return metrics;
|
|
}};
|
|
}}
|
|
}}
|
|
"""
|
|
|
|
self.scripts.append(script)
|
|
|
|
def apply_to_context(self, context=None):
|
|
"""
|
|
Wendet alle Skripte auf den Browser-Kontext an.
|
|
|
|
Args:
|
|
context: Der Browser-Kontext, falls er noch nicht gesetzt wurde
|
|
"""
|
|
if context:
|
|
self.context = context
|
|
|
|
if not self.context:
|
|
logger.warning("Kein Browser-Kontext zum Anwenden der Fingerprint-Schutzmaßnahmen")
|
|
return
|
|
|
|
for script in self.scripts:
|
|
self.context.add_init_script(script)
|
|
|
|
logger.info(f"Fingerprint-Schutzmaßnahmen auf Browser-Kontext angewendet ({len(self.scripts)} Skripte)")
|
|
|
|
def get_fingerprint_status(self) -> Dict[str, Any]:
|
|
"""
|
|
Gibt den aktuellen Status der Fingerprint-Schutzmaßnahmen zurück.
|
|
|
|
Returns:
|
|
Dict[str, Any]: Status der Fingerprint-Schutzmaßnahmen
|
|
"""
|
|
status = {
|
|
"active": self.context is not None,
|
|
"script_count": len(self.scripts),
|
|
"protections": {
|
|
"canvas": self.defaults["canvas_noise"],
|
|
"webgl": self.defaults["webgl_noise"],
|
|
"audio": self.defaults["audio_noise"],
|
|
"navigator": True,
|
|
"battery": "getBattery" in self.scripts[-1],
|
|
"timing": "performance.now" in self.scripts[-1]
|
|
},
|
|
"noise_level": self.noise_level,
|
|
"custom_values": {
|
|
"webgl_vendor": self.defaults["webgl_vendor"],
|
|
"webgl_renderer": self.defaults["webgl_renderer"],
|
|
"hardware_concurrency": self.defaults["hardware_concurrency"],
|
|
"device_memory": self.defaults["device_memory"],
|
|
"timezone_id": self.defaults["timezone_id"]
|
|
}
|
|
}
|
|
|
|
return status
|
|
|
|
def _generate_deterministic_value(self, base_value: Any, component: str, date_str: str = None) -> Any:
|
|
"""
|
|
Generate deterministic value based on rotation seed, component and date.
|
|
|
|
Args:
|
|
base_value: The base value to modify
|
|
component: The component name (e.g., 'canvas', 'audio')
|
|
date_str: Optional date string, defaults to today
|
|
|
|
Returns:
|
|
Deterministically modified value
|
|
"""
|
|
if not self.defaults.get("rotation_seed"):
|
|
# No seed = use random values (backward compatible)
|
|
return base_value
|
|
|
|
if date_str is None:
|
|
date_str = datetime.now().strftime("%Y-%m-%d")
|
|
|
|
# Create deterministic hash
|
|
hash_input = f"{self.defaults['rotation_seed']}:{component}:{date_str}"
|
|
hash_value = hashlib.sha256(hash_input.encode()).hexdigest()
|
|
|
|
# Convert hash to deterministic float between 0 and 1
|
|
deterministic_float = int(hash_value[:8], 16) / 0xFFFFFFFF
|
|
|
|
# Apply deterministic modification based on type
|
|
if isinstance(base_value, (int, float)):
|
|
# For numbers, add ±10% variation
|
|
variation = 0.8 + (deterministic_float * 0.4) # 0.8 to 1.2
|
|
return type(base_value)(base_value * variation)
|
|
elif isinstance(base_value, str):
|
|
# For strings, return the base value (no modification)
|
|
return base_value
|
|
elif isinstance(base_value, list):
|
|
# For lists, deterministically shuffle
|
|
import random as rand
|
|
rand.seed(hash_value)
|
|
shuffled = base_value.copy()
|
|
rand.shuffle(shuffled)
|
|
rand.seed() # Reset random seed
|
|
return shuffled
|
|
else:
|
|
return base_value
|
|
|
|
def _get_deterministic_noise_seed(self, component: str) -> int:
|
|
"""Get deterministic noise seed for a component"""
|
|
if not self.defaults.get("rotation_seed"):
|
|
return random.randint(1, 1000000)
|
|
|
|
date_str = datetime.now().strftime("%Y-%m-%d")
|
|
hash_input = f"{self.defaults['rotation_seed']}:noise:{component}:{date_str}"
|
|
hash_value = hashlib.sha256(hash_input.encode()).hexdigest()
|
|
return int(hash_value[:8], 16) % 1000000
|
|
|
|
def rotate_fingerprint(self, noise_level: Optional[float] = None):
|
|
"""
|
|
Rotiert den Fingerprint durch Neugenerierung der Schutzmaßnahmen.
|
|
|
|
Args:
|
|
noise_level: Optionales neues Rauschniveau (0.0-1.0)
|
|
|
|
Returns:
|
|
bool: True bei Erfolg, False bei Fehler
|
|
"""
|
|
if noise_level is not None:
|
|
self.noise_level = max(0.0, min(1.0, noise_level))
|
|
|
|
try:
|
|
# Skripte zurücksetzen
|
|
self.scripts = []
|
|
|
|
# Neues WebGL-Vendor/Renderer-Paar generieren
|
|
webgl_vendors = [
|
|
"Google Inc.",
|
|
"Google Inc. (Intel)",
|
|
"Google Inc. (NVIDIA)",
|
|
"Google Inc. (AMD)",
|
|
"Intel Inc.",
|
|
"NVIDIA Corporation",
|
|
"AMD"
|
|
]
|
|
|
|
webgl_renderers = [
|
|
"ANGLE (Intel, Intel(R) HD Graphics 620 Direct3D11 vs_5_0 ps_5_0)",
|
|
"ANGLE (NVIDIA, NVIDIA GeForce GTX 1060 Direct3D11 vs_5_0 ps_5_0)",
|
|
"ANGLE (AMD, AMD Radeon RX 580 Direct3D11 vs_5_0 ps_5_0)",
|
|
"Intel Iris OpenGL Engine",
|
|
"NVIDIA GeForce GTX 980 OpenGL Engine",
|
|
"AMD Radeon Pro 560 OpenGL Engine",
|
|
"Mesa DRI Intel(R) UHD Graphics 620 (KBL GT2)",
|
|
"Mesa DRI NVIDIA GeForce GTX 1650"
|
|
]
|
|
|
|
# Zufällige Werte wählen
|
|
self.defaults["webgl_vendor"] = random.choice(webgl_vendors)
|
|
self.defaults["webgl_renderer"] = random.choice(webgl_renderers)
|
|
|
|
# Hardware-Concurrency und Device-Memory variieren
|
|
self.defaults["hardware_concurrency"] = random.choice([2, 4, 6, 8, 12, 16])
|
|
self.defaults["device_memory"] = random.choice([2, 4, 8, 16])
|
|
|
|
# Schutzmaßnahmen neu initialisieren
|
|
self._init_protections()
|
|
|
|
# Auf Kontext anwenden, falls vorhanden
|
|
if self.context:
|
|
self.apply_to_context()
|
|
|
|
logger.info(f"Fingerprint erfolgreich rotiert (Noise-Level: {self.noise_level:.2f})")
|
|
return True
|
|
|
|
except Exception as e:
|
|
logger.error(f"Fehler bei der Rotation des Fingerprints: {e}")
|
|
return False |