//! End-to-end integration test: ingests beacon, probe, and 4-way handshake //! frames into `ScanTable` and verifies AP, client, handshake, and live CCMP //! data frame decryption. mod common; use sabia_core::scan::ScanTable; #[test] fn detects_ap_client_and_handshake_end_to_end() { let mut table = ScanTable::new(); // Include an RSN IE with PSK AKM (2) so the AP is classified as WPA2-PSK. let beacon = common::beacon_frame(common::ap(), "RedeTeste", 6, &[2]); table.ingest(&common::radiotap_wrap(&beacon, 2437, -35)); let probe = common::probe_request(common::sta(), "RedeTeste"); table.ingest(&common::radiotap_wrap(&probe, 2437, -50)); let (f1, f2) = common::handshake_radiotap_frames("RedeTeste", "senha-correta", 2, false); let ev1 = table.ingest_at(&f1, 10); assert!(ev1.eapol, "quadro msg1 deveria ser reconhecido como EAPOL"); let ev2 = table.ingest_at(&f2, 11); assert!(ev2.eapol, "quadro msg2 deveria ser reconhecido como EAPOL"); assert!( ev2.handshake_updated, "msg2 deveria completar/atualizar o handshake" ); let aps = table.access_points(); assert_eq!(aps.len(), 1); let ap = &aps[0]; assert_eq!(ap.bssid, sabia_core::dot11::mac_to_string(&common::ap())); assert_eq!(ap.ssid.as_deref(), Some("RedeTeste")); assert_eq!(ap.channel, Some(6)); assert_eq!(ap.security.as_deref(), Some("WPA2")); let clients = table.clients(); assert!(clients .iter() .any(|c| c.mac == sabia_core::dot11::mac_to_string(&common::sta()))); let handshakes = table.handshakes(); assert_eq!(handshakes.len(), 1); let hs = &handshakes[0]; assert_eq!(hs.ssid.as_deref(), Some("RedeTeste")); assert!(hs.complete, "handshake deveria estar completo (msg1+msg2)"); assert!(hs.crackable, "handshake deveria ser quebrável"); let stats = table.stats(); assert_eq!(stats.aps, 1); assert_eq!(stats.clients, 1); assert_eq!(stats.handshakes, 1); assert!(stats.eapol_frames >= 2); } #[test] fn hidden_ssid_beacon_is_marked_hidden() { let mut table = ScanTable::new(); // Empty 0-length SSID IE = hidden network. let beacon = common::beacon_frame(common::ap(), "", 11, &[2]); table.ingest(&common::radiotap_wrap(&beacon, 2462, -60)); let aps = table.access_points(); assert_eq!(aps.len(), 1); assert!(aps[0].hidden); } /// Once a WPA2/CCMP handshake has been cracked, subsequent protected QoS Data /// frames from that station are decrypted on the fly using its derived TK to /// populate client identity fields (such as its IPv4 address). /// /// #[test] fn decrypts_ccmp_traffic_once_password_is_known_and_fills_client_identity() { use sabia_core::crack::{compute_ptk, derive_pmk}; let mut table = ScanTable::new(); let ssid = "RedeCifrada"; let password = "senha-correta"; let beacon = common::beacon_frame(common::ap(), ssid, 6, &[2]); table.ingest(&common::radiotap_wrap(&beacon, 2437, -35)); let (f1, f2) = common::handshake_radiotap_frames(ssid, password, 2, false); table.ingest(&f1); table.ingest(&f2); let bssid_str = sabia_core::dot11::mac_to_string(&common::ap()); let station_str = sabia_core::dot11::mac_to_string(&common::sta()); table.set_password(&bssid_str, &station_str, password); // The pipeline-derived TK must match the TK computed directly from the same nonces. // let anonce = [0xaa; 32]; let snonce = [0xbb; 32]; let pmk = derive_pmk(password, ssid); let ptk = compute_ptk(&pmk, &common::ap(), &common::sta(), &anonce, &snonce); let expected_tk: [u8; 16] = ptk[32..48].try_into().unwrap(); let hs = table .handshake(&bssid_str, &station_str) .expect("handshake presente"); assert_eq!( hs.tk, Some(expected_tk), "TK deveria ser derivada ao quebrar a senha" ); // Build a protected STA->AP QoS Data frame (CCMP PN=1, ExtIV=1) carrying an // encrypted LLC/SNAP + IPv4 packet. // let llc_ipv4 = { let mut v = vec![0xAA, 0xAA, 0x03, 0x00, 0x00, 0x00, 0x08, 0x00]; let mut ip = vec![ 0x45, 0x00, 0x00, 20, 0x00, 0x00, 0x00, 0x00, 64, 17, 0x00, 0x00, ]; ip.extend_from_slice(&[192, 168, 1, 50]); // private source IPv4 ip.extend_from_slice(&[192, 168, 1, 1]); // destination IPv4 v.extend_from_slice(&ip); v }; let ccmp_header = [0x01u8, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00]; // PN=1, ExtIV let mut to_cipher = Vec::new(); to_cipher.extend_from_slice(&ccmp_header); to_cipher.extend_from_slice(&llc_ipv4); to_cipher.extend_from_slice(&[0u8; 8]); // 8-byte MIC placeholder let ciphertext = sabia_core::ccmp::decrypt_data(&expected_tk, &common::sta(), 0, &to_cipher) .expect("cifra via CTR simétrico"); let mut body = Vec::new(); body.extend_from_slice(&ccmp_header); body.extend_from_slice(&ciphertext); body.extend_from_slice(&[0u8; 8]); // trailing 8-byte MIC let mut frame = Vec::new(); let fc: u16 = 0x0088 | 0x0100 | 0x4000; // QoS Data, to_ds, protected frame.extend_from_slice(&fc.to_le_bytes()); frame.extend_from_slice(&0u16.to_le_bytes()); // duration frame.extend_from_slice(&common::ap()); // addr1 = BSSID frame.extend_from_slice(&common::sta()); // addr2 = transmitter (station) frame.extend_from_slice(&common::ap()); // addr3 = destination frame.extend_from_slice(&[0u8; 2]); // sequence control frame.extend_from_slice(&[0u8; 2]); // QoS control, TID 0 frame.extend_from_slice(&body); let ev = table.ingest_at(&common::radiotap_wrap(&frame, 2437, -45), 30); assert!(!ev.eapol, "não é um quadro EAPOL"); let clients = table.clients(); let client = clients .iter() .find(|c| c.mac == station_str) .expect("cliente deveria ter sido visto"); assert_eq!( client.ip.as_deref(), Some("192.168.1.50"), "o IP só aparece se o CCMP foi decifrado corretamente" ); } #[test] fn pmkid_only_handshake_is_detected_and_crackable() { let mut table = ScanTable::new(); // Beacon with visible SSID so the PMKID handshake inherits the SSID hint. let beacon = common::beacon_frame(common::ap(), "RedePmkid", 11, &[2]); table.ingest_at(&common::radiotap_wrap(&beacon, 2462, -60), 1); // PMKID-only handshake (message 1 with PMKID KDE, no message 2). let (m1, _m2) = common::handshake_frames("RedePmkid", "outra-senha", 2, true); let f1 = common::frame_with_eapol(true, common::ap(), common::sta(), common::sta(), &m1); let ev = table.ingest_at(&common::radiotap_wrap(&f1, 2462, -55), 20); assert!(ev.eapol); let handshakes = table.handshakes(); assert_eq!(handshakes.len(), 1); assert!(handshakes[0].crackable, "PMKID isolado já é quebrável"); }