blob: 0553b9131804f9068a7e88b6fe4f4f61858c111a [file] [log] [blame]
Ari Saha79d7c252015-06-26 10:31:48 -07001/*
2 *
3 * * Copyright 2015 AT&T Foundry
4 * *
5 * * Licensed under the Apache License, Version 2.0 (the "License");
6 * * you may not use this file except in compliance with the License.
7 * * You may obtain a copy of the License at
8 * *
9 * * http://www.apache.org/licenses/LICENSE-2.0
10 * *
11 * * Unless required by applicable law or agreed to in writing, software
12 * * distributed under the License is distributed on an "AS IS" BASIS,
13 * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * * See the License for the specific language governing permissions and
15 * * limitations under the License.
16 *
17 */
18
Jonathan Hart4a60bb32015-06-30 15:31:20 -070019package org.onlab.packet;
Ari Saha79d7c252015-06-26 10:31:48 -070020
Ari Saha79d7c252015-06-26 10:31:48 -070021import org.slf4j.Logger;
22
23import javax.crypto.Mac;
24import javax.crypto.spec.SecretKeySpec;
25import java.io.ByteArrayOutputStream;
26import java.io.IOException;
27import java.nio.ByteBuffer;
28import java.security.SecureRandom;
29import java.util.ArrayList;
30import java.util.Arrays;
Jonathan Hart4a60bb32015-06-30 15:31:20 -070031import java.util.List;
Ari Saha79d7c252015-06-26 10:31:48 -070032
Jian Li5fc14292015-12-04 11:30:46 -080033import static com.google.common.base.MoreObjects.toStringHelper;
Jonathan Hart4a60bb32015-06-30 15:31:20 -070034import static org.onlab.packet.PacketUtils.checkHeaderLength;
35import static org.onlab.packet.PacketUtils.checkInput;
Ari Saha79d7c252015-06-26 10:31:48 -070036import static org.slf4j.LoggerFactory.getLogger;
37
38/**
Jonathan Hart4a60bb32015-06-30 15:31:20 -070039 * RADIUS packet.
Ari Saha79d7c252015-06-26 10:31:48 -070040 */
41public class RADIUS extends BasePacket {
42 protected byte code;
43 protected byte identifier;
44 protected short length = RADIUS_MIN_LENGTH;
45 protected byte[] authenticator = new byte[16];
Jonathan Hart4a60bb32015-06-30 15:31:20 -070046 protected List<RADIUSAttribute> attributes = new ArrayList<>();
Ari Saha79d7c252015-06-26 10:31:48 -070047
Jonathan Hart4a60bb32015-06-30 15:31:20 -070048 // RADIUS parameters
Ari Saha79d7c252015-06-26 10:31:48 -070049 public static final short RADIUS_MIN_LENGTH = 20;
50 public static final short MAX_ATTR_VALUE_LENGTH = 253;
51 public static final short RADIUS_MAX_LENGTH = 4096;
52
Jonathan Hart4a60bb32015-06-30 15:31:20 -070053 // RADIUS packet types
Ari Saha79d7c252015-06-26 10:31:48 -070054 public static final byte RADIUS_CODE_ACCESS_REQUEST = 0x01;
55 public static final byte RADIUS_CODE_ACCESS_ACCEPT = 0x02;
56 public static final byte RADIUS_CODE_ACCESS_REJECT = 0x03;
57 public static final byte RADIUS_CODE_ACCOUNTING_REQUEST = 0x04;
58 public static final byte RADIUS_CODE_ACCOUNTING_RESPONSE = 0x05;
59 public static final byte RADIUS_CODE_ACCESS_CHALLENGE = 0x0b;
60
61 private final Logger log = getLogger(getClass());
62
Jonathan Hart4a60bb32015-06-30 15:31:20 -070063 /**
64 * Default constructor.
65 */
Ari Saha79d7c252015-06-26 10:31:48 -070066 public RADIUS() {
67 }
68
Jonathan Hart4a60bb32015-06-30 15:31:20 -070069 /**
70 * Constructs a RADIUS packet with the given code and identifier.
71 *
Jian Li5fc14292015-12-04 11:30:46 -080072 * @param code code
Jonathan Hart4a60bb32015-06-30 15:31:20 -070073 * @param identifier identifier
74 */
Ari Saha79d7c252015-06-26 10:31:48 -070075 public RADIUS(byte code, byte identifier) {
76 this.code = code;
77 this.identifier = identifier;
78 }
79
Jonathan Hart4a60bb32015-06-30 15:31:20 -070080 /**
81 * Gets the code.
82 *
83 * @return code
84 */
Ari Saha79d7c252015-06-26 10:31:48 -070085 public byte getCode() {
86 return this.code;
87 }
88
Jonathan Hart4a60bb32015-06-30 15:31:20 -070089 /**
90 * Sets the code.
91 *
92 * @param code code
93 */
Ari Saha79d7c252015-06-26 10:31:48 -070094 public void setCode(byte code) {
95 this.code = code;
96 }
97
Jonathan Hart4a60bb32015-06-30 15:31:20 -070098 /**
99 * Gets the identifier.
100 *
101 * @return identifier
102 */
Ari Saha79d7c252015-06-26 10:31:48 -0700103 public byte getIdentifier() {
104 return this.identifier;
105 }
106
Jonathan Hart4a60bb32015-06-30 15:31:20 -0700107 /**
Matteo Scandolo4f4c5342020-10-23 10:17:00 -0700108 * Get the identifier in a readable format.
109 *
110 * @return identifier
111 */
112 public int getReadableIdentifier() {
113 return this.identifier & 0xff;
114 }
115
116 /**
Jonathan Hart4a60bb32015-06-30 15:31:20 -0700117 * Sets the identifier.
118 *
119 * @param identifier identifier
120 */
Ari Saha79d7c252015-06-26 10:31:48 -0700121 public void setIdentifier(byte identifier) {
122 this.identifier = identifier;
123 }
124
Jonathan Hart4a60bb32015-06-30 15:31:20 -0700125 /**
126 * Gets the authenticator.
127 *
128 * @return authenticator
129 */
Ari Saha79d7c252015-06-26 10:31:48 -0700130 public byte[] getAuthenticator() {
131 return this.authenticator;
132 }
133
Jonathan Hart4a60bb32015-06-30 15:31:20 -0700134 /**
135 * Sets the authenticator.
136 *
137 * @param authenticator authenticator
138 */
139 public void setAuthenticator(byte[] authenticator) {
140 this.authenticator = authenticator;
Ari Saha79d7c252015-06-26 10:31:48 -0700141 }
142
Jonathan Hart4a60bb32015-06-30 15:31:20 -0700143 /**
144 * Generates an authenticator code.
145 *
146 * @return the authenticator
147 */
Ari Saha79d7c252015-06-26 10:31:48 -0700148 public byte[] generateAuthCode() {
149 new SecureRandom().nextBytes(this.authenticator);
150 return this.authenticator;
151 }
152
Jonathan Hart4a60bb32015-06-30 15:31:20 -0700153 /**
154 * Checks if the packet's code field is valid.
155 *
156 * @return whether the code is valid
157 */
Ari Saha79d7c252015-06-26 10:31:48 -0700158 public boolean isValidCode() {
159 return this.code == RADIUS_CODE_ACCESS_REQUEST ||
160 this.code == RADIUS_CODE_ACCESS_ACCEPT ||
161 this.code == RADIUS_CODE_ACCESS_REJECT ||
162 this.code == RADIUS_CODE_ACCOUNTING_REQUEST ||
163 this.code == RADIUS_CODE_ACCOUNTING_RESPONSE ||
164 this.code == RADIUS_CODE_ACCESS_CHALLENGE;
165 }
166
Jonathan Hart4a60bb32015-06-30 15:31:20 -0700167 /**
168 * Adds a message authenticator to the packet based on the given key.
169 *
170 * @param key key to generate message authenticator
171 * @return the messgae authenticator RADIUS attribute
172 */
Ari Saha79d7c252015-06-26 10:31:48 -0700173 public RADIUSAttribute addMessageAuthenticator(String key) {
Jonathan Hart4a60bb32015-06-30 15:31:20 -0700174 // Message-Authenticator = HMAC-MD5 (Type, Identifier, Length,
175 // Request Authenticator, Attributes)
176 // When the message integrity check is calculated the signature string
177 // should be considered to be sixteen octets of zero.
Ari Saha79d7c252015-06-26 10:31:48 -0700178 byte[] hashOutput = new byte[16];
179 Arrays.fill(hashOutput, (byte) 0);
180
181 RADIUSAttribute authAttribute = this.getAttribute(RADIUSAttribute.RADIUS_ATTR_MESSAGE_AUTH);
182 if (authAttribute != null) {
183 // If Message-Authenticator was already present, override it
184 this.log.warn("Attempted to add duplicate Message-Authenticator");
185 authAttribute = this.updateAttribute(RADIUSAttribute.RADIUS_ATTR_MESSAGE_AUTH, hashOutput);
186 } else {
187 // Else generate a new attribute padded with zeroes
188 authAttribute = this.setAttribute(RADIUSAttribute.RADIUS_ATTR_MESSAGE_AUTH, hashOutput);
189 }
190 // Calculate the MD5 HMAC based on the message
191 try {
192 SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(), "HmacMD5");
193 Mac mac = Mac.getInstance("HmacMD5");
194 mac.init(keySpec);
195 hashOutput = mac.doFinal(this.serialize());
196 // Update HMAC in Message-Authenticator
197 authAttribute = this.updateAttribute(RADIUSAttribute.RADIUS_ATTR_MESSAGE_AUTH, hashOutput);
198 } catch (Exception e) {
199 this.log.error("Failed to generate message authenticator: {}", e.getMessage());
200 }
201
202 return authAttribute;
203 }
204
Jonathan Hart4a60bb32015-06-30 15:31:20 -0700205 /**
206 * Checks the message authenticator in the packet with one generated from
207 * the given key.
208 *
209 * @param key key to generate message authenticator
210 * @return whether the message authenticators match or not
211 */
Ari Saha79d7c252015-06-26 10:31:48 -0700212 public boolean checkMessageAuthenticator(String key) {
213 byte[] newHash = new byte[16];
214 Arrays.fill(newHash, (byte) 0);
215 byte[] messageAuthenticator = this.getAttribute(RADIUSAttribute.RADIUS_ATTR_MESSAGE_AUTH).getValue();
216 this.updateAttribute(RADIUSAttribute.RADIUS_ATTR_MESSAGE_AUTH, newHash);
217 // Calculate the MD5 HMAC based on the message
218 try {
219 SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(), "HmacMD5");
220 Mac mac = Mac.getInstance("HmacMD5");
221 mac.init(keySpec);
222 newHash = mac.doFinal(this.serialize());
223 } catch (Exception e) {
224 log.error("Failed to generate message authenticator: {}", e.getMessage());
225 }
226 this.updateAttribute(RADIUSAttribute.RADIUS_ATTR_MESSAGE_AUTH, messageAuthenticator);
227 // Compare the calculated Message-Authenticator with the one in the message
228 return Arrays.equals(newHash, messageAuthenticator);
229 }
230
231 /**
Jonathan Hart4a60bb32015-06-30 15:31:20 -0700232 * Encapsulates an EAP packet in this RADIUS packet.
233 *
234 * @param message EAP message object to be embedded in the RADIUS
235 * EAP-Message attributed
Ari Saha79d7c252015-06-26 10:31:48 -0700236 */
237 public void encapsulateMessage(EAP message) {
238 if (message.length <= MAX_ATTR_VALUE_LENGTH) {
239 // Use the regular serialization method as it fits into one EAP-Message attribute
240 this.setAttribute(RADIUSAttribute.RADIUS_ATTR_EAP_MESSAGE,
241 message.serialize());
242 } else {
243 // Segment the message into chucks and embed them in several EAP-Message attributes
244 short remainingLength = message.length;
245 byte[] messageBuffer = message.serialize();
246 final ByteBuffer bb = ByteBuffer.wrap(messageBuffer);
247 while (bb.hasRemaining()) {
248 byte[] messageAttributeData;
249 if (remainingLength > MAX_ATTR_VALUE_LENGTH) {
250 // The remaining data is still too long to fit into one attribute, keep going
251 messageAttributeData = new byte[MAX_ATTR_VALUE_LENGTH];
252 bb.get(messageAttributeData, 0, MAX_ATTR_VALUE_LENGTH);
253 remainingLength -= MAX_ATTR_VALUE_LENGTH;
254 } else {
255 // The remaining data fits, this will be the last chunk
256 messageAttributeData = new byte[remainingLength];
257 bb.get(messageAttributeData, 0, remainingLength);
258 }
259 this.attributes.add(new RADIUSAttribute(RADIUSAttribute.RADIUS_ATTR_EAP_MESSAGE,
260 (byte) (messageAttributeData.length + 2), messageAttributeData));
261
262 // Adding the size of the data to the total RADIUS length
263 this.length += (short) (messageAttributeData.length & 0xFF);
264 // Adding the size of the overhead attribute type and length
265 this.length += 2;
266 }
267 }
268 }
269
270 /**
Jonathan Hart4a60bb32015-06-30 15:31:20 -0700271 * Decapsulates an EAP packet from the RADIUS packet.
272 *
Ari Saha79d7c252015-06-26 10:31:48 -0700273 * @return An EAP object containing the reassembled EAP message
Terje Mikal Mjelde34cc8e02018-03-09 08:31:14 +0100274 * @throws DeserializationException if packet deserialization fails
Ari Saha79d7c252015-06-26 10:31:48 -0700275 */
Terje Mikal Mjelde34cc8e02018-03-09 08:31:14 +0100276 public EAP decapsulateMessage() throws DeserializationException {
Ari Saha79d7c252015-06-26 10:31:48 -0700277 EAP message = new EAP();
278 ByteArrayOutputStream messageStream = new ByteArrayOutputStream();
279 // Iterating through EAP-Message attributes to concatenate their value
280 for (RADIUSAttribute ra : this.getAttributeList(RADIUSAttribute.RADIUS_ATTR_EAP_MESSAGE)) {
281 try {
282 messageStream.write(ra.getValue());
283 } catch (IOException e) {
284 log.error("Error while reassembling EAP message: {}", e.getMessage());
285 }
286 }
287 // Assembling EAP object from the concatenated stream
Terje Mikal Mjelde34cc8e02018-03-09 08:31:14 +0100288 message = EAP.deserializer().deserialize(messageStream.toByteArray(), 0, messageStream.size());
Ari Saha79d7c252015-06-26 10:31:48 -0700289 return message;
290 }
291
292 /**
Jonathan Hart4a60bb32015-06-30 15:31:20 -0700293 * Gets a list of attributes from the RADIUS packet.
294 *
295 * @param attrType the type field of the required attributes
Ari Saha79d7c252015-06-26 10:31:48 -0700296 * @return List of the attributes that matches the type or an empty list if there is none
297 */
298 public ArrayList<RADIUSAttribute> getAttributeList(byte attrType) {
299 ArrayList<RADIUSAttribute> attrList = new ArrayList<>();
300 for (int i = 0; i < this.attributes.size(); i++) {
301 if (this.attributes.get(i).getType() == attrType) {
302 attrList.add(this.attributes.get(i));
303 }
304 }
305 return attrList;
306 }
307
308 /**
Jonathan Hart4a60bb32015-06-30 15:31:20 -0700309 * Gets an attribute from the RADIUS packet.
310 *
311 * @param attrType the type field of the required attribute
Ari Saha79d7c252015-06-26 10:31:48 -0700312 * @return the first attribute that matches the type or null if does not exist
313 */
314 public RADIUSAttribute getAttribute(byte attrType) {
315 for (int i = 0; i < this.attributes.size(); i++) {
316 if (this.attributes.get(i).getType() == attrType) {
317 return this.attributes.get(i);
318 }
319 }
320 return null;
321 }
322
323 /**
Jonathan Hart4a60bb32015-06-30 15:31:20 -0700324 * Sets an attribute in the RADIUS packet.
325 *
326 * @param attrType the type field of the attribute to set
Jian Li5fc14292015-12-04 11:30:46 -0800327 * @param value value to be set
Ari Saha79d7c252015-06-26 10:31:48 -0700328 * @return reference to the attribute object
329 */
330 public RADIUSAttribute setAttribute(byte attrType, byte[] value) {
331 byte attrLength = (byte) (value.length + 2);
332 RADIUSAttribute newAttribute = new RADIUSAttribute(attrType, attrLength, value);
333 this.attributes.add(newAttribute);
334 this.length += (short) (attrLength & 0xFF);
335 return newAttribute;
336 }
337
Jonathan Hart4a60bb32015-06-30 15:31:20 -0700338 /**
339 * Updates an attribute in the RADIUS packet.
340 *
341 * @param attrType the type field of the attribute to update
Jian Li5fc14292015-12-04 11:30:46 -0800342 * @param value the value to update to
Jonathan Hart4a60bb32015-06-30 15:31:20 -0700343 * @return reference to the attribute object
344 */
Ari Saha79d7c252015-06-26 10:31:48 -0700345 public RADIUSAttribute updateAttribute(byte attrType, byte[] value) {
346 for (int i = 0; i < this.attributes.size(); i++) {
347 if (this.attributes.get(i).getType() == attrType) {
348 this.length -= (short) (this.attributes.get(i).getLength() & 0xFF);
349 RADIUSAttribute newAttr = new RADIUSAttribute(attrType, (byte) (value.length + 2), value);
350 this.attributes.set(i, newAttr);
351 this.length += (short) (newAttr.getLength() & 0xFF);
352 return newAttr;
353 }
354 }
355 return null;
356 }
357
Jonathan Hart4a60bb32015-06-30 15:31:20 -0700358 /**
359 * Deserializer for RADIUS packets.
360 *
361 * @return deserializer
362 */
363 public static Deserializer<RADIUS> deserializer() {
364 return (data, offset, length) -> {
365 checkInput(data, offset, length, RADIUS_MIN_LENGTH);
366
367 final ByteBuffer bb = ByteBuffer.wrap(data, offset, length);
368 RADIUS radius = new RADIUS();
369 radius.code = bb.get();
370 radius.identifier = bb.get();
371 radius.length = bb.getShort();
372 bb.get(radius.authenticator, 0, 16);
373
374 checkHeaderLength(length, radius.length);
375
376 int remainingLength = radius.length - RADIUS_MIN_LENGTH;
377 while (remainingLength > 0 && bb.hasRemaining()) {
378
379 RADIUSAttribute attr = new RADIUSAttribute();
380 attr.setType(bb.get());
381 attr.setLength(bb.get());
382 short attrLength = (short) (attr.length & 0xff);
383 attr.value = new byte[attrLength - 2];
384 bb.get(attr.value, 0, attrLength - 2);
385 radius.attributes.add(attr);
Qianqian Hu33836df2015-12-23 20:44:48 +0800386 remainingLength -= attrLength;
Jonathan Hart4a60bb32015-06-30 15:31:20 -0700387 }
388 return radius;
389 };
390 }
391
Ari Saha79d7c252015-06-26 10:31:48 -0700392 @Override
393 public byte[] serialize() {
394 final byte[] data = new byte[this.length];
395 final ByteBuffer bb = ByteBuffer.wrap(data);
396
397 bb.put(this.code);
398 bb.put(this.identifier);
399 bb.putShort(this.length);
400 bb.put(this.authenticator);
401 for (int i = 0; i < this.attributes.size(); i++) {
402 RADIUSAttribute attr = this.attributes.get(i);
403 bb.put(attr.getType());
404 bb.put(attr.getLength());
405 bb.put(attr.getValue());
406 }
407
408 return data;
409 }
410
Ari Saha79d7c252015-06-26 10:31:48 -0700411
Jian Li5fc14292015-12-04 11:30:46 -0800412 @Override
413 public String toString() {
414 return toStringHelper(getClass())
415 .add("code", Byte.toString(code))
416 .add("identifier", Byte.toString(identifier))
417 .add("length", Short.toString(length))
418 .add("authenticator", Arrays.toString(authenticator))
419 .toString();
420
421 // TODO: need to handle attributes
422 }
Ari Saha79d7c252015-06-26 10:31:48 -0700423}