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,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());
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user