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:
2025-12-16 22:22:38 +01:00
parent 704c271b91
commit 9d9b391cd1
7 changed files with 1883 additions and 407 deletions

View File

@@ -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<void> {
// 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) {
// 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<void> {
// 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,48 +220,68 @@ 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(() => { })
}
}
// IMPROVED: Variable scroll probability (not always 30%)
const scrollProb = 0.2 + Math.random() * 0.25 // 20-45% chance
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
await this.page.mouse.wheel(0, dy).catch(() => {
// Scroll failed - page may be closed or unavailable
})
gestureNotes.push(`scroll ${direction > 0 ? '↓' : '↑'} ${distance}px`)
gestureNotes.push(`mouse→(${targetX},${targetY})`)
}
}
// IMPROVED: Sometimes NO gesture at all (humans sometimes just stare)
// Already handled by caller's random probability
// Scroll probability
const scrollProb = 0.2 + this.personality.pauseTendency * 0.15
if (secureRandomBool(scrollProb)) {
const direction = secureRandomBool(0.65) ? 1 : -1
const distance = secureRandomInt(40, 250) * direction
// Natural scroll with inertia
const scrollPath = generateScrollPath(distance, { smooth: true })
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`)
}
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<boolean> {
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(() => { })
}
}
// NATURAL CLICK: No force (respects visibility/interactability)
// Pre-click pause (human aims before clicking)
await this.humanDelay(50, 200, context)
}
// 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<void> {
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<boolean> {
// 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<void> {
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
}
}
}

View File

@@ -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')
}

View File

@@ -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;
}

View File

@@ -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
})
/**
* 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()
}
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
}
/**
* 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 {
// Gesture execution failed - not critical for operation
// 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
}
}

View 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
}

View 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
}

View 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
}