1use md5::{Digest, Md5};
23use std::fmt;
24
25pub const ETHERTYPE_EAPOL: u16 = 0x888E;
31
32pub const PAE_GROUP_ADDRESS: [u8; 6] = [0x01, 0x80, 0xC2, 0x00, 0x00, 0x03];
34
35pub const ETHERNET_HEADER_LEN: usize = 14;
37
38pub const EAPOL_HEADER_LEN: usize = 4;
40
41pub const EAP_HEADER_LEN: usize = 4;
43
44pub const ETHERNET_MIN_FRAME_LEN: usize = 60;
48
49pub const EAPOL_TYPE_EAP_PACKET: u8 = 0;
51pub const EAPOL_TYPE_START: u8 = 1;
52pub const EAPOL_TYPE_LOGOFF: u8 = 2;
53pub const EAPOL_TYPE_KEY: u8 = 3;
54pub const EAPOL_TYPE_ASF_ALERT: u8 = 4;
55
56pub const EAPOL_VERSION_2001: u8 = 1;
58pub const EAPOL_VERSION_2004: u8 = 2;
60pub const EAPOL_VERSION_2010: u8 = 3;
62
63pub const EAP_CODE_REQUEST: u8 = 1;
65pub const EAP_CODE_RESPONSE: u8 = 2;
66pub const EAP_CODE_SUCCESS: u8 = 3;
67pub const EAP_CODE_FAILURE: u8 = 4;
68
69pub const EAP_TYPE_IDENTITY: u8 = 1;
71pub const EAP_TYPE_NOTIFICATION: u8 = 2;
72pub const EAP_TYPE_NAK: u8 = 3;
73pub const EAP_TYPE_MD5_CHALLENGE: u8 = 4;
74pub const EAP_TYPE_TLS: u8 = 13;
75pub const EAP_TYPE_PEAP: u8 = 25;
76pub const EAP_TYPE_MSCHAPV2: u8 = 26;
77
78pub const TLS_FLAG_LENGTH_INCLUDED: u8 = 0x80;
80pub const TLS_FLAG_MORE_FRAGMENTS: u8 = 0x40;
81pub const TLS_FLAG_START: u8 = 0x20;
82
83pub const MD5_CHALLENGE_LEN: usize = 16;
85
86#[derive(Debug, Clone, PartialEq, Eq)]
93pub enum CodecError {
94 TooShort {
96 layer: &'static str,
97 needed: usize,
98 got: usize,
99 },
100 UnsupportedVersion(u8),
102 BadLength {
104 layer: &'static str,
105 declared: usize,
106 available: usize,
107 },
108 MalformedEap(&'static str),
110 NotEapol(u16),
112 BadMac(String),
114 FieldTooLong {
116 field: &'static str,
117 max: usize,
118 got: usize,
119 },
120}
121
122impl fmt::Display for CodecError {
123 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
124 match self {
125 CodecError::TooShort { layer, needed, got } => {
126 write!(f, "{} needs at least {} octets, got {}", layer, needed, got)
127 }
128 CodecError::UnsupportedVersion(v) => write!(
129 f,
130 "EAPOL protocol version {} is not 1 (802.1X-2001), 2 (802.1X-2004) or 3 \
131 (802.1X-2010)",
132 v
133 ),
134 CodecError::BadLength {
135 layer,
136 declared,
137 available,
138 } => write!(
139 f,
140 "{} declares a length of {} octets but only {} are present",
141 layer, declared, available
142 ),
143 CodecError::MalformedEap(detail) => write!(f, "malformed EAP packet: {}", detail),
144 CodecError::NotEapol(ethertype) => write!(
145 f,
146 "EtherType 0x{:04x} is not EAPOL (0x{:04x})",
147 ethertype, ETHERTYPE_EAPOL
148 ),
149 CodecError::BadMac(s) => write!(
150 f,
151 "'{}' is not a MAC address; expected six hex octets like 02:00:00:00:00:01",
152 s
153 ),
154 CodecError::FieldTooLong { field, max, got } => write!(
155 f,
156 "{} is {} octets; the wire format allows at most {}",
157 field, got, max
158 ),
159 }
160 }
161}
162
163impl std::error::Error for CodecError {}
164
165pub type CodecResult<T> = Result<T, CodecError>;
166
167#[derive(Debug, Clone, PartialEq, Eq)]
174pub struct EthernetFrame {
175 pub destination: [u8; 6],
176 pub source: [u8; 6],
177 pub ethertype: u16,
178 pub payload: Vec<u8>,
179}
180
181pub fn parse_ethernet_frame(bytes: &[u8]) -> CodecResult<EthernetFrame> {
182 if bytes.len() < ETHERNET_HEADER_LEN {
183 return Err(CodecError::TooShort {
184 layer: "Ethernet header",
185 needed: ETHERNET_HEADER_LEN,
186 got: bytes.len(),
187 });
188 }
189 let mut destination = [0u8; 6];
190 let mut source = [0u8; 6];
191 destination.copy_from_slice(&bytes[0..6]);
192 source.copy_from_slice(&bytes[6..12]);
193 Ok(EthernetFrame {
194 destination,
195 source,
196 ethertype: u16::from_be_bytes([bytes[12], bytes[13]]),
197 payload: bytes[ETHERNET_HEADER_LEN..].to_vec(),
198 })
199}
200
201pub fn build_ethernet_frame(
205 destination: [u8; 6],
206 source: [u8; 6],
207 ethertype: u16,
208 payload: &[u8],
209) -> Vec<u8> {
210 let mut out =
211 Vec::with_capacity(ETHERNET_MIN_FRAME_LEN.max(ETHERNET_HEADER_LEN + payload.len()));
212 out.extend_from_slice(&destination);
213 out.extend_from_slice(&source);
214 out.extend_from_slice(ðertype.to_be_bytes());
215 out.extend_from_slice(payload);
216 if out.len() < ETHERNET_MIN_FRAME_LEN {
217 out.resize(ETHERNET_MIN_FRAME_LEN, 0);
218 }
219 out
220}
221
222pub fn format_mac(mac: &[u8; 6]) -> String {
224 format!(
225 "{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}",
226 mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]
227 )
228}
229
230pub fn parse_mac(s: &str) -> CodecResult<[u8; 6]> {
232 let cleaned: String = s
233 .chars()
234 .filter(|c| !matches!(c, ':' | '-' | '.' | ' '))
235 .collect();
236 if cleaned.len() != 12 || !cleaned.chars().all(|c| c.is_ascii_hexdigit()) {
237 return Err(CodecError::BadMac(s.to_string()));
238 }
239 let mut mac = [0u8; 6];
240 for (i, slot) in mac.iter_mut().enumerate() {
241 *slot = u8::from_str_radix(&cleaned[i * 2..i * 2 + 2], 16)
242 .map_err(|_| CodecError::BadMac(s.to_string()))?;
243 }
244 Ok(mac)
245}
246
247#[derive(Debug, Clone, PartialEq, Eq)]
253pub struct EapolFrame {
254 pub version: u8,
255 pub packet_type: u8,
256 pub body: Vec<u8>,
257}
258
259impl EapolFrame {
260 pub fn decode(bytes: &[u8]) -> CodecResult<Self> {
266 if bytes.len() < EAPOL_HEADER_LEN {
267 return Err(CodecError::TooShort {
268 layer: "EAPOL header",
269 needed: EAPOL_HEADER_LEN,
270 got: bytes.len(),
271 });
272 }
273 let version = bytes[0];
274 if !(EAPOL_VERSION_2001..=EAPOL_VERSION_2010).contains(&version) {
275 return Err(CodecError::UnsupportedVersion(version));
276 }
277 let declared = u16::from_be_bytes([bytes[2], bytes[3]]) as usize;
278 let available = bytes.len() - EAPOL_HEADER_LEN;
279 if declared > available {
280 return Err(CodecError::BadLength {
281 layer: "EAPOL body",
282 declared,
283 available,
284 });
285 }
286 Ok(EapolFrame {
287 version,
288 packet_type: bytes[1],
289 body: bytes[EAPOL_HEADER_LEN..EAPOL_HEADER_LEN + declared].to_vec(),
290 })
291 }
292
293 pub fn encode(&self) -> CodecResult<Vec<u8>> {
303 let declared = u16::try_from(self.body.len()).map_err(|_| CodecError::FieldTooLong {
304 field: "EAPOL body",
305 max: u16::MAX as usize,
306 got: self.body.len(),
307 })?;
308 let mut out = Vec::with_capacity(EAPOL_HEADER_LEN + self.body.len());
309 out.push(self.version);
310 out.push(self.packet_type);
311 out.extend_from_slice(&declared.to_be_bytes());
312 out.extend_from_slice(&self.body);
313 Ok(out)
314 }
315}
316
317pub fn eapol_packet_type_name(packet_type: u8) -> &'static str {
318 match packet_type {
319 EAPOL_TYPE_EAP_PACKET => "EAP-Packet",
320 EAPOL_TYPE_START => "EAPOL-Start",
321 EAPOL_TYPE_LOGOFF => "EAPOL-Logoff",
322 EAPOL_TYPE_KEY => "EAPOL-Key",
323 EAPOL_TYPE_ASF_ALERT => "EAPOL-Encapsulated-ASF-Alert",
324 _ => "Unknown-EAPOL-Type",
325 }
326}
327
328pub fn eapol_wrap_eap(version: u8, eap: &[u8]) -> CodecResult<Vec<u8>> {
333 EapolFrame {
334 version,
335 packet_type: EAPOL_TYPE_EAP_PACKET,
336 body: eap.to_vec(),
337 }
338 .encode()
339}
340
341pub fn eapol_start_frame(version: u8) -> Vec<u8> {
343 vec![version, EAPOL_TYPE_START, 0x00, 0x00]
344}
345
346pub fn eapol_logoff_frame(version: u8) -> Vec<u8> {
348 vec![version, EAPOL_TYPE_LOGOFF, 0x00, 0x00]
349}
350
351#[derive(Debug, Clone, PartialEq, Eq)]
358pub struct EapPacket {
359 pub code: u8,
360 pub identifier: u8,
361 pub eap_type: Option<u8>,
362 pub type_data: Vec<u8>,
363}
364
365impl EapPacket {
366 pub fn decode(bytes: &[u8]) -> CodecResult<Self> {
367 if bytes.len() < EAP_HEADER_LEN {
368 return Err(CodecError::TooShort {
369 layer: "EAP header",
370 needed: EAP_HEADER_LEN,
371 got: bytes.len(),
372 });
373 }
374 let code = bytes[0];
375 let identifier = bytes[1];
376 let declared = u16::from_be_bytes([bytes[2], bytes[3]]) as usize;
377 if declared < EAP_HEADER_LEN {
378 return Err(CodecError::BadLength {
379 layer: "EAP packet",
380 declared,
381 available: bytes.len(),
382 });
383 }
384 if declared > bytes.len() {
385 return Err(CodecError::BadLength {
386 layer: "EAP packet",
387 declared,
388 available: bytes.len(),
389 });
390 }
391 let body = &bytes[..declared];
392
393 match code {
394 EAP_CODE_REQUEST | EAP_CODE_RESPONSE => {
395 if declared < EAP_HEADER_LEN + 1 {
396 return Err(CodecError::MalformedEap(
397 "a Request or Response must carry a type octet (RFC 3748 §4.1)",
398 ));
399 }
400 Ok(EapPacket {
401 code,
402 identifier,
403 eap_type: Some(body[4]),
404 type_data: body[5..].to_vec(),
405 })
406 }
407 EAP_CODE_SUCCESS | EAP_CODE_FAILURE => {
408 if declared != EAP_HEADER_LEN {
409 return Err(CodecError::MalformedEap(
410 "a Success or Failure is exactly four octets (RFC 3748 §4.2)",
411 ));
412 }
413 Ok(EapPacket {
414 code,
415 identifier,
416 eap_type: None,
417 type_data: Vec::new(),
418 })
419 }
420 _ => Err(CodecError::MalformedEap(
421 "EAP code must be 1 (Request), 2 (Response), 3 (Success) or 4 (Failure)",
422 )),
423 }
424 }
425}
426
427pub fn eap_code_name(code: u8) -> &'static str {
428 match code {
429 EAP_CODE_REQUEST => "EAP-Request",
430 EAP_CODE_RESPONSE => "EAP-Response",
431 EAP_CODE_SUCCESS => "EAP-Success",
432 EAP_CODE_FAILURE => "EAP-Failure",
433 _ => "Unknown-EAP-Code",
434 }
435}
436
437pub fn eap_type_name(eap_type: u8) -> &'static str {
438 match eap_type {
439 EAP_TYPE_IDENTITY => "identity",
440 EAP_TYPE_NOTIFICATION => "notification",
441 EAP_TYPE_NAK => "nak",
442 EAP_TYPE_MD5_CHALLENGE => "md5-challenge",
443 EAP_TYPE_TLS => "tls",
444 EAP_TYPE_PEAP => "peap",
445 EAP_TYPE_MSCHAPV2 => "mschapv2",
446 _ => "unknown",
447 }
448}
449
450fn encode_eap_typed(
453 code: u8,
454 identifier: u8,
455 eap_type: u8,
456 type_data: &[u8],
457) -> CodecResult<Vec<u8>> {
458 let length = EAP_HEADER_LEN + 1 + type_data.len();
459 if length > u16::MAX as usize {
460 return Err(CodecError::FieldTooLong {
461 field: "EAP type-data",
462 max: u16::MAX as usize - EAP_HEADER_LEN - 1,
463 got: type_data.len(),
464 });
465 }
466 let mut out = Vec::with_capacity(length);
467 out.push(code);
468 out.push(identifier);
469 out.extend_from_slice(&(length as u16).to_be_bytes());
470 out.push(eap_type);
471 out.extend_from_slice(type_data);
472 Ok(out)
473}
474
475pub fn eap_request_identity(identifier: u8) -> Vec<u8> {
477 vec![EAP_CODE_REQUEST, identifier, 0x00, 0x05, EAP_TYPE_IDENTITY]
479}
480
481pub fn eap_request_notification(identifier: u8, text: &str) -> CodecResult<Vec<u8>> {
483 encode_eap_typed(
484 EAP_CODE_REQUEST,
485 identifier,
486 EAP_TYPE_NOTIFICATION,
487 text.as_bytes(),
488 )
489}
490
491pub fn eap_request_md5_challenge(
494 identifier: u8,
495 challenge: &[u8],
496 name: &str,
497) -> CodecResult<Vec<u8>> {
498 let value = encode_md5_value(challenge, name)?;
499 encode_eap_typed(EAP_CODE_REQUEST, identifier, EAP_TYPE_MD5_CHALLENGE, &value)
500}
501
502pub fn eap_request_tls_start(identifier: u8, eap_type: u8) -> CodecResult<Vec<u8>> {
507 encode_eap_typed(EAP_CODE_REQUEST, identifier, eap_type, &[TLS_FLAG_START])
508}
509
510pub fn eap_response(identifier: u8, eap_type: u8, type_data: &[u8]) -> CodecResult<Vec<u8>> {
512 encode_eap_typed(EAP_CODE_RESPONSE, identifier, eap_type, type_data)
513}
514
515pub fn eap_response_identity(identifier: u8, identity: &str) -> CodecResult<Vec<u8>> {
517 encode_eap_typed(
518 EAP_CODE_RESPONSE,
519 identifier,
520 EAP_TYPE_IDENTITY,
521 identity.as_bytes(),
522 )
523}
524
525pub fn eapol_eap_success_frame(version: u8, identifier: u8) -> Vec<u8> {
540 vec![
541 version,
542 EAPOL_TYPE_EAP_PACKET,
543 0x00,
544 0x04,
545 EAP_CODE_SUCCESS,
546 identifier,
547 0x00,
548 0x04,
549 ]
550}
551
552pub fn eapol_eap_failure_frame(version: u8, identifier: u8) -> Vec<u8> {
559 vec![
560 version,
561 EAPOL_TYPE_EAP_PACKET,
562 0x00,
563 0x04,
564 EAP_CODE_FAILURE,
565 identifier,
566 0x00,
567 0x04,
568 ]
569}
570
571pub fn encode_md5_value(value: &[u8], name: &str) -> CodecResult<Vec<u8>> {
577 if value.len() > u8::MAX as usize {
578 return Err(CodecError::FieldTooLong {
579 field: "MD5-Challenge value",
580 max: u8::MAX as usize,
581 got: value.len(),
582 });
583 }
584 let mut out = Vec::with_capacity(1 + value.len() + name.len());
585 out.push(value.len() as u8);
586 out.extend_from_slice(value);
587 out.extend_from_slice(name.as_bytes());
588 Ok(out)
589}
590
591pub fn decode_md5_value(type_data: &[u8]) -> CodecResult<(Vec<u8>, String)> {
593 if type_data.is_empty() {
594 return Err(CodecError::TooShort {
595 layer: "MD5-Challenge value",
596 needed: 1,
597 got: 0,
598 });
599 }
600 let size = type_data[0] as usize;
601 if type_data.len() < 1 + size {
602 return Err(CodecError::BadLength {
603 layer: "MD5-Challenge value",
604 declared: size,
605 available: type_data.len() - 1,
606 });
607 }
608 Ok((
609 type_data[1..1 + size].to_vec(),
610 String::from_utf8_lossy(&type_data[1 + size..]).into_owned(),
611 ))
612}
613
614pub fn tls_flags(type_data: &[u8]) -> Option<(bool, bool, bool)> {
617 type_data.first().map(|f| {
618 (
619 f & TLS_FLAG_LENGTH_INCLUDED != 0,
620 f & TLS_FLAG_MORE_FRAGMENTS != 0,
621 f & TLS_FLAG_START != 0,
622 )
623 })
624}
625
626pub fn md5(data: &[u8]) -> [u8; 16] {
645 let mut hasher = Md5::new();
646 hasher.update(data);
647 hasher.finalize().into()
648}
649
650pub fn md5_challenge_digest(identifier: u8, secret: &str, challenge: &[u8]) -> [u8; 16] {
655 let mut hasher = Md5::new();
659 hasher.update([identifier]);
660 hasher.update(secret.as_bytes());
661 hasher.update(challenge);
662 hasher.finalize().into()
663}
664
665pub fn md5_response_matches(
671 identifier: u8,
672 secret: &str,
673 challenge: &[u8],
674 response_value: &[u8],
675) -> bool {
676 let expected = md5_challenge_digest(identifier, secret, challenge);
677 if response_value.len() != expected.len() {
678 return false;
679 }
680 let mut diff = 0u8;
681 for (a, b) in expected.iter().zip(response_value.iter()) {
682 diff |= a ^ b;
683 }
684 diff == 0
685}