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