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:
Lucas RayanandClaude Opus 5.5 committed 2026-10-23 12:46:19 -03:00
1 parent df27974359
commit 791538c9d9
112 files changed
+15620 -494

No files matched your search

+194
View File
@@ -0,0 +1,194 @@
//! Parser do handshake WPA/WPA2 (EAPOL-Key, IEEE 802.1X).
//!
//! Lê as quatro mensagens do 4-way handshake, identifica o número de cada
//! uma, extrai nonces, MIC, versão de chave e PMKID — matéria-prima para
//! montar um alvo de quebra por dicionário em [`crate::crack`].
pub const EAPOL_TYPE_KEY: u8 = 3;
const KEY_INFO_PAIRWISE: u16 = 0x0008;
const KEY_INFO_INSTALL: u16 = 0x0040;
const KEY_INFO_ACK: u16 = 0x0080;
const KEY_INFO_MIC: u16 = 0x0100;
const KEY_INFO_SECURE: u16 = 0x0200;
/// Deslocamento absoluto do campo MIC dentro do quadro EAPOL (já incluindo
/// o cabeçalho de 4 bytes).
pub const MIC_OFFSET: usize = 81;
pub const KEY_DATA_OFFSET: usize = 99;
pub const MIN_KEY_FRAME_LEN: usize = 99;
#[derive(Debug, Clone)]
pub struct EapolKey {
pub version: u8,
pub descriptor_type: u8,
/// 1, 2, 3 ou 4; 0 quando não é uma das mensagens do 4-way handshake.
pub message: u8,
pub key_info: u16,
/// Versão do descritor: 1 = TKIP, 2 = CCMP, 3 = AES-CMAC.
pub key_version: u8,
pub pairwise: bool,
pub replay_counter: u64,
pub nonce: [u8; 32],
pub mic: [u8; 16],
pub key_data: Vec<u8>,
pub pmkid: Option<[u8; 16]>,
/// Quadro EAPOL completo (cabeçalho + descritor + dados), com o MIC no
/// lugar — usado para recalcular o MIC durante a quebra.
pub frame: Vec<u8>,
}
pub fn parse(buf: &[u8]) -> Option<EapolKey> {
if buf.len() < 4 || buf[1] != EAPOL_TYPE_KEY {
return None;
}
let version = buf[0];
let declared = u16::from_be_bytes([buf[2], buf[3]]) as usize;
let total = 4 + declared;
if total < MIN_KEY_FRAME_LEN || buf.len() < total {
return None;
}
let frame = buf[..total].to_vec();
let descriptor_type = frame[4];
// 2 = RSN (WPA2/WPA3), 254 = WPA1. Outros descritores não interessam.
if descriptor_type != 2 && descriptor_type != 254 {
return None;
}
let key_info = u16::from_le_bytes([frame[5], frame[6]]);
let key_version = (key_info & 0x0007) as u8;
let pairwise = key_info & KEY_INFO_PAIRWISE != 0;
let ack = key_info & KEY_INFO_ACK != 0;
let mic = key_info & KEY_INFO_MIC != 0;
let install = key_info & KEY_INFO_INSTALL != 0;
let secure = key_info & KEY_INFO_SECURE != 0;
let message = if ack && !mic {
1
} else if !ack && mic && !secure {
2
} else if ack && mic && (install || !secure) {
3
} else if !ack && mic && secure {
4
} else {
0
};
let replay_counter = u64::from_le_bytes(frame[9..17].try_into().ok()?);
let mut nonce = [0u8; 32];
nonce.copy_from_slice(&frame[17..49]);
let mut mic_bytes = [0u8; 16];
mic_bytes.copy_from_slice(&frame[MIC_OFFSET..MIC_OFFSET + 16]);
let key_data_len = u16::from_le_bytes([frame[97], frame[98]]) as usize;
let data_end = KEY_DATA_OFFSET.checked_add(key_data_len)?;
if data_end > frame.len() {
return None;
}
let key_data = frame[KEY_DATA_OFFSET..data_end].to_vec();
let pmkid = parse_pmkid_kde(&key_data);
Some(EapolKey {
version,
descriptor_type,
message,
key_info,
key_version,
pairwise,
replay_counter,
nonce,
mic: mic_bytes,
key_data,
pmkid,
frame,
})
}
/// PMKID KDE: `dd 14 00 0f ac 04` + 16 bytes. Pode vir em qualquer posição
/// dos dados da chave (normalmente na mensagem 1).
pub fn parse_pmkid_kde(data: &[u8]) -> Option<[u8; 16]> {
let mut i = 0usize;
while i + 2 <= data.len() {
let tag = data[i];
let len = data[i + 1] as usize;
let start = i + 2;
if start + len > data.len() {
break;
}
if tag == 0xdd && len >= 20 && data[start..start + 4] == [0x00, 0x0f, 0xac, 0x04] {
let mut pmkid = [0u8; 16];
pmkid.copy_from_slice(&data[start + 4..start + 20]);
return Some(pmkid);
}
i = start + len;
}
None
}
#[cfg(test)]
mod tests {
use super::*;
pub(crate) fn key_frame(message: u8, keyver: u8, nonce: [u8; 32], key_data: &[u8]) -> Vec<u8> {
let mut key_info = keyver as u16 | KEY_INFO_PAIRWISE;
match message {
1 => key_info |= KEY_INFO_ACK,
2 => key_info |= KEY_INFO_MIC,
3 => key_info |= KEY_INFO_ACK | KEY_INFO_MIC | KEY_INFO_INSTALL,
4 => key_info |= KEY_INFO_MIC | KEY_INFO_SECURE,
_ => {}
}
let total = KEY_DATA_OFFSET + key_data.len();
let mut f = vec![0u8; total];
f[0] = 2; // EAPOL version
f[1] = EAPOL_TYPE_KEY;
f[2..4].copy_from_slice(&((total - 4) as u16).to_be_bytes());
f[4] = 2; // RSN descriptor
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);
for (i, b) in f[MIC_OFFSET..MIC_OFFSET + 16].iter_mut().enumerate() {
*b = i as u8;
}
f[97..99].copy_from_slice(&(key_data.len() as u16).to_le_bytes());
f[KEY_DATA_OFFSET..].copy_from_slice(key_data);
f
}
#[test]
fn identifies_all_four_messages() {
for msg in 1..=4u8 {
let frame = key_frame(msg, 2, [msg; 32], &[]);
let parsed = parse(&frame).expect("eapol");
assert_eq!(parsed.message, msg);
assert_eq!(parsed.key_version, 2);
assert!(parsed.pairwise);
assert_eq!(parsed.nonce, [msg; 32]);
assert_eq!(parsed.mic, {
let mut m = [0u8; 16];
for (i, b) in m.iter_mut().enumerate() {
*b = i as u8;
}
m
});
assert_eq!(parsed.frame, frame);
}
}
#[test]
fn extracts_pmkid_from_key_data() {
let mut data = vec![0xdd, 20, 0x00, 0x0f, 0xac, 0x04];
data.extend_from_slice(&[0xab; 16]);
let frame = key_frame(1, 2, [0x11; 32], &data);
let parsed = parse(&frame).expect("eapol");
assert_eq!(parsed.pmkid, Some([0xab; 16]));
}
#[test]
fn rejects_non_key_and_truncated_frames() {
assert!(parse(&[2, 0, 0, 0]).is_none());
let frame = key_frame(2, 2, [0; 32], &[]);
assert!(parse(&frame[..frame.len() - 1]).is_none());
}
}