Files
sabia/crates/sabia-core/tests/scan_e2e.rs
T
Lucas RayanandClaude Sonnet 5 55e19a8e5b
CI / Format and clippy (push) Successful in 1m13s
CI / Dependency audit (push) Successful in 13s
CI / Tests (Linux) (push) Successful in 1m35s
Release / Create release (push) Successful in 7s
CI / Check desktop app (Linux) (push) Successful in 3m19s
Release / CLI (linux-aarch64) (push) Successful in 1m3s
Release / CLI (linux-x86_64) (push) Successful in 1m28s
Release / Desktop app (linux-aarch64) (push) Successful in 7m16s
Release / Desktop app (linux-x86_64) (push) Successful in 10m17s
Release / Desktop app (windows) (push) Successful in 4m54s
chore(i18n): finish translating code comments and scripts to English
Completes the English-only standardization for code, comments, scripts
and CI templates (UI/CLI-facing strings stay bilingual pt-BR/en,
untouched). Covers the remaining crates/, apps/gui/ui/ and
apps/gui/src-tauri/ comments, .gitea/ workflow and template comments,
.githooks/ hooks, and the scripts/ source comments for the files
already renamed in a prior commit (check.sh, generate-logo.py,
install-hooks.sh).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VAyr8d7HuAdUGB6aA5xmDD
2026-10-30 20:44:39 -03:00

180 lines
6.7 KiB
Rust

//! 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");
}