storageDir)) { mkdir($this->storageDir, 0700, true); } if (!file_exists($this->backupDir)) { mkdir($this->backupDir, 0700, true); } } /** * Main download function with all security checks */ public function downloadRemailers() { // Download both stats and keyring $statsResult = $this->downloadStats(); $keyringResult = $this->downloadKeyring(); return $statsResult && $keyringResult; } /** * Download remailer stats (mlist.txt) */ private function downloadStats() { // Check if update is needed (with randomized interval) if (!$this->needsUpdate($this->remailersFile)) { $this->log("Stats update not needed yet"); return true; } // Add random delay to prevent timing correlation $this->randomDelay(5, 60); // Verify Tor is working if (!$this->checkTorConnection()) { $this->log("ERROR: Tor connection not available", 'ERROR'); return false; } // Backup current file $this->backupCurrentFile($this->remailersFile, 'remailers'); // Try each pinger source $success = false; foreach ($this->pingers as $url) { $this->log("Attempting stats download from: $url"); // Random delay between attempts $this->randomDelay(3, 20); if ($this->downloadFromSource($url, $this->remailersFile, 'stats')) { $success = true; break; } } if (!$success) { $this->log("ERROR: Failed to download stats from all sources", 'ERROR'); $this->restoreBackup($this->remailersFile, 'remailers'); return false; } $this->log("Remailer stats updated successfully"); $this->cleanOldBackups('remailers'); return true; } /** * Download public keyring (pubring.mix) */ private function downloadKeyring() { // Check if update is needed (with randomized interval) if (!$this->needsUpdate($this->pubringFile)) { $this->log("Keyring update not needed yet"); return true; } // Add random delay to prevent timing correlation $this->randomDelay(5, 60); // Verify Tor is working if (!$this->checkTorConnection()) { $this->log("ERROR: Tor connection not available", 'ERROR'); return false; } // Backup current file $this->backupCurrentFile($this->pubringFile, 'pubring'); // Build pubring URLs $pubringUrls = []; foreach ($this->pingers as $statsUrl) { // Replace mlist.txt or mlist2.txt with pubring.mix $pubringUrl = preg_replace('/mlist2?\.txt$/', 'pubring.mix', $statsUrl); $pubringUrls[] = $pubringUrl; } // Try each pinger source $success = false; foreach ($pubringUrls as $url) { $this->log("Attempting keyring download from: $url"); // Random delay between attempts $this->randomDelay(3, 20); if ($this->downloadFromSource($url, $this->pubringFile, 'keyring')) { $success = true; break; } } if (!$success) { $this->log("ERROR: Failed to download keyring from all sources", 'ERROR'); $this->restoreBackup($this->pubringFile, 'pubring'); return false; } $this->log("Public keyring updated successfully"); $this->cleanOldBackups('pubring'); return true; } /** * Check if update is needed with randomized interval */ private function needsUpdate($filepath) { if (!file_exists($filepath)) { return true; } $fileAge = time() - filemtime($filepath); // Random threshold between 20-28 hours $threshold = mt_rand($this->minUpdateInterval, $this->maxUpdateInterval); return $fileAge > $threshold; } /** * Random delay to prevent timing attacks */ private function randomDelay($minSeconds, $maxSeconds) { $delay = mt_rand($minSeconds * 1000000, $maxSeconds * 1000000); usleep($delay); } /** * Verify Tor is working */ private function checkTorConnection() { $context = stream_context_create([ 'http' => [ 'proxy' => "socks5://{$this->torProxy}", 'request_fulluri' => true, 'timeout' => 30 ] ]); try { $result = @file_get_contents( 'https://check.torproject.org/api/ip', false, $context ); if ($result) { $data = json_decode($result, true); if (isset($data['IsTor']) && $data['IsTor'] === true) { $this->log("Tor connection verified"); return true; } } } catch (Exception $e) { $this->log("Tor check failed: " . $e->getMessage(), 'ERROR'); } return false; } /** * Download from a specific source via Tor */ private function downloadFromSource($url, $destinationFile, $type = 'stats') { $tempFile = $this->storageDir . '/' . $type . '_' . bin2hex(random_bytes(8)) . '.tmp'; // Configure cURL for Tor SOCKS5 proxy $ch = curl_init(); curl_setopt_array($ch, [ CURLOPT_URL => $url, CURLOPT_RETURNTRANSFER => true, CURLOPT_PROXY => $this->torProxy, CURLOPT_PROXYTYPE => CURLPROXY_SOCKS5_HOSTNAME, CURLOPT_TIMEOUT => 120, CURLOPT_CONNECTTIMEOUT => 60, CURLOPT_FOLLOWLOCATION => false, CURLOPT_MAXREDIRS => 0, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_USERAGENT => $this->getRandomUserAgent(), // Anti-fingerprinting headers CURLOPT_HTTPHEADER => [ 'Accept: text/plain,*/*', 'Accept-Language: en-US,en;q=0.9', 'Accept-Encoding: gzip, deflate', 'DNT: 1', 'Connection: close' ] ]); $content = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); $error = curl_error($ch); curl_close($ch); if ($content === false || $httpCode !== 200) { $this->log("Download failed from $url: $error (HTTP $httpCode)", 'ERROR'); return false; } // Validate content based on type if ($type === 'stats') { if (!$this->validateRemailerList($content)) { $this->log("Invalid stats content from $url", 'ERROR'); return false; } } elseif ($type === 'keyring') { if (!$this->validateKeyring($content)) { $this->log("Invalid keyring content from $url", 'ERROR'); return false; } } // Write to temp file with secure permissions file_put_contents($tempFile, $content); chmod($tempFile, 0600); // Verify GPG signature if available if ($this->verifyGPG) { if (!$this->verifySignature($tempFile, $url)) { unlink($tempFile); return false; } } // Move to final location rename($tempFile, $destinationFile); chmod($destinationFile, 0600); $this->log("Successfully downloaded $type from $url"); return true; } /** * Validate remailer list content */ private function validateRemailerList($content) { // Check minimum size if (strlen($content) < 100) { return false; } // Check for typical remailer list patterns $patterns = [ '/mixmaster/i', '/history/i', '/latency/i', '/uptime/i', '/\*{3,}/' // Stars for reliability ]; $matches = 0; foreach ($patterns as $pattern) { if (preg_match($pattern, $content)) { $matches++; } } // At least 2 patterns should match return $matches >= 2; } /** * Validate keyring content (pubring.mix) */ private function validateKeyring($content) { // Check minimum size (keyring should be substantial) if (strlen($content) < 1000) { $this->log("Keyring too small: " . strlen($content) . " bytes", 'ERROR'); return false; } // Check for PGP/GPG key markers or binary key format $validPatterns = [ '/BEGIN PGP/i', // PGP format '/-----BEGIN/i', // Generic armored format '/remailer-key/i', // Remailer key marker ]; // Also check if it's binary (Mixmaster format) // Pubring.mix can be binary or ASCII armored $isBinary = !mb_detect_encoding($content, 'ASCII', true); $hasPattern = false; foreach ($validPatterns as $pattern) { if (preg_match($pattern, $content)) { $hasPattern = true; break; } } // Valid if either has expected patterns OR is binary if (!$hasPattern && !$isBinary) { $this->log("Keyring validation failed: no key markers found", 'ERROR'); return false; } // Additional validation: check for multiple key entries // A valid keyring should have multiple remailer keys $keyCount = substr_count($content, 'remailer-key') + substr_count($content, 'BEGIN PGP') + substr_count($content, '-----BEGIN'); if ($keyCount > 0 && $keyCount < 3) { $this->log("Keyring has too few keys: $keyCount", 'WARNING'); // Don't fail, but log warning } $this->log("Keyring validation passed: " . strlen($content) . " bytes, $keyCount keys"); return true; } /** * Verify GPG signature (if enabled) */ private function verifySignature($file, $url) { $sigUrl = $url . '.asc'; $sigFile = $file . '.asc'; // Download signature $ch = curl_init(); curl_setopt_array($ch, [ CURLOPT_URL => $sigUrl, CURLOPT_RETURNTRANSFER => true, CURLOPT_PROXY => $this->torProxy, CURLOPT_PROXYTYPE => CURLPROXY_SOCKS5_HOSTNAME, CURLOPT_TIMEOUT => 60 ]); $signature = curl_exec($ch); curl_close($ch); if (!$signature) { $this->log("No signature available for $url", 'WARNING'); return true; // Continue without signature } file_put_contents($sigFile, $signature); // Verify with GPG $output = []; $returnVar = 0; exec("gpg --verify " . escapeshellarg($sigFile) . " " . escapeshellarg($file) . " 2>&1", $output, $returnVar); unlink($sigFile); if ($returnVar === 0) { $this->log("GPG signature verified"); return true; } else { $this->log("GPG signature verification failed", 'ERROR'); return false; } } /** * Backup current file */ private function backupCurrentFile($filepath, $prefix = 'file') { if (file_exists($filepath)) { $timestamp = date('Ymd_His'); $backupFile = $this->backupDir . "/{$prefix}_$timestamp.bak"; copy($filepath, $backupFile); chmod($backupFile, 0600); $this->log("Backup created: $backupFile"); } } /** * Restore from backup */ private function restoreBackup($filepath, $prefix = 'file') { $backups = glob($this->backupDir . "/{$prefix}_*.bak"); if (empty($backups)) { $this->log("No backup available to restore for $prefix", 'ERROR'); return false; } // Sort by modification time (newest first) usort($backups, function($a, $b) { return filemtime($b) - filemtime($a); }); $latestBackup = $backups[0]; copy($latestBackup, $filepath); $this->log("Restored backup from: $latestBackup"); return true; } /** * Clean old backups (keep last 10) */ private function cleanOldBackups($prefix = 'file') { $backups = glob($this->backupDir . "/{$prefix}_*.bak"); if (count($backups) <= 10) { return; } usort($backups, function($a, $b) { return filemtime($b) - filemtime($a); }); $toDelete = array_slice($backups, 10); foreach ($toDelete as $file) { unlink($file); } $this->log("Cleaned " . count($toDelete) . " old $prefix backups"); } /** * Get random user agent to avoid fingerprinting */ private function getRandomUserAgent() { $agents = [ 'Mozilla/5.0 (Windows NT 10.0; rv:109.0) Gecko/20100101 Firefox/115.0', 'Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/115.0' ]; return $agents[array_rand($agents)]; } /** * Secure logging without metadata leakage */ private function log($message, $level = 'INFO') { // Don't log to file if it could leak metadata // For production: send to syslog or use in-memory logging $timestamp = date('Y-m-d H:i:s'); $logEntry = "[$timestamp] [$level] $message\n"; // Only log to file if explicitly enabled if (getenv('YAMN_ENABLE_LOGGING') === 'true') { file_put_contents($this->logFile, $logEntry, FILE_APPEND | LOCK_EX); } // For debugging (remove in production) if ($level === 'ERROR') { error_log($logEntry); } } /** * Get current remailer list (public method) */ public function getRemailerList() { if (!file_exists($this->remailersFile)) { $this->downloadStats(); } if (file_exists($this->remailersFile)) { return file_get_contents($this->remailersFile); } return false; } /** * Get current public keyring (public method) */ public function getPublicKeyring() { if (!file_exists($this->pubringFile)) { $this->downloadKeyring(); } if (file_exists($this->pubringFile)) { return file_get_contents($this->pubringFile); } return false; } /** * Force update of both stats and keyring */ public function forceUpdate() { $statsResult = $this->downloadStats(); $keyringResult = $this->downloadKeyring(); return $statsResult && $keyringResult; } /** * Get status of downloaded files */ public function getStatus() { $status = [ 'stats' => [ 'exists' => file_exists($this->remailersFile), 'size' => file_exists($this->remailersFile) ? filesize($this->remailersFile) : 0, 'age_hours' => file_exists($this->remailersFile) ? round((time() - filemtime($this->remailersFile)) / 3600, 1) : null, 'last_modified' => file_exists($this->remailersFile) ? date('Y-m-d H:i:s', filemtime($this->remailersFile)) : null ], 'keyring' => [ 'exists' => file_exists($this->pubringFile), 'size' => file_exists($this->pubringFile) ? filesize($this->pubringFile) : 0, 'age_hours' => file_exists($this->pubringFile) ? round((time() - filemtime($this->pubringFile)) / 3600, 1) : null, 'last_modified' => file_exists($this->pubringFile) ? date('Y-m-d H:i:s', filemtime($this->pubringFile)) : null ], 'tor_available' => $this->checkTorConnection() ]; return $status; } } // Usage try { $downloader = new SecureRemailerDownloader(); $downloader->downloadRemailers(); } catch (Exception $e) { error_log("Remailer download failed: " . $e->getMessage()); } ?>