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
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
180 lines
6.7 KiB
Rust
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");
|
|
}
|