//****************************************************************************************************** // RadiusPacket.cs - Gbtc // // Copyright © 2012, Grid Protection Alliance. All Rights Reserved. // // Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See // the NOTICE file distributed with this work for additional information regarding copyright ownership. // The GPA licenses this file to you under the MIT License (MIT), the "License"; you may // not use this file except in compliance with the License. You may obtain a copy of the License at: // // http://www.opensource.org/licenses/MIT // // Unless agreed to in writing, the subject software distributed under the License is distributed on an // "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the // License for the specific language governing permissions and limitations. // // Code Modification History: // ---------------------------------------------------------------------------------------------------- // 11/26/2010 - Pinal C. Patel // Generated original version of source code. // 11/23/2011 - J. Ritchie Carroll // Modified to support buffer optimized ISupportBinaryImage. // 12/13/2012 - Starlynn Danyelle Gilliam // Modified Header. // //****************************************************************************************************** using System; using System.Collections.Generic; using System.Linq; using System.Security.Cryptography; using System.Text; using GSF.Parsing; using Random = GSF.Security.Cryptography.Random; #pragma warning disable SCS0006 // Weak hashing function namespace GSF.Communication.Radius { #region [ Enumerations ] /// /// Specifies the type of the . /// public enum PacketType { /// /// Packet sent to a RADIUS server for verification of credentials. /// AccessRequest = 1, /// /// Packet sent by a RADIUS server when credential verification is successful. /// AccessAccept = 2, /// /// Packet sent by a RADIUS server when credential verification is unsuccessful. /// AccessReject = 3, /// /// Not used. No description available. [RFC 2882] /// AccountingRequest = 4, /// /// Not used. No description available. [RFC 2882] /// AccountingResponse = 5, /// /// Not used. No description available. [RFC 2882] /// AccountingStatus = 6, /// /// Not used. No description available. [RFC 2882] /// PasswordRequest = 7, /// /// Not used. No description available. [RFC 2882] /// PasswordAccept = 8, /// /// Not used. No description available. [RFC 2882] /// PasswordReject = 9, /// /// Not used. No description available. [RFC 2882] /// AccountingMessage = 10, /// /// Packet sent by a RADIUS server when further information is needed for credential verification. /// AccessChallenge = 11, /// /// Not used. No description available. [RFC 2882] /// StatuServer = 12, /// /// Not used. No description available. [RFC 2882] /// StatusClient = 13 } #endregion /// /// Represents a data packet transferred between RADIUS client and server. /// /// /// public class RadiusPacket : ISupportBinaryImage { // 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 //+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ //| Code | Identifier | Length | //+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ //| | //| Authenticator | //| | //| | //+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ //| Attributes ... //+-+-+-+-+-+-+-+-+-+-+-+-+- #region [ Members ] // Fields private byte[] m_authenticator; #endregion #region [ Constructors ] /// /// Initializes a new instance of the class. /// public RadiusPacket() { Identifier = (byte)Random.Between(0, 255); m_authenticator = new byte[16]; Attributes = new List(); } /// /// Initializes a new instance of the class. /// /// Type of the . public RadiusPacket(PacketType type) : this() => Type = type; /// /// Initializes a new instance of the class. /// /// Binary image to be used for initializing . /// 0-based starting index of initialization data in the . /// Valid number of bytes in from . public RadiusPacket(byte[] binaryImage, int startIndex, int length) : this() => ParseBinaryImage(binaryImage, startIndex, length); #endregion #region [ Properties ] /// /// Gets or sets the type of the . /// public PacketType Type { get; set; } /// /// Gets or sets the identifier. /// public byte Identifier { get; set; } /// /// Gets or sets the authenticator. /// /// The value being assigned is null. /// The value being assigned is not 16-bytes in length. public byte[] Authenticator { get => m_authenticator; set { if (value == null) throw new ArgumentNullException(nameof(value)); if (value.Length != 16) throw new ArgumentException("Value must 16-byte long."); m_authenticator = value; } } /// /// Gets a list of s. /// public List Attributes { get; } /// /// Gets the length of the . /// // 20 bytes are fixed + length of all attributes combined public int BinaryLength => 20 + Attributes.Where(attribute => attribute != null).Sum(attribute => attribute.BinaryLength); #endregion #region [ Methods ] /// /// Parses object by parsing the specified containing a binary image. /// /// Buffer containing binary image to parse. /// 0-based starting index in the to start parsing. /// Valid number of bytes within from . /// The number of bytes used for initialization in the (i.e., the number of bytes parsed), or 0 if not enough data. /// is null. public int ParseBinaryImage(byte[] buffer, int startIndex, int length) { if (buffer == null) throw new ArgumentNullException(nameof(buffer)); int imageLength = BinaryLength; // Binary image does not have sufficient data. if (length < imageLength) return 0; // Binary image has sufficient data. Type = (PacketType)buffer[startIndex]; Identifier = buffer[startIndex + 1]; ushort size = EndianOrder.ToUInt16(buffer, startIndex + 2); Buffer.BlockCopy(buffer, startIndex + 4, m_authenticator, 0, m_authenticator.Length); // Parse all attributes in the packet. int cursor = 20; while (cursor < size) { RadiusPacketAttribute attribute = new RadiusPacketAttribute(buffer, startIndex + cursor, length); Attributes.Add(attribute); cursor += attribute.BinaryLength; } return imageLength; } /// /// Generates a binary representation of this object and copies it into the given buffer. /// /// Buffer used to hold generated binary image of the source object. /// 0-based starting index in the to start writing. /// The number of bytes written to the . /// is null. /// /// or is less than 0 -or- /// and will exceed length. /// public int GenerateBinaryImage(byte[] buffer, int startIndex) { int length = BinaryLength; buffer.ValidateParameters(startIndex, length); // Populate the buffer buffer[startIndex] = Convert.ToByte(Type); buffer[startIndex + 1] = Identifier; Buffer.BlockCopy(EndianOrder.GetBytes((ushort)BinaryLength), 0, buffer, startIndex + 2, 2); Buffer.BlockCopy(m_authenticator, 0, buffer, startIndex + 4, m_authenticator.Length); startIndex += 20; foreach (RadiusPacketAttribute attribute in Attributes) { if (attribute != null) startIndex += attribute.GenerateBinaryImage(buffer, startIndex); } return length; } /// /// Gets the value of the specified if it is present in the . /// /// of the whose value is to be retrieved. /// . if is present; otherwise null. public byte[] GetAttributeValue(AttributeType attributeType) { RadiusPacketAttribute match = Attributes.Find(attribute => attribute.Type == attributeType); return match?.Value; } #endregion #region [ Static ] // Static Fields /// /// format for encoding text. /// public static Encoding Encoding = Encoding.UTF8; /// /// to use for byte conversion. /// public static EndianOrder EndianOrder = EndianOrder.BigEndian; // Static Methods /// /// Generates an "Authenticator" value used in a RADIUS request packet sent by the client to server. /// /// The shared secret to be used in generating the output. /// A byte array. public static byte[] CreateRequestAuthenticator(string sharedSecret) { // We create a input buffer that'll be used to create a 16-byte value using the RSA MD5 algorithm. // Since the output value (The Authenticator) has to be unique over the life of the shared secret, // we prepend a randomly generated "salt" text to ensure the uniqueness of the output value. byte[] randomBuffer = new byte[16]; byte[] secretBuffer = Encoding.GetBytes(sharedSecret); Random.GetBytes(randomBuffer); using (MD5CryptoServiceProvider md5Provider = new MD5CryptoServiceProvider()) return md5Provider.ComputeHash(randomBuffer.Combine(secretBuffer)); } /// /// Generates an "Authenticator" value used in a RADIUS response packet sent by the server to client. /// /// The shared secret key. /// RADIUS packet sent from client to server. /// RADIUS packet sent from server to client. /// A byte array. public static byte[] CreateResponseAuthenticator(string sharedSecret, RadiusPacket requestPacket, RadiusPacket responsePacket) { // Response authenticator is generated as follows: // MD5(Code + Identifier + Length + Request Authenticator + Attributes + Shared Secret) // where: // Code, Identifier, Length & Attributes are from the response RADIUS packet // Request Authenticator is from the request RADIUS packet // Shared Secret is the shared secret key int length = responsePacket.BinaryLength; byte[] sharedSecretBytes = Encoding.GetBytes(sharedSecret); byte[] buffer = new byte[length + sharedSecretBytes.Length]; responsePacket.GenerateBinaryImage(buffer, 0); Buffer.BlockCopy(requestPacket.BinaryImage(), 4, buffer, 4, 16); Buffer.BlockCopy(sharedSecretBytes, 0, buffer, length, sharedSecretBytes.Length); using (MD5CryptoServiceProvider md5Provider = new MD5CryptoServiceProvider()) return md5Provider.ComputeHash(buffer); } /// /// Generates an encrypted password using the RADIUS protocol specification (RFC 2285). /// /// User's password. /// Shared secret key. /// Request authenticator byte array. /// A byte array. public static byte[] EncryptPassword(string password, string sharedSecret, byte[] requestAuthenticator) { // Avoiding Null Dereferences if (string.IsNullOrEmpty(sharedSecret)) throw new ArgumentException("Shared secret cannot be null or empty."); if (string.IsNullOrEmpty(password)) throw new ArgumentException("Password cannot be null or empty."); if (requestAuthenticator == null) throw new ArgumentException("Request authenticator cannot be null."); // Max length of the password can be 130 according to RFC 2865. Since 128 is the closest multiple // of 16 (password segment length), we allow the password to be no longer than 128 characters. if (password.Length > 128) throw new ArgumentException("Password can be a maximum of 128 characters in length."); byte[] result; byte[] xorBytes = null; byte[] passwordBytes = Encoding.GetBytes(password); byte[] sharedSecretBytes = Encoding.GetBytes(sharedSecret); byte[] md5HashInputBytes = new byte[sharedSecretBytes.Length + 16]; using (MD5CryptoServiceProvider md5Provider = new MD5CryptoServiceProvider()) { if (passwordBytes.Length % 16 == 0) { // Length of password is a multiple of 16. result = new byte[passwordBytes.Length]; } else { // Length of password is not a multiple of 16, so we'll take the multiple of 16 that's next // closest to the password's length and leave the empty space at the end as padding. result = new byte[passwordBytes.Length / 16 * 16 + 16]; } // Copy the password to the result buffer where it'll be XORed. Buffer.BlockCopy(passwordBytes, 0, result, 0, passwordBytes.Length); // For the first 16-byte segment of the password, password characters are to be XORed with the // MD5 hash value that's computed as follows: // MD5(Shared secret key + Request authenticator) Buffer.BlockCopy(sharedSecretBytes, 0, md5HashInputBytes, 0, sharedSecretBytes.Length); Buffer.BlockCopy(requestAuthenticator, 0, md5HashInputBytes, sharedSecretBytes.Length, requestAuthenticator.Length); for (int i = 0; i <= result.Length - 1; i += 16) { // Perform XOR-based encryption of the password in 16-byte segments. if (i > 0 && xorBytes != null) { // For passwords that are more than 16 characters in length, each consecutive 16-byte // segment of the password is XORed with MD5 hash value that's computed as follows: // MD5(Shared secret key + XOR bytes used in the previous segment) // ReSharper disable once PossibleNullReferenceException Buffer.BlockCopy(xorBytes, 0, md5HashInputBytes, sharedSecretBytes.Length, xorBytes.Length); } xorBytes = md5Provider.ComputeHash(md5HashInputBytes); // XOR the password bytes in the current segment with the XOR bytes. for (int j = i; j <= i + 16 - 1; j++) result[j] = (byte)(result[j] ^ xorBytes[j]); } } return result; } #endregion } }