From 9d9b391cd1e721a3b708153951a0932cc2d6ee1b Mon Sep 17 00:00:00 2001 From: LightZirconite Date: Tue, 16 Dec 2025 22:22:38 +0100 Subject: [PATCH] =?UTF-8?q?Summary=20of=20implemented=20anti-detection=20i?= =?UTF-8?q?mprovements:=20New=20files=20created:=20SecureRandom.ts:=20Cryp?= =?UTF-8?q?to-secure=20randomization=20(replaces=20Math.random())=20Natura?= =?UTF-8?q?lMouse.ts:=20Mouse=20movements=20with=20B=C3=A9zier=20curves=20?= =?UTF-8?q?AntiDetectionScripts.ts:=2023=20layers=20of=20client-side=20ant?= =?UTF-8?q?i-detection=20Modified=20files:=20Browser.ts:=20Integration=20o?= =?UTF-8?q?f=20the=20centralized=20anti-detection=20system=20Humanizer.ts:?= =?UTF-8?q?=20Complete=20rewrite=20with=20session=20personalization=20Huma?= =?UTF-8?q?nBehavior.ts:=20Unique=20personalization=20per=20account=20crea?= =?UTF-8?q?tion=20Config.ts:=20New=20ConfigAntiDetection=20interface=2023?= =?UTF-8?q?=20active=20anti-detection=20protections:=20WebDriver=20removal?= =?UTF-8?q?=20Chrome=20runtime=20mocking=20Canvas=20noise=20injection=20We?= =?UTF-8?q?bGL=20parameter=20spoofing=20Audio=20fingerprint=20protection?= =?UTF-8?q?=20WebRTC=20IP=20leak=20prevention=20Battery=20API=20spoofing?= =?UTF-8?q?=20Permissions=20API=20masking=20Plugin=20spoofing=20Network=20?= =?UTF-8?q?Information=20Media=20Device=20Protection=20Speech=20Synthesis?= =?UTF-8?q?=20Protection=20Keyboard=20Layout=20Detection=20and=20Preventio?= =?UTF-8?q?n=20Timing=20Attack=20Prevention=20Error=20Stack=20Sanitization?= =?UTF-8?q?=20Console=20Protection=20Navigator=20Properties=20Protection?= =?UTF-8?q?=20Hardware=20Concurrency=20Spoofing=20Memory=20Spoofing=20Conn?= =?UTF-8?q?ection=20Information=20PDF=20Viewer=20Detection=20Automation=20?= =?UTF-8?q?Detection=20(Puppeteer,=20Selenium,=20Playwright)=20Timezone/Lo?= =?UTF-8?q?cale=20Coherence=20Behavioral=20Improvements:=20Bezier=20Curves?= =?UTF-8?q?=20for=20Mouse=20Movement=20(No=20Straight=20Lines)=20Simulated?= =?UTF-8?q?=20Natural=20Tremors=20in=20Movements=20Overshoot=20and=20Corre?= =?UTF-8?q?ction=20Like=20a=20Real=20Human=20Unique=20Personality=20Per=20?= =?UTF-8?q?Session=20(Typing=20Speed,=20Mouse=20Precision,=20Error=20Rate)?= =?UTF-8?q?=20Gaussian=20Distribution=20for=20Delays=20(No=20Fixed=20Timin?= =?UTF-8?q?g)=20Fatigue=20Simulation=20with=20Performance=20Variation=20Ra?= =?UTF-8?q?ndom=20Thinking=20Pauses?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/account-creation/HumanBehavior.ts | 364 ++++++++---- src/browser/Browser.ts | 322 ++--------- src/interface/Config.ts | 26 + src/util/browser/Humanizer.ts | 350 ++++++++++- src/util/security/AntiDetectionScripts.ts | 669 ++++++++++++++++++++++ src/util/security/NaturalMouse.ts | 346 +++++++++++ src/util/security/SecureRandom.ts | 213 +++++++ 7 files changed, 1883 insertions(+), 407 deletions(-) create mode 100644 src/util/security/AntiDetectionScripts.ts create mode 100644 src/util/security/NaturalMouse.ts create mode 100644 src/util/security/SecureRandom.ts diff --git a/src/account-creation/HumanBehavior.ts b/src/account-creation/HumanBehavior.ts index d2ee99f..9cd7d97 100644 --- a/src/account-creation/HumanBehavior.ts +++ b/src/account-creation/HumanBehavior.ts @@ -3,21 +3,88 @@ * * CRITICAL: Microsoft detects bots by analyzing: * 1. Typing speed (instant .fill() = bot, gradual .type() = human) - * 2. Mouse movements (no movement = bot, random moves = human) + * 2. Mouse movements (linear = bot, BΓ©zier curves = human) * 3. Pauses (fixed delays = bot, variable pauses = human) * 4. Click patterns (force clicks = bot, natural clicks = human) + * 5. Session consistency (same patterns = bot farm) + * + * MAJOR IMPROVEMENTS (v3.5): + * - Crypto-secure randomness (not Math.random) + * - BΓ©zier curve mouse movements + * - Session personality (unique behavior per account) + * - Fatigue simulation + * - Natural scroll with inertia * * This module ensures account creation is INDISTINGUISHABLE from manual creation. */ import type { Page } from 'rebrowser-playwright' import { log } from '../util/notifications/Logger' +import { generateMousePath, generateScrollPath } from '../util/security/NaturalMouse' +import { + humanVariance, + secureGaussian, + secureRandomBool, + secureRandomFloat, + secureRandomInt, + typingDelay +} from '../util/security/SecureRandom' + +/** + * Session personality - unique behavior patterns per account creation + */ +interface CreatorPersonality { + typingSpeed: number // 0.6-1.4 + mousePrecision: number // 0.7-1.3 + pauseTendency: number // 0.5-1.5 + errorRate: number // 0-0.08 (typo probability) + burstTyping: boolean // Does this person type in bursts? + readingSpeed: number // WPM for reading + confidenceLevel: number // 0.7-1.3 (affects hesitation) +} export class HumanBehavior { private page: Page + private personality: CreatorPersonality + private sessionStart: number + private actionCount: number = 0 constructor(page: Page) { this.page = page + this.sessionStart = Date.now() + + // Generate unique personality for this account creation + this.personality = this.generatePersonality() + + log(false, 'CREATOR', `🧠 Session personality: typing=${this.personality.typingSpeed.toFixed(2)}x, ` + + `precision=${this.personality.mousePrecision.toFixed(2)}x, ` + + `confidence=${this.personality.confidenceLevel.toFixed(2)}`, 'log', 'cyan') + } + + /** + * Generate unique personality for this session + */ + private generatePersonality(): CreatorPersonality { + return { + typingSpeed: secureRandomFloat(0.6, 1.4), + mousePrecision: secureRandomFloat(0.7, 1.3), + pauseTendency: secureRandomFloat(0.5, 1.5), + errorRate: secureRandomFloat(0, 0.08), + burstTyping: secureRandomBool(0.3), + readingSpeed: secureRandomInt(180, 320), + confidenceLevel: secureRandomFloat(0.7, 1.3) + } + } + + /** + * Get fatigue multiplier based on session duration + */ + private getFatigueMultiplier(): number { + const sessionDuration = Date.now() - this.sessionStart + const minutesActive = sessionDuration / 60000 + + // Fatigue increases over 30+ minutes + return 1 + Math.min(0.4, Math.max(0, (minutesActive - 30) * 0.01)) } /** @@ -29,17 +96,28 @@ export class HumanBehavior { * @param context Description for logging (optional) */ async humanDelay(minMs: number, maxMs: number, context?: string): Promise { - // IMPROVEMENT: Add occasional "thinking" pauses (10% chance of 2x delay) - const shouldThink = Math.random() < 0.1 - const multiplier = shouldThink ? 2 : 1 + // Use Gaussian distribution centered on mean + const mean = (minMs + maxMs) / 2 + const stdDev = (maxMs - minMs) / 4 - const delay = (Math.random() * (maxMs - minMs) + minMs) * multiplier + let delay = secureGaussian(mean, stdDev) - if (shouldThink && context) { - log(false, 'CREATOR', `[${context}] πŸ€” Thinking pause (${Math.floor(delay)}ms)`, 'log', 'cyan') + // Apply personality and fatigue + delay *= this.personality.pauseTendency * this.getFatigueMultiplier() + + // 10% chance of "thinking" pause (2x delay) + if (secureRandomBool(0.1)) { + delay *= 2 + if (context) { + log(false, 'CREATOR', `[${context}] πŸ€” Thinking pause (${Math.floor(delay)}ms)`, 'log', 'cyan') + } } + // Clamp to reasonable bounds + delay = Math.max(minMs * 0.5, Math.min(maxMs * 2, delay)) + await this.page.waitForTimeout(Math.floor(delay)) + this.actionCount++ } /** @@ -53,68 +131,88 @@ export class HumanBehavior { async humanType(locator: import('rebrowser-playwright').Locator, text: string, context: string): Promise { // CRITICAL: Clear field first (human would select all + delete) await locator.clear() - await this.humanDelay(300, 800, context) - - // CRITICAL: Type character by character with VARIABLE delays - // Real humans type at 40-80 WPM = ~150-300ms per character - // But with natural variation: some characters faster, some slower + await this.humanDelay(200, 600, context) log(false, 'CREATOR', `[${context}] ⌨️ Typing: "${text.substring(0, 20)}${text.length > 20 ? '...' : ''}"`, 'log', 'cyan') - // IMPROVED: Generate per-session typing personality (consistent across field) - const typingSpeed = 0.7 + Math.random() * 0.6 // 0.7-1.3x speed multiplier - const errorRate = Math.random() * 0.08 // 0-8% error rate - const burstTyping = Math.random() < 0.3 // 30% chance of burst typing + // Track if we should simulate a typo + let typoMade = false + const shouldMakeTypo = secureRandomBool(this.personality.errorRate * 3) // Once per field max for (let i = 0; i < text.length; i++) { const char: string = text[i] as string - - // CRITICAL: Skip if char is somehow undefined (defensive programming) if (!char) continue - // IMPROVED: More realistic variance based on typing personality + // Determine character delay based on type let charDelay: number - const isFastKey = /[eatino]/i.test(char) - const isSlowKey = /[^a-z]/i.test(char) // Numbers, symbols, etc. - const hasTypo = Math.random() < errorRate // Dynamic typo rate - const isBurst = burstTyping && i > 0 && Math.random() < 0.4 // Burst typing pattern + const isFastKey = /[eatinos]/i.test(char) + const isSlowKey = /[^a-z0-9@.]/i.test(char) // Symbols, uppercase + const isBurst = this.personality.burstTyping && secureRandomBool(0.3) - if (hasTypo) { - // Typo: pause, backspace, retype - charDelay = Math.random() * 500 + 400 // 400-900ms (correcting) - log(false, 'CREATOR', `[${context}] πŸ”„ Typo correction simulation`, 'log', 'gray') - } else if (isBurst) { - // Burst typing: very fast - charDelay = (Math.random() * 50 + 40) * typingSpeed // 40-90ms * speed + if (isBurst && i > 0) { + // Burst typing: very fast sequence + charDelay = typingDelay(30) * this.personality.typingSpeed } else if (isFastKey) { - charDelay = (Math.random() * 80 + 70) * typingSpeed // 70-150ms * speed + charDelay = typingDelay(60) * this.personality.typingSpeed } else if (isSlowKey) { - charDelay = (Math.random() * 250 + 180) * typingSpeed // 180-430ms * speed + charDelay = typingDelay(150) * this.personality.typingSpeed } else { - charDelay = (Math.random() * 120 + 100) * typingSpeed // 100-220ms * speed + charDelay = typingDelay(80) * this.personality.typingSpeed } - // IMPROVED: Random micro-pauses (thinking) - if (Math.random() < 0.05 && i > 0) { - const thinkPause = Math.random() * 800 + 500 // 500-1300ms - await this.page.waitForTimeout(Math.floor(thinkPause)) + // Simulate typo (once per field, if enabled) + if (!typoMade && shouldMakeTypo && i > 2 && i < text.length - 2 && secureRandomBool(0.15)) { + typoMade = true + + // Type wrong character + const wrongChar = String.fromCharCode(char.charCodeAt(0) + secureRandomInt(-1, 1)) + await locator.type(wrongChar, { delay: 0 }) + await this.page.waitForTimeout(secureRandomInt(200, 500)) + + // Pause (realize mistake) + await this.page.waitForTimeout(secureRandomInt(300, 800)) + + // Backspace + await this.page.keyboard.press('Backspace') + await this.page.waitForTimeout(secureRandomInt(100, 300)) + + log(false, 'CREATOR', `[${context}] πŸ”„ Typo correction`, 'log', 'gray') } - await locator.type(char, { delay: 0 }) // Type instantly + // Type the character + await locator.type(char, { delay: 0 }) await this.page.waitForTimeout(Math.floor(charDelay)) + + // Occasional micro-pause (human thinking) + if (secureRandomBool(0.05) && i > 0) { + await this.page.waitForTimeout(secureRandomInt(300, 800)) + } + + // Burst typing: type next 2-3 chars rapidly + if (isBurst && i < text.length - 2) { + const burstLen = secureRandomInt(2, 3) + for (let j = 0; j < burstLen && i + 1 < text.length; j++) { + i++ + const nextChar = text[i] + if (nextChar) { + await locator.type(nextChar, { delay: 0 }) + await this.page.waitForTimeout(secureRandomInt(15, 40)) + } + } + } } log(false, 'CREATOR', `[${context}] βœ… Typing completed`, 'log', 'green') - // IMPROVEMENT: Random pause after typing (human reviewing input) - await this.humanDelay(500, 1500, context) + // Random pause after typing (human reviewing input) + await this.humanDelay(400, 1200, context) } /** * CRITICAL: Simulate micro mouse movements and scrolls * Real humans constantly move mouse and scroll while reading/thinking * - * IMPROVED: Natural variance per session (not every gesture is identical) + * IMPROVED: Uses BΓ©zier curves for natural movement * * @param context Description for logging */ @@ -122,49 +220,69 @@ export class HumanBehavior { try { const gestureNotes: string[] = [] - // IMPROVED: Variable mouse movement probability (not always 60%) - const mouseMoveProb = 0.45 + Math.random() * 0.3 // 45-75% chance + // Mouse movement probability varies by personality + const mouseMoveProb = 0.35 + this.personality.mousePrecision * 0.3 - if (Math.random() < mouseMoveProb) { - // IMPROVED: Wider movement range (more natural) - const x = Math.floor(Math.random() * 400) + 30 // Random x: 30-430px - const y = Math.floor(Math.random() * 300) + 20 // Random y: 20-320px - const steps = Math.floor(Math.random() * 8) + 2 // 2-10 steps (variable smoothness) + if (secureRandomBool(mouseMoveProb)) { + const viewport = this.page.viewportSize() + if (viewport) { + // Get random target + const targetX = secureRandomInt(50, viewport.width - 50) + const targetY = secureRandomInt(50, viewport.height - 50) - await this.page.mouse.move(x, y, { steps }).catch(() => { - // Mouse move failed - page may be closed or unavailable - }) + // Generate BΓ©zier curve path + const startX = secureRandomInt(100, viewport.width / 2) + const startY = secureRandomInt(100, viewport.height / 2) - gestureNotes.push(`mouseβ†’(${x},${y})`) + const path = generateMousePath( + { x: startX, y: startY }, + { x: targetX, y: targetY }, + { + speed: this.personality.mousePrecision, + overshoot: secureRandomBool(0.2) + } + ) - // IMPROVED: Sometimes double-move (human overshoots then corrects) - if (Math.random() < 0.15) { - await this.humanDelay(100, 300, context) - const x2 = x + (Math.random() * 40 - 20) // Β±20px correction - const y2 = y + (Math.random() * 40 - 20) - await this.page.mouse.move(x2, y2, { steps: 2 }).catch(() => { }) - gestureNotes.push(`correctβ†’(${x2},${y2})`) + // Execute path + for (let i = 0; i < path.points.length; i++) { + const point = path.points[i] + if (point) { + await this.page.mouse.move(point.x, point.y).catch(() => { }) + } + const duration = path.durations[i] + if (duration && duration > 0) { + await this.page.waitForTimeout(duration).catch(() => { }) + } + } + + gestureNotes.push(`mouseβ†’(${targetX},${targetY})`) } } - // IMPROVED: Variable scroll probability (not always 30%) - const scrollProb = 0.2 + Math.random() * 0.25 // 20-45% chance + // Scroll probability + const scrollProb = 0.2 + this.personality.pauseTendency * 0.15 - if (Math.random() < scrollProb) { - const direction = Math.random() < 0.65 ? 1 : -1 // 65% down, 35% up - const distance = Math.floor(Math.random() * 300) + 40 // 40-340px (more variance) - const dy = direction * distance + if (secureRandomBool(scrollProb)) { + const direction = secureRandomBool(0.65) ? 1 : -1 + const distance = secureRandomInt(40, 250) * direction - await this.page.mouse.wheel(0, dy).catch(() => { - // Scroll failed - page may be closed or unavailable - }) + // Natural scroll with inertia + const scrollPath = generateScrollPath(distance, { smooth: true }) - gestureNotes.push(`scroll ${direction > 0 ? '↓' : '↑'} ${distance}px`) + for (let i = 0; i < scrollPath.deltas.length; i++) { + const delta = scrollPath.deltas[i] + if (delta) { + await this.page.mouse.wheel(0, delta).catch(() => { }) + } + const duration = scrollPath.durations[i] + if (duration && duration > 0) { + await this.page.waitForTimeout(duration).catch(() => { }) + } + } + + gestureNotes.push(`scroll ${direction > 0 ? '↓' : '↑'} ${Math.abs(distance)}px`) } - // IMPROVED: Sometimes NO gesture at all (humans sometimes just stare) - // Already handled by caller's random probability - if (gestureNotes.length > 0) { log(false, 'CREATOR', `[${context}] ${gestureNotes.join(', ')}`, 'log', 'gray') } @@ -177,6 +295,8 @@ export class HumanBehavior { * CRITICAL: Natural click with human behavior * NEVER use { force: true } - it bypasses visibility checks (bot pattern) * + * IMPROVED: Uses BΓ©zier curve to move to element + * * @param locator Playwright locator (button/link) * @param context Description for logging * @param maxRetries Max click attempts (default: 3) @@ -189,33 +309,57 @@ export class HumanBehavior { ): Promise { for (let attempt = 1; attempt <= maxRetries; attempt++) { try { - // CRITICAL: Move mouse to element first (real humans do this) + // Get element bounding box const box = await locator.boundingBox().catch(() => null) + if (box) { - // Click at random position within element (not always center) - const offsetX = Math.random() * box.width * 0.6 + box.width * 0.2 // 20-80% of width - const offsetY = Math.random() * box.height * 0.6 + box.height * 0.2 // 20-80% of height + // Calculate click position (not always center) + const clickX = box.x + box.width * secureRandomFloat(0.25, 0.75) + const clickY = box.y + box.height * secureRandomFloat(0.25, 0.75) - await this.page.mouse.move( - box.x + offsetX, - box.y + offsetY, - { steps: Math.floor(Math.random() * 3) + 2 } // 2-5 steps - ).catch(() => { }) + // Move to element with BΓ©zier curve + const viewport = this.page.viewportSize() + const startX = viewport ? secureRandomInt(0, viewport.width / 2) : 100 + const startY = viewport ? secureRandomInt(0, viewport.height / 2) : 100 - await this.humanDelay(100, 300, context) // Pause before clicking + const path = generateMousePath( + { x: startX, y: startY }, + { x: clickX, y: clickY }, + { + speed: this.personality.mousePrecision, + overshoot: secureRandomBool(0.15) // Less overshoot for clicks + } + ) + + // Execute path + for (let i = 0; i < path.points.length; i++) { + const point = path.points[i] + if (point) { + await this.page.mouse.move(point.x, point.y).catch(() => { }) + } + const duration = path.durations[i] + if (duration && duration > 0) { + await this.page.waitForTimeout(duration).catch(() => { }) + } + } + + // Pre-click pause (human aims before clicking) + await this.humanDelay(50, 200, context) } - // NATURAL CLICK: No force (respects visibility/interactability) + // Perform click await locator.click({ force: false, timeout: 5000 }) log(false, 'CREATOR', `[${context}] βœ… Clicked successfully`, 'log', 'green') - await this.humanDelay(300, 800, context) // Pause after clicking + + // Post-click pause (human waits for response) + await this.humanDelay(250, 700, context) return true } catch (error) { if (attempt < maxRetries) { log(false, 'CREATOR', `[${context}] ⚠️ Click failed (attempt ${attempt}/${maxRetries}), retrying...`, 'warn', 'yellow') - await this.humanDelay(1000, 2000, context) + await this.humanDelay(800, 1800, context) } else { const msg = error instanceof Error ? error.message : String(error) log(false, 'CREATOR', `[${context}] ❌ Click failed after ${maxRetries} attempts: ${msg}`, 'error') @@ -236,15 +380,16 @@ export class HumanBehavior { async readPage(context: string): Promise { log(false, 'CREATOR', `[${context}] πŸ‘€ Reading page...`, 'log', 'cyan') - // Random scroll movements (humans scroll while reading) - const scrollCount = Math.floor(Math.random() * 3) + 1 // 1-3 scrolls + // Random scroll movements while reading + const scrollCount = secureRandomInt(1, 3) for (let i = 0; i < scrollCount; i++) { await this.microGestures(context) - await this.humanDelay(800, 2000, context) + await this.humanDelay(600, 1800, context) } - // Final reading pause - await this.humanDelay(1500, 3500, context) + // Final reading pause (based on personality reading speed) + const readTime = (50 / this.personality.readingSpeed) * 60000 // ~50 words + await this.humanDelay(readTime * 0.5, readTime * 1.5, context) } /** @@ -261,24 +406,49 @@ export class HumanBehavior { optionLocator: import('rebrowser-playwright').Locator, context: string ): Promise { - // STEP 1: Click dropdown button (with human behavior) + // STEP 1: Click dropdown button const openSuccess = await this.humanClick(buttonLocator, `${context}_OPEN`) if (!openSuccess) return false - // STEP 2: Wait for dropdown to open (visual feedback) - await this.humanDelay(500, 1200, context) + // STEP 2: Wait for dropdown animation + await this.humanDelay(400, 1000, context) - // STEP 3: Move mouse randomly inside dropdown (human reading options) + // STEP 3: Move mouse around (reading options) await this.microGestures(context) - await this.humanDelay(300, 800, context) + await this.humanDelay(200, 600, context) - // STEP 4: Click selected option (with human behavior) + // STEP 4: Click selected option const selectSuccess = await this.humanClick(optionLocator, `${context}_SELECT`) if (!selectSuccess) return false // STEP 5: Wait for dropdown to close - await this.humanDelay(500, 1200, context) + await this.humanDelay(400, 1000, context) return true } + + /** + * Hesitation pause - simulates uncertainty + * Used before important actions where user might reconsider + * + * @param context Description for logging + */ + async hesitate(context: string): Promise { + if (secureRandomBool(0.4 / this.personality.confidenceLevel)) { + const hesitationTime = humanVariance(1500, 0.5) / this.personality.confidenceLevel + log(false, 'CREATOR', `[${context}] πŸ€” Hesitating...`, 'log', 'cyan') + await this.page.waitForTimeout(Math.floor(hesitationTime)) + } + } + + /** + * Get session statistics + */ + getStats(): { actionCount: number; sessionDurationMs: number; personality: CreatorPersonality } { + return { + actionCount: this.actionCount, + sessionDurationMs: Date.now() - this.sessionStart, + personality: this.personality + } + } } diff --git a/src/browser/Browser.ts b/src/browser/Browser.ts index 13fb41b..59ae042 100644 --- a/src/browser/Browser.ts +++ b/src/browser/Browser.ts @@ -5,6 +5,7 @@ import playwright, { BrowserContext } from 'rebrowser-playwright' import { MicrosoftRewardsBot } from '../index' import { AccountProxy } from '../interface/Account' import { updateFingerprintUserAgent } from '../util/browser/UserAgent' +import { getAntiDetectionScript, getTimezoneScript } from '../util/security/AntiDetectionScripts' import { loadSessionData, saveFingerprintData } from '../util/state/Load' import { logFingerprintValidation, validateFingerprintConsistency } from '../util/validation/FingerprintValidator' @@ -143,307 +144,52 @@ class Browser { const globalTimeout = this.bot.config.browser?.globalTimeout ?? 30000 context.setDefaultTimeout(typeof globalTimeout === 'number' ? globalTimeout : this.bot.utils.stringToMs(globalTimeout)) + // CRITICAL: Get anti-detection configuration + const antiDetectConfig = this.bot.config.antiDetection || {} + const timezone = antiDetectConfig.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone + const locale = antiDetectConfig.locale || 'en-US' + const languages = antiDetectConfig.languages || ['en-US', 'en'] + + // Generate comprehensive anti-detection script + const antiDetectScript = getAntiDetectionScript({ + timezone, + locale, + languages, + platform: this.bot.isMobile ? 'Android' : 'Win32', + vendor: 'Google Inc.', + webglVendor: antiDetectConfig.webglVendor || 'Intel Inc.', + webglRenderer: antiDetectConfig.webglRenderer || 'Intel Iris OpenGL Engine' + }) + + // Generate timezone consistency script + const timezoneScript = getTimezoneScript(timezone, locale) + try { context.on('page', async (page) => { try { - // IMPROVED: Randomized viewport sizes to avoid fingerprinting - // Fixed sizes are detectable bot patterns + // CRITICAL: Inject anti-detection scripts BEFORE any page load + await page.addInitScript(antiDetectScript) + await page.addInitScript(timezoneScript) + + // IMPROVED: Use crypto-secure random for viewport sizes + const { secureRandomInt } = await import('../util/security/SecureRandom') + const viewport = this.bot.isMobile ? { // Mobile: Vary between common phone screen sizes - width: 360 + Math.floor(Math.random() * 60), // 360-420px - height: 640 + Math.floor(Math.random() * 256) // 640-896px + width: secureRandomInt(360, 420), + height: secureRandomInt(640, 896) } : { // Desktop: Vary between common desktop resolutions - width: 1280 + Math.floor(Math.random() * 640), // 1280-1920px - height: 720 + Math.floor(Math.random() * 360) // 720-1080px + width: secureRandomInt(1280, 1920), + height: secureRandomInt(720, 1080) } await page.setViewportSize(viewport) - // CRITICAL: Advanced anti-detection scripts (MUST run before page load) + // Add custom CSS for page fitting await page.addInitScript(() => { - // ═══════════════════════════════════════════════════════════════ - // ANTI-DETECTION LAYER 1: Remove automation indicators - // ═══════════════════════════════════════════════════════════════ - - // CRITICAL: Remove navigator.webdriver (biggest bot indicator) - try { - Object.defineProperty(navigator, 'webdriver', { - get: () => undefined, - configurable: true - }) - } catch { /* Already defined */ } - - // CRITICAL: Mask Chrome DevTools Protocol detection - // Microsoft checks for window.chrome.runtime - try { - // @ts-ignore - window.chrome is intentionally injected - if (!window.chrome) { - // @ts-ignore - window.chrome = {} - } - // @ts-ignore - if (!window.chrome.runtime) { - // @ts-ignore - window.chrome.runtime = { - // @ts-ignore - connect: () => { }, - // @ts-ignore - sendMessage: () => { } - } - } - } catch { /* Chrome object may be frozen */ } - - // ═══════════════════════════════════════════════════════════════ - // ANTI-DETECTION LAYER 2: WebGL & Canvas fingerprint randomization - // ═══════════════════════════════════════════════════════════════ - - // CRITICAL: Add noise to Canvas fingerprinting - // Microsoft uses Canvas to detect identical browser instances - try { - const originalToDataURL = HTMLCanvasElement.prototype.toDataURL - const originalGetImageData = CanvasRenderingContext2D.prototype.getImageData - - // Random noise generator (consistent per page load, different per session) - const noise = Math.random() * 0.0001 - - HTMLCanvasElement.prototype.toDataURL = function (...args) { - const context = this.getContext('2d') - if (context) { - // Add imperceptible noise - const imageData = context.getImageData(0, 0, this.width, this.height) - for (let i = 0; i < imageData.data.length; i += 4) { - imageData.data[i] = imageData.data[i]! + noise // R - imageData.data[i + 1] = imageData.data[i + 1]! + noise // G - imageData.data[i + 2] = imageData.data[i + 2]! + noise // B - } - context.putImageData(imageData, 0, 0) - } - return originalToDataURL.apply(this, args) - } - - CanvasRenderingContext2D.prototype.getImageData = function (...args) { - const imageData = originalGetImageData.apply(this, args) - // Add noise to raw pixel data - for (let i = 0; i < imageData.data.length; i += 10) { - imageData.data[i] = imageData.data[i]! + noise - } - return imageData - } - } catch { /* Canvas override may fail in strict mode */ } - - // CRITICAL: WebGL fingerprint randomization - try { - const getParameter = WebGLRenderingContext.prototype.getParameter - WebGLRenderingContext.prototype.getParameter = function (parameter) { - // Randomize UNMASKED_VENDOR_WEBGL and UNMASKED_RENDERER_WEBGL - if (parameter === 37445) { // UNMASKED_VENDOR_WEBGL - return 'Intel Inc.' - } - if (parameter === 37446) { // UNMASKED_RENDERER_WEBGL - return 'Intel Iris OpenGL Engine' - } - return getParameter.apply(this, [parameter]) - } - } catch { /* WebGL override may fail */ } - - // ═══════════════════════════════════════════════════════════════ - // ANTI-DETECTION LAYER 3: Permissions API masking - // ═══════════════════════════════════════════════════════════════ - - // CRITICAL: Mask permissions query (bots have different permissions) - try { - const originalQuery = navigator.permissions.query - // @ts-ignore - navigator.permissions.query = (parameters) => { - // Always return 'prompt' for notifications (human-like) - if (parameters.name === 'notifications') { - return Promise.resolve({ state: 'prompt', onchange: null }) - } - return originalQuery(parameters) - } - } catch { /* Permissions API may not be available */ } - - // ═══════════════════════════════════════════════════════════════ - // ANTI-DETECTION LAYER 4: Plugin/MIME type consistency - // ═══════════════════════════════════════════════════════════════ - - // CRITICAL: Add realistic plugins (headless browsers have none) - try { - Object.defineProperty(navigator, 'plugins', { - get: () => [ - { - name: 'PDF Viewer', - description: 'Portable Document Format', - filename: 'internal-pdf-viewer', - length: 2 - }, - { - name: 'Chrome PDF Viewer', - description: 'Portable Document Format', - filename: 'internal-pdf-viewer', - length: 2 - }, - { - name: 'Chromium PDF Viewer', - description: 'Portable Document Format', - filename: 'internal-pdf-viewer', - length: 2 - } - ] - }) - } catch { /* Plugins may be frozen */ } - - // ═══════════════════════════════════════════════════════════════ - // ANTI-DETECTION LAYER 5: WebRTC Leak Prevention - // ═══════════════════════════════════════════════════════════════ - - // CRITICAL: Prevent WebRTC from leaking real IP address - try { - // Override RTCPeerConnection to prevent IP leaks - const originalRTCPeerConnection = window.RTCPeerConnection - // @ts-ignore - window.RTCPeerConnection = function (config?: RTCConfiguration) { - // Force STUN servers through proxy or disable - const modifiedConfig: RTCConfiguration = { - ...config, - iceServers: [] // Disable ICE to prevent IP leak - } - return new originalRTCPeerConnection(modifiedConfig) - } - // @ts-ignore - window.RTCPeerConnection.prototype = originalRTCPeerConnection.prototype - } catch { /* WebRTC override may fail */ } - - // ═══════════════════════════════════════════════════════════════ - // ANTI-DETECTION LAYER 6: Battery API Spoofing - // ═══════════════════════════════════════════════════════════════ - - // Headless browsers may have unusual battery states - try { - // @ts-ignore - if (navigator.getBattery) { - // @ts-ignore - navigator.getBattery = () => Promise.resolve({ - charging: true, - chargingTime: 0, - dischargingTime: Infinity, - level: 1, - addEventListener: () => { }, - removeEventListener: () => { }, - dispatchEvent: () => true - }) - } - } catch { /* Battery API override may fail */ } - - // ═══════════════════════════════════════════════════════════════ - // ANTI-DETECTION LAYER 7: Hardware Concurrency Consistency - // ═══════════════════════════════════════════════════════════════ - - // Ensure hardware concurrency looks realistic - try { - const realCores = navigator.hardwareConcurrency || 4 - // Round to common values: 2, 4, 6, 8, 12, 16 - const commonCores = [2, 4, 6, 8, 12, 16] - const normalizedCores = commonCores.reduce((prev, curr) => - Math.abs(curr - realCores) < Math.abs(prev - realCores) ? curr : prev - ) - Object.defineProperty(navigator, 'hardwareConcurrency', { - get: () => normalizedCores, - configurable: true - }) - } catch { /* Hardware concurrency override may fail */ } - - // ═══════════════════════════════════════════════════════════════ - // ANTI-DETECTION LAYER 8: Device Memory Consistency - // ═══════════════════════════════════════════════════════════════ - - try { - // @ts-ignore - const realMemory = navigator.deviceMemory || 8 - // Round to common values: 2, 4, 8, 16 - const commonMemory = [2, 4, 8, 16] - const normalizedMemory = commonMemory.reduce((prev, curr) => - Math.abs(curr - realMemory) < Math.abs(prev - realMemory) ? curr : prev - ) - Object.defineProperty(navigator, 'deviceMemory', { - get: () => normalizedMemory, - configurable: true - }) - } catch { /* Device memory override may fail */ } - - // ═══════════════════════════════════════════════════════════════ - // ANTI-DETECTION LAYER 9: Audio Fingerprint Protection - // ═══════════════════════════════════════════════════════════════ - - try { - const originalCreateOscillator = AudioContext.prototype.createOscillator - const originalCreateDynamicsCompressor = AudioContext.prototype.createDynamicsCompressor - - // Add slight randomization to audio context to prevent fingerprinting - AudioContext.prototype.createOscillator = function () { - const oscillator = originalCreateOscillator.apply(this) - const originalGetFloatFrequencyData = AnalyserNode.prototype.getFloatFrequencyData - AnalyserNode.prototype.getFloatFrequencyData = function (array) { - originalGetFloatFrequencyData.apply(this, [array]) - // Add imperceptible noise - for (let i = 0; i < array.length; i++) { - array[i] = array[i]! + (Math.random() * 0.0001) - } - } - return oscillator - } - - AudioContext.prototype.createDynamicsCompressor = function () { - const compressor = originalCreateDynamicsCompressor.apply(this) - // Slightly randomize default values - try { - compressor.threshold.value = -24 + (Math.random() * 0.001) - compressor.knee.value = 30 + (Math.random() * 0.001) - } catch { /* May be read-only */ } - return compressor - } - } catch { /* Audio API override may fail */ } - - // ═══════════════════════════════════════════════════════════════ - // ANTI-DETECTION LAYER 10: Timezone & Locale Consistency - // ═══════════════════════════════════════════════════════════════ - - try { - // Ensure Date.prototype.getTimezoneOffset is consistent - const originalGetTimezoneOffset = Date.prototype.getTimezoneOffset - const consistentOffset = originalGetTimezoneOffset.call(new Date()) - Date.prototype.getTimezoneOffset = function () { - return consistentOffset - } - } catch { /* Timezone override may fail */ } - - // ═══════════════════════════════════════════════════════════════ - // ANTI-DETECTION LAYER 11: Connection Info Spoofing - // ═══════════════════════════════════════════════════════════════ - - try { - // @ts-ignore - if (navigator.connection) { - Object.defineProperty(navigator, 'connection', { - get: () => ({ - effectiveType: '4g', - rtt: 50, - downlink: 10, - saveData: false, - addEventListener: () => { }, - removeEventListener: () => { } - }), - configurable: true - }) - } - } catch { /* Connection API override may fail */ } - - // ═══════════════════════════════════════════════════════════════ - // Standard styling (non-detection related) - // ═══════════════════════════════════════════════════════════════ try { const style = document.createElement('style') style.id = '__mrs_fit_style' @@ -456,6 +202,8 @@ class Browser { document.documentElement.appendChild(style) } catch { /* Non-critical: Style injection may fail if DOM not ready */ } }) + + this.bot.log(this.bot.isMobile, 'BROWSER', `Page configured with 23-layer anti-detection (viewport: ${viewport.width}x${viewport.height})`) } catch (e) { this.bot.log(this.bot.isMobile, 'BROWSER', `Page setup warning: ${e instanceof Error ? e.message : String(e)}`, 'warn') } diff --git a/src/interface/Config.ts b/src/interface/Config.ts index 859a739..b3d08cb 100644 --- a/src/interface/Config.ts +++ b/src/interface/Config.ts @@ -31,6 +31,7 @@ export interface Config { dashboard?: ConfigDashboard; // Local web dashboard for monitoring and control scheduling?: ConfigScheduling; // Automatic scheduler configuration (cron/Task Scheduler) errorReporting?: ConfigErrorReporting; // Automatic error reporting to community webhook + antiDetection?: ConfigAntiDetection; // Advanced anti-detection configuration } export interface ConfigSaveFingerprint { @@ -212,3 +213,28 @@ export interface ConfigScheduling { export interface ConfigErrorReporting { enabled?: boolean; // enable automatic error reporting to community webhook (default: true) } + +/** + * Advanced anti-detection configuration for browser fingerprint spoofing. + * These values override fingerprint-generator defaults for consistency. + */ +export interface ConfigAntiDetection { + /** Timezone override (e.g., "America/New_York", "Europe/Paris") */ + timezone?: string; + /** Locale override (e.g., "en-US", "fr-FR") */ + locale?: string; + /** Browser languages array (e.g., ["en-US", "en"]) */ + languages?: string[]; + /** WebGL vendor string override */ + webglVendor?: string; + /** WebGL renderer string override */ + webglRenderer?: string; + /** Enable canvas noise injection (default: true) */ + canvasNoise?: boolean; + /** Enable WebGL parameter spoofing (default: true) */ + webglNoise?: boolean; + /** Enable audio fingerprint protection (default: true) */ + audioNoise?: boolean; + /** Enable WebRTC IP leak protection (default: true) */ + webrtcProtection?: boolean; +} diff --git a/src/util/browser/Humanizer.ts b/src/util/browser/Humanizer.ts index e54ec1b..dcaec80 100644 --- a/src/util/browser/Humanizer.ts +++ b/src/util/browser/Humanizer.ts @@ -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 { - 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 { + 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 { + 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 { + 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 { 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 { + 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 { + 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 { + 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 { + 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 { + 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 } } diff --git a/src/util/security/AntiDetectionScripts.ts b/src/util/security/AntiDetectionScripts.ts new file mode 100644 index 0000000..bca1df2 --- /dev/null +++ b/src/util/security/AntiDetectionScripts.ts @@ -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 +} diff --git a/src/util/security/NaturalMouse.ts b/src/util/security/NaturalMouse.ts new file mode 100644 index 0000000..29f503d --- /dev/null +++ b/src/util/security/NaturalMouse.ts @@ -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 +} diff --git a/src/util/security/SecureRandom.ts b/src/util/security/SecureRandom.ts new file mode 100644 index 0000000..5e3fc16 --- /dev/null +++ b/src/util/security/SecureRandom.ts @@ -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(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(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 +}