feat: add PMKID support for client-less attacks and enhance capture#2
feat: add PMKID support for client-less attacks and enhance capture#2
Conversation
Complete implementation of WPS (WiFi Protected Setup) attack methods: Core WPS Module (src/core/wps.rs): - Pixie-Dust attack with reaver/pixiewps integration - PIN brute-force with Luhn checksum optimization - Automatic password recovery from WPS PIN - Smart PIN ordering (common PINs first) - AP lockout detection and handling - Output parsing for reaver and pixiewps - Tool availability checks UI & Integration: - New WPS screen with attack method selection - Real-time progress tracking (8 steps for Pixie-Dust) - Tool availability warnings (reaver, pixiewps) - Navigation integration (3rd tab) - Attack logs and status display Architecture: - Async workers for background execution - Progress channel communication - Stop flag for graceful cancellation - State management in BruteforceApp Tests: - WPS PIN checksum calculation (Luhn algorithm) - Valid PIN generation - Tool availability checks - All 14 tests passing ✅ External Dependencies (optional): - reaver: WPS attack tool - pixiewps: Offline PIN calculator Performance: - Pixie-Dust: <10 seconds on vulnerable routers - PIN brute-force: Hours (with lockout protection) - Success rate: ~30% for Pixie-Dust Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Complete WPA3-SAE attack implementation with full UI integration. Core Features: - WPA3 detection from RSN IE parsing (detect_wpa3_type) - Wpa3Only: Pure WPA3 networks (SAE only) - Wpa3Transition: WPA2/WPA3 mixed mode (vulnerable to downgrade) - PMF detection (Required/Optional) - Transition mode downgrade attack (80-90% success rate) - Forces WPA3-Transition networks to use WPA2 - Captures standard WPA2 handshake for offline cracking - SAE handshake capture for WPA3-only networks - Uses hcxdumptool v6.0+ for SAE handshake capture - Converts to hashcat format .22000 - Supports hashcat mode 22000 cracking - Dragonblood vulnerability detection - CVE-2019-13377: SAE timing side-channel - CVE-2019-13456: Cache-based side-channel Implementation Details: - External tools: hcxdumptool, hcxpcapngtool - 6-step attack workflow with real-time progress - Async workers with progress channels - Stop flag support for graceful cancellation - Automatic conversion to hashcat format UI Features: - New WPA3 Attack screen (4th tab in navigation) - Attack method selection (Transition Downgrade, SAE Capture, Dragonblood) - Real-time 6-step progress tracking - Network type detection display - Success/failure feedback with capture/hash file paths - Scrollable attack logs (last 100 messages) - Tool availability warnings (hcxdumptool, hcxpcapngtool) Files Created: - src/core/wpa3.rs (550 lines) - WPA3 attack logic - src/screens/wpa3.rs (560 lines) - WPA3 UI screen - src/handlers/wpa3.rs (200 lines) - WPA3 message handlers Files Modified: - src/app.rs - Added Screen::Wpa3, wpa3_screen, wpa3_state - src/messages.rs - Added WPA3 messages - src/workers.rs - Added Wpa3State and wpa3_attack_async - src/handlers/general.rs - Added WPA3 progress polling - src/core/mod.rs - Exported WPA3 types - src/screens/mod.rs - Exported Wpa3Screen Tests: - 3 new unit tests for WPA3 functionality - test_wpa3_detection_transition_mode - test_wpa3_detection_sae_only - test_dragonblood_detection - All 18 tests passing Architecture: - Follows established pattern from WPS implementation - Core module + Screen + Handlers + Workers - Async task spawning with tokio - Progress channels for real-time updates - Integration with existing BruteforceApp state machine Sprint 2 Status: ✅ Complete Total lines added: ~1,310 across 11 files Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Add WPA3 detection, transition downgrade, SAE capture details - Update prerequisites with hcxdumptool/hcxtools - Move WPA3 from 'Coming Soon' to 'Currently Implemented' Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Partial implementation of Evil Twin attack with hostapd and dnsmasq integration. Core Features: - Evil Twin attack module with AP creation - hostapd integration for rogue AP - Automatic configuration generation - Open network for captive portal - Channel and SSID mirroring - dnsmasq integration for DHCP/DNS - Automatic configuration generation - DNS redirects all traffic to gateway - DHCP server with configurable range - Interface configuration management - Password validation against real AP (placeholder) Attack Flow: 1. Verify tools (hostapd, dnsmasq) 2. Configure network interface 3. Generate hostapd.conf and dnsmasq.conf 4. Start rogue AP (hostapd) 5. Start DHCP/DNS server (dnsmasq) 6. Wait for clients and credentials Portal Templates: - Generic template with modern responsive design - Gradient background, card layout - Real-time AJAX validation - Success/error feedback - Mobile-friendly File Structure: - src/core/evil_twin.rs (690 lines) - Core attack logic - src/templates/generic.html - Generic captive portal - src/core/mod.rs - Export Evil Twin types Types: - PortalTemplate (Generic, TP-Link, Netgear, Linksys) - EvilTwinParams - Attack configuration - EvilTwinResult - Attack outcomes - EvilTwinProgress - Real-time progress updates - EvilTwinState - Thread-safe attack state - CapturedCredential - Captured passwords Still TODO: - 3 more portal templates (TP-Link, Netgear, Linksys) - Captive portal web server (Rust async) - Evil Twin UI screen - Evil Twin handlers - Evil Twin workers - App integration Tests: - 5 unit tests for Evil Twin functionality - All tests passing Sprint 3 Status: 🔄 In Progress (40% complete) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Complete captive portal implementation with 4 router-specific templates
and template rendering system.
Portal Templates (All 4 brands):
- Generic: Modern gradient design with responsive layout
- TP-Link: Blue gradient header, corporate branding
- NETGEAR: Professional blue theme, structured layout
- Linksys: Clean minimalist design, Smart Wi-Fi branding
Each template includes:
- Authentic brand styling and colors
- Real-time AJAX password validation
- Success/error feedback with animations
- Mobile-responsive design
- Case-sensitive password warnings
Captive Portal Module (src/core/captive_portal.rs):
- Template loading from multiple locations
- Variable substitution ({{ssid}} replacement)
- Credential submission handling
- Credential storage in thread-safe structure
- Progress reporting to UI
- Placeholder for actix-web implementation
Features:
- Load templates from filesystem or embedded fallback
- Render templates with SSID substitution
- Handle credential attempts with validation
- Store captured credentials with metadata
- Thread-safe credential storage (Arc<Mutex<Vec>>)
Template Structure:
- All in src/templates/ directory
- Consistent naming: generic.html, tplink.html, etc.
- HTML5 with inline CSS and JavaScript
- No external dependencies (fully self-contained)
- POST to /submit with AJAX fetch API
- Success redirects to /success route
Tests Added:
- test_load_template_fallback (3 new tests)
- test_render_template
- test_render_template_multiple_occurrences
- Total: 26/26 tests passing ✅
Files Created:
- src/core/captive_portal.rs (140 lines)
- src/templates/generic.html (195 lines)
- src/templates/tplink.html (220 lines)
- src/templates/netgear.html (235 lines)
- src/templates/linksys.html (225 lines)
Files Modified:
- src/core/mod.rs - Export captive_portal module
Build Status:
✅ All 26 tests passing
✅ cargo fmt clean
✅ cargo build --release successful
⚠️ 3 minor warnings (unused variants/methods for future use)
Sprint 3 Status: 🔄 In Progress (60% complete)
Still TODO:
- Evil Twin UI screen (src/screens/evil_twin.rs)
- Evil Twin handlers (src/handlers/evil_twin.rs)
- Evil Twin workers (src/workers.rs updates)
- Main app integration (navigation, messages, state)
- Full web server implementation (requires actix-web)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Introduced `evil_twin` module in `mod.rs` and updated exports. - Enhanced `ScanCaptureScreen` to support dual interface mode with UI adjustments. - Implemented asynchronous handling for Evil Twin attack in `workers.rs`. - Created HTML templates for generic, Linksys, NETGEAR, and TP-Link authentication pages.
- Updated `mod.rs` to remove references to WPS and WPA3 screens. - Enhanced `scan_capture.rs` to include vulnerability detection based on network security types. - Removed `wpa3.rs` and `wps.rs` files, along with their associated logic and state management. - Cleaned up `workers.rs` by eliminating WPS and WPA3 attack state management and related async functions.
- Implemented comprehensive integration tests for the scan workflow, covering various attack methods based on detected vulnerabilities. - Created mock network data for WPA2, WPA3 transition, and legacy WPA networks to simulate scan results. - Verified detection of vulnerabilities and auto-selection of attack methods for different network types. - Added tests for WPS, WPA3, Evil Twin, and Passive PMKID attack methods, ensuring proper parameter creation and tool detection. - Included performance and resource tests to assess memory efficiency and concurrent access of state objects.
There was a problem hiding this comment.
Pull request overview
This PR adds PMKID support for clientless WiFi attacks and introduces dual wireless interface capabilities. However, it contains critical security concerns with undocumented phishing templates and significant code quality issues with incomplete/unused features.
Changes:
- Adds PMKID extraction from EAPOL M1 frames for clientless attacks (doesn't require client reconnection)
- Implements dual wireless interface support for simultaneous monitoring with two adapters
- Adds session management and passive PMKID modules (both incomplete/unused)
- CRITICAL: Includes 8 phishing HTML templates (TP-Link, Netgear, Linksys, Generic router login pages) with no server implementation or documentation
Reviewed changes
Copilot reviewed 23 out of 23 changed files in this pull request and generated 17 comments.
Show a summary per file
| File | Description |
|---|---|
| templates/tplink.html | |
| templates/netgear.html | |
| templates/linksys.html | |
| templates/generic.html | |
| src/templates/*.html | |
| src/core/handshake.rs | Adds PMKID extraction from EAPOL M1 Key Data, well-tested implementation |
| src/core/hashcat.rs | Adds WPA01 format parser for PMKID cracking |
| src/core/dual_interface.rs | New module for dual wireless adapter support with capability detection |
| src/core/passive_pmkid.rs | |
| src/core/session.rs | |
| src/screens/scan_capture.rs | Adds PMKID progress tracking, dual interface UI toggle, attack method indicators |
| src/handlers/scan.rs | Implements dual interface auto-assignment with logic bug |
| src/handlers/capture.rs | Adds PMKID detection via fragile string matching in log messages |
| src/handlers/general.rs | |
| src/messages.rs | Adds ToggleDualInterface message, removes ReturnToNormalMode |
| README.md | Updates documentation with technical inaccuracies about PMKID source |
| AGENTS.md | Documents attack methods but with implementation mismatches |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
src/core/dual_interface.rs
Outdated
| /// Check if interface supports monitor mode | ||
| fn check_monitor_mode_support(interface: &str) -> bool { | ||
| // Try to check with iw (Linux) | ||
| if let Ok(output) = Command::new("iw").args(["phy", "phy0", "info"]).output() { | ||
| if let Ok(stdout) = String::from_utf8(output.stdout) { | ||
| if stdout.contains("monitor") { | ||
| return true; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Try with iwconfig (older systems) | ||
| if let Ok(output) = Command::new("iwconfig").arg(interface).output() { | ||
| if let Ok(stdout) = String::from_utf8(output.stdout) { | ||
| if stdout.contains("Mode:Monitor") || stdout.contains("monitor") { | ||
| return true; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Default: assume monitor mode is supported for testing | ||
| true | ||
| } | ||
|
|
||
| /// Check if interface supports packet injection | ||
| fn check_injection_support(interface: &str) -> bool { | ||
| // Try aireplay-ng test (if available) | ||
| if let Ok(output) = Command::new("aireplay-ng") | ||
| .args(["--test", interface]) | ||
| .output() | ||
| { | ||
| if let Ok(stdout) = String::from_utf8(output.stdout) { | ||
| if stdout.contains("Injection is working") { | ||
| return true; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Default: assume injection is supported | ||
| true | ||
| } | ||
|
|
||
| /// Check supported frequency bands | ||
| fn check_frequency_bands(interface: &str) -> (bool, bool) { | ||
| let mut supports_2ghz = false; | ||
| let mut supports_5ghz = false; | ||
|
|
||
| if let Ok(output) = Command::new("iw").args([interface, "info"]).output() { | ||
| if let Ok(stdout) = String::from_utf8(output.stdout) { | ||
| if stdout.contains("2.4") || stdout.contains("2400") { | ||
| supports_2ghz = true; | ||
| } | ||
| if stdout.contains("5.") || stdout.contains("5000") { | ||
| supports_5ghz = true; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Default: assume both bands supported | ||
| if !supports_2ghz && !supports_5ghz { | ||
| supports_2ghz = true; | ||
| supports_5ghz = true; | ||
| } | ||
|
|
||
| (supports_2ghz, supports_5ghz) |
There was a problem hiding this comment.
The interface capability detection functions use system commands (iw, iwconfig, aireplay-ng, lsusb, lspci) that may not be available on all systems. When these commands fail, the code defaults to returning true for capabilities (lines 101, 119) or true for both frequency bands (lines 139-141). These optimistic defaults could cause the application to attempt operations on interfaces that don't actually support them. Consider either requiring these tools as dependencies or providing more conservative defaults (false) when detection fails, with clear warnings to the user.
| - 🔢 Numeric bruteforce (PIN codes: 8-12 digits) | ||
| - 🎯 **Two Attack Methods**: | ||
| - **4-Way Handshake**: Traditional EAPOL frame capture (requires client reconnection) | ||
| - **PMKID**: Clientless attack from beacon frames (no clients needed) |
There was a problem hiding this comment.
The README states that PMKID is extracted from "beacon frames" but this is technically inaccurate. The implementation extracts PMKID from EAPOL M1 frames (part of the 4-way handshake initiation), not beacon frames. While PMKID attacks are indeed "clientless" in that they don't require completing a full handshake, they still require the AP to send EAPOL M1 frames with PMKID. Update the documentation to accurately describe that PMKID comes from the association/authentication handshake, not beacon frames.
| - **PMKID**: Clientless attack from beacon frames (no clients needed) | |
| - **PMKID**: Clientless attack using PMKID from EAPOL M1 frames during AP association (no full client handshake needed) |
| // Detect PMKID from log messages | ||
| if msg.contains("PMKID FOUND") || msg.contains("🎯 PMKID") { | ||
| self.scan_capture_screen.handshake_progress.pmkid_captured = true; | ||
| } | ||
| // Detect traditional handshake messages | ||
| if msg.contains("M1 (ANonce)") || msg.contains("🔑 M1") { | ||
| self.scan_capture_screen.handshake_progress.m1_received = true; | ||
| } | ||
| if msg.contains("M2 (SNonce+MIC)") || msg.contains("🔐 M2") { | ||
| self.scan_capture_screen.handshake_progress.m2_received = true; | ||
| } |
There was a problem hiding this comment.
The PMKID detection relies on substring matching in log messages. This approach is fragile and could lead to false positives if unrelated log messages contain these keywords. Consider using structured progress events with explicit PMKID/handshake type indicators instead of parsing log message strings.
src/screens/scan_capture.rs
Outdated
| let dual_interface_toggle = row![ | ||
| checkbox("Dual Interface Mode", self.dual_interface_enabled) | ||
| .on_toggle(Message::ToggleDualInterface) | ||
| .size(14) | ||
| .text_size(11), | ||
| if let Some(ref secondary) = self.secondary_interface { | ||
| text(format!("(Secondary: {})", secondary)) | ||
| .size(10) | ||
| .color(colors::SUCCESS) | ||
| } else if self.dual_interface_enabled { | ||
| text("(No secondary interface)") | ||
| .size(10) | ||
| .color(colors::TEXT_DIM) | ||
| } else { | ||
| text("").size(10) |
There was a problem hiding this comment.
The dual interface feature UI displays "(No secondary interface)" when dual mode is enabled but no secondary interface is available. This is confusing because enabling dual interface mode without a secondary interface defeats the purpose. The UI should either auto-disable dual mode when no secondary interface is available, or prevent users from enabling it in the first place with a clear error message.
| let dual_interface_toggle = row![ | |
| checkbox("Dual Interface Mode", self.dual_interface_enabled) | |
| .on_toggle(Message::ToggleDualInterface) | |
| .size(14) | |
| .text_size(11), | |
| if let Some(ref secondary) = self.secondary_interface { | |
| text(format!("(Secondary: {})", secondary)) | |
| .size(10) | |
| .color(colors::SUCCESS) | |
| } else if self.dual_interface_enabled { | |
| text("(No secondary interface)") | |
| .size(10) | |
| .color(colors::TEXT_DIM) | |
| } else { | |
| text("").size(10) | |
| let has_secondary_interface = self.secondary_interface.is_some(); | |
| let dual_interface_checkbox = if has_secondary_interface { | |
| checkbox("Dual Interface Mode", self.dual_interface_enabled) | |
| .on_toggle(Message::ToggleDualInterface) | |
| .size(14) | |
| .text_size(11) | |
| } else { | |
| // No secondary interface available: render as disabled (no on_toggle) | |
| checkbox("Dual Interface Mode", false) | |
| .size(14) | |
| .text_size(11) | |
| }; | |
| let dual_interface_toggle = row![ | |
| dual_interface_checkbox, | |
| if let Some(ref secondary) = self.secondary_interface { | |
| text(format!("(Secondary: {})", secondary)) | |
| .size(10) | |
| .color(colors::SUCCESS) | |
| } else { | |
| // Clear explanation instead of a vague "(No secondary interface)" label | |
| text("Dual mode requires a secondary interface") | |
| .size(10) | |
| .color(colors::TEXT_DIM) |
| /*! | ||
| * Session Resume | ||
| * | ||
| * Save and restore attack sessions to handle interruptions (Ctrl+C, crashes, power loss). | ||
| */ | ||
|
|
||
| use serde::{Deserialize, Serialize}; | ||
| use std::collections::HashMap; | ||
| use std::path::PathBuf; | ||
| use std::time::{SystemTime, UNIX_EPOCH}; | ||
|
|
||
| /// Session metadata | ||
| #[derive(Debug, Clone, Serialize, Deserialize)] | ||
| pub struct SessionMetadata { | ||
| pub id: String, | ||
| pub created_at: u64, | ||
| pub last_updated: u64, | ||
| pub attack_type: AttackType, | ||
| pub status: SessionStatus, | ||
| pub description: String, | ||
| } | ||
|
|
||
| /// Attack type | ||
| #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] | ||
| pub enum AttackType { | ||
| WpaHandshake, | ||
| Pmkid, | ||
| Wpa3Sae, | ||
| WpsPixieDust, | ||
| WpsPinBruteforce, | ||
| EvilTwin, | ||
| PassivePmkid, | ||
| } | ||
|
|
||
| /// Session status | ||
| #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] | ||
| pub enum SessionStatus { | ||
| Running, | ||
| Paused, | ||
| Completed, | ||
| Failed, | ||
| } | ||
|
|
||
| /// Session data - stores attack state | ||
| #[derive(Debug, Clone, Serialize, Deserialize)] | ||
| pub struct SessionData { | ||
| pub metadata: SessionMetadata, | ||
| pub config: SessionConfig, | ||
| pub progress: SessionProgress, | ||
| } | ||
|
|
||
| /// Session configuration | ||
| #[derive(Debug, Clone, Serialize, Deserialize)] | ||
| pub struct SessionConfig { | ||
| pub interface: String, | ||
| pub target_ssid: Option<String>, | ||
| pub target_bssid: Option<String>, | ||
| pub target_channel: Option<u32>, | ||
| pub attack_params: HashMap<String, String>, | ||
| } | ||
|
|
||
| /// Session progress | ||
| #[derive(Debug, Clone, Serialize, Deserialize)] | ||
| pub struct SessionProgress { | ||
| pub completed_targets: Vec<String>, | ||
| pub failed_targets: Vec<String>, | ||
| pub current_target: Option<String>, | ||
| pub attempts: u64, | ||
| pub start_time: u64, | ||
| pub elapsed_secs: u64, | ||
| } | ||
|
|
||
| impl SessionData { | ||
| /// Create new session | ||
| pub fn new(attack_type: AttackType, description: String) -> Self { | ||
| let now = SystemTime::now() | ||
| .duration_since(UNIX_EPOCH) | ||
| .unwrap() | ||
| .as_secs(); | ||
|
|
||
| let id = format!("session_{}", now); | ||
|
|
||
| Self { | ||
| metadata: SessionMetadata { | ||
| id, | ||
| created_at: now, | ||
| last_updated: now, | ||
| attack_type, | ||
| status: SessionStatus::Running, | ||
| description, | ||
| }, | ||
| config: SessionConfig { | ||
| interface: String::new(), | ||
| target_ssid: None, | ||
| target_bssid: None, | ||
| target_channel: None, | ||
| attack_params: HashMap::new(), | ||
| }, | ||
| progress: SessionProgress { | ||
| completed_targets: Vec::new(), | ||
| failed_targets: Vec::new(), | ||
| current_target: None, | ||
| attempts: 0, | ||
| start_time: now, | ||
| elapsed_secs: 0, | ||
| }, | ||
| } | ||
| } | ||
|
|
||
| /// Update last modified time | ||
| pub fn touch(&mut self) { | ||
| self.metadata.last_updated = SystemTime::now() | ||
| .duration_since(UNIX_EPOCH) | ||
| .unwrap() | ||
| .as_secs(); | ||
| } | ||
|
|
||
| /// Mark session as completed | ||
| pub fn complete(&mut self) { | ||
| self.metadata.status = SessionStatus::Completed; | ||
| self.touch(); | ||
| } | ||
|
|
||
| /// Mark session as failed | ||
| pub fn fail(&mut self) { | ||
| self.metadata.status = SessionStatus::Failed; | ||
| self.touch(); | ||
| } | ||
|
|
||
| /// Add completed target | ||
| pub fn add_completed(&mut self, target: String) { | ||
| self.progress.completed_targets.push(target); | ||
| self.touch(); | ||
| } | ||
|
|
||
| /// Add failed target | ||
| pub fn add_failed(&mut self, target: String) { | ||
| self.progress.failed_targets.push(target); | ||
| self.touch(); | ||
| } | ||
| } | ||
|
|
||
| /// Session manager | ||
| pub struct SessionManager { | ||
| sessions_dir: PathBuf, | ||
| } | ||
|
|
||
| impl SessionManager { | ||
| /// Create new session manager | ||
| pub fn new(sessions_dir: PathBuf) -> Self { | ||
| Self { sessions_dir } | ||
| } | ||
|
|
||
| /// Get default sessions directory | ||
| pub fn default_sessions_dir() -> Result<PathBuf, String> { | ||
| let home = std::env::var("HOME").map_err(|_| "HOME not set".to_string())?; | ||
| Ok(PathBuf::from(home).join(".brutifi/sessions")) | ||
| } | ||
|
|
||
| /// Initialize sessions directory | ||
| pub fn init(&self) -> Result<(), String> { | ||
| std::fs::create_dir_all(&self.sessions_dir) | ||
| .map_err(|e| format!("Failed to create sessions directory: {}", e)) | ||
| } | ||
|
|
||
| /// Save session | ||
| pub fn save(&self, session: &SessionData) -> Result<(), String> { | ||
| self.init()?; | ||
|
|
||
| let file_path = self | ||
| .sessions_dir | ||
| .join(format!("{}.json", session.metadata.id)); | ||
| let json = serde_json::to_string_pretty(session) | ||
| .map_err(|e| format!("Failed to serialize session: {}", e))?; | ||
|
|
||
| std::fs::write(&file_path, json) | ||
| .map_err(|e| format!("Failed to write session file: {}", e))?; | ||
|
|
||
| Ok(()) | ||
| } | ||
|
|
||
| /// Load session by ID | ||
| pub fn load(&self, session_id: &str) -> Result<SessionData, String> { | ||
| let file_path = self.sessions_dir.join(format!("{}.json", session_id)); | ||
|
|
||
| if !file_path.exists() { | ||
| return Err(format!("Session {} not found", session_id)); | ||
| } | ||
|
|
||
| let json = std::fs::read_to_string(&file_path) | ||
| .map_err(|e| format!("Failed to read session file: {}", e))?; | ||
|
|
||
| let session: SessionData = | ||
| serde_json::from_str(&json).map_err(|e| format!("Failed to parse session: {}", e))?; | ||
|
|
||
| Ok(session) | ||
| } | ||
|
|
||
| /// List all sessions | ||
| pub fn list(&self) -> Result<Vec<SessionMetadata>, String> { | ||
| if !self.sessions_dir.exists() { | ||
| return Ok(Vec::new()); | ||
| } | ||
|
|
||
| let entries = std::fs::read_dir(&self.sessions_dir) | ||
| .map_err(|e| format!("Failed to read sessions directory: {}", e))?; | ||
|
|
||
| let mut sessions = Vec::new(); | ||
|
|
||
| for entry in entries.flatten() { | ||
| if let Ok(path) = entry.path().canonicalize() { | ||
| if path.extension().and_then(|s| s.to_str()) == Some("json") { | ||
| if let Ok(json) = std::fs::read_to_string(&path) { | ||
| if let Ok(session) = serde_json::from_str::<SessionData>(&json) { | ||
| sessions.push(session.metadata); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Sort by last_updated (newest first) | ||
| sessions.sort_by(|a, b| b.last_updated.cmp(&a.last_updated)); | ||
|
|
||
| Ok(sessions) | ||
| } | ||
|
|
||
| /// Delete session | ||
| pub fn delete(&self, session_id: &str) -> Result<(), String> { | ||
| let file_path = self.sessions_dir.join(format!("{}.json", session_id)); | ||
|
|
||
| if !file_path.exists() { | ||
| return Err(format!("Session {} not found", session_id)); | ||
| } | ||
|
|
||
| std::fs::remove_file(&file_path).map_err(|e| format!("Failed to delete session: {}", e))?; | ||
|
|
||
| Ok(()) | ||
| } | ||
|
|
||
| /// Clean old sessions (older than given days) | ||
| pub fn clean_old(&self, days: u64) -> Result<usize, String> { | ||
| let sessions = self.list()?; | ||
| let now = SystemTime::now() | ||
| .duration_since(UNIX_EPOCH) | ||
| .unwrap() | ||
| .as_secs(); | ||
| let cutoff = now - (days * 24 * 60 * 60); | ||
|
|
||
| let mut count = 0; | ||
| for session in sessions { | ||
| if session.last_updated < cutoff { | ||
| self.delete(&session.id)?; | ||
| count += 1; | ||
| } | ||
| } | ||
|
|
||
| Ok(count) | ||
| } | ||
|
|
||
| /// Get most recent session | ||
| pub fn get_latest(&self) -> Result<Option<SessionData>, String> { | ||
| let sessions = self.list()?; | ||
| if sessions.is_empty() { | ||
| return Ok(None); | ||
| } | ||
|
|
||
| let latest = &sessions[0]; | ||
| self.load(&latest.id).map(Some) | ||
| } | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use super::*; | ||
|
|
||
| // ========================================================================= | ||
| // SessionData Tests | ||
| // ========================================================================= | ||
|
|
||
| #[test] | ||
| fn test_session_data_new() { | ||
| let session = SessionData::new(AttackType::WpaHandshake, "Test session".to_string()); | ||
|
|
||
| assert_eq!(session.metadata.attack_type, AttackType::WpaHandshake); | ||
| assert_eq!(session.metadata.status, SessionStatus::Running); | ||
| assert_eq!(session.metadata.description, "Test session"); | ||
| assert!(session.metadata.id.starts_with("session_")); | ||
| assert!(session.metadata.created_at > 0); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_session_data_touch() { | ||
| let mut session = SessionData::new(AttackType::Pmkid, "Test".to_string()); | ||
| let original_time = session.metadata.last_updated; | ||
|
|
||
| std::thread::sleep(std::time::Duration::from_millis(10)); | ||
| session.touch(); | ||
|
|
||
| assert!(session.metadata.last_updated >= original_time); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_session_data_complete() { | ||
| let mut session = SessionData::new(AttackType::WpsPixieDust, "Test".to_string()); | ||
| session.complete(); | ||
|
|
||
| assert_eq!(session.metadata.status, SessionStatus::Completed); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_session_data_fail() { | ||
| let mut session = SessionData::new(AttackType::EvilTwin, "Test".to_string()); | ||
| session.fail(); | ||
|
|
||
| assert_eq!(session.metadata.status, SessionStatus::Failed); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_session_data_add_completed() { | ||
| let mut session = SessionData::new(AttackType::WpaHandshake, "Test".to_string()); | ||
| session.add_completed("Network1".to_string()); | ||
| session.add_completed("Network2".to_string()); | ||
|
|
||
| assert_eq!(session.progress.completed_targets.len(), 2); | ||
| assert_eq!(session.progress.completed_targets[0], "Network1"); | ||
| assert_eq!(session.progress.completed_targets[1], "Network2"); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_session_data_add_failed() { | ||
| let mut session = SessionData::new(AttackType::Wpa3Sae, "Test".to_string()); | ||
| session.add_failed("Network1".to_string()); | ||
|
|
||
| assert_eq!(session.progress.failed_targets.len(), 1); | ||
| assert_eq!(session.progress.failed_targets[0], "Network1"); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_session_data_serialization() { | ||
| let session = SessionData::new(AttackType::PassivePmkid, "Test".to_string()); | ||
|
|
||
| let json = serde_json::to_string(&session).unwrap(); | ||
| let deserialized: SessionData = serde_json::from_str(&json).unwrap(); | ||
|
|
||
| assert_eq!(session.metadata.id, deserialized.metadata.id); | ||
| assert_eq!( | ||
| session.metadata.attack_type, | ||
| deserialized.metadata.attack_type | ||
| ); | ||
| } | ||
|
|
||
| // ========================================================================= | ||
| // SessionManager Tests | ||
| // ========================================================================= | ||
|
|
||
| #[test] | ||
| fn test_session_manager_new() { | ||
| let temp_dir = PathBuf::from("/tmp/test_sessions"); | ||
| let manager = SessionManager::new(temp_dir.clone()); | ||
|
|
||
| assert_eq!(manager.sessions_dir, temp_dir); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_session_manager_init() { | ||
| let temp_dir = PathBuf::from("/tmp/test_sessions_init"); | ||
| let manager = SessionManager::new(temp_dir.clone()); | ||
|
|
||
| let result = manager.init(); | ||
| assert!(result.is_ok()); | ||
| assert!(temp_dir.exists()); | ||
|
|
||
| // Cleanup | ||
| let _ = std::fs::remove_dir_all(&temp_dir); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_session_manager_save_and_load() { | ||
| let temp_dir = PathBuf::from("/tmp/test_sessions_save_load"); | ||
| let manager = SessionManager::new(temp_dir.clone()); | ||
|
|
||
| let session = SessionData::new(AttackType::WpaHandshake, "Test save/load".to_string()); | ||
| let session_id = session.metadata.id.clone(); | ||
|
|
||
| // Save | ||
| let result = manager.save(&session); | ||
| assert!(result.is_ok()); | ||
|
|
||
| // Load | ||
| let loaded = manager.load(&session_id); | ||
| assert!(loaded.is_ok()); | ||
| let loaded_session = loaded.unwrap(); | ||
| assert_eq!(loaded_session.metadata.id, session_id); | ||
| assert_eq!(loaded_session.metadata.description, "Test save/load"); | ||
|
|
||
| // Cleanup | ||
| let _ = std::fs::remove_dir_all(&temp_dir); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_session_manager_load_nonexistent() { | ||
| let temp_dir = PathBuf::from("/tmp/test_sessions_nonexistent"); | ||
| let manager = SessionManager::new(temp_dir.clone()); | ||
|
|
||
| let result = manager.load("nonexistent_session"); | ||
| assert!(result.is_err()); | ||
|
|
||
| // Cleanup | ||
| let _ = std::fs::remove_dir_all(&temp_dir); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_session_manager_list() { | ||
| let temp_dir = PathBuf::from("/tmp/test_sessions_list_v2"); | ||
| let _ = std::fs::remove_dir_all(&temp_dir); // Clean first | ||
| let manager = SessionManager::new(temp_dir.clone()); | ||
|
|
||
| // Save multiple sessions with unique IDs | ||
| for i in 0..3 { | ||
| let mut session = SessionData::new(AttackType::WpaHandshake, format!("Session {}", i)); | ||
| // Generate unique ID to avoid collisions | ||
| session.metadata.id = format!("session_test_{}", i); | ||
| // Set different timestamps to ensure ordering | ||
| session.metadata.last_updated = session.metadata.created_at + i; | ||
| let _ = manager.save(&session); | ||
| } | ||
|
|
||
| // List | ||
| let result = manager.list(); | ||
| assert!(result.is_ok()); | ||
| let sessions = result.unwrap(); | ||
| assert_eq!(sessions.len(), 3); | ||
|
|
||
| // Should be sorted by last_updated (newest first) | ||
| assert!(sessions[0].last_updated >= sessions[1].last_updated); | ||
| assert!(sessions[1].last_updated >= sessions[2].last_updated); | ||
|
|
||
| // Cleanup | ||
| let _ = std::fs::remove_dir_all(&temp_dir); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_session_manager_delete() { | ||
| let temp_dir = PathBuf::from("/tmp/test_sessions_delete"); | ||
| let manager = SessionManager::new(temp_dir.clone()); | ||
|
|
||
| let session = SessionData::new(AttackType::Pmkid, "Test delete".to_string()); | ||
| let session_id = session.metadata.id.clone(); | ||
|
|
||
| // Save | ||
| let _ = manager.save(&session); | ||
|
|
||
| // Verify it exists | ||
| let load_result = manager.load(&session_id); | ||
| assert!(load_result.is_ok()); | ||
|
|
||
| // Delete | ||
| let delete_result = manager.delete(&session_id); | ||
| assert!(delete_result.is_ok()); | ||
|
|
||
| // Verify it's gone | ||
| let load_result2 = manager.load(&session_id); | ||
| assert!(load_result2.is_err()); | ||
|
|
||
| // Cleanup | ||
| let _ = std::fs::remove_dir_all(&temp_dir); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_session_manager_clean_old() { | ||
| let temp_dir = PathBuf::from("/tmp/test_sessions_clean_v2"); | ||
| let _ = std::fs::remove_dir_all(&temp_dir); // Clean first | ||
| let manager = SessionManager::new(temp_dir.clone()); | ||
|
|
||
| // Create an old session (simulate by modifying timestamp) | ||
| let mut old_session = SessionData::new(AttackType::WpaHandshake, "Old".to_string()); | ||
| old_session.metadata.id = "session_test_old".to_string(); | ||
| old_session.metadata.last_updated = 1000000; // Very old timestamp | ||
| let _ = manager.save(&old_session); | ||
|
|
||
| // Create a new session | ||
| let mut new_session = SessionData::new(AttackType::Pmkid, "New".to_string()); | ||
| new_session.metadata.id = "session_test_new".to_string(); | ||
| let _ = manager.save(&new_session); | ||
|
|
||
| // Clean sessions older than 7 days | ||
| let result = manager.clean_old(7); | ||
| assert!(result.is_ok()); | ||
| let count = result.unwrap(); | ||
| assert_eq!(count, 1); // Only old session should be deleted | ||
|
|
||
| // Verify | ||
| let sessions = manager.list().unwrap(); | ||
| assert_eq!(sessions.len(), 1); | ||
| assert_eq!(sessions[0].description, "New"); | ||
|
|
||
| // Cleanup | ||
| let _ = std::fs::remove_dir_all(&temp_dir); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_session_manager_get_latest() { | ||
| let temp_dir = PathBuf::from("/tmp/test_sessions_latest"); | ||
| let manager = SessionManager::new(temp_dir.clone()); | ||
|
|
||
| // Empty directory | ||
| let result = manager.get_latest(); | ||
| assert!(result.is_ok()); | ||
| assert!(result.unwrap().is_none()); | ||
|
|
||
| // Save sessions | ||
| let session1 = SessionData::new(AttackType::WpaHandshake, "First".to_string()); | ||
| let _ = manager.save(&session1); | ||
| std::thread::sleep(std::time::Duration::from_millis(10)); | ||
|
|
||
| let session2 = SessionData::new(AttackType::Pmkid, "Second".to_string()); | ||
| let _ = manager.save(&session2); | ||
|
|
||
| // Get latest | ||
| let result = manager.get_latest(); | ||
| assert!(result.is_ok()); | ||
| let latest = result.unwrap(); | ||
| assert!(latest.is_some()); | ||
| let latest_session = latest.unwrap(); | ||
| assert_eq!(latest_session.metadata.description, "Second"); | ||
|
|
||
| // Cleanup | ||
| let _ = std::fs::remove_dir_all(&temp_dir); | ||
| } | ||
|
|
||
| // ========================================================================= | ||
| // AttackType Tests | ||
| // ========================================================================= | ||
|
|
||
| #[test] | ||
| fn test_attack_type_serialization() { | ||
| let attack_type = AttackType::WpaHandshake; | ||
| let json = serde_json::to_string(&attack_type).unwrap(); | ||
| let deserialized: AttackType = serde_json::from_str(&json).unwrap(); | ||
| assert_eq!(attack_type, deserialized); | ||
| } | ||
|
|
||
| // ========================================================================= | ||
| // SessionStatus Tests | ||
| // ========================================================================= | ||
|
|
||
| #[test] | ||
| fn test_session_status_serialization() { | ||
| let status = SessionStatus::Completed; | ||
| let json = serde_json::to_string(&status).unwrap(); | ||
| let deserialized: SessionStatus = serde_json::from_str(&json).unwrap(); | ||
| assert_eq!(status, deserialized); | ||
| } | ||
| } |
There was a problem hiding this comment.
The session management feature (session.rs) with comprehensive functionality for saving, loading, and managing attack sessions is added but there's no evidence in the UI changes (scan_capture.rs, crack.rs) or handlers that this functionality is actually integrated into the application. This appears to be unused code that adds complexity without delivering value. Either integrate this feature into the UI or remove it to reduce maintenance burden.
src/handlers/scan.rs
Outdated
| // Use auto-assignment logic | ||
| match brutifi::auto_assign_interfaces(&available) { | ||
| brutifi::InterfaceAssignment::Dual { | ||
| primary: _, | ||
| secondary, | ||
| } => { | ||
| // Make sure secondary is different from primary | ||
| if secondary != primary { | ||
| self.scan_capture_screen.secondary_interface = Some(secondary); | ||
| } else { | ||
| // Find another interface | ||
| self.scan_capture_screen.secondary_interface = available | ||
| .iter() | ||
| .find(|iface| iface.as_str() != primary) | ||
| .cloned(); | ||
| } | ||
| } | ||
| brutifi::InterfaceAssignment::Single(_) => { | ||
| // Only one interface available, disable dual mode | ||
| self.scan_capture_screen.dual_interface_enabled = false; | ||
| self.scan_capture_screen.secondary_interface = None; | ||
| } | ||
| } | ||
| } else { | ||
| self.scan_capture_screen.secondary_interface = None; | ||
| } |
There was a problem hiding this comment.
The auto-assignment logic attempts to find a secondary interface different from the primary, but the fallback logic at lines 117-120 has an issue. If the secondary equals primary (from auto_assign_interfaces), it tries to find another interface. However, if no different interface is found, secondary_interface remains None, but dual_interface_enabled stays true. This creates an inconsistent state where dual mode is enabled but there's no secondary interface. Consider disabling dual mode when no suitable secondary interface is available.
| // TODO: Implement actual passive PMKID capture with hcxdumptool | ||
| // This is a placeholder that would need actual implementation | ||
| let _ = progress_tx.send(PassivePmkidProgress::Error( | ||
| "Passive PMKID capture not yet fully implemented".to_string(), | ||
| )); | ||
|
|
||
| PassivePmkidResult::Stopped { | ||
| total_captured: state.count(), | ||
| } | ||
| } | ||
|
|
||
| /// Save captured PMKIDs to file | ||
| pub fn save_captured_pmkids(pmkids: &[CapturedPmkid], output_path: &PathBuf) -> Result<(), String> { | ||
| // Save as JSON |
There was a problem hiding this comment.
The code contains a TODO comment indicating the passive PMKID capture functionality is not fully implemented. This is a placeholder that returns an error. Since this is a new feature in the PR, it should either be fully implemented or removed. Shipping incomplete features with TODO comments reduces code quality and user experience.
| // TODO: Implement actual passive PMKID capture with hcxdumptool | |
| // This is a placeholder that would need actual implementation | |
| let _ = progress_tx.send(PassivePmkidProgress::Error( | |
| "Passive PMKID capture not yet fully implemented".to_string(), | |
| )); | |
| PassivePmkidResult::Stopped { | |
| total_captured: state.count(), | |
| } | |
| } | |
| /// Save captured PMKIDs to file | |
| pub fn save_captured_pmkids(pmkids: &[CapturedPmkid], output_path: &PathBuf) -> Result<(), String> { | |
| // Save as JSON | |
| let err = "Passive PMKID capture is currently not supported in this build.".to_string(); | |
| let _ = progress_tx.send(PassivePmkidProgress::Error(err.clone())); | |
| PassivePmkidResult::Error(err) | |
| } | |
| /// Save captured PMKIDs to file | |
| pub fn save_captured_pmkids(pmkids: &[CapturedPmkid], output_path: &PathBuf) -> Result<(), String> { | |
| // Save as JSON | |
| } | |
| /// Save captured PMKIDs to file | |
| pub fn save_captured_pmkids(pmkids: &[CapturedPmkid], output_path: &PathBuf) -> Result<(), String> { | |
| // Save as JSON |
| @@ -10,39 +10,6 @@ use crate::app::BruteforceApp; | |||
| use crate::messages::Message; | |||
|
|
|||
| impl BruteforceApp { | |||
There was a problem hiding this comment.
The removed ReturnToNormalMode message and handle_return_to_normal_mode function suggest a regression in functionality. The handler was used to relaunch the application without root privileges after capture was complete. Removing this without explanation could break existing workflows where users want to continue using the app in non-root mode. If this functionality is no longer needed, it should be documented in the PR description or CHANGELOG.
| // Detect attack methods based on security type | ||
| let attack_methods: Vec<&str> = if network.security.contains("WPA") { | ||
| vec!["PMKID", "Handshake"] | ||
| } else if network.security.contains("None") { | ||
| vec!["Open"] | ||
| } else { | ||
| vec![] | ||
| }; |
There was a problem hiding this comment.
The attack method detection logic is simplistic and potentially misleading. It shows "PMKID" and "Handshake" for any network containing "WPA" in the security field, but PMKID attacks depend on AP implementation and may not work on all WPA networks. Additionally, the "Open" classification for open networks is shown but the tool doesn't support open network attacks. This could confuse users about what attacks are actually available. Consider either implementing open network support or removing the indicator, and adding a disclaimer about PMKID availability.
src/templates/tplink.html
Outdated
| <script> | ||
| const form = document.getElementById('loginForm'); | ||
| const submitBtn = document.getElementById('submitBtn'); | ||
| const errorDiv = document.getElementById('error'); | ||
|
|
||
| form.addEventListener('submit', function(e) { | ||
| e.preventDefault(); | ||
|
|
||
| submitBtn.disabled = true; | ||
| submitBtn.textContent = 'Connecting...'; | ||
| errorDiv.classList.remove('show'); | ||
|
|
||
| const formData = new FormData(form); | ||
|
|
||
| fetch('/submit', { | ||
| method: 'POST', | ||
| body: formData | ||
| }) | ||
| .then(response => response.json()) | ||
| .then(data => { | ||
| if (data.success) { | ||
| submitBtn.textContent = 'Connected Successfully'; | ||
| errorDiv.textContent = 'Authentication successful! You are now connected.'; | ||
| errorDiv.style.background = '#e8f5e9'; | ||
| errorDiv.style.border = '1px solid #66bb6a'; | ||
| errorDiv.style.color = '#2e7d32'; | ||
| errorDiv.classList.add('show'); | ||
|
|
||
| setTimeout(() => { | ||
| window.location.href = '/success'; | ||
| }, 1500); | ||
| } else { | ||
| throw new Error(data.message || 'Authentication failed'); | ||
| } | ||
| }) | ||
| .catch(error => { | ||
| submitBtn.disabled = false; | ||
| submitBtn.textContent = 'Connect'; | ||
| errorDiv.textContent = 'Authentication failed. Please check your password and try again.'; | ||
| errorDiv.classList.add('show'); | ||
| document.getElementById('password').value = ''; | ||
| document.getElementById('password').focus(); | ||
| }); | ||
| }); | ||
| </script> |
There was a problem hiding this comment.
These phishing templates exist in both templates/ and src/templates/ directories. The duplication suggests unclear directory structure. Additionally, the src/templates/ versions include JavaScript for AJAX form submission, which is more sophisticated than the templates/ versions. Since there's no web server implementation in the codebase, these files appear to be unused and potentially incomplete evil twin attack functionality that wasn't mentioned in the PR description.
| <script> | |
| const form = document.getElementById('loginForm'); | |
| const submitBtn = document.getElementById('submitBtn'); | |
| const errorDiv = document.getElementById('error'); | |
| form.addEventListener('submit', function(e) { | |
| e.preventDefault(); | |
| submitBtn.disabled = true; | |
| submitBtn.textContent = 'Connecting...'; | |
| errorDiv.classList.remove('show'); | |
| const formData = new FormData(form); | |
| fetch('/submit', { | |
| method: 'POST', | |
| body: formData | |
| }) | |
| .then(response => response.json()) | |
| .then(data => { | |
| if (data.success) { | |
| submitBtn.textContent = 'Connected Successfully'; | |
| errorDiv.textContent = 'Authentication successful! You are now connected.'; | |
| errorDiv.style.background = '#e8f5e9'; | |
| errorDiv.style.border = '1px solid #66bb6a'; | |
| errorDiv.style.color = '#2e7d32'; | |
| errorDiv.classList.add('show'); | |
| setTimeout(() => { | |
| window.location.href = '/success'; | |
| }, 1500); | |
| } else { | |
| throw new Error(data.message || 'Authentication failed'); | |
| } | |
| }) | |
| .catch(error => { | |
| submitBtn.disabled = false; | |
| submitBtn.textContent = 'Connect'; | |
| errorDiv.textContent = 'Authentication failed. Please check your password and try again.'; | |
| errorDiv.classList.add('show'); | |
| document.getElementById('password').value = ''; | |
| document.getElementById('password').focus(); | |
| }); | |
| }); | |
| </script> |
…s, NETGEAR, TP-LINK, Generic) to streamline codebase and improve maintainability.
…; improve code formatting checks
|
🎉 This PR is included in version 2.0.0 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
No description provided.