use md5::{Digest, Md5};
use std::fmt;
pub const ETHERTYPE_EAPOL: u16 = 0x888E;
pub const PAE_GROUP_ADDRESS: [u8; 6] = [0x01, 0x80, 0xC2, 0x00, 0x00, 0x03];
pub const ETHERNET_HEADER_LEN: usize = 14;
pub const EAPOL_HEADER_LEN: usize = 4;
pub const EAP_HEADER_LEN: usize = 4;
pub const ETHERNET_MIN_FRAME_LEN: usize = 60;
pub const EAPOL_TYPE_EAP_PACKET: u8 = 0;
pub const EAPOL_TYPE_START: u8 = 1;
pub const EAPOL_TYPE_LOGOFF: u8 = 2;
pub const EAPOL_TYPE_KEY: u8 = 3;
pub const EAPOL_TYPE_ASF_ALERT: u8 = 4;
pub const EAPOL_VERSION_2001: u8 = 1;
pub const EAPOL_VERSION_2004: u8 = 2;
pub const EAPOL_VERSION_2010: u8 = 3;
pub const EAP_CODE_REQUEST: u8 = 1;
pub const EAP_CODE_RESPONSE: u8 = 2;
pub const EAP_CODE_SUCCESS: u8 = 3;
pub const EAP_CODE_FAILURE: u8 = 4;
pub const EAP_TYPE_IDENTITY: u8 = 1;
pub const EAP_TYPE_NOTIFICATION: u8 = 2;
pub const EAP_TYPE_NAK: u8 = 3;
pub const EAP_TYPE_MD5_CHALLENGE: u8 = 4;
pub const EAP_TYPE_TLS: u8 = 13;
pub const EAP_TYPE_PEAP: u8 = 25;
pub const EAP_TYPE_MSCHAPV2: u8 = 26;
pub const TLS_FLAG_LENGTH_INCLUDED: u8 = 0x80;
pub const TLS_FLAG_MORE_FRAGMENTS: u8 = 0x40;
pub const TLS_FLAG_START: u8 = 0x20;
pub const MD5_CHALLENGE_LEN: usize = 16;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CodecError {
TooShort {
layer: &'static str,
needed: usize,
got: usize,
},
UnsupportedVersion(u8),
BadLength {
layer: &'static str,
declared: usize,
available: usize,
},
MalformedEap(&'static str),
NotEapol(u16),
BadMac(String),
FieldTooLong {
field: &'static str,
max: usize,
got: usize,
},
}
impl fmt::Display for CodecError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
CodecError::TooShort { layer, needed, got } => {
write!(f, "{} needs at least {} octets, got {}", layer, needed, got)
}
CodecError::UnsupportedVersion(v) => write!(
f,
"EAPOL protocol version {} is not 1 (802.1X-2001), 2 (802.1X-2004) or 3 \
(802.1X-2010)",
v
),
CodecError::BadLength {
layer,
declared,
available,
} => write!(
f,
"{} declares a length of {} octets but only {} are present",
layer, declared, available
),
CodecError::MalformedEap(detail) => write!(f, "malformed EAP packet: {}", detail),
CodecError::NotEapol(ethertype) => write!(
f,
"EtherType 0x{:04x} is not EAPOL (0x{:04x})",
ethertype, ETHERTYPE_EAPOL
),
CodecError::BadMac(s) => write!(
f,
"'{}' is not a MAC address; expected six hex octets like 02:00:00:00:00:01",
s
),
CodecError::FieldTooLong { field, max, got } => write!(
f,
"{} is {} octets; the wire format allows at most {}",
field, got, max
),
}
}
}
impl std::error::Error for CodecError {}
pub type CodecResult<T> = Result<T, CodecError>;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EthernetFrame {
pub destination: [u8; 6],
pub source: [u8; 6],
pub ethertype: u16,
pub payload: Vec<u8>,
}
pub fn parse_ethernet_frame(bytes: &[u8]) -> CodecResult<EthernetFrame> {
if bytes.len() < ETHERNET_HEADER_LEN {
return Err(CodecError::TooShort {
layer: "Ethernet header",
needed: ETHERNET_HEADER_LEN,
got: bytes.len(),
});
}
let mut destination = [0u8; 6];
let mut source = [0u8; 6];
destination.copy_from_slice(&bytes[0..6]);
source.copy_from_slice(&bytes[6..12]);
Ok(EthernetFrame {
destination,
source,
ethertype: u16::from_be_bytes([bytes[12], bytes[13]]),
payload: bytes[ETHERNET_HEADER_LEN..].to_vec(),
})
}
pub fn build_ethernet_frame(
destination: [u8; 6],
source: [u8; 6],
ethertype: u16,
payload: &[u8],
) -> Vec<u8> {
let mut out =
Vec::with_capacity(ETHERNET_MIN_FRAME_LEN.max(ETHERNET_HEADER_LEN + payload.len()));
out.extend_from_slice(&destination);
out.extend_from_slice(&source);
out.extend_from_slice(ðertype.to_be_bytes());
out.extend_from_slice(payload);
if out.len() < ETHERNET_MIN_FRAME_LEN {
out.resize(ETHERNET_MIN_FRAME_LEN, 0);
}
out
}
pub fn format_mac(mac: &[u8; 6]) -> String {
format!(
"{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}",
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]
)
}
pub fn parse_mac(s: &str) -> CodecResult<[u8; 6]> {
let cleaned: String = s
.chars()
.filter(|c| !matches!(c, ':' | '-' | '.' | ' '))
.collect();
if cleaned.len() != 12 || !cleaned.chars().all(|c| c.is_ascii_hexdigit()) {
return Err(CodecError::BadMac(s.to_string()));
}
let mut mac = [0u8; 6];
for (i, slot) in mac.iter_mut().enumerate() {
*slot = u8::from_str_radix(&cleaned[i * 2..i * 2 + 2], 16)
.map_err(|_| CodecError::BadMac(s.to_string()))?;
}
Ok(mac)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EapolFrame {
pub version: u8,
pub packet_type: u8,
pub body: Vec<u8>,
}
impl EapolFrame {
pub fn decode(bytes: &[u8]) -> CodecResult<Self> {
if bytes.len() < EAPOL_HEADER_LEN {
return Err(CodecError::TooShort {
layer: "EAPOL header",
needed: EAPOL_HEADER_LEN,
got: bytes.len(),
});
}
let version = bytes[0];
if !(EAPOL_VERSION_2001..=EAPOL_VERSION_2010).contains(&version) {
return Err(CodecError::UnsupportedVersion(version));
}
let declared = u16::from_be_bytes([bytes[2], bytes[3]]) as usize;
let available = bytes.len() - EAPOL_HEADER_LEN;
if declared > available {
return Err(CodecError::BadLength {
layer: "EAPOL body",
declared,
available,
});
}
Ok(EapolFrame {
version,
packet_type: bytes[1],
body: bytes[EAPOL_HEADER_LEN..EAPOL_HEADER_LEN + declared].to_vec(),
})
}
pub fn encode(&self) -> CodecResult<Vec<u8>> {
let declared = u16::try_from(self.body.len()).map_err(|_| CodecError::FieldTooLong {
field: "EAPOL body",
max: u16::MAX as usize,
got: self.body.len(),
})?;
let mut out = Vec::with_capacity(EAPOL_HEADER_LEN + self.body.len());
out.push(self.version);
out.push(self.packet_type);
out.extend_from_slice(&declared.to_be_bytes());
out.extend_from_slice(&self.body);
Ok(out)
}
}
pub fn eapol_packet_type_name(packet_type: u8) -> &'static str {
match packet_type {
EAPOL_TYPE_EAP_PACKET => "EAP-Packet",
EAPOL_TYPE_START => "EAPOL-Start",
EAPOL_TYPE_LOGOFF => "EAPOL-Logoff",
EAPOL_TYPE_KEY => "EAPOL-Key",
EAPOL_TYPE_ASF_ALERT => "EAPOL-Encapsulated-ASF-Alert",
_ => "Unknown-EAPOL-Type",
}
}
pub fn eapol_wrap_eap(version: u8, eap: &[u8]) -> CodecResult<Vec<u8>> {
EapolFrame {
version,
packet_type: EAPOL_TYPE_EAP_PACKET,
body: eap.to_vec(),
}
.encode()
}
pub fn eapol_start_frame(version: u8) -> Vec<u8> {
vec![version, EAPOL_TYPE_START, 0x00, 0x00]
}
pub fn eapol_logoff_frame(version: u8) -> Vec<u8> {
vec![version, EAPOL_TYPE_LOGOFF, 0x00, 0x00]
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EapPacket {
pub code: u8,
pub identifier: u8,
pub eap_type: Option<u8>,
pub type_data: Vec<u8>,
}
impl EapPacket {
pub fn decode(bytes: &[u8]) -> CodecResult<Self> {
if bytes.len() < EAP_HEADER_LEN {
return Err(CodecError::TooShort {
layer: "EAP header",
needed: EAP_HEADER_LEN,
got: bytes.len(),
});
}
let code = bytes[0];
let identifier = bytes[1];
let declared = u16::from_be_bytes([bytes[2], bytes[3]]) as usize;
if declared < EAP_HEADER_LEN {
return Err(CodecError::BadLength {
layer: "EAP packet",
declared,
available: bytes.len(),
});
}
if declared > bytes.len() {
return Err(CodecError::BadLength {
layer: "EAP packet",
declared,
available: bytes.len(),
});
}
let body = &bytes[..declared];
match code {
EAP_CODE_REQUEST | EAP_CODE_RESPONSE => {
if declared < EAP_HEADER_LEN + 1 {
return Err(CodecError::MalformedEap(
"a Request or Response must carry a type octet (RFC 3748 §4.1)",
));
}
Ok(EapPacket {
code,
identifier,
eap_type: Some(body[4]),
type_data: body[5..].to_vec(),
})
}
EAP_CODE_SUCCESS | EAP_CODE_FAILURE => {
if declared != EAP_HEADER_LEN {
return Err(CodecError::MalformedEap(
"a Success or Failure is exactly four octets (RFC 3748 §4.2)",
));
}
Ok(EapPacket {
code,
identifier,
eap_type: None,
type_data: Vec::new(),
})
}
_ => Err(CodecError::MalformedEap(
"EAP code must be 1 (Request), 2 (Response), 3 (Success) or 4 (Failure)",
)),
}
}
}
pub fn eap_code_name(code: u8) -> &'static str {
match code {
EAP_CODE_REQUEST => "EAP-Request",
EAP_CODE_RESPONSE => "EAP-Response",
EAP_CODE_SUCCESS => "EAP-Success",
EAP_CODE_FAILURE => "EAP-Failure",
_ => "Unknown-EAP-Code",
}
}
pub fn eap_type_name(eap_type: u8) -> &'static str {
match eap_type {
EAP_TYPE_IDENTITY => "identity",
EAP_TYPE_NOTIFICATION => "notification",
EAP_TYPE_NAK => "nak",
EAP_TYPE_MD5_CHALLENGE => "md5-challenge",
EAP_TYPE_TLS => "tls",
EAP_TYPE_PEAP => "peap",
EAP_TYPE_MSCHAPV2 => "mschapv2",
_ => "unknown",
}
}
fn encode_eap_typed(
code: u8,
identifier: u8,
eap_type: u8,
type_data: &[u8],
) -> CodecResult<Vec<u8>> {
let length = EAP_HEADER_LEN + 1 + type_data.len();
if length > u16::MAX as usize {
return Err(CodecError::FieldTooLong {
field: "EAP type-data",
max: u16::MAX as usize - EAP_HEADER_LEN - 1,
got: type_data.len(),
});
}
let mut out = Vec::with_capacity(length);
out.push(code);
out.push(identifier);
out.extend_from_slice(&(length as u16).to_be_bytes());
out.push(eap_type);
out.extend_from_slice(type_data);
Ok(out)
}
pub fn eap_request_identity(identifier: u8) -> Vec<u8> {
vec![EAP_CODE_REQUEST, identifier, 0x00, 0x05, EAP_TYPE_IDENTITY]
}
pub fn eap_request_notification(identifier: u8, text: &str) -> CodecResult<Vec<u8>> {
encode_eap_typed(
EAP_CODE_REQUEST,
identifier,
EAP_TYPE_NOTIFICATION,
text.as_bytes(),
)
}
pub fn eap_request_md5_challenge(
identifier: u8,
challenge: &[u8],
name: &str,
) -> CodecResult<Vec<u8>> {
let value = encode_md5_value(challenge, name)?;
encode_eap_typed(EAP_CODE_REQUEST, identifier, EAP_TYPE_MD5_CHALLENGE, &value)
}
pub fn eap_request_tls_start(identifier: u8, eap_type: u8) -> CodecResult<Vec<u8>> {
encode_eap_typed(EAP_CODE_REQUEST, identifier, eap_type, &[TLS_FLAG_START])
}
pub fn eap_response(identifier: u8, eap_type: u8, type_data: &[u8]) -> CodecResult<Vec<u8>> {
encode_eap_typed(EAP_CODE_RESPONSE, identifier, eap_type, type_data)
}
pub fn eap_response_identity(identifier: u8, identity: &str) -> CodecResult<Vec<u8>> {
encode_eap_typed(
EAP_CODE_RESPONSE,
identifier,
EAP_TYPE_IDENTITY,
identity.as_bytes(),
)
}
pub fn eapol_eap_success_frame(version: u8, identifier: u8) -> Vec<u8> {
vec![
version,
EAPOL_TYPE_EAP_PACKET,
0x00,
0x04,
EAP_CODE_SUCCESS,
identifier,
0x00,
0x04,
]
}
pub fn eapol_eap_failure_frame(version: u8, identifier: u8) -> Vec<u8> {
vec![
version,
EAPOL_TYPE_EAP_PACKET,
0x00,
0x04,
EAP_CODE_FAILURE,
identifier,
0x00,
0x04,
]
}
pub fn encode_md5_value(value: &[u8], name: &str) -> CodecResult<Vec<u8>> {
if value.len() > u8::MAX as usize {
return Err(CodecError::FieldTooLong {
field: "MD5-Challenge value",
max: u8::MAX as usize,
got: value.len(),
});
}
let mut out = Vec::with_capacity(1 + value.len() + name.len());
out.push(value.len() as u8);
out.extend_from_slice(value);
out.extend_from_slice(name.as_bytes());
Ok(out)
}
pub fn decode_md5_value(type_data: &[u8]) -> CodecResult<(Vec<u8>, String)> {
if type_data.is_empty() {
return Err(CodecError::TooShort {
layer: "MD5-Challenge value",
needed: 1,
got: 0,
});
}
let size = type_data[0] as usize;
if type_data.len() < 1 + size {
return Err(CodecError::BadLength {
layer: "MD5-Challenge value",
declared: size,
available: type_data.len() - 1,
});
}
Ok((
type_data[1..1 + size].to_vec(),
String::from_utf8_lossy(&type_data[1 + size..]).into_owned(),
))
}
pub fn tls_flags(type_data: &[u8]) -> Option<(bool, bool, bool)> {
type_data.first().map(|f| {
(
f & TLS_FLAG_LENGTH_INCLUDED != 0,
f & TLS_FLAG_MORE_FRAGMENTS != 0,
f & TLS_FLAG_START != 0,
)
})
}
pub fn md5(data: &[u8]) -> [u8; 16] {
let mut hasher = Md5::new();
hasher.update(data);
hasher.finalize().into()
}
pub fn md5_challenge_digest(identifier: u8, secret: &str, challenge: &[u8]) -> [u8; 16] {
let mut hasher = Md5::new();
hasher.update([identifier]);
hasher.update(secret.as_bytes());
hasher.update(challenge);
hasher.finalize().into()
}
pub fn md5_response_matches(
identifier: u8,
secret: &str,
challenge: &[u8],
response_value: &[u8],
) -> bool {
let expected = md5_challenge_digest(identifier, secret, challenge);
if response_value.len() != expected.len() {
return false;
}
let mut diff = 0u8;
for (a, b) in expected.iter().zip(response_value.iter()) {
diff |= a ^ b;
}
diff == 0
}