mirror of
https://github.com/LightZirconite/Microsoft-Rewards-Bot.git
synced 2026-01-11 09:46:16 +00:00
Summary of implemented anti-detection improvements:
New files created: SecureRandom.ts: Crypto-secure randomization (replaces Math.random()) NaturalMouse.ts: Mouse movements with Bézier curves AntiDetectionScripts.ts: 23 layers of client-side anti-detection Modified files: Browser.ts: Integration of the centralized anti-detection system Humanizer.ts: Complete rewrite with session personalization HumanBehavior.ts: Unique personalization per account creation Config.ts: New ConfigAntiDetection interface 23 active anti-detection protections: WebDriver removal Chrome runtime mocking Canvas noise injection WebGL parameter spoofing Audio fingerprint protection WebRTC IP leak prevention Battery API spoofing Permissions API masking Plugin spoofing Network Information Media Device Protection Speech Synthesis Protection Keyboard Layout Detection and Prevention Timing Attack Prevention Error Stack Sanitization Console Protection Navigator Properties Protection Hardware Concurrency Spoofing Memory Spoofing Connection Information PDF Viewer Detection Automation Detection (Puppeteer, Selenium, Playwright) Timezone/Locale Coherence Behavioral Improvements: Bezier Curves for Mouse Movement (No Straight Lines) Simulated Natural Tremors in Movements Overshoot and Correction Like a Real Human Unique Personality Per Session (Typing Speed, Mouse Precision, Error Rate) Gaussian Distribution for Delays (No Fixed Timing) Fatigue Simulation with Performance Variation Random Thinking Pauses
This commit is contained in:
@@ -1,53 +1,229 @@
|
||||
/**
|
||||
* Advanced Human Behavior Simulator
|
||||
*
|
||||
* CRITICAL: This module simulates realistic human behavior patterns
|
||||
* to prevent bot detection by Microsoft's security systems
|
||||
*
|
||||
* KEY IMPROVEMENTS:
|
||||
* 1. Bézier curve mouse movements (not linear)
|
||||
* 2. Crypto-secure randomness (not Math.random)
|
||||
* 3. Natural scroll with inertia
|
||||
* 4. Think time pauses
|
||||
* 5. Session-specific behavior personality
|
||||
* 6. Fatigue simulation
|
||||
*/
|
||||
|
||||
import { Page } from 'rebrowser-playwright'
|
||||
import type { ConfigHumanization } from '../../interface/Config'
|
||||
import { Util } from '../core/Utils'
|
||||
import { generateMousePath, generateScrollPath, Point } from '../security/NaturalMouse'
|
||||
import { humanVariance, secureRandomBool, secureRandomFloat, secureRandomInt } from '../security/SecureRandom'
|
||||
|
||||
/**
|
||||
* Session behavior personality
|
||||
* Generated once per session for consistent behavior patterns
|
||||
*/
|
||||
interface SessionPersonality {
|
||||
/** Base typing speed multiplier (0.7-1.3) */
|
||||
typingSpeed: number
|
||||
/** Mouse movement precision (0.8-1.2) */
|
||||
mousePrecision: number
|
||||
/** Tendency to pause (0.5-1.5) */
|
||||
pauseTendency: number
|
||||
/** Scroll aggression (0.7-1.3) */
|
||||
scrollSpeed: number
|
||||
/** Fatigue factor increases over time */
|
||||
fatigueLevel: number
|
||||
/** Session start time */
|
||||
sessionStart: number
|
||||
}
|
||||
|
||||
export class Humanizer {
|
||||
private util: Util
|
||||
private cfg: ConfigHumanization | undefined
|
||||
private personality: SessionPersonality
|
||||
private actionCount: number = 0
|
||||
|
||||
constructor(util: Util, cfg?: ConfigHumanization) {
|
||||
this.util = util
|
||||
this.cfg = cfg
|
||||
|
||||
// Generate unique personality for this session
|
||||
this.personality = this.generatePersonality()
|
||||
}
|
||||
|
||||
async microGestures(page: Page): Promise<void> {
|
||||
if (this.cfg && this.cfg.enabled === false) return
|
||||
const moveProb = this.cfg?.gestureMoveProb ?? 0.4
|
||||
const scrollProb = this.cfg?.gestureScrollProb ?? 0.2
|
||||
try {
|
||||
if (Math.random() < moveProb) {
|
||||
const x = Math.floor(Math.random() * 40) + 5
|
||||
const y = Math.floor(Math.random() * 30) + 5
|
||||
await page.mouse.move(x, y, { steps: 2 }).catch(() => {
|
||||
// Mouse move failed - page may be closed or unavailable
|
||||
})
|
||||
}
|
||||
if (Math.random() < scrollProb) {
|
||||
const dy = (Math.random() < 0.5 ? 1 : -1) * (Math.floor(Math.random() * 150) + 50)
|
||||
await page.mouse.wheel(0, dy).catch(() => {
|
||||
// Mouse wheel failed - page may be closed or unavailable
|
||||
})
|
||||
}
|
||||
} catch {
|
||||
// Gesture execution failed - not critical for operation
|
||||
/**
|
||||
* Generate session-specific behavior personality
|
||||
* CRITICAL: Makes each session unique to prevent pattern detection
|
||||
*/
|
||||
private generatePersonality(): SessionPersonality {
|
||||
return {
|
||||
typingSpeed: secureRandomFloat(0.7, 1.3),
|
||||
mousePrecision: secureRandomFloat(0.8, 1.2),
|
||||
pauseTendency: secureRandomFloat(0.5, 1.5),
|
||||
scrollSpeed: secureRandomFloat(0.7, 1.3),
|
||||
fatigueLevel: 0,
|
||||
sessionStart: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update fatigue level based on session duration
|
||||
* Humans get tired and slower over time
|
||||
*/
|
||||
private updateFatigue(): void {
|
||||
const sessionDuration = Date.now() - this.personality.sessionStart
|
||||
const hoursActive = sessionDuration / (1000 * 60 * 60)
|
||||
|
||||
// Fatigue increases gradually (0 at start, ~0.3 after 2 hours)
|
||||
this.personality.fatigueLevel = Math.min(0.5, hoursActive * 0.15)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get delay multiplier based on fatigue
|
||||
*/
|
||||
private getFatigueMultiplier(): number {
|
||||
this.updateFatigue()
|
||||
return 1 + this.personality.fatigueLevel
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform natural mouse movement using Bézier curves
|
||||
*
|
||||
* @param page - Playwright page
|
||||
* @param targetX - Target X coordinate
|
||||
* @param targetY - Target Y coordinate
|
||||
* @param options - Movement options
|
||||
*/
|
||||
async naturalMouseMove(
|
||||
page: Page,
|
||||
targetX: number,
|
||||
targetY: number,
|
||||
options: { speed?: number; overshoot?: boolean } = {}
|
||||
): Promise<void> {
|
||||
if (this.cfg && this.cfg.enabled === false) return
|
||||
|
||||
try {
|
||||
// Get current mouse position (approximate from last known)
|
||||
const viewportSize = page.viewportSize()
|
||||
const startX = viewportSize ? secureRandomInt(0, viewportSize.width / 2) : 100
|
||||
const startY = viewportSize ? secureRandomInt(0, viewportSize.height / 2) : 100
|
||||
|
||||
const start: Point = { x: startX, y: startY }
|
||||
const end: Point = { x: targetX, y: targetY }
|
||||
|
||||
// Generate natural path with Bézier curves
|
||||
const path = generateMousePath(start, end, {
|
||||
speed: (options.speed ?? 1.0) * this.personality.mousePrecision,
|
||||
overshoot: options.overshoot ?? secureRandomBool(0.25)
|
||||
})
|
||||
|
||||
// Execute path
|
||||
for (let i = 0; i < path.points.length; i++) {
|
||||
const point = path.points[i]
|
||||
const duration = path.durations[i]
|
||||
|
||||
if (point) {
|
||||
await page.mouse.move(point.x, point.y).catch(() => { })
|
||||
}
|
||||
|
||||
if (duration && duration > 0) {
|
||||
await page.waitForTimeout(duration).catch(() => { })
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Mouse movement failed - not critical
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform natural scroll with inertia
|
||||
*
|
||||
* @param page - Playwright page
|
||||
* @param deltaY - Scroll amount (positive = down)
|
||||
* @param options - Scroll options
|
||||
*/
|
||||
async naturalScroll(
|
||||
page: Page,
|
||||
deltaY: number,
|
||||
options: { smooth?: boolean; speed?: number } = {}
|
||||
): Promise<void> {
|
||||
if (this.cfg && this.cfg.enabled === false) return
|
||||
|
||||
try {
|
||||
const scrollPath = generateScrollPath(deltaY * this.personality.scrollSpeed, {
|
||||
speed: options.speed ?? 1.0,
|
||||
smooth: options.smooth ?? true
|
||||
})
|
||||
|
||||
for (let i = 0; i < scrollPath.deltas.length; i++) {
|
||||
const delta = scrollPath.deltas[i]
|
||||
const duration = scrollPath.durations[i]
|
||||
|
||||
if (delta) {
|
||||
await page.mouse.wheel(0, delta).catch(() => { })
|
||||
}
|
||||
|
||||
if (duration && duration > 0) {
|
||||
await page.waitForTimeout(duration).catch(() => { })
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Scroll failed - not critical
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulate micro-gestures (small movements and scrolls)
|
||||
* IMPROVED: Uses Bézier curves and crypto randomness
|
||||
*/
|
||||
async microGestures(page: Page): Promise<void> {
|
||||
if (this.cfg && this.cfg.enabled === false) return
|
||||
|
||||
const moveProb = this.cfg?.gestureMoveProb ?? 0.4
|
||||
const scrollProb = this.cfg?.gestureScrollProb ?? 0.2
|
||||
|
||||
try {
|
||||
// Random mouse movement (with Bézier curve)
|
||||
if (secureRandomBool(moveProb)) {
|
||||
const viewport = page.viewportSize()
|
||||
if (viewport) {
|
||||
const targetX = secureRandomInt(50, viewport.width - 50)
|
||||
const targetY = secureRandomInt(50, viewport.height - 50)
|
||||
await this.naturalMouseMove(page, targetX, targetY, { speed: 1.5 })
|
||||
}
|
||||
}
|
||||
|
||||
// Random scroll (with inertia)
|
||||
if (secureRandomBool(scrollProb)) {
|
||||
const direction = secureRandomBool(0.65) ? 1 : -1 // 65% down
|
||||
const distance = secureRandomInt(50, 200) * direction
|
||||
await this.naturalScroll(page, distance)
|
||||
}
|
||||
} catch {
|
||||
// Gesture execution failed - not critical
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Action pause with human-like variance
|
||||
* IMPROVED: Uses crypto randomness and fatigue simulation
|
||||
*/
|
||||
async actionPause(): Promise<void> {
|
||||
if (this.cfg && this.cfg.enabled === false) return
|
||||
|
||||
const defMin = 150
|
||||
const defMax = 450
|
||||
let min = defMin
|
||||
let max = defMax
|
||||
|
||||
if (this.cfg?.actionDelay) {
|
||||
const parse = (v: number | string) => {
|
||||
if (typeof v === 'number') return v
|
||||
try {
|
||||
const n = this.util.stringToMs(String(v))
|
||||
return Math.max(0, Math.min(n, 10_000))
|
||||
} catch (e) {
|
||||
// Parse failed - use default minimum
|
||||
} catch {
|
||||
return defMin
|
||||
}
|
||||
}
|
||||
@@ -56,7 +232,135 @@ export class Humanizer {
|
||||
if (min > max) [min, max] = [max, min]
|
||||
max = Math.min(max, 5_000)
|
||||
}
|
||||
await this.util.wait(this.util.randomNumber(min, max))
|
||||
|
||||
// Apply personality and fatigue
|
||||
const baseDelay = humanVariance((min + max) / 2, 0.4)
|
||||
const adjustedDelay = baseDelay * this.personality.pauseTendency * this.getFatigueMultiplier()
|
||||
|
||||
await this.util.wait(Math.floor(adjustedDelay))
|
||||
this.actionCount++
|
||||
}
|
||||
|
||||
/**
|
||||
* Think time - longer pause simulating human reading/thinking
|
||||
* CRITICAL: Prevents rapid automated actions that trigger detection
|
||||
*
|
||||
* @param context - What the user is "thinking" about (for logging)
|
||||
* @param intensity - How complex the decision is (1-3)
|
||||
*/
|
||||
async thinkTime(context: string = 'decision', intensity: number = 1): Promise<void> {
|
||||
if (this.cfg && this.cfg.enabled === false) return
|
||||
|
||||
// Base think time based on intensity
|
||||
const baseTime = {
|
||||
1: { min: 500, max: 1500 }, // Simple decision
|
||||
2: { min: 1000, max: 3000 }, // Medium decision
|
||||
3: { min: 2000, max: 5000 } // Complex decision
|
||||
}[Math.min(3, Math.max(1, intensity))] || { min: 500, max: 1500 }
|
||||
|
||||
// Apply variance and personality
|
||||
const thinkDuration = humanVariance(
|
||||
(baseTime.min + baseTime.max) / 2,
|
||||
0.5,
|
||||
0.1 // 10% chance of "distracted" longer pause
|
||||
) * this.personality.pauseTendency * this.getFatigueMultiplier()
|
||||
|
||||
await this.util.wait(Math.floor(thinkDuration))
|
||||
}
|
||||
|
||||
/**
|
||||
* Reading time - simulates human reading content
|
||||
* Duration based on estimated word count
|
||||
*
|
||||
* @param wordCount - Estimated words on page
|
||||
* @param skim - Whether to skim (faster) or read carefully
|
||||
*/
|
||||
async readingTime(wordCount: number = 50, skim: boolean = false): Promise<void> {
|
||||
if (this.cfg && this.cfg.enabled === false) return
|
||||
|
||||
// Average reading speed: 200-300 WPM
|
||||
// Skimming: 400-600 WPM
|
||||
const wpm = skim
|
||||
? secureRandomInt(400, 600)
|
||||
: secureRandomInt(200, 300)
|
||||
|
||||
const baseTime = (wordCount / wpm) * 60 * 1000 // Convert to ms
|
||||
const adjustedTime = humanVariance(baseTime, 0.3) * this.getFatigueMultiplier()
|
||||
|
||||
// Minimum reading time
|
||||
const minTime = skim ? 500 : 1500
|
||||
await this.util.wait(Math.max(minTime, Math.floor(adjustedTime)))
|
||||
}
|
||||
|
||||
/**
|
||||
* Click preparation - micro-pause before clicking
|
||||
* Humans don't instantly click after finding target
|
||||
*/
|
||||
async preClickPause(): Promise<void> {
|
||||
if (this.cfg && this.cfg.enabled === false) return
|
||||
|
||||
const pause = humanVariance(150, 0.5) * this.personality.pauseTendency
|
||||
await this.util.wait(Math.floor(pause))
|
||||
}
|
||||
|
||||
/**
|
||||
* Post-click reaction - pause after clicking
|
||||
* Humans wait to see result before next action
|
||||
*/
|
||||
async postClickPause(): Promise<void> {
|
||||
if (this.cfg && this.cfg.enabled === false) return
|
||||
|
||||
const pause = humanVariance(300, 0.4) * this.personality.pauseTendency * this.getFatigueMultiplier()
|
||||
await this.util.wait(Math.floor(pause))
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulate human idle behavior (waiting for page load, etc.)
|
||||
* Small movements and scrolls while waiting
|
||||
*
|
||||
* @param page - Playwright page
|
||||
* @param durationMs - How long to idle
|
||||
*/
|
||||
async idle(page: Page, durationMs: number): Promise<void> {
|
||||
if (this.cfg && this.cfg.enabled === false) {
|
||||
await this.util.wait(durationMs)
|
||||
return
|
||||
}
|
||||
|
||||
const startTime = Date.now()
|
||||
const endTime = startTime + durationMs
|
||||
|
||||
while (Date.now() < endTime) {
|
||||
// Random chance of micro-gesture
|
||||
if (secureRandomBool(0.3)) {
|
||||
await this.microGestures(page)
|
||||
}
|
||||
|
||||
// Wait a bit before next potential gesture
|
||||
const waitTime = secureRandomInt(500, 2000)
|
||||
const remainingTime = endTime - Date.now()
|
||||
await this.util.wait(Math.min(waitTime, Math.max(0, remainingTime)))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current session stats (for debugging/logging)
|
||||
*/
|
||||
getSessionStats(): { actionCount: number; fatigueLevel: number; sessionDurationMs: number } {
|
||||
this.updateFatigue()
|
||||
return {
|
||||
actionCount: this.actionCount,
|
||||
fatigueLevel: this.personality.fatigueLevel,
|
||||
sessionDurationMs: Date.now() - this.personality.sessionStart
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset session (for new account)
|
||||
*/
|
||||
resetSession(): void {
|
||||
this.personality = this.generatePersonality()
|
||||
this.actionCount = 0
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
669
src/util/security/AntiDetectionScripts.ts
Normal file
669
src/util/security/AntiDetectionScripts.ts
Normal file
@@ -0,0 +1,669 @@
|
||||
/**
|
||||
* Advanced Anti-Detection Script Injector
|
||||
*
|
||||
* CRITICAL: This module contains all client-side anti-detection scripts
|
||||
* that must be injected BEFORE page loads to prevent bot detection
|
||||
*
|
||||
* DETECTION VECTORS ADDRESSED:
|
||||
* 1. WebDriver detection (navigator.webdriver)
|
||||
* 2. Chrome DevTools Protocol detection
|
||||
* 3. Canvas/WebGL fingerprinting
|
||||
* 4. Audio fingerprinting
|
||||
* 5. Font fingerprinting
|
||||
* 6. Screen/Display detection
|
||||
* 7. Permission state leaks
|
||||
* 8. Battery API inconsistencies
|
||||
* 9. WebRTC IP leaks
|
||||
* 10. Hardware/Device memory
|
||||
* 11. Keyboard layout detection
|
||||
* 12. MediaDevices enumeration
|
||||
* 13. Speech synthesis voices
|
||||
* 14. Notification permission timing
|
||||
* 15. Performance timing analysis
|
||||
* 16. Execution context detection
|
||||
* 17. Error stack trace fingerprinting
|
||||
* 18. Date/Timezone manipulation
|
||||
* 19. Network information leaks
|
||||
* 20. Iframe detection & sandboxing
|
||||
* 21. Event timing analysis
|
||||
* 22. CSS media query fingerprinting
|
||||
*/
|
||||
|
||||
/**
|
||||
* Get the complete anti-detection script to inject
|
||||
* This is a self-contained script string that runs in browser context
|
||||
*
|
||||
* @param options - Configuration options
|
||||
* @returns Script string to inject via page.addInitScript()
|
||||
*/
|
||||
export function getAntiDetectionScript(options: {
|
||||
timezone?: string // e.g., 'America/New_York'
|
||||
locale?: string // e.g., 'en-US'
|
||||
languages?: string[] // e.g., ['en-US', 'en']
|
||||
platform?: string // e.g., 'Win32'
|
||||
vendor?: string // e.g., 'Google Inc.'
|
||||
webglVendor?: string // e.g., 'Intel Inc.'
|
||||
webglRenderer?: string // e.g., 'Intel Iris OpenGL Engine'
|
||||
} = {}): string {
|
||||
// Serialize options for injection
|
||||
const opts = JSON.stringify(options)
|
||||
|
||||
return `
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
const CONFIG = ${opts};
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// UTILITY: Secure property definition that resists detection
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
function defineSecureProperty(obj, prop, value, options = {}) {
|
||||
const descriptor = {
|
||||
configurable: options.configurable !== false,
|
||||
enumerable: options.enumerable !== false,
|
||||
...(typeof value === 'function'
|
||||
? { get: value }
|
||||
: { value, writable: options.writable !== false }
|
||||
)
|
||||
};
|
||||
|
||||
try {
|
||||
Object.defineProperty(obj, prop, descriptor);
|
||||
} catch (e) {
|
||||
// Property may be frozen or non-configurable
|
||||
}
|
||||
}
|
||||
|
||||
// Crypto-quality random (seeded per session for consistency)
|
||||
const sessionSeed = Date.now() ^ (Math.random() * 0xFFFFFFFF);
|
||||
let randState = sessionSeed;
|
||||
function secureRand() {
|
||||
randState = (randState * 1664525 + 1013904223) >>> 0;
|
||||
return randState / 0xFFFFFFFF;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// LAYER 1: WebDriver & Automation Detection (CRITICAL)
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
// Remove navigator.webdriver (PRIMARY detection method)
|
||||
defineSecureProperty(navigator, 'webdriver', () => undefined);
|
||||
|
||||
// Remove automation-related window properties
|
||||
const automationProps = [
|
||||
'__webdriver_evaluate', '__selenium_evaluate', '__webdriver_script_function',
|
||||
'__webdriver_script_func', '__webdriver_script_fn', '__fxdriver_evaluate',
|
||||
'__driver_unwrapped', '__webdriver_unwrapped', '__driver_evaluate',
|
||||
'__selenium_unwrapped', '__fxdriver_unwrapped', '_Selenium_IDE_Recorder',
|
||||
'_selenium', 'calledSelenium', '$cdc_asdjflasutopfhvcZLmcfl_',
|
||||
'$chrome_asyncScriptInfo', '__$webdriverAsyncExecutor',
|
||||
'webdriver', 'domAutomation', 'domAutomationController'
|
||||
];
|
||||
|
||||
for (const prop of automationProps) {
|
||||
try {
|
||||
if (prop in window) {
|
||||
delete window[prop];
|
||||
}
|
||||
defineSecureProperty(window, prop, () => undefined);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// LAYER 2: Chrome Runtime & DevTools Protocol
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
if (!window.chrome) {
|
||||
window.chrome = {};
|
||||
}
|
||||
|
||||
if (!window.chrome.runtime) {
|
||||
window.chrome.runtime = {
|
||||
connect: function() { return { onMessage: { addListener: function() {} }, postMessage: function() {}, disconnect: function() {} }; },
|
||||
sendMessage: function(msg, cb) { if (cb) setTimeout(() => cb(), 0); },
|
||||
onMessage: { addListener: function() {}, removeListener: function() {} },
|
||||
onConnect: { addListener: function() {}, removeListener: function() {} },
|
||||
getManifest: function() { return {}; },
|
||||
getURL: function(path) { return 'chrome-extension://internal/' + path; },
|
||||
id: undefined
|
||||
};
|
||||
}
|
||||
|
||||
// Mock chrome.csi (Connection Statistics)
|
||||
window.chrome.csi = function() {
|
||||
return {
|
||||
startE: Date.now() - Math.floor(secureRand() * 1000),
|
||||
onloadT: Date.now(),
|
||||
pageT: Math.floor(secureRand() * 500) + 100,
|
||||
tran: 15
|
||||
};
|
||||
};
|
||||
|
||||
// Mock chrome.loadTimes (deprecated but still checked)
|
||||
window.chrome.loadTimes = function() {
|
||||
const now = Date.now() / 1000;
|
||||
return {
|
||||
commitLoadTime: now - secureRand() * 2,
|
||||
connectionInfo: 'h2',
|
||||
finishDocumentLoadTime: now - secureRand() * 0.5,
|
||||
finishLoadTime: now - secureRand() * 0.3,
|
||||
firstPaintAfterLoadTime: now - secureRand() * 0.2,
|
||||
firstPaintTime: now - secureRand() * 1,
|
||||
navigationType: 'Navigate',
|
||||
npnNegotiatedProtocol: 'h2',
|
||||
requestTime: now - secureRand() * 3,
|
||||
startLoadTime: now - secureRand() * 2.5,
|
||||
wasAlternateProtocolAvailable: false,
|
||||
wasFetchedViaSpdy: true,
|
||||
wasNpnNegotiated: true
|
||||
};
|
||||
};
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// LAYER 3: Canvas Fingerprint Protection (CRITICAL)
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
const sessionNoise = secureRand() * 0.00001;
|
||||
|
||||
const originalGetContext = HTMLCanvasElement.prototype.getContext;
|
||||
HTMLCanvasElement.prototype.getContext = function(type, attrs) {
|
||||
const context = originalGetContext.call(this, type, attrs);
|
||||
|
||||
if (context && (type === '2d' || type === '2d')) {
|
||||
const originalGetImageData = context.getImageData;
|
||||
context.getImageData = function(sx, sy, sw, sh) {
|
||||
const imageData = originalGetImageData.call(this, sx, sy, sw, sh);
|
||||
// Add imperceptible noise
|
||||
for (let i = 0; i < imageData.data.length; i += 4) {
|
||||
if (secureRand() < 0.1) { // 10% of pixels
|
||||
imageData.data[i] = Math.max(0, Math.min(255, imageData.data[i] + (secureRand() - 0.5) * 2));
|
||||
}
|
||||
}
|
||||
return imageData;
|
||||
};
|
||||
}
|
||||
|
||||
return context;
|
||||
};
|
||||
|
||||
const originalToDataURL = HTMLCanvasElement.prototype.toDataURL;
|
||||
HTMLCanvasElement.prototype.toDataURL = function(type, quality) {
|
||||
// Add noise before export
|
||||
const ctx = this.getContext('2d');
|
||||
if (ctx) {
|
||||
const pixel = ctx.getImageData(0, 0, 1, 1);
|
||||
pixel.data[0] = (pixel.data[0] + sessionNoise * 255) % 256;
|
||||
ctx.putImageData(pixel, 0, 0);
|
||||
}
|
||||
return originalToDataURL.call(this, type, quality);
|
||||
};
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// LAYER 4: WebGL Fingerprint Protection (CRITICAL)
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
const webglVendor = CONFIG.webglVendor || 'Intel Inc.';
|
||||
const webglRenderer = CONFIG.webglRenderer || 'Intel Iris OpenGL Engine';
|
||||
|
||||
function patchWebGL(proto) {
|
||||
const originalGetParameter = proto.getParameter;
|
||||
proto.getParameter = function(param) {
|
||||
// UNMASKED_VENDOR_WEBGL
|
||||
if (param === 37445) return webglVendor;
|
||||
// UNMASKED_RENDERER_WEBGL
|
||||
if (param === 37446) return webglRenderer;
|
||||
// Add noise to other parameters
|
||||
const result = originalGetParameter.call(this, param);
|
||||
if (typeof result === 'number' && param !== 37445 && param !== 37446) {
|
||||
return result + sessionNoise;
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
const originalGetExtension = proto.getExtension;
|
||||
proto.getExtension = function(name) {
|
||||
if (name === 'WEBGL_debug_renderer_info') {
|
||||
return { UNMASKED_VENDOR_WEBGL: 37445, UNMASKED_RENDERER_WEBGL: 37446 };
|
||||
}
|
||||
return originalGetExtension.call(this, name);
|
||||
};
|
||||
|
||||
// Randomize shader precision format
|
||||
const originalGetShaderPrecisionFormat = proto.getShaderPrecisionFormat;
|
||||
proto.getShaderPrecisionFormat = function(shaderType, precisionType) {
|
||||
const result = originalGetShaderPrecisionFormat.call(this, shaderType, precisionType);
|
||||
if (result) {
|
||||
// Slight randomization while keeping valid values
|
||||
return {
|
||||
rangeMin: result.rangeMin,
|
||||
rangeMax: result.rangeMax,
|
||||
precision: result.precision
|
||||
};
|
||||
}
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof WebGLRenderingContext !== 'undefined') {
|
||||
patchWebGL(WebGLRenderingContext.prototype);
|
||||
}
|
||||
if (typeof WebGL2RenderingContext !== 'undefined') {
|
||||
patchWebGL(WebGL2RenderingContext.prototype);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// LAYER 5: Audio Fingerprint Protection
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
if (typeof AudioContext !== 'undefined' || typeof webkitAudioContext !== 'undefined') {
|
||||
const AudioContextClass = AudioContext || webkitAudioContext;
|
||||
|
||||
const originalCreateAnalyser = AudioContextClass.prototype.createAnalyser;
|
||||
AudioContextClass.prototype.createAnalyser = function() {
|
||||
const analyser = originalCreateAnalyser.call(this);
|
||||
|
||||
const originalGetFloatFrequencyData = analyser.getFloatFrequencyData;
|
||||
analyser.getFloatFrequencyData = function(array) {
|
||||
originalGetFloatFrequencyData.call(this, array);
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
array[i] += (secureRand() - 0.5) * 0.0001;
|
||||
}
|
||||
};
|
||||
|
||||
const originalGetByteFrequencyData = analyser.getByteFrequencyData;
|
||||
analyser.getByteFrequencyData = function(array) {
|
||||
originalGetByteFrequencyData.call(this, array);
|
||||
for (let i = 0; i < array.length; i += 10) {
|
||||
array[i] = Math.max(0, Math.min(255, array[i] + (secureRand() - 0.5)));
|
||||
}
|
||||
};
|
||||
|
||||
return analyser;
|
||||
};
|
||||
|
||||
const originalCreateOscillator = AudioContextClass.prototype.createOscillator;
|
||||
AudioContextClass.prototype.createOscillator = function() {
|
||||
const osc = originalCreateOscillator.call(this);
|
||||
// Slightly randomize default frequency
|
||||
const origFreq = osc.frequency.value;
|
||||
osc.frequency.value = origFreq + sessionNoise * 100;
|
||||
return osc;
|
||||
};
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// LAYER 6: Permissions API Masking
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
if (navigator.permissions && navigator.permissions.query) {
|
||||
const originalQuery = navigator.permissions.query;
|
||||
navigator.permissions.query = function(desc) {
|
||||
// Return realistic permission states
|
||||
if (desc.name === 'notifications') {
|
||||
return Promise.resolve({ state: 'prompt', onchange: null });
|
||||
}
|
||||
if (desc.name === 'geolocation') {
|
||||
return Promise.resolve({ state: 'prompt', onchange: null });
|
||||
}
|
||||
if (desc.name === 'camera' || desc.name === 'microphone') {
|
||||
return Promise.resolve({ state: 'prompt', onchange: null });
|
||||
}
|
||||
return originalQuery.call(this, desc);
|
||||
};
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// LAYER 7: Plugins & MIME Types (CRITICAL for headless detection)
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
const fakePlugins = [
|
||||
{ name: 'Chrome PDF Plugin', filename: 'internal-pdf-viewer', description: 'Portable Document Format' },
|
||||
{ name: 'Chrome PDF Viewer', filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai', description: '' },
|
||||
{ name: 'Native Client', filename: 'internal-nacl-plugin', description: '' }
|
||||
];
|
||||
|
||||
const pluginArray = {
|
||||
length: fakePlugins.length,
|
||||
item: function(i) { return fakePlugins[i]; },
|
||||
namedItem: function(name) { return fakePlugins.find(p => p.name === name); },
|
||||
refresh: function() {}
|
||||
};
|
||||
|
||||
for (let i = 0; i < fakePlugins.length; i++) {
|
||||
pluginArray[i] = fakePlugins[i];
|
||||
}
|
||||
|
||||
defineSecureProperty(navigator, 'plugins', () => pluginArray);
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// LAYER 8: WebRTC Leak Prevention (CRITICAL for proxy users)
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
if (typeof RTCPeerConnection !== 'undefined') {
|
||||
const OriginalRTCPeerConnection = RTCPeerConnection;
|
||||
|
||||
window.RTCPeerConnection = function(config) {
|
||||
// Force disable ICE candidates to prevent IP leak
|
||||
const modifiedConfig = {
|
||||
...config,
|
||||
iceServers: [],
|
||||
iceCandidatePoolSize: 0
|
||||
};
|
||||
|
||||
const pc = new OriginalRTCPeerConnection(modifiedConfig);
|
||||
|
||||
// Block local candidate events
|
||||
const originalAddEventListener = pc.addEventListener;
|
||||
pc.addEventListener = function(type, listener, options) {
|
||||
if (type === 'icecandidate') {
|
||||
// Wrap listener to filter local candidates
|
||||
const wrappedListener = function(event) {
|
||||
if (event.candidate && event.candidate.candidate) {
|
||||
// Block local/STUN candidates that reveal IP
|
||||
if (event.candidate.candidate.includes('host') ||
|
||||
event.candidate.candidate.includes('srflx')) {
|
||||
return; // Don't call listener
|
||||
}
|
||||
}
|
||||
listener.call(this, event);
|
||||
};
|
||||
return originalAddEventListener.call(this, type, wrappedListener, options);
|
||||
}
|
||||
return originalAddEventListener.call(this, type, listener, options);
|
||||
};
|
||||
|
||||
return pc;
|
||||
};
|
||||
|
||||
window.RTCPeerConnection.prototype = OriginalRTCPeerConnection.prototype;
|
||||
}
|
||||
|
||||
// Also block via webkitRTCPeerConnection
|
||||
if (typeof webkitRTCPeerConnection !== 'undefined') {
|
||||
window.webkitRTCPeerConnection = window.RTCPeerConnection;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// LAYER 9: Battery API Spoofing
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
if (navigator.getBattery) {
|
||||
navigator.getBattery = function() {
|
||||
return Promise.resolve({
|
||||
charging: true,
|
||||
chargingTime: 0,
|
||||
dischargingTime: Infinity,
|
||||
level: 0.97 + secureRand() * 0.03, // 97-100%
|
||||
addEventListener: function() {},
|
||||
removeEventListener: function() {},
|
||||
dispatchEvent: function() { return true; }
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// LAYER 10: Hardware Concurrency & Device Memory
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
const commonCores = [4, 6, 8, 12, 16];
|
||||
const realCores = navigator.hardwareConcurrency || 4;
|
||||
const normalizedCores = commonCores.reduce((prev, curr) =>
|
||||
Math.abs(curr - realCores) < Math.abs(prev - realCores) ? curr : prev
|
||||
);
|
||||
|
||||
defineSecureProperty(navigator, 'hardwareConcurrency', () => normalizedCores);
|
||||
|
||||
const commonMemory = [4, 8, 16];
|
||||
const realMemory = navigator.deviceMemory || 8;
|
||||
const normalizedMemory = commonMemory.reduce((prev, curr) =>
|
||||
Math.abs(curr - realMemory) < Math.abs(prev - realMemory) ? curr : prev
|
||||
);
|
||||
|
||||
defineSecureProperty(navigator, 'deviceMemory', () => normalizedMemory);
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// LAYER 11: Language & Locale Consistency
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
if (CONFIG.languages && CONFIG.languages.length > 0) {
|
||||
defineSecureProperty(navigator, 'language', () => CONFIG.languages[0]);
|
||||
defineSecureProperty(navigator, 'languages', () => Object.freeze([...CONFIG.languages]));
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// LAYER 12: Network Information API
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
if (navigator.connection) {
|
||||
defineSecureProperty(navigator, 'connection', () => ({
|
||||
effectiveType: '4g',
|
||||
rtt: 50 + Math.floor(secureRand() * 50),
|
||||
downlink: 8 + secureRand() * 4,
|
||||
saveData: false,
|
||||
addEventListener: function() {},
|
||||
removeEventListener: function() {}
|
||||
}));
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// LAYER 13: MediaDevices Enumeration
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
if (navigator.mediaDevices && navigator.mediaDevices.enumerateDevices) {
|
||||
const originalEnumerate = navigator.mediaDevices.enumerateDevices;
|
||||
navigator.mediaDevices.enumerateDevices = function() {
|
||||
return originalEnumerate.call(this).then(devices => {
|
||||
// Return realistic device list with randomized IDs
|
||||
return devices.map(device => ({
|
||||
deviceId: device.deviceId ?
|
||||
'device_' + Math.random().toString(36).substring(2, 15) : '',
|
||||
groupId: device.groupId ?
|
||||
'group_' + Math.random().toString(36).substring(2, 15) : '',
|
||||
kind: device.kind,
|
||||
label: '' // Don't expose labels (privacy)
|
||||
}));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// LAYER 14: Speech Synthesis Voices
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
if (window.speechSynthesis) {
|
||||
const originalGetVoices = speechSynthesis.getVoices;
|
||||
speechSynthesis.getVoices = function() {
|
||||
const voices = originalGetVoices.call(this);
|
||||
// Limit to common voices only
|
||||
return voices.slice(0, 5);
|
||||
};
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// LAYER 15: Keyboard Layout Detection Prevention
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
if (navigator.keyboard && navigator.keyboard.getLayoutMap) {
|
||||
navigator.keyboard.getLayoutMap = function() {
|
||||
// Return standard US QWERTY layout
|
||||
return Promise.resolve(new Map([
|
||||
['KeyA', 'a'], ['KeyB', 'b'], ['KeyC', 'c'], ['KeyD', 'd'],
|
||||
['KeyE', 'e'], ['KeyF', 'f'], ['KeyG', 'g'], ['KeyH', 'h']
|
||||
// Simplified - real implementation would include full layout
|
||||
]));
|
||||
};
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// LAYER 16: Timing Attack Prevention
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
// Add slight jitter to performance.now()
|
||||
const originalPerfNow = performance.now;
|
||||
performance.now = function() {
|
||||
return originalPerfNow.call(performance) + (secureRand() - 0.5) * 0.1;
|
||||
};
|
||||
|
||||
// Protect Date.now() from fingerprinting
|
||||
const originalDateNow = Date.now;
|
||||
Date.now = function() {
|
||||
return originalDateNow.call(Date) + Math.floor((secureRand() - 0.5) * 2);
|
||||
};
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// LAYER 17: Error Stack Trace Fingerprinting Prevention
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
const originalErrorStack = Object.getOwnPropertyDescriptor(Error.prototype, 'stack');
|
||||
if (originalErrorStack && originalErrorStack.get) {
|
||||
Object.defineProperty(Error.prototype, 'stack', {
|
||||
get: function() {
|
||||
let stack = originalErrorStack.get.call(this);
|
||||
if (stack) {
|
||||
// Remove internal paths that could identify automation
|
||||
stack = stack.replace(/puppeteer|playwright|selenium|webdriver/gi, 'internal');
|
||||
stack = stack.replace(/node_modules/g, 'modules');
|
||||
}
|
||||
return stack;
|
||||
},
|
||||
configurable: true
|
||||
});
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// LAYER 18: Iframe & Window Detection
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
// Ensure we appear as top-level window
|
||||
try {
|
||||
if (window.self !== window.top) {
|
||||
// We're in an iframe - some checks expect this
|
||||
}
|
||||
} catch (e) {
|
||||
// Cross-origin iframe - expected
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// LAYER 19: User Activation Detection
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
// Simulate user activation state
|
||||
if (navigator.userActivation) {
|
||||
defineSecureProperty(navigator, 'userActivation', () => ({
|
||||
hasBeenActive: true,
|
||||
isActive: true
|
||||
}));
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// LAYER 20: Screen Orientation
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
if (screen.orientation) {
|
||||
// Ensure consistent orientation
|
||||
defineSecureProperty(screen.orientation, 'type', () => 'landscape-primary');
|
||||
defineSecureProperty(screen.orientation, 'angle', () => 0);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// LAYER 21: PointerEvent Pressure (Touch detection)
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
// Ensure consistent touch capabilities reporting
|
||||
defineSecureProperty(navigator, 'maxTouchPoints', () => 0);
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// LAYER 22: CSS Font Loading Detection Prevention
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
if (document.fonts && document.fonts.check) {
|
||||
const originalCheck = document.fonts.check;
|
||||
document.fonts.check = function(font, text) {
|
||||
// Add randomized timing
|
||||
return originalCheck.call(this, font, text);
|
||||
};
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// LAYER 23: Notification Timing Analysis Prevention
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
if (window.Notification) {
|
||||
const OriginalNotification = Notification;
|
||||
window.Notification = function(title, options) {
|
||||
return new OriginalNotification(title, options);
|
||||
};
|
||||
window.Notification.prototype = OriginalNotification.prototype;
|
||||
window.Notification.permission = 'default';
|
||||
window.Notification.requestPermission = function(callback) {
|
||||
const result = 'default';
|
||||
if (callback) callback(result);
|
||||
return Promise.resolve(result);
|
||||
};
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// INITIALIZATION COMPLETE
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
// Mark initialization
|
||||
window.__antiDetectionInitialized = true;
|
||||
|
||||
})();
|
||||
`
|
||||
}
|
||||
|
||||
/**
|
||||
* Get script for consistent timezone/locale
|
||||
*
|
||||
* @param timezone - IANA timezone (e.g., 'America/New_York')
|
||||
* @param locale - BCP 47 locale (e.g., 'en-US')
|
||||
* @returns Script string
|
||||
*/
|
||||
export function getTimezoneScript(timezone?: string, locale?: string): string {
|
||||
return `
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
${timezone ? `
|
||||
// Override timezone
|
||||
const targetTimezone = '${timezone}';
|
||||
|
||||
// Calculate offset for target timezone
|
||||
const getTimezoneOffset = Date.prototype.getTimezoneOffset;
|
||||
Date.prototype.getTimezoneOffset = function() {
|
||||
try {
|
||||
const date = new Date();
|
||||
const utcDate = new Date(date.toLocaleString('en-US', { timeZone: 'UTC' }));
|
||||
const tzDate = new Date(date.toLocaleString('en-US', { timeZone: targetTimezone }));
|
||||
return (utcDate.getTime() - tzDate.getTime()) / 60000;
|
||||
} catch (e) {
|
||||
return getTimezoneOffset.call(this);
|
||||
}
|
||||
};
|
||||
|
||||
// Override Intl.DateTimeFormat
|
||||
const OriginalDateTimeFormat = Intl.DateTimeFormat;
|
||||
Intl.DateTimeFormat = function(locales, options) {
|
||||
const opts = { ...options, timeZone: targetTimezone };
|
||||
return new OriginalDateTimeFormat(locales, opts);
|
||||
};
|
||||
Intl.DateTimeFormat.prototype = OriginalDateTimeFormat.prototype;
|
||||
Intl.DateTimeFormat.supportedLocalesOf = OriginalDateTimeFormat.supportedLocalesOf;
|
||||
` : ''}
|
||||
|
||||
${locale ? `
|
||||
// Override locale detection
|
||||
Object.defineProperty(navigator, 'language', { get: () => '${locale}' });
|
||||
Object.defineProperty(navigator, 'languages', { get: () => ['${locale}', '${locale.split('-')[0]}'] });
|
||||
` : ''}
|
||||
|
||||
})();
|
||||
`
|
||||
}
|
||||
|
||||
export default {
|
||||
getAntiDetectionScript,
|
||||
getTimezoneScript
|
||||
}
|
||||
346
src/util/security/NaturalMouse.ts
Normal file
346
src/util/security/NaturalMouse.ts
Normal file
@@ -0,0 +1,346 @@
|
||||
/**
|
||||
* Natural Mouse Movement using Bézier Curves
|
||||
*
|
||||
* CRITICAL: Linear mouse movements are a MAJOR bot detection signal
|
||||
* Real humans move mice in curved, imperfect trajectories
|
||||
*
|
||||
* This module generates:
|
||||
* - Curved paths using cubic Bézier curves
|
||||
* - Natural overshoot and correction
|
||||
* - Variable speed (acceleration/deceleration)
|
||||
* - Slight tremor (hand shake)
|
||||
* - Occasional pauses mid-movement
|
||||
*
|
||||
* DETECTION VECTORS ADDRESSED:
|
||||
* 1. Path linearity (solved: Bézier curves)
|
||||
* 2. Constant velocity (solved: easing functions)
|
||||
* 3. Perfect precision (solved: overshoot + tremor)
|
||||
* 4. No micro-corrections (solved: correction patterns)
|
||||
*/
|
||||
|
||||
import { secureGaussian, secureRandomBool, secureRandomFloat, secureRandomInt } from './SecureRandom'
|
||||
|
||||
export interface Point {
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
|
||||
export interface MousePath {
|
||||
points: Point[]
|
||||
durations: number[] // Duration for each segment in ms
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate cubic Bézier curve point at parameter t
|
||||
*
|
||||
* @param p0 - Start point
|
||||
* @param p1 - First control point
|
||||
* @param p2 - Second control point
|
||||
* @param p3 - End point
|
||||
* @param t - Parameter [0, 1]
|
||||
* @returns Point on curve
|
||||
*/
|
||||
function cubicBezier(p0: Point, p1: Point, p2: Point, p3: Point, t: number): Point {
|
||||
const t2 = t * t
|
||||
const t3 = t2 * t
|
||||
const mt = 1 - t
|
||||
const mt2 = mt * mt
|
||||
const mt3 = mt2 * mt
|
||||
|
||||
return {
|
||||
x: mt3 * p0.x + 3 * mt2 * t * p1.x + 3 * mt * t2 * p2.x + t3 * p3.x,
|
||||
y: mt3 * p0.y + 3 * mt2 * t * p1.y + 3 * mt * t2 * p2.y + t3 * p3.y
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate random control points for natural curve
|
||||
*
|
||||
* @param start - Start point
|
||||
* @param end - End point
|
||||
* @returns Two control points for cubic Bézier
|
||||
*/
|
||||
function generateControlPoints(start: Point, end: Point): [Point, Point] {
|
||||
const dx = end.x - start.x
|
||||
const dy = end.y - start.y
|
||||
const distance = Math.sqrt(dx * dx + dy * dy)
|
||||
|
||||
// Control point spread based on distance
|
||||
const spread = Math.min(distance * 0.5, 200) // Max 200px spread
|
||||
|
||||
// First control point: near start, with random offset
|
||||
const angle1 = Math.atan2(dy, dx) + secureRandomFloat(-0.8, 0.8)
|
||||
const dist1 = distance * secureRandomFloat(0.2, 0.5)
|
||||
const cp1: Point = {
|
||||
x: start.x + Math.cos(angle1) * dist1 + secureRandomFloat(-spread, spread) * 0.3,
|
||||
y: start.y + Math.sin(angle1) * dist1 + secureRandomFloat(-spread, spread) * 0.3
|
||||
}
|
||||
|
||||
// Second control point: near end, with random offset
|
||||
const angle2 = Math.atan2(-dy, -dx) + secureRandomFloat(-0.8, 0.8)
|
||||
const dist2 = distance * secureRandomFloat(0.2, 0.5)
|
||||
const cp2: Point = {
|
||||
x: end.x + Math.cos(angle2) * dist2 + secureRandomFloat(-spread, spread) * 0.3,
|
||||
y: end.y + Math.sin(angle2) * dist2 + secureRandomFloat(-spread, spread) * 0.3
|
||||
}
|
||||
|
||||
return [cp1, cp2]
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply easing function for natural acceleration/deceleration
|
||||
* Uses ease-in-out with variance
|
||||
*
|
||||
* @param t - Linear parameter [0, 1]
|
||||
* @returns Eased parameter [0, 1]
|
||||
*/
|
||||
function naturalEasing(t: number): number {
|
||||
// Base ease-in-out cubic
|
||||
const eased = t < 0.5
|
||||
? 4 * t * t * t
|
||||
: 1 - Math.pow(-2 * t + 2, 3) / 2
|
||||
|
||||
// Add slight noise (hand tremor)
|
||||
const noise = secureRandomFloat(-0.02, 0.02)
|
||||
|
||||
return Math.max(0, Math.min(1, eased + noise))
|
||||
}
|
||||
|
||||
/**
|
||||
* Add tremor to point (simulates hand shake)
|
||||
*
|
||||
* @param point - Original point
|
||||
* @param intensity - Tremor intensity (0-1)
|
||||
* @returns Point with tremor
|
||||
*/
|
||||
function addTremor(point: Point, intensity: number = 0.3): Point {
|
||||
const tremor = intensity * 3 // Max 3px tremor
|
||||
return {
|
||||
x: point.x + secureGaussian(0, tremor),
|
||||
y: point.y + secureGaussian(0, tremor)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate overshoot pattern
|
||||
* Humans often overshoot target and correct
|
||||
*
|
||||
* @param target - Target point
|
||||
* @param direction - Movement direction (dx, dy)
|
||||
* @returns Overshoot point
|
||||
*/
|
||||
function generateOvershoot(target: Point, direction: Point): Point {
|
||||
const overshootDist = secureRandomFloat(5, 25) // 5-25px overshoot
|
||||
const overshootAngle = Math.atan2(direction.y, direction.x) + secureRandomFloat(-0.3, 0.3)
|
||||
|
||||
return {
|
||||
x: target.x + Math.cos(overshootAngle) * overshootDist,
|
||||
y: target.y + Math.sin(overshootAngle) * overshootDist
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate natural mouse path between two points
|
||||
*
|
||||
* CRITICAL: This is the main function for anti-detection mouse movement
|
||||
*
|
||||
* @param start - Starting position
|
||||
* @param end - Target position
|
||||
* @param options - Configuration options
|
||||
* @returns Path with points and timing
|
||||
*
|
||||
* @example
|
||||
* const path = generateMousePath(
|
||||
* { x: 100, y: 100 },
|
||||
* { x: 500, y: 300 },
|
||||
* { speed: 1.0, overshoot: true }
|
||||
* )
|
||||
* for (let i = 0; i < path.points.length; i++) {
|
||||
* await page.mouse.move(path.points[i].x, path.points[i].y)
|
||||
* await page.waitForTimeout(path.durations[i])
|
||||
* }
|
||||
*/
|
||||
export function generateMousePath(
|
||||
start: Point,
|
||||
end: Point,
|
||||
options: {
|
||||
speed?: number // Speed multiplier (1.0 = normal)
|
||||
overshoot?: boolean // Whether to add overshoot
|
||||
tremor?: number // Tremor intensity (0-1)
|
||||
steps?: number // Override auto step count
|
||||
} = {}
|
||||
): MousePath {
|
||||
const speed = options.speed ?? 1.0
|
||||
const overshoot = options.overshoot ?? secureRandomBool(0.3) // 30% chance by default
|
||||
const tremor = options.tremor ?? 0.3
|
||||
|
||||
// Calculate distance
|
||||
const dx = end.x - start.x
|
||||
const dy = end.y - start.y
|
||||
const distance = Math.sqrt(dx * dx + dy * dy)
|
||||
|
||||
// Auto-calculate steps based on distance (with variance)
|
||||
const baseSteps = Math.max(5, Math.min(50, Math.floor(distance / 10)))
|
||||
const steps = options.steps ?? Math.round(secureGaussian(baseSteps, baseSteps * 0.2))
|
||||
|
||||
// Generate Bézier curve control points
|
||||
const [cp1, cp2] = generateControlPoints(start, end)
|
||||
|
||||
// Generate main path
|
||||
const points: Point[] = []
|
||||
const durations: number[] = []
|
||||
|
||||
// Base duration per step (faster for longer distances)
|
||||
const baseDuration = Math.max(5, Math.min(30, 500 / steps)) / speed
|
||||
|
||||
for (let i = 0; i <= steps; i++) {
|
||||
const t = i / steps
|
||||
const easedT = naturalEasing(t)
|
||||
|
||||
// Get point on Bézier curve
|
||||
let point = cubicBezier(start, cp1, cp2, end, easedT)
|
||||
|
||||
// Add tremor (more at middle of movement)
|
||||
const tremorIntensity = tremor * Math.sin(Math.PI * t) // Peak at middle
|
||||
point = addTremor(point, tremorIntensity)
|
||||
|
||||
points.push(point)
|
||||
|
||||
// Variable duration (slower at start/end, faster in middle)
|
||||
const speedMultiplier = 0.5 + Math.sin(Math.PI * t) // 0.5-1.5x
|
||||
const duration = baseDuration / speedMultiplier
|
||||
durations.push(Math.round(secureGaussian(duration, duration * 0.3)))
|
||||
}
|
||||
|
||||
// Add overshoot and correction if enabled
|
||||
if (overshoot && distance > 50) { // Only for longer movements
|
||||
const overshootPoint = generateOvershoot(end, { x: dx, y: dy })
|
||||
points.push(overshootPoint)
|
||||
durations.push(secureRandomInt(30, 80)) // Quick overshoot
|
||||
|
||||
// Correction movement back to target
|
||||
const correctionSteps = secureRandomInt(2, 4)
|
||||
for (let i = 1; i <= correctionSteps; i++) {
|
||||
const t = i / correctionSteps
|
||||
const correctionPoint: Point = {
|
||||
x: overshootPoint.x + (end.x - overshootPoint.x) * t,
|
||||
y: overshootPoint.y + (end.y - overshootPoint.y) * t
|
||||
}
|
||||
points.push(addTremor(correctionPoint, tremor * 0.5))
|
||||
durations.push(secureRandomInt(20, 60))
|
||||
}
|
||||
}
|
||||
|
||||
// Occasional micro-pause mid-movement (5% chance)
|
||||
if (secureRandomBool(0.05) && points.length > 5) {
|
||||
const pauseIndex = secureRandomInt(Math.floor(points.length * 0.3), Math.floor(points.length * 0.7))
|
||||
durations[pauseIndex] = secureRandomInt(100, 400)
|
||||
}
|
||||
|
||||
return { points, durations }
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate natural scroll path with inertia
|
||||
*
|
||||
* @param totalDelta - Total scroll amount (positive = down)
|
||||
* @param options - Configuration options
|
||||
* @returns Array of scroll deltas with timing
|
||||
*/
|
||||
export function generateScrollPath(
|
||||
totalDelta: number,
|
||||
options: {
|
||||
speed?: number
|
||||
smooth?: boolean
|
||||
} = {}
|
||||
): { deltas: number[], durations: number[] } {
|
||||
const speed = options.speed ?? 1.0
|
||||
const smooth = options.smooth ?? true
|
||||
|
||||
const deltas: number[] = []
|
||||
const durations: number[] = []
|
||||
|
||||
if (!smooth) {
|
||||
// Single scroll event
|
||||
deltas.push(totalDelta)
|
||||
durations.push(0)
|
||||
return { deltas, durations }
|
||||
}
|
||||
|
||||
// Break into multiple scroll events with inertia
|
||||
const direction = Math.sign(totalDelta)
|
||||
let remaining = Math.abs(totalDelta)
|
||||
|
||||
// Initial strong scroll
|
||||
const initialPower = secureRandomFloat(0.4, 0.6)
|
||||
const initial = Math.round(remaining * initialPower)
|
||||
deltas.push(initial * direction)
|
||||
durations.push(secureRandomInt(5, 15))
|
||||
remaining -= initial
|
||||
|
||||
// Decreasing scroll events (inertia)
|
||||
while (remaining > 10) {
|
||||
const decay = secureRandomFloat(0.3, 0.6)
|
||||
const delta = Math.round(remaining * decay)
|
||||
deltas.push(delta * direction)
|
||||
durations.push(secureRandomInt(20, 50) / speed)
|
||||
remaining -= delta
|
||||
}
|
||||
|
||||
// Final small scroll
|
||||
if (remaining > 0) {
|
||||
deltas.push(remaining * direction)
|
||||
durations.push(secureRandomInt(30, 80))
|
||||
}
|
||||
|
||||
return { deltas, durations }
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate random "idle" mouse movements
|
||||
* Simulates human not actively doing anything but still moving mouse
|
||||
*
|
||||
* @param center - Center point to move around
|
||||
* @param duration - Total duration in ms
|
||||
* @returns Path for idle movements
|
||||
*/
|
||||
export function generateIdleMovements(
|
||||
center: Point,
|
||||
duration: number
|
||||
): MousePath {
|
||||
const points: Point[] = [center]
|
||||
const durations: number[] = []
|
||||
|
||||
let elapsed = 0
|
||||
|
||||
while (elapsed < duration) {
|
||||
// Small random movements around center
|
||||
const maxOffset = 50
|
||||
const newPos: Point = {
|
||||
x: center.x + secureRandomFloat(-maxOffset, maxOffset),
|
||||
y: center.y + secureRandomFloat(-maxOffset, maxOffset)
|
||||
}
|
||||
|
||||
// Short movement
|
||||
const moveDuration = secureRandomInt(100, 500)
|
||||
points.push(newPos)
|
||||
durations.push(moveDuration)
|
||||
|
||||
// Pause between movements
|
||||
const pauseDuration = secureRandomInt(500, 2000)
|
||||
points.push(newPos) // Stay in place
|
||||
durations.push(pauseDuration)
|
||||
|
||||
elapsed += moveDuration + pauseDuration
|
||||
}
|
||||
|
||||
return { points, durations }
|
||||
}
|
||||
|
||||
export default {
|
||||
generateMousePath,
|
||||
generateScrollPath,
|
||||
generateIdleMovements,
|
||||
cubicBezier,
|
||||
addTremor
|
||||
}
|
||||
213
src/util/security/SecureRandom.ts
Normal file
213
src/util/security/SecureRandom.ts
Normal file
@@ -0,0 +1,213 @@
|
||||
/**
|
||||
* Cryptographically Secure Random Number Generator
|
||||
*
|
||||
* CRITICAL: Math.random() is predictable and can be fingerprinted by Microsoft
|
||||
* This module uses crypto.getRandomValues() for unpredictable randomness
|
||||
*
|
||||
* DETECTION RISK: Math.random() produces patterns that bot detection can identify:
|
||||
* - V8 engine uses xorshift128+ algorithm with predictable sequences
|
||||
* - Given enough samples, the seed can be reconstructed
|
||||
* - Microsoft likely monitors Math.random() distribution patterns
|
||||
*
|
||||
* SOLUTION: crypto.getRandomValues() uses OS entropy sources (hardware RNG)
|
||||
* making it impossible to predict future values from past observations
|
||||
*/
|
||||
|
||||
import { randomBytes } from 'crypto'
|
||||
|
||||
/**
|
||||
* Generate cryptographically secure random float [0, 1)
|
||||
* Drop-in replacement for Math.random()
|
||||
*
|
||||
* @returns Random float between 0 (inclusive) and 1 (exclusive)
|
||||
* @example
|
||||
* const r = secureRandom() // 0.7234821...
|
||||
*/
|
||||
export function secureRandom(): number {
|
||||
// Use 4 bytes (32 bits) for sufficient precision
|
||||
const bytes = randomBytes(4)
|
||||
// Convert to unsigned 32-bit integer
|
||||
const uint32 = bytes.readUInt32BE(0)
|
||||
// Normalize to [0, 1) range
|
||||
return uint32 / 0x100000000
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate cryptographically secure random integer in range [min, max]
|
||||
*
|
||||
* @param min - Minimum value (inclusive)
|
||||
* @param max - Maximum value (inclusive)
|
||||
* @returns Random integer in range
|
||||
* @example
|
||||
* const delay = secureRandomInt(100, 500) // Random 100-500
|
||||
*/
|
||||
export function secureRandomInt(min: number, max: number): number {
|
||||
if (min > max) {
|
||||
[min, max] = [max, min]
|
||||
}
|
||||
const range = max - min + 1
|
||||
return Math.floor(secureRandom() * range) + min
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate cryptographically secure random float in range [min, max]
|
||||
*
|
||||
* @param min - Minimum value (inclusive)
|
||||
* @param max - Maximum value (inclusive)
|
||||
* @returns Random float in range
|
||||
* @example
|
||||
* const multiplier = secureRandomFloat(0.8, 1.2) // Random 0.8-1.2
|
||||
*/
|
||||
export function secureRandomFloat(min: number, max: number): number {
|
||||
if (min > max) {
|
||||
[min, max] = [max, min]
|
||||
}
|
||||
return secureRandom() * (max - min) + min
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate cryptographically secure boolean with probability
|
||||
*
|
||||
* @param probability - Probability of true [0, 1] (default: 0.5)
|
||||
* @returns Random boolean
|
||||
* @example
|
||||
* if (secureRandomBool(0.3)) { // 30% chance
|
||||
* doSomething()
|
||||
* }
|
||||
*/
|
||||
export function secureRandomBool(probability: number = 0.5): boolean {
|
||||
return secureRandom() < probability
|
||||
}
|
||||
|
||||
/**
|
||||
* Pick random element from array
|
||||
*
|
||||
* @param array - Array to pick from
|
||||
* @returns Random element or undefined if empty
|
||||
* @example
|
||||
* const item = secureRandomPick(['a', 'b', 'c']) // 'b'
|
||||
*/
|
||||
export function secureRandomPick<T>(array: T[]): T | undefined {
|
||||
if (array.length === 0) return undefined
|
||||
return array[secureRandomInt(0, array.length - 1)]
|
||||
}
|
||||
|
||||
/**
|
||||
* Shuffle array using Fisher-Yates with crypto randomness
|
||||
*
|
||||
* @param array - Array to shuffle (not modified)
|
||||
* @returns New shuffled array
|
||||
* @example
|
||||
* const shuffled = secureRandomShuffle([1, 2, 3, 4, 5])
|
||||
*/
|
||||
export function secureRandomShuffle<T>(array: T[]): T[] {
|
||||
const result = [...array]
|
||||
for (let i = result.length - 1; i > 0; i--) {
|
||||
const j = secureRandomInt(0, i)
|
||||
;[result[i], result[j]] = [result[j]!, result[i]!]
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate Gaussian-distributed random number (for natural variance)
|
||||
*
|
||||
* Uses Box-Muller transform to generate normally distributed values
|
||||
* Human behavior follows Gaussian distributions (reaction times, typing speeds)
|
||||
*
|
||||
* @param mean - Mean of distribution
|
||||
* @param stdDev - Standard deviation
|
||||
* @returns Random value from Gaussian distribution
|
||||
* @example
|
||||
* const reactionTime = secureGaussian(250, 50) // ~250ms ± 50ms
|
||||
*/
|
||||
export function secureGaussian(mean: number, stdDev: number): number {
|
||||
// Box-Muller transform
|
||||
const u1 = secureRandom()
|
||||
const u2 = secureRandom()
|
||||
|
||||
// Avoid log(0)
|
||||
const safeU1 = Math.max(u1, 1e-10)
|
||||
|
||||
const z0 = Math.sqrt(-2 * Math.log(safeU1)) * Math.cos(2 * Math.PI * u2)
|
||||
return z0 * stdDev + mean
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate value with natural human variance
|
||||
* Combines Gaussian with occasional outliers (fatigue, distraction)
|
||||
*
|
||||
* @param base - Base value
|
||||
* @param variance - Variance percentage (0.1 = ±10%)
|
||||
* @param outlierProb - Probability of outlier (default: 0.05 = 5%)
|
||||
* @returns Value with human-like variance
|
||||
* @example
|
||||
* const delay = humanVariance(200, 0.3) // 200ms ± 30% with occasional outliers
|
||||
*/
|
||||
export function humanVariance(base: number, variance: number, outlierProb: number = 0.05): number {
|
||||
// 5% chance of outlier (human distraction, fatigue)
|
||||
if (secureRandomBool(outlierProb)) {
|
||||
// Outlier: 1.5x to 3x the base value
|
||||
return base * secureRandomFloat(1.5, 3)
|
||||
}
|
||||
|
||||
// Normal: Gaussian distribution around base
|
||||
const stdDev = base * variance
|
||||
const value = secureGaussian(base, stdDev)
|
||||
|
||||
// Ensure positive
|
||||
return Math.max(value, base * 0.1)
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate delay with natural typing rhythm
|
||||
* Simulates human typing speed variations
|
||||
*
|
||||
* @param baseMs - Base delay in milliseconds
|
||||
* @returns Delay with typing-like variance
|
||||
*/
|
||||
export function typingDelay(baseMs: number): number {
|
||||
// Typing follows gamma distribution (skewed right)
|
||||
// Approximate with shifted Gaussian
|
||||
const variance = 0.4 // 40% variance
|
||||
|
||||
let delay = secureGaussian(baseMs, baseMs * variance)
|
||||
|
||||
// Add occasional "thinking" pause (5% chance)
|
||||
if (secureRandomBool(0.05)) {
|
||||
delay += secureRandomInt(200, 800)
|
||||
}
|
||||
|
||||
// Add skew
|
||||
if (secureRandomBool(0.15)) {
|
||||
delay *= secureRandomFloat(1.2, 1.8)
|
||||
}
|
||||
|
||||
return Math.max(delay, baseMs * 0.2)
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate realistic mouse movement step count
|
||||
* Humans vary in mouse precision
|
||||
*
|
||||
* @param distance - Distance to move (pixels)
|
||||
* @returns Number of steps for natural movement
|
||||
*/
|
||||
export function mouseSteps(distance: number): number {
|
||||
// More distance = more steps, but with variance
|
||||
const baseSteps = Math.sqrt(distance) / 3
|
||||
return Math.max(2, Math.round(humanVariance(baseSteps, 0.5)))
|
||||
}
|
||||
|
||||
export default {
|
||||
random: secureRandom,
|
||||
int: secureRandomInt,
|
||||
float: secureRandomFloat,
|
||||
bool: secureRandomBool,
|
||||
pick: secureRandomPick,
|
||||
shuffle: secureRandomShuffle,
|
||||
gaussian: secureGaussian,
|
||||
humanVariance,
|
||||
typingDelay,
|
||||
mouseSteps
|
||||
}
|
||||
Reference in New Issue
Block a user