From a86e88cd20a0e326646ec6b6e1356b3eedd32811 Mon Sep 17 00:00:00 2001 From: moznion Date: Wed, 2 Dec 2020 00:18:31 +0900 Subject: [PATCH] Add Tunnel-Password encoder-decoder --- radius/src/avp.rs | 222 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 214 insertions(+), 8 deletions(-) diff --git a/radius/src/avp.rs b/radius/src/avp.rs index 2e60fd7..29711a3 100644 --- a/radius/src/avp.rs +++ b/radius/src/avp.rs @@ -1,6 +1,7 @@ use std::convert::TryInto; use std::net::{Ipv4Addr, Ipv6Addr}; +use crate::tag::Tag; use chrono::{DateTime, TimeZone, Utc}; use thiserror::Error; @@ -16,8 +17,11 @@ pub enum AVPError { InvalidRequestAuthenticatorLength(), #[error("invalid attribute length: {0}")] InvalidAttributeLengthError(usize), + // TODO: more meaningful error message #[error("unexpected decoding error: {0}")] UnexpectedDecodingError(String), + #[error("invalid salt. the MSB has to be 1, but given value isn't: {0}")] + InvalidSaltMSBError(u8), } pub type AVPType = u8; @@ -101,17 +105,11 @@ impl AVP { let mut buff = request_authenticator.to_vec(); - let l = plain_text.len(); - if l < 16 { + if plain_text.is_empty() { let enc = md5::compute([secret, &buff[..]].concat()).to_vec(); return Ok(AVP { typ, - value: enc - .iter() - .zip([plain_text, vec![0 as u8; 16 - l].as_slice()].concat()) - // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ zero padding - .map(|(d, p)| d ^ p) - .collect(), + value: enc.iter().zip(vec![0; 16]).map(|(d, p)| d ^ p).collect(), }); } @@ -142,6 +140,97 @@ impl AVP { } } + pub fn encode_tunnel_password( + typ: AVPType, + plain_text: &[u8], + tag: u8, + salt: &[u8], + secret: &[u8], + request_authenticator: &[u8], + ) -> Result { + /* + * 0 1 2 3 + * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | Type | Length | Tag | Salt + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * Salt (cont) | String ... + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * + * b(1) = MD5(S + R + A) c(1) = p(1) xor b(1) C = c(1) + * b(2) = MD5(S + c(1)) c(2) = p(2) xor b(2) C = C + c(2) + * . . + * . . + * . . + * b(i) = MD5(S + c(i-1)) c(i) = p(i) xor b(i) C = C + c(i) + * + * The resulting encrypted String field will contain + * c(1)+c(2)+...+c(i). + * + * https://tools.ietf.org/html/rfc2868#section-3.5 + */ + + if request_authenticator.len() > 240 { + return Err(AVPError::InvalidAttributeLengthError( + request_authenticator.len(), + )); + } + + if salt.len() != 2 { + return Err(AVPError::InvalidAttributeLengthError(2)); + } + + if salt[0] & 0x80 != 0x80 { + return Err(AVPError::InvalidSaltMSBError(salt[0])); + } + + if secret.is_empty() { + return Err(AVPError::SecretMissingError()); + } + + if request_authenticator.len() != 16 { + return Err(AVPError::InvalidRequestAuthenticatorLength()); + } + + // NOTE: prepend one byte as a tag and two bytes as a salt + // TODO: should it separate them to private struct fields? + let mut enc: Vec = [vec![tag], salt.to_vec()].concat(); + + let mut buff = [request_authenticator, salt].concat(); + if plain_text.is_empty() { + return Ok(AVP { + typ, + value: [ + enc, + md5::compute([secret, &buff[..]].concat()) + .iter() + .zip(vec![0; 16]) + .map(|(d, p)| d ^ p) + .collect::>(), + ] + .concat(), + }); + } + + for chunk in plain_text.chunks(16) { + let mut chunk_vec = chunk.to_vec(); + let l = chunk.len(); + if l < 16 { + chunk_vec.extend(vec![0 as u8; 16 - l]); // zero padding + } + + let enc_block = md5::compute([secret, &buff[..]].concat()).to_vec(); + buff = enc_block + .iter() + .zip(chunk_vec) + .map(|(d, p)| d ^ p) + .collect(); + enc.extend(&buff); + } + + Ok(AVP { typ, value: enc }) + } + pub fn decode_u32(&self) -> Result { const U32_SIZE: usize = std::mem::size_of::(); if self.value.len() != U32_SIZE { @@ -250,6 +339,58 @@ impl AVP { Err(e) => Err(AVPError::UnexpectedDecodingError(e.to_string())), } } + + pub fn decode_tunnel_password( + &self, + secret: &[u8], + request_authenticator: &[u8], + ) -> Result<(Vec, Tag), AVPError> { + if self.value.len() - 3 < 16 + || self.value.len() - 3 > 240 + || (self.value.len() - 3) % 16 != 0 + { + return Err(AVPError::InvalidAttributeLengthError(self.value.len())); + } + + if self.value[1] & 0x80 != 0x80 { + // salt + return Err(AVPError::InvalidSaltMSBError(self.value[1])); + } + + if secret.is_empty() { + return Err(AVPError::SecretMissingError()); + } + + if request_authenticator.len() != 16 { + return Err(AVPError::InvalidRequestAuthenticatorLength()); + } + + let tag = Tag { + value: self.value[0], + }; + let mut dec: Vec = Vec::new(); + let mut buff: Vec = + [request_authenticator.to_vec(), self.value[1..3].to_vec()].concat(); + + for chunk in self.value[3..].chunks(16) { + let chunk_vec = chunk.to_vec(); + let dec_block = md5::compute([secret, &buff[..]].concat()).to_vec(); + dec.extend( + dec_block + .iter() + .zip(&chunk_vec) + .map(|(d, p)| d ^ p) + .collect::>(), + ); + buff = chunk_vec.clone(); + } + + // remove trailing zero bytes + match dec.split(|b| *b == 0).next() { + Some(dec) => Ok((dec.to_vec(), tag)), + None => Ok((vec![], tag)), + } + } } #[cfg(test)] @@ -259,6 +400,7 @@ mod tests { use chrono::Utc; use crate::avp::{AVPError, AVP}; + use crate::tag::Tag; #[test] fn it_should_convert_attribute_to_integer32() -> Result<(), AVPError> { @@ -365,4 +507,68 @@ mod tests { assert_eq!(avp.decode_date()?.timestamp(), now.timestamp(),); Ok(()) } + + #[test] + fn it_should_convert_tunnel_password() -> Result<(), AVPError> { + let salt: Vec = vec![0x80, 0xef]; + let tag = Tag { value: 0x1e }; + let secret = b"12345".to_vec(); + let request_authenticator = b"0123456789abcdef".to_vec(); + + struct TestCase<'a> { + plain_text: &'a str, + expected_encoded_len: usize, + }; + + let test_cases = &[ + TestCase { + plain_text: "", + expected_encoded_len: 16 + 3, + }, + TestCase { + plain_text: "abc", + expected_encoded_len: 16 + 3, + }, + TestCase { + plain_text: "0123456789abcde", + expected_encoded_len: 16 + 3, + }, + TestCase { + plain_text: "0123456789abcdef", + expected_encoded_len: 16 + 3, + }, + TestCase { + plain_text: "0123456789abcdef0", + expected_encoded_len: 32 + 3, + }, + TestCase { + plain_text: "0123456789abcdef0123456789abcdef0123456789abcdef", + expected_encoded_len: 48 + 3, + }, + ]; + + for test_case in test_cases { + let user_password_avp_result = AVP::encode_tunnel_password( + 1, + test_case.plain_text.as_bytes(), + tag.value, + &salt, + &secret, + &request_authenticator, + ); + let avp = user_password_avp_result.unwrap(); + assert_eq!(avp.value.len(), test_case.expected_encoded_len); + + let (decoded_password, got_tag) = avp + .decode_tunnel_password(&secret, &request_authenticator) + .unwrap(); + assert_eq!(got_tag, tag); + assert_eq!( + String::from_utf8(decoded_password).unwrap(), + test_case.plain_text + ); + } + + Ok(()) + } }