v0.1.0: logo, i18n pt/en, CI com caches, testes e documentação
- Logo oficial (sabia.png) e conjunto de ícones do app desktop. - Interface em português e inglês: tradutor por observação do DOM, seletor de idioma e checagem de cobertura de tradução no CI. - CI/CD com caches (rust-cache, .deb do Tauri em cache) e pipeline de release multiplataforma (CLI em 5 alvos + app desktop via tauri-action), templates de issue/PR. - Suíte de testes de integração do sabia-core (scan e2e, handshake/crack 4-way e PMKID, pcap, trilha de auditoria) — 46 testes verdes. - Documentação completa pt/en: README, ROADMAP, SECURITY, CONTRIBUTING, CHANGELOG e docs/ (cli, interface, arquitetura), com enquadramento de uso autorizado. - Correções de clippy (-D warnings) e formatação em toda a árvore. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
df27974359
commit
791538c9d9
112 files changed
+15620
-494
No files matched your search
@@ -0,0 +1,77 @@
|
||||
//! Integração: trilha de auditoria encadeada por hash — grava entradas,
|
||||
//! confirma que a cadeia valida, depois adultera um registro no disco e
|
||||
//! confirma que a verificação detecta a falha.
|
||||
|
||||
mod common;
|
||||
|
||||
use sabia_core::audit::AuditLog;
|
||||
use std::io::Write;
|
||||
|
||||
#[test]
|
||||
fn chain_validates_after_recording_entries() {
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let path = dir.path().join("audit.log");
|
||||
|
||||
{
|
||||
let mut log = AuditLog::open(&path).expect("open");
|
||||
log.record("monitor-on", "wlan0", "modo monitor ativado")
|
||||
.expect("record 1");
|
||||
log.record("scan-start", "wlan0", "início da captura")
|
||||
.expect("record 2");
|
||||
log.record("deauth", "00:11:22:33:44:55", "teste de deautenticação")
|
||||
.expect("record 3");
|
||||
}
|
||||
|
||||
let entries = AuditLog::read_entries(&path, None).expect("read_entries");
|
||||
assert_eq!(entries.len(), 3);
|
||||
assert_eq!(entries[0].action, "monitor-on");
|
||||
assert_eq!(entries[2].action, "deauth");
|
||||
// cadeia encadeada corretamente
|
||||
assert_eq!(entries[1].prev_hash, entries[0].hash);
|
||||
assert_eq!(entries[2].prev_hash, entries[1].hash);
|
||||
|
||||
let result = AuditLog::verify(&path).expect("verify");
|
||||
assert_eq!(
|
||||
result, None,
|
||||
"cadeia íntegra não deve apontar entrada inválida"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tampering_with_an_entry_breaks_verification() {
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let path = dir.path().join("audit.log");
|
||||
|
||||
{
|
||||
let mut log = AuditLog::open(&path).expect("open");
|
||||
log.record("monitor-on", "wlan0", "modo monitor ativado")
|
||||
.expect("record 1");
|
||||
log.record("scan-start", "wlan0", "início da captura")
|
||||
.expect("record 2");
|
||||
log.record("scan-stop", "wlan0", "fim da captura")
|
||||
.expect("record 3");
|
||||
}
|
||||
|
||||
// Confirma que, intacta, a cadeia passa.
|
||||
assert_eq!(AuditLog::verify(&path).expect("verify ok"), None);
|
||||
|
||||
// Adultera o "detail" da segunda entrada sem recalcular o hash,
|
||||
// simulando uma edição retroativa do log.
|
||||
let contents = std::fs::read_to_string(&path).expect("read");
|
||||
let mut lines: Vec<String> = contents.lines().map(|l| l.to_string()).collect();
|
||||
assert_eq!(lines.len(), 3);
|
||||
assert!(lines[1].contains("início da captura"));
|
||||
lines[1] = lines[1].replace("início da captura", "evento adulterado");
|
||||
|
||||
let mut file = std::fs::File::create(&path).expect("recreate");
|
||||
for line in &lines {
|
||||
writeln!(file, "{line}").expect("write tampered line");
|
||||
}
|
||||
|
||||
let result = AuditLog::verify(&path).expect("verify");
|
||||
assert_eq!(
|
||||
result,
|
||||
Some(1),
|
||||
"verificação deveria apontar a entrada adulterada (índice 1)"
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
#![allow(dead_code)]
|
||||
//! Helpers de teste de integração: quadros 802.11/EAPOL sintéticos,
|
||||
//! radiotap e handshakes válidos gerados a partir de uma senha conhecida.
|
||||
//!
|
||||
//! Porta a lógica de `crates/sabia-core/src/testutil.rs` (que só existe em
|
||||
//! `#[cfg(test)]` e por isso não fica visível aqui), usando só a API
|
||||
//! pública de `sabia_core`.
|
||||
|
||||
use sabia_core::crack::{compute_mic, compute_pmkid, compute_ptk, derive_pmk};
|
||||
use sabia_core::dot11;
|
||||
use sabia_core::eapol::{self, EAPOL_TYPE_KEY, KEY_DATA_OFFSET, MIC_OFFSET};
|
||||
use sabia_core::handshake::Handshake;
|
||||
|
||||
pub const AP: [u8; 6] = [0x00, 0x11, 0x22, 0x33, 0x44, 0x55];
|
||||
pub const STA: [u8; 6] = [0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb];
|
||||
|
||||
pub fn ap() -> [u8; 6] {
|
||||
AP
|
||||
}
|
||||
|
||||
pub fn sta() -> [u8; 6] {
|
||||
STA
|
||||
}
|
||||
|
||||
pub fn key_frame(message: u8, keyver: u8, nonce: [u8; 32], key_data: &[u8]) -> Vec<u8> {
|
||||
let mut key_info = keyver as u16 | 0x0008; // pairwise
|
||||
match message {
|
||||
1 => key_info |= 0x0080,
|
||||
2 => key_info |= 0x0100,
|
||||
3 => key_info |= 0x0080 | 0x0100 | 0x0040,
|
||||
4 => key_info |= 0x0100 | 0x0200,
|
||||
_ => {}
|
||||
}
|
||||
let total = KEY_DATA_OFFSET + key_data.len();
|
||||
let mut f = vec![0u8; total];
|
||||
f[0] = 2;
|
||||
f[1] = EAPOL_TYPE_KEY;
|
||||
f[2..4].copy_from_slice(&((total - 4) as u16).to_be_bytes());
|
||||
f[4] = 2;
|
||||
f[5..7].copy_from_slice(&key_info.to_le_bytes());
|
||||
f[7..9].copy_from_slice(&16u16.to_le_bytes());
|
||||
f[9..17].copy_from_slice(&1u64.to_le_bytes());
|
||||
f[17..49].copy_from_slice(&nonce);
|
||||
f[97..99].copy_from_slice(&(key_data.len() as u16).to_le_bytes());
|
||||
f[KEY_DATA_OFFSET..].copy_from_slice(key_data);
|
||||
f
|
||||
}
|
||||
|
||||
pub fn frame_with_eapol(
|
||||
from_ap: bool,
|
||||
ap: [u8; 6],
|
||||
sta: [u8; 6],
|
||||
da: [u8; 6],
|
||||
eapol: &[u8],
|
||||
) -> Vec<u8> {
|
||||
let mut f = Vec::new();
|
||||
let fc: u16 = if from_ap { 0x0208 } else { 0x0108 };
|
||||
f.extend_from_slice(&fc.to_le_bytes());
|
||||
f.extend_from_slice(&0u16.to_le_bytes());
|
||||
if from_ap {
|
||||
f.extend_from_slice(&sta); // addr1 = destino
|
||||
f.extend_from_slice(&ap); // addr2 = BSSID
|
||||
} else {
|
||||
f.extend_from_slice(&ap); // addr1 = BSSID
|
||||
f.extend_from_slice(&sta); // addr2 = origem
|
||||
}
|
||||
f.extend_from_slice(&da);
|
||||
f.extend_from_slice(&[0u8; 2]);
|
||||
f.extend_from_slice(&[0xAA, 0xAA, 0x03, 0, 0, 0, 0x88, 0x8e]);
|
||||
f.extend_from_slice(eapol);
|
||||
f
|
||||
}
|
||||
|
||||
pub fn radiotap_wrap(dot11: &[u8], freq: u16, signal: i8) -> Vec<u8> {
|
||||
// present: Channel | AntSignal
|
||||
let present: u32 = (1 << 3) | (1 << 5);
|
||||
let mut f = vec![0u8, 0, 13, 0];
|
||||
f.extend_from_slice(&present.to_le_bytes());
|
||||
f.extend_from_slice(&freq.to_le_bytes());
|
||||
f.extend_from_slice(&0xa0u16.to_le_bytes());
|
||||
f.push(signal as u8);
|
||||
f.extend_from_slice(dot11);
|
||||
f
|
||||
}
|
||||
|
||||
fn ie(tag: u8, val: &[u8]) -> Vec<u8> {
|
||||
let mut v = vec![tag, val.len() as u8];
|
||||
v.extend_from_slice(val);
|
||||
v
|
||||
}
|
||||
|
||||
fn rsn_ie(akms: &[u8]) -> Vec<u8> {
|
||||
let mut val = Vec::new();
|
||||
val.extend_from_slice(&1u16.to_le_bytes());
|
||||
val.extend_from_slice(&[0x00, 0x0f, 0xac, 4]);
|
||||
val.extend_from_slice(&1u16.to_le_bytes());
|
||||
val.extend_from_slice(&[0x00, 0x0f, 0xac, 4]);
|
||||
val.extend_from_slice(&(akms.len() as u16).to_le_bytes());
|
||||
for akm in akms {
|
||||
val.extend_from_slice(&[0x00, 0x0f, 0xac, *akm]);
|
||||
}
|
||||
val.extend_from_slice(&0x0040u16.to_le_bytes());
|
||||
ie(48, &val)
|
||||
}
|
||||
|
||||
pub fn beacon_frame(bssid: [u8; 6], ssid: &str, channel: u8, akms: &[u8]) -> Vec<u8> {
|
||||
let mut f = Vec::new();
|
||||
f.extend_from_slice(&0x0080u16.to_le_bytes());
|
||||
f.extend_from_slice(&0u16.to_le_bytes());
|
||||
f.extend_from_slice(&[0xff; 6]);
|
||||
f.extend_from_slice(&bssid);
|
||||
f.extend_from_slice(&bssid);
|
||||
f.extend_from_slice(&[0u8; 2]);
|
||||
f.extend_from_slice(&[0u8; 8]);
|
||||
f.extend_from_slice(&100u16.to_le_bytes());
|
||||
f.extend_from_slice(&0x0031u16.to_le_bytes());
|
||||
f.extend_from_slice(&ie(0, ssid.as_bytes()));
|
||||
f.extend_from_slice(&ie(3, &[channel]));
|
||||
if !akms.is_empty() {
|
||||
f.extend_from_slice(&rsn_ie(akms));
|
||||
}
|
||||
f
|
||||
}
|
||||
|
||||
pub fn probe_request(station: [u8; 6], ssid: &str) -> Vec<u8> {
|
||||
let mut f = Vec::new();
|
||||
f.extend_from_slice(&0x0040u16.to_le_bytes());
|
||||
f.extend_from_slice(&0u16.to_le_bytes());
|
||||
f.extend_from_slice(&[0xff; 6]);
|
||||
f.extend_from_slice(&station);
|
||||
f.extend_from_slice(&[0xff; 6]);
|
||||
f.extend_from_slice(&[0u8; 2]);
|
||||
f.extend_from_slice(&ie(0, ssid.as_bytes()));
|
||||
f
|
||||
}
|
||||
|
||||
pub fn data_frame(
|
||||
to_ds: bool,
|
||||
from_ds: bool,
|
||||
addr1: [u8; 6],
|
||||
addr2: [u8; 6],
|
||||
addr3: [u8; 6],
|
||||
payload: &[u8],
|
||||
) -> Vec<u8> {
|
||||
let mut fc: u16 = 0x0008;
|
||||
if to_ds {
|
||||
fc |= 0x0100;
|
||||
}
|
||||
if from_ds {
|
||||
fc |= 0x0200;
|
||||
}
|
||||
let mut f = Vec::new();
|
||||
f.extend_from_slice(&fc.to_le_bytes());
|
||||
f.extend_from_slice(&0u16.to_le_bytes());
|
||||
f.extend_from_slice(&addr1);
|
||||
f.extend_from_slice(&addr2);
|
||||
f.extend_from_slice(&addr3);
|
||||
f.extend_from_slice(&[0u8; 2]);
|
||||
f.extend_from_slice(payload);
|
||||
f
|
||||
}
|
||||
|
||||
/// Gera (msg1, msg2) válidas para a senha informada, com MIC correto.
|
||||
pub fn handshake_frames(
|
||||
ssid: &str,
|
||||
password: &str,
|
||||
keyver: u8,
|
||||
with_pmkid: bool,
|
||||
) -> (Vec<u8>, Vec<u8>) {
|
||||
let anonce = [0xaa; 32];
|
||||
let snonce = [0xbb; 32];
|
||||
let pmk = derive_pmk(password, ssid);
|
||||
let mut m1_key_data = Vec::new();
|
||||
if with_pmkid {
|
||||
let pmkid = compute_pmkid(&pmk, &AP, &STA);
|
||||
m1_key_data.extend_from_slice(&[0xdd, 20, 0x00, 0x0f, 0xac, 0x04]);
|
||||
m1_key_data.extend_from_slice(&pmkid);
|
||||
}
|
||||
let m1 = key_frame(1, keyver, anonce, &m1_key_data);
|
||||
let mut m2 = key_frame(2, keyver, snonce, &[]);
|
||||
let ptk = compute_ptk(&pmk, &AP, &STA, &anonce, &snonce);
|
||||
let mic = compute_mic(keyver, &ptk[..16], &m2, MIC_OFFSET).expect("mic");
|
||||
m2[MIC_OFFSET..MIC_OFFSET + 16].copy_from_slice(&mic);
|
||||
(m1, m2)
|
||||
}
|
||||
|
||||
/// Monta quadros radiotap+802.11 completos (prontos para `ScanTable::ingest`)
|
||||
/// com o 4-way handshake entre `AP` e `STA` para o SSID/senha dados.
|
||||
pub fn handshake_radiotap_frames(
|
||||
ssid: &str,
|
||||
password: &str,
|
||||
keyver: u8,
|
||||
with_pmkid: bool,
|
||||
) -> (Vec<u8>, Vec<u8>) {
|
||||
let (m1, m2) = handshake_frames(ssid, password, keyver, with_pmkid);
|
||||
let f1 = frame_with_eapol(true, AP, STA, STA, &m1);
|
||||
let f2 = frame_with_eapol(false, AP, STA, AP, &m2);
|
||||
(radiotap_wrap(&f1, 2437, -40), radiotap_wrap(&f2, 2437, -42))
|
||||
}
|
||||
|
||||
/// Monta um `Handshake` diretamente dos quadros EAPOL, para testes de quebra
|
||||
/// sem depender do pipeline de captura.
|
||||
pub fn synthetic_handshake(
|
||||
ssid: &str,
|
||||
bssid: &str,
|
||||
station: &str,
|
||||
m1: &[u8],
|
||||
m2: &[u8],
|
||||
) -> Handshake {
|
||||
let k1 = eapol::parse(m1).expect("msg1");
|
||||
let k2 = if m2.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(eapol::parse(m2).expect("msg2"))
|
||||
};
|
||||
let mut messages = 1u8;
|
||||
if k2.is_some() {
|
||||
messages |= 2;
|
||||
}
|
||||
let hs = Handshake {
|
||||
bssid: bssid.to_string(),
|
||||
station: station.to_string(),
|
||||
ssid: Some(ssid.to_string()),
|
||||
anonce: Some(k1.nonce),
|
||||
snonce: k2.as_ref().map(|k| k.nonce),
|
||||
pmkid: k1.pmkid,
|
||||
key_version: k1.key_version,
|
||||
messages,
|
||||
eapol_msg2: k2.as_ref().map(|k| k.frame.clone()),
|
||||
raw: Vec::new(),
|
||||
first_seen_unix: 1,
|
||||
last_seen_unix: 2,
|
||||
password: None,
|
||||
};
|
||||
// Exercita o parser de MAC do alvo.
|
||||
assert!(dot11::parse_mac(&hs.bssid).is_some());
|
||||
assert!(dot11::parse_mac(&hs.station).is_some());
|
||||
hs
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
//! Integração: handshake sintético (gerado com senha conhecida) -> montagem
|
||||
//! de `CrackTarget` -> `verify()`, para os dois métodos (EAPOL 4-way e
|
||||
//! PMKID), cobrindo senha certa e errada.
|
||||
|
||||
mod common;
|
||||
|
||||
use sabia_core::crack::{compute_mic, compute_pmkid, compute_ptk, derive_pmk, CrackTarget};
|
||||
|
||||
const BSSID: &str = "00:11:22:33:44:55";
|
||||
const STATION: &str = "66:77:88:99:aa:bb";
|
||||
|
||||
#[test]
|
||||
fn eapol_4way_roundtrip_verifies_password() {
|
||||
let (m1, m2) = common::handshake_frames("MinhaRede", "senha-correta", 2, false);
|
||||
let hs = common::synthetic_handshake("MinhaRede", BSSID, STATION, &m1, &m2);
|
||||
|
||||
let target = CrackTarget::from_handshake(&hs).expect("alvo EAPOL válido");
|
||||
assert_eq!(target.method(), "EAPOL/CCMP");
|
||||
assert!(target.verify("senha-correta"));
|
||||
assert!(!target.verify("senha-errada"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pmkid_roundtrip_verifies_password() {
|
||||
let (m1, _m2) = common::handshake_frames("MinhaRede", "senha-correta", 2, true);
|
||||
let hs = common::synthetic_handshake("MinhaRede", BSSID, STATION, &m1, &[]);
|
||||
|
||||
let target = CrackTarget::from_handshake(&hs).expect("alvo PMKID válido");
|
||||
assert_eq!(target.method(), "PMKID");
|
||||
assert!(target.verify("senha-correta"));
|
||||
assert!(!target.verify("senha-errada"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tkip_keyversion_roundtrip_verifies_password() {
|
||||
let (m1, m2) = common::handshake_frames("RedeAntiga", "outra-senha", 1, false);
|
||||
let hs = common::synthetic_handshake("RedeAntiga", BSSID, STATION, &m1, &m2);
|
||||
|
||||
let target = CrackTarget::from_handshake(&hs).expect("alvo EAPOL/TKIP válido");
|
||||
assert_eq!(target.method(), "EAPOL/TKIP");
|
||||
assert!(target.verify("outra-senha"));
|
||||
assert!(!target.verify("senha-errada"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn incomplete_handshake_without_pmkid_or_msg2_has_no_target() {
|
||||
let (m1, _m2) = common::handshake_frames("MinhaRede", "senha-correta", 2, false);
|
||||
// Sem msg2 e sem PMKID: não há prova suficiente para quebrar.
|
||||
let hs = common::synthetic_handshake("MinhaRede", BSSID, STATION, &m1, &[]);
|
||||
assert!(CrackTarget::from_handshake(&hs).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn derive_pmk_compute_ptk_and_mic_are_consistent() {
|
||||
let ssid = "VetorDireto";
|
||||
let password = "senha-direta";
|
||||
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);
|
||||
|
||||
// MIC calculado sobre um quadro msg2 qualquer, zerando o campo MIC antes.
|
||||
let mut m2 = common::key_frame(2, 2, snonce, &[]);
|
||||
let mic = compute_mic(2, &ptk[..16], &m2, sabia_core::eapol::MIC_OFFSET).expect("mic");
|
||||
m2[sabia_core::eapol::MIC_OFFSET..sabia_core::eapol::MIC_OFFSET + 16].copy_from_slice(&mic);
|
||||
|
||||
// Recalculando o MIC sobre o quadro já preenchido deve bater de novo
|
||||
// (compute_mic zera o campo internamente antes de recalcular).
|
||||
let mic_again = compute_mic(2, &ptk[..16], &m2, sabia_core::eapol::MIC_OFFSET).expect("mic");
|
||||
assert_eq!(mic, mic_again);
|
||||
|
||||
// PMKID direto também deve ser determinístico e usável no verify().
|
||||
let pmkid = compute_pmkid(&pmk, &common::ap(), &common::sta());
|
||||
assert_eq!(
|
||||
pmkid,
|
||||
compute_pmkid(&pmk, &common::ap(), &common::sta()),
|
||||
"PMKID deve ser determinístico"
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
//! Integração: grava quadros com `PcapWriter`, lê de volta com
|
||||
//! `PcapReader` e confere que batem byte a byte; depois confirma que
|
||||
//! `Engine::analyze_pcap` encontra redes e handshakes no arquivo recém-escrito.
|
||||
|
||||
mod common;
|
||||
|
||||
use sabia_core::engine::Engine;
|
||||
use sabia_core::pcapreader::PcapReader;
|
||||
use sabia_core::pcapwriter::PcapWriter;
|
||||
|
||||
#[test]
|
||||
fn writes_and_reads_back_identical_frames() {
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let path = dir.path().join("captura.pcap");
|
||||
|
||||
let beacon = common::radiotap_wrap(
|
||||
&common::beacon_frame(common::ap(), "PcapRede", 1, &[2]),
|
||||
2412,
|
||||
-30,
|
||||
);
|
||||
let probe = common::radiotap_wrap(&common::probe_request(common::sta(), "PcapRede"), 2412, -45);
|
||||
|
||||
{
|
||||
let mut writer = PcapWriter::create(&path).expect("create");
|
||||
writer.write_frame(&beacon).expect("write beacon");
|
||||
writer.write_frame(&probe).expect("write probe");
|
||||
writer.flush().expect("flush");
|
||||
}
|
||||
|
||||
let mut reader = PcapReader::open(&path).expect("open");
|
||||
assert!(reader.supports_80211());
|
||||
|
||||
let f1 = reader
|
||||
.next_frame()
|
||||
.expect("read 1")
|
||||
.expect("frame 1 presente");
|
||||
assert_eq!(f1.data, beacon);
|
||||
|
||||
let f2 = reader
|
||||
.next_frame()
|
||||
.expect("read 2")
|
||||
.expect("frame 2 presente");
|
||||
assert_eq!(f2.data, probe);
|
||||
|
||||
assert!(reader.next_frame().expect("read 3").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn analyze_pcap_finds_networks_and_handshake() {
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let pcap_path = dir.path().join("sessao.pcap");
|
||||
let audit_path = dir.path().join("audit.log");
|
||||
|
||||
let beacon = common::radiotap_wrap(
|
||||
&common::beacon_frame(common::ap(), "PcapHandshake", 3, &[2]),
|
||||
2422,
|
||||
-38,
|
||||
);
|
||||
let (f1, f2) = common::handshake_radiotap_frames("PcapHandshake", "senha-pcap", 2, false);
|
||||
|
||||
{
|
||||
let mut writer = PcapWriter::create(&pcap_path).expect("create");
|
||||
writer.write_frame(&beacon).expect("write beacon");
|
||||
writer.write_frame(&f1).expect("write msg1");
|
||||
writer.write_frame(&f2).expect("write msg2");
|
||||
writer.flush().expect("flush");
|
||||
}
|
||||
|
||||
let engine = Engine::with_audit_path(&audit_path);
|
||||
let snapshot = engine.analyze_pcap(&pcap_path).expect("analyze_pcap");
|
||||
|
||||
assert_eq!(snapshot.access_points.len(), 1);
|
||||
assert_eq!(
|
||||
snapshot.access_points[0].ssid.as_deref(),
|
||||
Some("PcapHandshake")
|
||||
);
|
||||
|
||||
assert_eq!(snapshot.handshakes.len(), 1);
|
||||
let hs = &snapshot.handshakes[0];
|
||||
assert_eq!(hs.ssid.as_deref(), Some("PcapHandshake"));
|
||||
assert!(hs.complete);
|
||||
assert!(hs.crackable);
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
//! Integração: beacon + probe + handshake completo passando pelo caminho
|
||||
//! público de agregação (`ScanTable`), verificando que AP, cliente e
|
||||
//! handshake são detectados de ponta a ponta.
|
||||
|
||||
mod common;
|
||||
|
||||
use sabia_core::scan::ScanTable;
|
||||
|
||||
#[test]
|
||||
fn detects_ap_client_and_handshake_end_to_end() {
|
||||
let mut table = ScanTable::new();
|
||||
|
||||
// RSN com AKM PSK (2) para que a rede apareça como 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();
|
||||
|
||||
// SSID vazio = rede oculta.
|
||||
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);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pmkid_only_handshake_is_detected_and_crackable() {
|
||||
let mut table = ScanTable::new();
|
||||
|
||||
// Beacon com SSID visível, para que o handshake herde o SSID via hint.
|
||||
let beacon = common::beacon_frame(common::ap(), "RedePmkid", 11, &[2]);
|
||||
table.ingest_at(&common::radiotap_wrap(&beacon, 2462, -60), 1);
|
||||
|
||||
// Handshake via PMKID (só msg1, sem msg2/snonce).
|
||||
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");
|
||||
}
|
||||
Reference in new issue
Block a user