use md5::{Digest, Md5};
use std::net::Ipv4Addr;
pub const CODE_ACCESS_REQUEST: u8 = 1;
pub const CODE_ACCESS_ACCEPT: u8 = 2;
pub const CODE_ACCESS_REJECT: u8 = 3;
pub const CODE_ACCOUNTING_REQUEST: u8 = 4;
pub const CODE_ACCOUNTING_RESPONSE: u8 = 5;
pub const CODE_ACCESS_CHALLENGE: u8 = 11;
pub const CODE_STATUS_SERVER: u8 = 12;
pub const HEADER_LEN: usize = 20;
pub const MIN_PACKET_LEN: usize = 20;
pub const MAX_PACKET_LEN: usize = 4096;
pub const MAX_USER_PASSWORD_LEN: usize = 128;
pub const ATTR_USER_NAME: u8 = 1;
pub const ATTR_USER_PASSWORD: u8 = 2;
pub const ATTR_CHAP_PASSWORD: u8 = 3;
pub const ATTR_NAS_IP_ADDRESS: u8 = 4;
pub const ATTR_NAS_PORT: u8 = 5;
pub const ATTR_SERVICE_TYPE: u8 = 6;
pub const ATTR_FRAMED_PROTOCOL: u8 = 7;
pub const ATTR_FRAMED_IP_ADDRESS: u8 = 8;
pub const ATTR_FRAMED_IP_NETMASK: u8 = 9;
pub const ATTR_FILTER_ID: u8 = 11;
pub const ATTR_FRAMED_MTU: u8 = 12;
pub const ATTR_REPLY_MESSAGE: u8 = 18;
pub const ATTR_STATE: u8 = 24;
pub const ATTR_CLASS: u8 = 25;
pub const ATTR_SESSION_TIMEOUT: u8 = 27;
pub const ATTR_IDLE_TIMEOUT: u8 = 28;
pub const ATTR_TERMINATION_ACTION: u8 = 29;
pub const ATTR_CALLED_STATION_ID: u8 = 30;
pub const ATTR_CALLING_STATION_ID: u8 = 31;
pub const ATTR_NAS_IDENTIFIER: u8 = 32;
pub const ATTR_PROXY_STATE: u8 = 33;
pub const ATTR_ACCT_STATUS_TYPE: u8 = 40;
pub const ATTR_ACCT_DELAY_TIME: u8 = 41;
pub const ATTR_ACCT_INPUT_OCTETS: u8 = 42;
pub const ATTR_ACCT_OUTPUT_OCTETS: u8 = 43;
pub const ATTR_ACCT_SESSION_ID: u8 = 44;
pub const ATTR_ACCT_AUTHENTIC: u8 = 45;
pub const ATTR_ACCT_SESSION_TIME: u8 = 46;
pub const ATTR_ACCT_TERMINATE_CAUSE: u8 = 49;
pub const ATTR_CHAP_CHALLENGE: u8 = 60;
pub const ATTR_NAS_PORT_TYPE: u8 = 61;
pub const ATTR_PORT_LIMIT: u8 = 62;
pub const ATTR_EAP_MESSAGE: u8 = 79;
pub const ATTR_MESSAGE_AUTHENTICATOR: u8 = 80;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AttrKind {
Text,
Integer,
IpAddr,
Octets,
}
pub fn attribute_info(attr_type: u8) -> (&'static str, AttrKind) {
match attr_type {
ATTR_USER_NAME => ("User-Name", AttrKind::Text),
ATTR_USER_PASSWORD => ("User-Password", AttrKind::Octets),
ATTR_CHAP_PASSWORD => ("CHAP-Password", AttrKind::Octets),
ATTR_NAS_IP_ADDRESS => ("NAS-IP-Address", AttrKind::IpAddr),
ATTR_NAS_PORT => ("NAS-Port", AttrKind::Integer),
ATTR_SERVICE_TYPE => ("Service-Type", AttrKind::Integer),
ATTR_FRAMED_PROTOCOL => ("Framed-Protocol", AttrKind::Integer),
ATTR_FRAMED_IP_ADDRESS => ("Framed-IP-Address", AttrKind::IpAddr),
ATTR_FRAMED_IP_NETMASK => ("Framed-IP-Netmask", AttrKind::IpAddr),
ATTR_FILTER_ID => ("Filter-Id", AttrKind::Text),
ATTR_FRAMED_MTU => ("Framed-MTU", AttrKind::Integer),
ATTR_REPLY_MESSAGE => ("Reply-Message", AttrKind::Text),
ATTR_STATE => ("State", AttrKind::Octets),
ATTR_CLASS => ("Class", AttrKind::Octets),
ATTR_SESSION_TIMEOUT => ("Session-Timeout", AttrKind::Integer),
ATTR_IDLE_TIMEOUT => ("Idle-Timeout", AttrKind::Integer),
ATTR_TERMINATION_ACTION => ("Termination-Action", AttrKind::Integer),
ATTR_CALLED_STATION_ID => ("Called-Station-Id", AttrKind::Text),
ATTR_CALLING_STATION_ID => ("Calling-Station-Id", AttrKind::Text),
ATTR_NAS_IDENTIFIER => ("NAS-Identifier", AttrKind::Text),
ATTR_PROXY_STATE => ("Proxy-State", AttrKind::Octets),
ATTR_ACCT_STATUS_TYPE => ("Acct-Status-Type", AttrKind::Integer),
ATTR_ACCT_DELAY_TIME => ("Acct-Delay-Time", AttrKind::Integer),
ATTR_ACCT_INPUT_OCTETS => ("Acct-Input-Octets", AttrKind::Integer),
ATTR_ACCT_OUTPUT_OCTETS => ("Acct-Output-Octets", AttrKind::Integer),
ATTR_ACCT_SESSION_ID => ("Acct-Session-Id", AttrKind::Text),
ATTR_ACCT_AUTHENTIC => ("Acct-Authentic", AttrKind::Integer),
ATTR_ACCT_SESSION_TIME => ("Acct-Session-Time", AttrKind::Integer),
ATTR_ACCT_TERMINATE_CAUSE => ("Acct-Terminate-Cause", AttrKind::Integer),
ATTR_CHAP_CHALLENGE => ("CHAP-Challenge", AttrKind::Octets),
ATTR_NAS_PORT_TYPE => ("NAS-Port-Type", AttrKind::Integer),
ATTR_PORT_LIMIT => ("Port-Limit", AttrKind::Integer),
ATTR_EAP_MESSAGE => ("EAP-Message", AttrKind::Octets),
ATTR_MESSAGE_AUTHENTICATOR => ("Message-Authenticator", AttrKind::Octets),
_ => ("Unknown", AttrKind::Octets),
}
}
pub fn is_authorization_request(code: u8) -> bool {
matches!(code, CODE_ACCESS_REQUEST | CODE_STATUS_SERVER)
}
pub fn code_name(code: u8) -> &'static str {
match code {
CODE_ACCESS_REQUEST => "Access-Request",
CODE_ACCESS_ACCEPT => "Access-Accept",
CODE_ACCESS_REJECT => "Access-Reject",
CODE_ACCOUNTING_REQUEST => "Accounting-Request",
CODE_ACCOUNTING_RESPONSE => "Accounting-Response",
CODE_ACCESS_CHALLENGE => "Access-Challenge",
CODE_STATUS_SERVER => "Status-Server",
_ => "Unknown",
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Attribute {
pub attr_type: u8,
pub value: Vec<u8>,
}
impl Attribute {
pub fn new(attr_type: u8, value: Vec<u8>) -> Self {
Self { attr_type, value }
}
pub fn text(attr_type: u8, value: &str) -> Self {
Self::new(attr_type, value.as_bytes().to_vec())
}
pub fn integer(attr_type: u8, value: u32) -> Self {
Self::new(attr_type, value.to_be_bytes().to_vec())
}
pub fn ipv4(attr_type: u8, value: Ipv4Addr) -> Self {
Self::new(attr_type, value.octets().to_vec())
}
pub fn encode(&self) -> Result<Vec<u8>, RadiusError> {
if self.value.len() > 253 {
return Err(RadiusError::AttributeTooLong {
attr_type: self.attr_type,
len: self.value.len(),
});
}
let mut out = Vec::with_capacity(2 + self.value.len());
out.push(self.attr_type);
out.push((self.value.len() + 2) as u8);
out.extend_from_slice(&self.value);
Ok(out)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RadiusPacket {
pub code: u8,
pub identifier: u8,
pub authenticator: [u8; 16],
pub attributes: Vec<Attribute>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RadiusError {
TooShort(usize),
TooLong(usize),
LengthMismatch {
declared: usize,
actual: usize,
},
MalformedAttribute {
offset: usize,
},
AttributeTooLong {
attr_type: u8,
len: usize,
},
BadPasswordLength(usize),
BadAccountingAuthenticator,
ResponseTooLong(usize),
}
impl std::fmt::Display for RadiusError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
RadiusError::TooShort(n) => write!(f, "packet is {} bytes, minimum is 20", n),
RadiusError::TooLong(n) => write!(f, "packet is {} bytes, maximum is 4096", n),
RadiusError::LengthMismatch { declared, actual } => write!(
f,
"header declares {} bytes but datagram carries {}",
declared, actual
),
RadiusError::MalformedAttribute { offset } => {
write!(f, "malformed attribute at offset {}", offset)
}
RadiusError::AttributeTooLong { attr_type, len } => write!(
f,
"attribute {} value is {} bytes, maximum is 253",
attr_type, len
),
RadiusError::BadPasswordLength(n) => write!(
f,
"User-Password is {} bytes; must be 16..=128 and a multiple of 16",
n
),
RadiusError::BadAccountingAuthenticator => {
write!(f, "Accounting-Request Authenticator did not verify")
}
RadiusError::ResponseTooLong(n) => {
write!(f, "response would be {} bytes, maximum is 4096", n)
}
}
}
}
impl std::error::Error for RadiusError {}
impl RadiusPacket {
pub fn decode(data: &[u8]) -> Result<Self, RadiusError> {
if data.len() < MIN_PACKET_LEN {
return Err(RadiusError::TooShort(data.len()));
}
if data.len() > MAX_PACKET_LEN {
return Err(RadiusError::TooLong(data.len()));
}
let declared = u16::from_be_bytes([data[2], data[3]]) as usize;
if declared < MIN_PACKET_LEN || declared > data.len() {
return Err(RadiusError::LengthMismatch {
declared,
actual: data.len(),
});
}
let mut authenticator = [0u8; 16];
authenticator.copy_from_slice(&data[4..20]);
let attributes = decode_attributes(&data[HEADER_LEN..declared])?;
Ok(Self {
code: data[0],
identifier: data[1],
authenticator,
attributes,
})
}
pub fn first(&self, attr_type: u8) -> Option<&[u8]> {
self.attributes
.iter()
.find(|a| a.attr_type == attr_type)
.map(|a| a.value.as_slice())
}
pub fn all(&self, attr_type: u8) -> Vec<&[u8]> {
self.attributes
.iter()
.filter(|a| a.attr_type == attr_type)
.map(|a| a.value.as_slice())
.collect()
}
pub fn encoded_attributes(&self) -> Result<Vec<u8>, RadiusError> {
let mut out = Vec::new();
for attr in &self.attributes {
out.extend_from_slice(&attr.encode()?);
}
Ok(out)
}
}
pub fn decode_attributes(mut body: &[u8]) -> Result<Vec<Attribute>, RadiusError> {
let total = body.len();
let mut attributes = Vec::new();
while !body.is_empty() {
let offset = total - body.len();
if body.len() < 2 {
return Err(RadiusError::MalformedAttribute { offset });
}
let attr_type = body[0];
let len = body[1] as usize;
if len < 2 || len > body.len() {
return Err(RadiusError::MalformedAttribute { offset });
}
attributes.push(Attribute::new(attr_type, body[2..len].to_vec()));
body = &body[len..];
}
Ok(attributes)
}
pub fn response_authenticator(
code: u8,
identifier: u8,
attributes: &[u8],
request_authenticator: &[u8; 16],
secret: &[u8],
) -> [u8; 16] {
let length = (HEADER_LEN + attributes.len()) as u16;
let mut hasher = Md5::new();
hasher.update([code, identifier]);
hasher.update(length.to_be_bytes());
hasher.update(request_authenticator);
hasher.update(attributes);
hasher.update(secret);
hasher.finalize().into()
}
pub fn encode_response(
code: u8,
identifier: u8,
attributes: &[Attribute],
request_authenticator: &[u8; 16],
secret: &[u8],
) -> Result<Vec<u8>, RadiusError> {
let mut attr_bytes = Vec::new();
for attr in attributes {
attr_bytes.extend_from_slice(&attr.encode()?);
}
let total = HEADER_LEN + attr_bytes.len();
if total > MAX_PACKET_LEN {
return Err(RadiusError::ResponseTooLong(total));
}
let auth = response_authenticator(code, identifier, &attr_bytes, request_authenticator, secret);
let mut out = Vec::with_capacity(total);
out.push(code);
out.push(identifier);
out.extend_from_slice(&(total as u16).to_be_bytes());
out.extend_from_slice(&auth);
out.extend_from_slice(&attr_bytes);
Ok(out)
}
pub fn hmac_md5(key: &[u8], message: &[u8]) -> [u8; 16] {
let mut block = [0u8; 64];
if key.len() > 64 {
block[..16].copy_from_slice(&Md5::digest(key));
} else {
block[..key.len()].copy_from_slice(key);
}
let mut inner = Md5::new();
inner.update(block.map(|b| b ^ 0x36));
inner.update(message);
let inner = inner.finalize();
let mut outer = Md5::new();
outer.update(block.map(|b| b ^ 0x5c));
outer.update(inner);
outer.finalize().into()
}
pub fn encode_signed_response(
code: u8,
identifier: u8,
attributes: &[Attribute],
request_authenticator: &[u8; 16],
secret: &[u8],
) -> Result<Vec<u8>, RadiusError> {
let mut attr_bytes = Attribute::new(ATTR_MESSAGE_AUTHENTICATOR, vec![0; 16]).encode()?;
for attr in attributes
.iter()
.filter(|a| a.attr_type != ATTR_MESSAGE_AUTHENTICATOR)
{
attr_bytes.extend_from_slice(&attr.encode()?);
}
let total = HEADER_LEN + attr_bytes.len();
if total > MAX_PACKET_LEN {
return Err(RadiusError::ResponseTooLong(total));
}
let mut out = Vec::with_capacity(total);
out.push(code);
out.push(identifier);
out.extend_from_slice(&(total as u16).to_be_bytes());
out.extend_from_slice(request_authenticator);
out.extend_from_slice(&attr_bytes);
let mac = hmac_md5(secret, &out);
out[HEADER_LEN + 2..HEADER_LEN + 18].copy_from_slice(&mac);
let auth = response_authenticator(
code,
identifier,
&out[HEADER_LEN..],
request_authenticator,
secret,
);
out[4..20].copy_from_slice(&auth);
Ok(out)
}
pub fn verify_request_message_authenticator(raw: &[u8], secret: &[u8]) -> Result<bool, String> {
if raw.len() < HEADER_LEN {
return Err("shorter than a RADIUS header".into());
}
let declared = usize::from(u16::from_be_bytes([raw[2], raw[3]])).min(raw.len());
let mut offset = HEADER_LEN;
let mut found = None;
while offset + 2 <= declared {
let len = usize::from(raw[offset + 1]);
if len < 2 {
break;
}
if raw[offset] == ATTR_MESSAGE_AUTHENTICATOR {
if len != 18 {
return Err("a Message-Authenticator that is not 16 octets".into());
}
found = Some(offset);
}
offset += len;
}
let Some(at) = found else { return Ok(false) };
let mut copy = raw[..declared].to_vec();
copy[at + 2..at + 18].fill(0);
let mac = hmac_md5(secret, ©);
if mac
.iter()
.zip(&raw[at + 2..at + 18])
.fold(0u8, |d, (x, y)| d | (x ^ y))
== 0
{
Ok(true)
} else {
Err(
"the Message-Authenticator does not verify: the sender does not hold the shared secret"
.into(),
)
}
}
pub fn accounting_request_authenticator(
identifier: u8,
length: u16,
attributes: &[u8],
secret: &[u8],
) -> [u8; 16] {
let mut hasher = Md5::new();
hasher.update([CODE_ACCOUNTING_REQUEST, identifier]);
hasher.update(length.to_be_bytes());
hasher.update([0u8; 16]);
hasher.update(attributes);
hasher.update(secret);
hasher.finalize().into()
}
pub fn verify_accounting_request(packet: &RadiusPacket, secret: &[u8]) -> Result<(), RadiusError> {
let attrs = packet.encoded_attributes()?;
let length = (HEADER_LEN + attrs.len()) as u16;
let expected = accounting_request_authenticator(packet.identifier, length, &attrs, secret);
if constant_time_eq(&expected, &packet.authenticator) {
Ok(())
} else {
Err(RadiusError::BadAccountingAuthenticator)
}
}
fn constant_time_eq(a: &[u8; 16], b: &[u8; 16]) -> bool {
let mut diff = 0u8;
for i in 0..16 {
diff |= a[i] ^ b[i];
}
diff == 0
}
pub fn decode_user_password(
ciphertext: &[u8],
request_authenticator: &[u8; 16],
secret: &[u8],
) -> Result<Vec<u8>, RadiusError> {
if ciphertext.is_empty()
|| ciphertext.len() > MAX_USER_PASSWORD_LEN
|| !ciphertext.len().is_multiple_of(16)
{
return Err(RadiusError::BadPasswordLength(ciphertext.len()));
}
let mut plaintext = Vec::with_capacity(ciphertext.len());
let mut previous: [u8; 16] = *request_authenticator;
for chunk in ciphertext.chunks(16) {
let mut hasher = Md5::new();
hasher.update(secret);
hasher.update(previous);
let b: [u8; 16] = hasher.finalize().into();
for (i, byte) in chunk.iter().enumerate() {
plaintext.push(byte ^ b[i]);
}
previous.copy_from_slice(chunk);
}
while plaintext.last() == Some(&0) {
plaintext.pop();
}
Ok(plaintext)
}
pub fn encode_user_password(
plaintext: &[u8],
request_authenticator: &[u8; 16],
secret: &[u8],
) -> Result<Vec<u8>, RadiusError> {
if plaintext.len() > MAX_USER_PASSWORD_LEN {
return Err(RadiusError::BadPasswordLength(plaintext.len()));
}
let mut padded = plaintext.to_vec();
if padded.is_empty() {
padded.resize(16, 0);
} else if !padded.len().is_multiple_of(16) {
let pad = 16 - (padded.len() % 16);
padded.resize(padded.len() + pad, 0);
}
let mut out = Vec::with_capacity(padded.len());
let mut previous: [u8; 16] = *request_authenticator;
for chunk in padded.chunks(16) {
let mut hasher = Md5::new();
hasher.update(secret);
hasher.update(previous);
let b: [u8; 16] = hasher.finalize().into();
let mut cipher = [0u8; 16];
for i in 0..16 {
cipher[i] = chunk[i] ^ b[i];
}
out.extend_from_slice(&cipher);
previous = cipher;
}
Ok(out)
}
pub fn attribute_value_json(attr_type: u8, value: &[u8]) -> serde_json::Value {
let (_, kind) = attribute_info(attr_type);
match kind {
AttrKind::Text => serde_json::Value::String(String::from_utf8_lossy(value).into_owned()),
AttrKind::Integer => {
if value.len() == 4 {
serde_json::Value::from(u32::from_be_bytes([
value[0], value[1], value[2], value[3],
]))
} else {
serde_json::Value::String(hex::encode(value))
}
}
AttrKind::IpAddr => {
if value.len() == 4 {
serde_json::Value::String(
Ipv4Addr::new(value[0], value[1], value[2], value[3]).to_string(),
)
} else {
serde_json::Value::String(hex::encode(value))
}
}
AttrKind::Octets => serde_json::Value::String(hex::encode(value)),
}
}
pub async fn read_frames<R: tokio::io::AsyncRead + Unpin>(
mut reader: R,
frames: tokio::sync::mpsc::Sender<std::io::Result<Vec<u8>>>,
) {
use tokio::io::AsyncReadExt;
loop {
let mut header = [0u8; 4];
let result = match reader.read_exact(&mut header).await {
Ok(_) => {
let len = u16::from_be_bytes([header[2], header[3]]) as usize;
if !(MIN_PACKET_LEN..=MAX_PACKET_LEN).contains(&len) {
Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("a packet length of {len} is outside 20..=4096"),
))
} else {
let mut packet = header.to_vec();
packet.resize(len, 0);
reader.read_exact(&mut packet[4..]).await.map(|_| packet)
}
}
Err(e) => Err(e),
};
let done = result.is_err();
if frames.send(result).await.is_err() || done {
return;
}
}
}