""" Fingerprint Rotation Service - Handles fingerprint rotation and modification. """ import random import copy from typing import List, Dict, Any, Optional from datetime import datetime, timedelta from enum import Enum from domain.entities.browser_fingerprint import ( BrowserFingerprint, CanvasNoise, WebRTCConfig, HardwareConfig, NavigatorProperties ) class RotationStrategy(Enum): """Fingerprint rotation strategies.""" MINIMAL = "minimal" # Only rotate most volatile attributes GRADUAL = "gradual" # Gradual changes over time COMPLETE = "complete" # Complete regeneration (new device) class FingerprintRotationService: """Service for rotating and modifying fingerprints.""" def __init__(self): self.rotation_history = {} # Track rotation history per fingerprint def rotate_fingerprint(self, fingerprint: BrowserFingerprint, strategy: RotationStrategy = RotationStrategy.MINIMAL) -> BrowserFingerprint: """Rotate a fingerprint based on the specified strategy.""" # Create a deep copy to avoid modifying the original rotated = self._deep_copy_fingerprint(fingerprint) # Track rotation self._track_rotation(fingerprint.fingerprint_id, strategy) # Apply rotation based on strategy if strategy == RotationStrategy.MINIMAL: self._apply_minimal_rotation(rotated) elif strategy == RotationStrategy.GRADUAL: self._apply_gradual_rotation(rotated) elif strategy == RotationStrategy.COMPLETE: self._apply_complete_rotation(rotated) # Update rotation timestamp rotated.last_rotated = datetime.now() return rotated def _apply_minimal_rotation(self, fingerprint: BrowserFingerprint) -> None: """Apply minimal rotation - only most volatile attributes.""" # Rotate canvas noise seed (most commonly changed) fingerprint.canvas_noise.seed = random.randint(1000, 99999) # Slight audio context variations fingerprint.audio_context_base_latency += random.uniform(-0.001, 0.001) fingerprint.audio_context_output_latency += random.uniform(-0.001, 0.001) # Update timezone offset if DST might have changed if self._should_update_dst(fingerprint): fingerprint.timezone_offset += 60 if fingerprint.timezone_offset > 0 else -60 # Minor font list changes (add/remove 1-2 fonts) self._rotate_fonts_minimal(fingerprint) def _apply_gradual_rotation(self, fingerprint: BrowserFingerprint) -> None: """Apply gradual rotation - simulate natural changes over time.""" # All minimal changes self._apply_minimal_rotation(fingerprint) # WebGL renderer might get driver updates if random.random() < 0.3: self._update_webgl_version(fingerprint) # Browser version update if random.random() < 0.4: self._update_browser_version(fingerprint) # Screen resolution might change (external monitor) if random.random() < 0.1: self._rotate_screen_resolution(fingerprint) # More significant font changes self._rotate_fonts_gradual(fingerprint) def _apply_complete_rotation(self, fingerprint: BrowserFingerprint) -> None: """Apply complete rotation - simulate device change.""" # Keep only static components if account-bound if fingerprint.account_bound and fingerprint.static_components: # Maintain same device class but change specifics self._rotate_within_device_class(fingerprint) else: # Complete change - new device simulation self._rotate_to_new_device(fingerprint) # New canvas noise configuration fingerprint.canvas_noise = CanvasNoise( noise_level=random.choice([0.01, 0.02, 0.03]), seed=random.randint(1000, 99999), algorithm=random.choice(["gaussian", "uniform"]) ) # New audio context fingerprint.audio_context_base_latency = random.uniform(0.005, 0.02) fingerprint.audio_context_output_latency = random.uniform(0.01, 0.04) # Complete font list regeneration self._rotate_fonts_complete(fingerprint) def _rotate_fonts_minimal(self, fingerprint: BrowserFingerprint) -> None: """Minimal font rotation - add/remove 1-2 fonts.""" current_fonts = fingerprint.font_list.copy() # Remove 1-2 random fonts if len(current_fonts) > 10: for _ in range(random.randint(0, 2)): if current_fonts: current_fonts.remove(random.choice(current_fonts)) # Add 1-2 new fonts possible_additions = ["Segoe UI Light", "Segoe UI Semibold", "Arial Narrow", "Century Gothic", "Franklin Gothic Medium"] for _ in range(random.randint(0, 2)): new_font = random.choice(possible_additions) if new_font not in current_fonts: current_fonts.append(new_font) fingerprint.font_list = current_fonts def _rotate_fonts_gradual(self, fingerprint: BrowserFingerprint) -> None: """Gradual font rotation - change 20-30% of fonts.""" current_fonts = fingerprint.font_list.copy() num_to_change = int(len(current_fonts) * random.uniform(0.2, 0.3)) # Remove some fonts for _ in range(num_to_change // 2): if current_fonts: current_fonts.remove(random.choice(current_fonts)) # Add new fonts base_fonts = self._get_base_fonts_for_platform(fingerprint.navigator_props.platform) for _ in range(num_to_change // 2): available = [f for f in base_fonts if f not in current_fonts] if available: current_fonts.append(random.choice(available)) fingerprint.font_list = current_fonts def _rotate_fonts_complete(self, fingerprint: BrowserFingerprint) -> None: """Complete font rotation - regenerate font list.""" base_fonts = self._get_base_fonts_for_platform(fingerprint.navigator_props.platform) num_fonts = random.randint(int(len(base_fonts) * 0.7), int(len(base_fonts) * 0.9)) fingerprint.font_list = random.sample(base_fonts, num_fonts) def _update_webgl_version(self, fingerprint: BrowserFingerprint) -> None: """Update WebGL renderer version (driver update).""" renderer = fingerprint.webgl_renderer # Update version numbers in renderer string if "ANGLE" in renderer: # Update Direct3D version if "Direct3D11" in renderer: renderer = renderer.replace("Direct3D11", "Direct3D11.1") elif "Direct3D9" in renderer: renderer = renderer.replace("Direct3D9", "Direct3D11") # Update driver versions import re version_pattern = r'\d+\.\d+\.\d+\.\d+' match = re.search(version_pattern, renderer) if match: old_version = match.group() parts = old_version.split('.') # Increment minor version parts[2] = str(int(parts[2]) + random.randint(1, 10)) new_version = '.'.join(parts) renderer = renderer.replace(old_version, new_version) fingerprint.webgl_renderer = renderer def _update_browser_version(self, fingerprint: BrowserFingerprint) -> None: """Update browser version in user agent.""" user_agent = fingerprint.navigator_props.user_agent # Update Chrome version if "Chrome/" in user_agent: import re match = re.search(r'Chrome/(\d+)\.', user_agent) if match: current_version = int(match.group(1)) new_version = current_version + random.randint(1, 3) user_agent = user_agent.replace(f'Chrome/{current_version}', f'Chrome/{new_version}') fingerprint.navigator_props.user_agent = user_agent def _rotate_screen_resolution(self, fingerprint: BrowserFingerprint) -> None: """Rotate screen resolution (external monitor change).""" common_resolutions = [ (1920, 1080), (2560, 1440), (3840, 2160), # 16:9 (1920, 1200), (2560, 1600), # 16:10 (1366, 768), (1600, 900) # Laptop ] current = fingerprint.hardware_config.screen_resolution available = [res for res in common_resolutions if res != current] if available: fingerprint.hardware_config.screen_resolution = random.choice(available) def _rotate_within_device_class(self, fingerprint: BrowserFingerprint) -> None: """Rotate within the same device class (for account-bound fingerprints).""" static = fingerprint.static_components if static.device_type == "desktop": # Change to different desktop configuration fingerprint.hardware_config.hardware_concurrency = random.choice([4, 8, 12, 16]) fingerprint.hardware_config.device_memory = random.choice([8, 16, 32]) elif static.device_type == "mobile": # Change to different mobile configuration fingerprint.hardware_config.hardware_concurrency = random.choice([4, 6, 8]) fingerprint.hardware_config.device_memory = random.choice([3, 4, 6]) # Update renderer within same GPU vendor if "Intel" in static.gpu_vendor: fingerprint.webgl_renderer = random.choice([ "ANGLE (Intel HD Graphics 620)", "ANGLE (Intel UHD Graphics 630)", "ANGLE (Intel Iris Xe Graphics)" ]) elif "NVIDIA" in static.gpu_vendor: fingerprint.webgl_renderer = random.choice([ "ANGLE (NVIDIA GeForce GTX 1060)", "ANGLE (NVIDIA GeForce RTX 3060)", "ANGLE (NVIDIA GeForce GTX 1660)" ]) def _rotate_to_new_device(self, fingerprint: BrowserFingerprint) -> None: """Rotate to completely new device.""" # This would typically regenerate most components # For now, we'll do significant changes # New hardware configuration fingerprint.hardware_config = HardwareConfig( hardware_concurrency=random.choice([4, 8, 12, 16]), device_memory=random.choice([4, 8, 16, 32]), max_touch_points=0, screen_resolution=random.choice([(1920, 1080), (2560, 1440)]), color_depth=random.choice([24, 32]), pixel_ratio=random.choice([1.0, 1.5, 2.0]) ) # New WebGL vendors = ["Intel Inc.", "NVIDIA Corporation", "AMD"] fingerprint.webgl_vendor = random.choice(vendors) if fingerprint.webgl_vendor == "Intel Inc.": fingerprint.webgl_renderer = "ANGLE (Intel HD Graphics)" elif fingerprint.webgl_vendor == "NVIDIA Corporation": fingerprint.webgl_renderer = "ANGLE (NVIDIA GeForce GTX)" else: fingerprint.webgl_renderer = "ANGLE (AMD Radeon)" def _should_update_dst(self, fingerprint: BrowserFingerprint) -> bool: """Check if DST update might be needed.""" # Simple check - in reality would check actual DST dates if fingerprint.last_rotated: days_since_rotation = (datetime.now() - fingerprint.last_rotated).days # DST changes roughly every 6 months return days_since_rotation > 180 return False def _get_base_fonts_for_platform(self, platform: str) -> List[str]: """Get base fonts for platform.""" if "Win" in platform: return ["Arial", "Times New Roman", "Verdana", "Tahoma", "Segoe UI", "Calibri", "Consolas", "Georgia", "Impact", "Comic Sans MS"] elif "Mac" in platform: return ["Arial", "Helvetica", "Times New Roman", "Georgia", "Verdana", "Monaco", "Courier", "Geneva", "Futura"] else: return ["Arial", "Times New Roman", "Liberation Sans", "DejaVu Sans", "Ubuntu", "Droid Sans", "Noto Sans"] def _track_rotation(self, fingerprint_id: str, strategy: RotationStrategy) -> None: """Track rotation history.""" if fingerprint_id not in self.rotation_history: self.rotation_history[fingerprint_id] = [] self.rotation_history[fingerprint_id].append({ "timestamp": datetime.now(), "strategy": strategy.value }) # Keep only last 100 rotations self.rotation_history[fingerprint_id] = self.rotation_history[fingerprint_id][-100:] def _deep_copy_fingerprint(self, fingerprint: BrowserFingerprint) -> BrowserFingerprint: """Create a deep copy of a fingerprint.""" # Manual deep copy to ensure all nested objects are copied return BrowserFingerprint( fingerprint_id=fingerprint.fingerprint_id, canvas_noise=CanvasNoise( noise_level=fingerprint.canvas_noise.noise_level, seed=fingerprint.canvas_noise.seed, algorithm=fingerprint.canvas_noise.algorithm ), webrtc_config=WebRTCConfig( enabled=fingerprint.webrtc_config.enabled, ice_servers=fingerprint.webrtc_config.ice_servers.copy(), local_ip_mask=fingerprint.webrtc_config.local_ip_mask, disable_webrtc=fingerprint.webrtc_config.disable_webrtc ), font_list=fingerprint.font_list.copy(), hardware_config=HardwareConfig( hardware_concurrency=fingerprint.hardware_config.hardware_concurrency, device_memory=fingerprint.hardware_config.device_memory, max_touch_points=fingerprint.hardware_config.max_touch_points, screen_resolution=fingerprint.hardware_config.screen_resolution, color_depth=fingerprint.hardware_config.color_depth, pixel_ratio=fingerprint.hardware_config.pixel_ratio ), navigator_props=NavigatorProperties( platform=fingerprint.navigator_props.platform, vendor=fingerprint.navigator_props.vendor, vendor_sub=fingerprint.navigator_props.vendor_sub, product=fingerprint.navigator_props.product, product_sub=fingerprint.navigator_props.product_sub, app_name=fingerprint.navigator_props.app_name, app_version=fingerprint.navigator_props.app_version, user_agent=fingerprint.navigator_props.user_agent, language=fingerprint.navigator_props.language, languages=fingerprint.navigator_props.languages.copy() if fingerprint.navigator_props.languages else [], online=fingerprint.navigator_props.online, do_not_track=fingerprint.navigator_props.do_not_track ), webgl_vendor=fingerprint.webgl_vendor, webgl_renderer=fingerprint.webgl_renderer, audio_context_base_latency=fingerprint.audio_context_base_latency, audio_context_output_latency=fingerprint.audio_context_output_latency, audio_context_sample_rate=fingerprint.audio_context_sample_rate, timezone=fingerprint.timezone, timezone_offset=fingerprint.timezone_offset, plugins=fingerprint.plugins.copy() if fingerprint.plugins else [], created_at=fingerprint.created_at, last_rotated=fingerprint.last_rotated, static_components=fingerprint.static_components, # This is immutable rotation_seed=fingerprint.rotation_seed, account_bound=fingerprint.account_bound, platform_specific_config=fingerprint.platform_specific_config.copy() if fingerprint.platform_specific_config else {} )