1 /*
2  * Copyright (C) 2019 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.internal.net.eap.message;
18 
19 import static com.android.internal.net.eap.EapAuthenticator.LOG;
20 import static com.android.internal.net.eap.message.EapData.EAP_NAK;
21 import static com.android.internal.net.eap.message.EapData.NOTIFICATION_DATA;
22 
23 import android.annotation.IntDef;
24 import android.annotation.NonNull;
25 import android.annotation.Nullable;
26 
27 import com.android.internal.net.eap.EapResult;
28 import com.android.internal.net.eap.EapResult.EapError;
29 import com.android.internal.net.eap.EapResult.EapResponse;
30 import com.android.internal.net.eap.exceptions.EapInvalidPacketLengthException;
31 import com.android.internal.net.eap.exceptions.EapSilentException;
32 import com.android.internal.net.eap.exceptions.InvalidEapCodeException;
33 import com.android.internal.net.eap.exceptions.UnsupportedEapTypeException;
34 
35 import java.lang.annotation.Retention;
36 import java.lang.annotation.RetentionPolicy;
37 import java.nio.BufferUnderflowException;
38 import java.nio.ByteBuffer;
39 import java.util.Collection;
40 import java.util.HashMap;
41 import java.util.Map;
42 
43 /**
44  * EapMessage represents an EAP Message.
45  *
46  * <p>EapMessages will be of type:
47  * <ul>
48  *     <li>@{link EAP_CODE_REQUEST}</li>
49  *     <li>@{link EAP_CODE_RESPONSE}</li>
50  *     <li>@{link EAP_CODE_SUCCESS}</li>
51  *     <li>@{link EAP_CODE_FAILURE}</li>
52  * </ul>
53  *
54  * Per RFC 3748 Section 4, EAP-Request and EAP-Response packets should be in the format:
55  *
56  * +-----------------+-----------------+----------------------------------+
57  * |    Code (1B)    | Identifier (1B) |           Length (2B)            |
58  * +-----------------+-----------------+----------------------------------+
59  * |    Type (1B)    |  Type-Data ...
60  * +-----------------+-----
61  *
62  * EAP-Success and EAP-Failure packets should be in the format:
63  *
64  * +-----------------+-----------------+----------------------------------+
65  * |   Code (1B)     | Identifier (1B) |       Length (2B) = '0004'       |
66  * +-----------------+-----------------+----------------------------------+
67  *
68  * Note that Length includes the EAP Header bytes.
69  *
70  * @see <a href="https://tools.ietf.org/html/rfc3748#section-4">RFC 3748, Extensible Authentication
71  * Protocol (EAP)</a>
72  */
73 public class EapMessage {
74     private static final String TAG = EapMessage.class.getSimpleName();
75 
76     @Retention(RetentionPolicy.SOURCE)
77     @IntDef({
78             EAP_CODE_REQUEST,
79             EAP_CODE_RESPONSE,
80             EAP_CODE_SUCCESS,
81             EAP_CODE_FAILURE
82     })
83     public @interface EapCode {}
84 
85     public static final int EAP_CODE_REQUEST = 1;
86     public static final int EAP_CODE_RESPONSE = 2;
87     public static final int EAP_CODE_SUCCESS = 3;
88     public static final int EAP_CODE_FAILURE = 4;
89 
90     public static final Map<Integer, String> EAP_CODE_STRING = new HashMap<>();
91     static {
EAP_CODE_STRING.put(EAP_CODE_REQUEST, "REQUEST")92         EAP_CODE_STRING.put(EAP_CODE_REQUEST, "REQUEST");
EAP_CODE_STRING.put(EAP_CODE_RESPONSE, "RESPONSE")93         EAP_CODE_STRING.put(EAP_CODE_RESPONSE, "RESPONSE");
EAP_CODE_STRING.put(EAP_CODE_SUCCESS, "SUCCESS")94         EAP_CODE_STRING.put(EAP_CODE_SUCCESS, "SUCCESS");
EAP_CODE_STRING.put(EAP_CODE_FAILURE, "FAILURE")95         EAP_CODE_STRING.put(EAP_CODE_FAILURE, "FAILURE");
96     }
97 
98     public static final int EAP_HEADER_LENGTH = 4;
99 
100     @EapCode public final int eapCode;
101     public final int eapIdentifier;
102     public final int eapLength;
103     public final EapData eapData;
104 
EapMessage(@apCode int eapCode, int eapIdentifier, @Nullable EapData eapData)105     public EapMessage(@EapCode int eapCode, int eapIdentifier, @Nullable EapData eapData)
106             throws EapSilentException {
107         this.eapCode = eapCode;
108         this.eapIdentifier = eapIdentifier;
109         this.eapLength = EAP_HEADER_LENGTH + ((eapData == null) ? 0 : eapData.getLength());
110         this.eapData = eapData;
111 
112         validate();
113     }
114 
115     /**
116      * Decodes and returns an EapMessage from the given byte array.
117      *
118      * @param packet byte array containing a byte-encoded EapMessage
119      * @return the EapMessage instance representing the given {@param packet}
120      * @throws EapSilentException for decoding errors that must be discarded silently
121      */
decode(@onNull byte[] packet)122     public static EapMessage decode(@NonNull byte[] packet) throws EapSilentException {
123         ByteBuffer buffer = ByteBuffer.wrap(packet);
124         int eapCode;
125         int eapIdentifier;
126         int eapLength;
127         EapData eapData;
128         try {
129             eapCode = Byte.toUnsignedInt(buffer.get());
130             eapIdentifier = Byte.toUnsignedInt(buffer.get());
131             eapLength = Short.toUnsignedInt(buffer.getShort());
132 
133             if (eapCode == EAP_CODE_REQUEST || eapCode == EAP_CODE_RESPONSE) {
134                 int eapType = Byte.toUnsignedInt(buffer.get());
135                 if (!EapData.isSupportedEapType(eapType)) {
136                     LOG.e(TAG, "Decoding EAP packet with unsupported EAP-Type: " + eapType);
137                     throw new UnsupportedEapTypeException(eapIdentifier,
138                             "Unsupported eapType=" + eapType);
139                 }
140 
141                 // Length of data to go into EapData.eapTypeData =
142                 //      eapLength - EAP_HEADER_LENGTH - 1B (eapType)
143                 int eapDataLengthRemaining = Math.max(0, eapLength - EAP_HEADER_LENGTH - 1);
144                 byte[] eapDataBytes =
145                         new byte[Math.min(eapDataLengthRemaining, buffer.remaining())];
146 
147                 buffer.get(eapDataBytes);
148                 eapData = new EapData(eapType, eapDataBytes);
149             } else {
150                 eapData = null;
151             }
152         } catch (BufferUnderflowException ex) {
153             String msg = "EAP packet is missing required values";
154             LOG.e(TAG, msg, ex);
155             throw new EapInvalidPacketLengthException(msg, ex);
156         }
157 
158         int eapDataLength = (eapData == null) ? 0 : eapData.getLength();
159         if (eapLength > EAP_HEADER_LENGTH + eapDataLength) {
160             String msg = "Packet is shorter than specified length";
161             LOG.e(TAG, msg);
162             throw new EapInvalidPacketLengthException(msg);
163         }
164 
165         return new EapMessage(eapCode, eapIdentifier, eapData);
166     }
167 
168     /**
169      * Converts this EapMessage instance to its byte-encoded representation.
170      *
171      * @return byte[] representing the byte-encoded EapMessage
172      */
encode()173     public byte[] encode() {
174         ByteBuffer byteBuffer = ByteBuffer.allocate(eapLength);
175         byteBuffer.put((byte) eapCode);
176         byteBuffer.put((byte) eapIdentifier);
177         byteBuffer.putShort((short) eapLength);
178 
179         if (eapData != null) {
180             eapData.encodeToByteBuffer(byteBuffer);
181         }
182 
183         return byteBuffer.array();
184     }
185 
186     /**
187      * Creates and returns an EAP-Response/Notification message for the given EAP Identifier wrapped
188      * in an EapResponse object.
189      *
190      * @param eapIdentifier the identifier for the message being responded to
191      * @return an EapResponse object containing an EAP-Response/Notification message with an
192      *         identifier matching the given identifier, or an EapError if an exception was thrown
193      */
getNotificationResponse(int eapIdentifier)194     public static EapResult getNotificationResponse(int eapIdentifier) {
195         try {
196             return EapResponse.getEapResponse(
197                     new EapMessage(EAP_CODE_RESPONSE, eapIdentifier, NOTIFICATION_DATA));
198         } catch (EapSilentException ex) {
199             // this should never happen - the only variable value is the identifier
200             LOG.wtf(TAG, "Failed to create Notification Response for message with identifier="
201                     + eapIdentifier);
202             return new EapError(ex);
203         }
204     }
205 
206     /**
207      * Creates and returns an EAP-Response/Nak message for the given EAP Identifier wrapped in an
208      * EapResponse object.
209      *
210      * @param eapIdentifier the identifier for the message being responded to
211      * @param supportedEapTypes Collection of EAP Method types supported by the EAP Session
212      * @return an EapResponse object containing an EAP-Response/Nak message with an identifier
213      *         matching the given identifier, or an EapError if an exception was thrown
214      */
getNakResponse( int eapIdentifier, Collection<Integer> supportedEapTypes)215     public static EapResult getNakResponse(
216             int eapIdentifier,
217             Collection<Integer> supportedEapTypes) {
218         try {
219             ByteBuffer buffer = ByteBuffer.allocate(supportedEapTypes.size());
220             for (int eapMethodType : supportedEapTypes) {
221                 buffer.put((byte) eapMethodType);
222             }
223             EapData nakData = new EapData(EAP_NAK, buffer.array());
224 
225             return EapResponse.getEapResponse(
226                     new EapMessage(EAP_CODE_RESPONSE, eapIdentifier, nakData));
227         } catch (EapSilentException ex) {
228             // this should never happen - the only variable value is the identifier
229             LOG.wtf(TAG,  "Failed to create Nak for message with identifier="
230                     + eapIdentifier);
231             return new EapError(ex);
232         }
233     }
234 
validate()235     private void validate() throws EapSilentException {
236         if (eapCode != EAP_CODE_REQUEST
237                 && eapCode != EAP_CODE_RESPONSE
238                 && eapCode != EAP_CODE_SUCCESS
239                 && eapCode != EAP_CODE_FAILURE) {
240             LOG.e(TAG, "Invalid EAP Code: " + eapCode);
241             throw new InvalidEapCodeException(eapCode);
242         }
243 
244         if ((eapCode == EAP_CODE_SUCCESS || eapCode == EAP_CODE_FAILURE)
245                 && eapLength != EAP_HEADER_LENGTH) {
246             LOG.e(TAG, "Invalid length for EAP-Success/EAP-Failure. Length: " + eapLength);
247             throw new EapInvalidPacketLengthException(
248                     "EAP Success/Failure packets must be length 4");
249         }
250 
251         if ((eapCode == EAP_CODE_REQUEST || eapCode == EAP_CODE_RESPONSE) && eapData == null) {
252             LOG.e(TAG, "No Type value included for EAP-Request/EAP-Response");
253             throw new EapInvalidPacketLengthException(
254                     "EAP Request/Response packets must include a Type value");
255         }
256     }
257 }
258