Skip to content

Comments

feat: add PMKID support for client-less attacks and enhance capture#2

Merged
maxgfr merged 13 commits intomainfrom
maxgfr/all-methods
Jan 30, 2026
Merged

feat: add PMKID support for client-less attacks and enhance capture#2
maxgfr merged 13 commits intomainfrom
maxgfr/all-methods

Conversation

@maxgfr
Copy link
Owner

@maxgfr maxgfr commented Jan 30, 2026

No description provided.

maxgfr and others added 11 commits January 25, 2026 19:52
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.
Copilot AI review requested due to automatic review settings January 30, 2026 12:07
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ⚠️ CRITICAL: Undocumented phishing template mimicking TP-Link router login
templates/netgear.html ⚠️ CRITICAL: Undocumented phishing template mimicking Netgear router login
templates/linksys.html ⚠️ CRITICAL: Undocumented phishing template mimicking Linksys router login
templates/generic.html ⚠️ CRITICAL: Undocumented phishing template for generic WiFi login
src/templates/*.html ⚠️ CRITICAL: Duplicate templates with JavaScript for AJAX submission (no server exists)
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 ⚠️ New module with TODO/not implemented placeholder code
src/core/session.rs ⚠️ Complete session management implementation but entirely unused in app
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 ⚠️ Removes ReturnToNormalMode functionality without documentation
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.

Comment on lines 80 to 144
/// 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)
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
- 🔢 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)
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
- **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)

Copilot uses AI. Check for mistakes.
Comment on lines +244 to +254
// 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;
}
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines 177 to 191
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)
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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)

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +555
/*!
* 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);
}
}
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines 106 to 131
// 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;
}
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +221 to +234
// 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
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
// 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

Copilot uses AI. Check for mistakes.
@@ -10,39 +10,6 @@ use crate::app::BruteforceApp;
use crate::messages::Message;

impl BruteforceApp {
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +249 to +256
// 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![]
};
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines 207 to 251
<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>
Copy link

Copilot AI Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
<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>

Copilot uses AI. Check for mistakes.
…s, NETGEAR, TP-LINK, Generic) to streamline codebase and improve maintainability.
@maxgfr maxgfr merged commit f6b59ca into main Jan 30, 2026
4 checks passed
@maxgfr maxgfr deleted the maxgfr/all-methods branch January 30, 2026 13:02
@github-actions
Copy link

🎉 This PR is included in version 2.0.0 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant