1use md5::{Digest, Md5};
28use std::net::Ipv4Addr;
29
30pub const CODE_ACCESS_REQUEST: u8 = 1;
32pub const CODE_ACCESS_ACCEPT: u8 = 2;
33pub const CODE_ACCESS_REJECT: u8 = 3;
34pub const CODE_ACCOUNTING_REQUEST: u8 = 4;
35pub const CODE_ACCOUNTING_RESPONSE: u8 = 5;
36pub const CODE_ACCESS_CHALLENGE: u8 = 11;
37pub const CODE_STATUS_SERVER: u8 = 12;
38
39pub const HEADER_LEN: usize = 20;
41
42pub const MIN_PACKET_LEN: usize = 20;
44pub const MAX_PACKET_LEN: usize = 4096;
45
46pub const MAX_USER_PASSWORD_LEN: usize = 128;
54
55pub const ATTR_USER_NAME: u8 = 1;
58pub const ATTR_USER_PASSWORD: u8 = 2;
59pub const ATTR_CHAP_PASSWORD: u8 = 3;
60pub const ATTR_NAS_IP_ADDRESS: u8 = 4;
61pub const ATTR_NAS_PORT: u8 = 5;
62pub const ATTR_SERVICE_TYPE: u8 = 6;
63pub const ATTR_FRAMED_PROTOCOL: u8 = 7;
64pub const ATTR_FRAMED_IP_ADDRESS: u8 = 8;
65pub const ATTR_FRAMED_IP_NETMASK: u8 = 9;
66pub const ATTR_FILTER_ID: u8 = 11;
67pub const ATTR_FRAMED_MTU: u8 = 12;
68pub const ATTR_REPLY_MESSAGE: u8 = 18;
69pub const ATTR_STATE: u8 = 24;
70pub const ATTR_CLASS: u8 = 25;
71pub const ATTR_SESSION_TIMEOUT: u8 = 27;
72pub const ATTR_IDLE_TIMEOUT: u8 = 28;
73pub const ATTR_TERMINATION_ACTION: u8 = 29;
74pub const ATTR_CALLED_STATION_ID: u8 = 30;
75pub const ATTR_CALLING_STATION_ID: u8 = 31;
76pub const ATTR_NAS_IDENTIFIER: u8 = 32;
77pub const ATTR_PROXY_STATE: u8 = 33;
78pub const ATTR_ACCT_STATUS_TYPE: u8 = 40;
79pub const ATTR_ACCT_DELAY_TIME: u8 = 41;
80pub const ATTR_ACCT_INPUT_OCTETS: u8 = 42;
81pub const ATTR_ACCT_OUTPUT_OCTETS: u8 = 43;
82pub const ATTR_ACCT_SESSION_ID: u8 = 44;
83pub const ATTR_ACCT_AUTHENTIC: u8 = 45;
84pub const ATTR_ACCT_SESSION_TIME: u8 = 46;
85pub const ATTR_ACCT_TERMINATE_CAUSE: u8 = 49;
86pub const ATTR_CHAP_CHALLENGE: u8 = 60;
87pub const ATTR_NAS_PORT_TYPE: u8 = 61;
88pub const ATTR_PORT_LIMIT: u8 = 62;
89pub const ATTR_EAP_MESSAGE: u8 = 79;
90pub const ATTR_MESSAGE_AUTHENTICATOR: u8 = 80;
91
92#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94pub enum AttrKind {
95 Text,
97 Integer,
99 IpAddr,
101 Octets,
103}
104
105pub fn attribute_info(attr_type: u8) -> (&'static str, AttrKind) {
109 match attr_type {
110 ATTR_USER_NAME => ("User-Name", AttrKind::Text),
111 ATTR_USER_PASSWORD => ("User-Password", AttrKind::Octets),
112 ATTR_CHAP_PASSWORD => ("CHAP-Password", AttrKind::Octets),
113 ATTR_NAS_IP_ADDRESS => ("NAS-IP-Address", AttrKind::IpAddr),
114 ATTR_NAS_PORT => ("NAS-Port", AttrKind::Integer),
115 ATTR_SERVICE_TYPE => ("Service-Type", AttrKind::Integer),
116 ATTR_FRAMED_PROTOCOL => ("Framed-Protocol", AttrKind::Integer),
117 ATTR_FRAMED_IP_ADDRESS => ("Framed-IP-Address", AttrKind::IpAddr),
118 ATTR_FRAMED_IP_NETMASK => ("Framed-IP-Netmask", AttrKind::IpAddr),
119 ATTR_FILTER_ID => ("Filter-Id", AttrKind::Text),
120 ATTR_FRAMED_MTU => ("Framed-MTU", AttrKind::Integer),
121 ATTR_REPLY_MESSAGE => ("Reply-Message", AttrKind::Text),
122 ATTR_STATE => ("State", AttrKind::Octets),
123 ATTR_CLASS => ("Class", AttrKind::Octets),
124 ATTR_SESSION_TIMEOUT => ("Session-Timeout", AttrKind::Integer),
125 ATTR_IDLE_TIMEOUT => ("Idle-Timeout", AttrKind::Integer),
126 ATTR_TERMINATION_ACTION => ("Termination-Action", AttrKind::Integer),
127 ATTR_CALLED_STATION_ID => ("Called-Station-Id", AttrKind::Text),
128 ATTR_CALLING_STATION_ID => ("Calling-Station-Id", AttrKind::Text),
129 ATTR_NAS_IDENTIFIER => ("NAS-Identifier", AttrKind::Text),
130 ATTR_PROXY_STATE => ("Proxy-State", AttrKind::Octets),
131 ATTR_ACCT_STATUS_TYPE => ("Acct-Status-Type", AttrKind::Integer),
132 ATTR_ACCT_DELAY_TIME => ("Acct-Delay-Time", AttrKind::Integer),
133 ATTR_ACCT_INPUT_OCTETS => ("Acct-Input-Octets", AttrKind::Integer),
134 ATTR_ACCT_OUTPUT_OCTETS => ("Acct-Output-Octets", AttrKind::Integer),
135 ATTR_ACCT_SESSION_ID => ("Acct-Session-Id", AttrKind::Text),
136 ATTR_ACCT_AUTHENTIC => ("Acct-Authentic", AttrKind::Integer),
137 ATTR_ACCT_SESSION_TIME => ("Acct-Session-Time", AttrKind::Integer),
138 ATTR_ACCT_TERMINATE_CAUSE => ("Acct-Terminate-Cause", AttrKind::Integer),
139 ATTR_CHAP_CHALLENGE => ("CHAP-Challenge", AttrKind::Octets),
140 ATTR_NAS_PORT_TYPE => ("NAS-Port-Type", AttrKind::Integer),
141 ATTR_PORT_LIMIT => ("Port-Limit", AttrKind::Integer),
142 ATTR_EAP_MESSAGE => ("EAP-Message", AttrKind::Octets),
143 ATTR_MESSAGE_AUTHENTICATOR => ("Message-Authenticator", AttrKind::Octets),
144 _ => ("Unknown", AttrKind::Octets),
145 }
146}
147
148pub fn is_authorization_request(code: u8) -> bool {
154 matches!(code, CODE_ACCESS_REQUEST | CODE_STATUS_SERVER)
155}
156
157pub fn code_name(code: u8) -> &'static str {
159 match code {
160 CODE_ACCESS_REQUEST => "Access-Request",
161 CODE_ACCESS_ACCEPT => "Access-Accept",
162 CODE_ACCESS_REJECT => "Access-Reject",
163 CODE_ACCOUNTING_REQUEST => "Accounting-Request",
164 CODE_ACCOUNTING_RESPONSE => "Accounting-Response",
165 CODE_ACCESS_CHALLENGE => "Access-Challenge",
166 CODE_STATUS_SERVER => "Status-Server",
167 _ => "Unknown",
168 }
169}
170
171#[derive(Debug, Clone, PartialEq, Eq)]
173pub struct Attribute {
174 pub attr_type: u8,
175 pub value: Vec<u8>,
176}
177
178impl Attribute {
179 pub fn new(attr_type: u8, value: Vec<u8>) -> Self {
180 Self { attr_type, value }
181 }
182
183 pub fn text(attr_type: u8, value: &str) -> Self {
184 Self::new(attr_type, value.as_bytes().to_vec())
185 }
186
187 pub fn integer(attr_type: u8, value: u32) -> Self {
188 Self::new(attr_type, value.to_be_bytes().to_vec())
189 }
190
191 pub fn ipv4(attr_type: u8, value: Ipv4Addr) -> Self {
192 Self::new(attr_type, value.octets().to_vec())
193 }
194
195 pub fn encode(&self) -> Result<Vec<u8>, RadiusError> {
200 if self.value.len() > 253 {
201 return Err(RadiusError::AttributeTooLong {
202 attr_type: self.attr_type,
203 len: self.value.len(),
204 });
205 }
206 let mut out = Vec::with_capacity(2 + self.value.len());
207 out.push(self.attr_type);
208 out.push((self.value.len() + 2) as u8);
209 out.extend_from_slice(&self.value);
210 Ok(out)
211 }
212}
213
214#[derive(Debug, Clone, PartialEq, Eq)]
216pub struct RadiusPacket {
217 pub code: u8,
218 pub identifier: u8,
219 pub authenticator: [u8; 16],
220 pub attributes: Vec<Attribute>,
221}
222
223#[derive(Debug, Clone, PartialEq, Eq)]
226pub enum RadiusError {
227 TooShort(usize),
228 TooLong(usize),
229 LengthMismatch {
231 declared: usize,
232 actual: usize,
233 },
234 MalformedAttribute {
236 offset: usize,
237 },
238 AttributeTooLong {
239 attr_type: u8,
240 len: usize,
241 },
242 BadPasswordLength(usize),
244 BadAccountingAuthenticator,
246 ResponseTooLong(usize),
248}
249
250impl std::fmt::Display for RadiusError {
251 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
252 match self {
253 RadiusError::TooShort(n) => write!(f, "packet is {} bytes, minimum is 20", n),
254 RadiusError::TooLong(n) => write!(f, "packet is {} bytes, maximum is 4096", n),
255 RadiusError::LengthMismatch { declared, actual } => write!(
256 f,
257 "header declares {} bytes but datagram carries {}",
258 declared, actual
259 ),
260 RadiusError::MalformedAttribute { offset } => {
261 write!(f, "malformed attribute at offset {}", offset)
262 }
263 RadiusError::AttributeTooLong { attr_type, len } => write!(
264 f,
265 "attribute {} value is {} bytes, maximum is 253",
266 attr_type, len
267 ),
268 RadiusError::BadPasswordLength(n) => write!(
269 f,
270 "User-Password is {} bytes; must be 16..=128 and a multiple of 16",
271 n
272 ),
273 RadiusError::BadAccountingAuthenticator => {
274 write!(f, "Accounting-Request Authenticator did not verify")
275 }
276 RadiusError::ResponseTooLong(n) => {
277 write!(f, "response would be {} bytes, maximum is 4096", n)
278 }
279 }
280 }
281}
282
283impl std::error::Error for RadiusError {}
284
285impl RadiusPacket {
286 pub fn decode(data: &[u8]) -> Result<Self, RadiusError> {
292 if data.len() < MIN_PACKET_LEN {
293 return Err(RadiusError::TooShort(data.len()));
294 }
295 if data.len() > MAX_PACKET_LEN {
296 return Err(RadiusError::TooLong(data.len()));
297 }
298
299 let declared = u16::from_be_bytes([data[2], data[3]]) as usize;
300 if declared < MIN_PACKET_LEN || declared > data.len() {
301 return Err(RadiusError::LengthMismatch {
302 declared,
303 actual: data.len(),
304 });
305 }
306
307 let mut authenticator = [0u8; 16];
308 authenticator.copy_from_slice(&data[4..20]);
309
310 let attributes = decode_attributes(&data[HEADER_LEN..declared])?;
311
312 Ok(Self {
313 code: data[0],
314 identifier: data[1],
315 authenticator,
316 attributes,
317 })
318 }
319
320 pub fn first(&self, attr_type: u8) -> Option<&[u8]> {
322 self.attributes
323 .iter()
324 .find(|a| a.attr_type == attr_type)
325 .map(|a| a.value.as_slice())
326 }
327
328 pub fn all(&self, attr_type: u8) -> Vec<&[u8]> {
330 self.attributes
331 .iter()
332 .filter(|a| a.attr_type == attr_type)
333 .map(|a| a.value.as_slice())
334 .collect()
335 }
336
337 pub fn encoded_attributes(&self) -> Result<Vec<u8>, RadiusError> {
339 let mut out = Vec::new();
340 for attr in &self.attributes {
341 out.extend_from_slice(&attr.encode()?);
342 }
343 Ok(out)
344 }
345}
346
347pub fn decode_attributes(mut body: &[u8]) -> Result<Vec<Attribute>, RadiusError> {
349 let total = body.len();
350 let mut attributes = Vec::new();
351 while !body.is_empty() {
352 let offset = total - body.len();
353 if body.len() < 2 {
354 return Err(RadiusError::MalformedAttribute { offset });
355 }
356 let attr_type = body[0];
357 let len = body[1] as usize;
358 if len < 2 || len > body.len() {
359 return Err(RadiusError::MalformedAttribute { offset });
360 }
361 attributes.push(Attribute::new(attr_type, body[2..len].to_vec()));
362 body = &body[len..];
363 }
364 Ok(attributes)
365}
366
367pub fn response_authenticator(
375 code: u8,
376 identifier: u8,
377 attributes: &[u8],
378 request_authenticator: &[u8; 16],
379 secret: &[u8],
380) -> [u8; 16] {
381 let length = (HEADER_LEN + attributes.len()) as u16;
382 let mut hasher = Md5::new();
383 hasher.update([code, identifier]);
384 hasher.update(length.to_be_bytes());
385 hasher.update(request_authenticator);
386 hasher.update(attributes);
387 hasher.update(secret);
388 hasher.finalize().into()
389}
390
391pub fn encode_response(
393 code: u8,
394 identifier: u8,
395 attributes: &[Attribute],
396 request_authenticator: &[u8; 16],
397 secret: &[u8],
398) -> Result<Vec<u8>, RadiusError> {
399 let mut attr_bytes = Vec::new();
400 for attr in attributes {
401 attr_bytes.extend_from_slice(&attr.encode()?);
402 }
403
404 let total = HEADER_LEN + attr_bytes.len();
405 if total > MAX_PACKET_LEN {
406 return Err(RadiusError::ResponseTooLong(total));
407 }
408
409 let auth = response_authenticator(code, identifier, &attr_bytes, request_authenticator, secret);
410
411 let mut out = Vec::with_capacity(total);
412 out.push(code);
413 out.push(identifier);
414 out.extend_from_slice(&(total as u16).to_be_bytes());
415 out.extend_from_slice(&auth);
416 out.extend_from_slice(&attr_bytes);
417 Ok(out)
418}
419
420pub fn hmac_md5(key: &[u8], message: &[u8]) -> [u8; 16] {
422 let mut block = [0u8; 64];
423 if key.len() > 64 {
424 block[..16].copy_from_slice(&Md5::digest(key));
425 } else {
426 block[..key.len()].copy_from_slice(key);
427 }
428 let mut inner = Md5::new();
429 inner.update(block.map(|b| b ^ 0x36));
430 inner.update(message);
431 let inner = inner.finalize();
432 let mut outer = Md5::new();
433 outer.update(block.map(|b| b ^ 0x5c));
434 outer.update(inner);
435 outer.finalize().into()
436}
437
438pub fn encode_signed_response(
444 code: u8,
445 identifier: u8,
446 attributes: &[Attribute],
447 request_authenticator: &[u8; 16],
448 secret: &[u8],
449) -> Result<Vec<u8>, RadiusError> {
450 let mut attr_bytes = Attribute::new(ATTR_MESSAGE_AUTHENTICATOR, vec![0; 16]).encode()?;
451 for attr in attributes
452 .iter()
453 .filter(|a| a.attr_type != ATTR_MESSAGE_AUTHENTICATOR)
454 {
455 attr_bytes.extend_from_slice(&attr.encode()?);
456 }
457 let total = HEADER_LEN + attr_bytes.len();
458 if total > MAX_PACKET_LEN {
459 return Err(RadiusError::ResponseTooLong(total));
460 }
461 let mut out = Vec::with_capacity(total);
462 out.push(code);
463 out.push(identifier);
464 out.extend_from_slice(&(total as u16).to_be_bytes());
465 out.extend_from_slice(request_authenticator);
466 out.extend_from_slice(&attr_bytes);
467 let mac = hmac_md5(secret, &out);
468 out[HEADER_LEN + 2..HEADER_LEN + 18].copy_from_slice(&mac);
469 let auth = response_authenticator(
470 code,
471 identifier,
472 &out[HEADER_LEN..],
473 request_authenticator,
474 secret,
475 );
476 out[4..20].copy_from_slice(&auth);
477 Ok(out)
478}
479
480pub fn verify_request_message_authenticator(raw: &[u8], secret: &[u8]) -> Result<bool, String> {
485 if raw.len() < HEADER_LEN {
486 return Err("shorter than a RADIUS header".into());
487 }
488 let declared = usize::from(u16::from_be_bytes([raw[2], raw[3]])).min(raw.len());
489 let mut offset = HEADER_LEN;
490 let mut found = None;
491 while offset + 2 <= declared {
492 let len = usize::from(raw[offset + 1]);
493 if len < 2 {
494 break;
495 }
496 if raw[offset] == ATTR_MESSAGE_AUTHENTICATOR {
497 if len != 18 {
498 return Err("a Message-Authenticator that is not 16 octets".into());
499 }
500 found = Some(offset);
501 }
502 offset += len;
503 }
504 let Some(at) = found else { return Ok(false) };
505 let mut copy = raw[..declared].to_vec();
506 copy[at + 2..at + 18].fill(0);
507 let mac = hmac_md5(secret, ©);
508 if mac
509 .iter()
510 .zip(&raw[at + 2..at + 18])
511 .fold(0u8, |d, (x, y)| d | (x ^ y))
512 == 0
513 {
514 Ok(true)
515 } else {
516 Err(
517 "the Message-Authenticator does not verify: the sender does not hold the shared secret"
518 .into(),
519 )
520 }
521}
522
523pub fn accounting_request_authenticator(
530 identifier: u8,
531 length: u16,
532 attributes: &[u8],
533 secret: &[u8],
534) -> [u8; 16] {
535 let mut hasher = Md5::new();
536 hasher.update([CODE_ACCOUNTING_REQUEST, identifier]);
537 hasher.update(length.to_be_bytes());
538 hasher.update([0u8; 16]);
539 hasher.update(attributes);
540 hasher.update(secret);
541 hasher.finalize().into()
542}
543
544pub fn verify_accounting_request(packet: &RadiusPacket, secret: &[u8]) -> Result<(), RadiusError> {
548 let attrs = packet.encoded_attributes()?;
549 let length = (HEADER_LEN + attrs.len()) as u16;
550 let expected = accounting_request_authenticator(packet.identifier, length, &attrs, secret);
551 if constant_time_eq(&expected, &packet.authenticator) {
552 Ok(())
553 } else {
554 Err(RadiusError::BadAccountingAuthenticator)
555 }
556}
557
558fn constant_time_eq(a: &[u8; 16], b: &[u8; 16]) -> bool {
560 let mut diff = 0u8;
561 for i in 0..16 {
562 diff |= a[i] ^ b[i];
563 }
564 diff == 0
565}
566
567pub fn decode_user_password(
579 ciphertext: &[u8],
580 request_authenticator: &[u8; 16],
581 secret: &[u8],
582) -> Result<Vec<u8>, RadiusError> {
583 if ciphertext.is_empty()
584 || ciphertext.len() > MAX_USER_PASSWORD_LEN
585 || !ciphertext.len().is_multiple_of(16)
586 {
587 return Err(RadiusError::BadPasswordLength(ciphertext.len()));
588 }
589
590 let mut plaintext = Vec::with_capacity(ciphertext.len());
591 let mut previous: [u8; 16] = *request_authenticator;
592
593 for chunk in ciphertext.chunks(16) {
594 let mut hasher = Md5::new();
595 hasher.update(secret);
596 hasher.update(previous);
597 let b: [u8; 16] = hasher.finalize().into();
598
599 for (i, byte) in chunk.iter().enumerate() {
600 plaintext.push(byte ^ b[i]);
601 }
602 previous.copy_from_slice(chunk);
603 }
604
605 while plaintext.last() == Some(&0) {
607 plaintext.pop();
608 }
609 Ok(plaintext)
610}
611
612pub fn encode_user_password(
620 plaintext: &[u8],
621 request_authenticator: &[u8; 16],
622 secret: &[u8],
623) -> Result<Vec<u8>, RadiusError> {
624 if plaintext.len() > MAX_USER_PASSWORD_LEN {
625 return Err(RadiusError::BadPasswordLength(plaintext.len()));
626 }
627
628 let mut padded = plaintext.to_vec();
629 if padded.is_empty() {
630 padded.resize(16, 0);
631 } else if !padded.len().is_multiple_of(16) {
632 let pad = 16 - (padded.len() % 16);
633 padded.resize(padded.len() + pad, 0);
634 }
635
636 let mut out = Vec::with_capacity(padded.len());
637 let mut previous: [u8; 16] = *request_authenticator;
638
639 for chunk in padded.chunks(16) {
640 let mut hasher = Md5::new();
641 hasher.update(secret);
642 hasher.update(previous);
643 let b: [u8; 16] = hasher.finalize().into();
644
645 let mut cipher = [0u8; 16];
646 for i in 0..16 {
647 cipher[i] = chunk[i] ^ b[i];
648 }
649 out.extend_from_slice(&cipher);
650 previous = cipher;
651 }
652 Ok(out)
653}
654
655pub fn attribute_value_json(attr_type: u8, value: &[u8]) -> serde_json::Value {
658 let (_, kind) = attribute_info(attr_type);
659 match kind {
660 AttrKind::Text => serde_json::Value::String(String::from_utf8_lossy(value).into_owned()),
661 AttrKind::Integer => {
662 if value.len() == 4 {
663 serde_json::Value::from(u32::from_be_bytes([
664 value[0], value[1], value[2], value[3],
665 ]))
666 } else {
667 serde_json::Value::String(hex::encode(value))
668 }
669 }
670 AttrKind::IpAddr => {
671 if value.len() == 4 {
672 serde_json::Value::String(
673 Ipv4Addr::new(value[0], value[1], value[2], value[3]).to_string(),
674 )
675 } else {
676 serde_json::Value::String(hex::encode(value))
677 }
678 }
679 AttrKind::Octets => serde_json::Value::String(hex::encode(value)),
680 }
681}
682
683pub async fn read_frames<R: tokio::io::AsyncRead + Unpin>(
687 mut reader: R,
688 frames: tokio::sync::mpsc::Sender<std::io::Result<Vec<u8>>>,
689) {
690 use tokio::io::AsyncReadExt;
691 loop {
692 let mut header = [0u8; 4];
693 let result = match reader.read_exact(&mut header).await {
694 Ok(_) => {
695 let len = u16::from_be_bytes([header[2], header[3]]) as usize;
696 if !(MIN_PACKET_LEN..=MAX_PACKET_LEN).contains(&len) {
697 Err(std::io::Error::new(
698 std::io::ErrorKind::InvalidData,
699 format!("a packet length of {len} is outside 20..=4096"),
700 ))
701 } else {
702 let mut packet = header.to_vec();
703 packet.resize(len, 0);
704 reader.read_exact(&mut packet[4..]).await.map(|_| packet)
705 }
706 }
707 Err(e) => Err(e),
708 };
709 let done = result.is_err();
710 if frames.send(result).await.is_err() || done {
711 return;
712 }
713 }
714}