diff --git a/AGENTS.md b/AGENTS.md index d56dbb1..aac7dfe 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -28,11 +28,25 @@ src/ │ └── security.rs # Security utilities └── screens/ ├── crack.rs # Cracking UI + engine selector - └── scan_capture.rs # Scanning & capture UI + └── scan_capture.rs # Scanning & capture UI ``` ## Key Implementation Details +### Two Attack Methods + +1. **4-Way Handshake** ([`src/core/network.rs`](src/core/network.rs)) + - Captures M1 (ANonce) and M2 (SNonce + MIC) EAPOL frames + - Requires client device to reconnect to AP + - Traditional WPA/WPA2 attack method + +2. **PMKID** ([`src/core/network.rs`](src/core/network.rs)) + - Extracts PMKID from RSN Information Element in beacon frames + - **Clientless**: No devices need to be connected + - Faster and stealthier than handshake capture + +Both methods produce a `.pcap` file that can be cracked offline. + ### Native CPU Engine ([`src/core/bruteforce.rs`](src/core/bruteforce.rs)) - **Zero-allocation**: PasswordBuffer on stack, no heap allocations @@ -91,10 +105,10 @@ cat src/core/hashcat.rs ### After Every Iteration ```bash -# Format code -cargo fmt --all +# Format code (CI command - must pass) +cargo fmt -- --check -# Run clippy (must pass) +# Run clippy (CI command - must pass with zero warnings) cargo clippy --all-targets --all-features -- -D warnings # Build release @@ -161,11 +175,16 @@ sudo ./target/release/brutifi ### Essential Commands ```bash -# Format + Lint + Build (all-in-one) -cargo fmt --all && cargo clippy --all-targets --all-features -- -D warnings && cargo build --release +# CI Commands (exactly as CI runs them) +cargo fmt -- --check # Format check +cargo clippy --all-targets --all-features -- -D warnings # Lint (zero warnings) +cargo build --verbose # Build +cargo test --verbose # Test -# Run tests -cargo test +# Development Commands +cargo fmt --all # Auto-format code +cargo clippy --all-targets --all-features -- -D warnings # Check for warnings +cargo build --release # Release build # Run app sudo ./target/release/brutifi @@ -197,11 +216,16 @@ When in doubt: --- -**Remember**: After every code change, always run: +**Remember**: After every code change, always run these **exact CI commands**: +```bash +cargo fmt -- --check # Check formatting (CI exact) +cargo clippy --all-targets --all-features -- -D warnings # Check lints (CI exact) +``` + +If formatting fails, auto-fix with: ```bash cargo fmt --all -cargo clippy --all-targets --all-features -- -D warnings ``` -These checks are enforced by CI and must pass before merging. +These checks are **enforced by CI** and must pass before merging. diff --git a/README.md b/README.md index 912af76..e4367cc 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # BrutiFi 🔐 -> Modern desktop application for WPA/WPA2 security testing on macOS with real-time feedback +> Simple desktop application for WPA/WPA2 password cracking on macOS [![Release](https://github.com/maxgfr/bruteforce-wifi/actions/workflows/release.yml/badge.svg)](https://github.com/maxgfr/bruteforce-wifi/releases) [![CI](https://github.com/maxgfr/bruteforce-wifi/actions/workflows/ci.yml/badge.svg)](https://github.com/maxgfr/bruteforce-wifi/actions) @@ -9,26 +9,23 @@ **⚠️ EDUCATIONAL USE ONLY - UNAUTHORIZED ACCESS IS ILLEGAL ⚠️** -A high-performance macOS desktop GUI application for testing WPA/WPA2 password security through offline bruteforce attacks. Built with Rust and Iced, featuring dual cracking engines (Native CPU and Hashcat GPU) for maximum performance. +A simple macOS desktop app for testing WiFi password security. Scan networks, capture handshakes, and crack passwords using CPU or GPU acceleration. ## ✨ Features -### Core Capabilities - -- 🖥️ **Modern Desktop GUI** - Built with Iced framework for smooth, native experience +- 🖥️ **Simple Desktop GUI** - Clean 2-screen interface built with Iced - 🚀 **Dual Cracking Engines**: - - **Native CPU**: Custom PBKDF2 implementation with Rayon parallelism (~10K-100K passwords/sec) - - **Hashcat GPU**: 10-100x faster acceleration with automatic device detection + - **Native CPU**: Custom PBKDF2 (~10K-100K passwords/sec) + - **Hashcat GPU**: 10-100x faster with automatic device detection - 📡 **WiFi Network Scanning** - Real-time discovery with channel detection -- 🎯 **Handshake Capture** - EAPOL frame analysis with visual progress indicators -- 🔑 **Dual Attack Modes**: - - 🔢 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) +- 🔑 **Two Crack Modes**: + - 🔢 Numeric bruteforce (8-12 digit PINs) - 📋 Wordlist attacks (rockyou.txt, custom lists) -- 📊 **Live Progress** - Real-time speed metrics, attempt counters, and ETA -- 🔒 **100% Offline** - No data transmitted anywhere - -### Platform Support -- 🍎 **macOS Native** - Apple Silicon and Intel support +- 📊 **Live Progress** - Real-time speed, attempts, and ETA +- 🔒 **100% Offline** - No data transmitted ## 📦 Installation @@ -36,89 +33,59 @@ A high-performance macOS desktop GUI application for testing WPA/WPA2 password s #### Quick Installation -1. Download the DMG from the latest release (Apple Silicon or Intel). -2. Open the DMG and drag **BrutiFi.app** to **Applications**. -3. Launch the app — macOS will ask for the admin (root) password at startup to enable capture. +1. Download the DMG from the latest release (Apple Silicon or Intel) +2. Open the DMG and drag **BrutiFi.app** to **Applications** +3. Launch the app — macOS will ask for admin password to enable capture -#### Remove Quarantine Attribute (Required for GitHub downloads) - -When downloading from GitHub, macOS adds a quarantine attribute. You must remove it to launch the app: +#### Remove Quarantine (Required for GitHub downloads) ```bash xattr -dr com.apple.quarantine /Applications/BrutiFi.app ``` -> This removes security warnings, but WiFi capture in monitor mode still requires root privileges on macOS. - ### From Source ```bash git clone https://github.com/maxgfr/bruteforce-wifi.git cd bruteforce-wifi cargo build --release -./target/release/bruteforce-wifi +./target/release/brutifi ``` ## 🚀 Usage -### Complete Workflow +### Simple 2-Step Workflow -```text -1. Scan Networks → 2. Select Target → 3. Capture Handshake → 4. Crack Password +``` +1. Scan & Capture → Generates .pcap file with handshake/PMKID +2. Crack → Bruteforce password from .pcap ``` -### Step 1: Scan for Networks - -Launch the app and click "Scan Networks" to discover nearby WiFi networks: - -- **SSID** (network name) -- **Channel number** -- **Signal strength** -- **Security type** (WPA/WPA2) - -### Step 2: Select & Capture Handshake - -Select a network → Click "Continue to Capture" - -**Before capturing:** - -1. **Choose output location**: Click "Choose Location" to save the .pcap file - - Default: `capture.pcap` in current directory - - Recommended: Save to Documents or Desktop for easy access -2. **Disconnect from WiFi** (macOS only): - - Option+Click WiFi icon → "Disconnect" - - This improves capture reliability - -Then click "Start Capture" - -The app monitors for the WPA/WPA2 4-way handshake: - -- ✅ **M1** - ANonce (from AP) -- ✅ **M2** - SNonce + MIC (from client) -- 🎉 **Handshake Complete!** - -> **macOS Note**: Deauth attacks don't work on Apple Silicon. Manually reconnect a device to trigger the handshake (turn WiFi off/on on your phone). - -### Step 3: Crack Password - -Navigate to "Crack" tab: +### Step 1: Scan & Capture -#### Engine Selection +1. Click **"Scan"** to discover nearby WiFi networks +2. Select a target network from the list +3. (Optional) Disconnect from WiFi for better capture: `Option+Click WiFi → Disconnect` +4. Click **"Start Capture"** -- **Native CPU**: Software-only cracking, works everywhere -- **Hashcat GPU**: Requires hashcat + hcxtools installed, 10-100x faster +The app automatically captures either: +- ✅ **PMKID** (clientless, instant) +- ✅ **4-Way Handshake** (M1 + M2 frames) -#### Attack Methods +> **macOS Note**: Deauth attacks don't work on Apple Silicon. Manually reconnect a device to trigger handshake (turn phone WiFi off/on). -- **Numeric Attack**: Tests PIN codes (e.g., 00000000-99999999) -- **Wordlist Attack**: Tests passwords from files like rockyou.txt +### Step 2: Crack Password -#### Real-time Stats +1. Navigate to **"Crack"** tab +2. Select cracking engine: + - **Native CPU**: Works everywhere + - **Hashcat GPU**: 10-100x faster (requires `brew install hashcat hcxtools`) +3. Choose attack method: + - **Numeric**: Tests 8-12 digit PIN codes + - **Wordlist**: Tests passwords from file (e.g., rockyou.txt) +4. Click **"Start Cracking"** -- Progress bar with percentage -- Current attempts / Total -- Passwords per second -- Live logs (copyable) +Watch real-time progress with speed and ETA! ## 🛠️ Development @@ -130,43 +97,26 @@ Navigate to "Crack" tab: ### Build Commands ```bash -# Development build with fast compile times +# Development build cargo build -# Optimized release build +# Release build cargo build --release # Run the app cargo run --release -# Format code (enforced by CI) +# Format code cargo fmt --all -# Lint code (enforced by CI) +# Lint code cargo clippy --all-targets --all-features -- -D warnings # Run tests cargo test ``` -### Build macOS DMG (Local) - -You can build a macOS DMG installer locally from the source code: - -```bash -# Build DMG (automatically detects architecture) -./scripts/build_dmg.sh -``` - -This will create: -- `BrutiFi-{VERSION}-macOS-arm64.dmg` (Apple Silicon) -- `BrutiFi-{VERSION}-macOS-arm64.dmg.sha256` (checksum) - -**Note**: The application is signed with ad-hoc signing by default, which is sufficient for local use and testing. No additional code signing is required. - -### Optional: Hashcat Integration - -For GPU-accelerated cracking, install: +### Optional: Hashcat GPU Acceleration ```bash brew install hashcat hcxtools @@ -176,43 +126,44 @@ brew install hashcat hcxtools ### Disclaimer -#### Educational Use Only - -This tool is for educational and authorized testing only. +**Educational Use Only** ✅ **Legal Uses:** - -- Testing your own WiFi network security +- Testing your own WiFi network - Authorized penetration testing with written permission - Security research and education -- CTF competitions and challenges ❌ **Illegal Activities:** - -- Unauthorized access to networks you don't own +- Unauthorized network access - Intercepting communications without permission -- Any malicious or unauthorized use -**Unauthorized access to computer networks is a criminal offense** in most jurisdictions (CFAA in USA, Computer Misuse Act in UK, etc.). Always obtain explicit written permission before testing. +**Unauthorized access is a criminal offense.** Always obtain explicit written permission. + +## 🔧 Alternatives -## 🙏 Acknowledgments & inspiration +**Looking for more advanced features?** -This project was inspired by several groundbreaking tools in the WiFi security space: +BrutiFi focuses on **simplicity** with just 2 core attacks (PMKID + Handshake). For a more comprehensive WiFi auditing tool with additional attack vectors, check out: -- [AirJack](https://github.com/rtulke/AirJack) - As `brutifi` but in a Python-based CLI -- [Aircrack-ng](https://github.com/aircrack-ng/aircrack-ng) - Industry-standard WiFi -- [Pyrit](https://github.com/JPaulMora/Pyrit) - Pre-computed tables for WPA-PSK attacks -- [Cowpatty](https://github.com/joswr1ght/cowpatty) - Early WPA-PSK cracking implementation +- **[Wifite2](https://github.com/kimocoder/wifite2)** - Complete automated wireless auditing tool + - WPS attacks (Pixie Dust, PIN brute-force) + - WPA3 attacks (Transition downgrade, SAE) + - Evil Twin phishing + - Multiple attack automation + - Linux-focused CLI tool -These tools demonstrated the feasibility of offline WPA/WPA2 password attacks and inspired the creation of a modern, user-friendly desktop application. +## 🙏 Acknowledgments -Special thanks to the following libraries and tools: +Inspired by: +- [AirJack](https://github.com/rtulke/AirJack) - Python-based CLI inspiration +- [Aircrack-ng](https://github.com/aircrack-ng/aircrack-ng) - Industry standard +- [Hashcat](https://github.com/hashcat/hashcat) - GPU acceleration +- [hcxtools](https://github.com/ZerBea/hcxtools) - Format conversion -- [Iced](https://github.com/iced-rs/iced) - Cross-platform GUI framework -- [Rayon](https://github.com/rayon-rs/rayon) - Data parallelism library -- [pcap-rs](https://github.com/rust-pcap/pcap) - Rust bindings for libpcap -- [Hashcat](https://github.com/hashcat/hashcat) - GPU-accelerated password recovery -- [hcxtools](https://github.com/ZerBea/hcxtools) - Wireless security auditing tools +Built with: +- [Iced](https://github.com/iced-rs/iced) - GUI framework +- [Rayon](https://github.com/rayon-rs/rayon) - Parallelism +- [pcap-rs](https://github.com/rust-pcap/pcap) - Packet capture ## 📄 License diff --git a/src/app.rs b/src/app.rs index c1b4e53..f686f3d 100644 --- a/src/app.rs +++ b/src/app.rs @@ -168,7 +168,6 @@ impl BruteforceApp { Message::CopyPassword => self.handle_copy_password(), // General - Message::ReturnToNormalMode => self.handle_return_to_normal_mode(), Message::Tick => self.handle_tick(), } } @@ -204,7 +203,7 @@ impl BruteforceApp { None }; - // Navigation header - simplified to 2 steps + // Navigation header - 2 tabs let nav = container( row![ nav_button("1. Scan & Capture", Screen::ScanCapture, self.screen), diff --git a/src/core/handshake.rs b/src/core/handshake.rs index 1a1fb46..d1b5dea 100644 --- a/src/core/handshake.rs +++ b/src/core/handshake.rs @@ -16,6 +16,7 @@ use serde::{Deserialize, Serialize}; /// - ANonce and SNonce (random nonces from the handshake) /// - MIC (Message Integrity Code to verify password) /// - EAPOL frame for MIC calculation +/// - Optional PMKID (for client-less attacks) #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Handshake { /// Network SSID (used in PMK derivation) @@ -41,6 +42,11 @@ pub struct Handshake { /// Key version (1 = HMAC-MD5, 2 = HMAC-SHA1, 3 = AES-CMAC) pub key_version: u8, + + /// PMKID (optional, for client-less PMKID attacks) + /// PMKID = HMAC-SHA1-128(PMK, "PMK Name" | MAC_AP | MAC_STA) + #[serde(skip_serializing_if = "Option::is_none")] + pub pmkid: Option<[u8; 16]>, } impl Handshake { @@ -160,6 +166,45 @@ pub struct EapolPacket { pub key_version: u8, pub message_type: u8, // 1=M1, 2=M2, 3=M3, 4=M4 pub replay_counter: u64, + pub pmkid: Option<[u8; 16]>, // PMKID from RSN IE in Key Data +} + +/// Extract PMKID from EAPOL Key Data field +/// PMKID is found in the RSN IE (tag 0xDD) within the Key Data +fn extract_pmkid_from_key_data(key_data: &[u8]) -> Option<[u8; 16]> { + let mut i = 0; + while i < key_data.len() { + if i + 2 > key_data.len() { + break; + } + + let tag = key_data[i]; + let len = key_data[i + 1] as usize; + + if i + 2 + len > key_data.len() { + break; + } + + // Look for Vendor Specific tag (0xDD) containing PMKID + if tag == 0xDD && len >= 20 { + // Vendor Specific: OUI (3 bytes) + Type (1 byte) + Data + let oui = &key_data[i + 2..i + 5]; + let vendor_type = key_data[i + 5]; + + // Microsoft OUI (00:50:F2) with type 4 (PMKID) + if oui == [0x00, 0x50, 0xF2] && vendor_type == 0x04 { + // PMKID is 16 bytes starting at offset i+6 + if i + 2 + len >= i + 22 { + let pmkid: [u8; 16] = key_data[i + 6..i + 22].try_into().ok()?; + return Some(pmkid); + } + } + } + + i += 2 + len; + } + + None } /// Extract EAPOL packet from raw packet data @@ -319,6 +364,17 @@ pub fn extract_eapol_from_packet(data: &[u8]) -> Option { mic = Some(eapol_data[81..97].to_vec()); } + // Extract PMKID from Key Data (if present in M1 or M3) + let mut pmkid: Option<[u8; 16]> = None; + if (message_type == 1 || message_type == 3) && eapol_data.len() > 99 { + // Key Data Length at offset 97-98 + let key_data_len = u16::from_be_bytes([eapol_data[97], eapol_data[98]]) as usize; + if eapol_data.len() >= 99 + key_data_len { + let key_data = &eapol_data[99..99 + key_data_len]; + pmkid = extract_pmkid_from_key_data(key_data); + } + } + Some(EapolPacket { ap_mac, client_mac, @@ -329,6 +385,7 @@ pub fn extract_eapol_from_packet(data: &[u8]) -> Option { key_version, message_type, replay_counter, + pmkid, }) } @@ -338,6 +395,58 @@ fn build_handshake_from_eapol( ssid: Option<&str>, bssid_map: &std::collections::HashMap<[u8; 6], String>, ) -> Result { + // First, try to find PMKID (client-less attack, faster and more reliable) + for packet in packets { + if let (1, Some(pmkid)) = (packet.message_type, packet.pmkid) { + let ap_mac = packet.ap_mac; + let client_mac = packet.client_mac; + + // Determine SSID + let ssid_str = if let Some(s) = ssid { + if let Some(detected) = bssid_map.get(&ap_mac) { + if s != detected { + println!("\n⚠️ WARNING: Provided SSID '{}' does not match the SSID '{}' broadcasted by the target AP ({:02X}:{:02X}:{:02X}:{:02X}:{:02X}:{:02X})", + s, detected, ap_mac[0], ap_mac[1], ap_mac[2], ap_mac[3], ap_mac[4], ap_mac[5]); + println!(" Using provided SSID. If cracking fails, rely on the detected SSID.\n"); + } + } + s.to_string() + } else { + match bssid_map.get(&ap_mac) { + Some(s) => { + println!("✨ Auto-detected SSID for target AP: {}", s); + s.clone() + }, + None => return Err(anyhow!("SSID not found for target AP ({:02X}:{:02X}:{:02X}:{:02X}:{:02X}:{:02X}). Please provide --ssid.", + ap_mac[0], ap_mac[1], ap_mac[2], ap_mac[3], ap_mac[4], ap_mac[5])) + } + }; + + println!("\n🎯 PMKID FOUND! (client-less attack)"); + println!( + " AP: {:02X}:{:02X}:{:02X}:{:02X}:{:02X}:{:02X}", + ap_mac[0], ap_mac[1], ap_mac[2], ap_mac[3], ap_mac[4], ap_mac[5] + ); + println!(" PMKID: {:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}", + pmkid[0], pmkid[1], pmkid[2], pmkid[3], pmkid[4], pmkid[5], pmkid[6], pmkid[7], + pmkid[8], pmkid[9], pmkid[10], pmkid[11], pmkid[12], pmkid[13], pmkid[14], pmkid[15]); + + // Return a PMKID-only handshake (use dummy values for unused fields) + return Ok(Handshake { + ssid: ssid_str, + ap_mac, + client_mac, + anonce: [0; 32], // Not used for PMKID attacks + snonce: [0; 32], // Not used for PMKID attacks + mic: vec![], // Not used for PMKID attacks + eapol_frame: vec![], // Not used for PMKID attacks + key_version: packet.key_version, + pmkid: Some(pmkid), + }); + } + } + + // If no PMKID found, try traditional 4-way handshake // Collect all potential M2 candidates along with their indices let m2_candidates: Vec<(usize, &EapolPacket)> = packets .iter() @@ -346,7 +455,9 @@ fn build_handshake_from_eapol( .collect(); if m2_candidates.is_empty() { - return Err(anyhow!("Message 2 (SNonce + MIC) not found in handshake")); + return Err(anyhow!( + "Neither PMKID nor complete handshake (Message 2 with SNonce + MIC) found in capture" + )); } // Iterate through all M2 candidates to find one with a matching M1 @@ -426,6 +537,7 @@ fn build_handshake_from_eapol( mic, eapol_frame, key_version, + pmkid: None, // Traditional handshake doesn't use PMKID }); } } @@ -433,3 +545,122 @@ fn build_handshake_from_eapol( // If we reach here, we found M2(s) but no matching M1s Err(anyhow!("Found valid Message 2 packets, but could not find any corresponding Message 1 (Replay Counter mismatch)")) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_extract_pmkid_from_key_data() { + // Test valid PMKID in key data + // Format: Tag (0xDD) | Length | OUI (00:50:F2) | Type (0x04) | PMKID (16 bytes) + let mut key_data = vec![ + 0xDD, // Tag: Vendor Specific + 0x14, // Length: 20 bytes (OUI + Type + PMKID) + 0x00, 0x50, 0xF2, // Microsoft OUI + 0x04, // Type: PMKID + // PMKID (16 bytes) + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, + 0x0F, 0x10, + ]; + + let pmkid = extract_pmkid_from_key_data(&key_data); + assert!(pmkid.is_some()); + assert_eq!( + pmkid.unwrap(), + [ + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, + 0x0F, 0x10 + ] + ); + + // Test with other tags before PMKID + key_data = vec![ + 0x30, 0x04, 0x01, 0x02, 0x03, 0x04, // Some other tag + 0xDD, 0x14, 0x00, 0x50, 0xF2, 0x04, // PMKID tag + 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, + 0x1F, 0x20, + ]; + + let pmkid = extract_pmkid_from_key_data(&key_data); + assert!(pmkid.is_some()); + assert_eq!( + pmkid.unwrap(), + [ + 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, + 0x1F, 0x20 + ] + ); + + // Test invalid OUI + let key_data_invalid_oui = vec![ + 0xDD, 0x14, 0x00, 0x50, 0xF3, // Wrong OUI (should be 00:50:F2) + 0x04, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, + 0x0E, 0x0F, 0x10, + ]; + + let pmkid = extract_pmkid_from_key_data(&key_data_invalid_oui); + assert!(pmkid.is_none()); + + // Test invalid type + let key_data_invalid_type = vec![ + 0xDD, 0x14, 0x00, 0x50, 0xF2, 0x05, // Wrong type (should be 0x04) + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, + 0x0F, 0x10, + ]; + + let pmkid = extract_pmkid_from_key_data(&key_data_invalid_type); + assert!(pmkid.is_none()); + + // Test empty data + let pmkid = extract_pmkid_from_key_data(&[]); + assert!(pmkid.is_none()); + } + + #[test] + fn test_handshake_serialization() { + // Test PMKID handshake serialization + let handshake = Handshake { + ssid: "TestNetwork".to_string(), + ap_mac: [0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF], + client_mac: [0x11, 0x22, 0x33, 0x44, 0x55, 0x66], + anonce: [0; 32], + snonce: [0; 32], + mic: vec![], + eapol_frame: vec![], + key_version: 2, + pmkid: Some([ + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, + 0x0F, 0x10, + ]), + }; + + // Serialize to JSON + let json = serde_json::to_string(&handshake).unwrap(); + assert!(json.contains("pmkid")); + assert!(json.contains("TestNetwork")); + + // Deserialize back + let deserialized: Handshake = serde_json::from_str(&json).unwrap(); + assert_eq!(deserialized.ssid, "TestNetwork"); + assert!(deserialized.pmkid.is_some()); + assert_eq!(deserialized.pmkid.unwrap(), handshake.pmkid.unwrap()); + + // Test traditional handshake (no PMKID) + let handshake_no_pmkid = Handshake { + ssid: "TestNetwork2".to_string(), + ap_mac: [0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF], + client_mac: [0x11, 0x22, 0x33, 0x44, 0x55, 0x66], + anonce: [1; 32], + snonce: [2; 32], + mic: vec![0x01, 0x02, 0x03, 0x04], + eapol_frame: vec![0x05, 0x06], + key_version: 2, + pmkid: None, + }; + + let json = serde_json::to_string(&handshake_no_pmkid).unwrap(); + // pmkid should be omitted when None (skip_serializing_if) + assert!(!json.contains("pmkid")); + } +} diff --git a/src/core/hashcat.rs b/src/core/hashcat.rs index 1395c8a..06d1677 100644 --- a/src/core/hashcat.rs +++ b/src/core/hashcat.rs @@ -190,7 +190,18 @@ fn parse_cracked_password(line: &str) -> Option { return None; } - // WPA 22000 format: WPA*02*...:password + // WPA 22000 format PMKID: WPA*01*...:password + if trimmed.starts_with("WPA*01*") && trimmed.contains(':') { + if let Some(password) = trimmed.split(':').next_back() { + let password = password.trim(); + if password.len() >= 8 && password.len() <= 63 && !password.contains('*') { + return Some(password.to_string()); + } + } + return None; + } + + // WPA 22000 format handshake: WPA*02*...:password if trimmed.starts_with("WPA*02*") && trimmed.contains(':') { if let Some(password) = trimmed.split(':').next_back() { let password = password.trim(); diff --git a/src/core/mod.rs b/src/core/mod.rs index 52e0e22..986de56 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -4,8 +4,10 @@ pub mod crypto; pub mod handshake; pub mod hashcat; pub mod network; +pub mod passive_pmkid; pub mod password_gen; pub mod security; +pub mod session; // Re-exports pub use bruteforce::OfflineBruteForcer; @@ -19,3 +21,12 @@ pub use network::{ capture_traffic, compact_duplicate_networks, disconnect_wifi, scan_networks, wifi_connected_ssid, CaptureOptions, WifiNetwork, }; +pub use passive_pmkid::{ + check_hcxdumptool_available, load_captured_pmkids, run_passive_pmkid_capture, + save_captured_pmkids, CapturedPmkid, PassivePmkidConfig, PassivePmkidProgress, + PassivePmkidResult, PassivePmkidState, +}; +pub use session::{ + AttackType, SessionConfig, SessionData, SessionManager, SessionMetadata, SessionProgress, + SessionStatus, +}; diff --git a/src/core/passive_pmkid.rs b/src/core/passive_pmkid.rs new file mode 100644 index 0000000..16c91ce --- /dev/null +++ b/src/core/passive_pmkid.rs @@ -0,0 +1,508 @@ +/*! + * Passive PMKID Sniffing + * + * Continuous, untargeted PMKID capture from all nearby networks. + * Runs in background and auto-saves captured PMKIDs. + */ + +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::path::PathBuf; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, Mutex}; +use std::time::{SystemTime, UNIX_EPOCH}; + +/// Captured PMKID entry +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct CapturedPmkid { + pub ssid: String, + pub bssid: String, + pub pmkid: String, + pub timestamp: u64, + pub channel: u32, + pub signal_strength: i32, +} + +impl CapturedPmkid { + /// Create new captured PMKID + pub fn new( + ssid: String, + bssid: String, + pmkid: String, + channel: u32, + signal_strength: i32, + ) -> Self { + Self { + ssid, + bssid, + pmkid, + timestamp: SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs(), + channel, + signal_strength, + } + } +} + +/// Passive PMKID sniffing configuration +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PassivePmkidConfig { + pub interface: String, + pub output_dir: PathBuf, + pub auto_save: bool, + pub save_interval_secs: u64, + pub hop_channels: bool, + pub channels: Vec, +} + +impl Default for PassivePmkidConfig { + fn default() -> Self { + Self { + interface: "wlan0".to_string(), + output_dir: PathBuf::from("/tmp/pmkid_captures"), + auto_save: true, + save_interval_secs: 60, + hop_channels: true, + channels: vec![1, 6, 11], // Common 2.4GHz channels + } + } +} + +/// Passive PMKID progress events +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum PassivePmkidProgress { + Started, + PmkidCaptured { + ssid: String, + bssid: String, + channel: u32, + }, + ChannelChanged { + channel: u32, + }, + AutoSaved { + count: usize, + path: String, + }, + Statistics { + total_captured: usize, + unique_networks: usize, + runtime_secs: u64, + }, + Error(String), + Stopped, +} + +/// Passive PMKID result +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum PassivePmkidResult { + Running, + Stopped { total_captured: usize }, + Error(String), +} + +/// Passive PMKID capture state +pub struct PassivePmkidState { + stop_flag: Arc, + captured: Arc>>, // key: BSSID + start_time: Arc>>, +} + +impl PassivePmkidState { + pub fn new() -> Self { + Self { + stop_flag: Arc::new(AtomicBool::new(false)), + captured: Arc::new(Mutex::new(HashMap::new())), + start_time: Arc::new(Mutex::new(None)), + } + } + + /// Signal to stop capture + pub fn stop(&self) { + self.stop_flag.store(true, Ordering::Relaxed); + } + + /// Check if should stop + pub fn should_stop(&self) -> bool { + self.stop_flag.load(Ordering::Relaxed) + } + + /// Add captured PMKID + pub fn add_pmkid(&self, pmkid: CapturedPmkid) { + if let Ok(mut captured) = self.captured.lock() { + captured.insert(pmkid.bssid.clone(), pmkid); + } + } + + /// Get all captured PMKIDs + pub fn get_captured(&self) -> Vec { + if let Ok(captured) = self.captured.lock() { + captured.values().cloned().collect() + } else { + Vec::new() + } + } + + /// Get count of captured PMKIDs + pub fn count(&self) -> usize { + if let Ok(captured) = self.captured.lock() { + captured.len() + } else { + 0 + } + } + + /// Set start time + pub fn set_start_time(&self, time: SystemTime) { + if let Ok(mut start_time) = self.start_time.lock() { + *start_time = Some(time); + } + } + + /// Get runtime in seconds + pub fn runtime_secs(&self) -> u64 { + if let Ok(start_time) = self.start_time.lock() { + if let Some(start) = *start_time { + SystemTime::now() + .duration_since(start) + .unwrap_or_default() + .as_secs() + } else { + 0 + } + } else { + 0 + } + } +} + +impl Default for PassivePmkidState { + fn default() -> Self { + Self::new() + } +} + +/// Check if hcxdumptool is available (required for passive PMKID capture) +pub fn check_hcxdumptool_available() -> bool { + std::process::Command::new("hcxdumptool") + .arg("--version") + .output() + .is_ok() +} + +/// Run passive PMKID capture +pub fn run_passive_pmkid_capture( + config: &PassivePmkidConfig, + state: Arc, + progress_tx: &tokio::sync::mpsc::UnboundedSender, +) -> PassivePmkidResult { + state.set_start_time(SystemTime::now()); + + let _ = progress_tx.send(PassivePmkidProgress::Started); + + // Check if hcxdumptool is available + if !check_hcxdumptool_available() { + let err = "hcxdumptool not found. Install with: apt install hcxdumptool".to_string(); + let _ = progress_tx.send(PassivePmkidProgress::Error(err.clone())); + return PassivePmkidResult::Error(err); + } + + // Create output directory if it doesn't exist + if config.auto_save { + if let Err(e) = std::fs::create_dir_all(&config.output_dir) { + let err = format!("Failed to create output directory: {}", e); + let _ = progress_tx.send(PassivePmkidProgress::Error(err.clone())); + return PassivePmkidResult::Error(err); + } + } + + // 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 json = serde_json::to_string_pretty(pmkids) + .map_err(|e| format!("Failed to serialize PMKIDs: {}", e))?; + + std::fs::write(output_path, json).map_err(|e| format!("Failed to write file: {}", e))?; + + Ok(()) +} + +/// Load captured PMKIDs from file +pub fn load_captured_pmkids(input_path: &PathBuf) -> Result, String> { + let json = + std::fs::read_to_string(input_path).map_err(|e| format!("Failed to read file: {}", e))?; + + let pmkids: Vec = + serde_json::from_str(&json).map_err(|e| format!("Failed to parse PMKIDs: {}", e))?; + + Ok(pmkids) +} + +#[cfg(test)] +mod tests { + use super::*; + + // ========================================================================= + // CapturedPmkid Tests + // ========================================================================= + + #[test] + fn test_captured_pmkid_creation() { + let pmkid = CapturedPmkid::new( + "TestNetwork".to_string(), + "AA:BB:CC:DD:EE:FF".to_string(), + "abcdef1234567890".to_string(), + 6, + -50, + ); + + assert_eq!(pmkid.ssid, "TestNetwork"); + assert_eq!(pmkid.bssid, "AA:BB:CC:DD:EE:FF"); + assert_eq!(pmkid.pmkid, "abcdef1234567890"); + assert_eq!(pmkid.channel, 6); + assert_eq!(pmkid.signal_strength, -50); + assert!(pmkid.timestamp > 0); + } + + #[test] + fn test_captured_pmkid_clone() { + let pmkid = CapturedPmkid::new( + "Test".to_string(), + "AA:BB:CC:DD:EE:FF".to_string(), + "abcd".to_string(), + 1, + -60, + ); + + let cloned = pmkid.clone(); + assert_eq!(pmkid, cloned); + } + + #[test] + fn test_captured_pmkid_serialization() { + let pmkid = CapturedPmkid::new( + "Test".to_string(), + "AA:BB:CC:DD:EE:FF".to_string(), + "abcd".to_string(), + 1, + -60, + ); + + let json = serde_json::to_string(&pmkid).unwrap(); + let deserialized: CapturedPmkid = serde_json::from_str(&json).unwrap(); + assert_eq!(pmkid, deserialized); + } + + // ========================================================================= + // PassivePmkidConfig Tests + // ========================================================================= + + #[test] + fn test_passive_pmkid_config_default() { + let config = PassivePmkidConfig::default(); + assert_eq!(config.interface, "wlan0"); + assert!(config.auto_save); + assert_eq!(config.save_interval_secs, 60); + assert!(config.hop_channels); + assert_eq!(config.channels, vec![1, 6, 11]); + } + + #[test] + fn test_passive_pmkid_config_serialization() { + let config = PassivePmkidConfig::default(); + let json = serde_json::to_string(&config).unwrap(); + let deserialized: PassivePmkidConfig = serde_json::from_str(&json).unwrap(); + assert_eq!(config.interface, deserialized.interface); + } + + // ========================================================================= + // PassivePmkidState Tests + // ========================================================================= + + #[test] + fn test_passive_pmkid_state_new() { + let state = PassivePmkidState::new(); + assert!(!state.should_stop()); + assert_eq!(state.count(), 0); + } + + #[test] + fn test_passive_pmkid_state_stop() { + let state = PassivePmkidState::new(); + assert!(!state.should_stop()); + state.stop(); + assert!(state.should_stop()); + } + + #[test] + fn test_passive_pmkid_state_add_pmkid() { + let state = PassivePmkidState::new(); + let pmkid = CapturedPmkid::new( + "Test".to_string(), + "AA:BB:CC:DD:EE:FF".to_string(), + "abcd".to_string(), + 1, + -60, + ); + + state.add_pmkid(pmkid.clone()); + assert_eq!(state.count(), 1); + + let captured = state.get_captured(); + assert_eq!(captured.len(), 1); + assert_eq!(captured[0], pmkid); + } + + #[test] + fn test_passive_pmkid_state_multiple_pmkids() { + let state = PassivePmkidState::new(); + + for i in 0..5 { + let pmkid = CapturedPmkid::new( + format!("Network{}", i), + format!("AA:BB:CC:DD:EE:{:02X}", i), + format!("pmkid{}", i), + 1, + -60, + ); + state.add_pmkid(pmkid); + } + + assert_eq!(state.count(), 5); + } + + #[test] + fn test_passive_pmkid_state_duplicate_bssid() { + let state = PassivePmkidState::new(); + + let pmkid1 = CapturedPmkid::new( + "Network1".to_string(), + "AA:BB:CC:DD:EE:FF".to_string(), + "pmkid1".to_string(), + 1, + -60, + ); + + let pmkid2 = CapturedPmkid::new( + "Network1".to_string(), + "AA:BB:CC:DD:EE:FF".to_string(), // Same BSSID + "pmkid2".to_string(), // Different PMKID + 1, + -65, + ); + + state.add_pmkid(pmkid1); + state.add_pmkid(pmkid2.clone()); + + // Should replace first with second (same BSSID) + assert_eq!(state.count(), 1); + let captured = state.get_captured(); + assert_eq!(captured[0].pmkid, "pmkid2"); + } + + #[test] + fn test_passive_pmkid_state_runtime() { + let state = PassivePmkidState::new(); + state.set_start_time(SystemTime::now()); + std::thread::sleep(std::time::Duration::from_millis(100)); + let runtime = state.runtime_secs(); + // Should be 0 or 1 second (very short sleep) + assert!(runtime <= 1); + } + + // ========================================================================= + // Progress Events Tests + // ========================================================================= + + #[test] + fn test_passive_pmkid_progress_serialization() { + let progress = PassivePmkidProgress::PmkidCaptured { + ssid: "Test".to_string(), + bssid: "AA:BB:CC:DD:EE:FF".to_string(), + channel: 6, + }; + + let json = serde_json::to_string(&progress).unwrap(); + assert!(json.contains("Test")); + assert!(json.contains("AA:BB:CC:DD:EE:FF")); + } + + #[test] + fn test_passive_pmkid_result_serialization() { + let result = PassivePmkidResult::Stopped { total_captured: 10 }; + let json = serde_json::to_string(&result).unwrap(); + assert!(json.contains("10")); + } + + // ========================================================================= + // File Operations Tests + // ========================================================================= + + #[test] + fn test_save_and_load_pmkids() { + let temp_path = PathBuf::from("/tmp/test_pmkids.json"); + + let pmkids = vec![ + CapturedPmkid::new( + "Network1".to_string(), + "AA:BB:CC:DD:EE:FF".to_string(), + "pmkid1".to_string(), + 1, + -50, + ), + CapturedPmkid::new( + "Network2".to_string(), + "11:22:33:44:55:66".to_string(), + "pmkid2".to_string(), + 6, + -60, + ), + ]; + + // Save + let result = save_captured_pmkids(&pmkids, &temp_path); + assert!(result.is_ok()); + + // Load + let loaded = load_captured_pmkids(&temp_path); + assert!(loaded.is_ok()); + let loaded_pmkids = loaded.unwrap(); + assert_eq!(loaded_pmkids.len(), 2); + assert_eq!(loaded_pmkids[0].ssid, "Network1"); + assert_eq!(loaded_pmkids[1].ssid, "Network2"); + + // Cleanup + let _ = std::fs::remove_file(&temp_path); + } + + #[test] + fn test_load_nonexistent_file() { + let path = PathBuf::from("/tmp/nonexistent_pmkids.json"); + let result = load_captured_pmkids(&path); + assert!(result.is_err()); + } + + // ========================================================================= + // Tool Detection Tests + // ========================================================================= + + #[test] + fn test_check_hcxdumptool_available() { + // Should not panic + let _available = check_hcxdumptool_available(); + // Result depends on system, so we just verify it doesn't crash + } +} diff --git a/src/core/session.rs b/src/core/session.rs new file mode 100644 index 0000000..392b968 --- /dev/null +++ b/src/core/session.rs @@ -0,0 +1,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, + pub target_bssid: Option, + pub target_channel: Option, + pub attack_params: HashMap, +} + +/// Session progress +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SessionProgress { + pub completed_targets: Vec, + pub failed_targets: Vec, + pub current_target: Option, + 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 { + 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 { + 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, 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::(&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 { + 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, 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); + } +} diff --git a/src/handlers/capture.rs b/src/handlers/capture.rs index 9d69e98..1727a6f 100644 --- a/src/handlers/capture.rs +++ b/src/handlers/capture.rs @@ -241,14 +241,32 @@ impl BruteforceApp { pub fn handle_capture_progress(&mut self, progress: CaptureProgress) -> Task { match progress { CaptureProgress::Log(msg) => { + // 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; + } self.add_capture_log(msg); } CaptureProgress::HandshakeComplete { ssid } => { self.scan_capture_screen.handshake_complete = true; - self.scan_capture_screen.handshake_progress.m1_received = true; - self.scan_capture_screen.handshake_progress.m2_received = true; + // Don't override specific flags, just mark as complete + // The specific type (PMKID vs handshake) is already set by Log messages + if !self.scan_capture_screen.handshake_progress.pmkid_captured { + // If no PMKID was detected, assume traditional handshake + self.scan_capture_screen.handshake_progress.m1_received = true; + self.scan_capture_screen.handshake_progress.m2_received = true; + } self.scan_capture_screen.is_capturing = false; - self.add_capture_log(format!("✅ Handshake captured for '{}'", ssid)); + + let capture_type = self.scan_capture_screen.handshake_progress.capture_type(); + self.add_capture_log(format!("✅ {} captured for '{}'", capture_type, ssid)); self.persist_state(); if self.is_root { diff --git a/src/handlers/general.rs b/src/handlers/general.rs index 84eb7d0..2fe8bb2 100644 --- a/src/handlers/general.rs +++ b/src/handlers/general.rs @@ -10,39 +10,6 @@ use crate::app::BruteforceApp; use crate::messages::Message; impl BruteforceApp { - /// Return to normal (non-root) mode - pub fn handle_return_to_normal_mode(&mut self) -> Task { - if self.is_root { - let mut envs = Vec::new(); - envs.push(("BRUTIFI_START_SCREEN", "crack".to_string())); - if !self.crack_screen.handshake_path.is_empty() { - envs.push(( - "BRUTIFI_HANDSHAKE_PATH", - self.crack_screen.handshake_path.clone(), - )); - } - if !self.crack_screen.ssid.is_empty() { - envs.push(("BRUTIFI_SSID", self.crack_screen.ssid.clone())); - } - if !self.crack_screen.wordlist_path.is_empty() { - envs.push(( - "BRUTIFI_WORDLIST_PATH", - self.crack_screen.wordlist_path.clone(), - )); - } - - if crate::relaunch_as_user(&envs) { - std::process::exit(0); - } - - self.crack_screen.error_message = Some( - "Failed to return to normal mode. Please restart the app manually.".to_string(), - ); - } - - Task::none() - } - /// Handle tick for polling progress channels pub fn handle_tick(&mut self) -> Task { let mut messages = Vec::new(); diff --git a/src/messages.rs b/src/messages.rs index 6db2a73..6aa8a61 100644 --- a/src/messages.rs +++ b/src/messages.rs @@ -11,6 +11,7 @@ use crate::workers::{CaptureProgress, CrackProgress, ScanResult}; /// Application messages #[derive(Debug, Clone)] +#[allow(dead_code)] pub enum Message { // Navigation GoToScanCapture, @@ -51,8 +52,6 @@ pub enum Message { StopCrack, CrackProgress(CrackProgress), CopyPassword, - #[allow(dead_code)] - ReturnToNormalMode, // General Tick, diff --git a/src/screens/mod.rs b/src/screens/mod.rs index ac099af..2e20429 100644 --- a/src/screens/mod.rs +++ b/src/screens/mod.rs @@ -2,7 +2,7 @@ * GUI Screens * * Each screen represents a step in the WiFi cracking workflow: - * 1. Scan & Capture - Discover networks and capture handshake (unified) + * 1. Scan & Capture - Scan networks and capture handshake/PMKID * 2. Crack - Bruteforce the password */ diff --git a/src/screens/scan_capture.rs b/src/screens/scan_capture.rs index 9872694..4e86509 100644 --- a/src/screens/scan_capture.rs +++ b/src/screens/scan_capture.rs @@ -20,11 +20,22 @@ use brutifi::WifiNetwork; pub struct HandshakeProgress { pub m1_received: bool, pub m2_received: bool, + pub pmkid_captured: bool, } impl HandshakeProgress { pub fn is_complete(&self) -> bool { - self.m1_received && self.m2_received + self.pmkid_captured || (self.m1_received && self.m2_received) + } + + pub fn capture_type(&self) -> &str { + if self.pmkid_captured { + "PMKID (client-less)" + } else if self.m1_received && self.m2_received { + "4-way handshake" + } else { + "In progress" + } } } @@ -208,6 +219,15 @@ impl ScanCaptureScreen { "?" }; + // 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![] + }; + let item_style = if is_selected { theme::network_item_selected_style } else { @@ -226,8 +246,36 @@ impl ScanCaptureScreen { text(format!("Ch {} | {}", network.channel, signal_icon)) .size(10) .color(colors::TEXT_DIM), + if !attack_methods.is_empty() { + row(attack_methods + .iter() + .map(|method| { + container( + text(*method).size(8).color(iced::Color::WHITE), + ) + .padding([2, 5]) + .style(|_: &Theme| container::Style { + background: Some(iced::Background::Color( + iced::Color::from_rgba( + 0.18, 0.55, 0.34, 0.2, + ), + )), + border: iced::Border { + color: colors::PRIMARY, + width: 1.0, + radius: 3.0.into(), + }, + ..Default::default() + }) + .into() + }) + .collect::>>()) + .spacing(3) + } else { + row![].spacing(0) + }, ] - .spacing(2), + .spacing(3), horizontal_space(), text(network.security.clone()) .size(10) @@ -385,15 +433,19 @@ impl ScanCaptureScreen { // ========== STATUS BLOCK ========== let status_block: Element<'_, Message> = if handshake_done { // Success state + let capture_type = self.handshake_progress.capture_type(); container( column![ row![ text("✅").size(20), - text("Handshake Captured!").size(14).color(colors::SUCCESS), + text("Capture Complete!").size(14).color(colors::SUCCESS), ] .spacing(8) .align_y(iced::Alignment::Center), - text("The capture file contains a valid WPA handshake.") + text(format!("Type: {}", capture_type)) + .size(11) + .color(colors::PRIMARY), + text("The capture file is ready for cracking.") .size(10) .color(colors::TEXT_DIM), ] @@ -420,24 +472,33 @@ impl ScanCaptureScreen { column![ row![ text("🔍").size(14), - text("Listening for handshake...") + text("Listening for capture...") .size(12) .color(colors::TEXT), ] .spacing(6), - row![ - if hp.m1_received { - text("✅ M1").size(10).color(colors::SUCCESS) - } else { - text("⏳ M1").size(10).color(colors::TEXT_DIM) - }, - if hp.m2_received { - text("✅ M2").size(10).color(colors::SUCCESS) - } else { - text("⏳ M2").size(10).color(colors::TEXT_DIM) - }, - ] - .spacing(12), + if hp.pmkid_captured { + row![text("✅ PMKID (client-less)") + .size(10) + .color(colors::SUCCESS),] + .spacing(12) + } else { + row![ + text("⏳ PMKID").size(10).color(colors::TEXT_DIM), + text("or").size(9).color(colors::TEXT_DIM), + if hp.m1_received { + text("✅ M1").size(10).color(colors::SUCCESS) + } else { + text("⏳ M1").size(10).color(colors::TEXT_DIM) + }, + if hp.m2_received { + text("✅ M2").size(10).color(colors::SUCCESS) + } else { + text("⏳ M2").size(10).color(colors::TEXT_DIM) + }, + ] + .spacing(12) + }, ] .spacing(6), )