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.statemachine;
18 
19 import static com.android.internal.net.eap.EapAuthenticator.LOG;
20 import static com.android.internal.net.eap.message.EapData.EAP_IDENTITY;
21 import static com.android.internal.net.eap.message.EapData.EAP_NAK;
22 import static com.android.internal.net.eap.message.EapData.EAP_NOTIFICATION;
23 import static com.android.internal.net.eap.message.EapData.EAP_TYPE_AKA;
24 import static com.android.internal.net.eap.message.EapData.EAP_TYPE_AKA_PRIME;
25 import static com.android.internal.net.eap.message.EapData.EAP_TYPE_MSCHAP_V2;
26 import static com.android.internal.net.eap.message.EapData.EAP_TYPE_SIM;
27 import static com.android.internal.net.eap.message.EapData.EAP_TYPE_STRING;
28 import static com.android.internal.net.eap.message.EapData.EAP_TYPE_TTLS;
29 import static com.android.internal.net.eap.message.EapMessage.EAP_CODE_FAILURE;
30 import static com.android.internal.net.eap.message.EapMessage.EAP_CODE_REQUEST;
31 import static com.android.internal.net.eap.message.EapMessage.EAP_CODE_RESPONSE;
32 import static com.android.internal.net.eap.message.EapMessage.EAP_CODE_STRING;
33 import static com.android.internal.net.eap.message.EapMessage.EAP_CODE_SUCCESS;
34 
35 import android.annotation.NonNull;
36 import android.annotation.Nullable;
37 import android.content.Context;
38 import android.net.eap.EapSessionConfig;
39 import android.net.eap.EapSessionConfig.EapAkaConfig;
40 import android.net.eap.EapSessionConfig.EapAkaPrimeConfig;
41 import android.net.eap.EapSessionConfig.EapMethodConfig;
42 import android.net.eap.EapSessionConfig.EapMsChapV2Config;
43 import android.net.eap.EapSessionConfig.EapSimConfig;
44 import android.net.eap.EapSessionConfig.EapTtlsConfig;
45 
46 import com.android.internal.annotations.VisibleForTesting;
47 import com.android.internal.net.eap.EapResult;
48 import com.android.internal.net.eap.EapResult.EapError;
49 import com.android.internal.net.eap.EapResult.EapFailure;
50 import com.android.internal.net.eap.EapResult.EapResponse;
51 import com.android.internal.net.eap.EapResult.EapSuccess;
52 import com.android.internal.net.eap.exceptions.EapInvalidRequestException;
53 import com.android.internal.net.eap.exceptions.EapSilentException;
54 import com.android.internal.net.eap.exceptions.UnsupportedEapTypeException;
55 import com.android.internal.net.eap.message.EapData;
56 import com.android.internal.net.eap.message.EapData.EapMethod;
57 import com.android.internal.net.eap.message.EapMessage;
58 import com.android.internal.net.utils.SimpleStateMachine;
59 
60 import java.nio.charset.StandardCharsets;
61 import java.security.SecureRandom;
62 
63 /**
64  * EapStateMachine represents the valid paths for a single EAP Authentication procedure.
65  *
66  * <p>EAP Authentication procedures will always follow the path:
67  *
68  * CreatedState --> IdentityState --> Method State --+--> SuccessState
69  *      |                                 ^          |
70  *      +---------------------------------+          +--> FailureState
71  *
72  */
73 public class EapStateMachine extends SimpleStateMachine<byte[], EapResult> {
74     private static final String TAG = EapStateMachine.class.getSimpleName();
75 
76     private final Context mContext;
77     private final EapSessionConfig mEapSessionConfig;
78     private final SecureRandom mSecureRandom;
79 
EapStateMachine( @onNull Context context, @NonNull EapSessionConfig eapSessionConfig, @NonNull SecureRandom secureRandom)80     public EapStateMachine(
81             @NonNull Context context,
82             @NonNull EapSessionConfig eapSessionConfig,
83             @NonNull SecureRandom secureRandom) {
84         this.mContext = context;
85         this.mEapSessionConfig = eapSessionConfig;
86         this.mSecureRandom = secureRandom;
87 
88         LOG.d(
89                 TAG,
90                 "Starting EapStateMachine with EAP-Identity="
91                         + LOG.pii(eapSessionConfig.eapIdentity)
92                         + " and configs=" + eapSessionConfig.eapConfigs.keySet());
93 
94         transitionTo(new CreatedState());
95     }
96 
97     @VisibleForTesting
getState()98     protected SimpleStateMachine.SimpleState getState() {
99         return mState;
100     }
101 
102     @VisibleForTesting
transitionTo(EapState newState)103     protected void transitionTo(EapState newState) {
104         LOG.d(
105                 TAG,
106                 "Transitioning from " + mState.getClass().getSimpleName()
107                         + " to " + newState.getClass().getSimpleName());
108         super.transitionTo(newState);
109     }
110 
111     @VisibleForTesting
transitionAndProcess(EapState newState, byte[] packet)112     protected EapResult transitionAndProcess(EapState newState, byte[] packet) {
113         return super.transitionAndProcess(newState, packet);
114     }
115 
116     protected abstract class EapState extends SimpleState {
decode(@onNull byte[] packet)117         protected DecodeResult decode(@NonNull byte[] packet) {
118             LOG.d(getClass().getSimpleName(),
119                     "Received packet=[" + LOG.pii(packet) + "]");
120 
121             if (packet == null) {
122                 return new DecodeResult(new EapError(
123                         new IllegalArgumentException("Attempting to decode null packet")));
124             }
125 
126             try {
127                 EapMessage eapMessage = EapMessage.decode(packet);
128 
129                 // Log inbound message in the format "EAP-<Code>/<Type>"
130                 String eapDataString =
131                         (eapMessage.eapData == null)
132                                 ? ""
133                                 : "/" + EAP_TYPE_STRING.getOrDefault(
134                                         eapMessage.eapData.eapType,
135                                         "UNKNOWN (" + eapMessage.eapData.eapType + ")");
136                 String msg = "Decoded message: EAP-"
137                         + EAP_CODE_STRING.getOrDefault(eapMessage.eapCode, "UNKNOWN")
138                         + eapDataString;
139                 LOG.i(getClass().getSimpleName(), msg);
140 
141                 if (eapMessage.eapCode == EAP_CODE_RESPONSE) {
142                     EapInvalidRequestException cause =
143                             new EapInvalidRequestException("Received an EAP-Response message");
144                     return new DecodeResult(new EapError(cause));
145                 } else if (eapMessage.eapCode == EAP_CODE_REQUEST
146                         && eapMessage.eapData.eapType == EAP_NAK) {
147                     // RFC 3748 Section 5.3.1 states that Nak type is only valid in responses
148                     EapInvalidRequestException cause =
149                             new EapInvalidRequestException("Received an EAP-Request of type Nak");
150                     return new DecodeResult(new EapError(cause));
151                 }
152 
153                 return new DecodeResult(eapMessage);
154             } catch (UnsupportedEapTypeException ex) {
155                 return new DecodeResult(
156                         EapMessage.getNakResponse(
157                                 ex.eapIdentifier,
158                                 mEapSessionConfig.eapConfigs.keySet()));
159             } catch (EapSilentException ex) {
160                 return new DecodeResult(new EapError(ex));
161             }
162         }
163 
164         protected final class DecodeResult {
165             public final EapMessage eapMessage;
166             public final EapResult eapResult;
167 
DecodeResult(EapMessage eapMessage)168             public DecodeResult(EapMessage eapMessage) {
169                 this.eapMessage = eapMessage;
170                 this.eapResult = null;
171             }
172 
DecodeResult(EapResult eapResult)173             public DecodeResult(EapResult eapResult) {
174                 this.eapMessage = null;
175                 this.eapResult = eapResult;
176             }
177 
isValidEapMessage()178             public boolean isValidEapMessage() {
179                 return eapMessage != null;
180             }
181         }
182     }
183 
184     protected class CreatedState extends EapState {
185         private final String mTAG = CreatedState.class.getSimpleName();
186 
process(@onNull byte[] packet)187         public EapResult process(@NonNull byte[] packet) {
188             DecodeResult decodeResult = decode(packet);
189             if (!decodeResult.isValidEapMessage()) {
190                 return decodeResult.eapResult;
191             }
192             EapMessage message = decodeResult.eapMessage;
193 
194             if (message.eapCode != EAP_CODE_REQUEST) {
195                 return new EapError(
196                         new EapInvalidRequestException("Received non EAP-Request in CreatedState"));
197             }
198 
199             // EapMessage#validate verifies that all EapMessage objects representing
200             // EAP-Request packets have a Type value
201             switch (message.eapData.eapType) {
202                 case EAP_NOTIFICATION:
203                     return handleNotification(mTAG, message);
204 
205                 case EAP_IDENTITY:
206                     return transitionAndProcess(new IdentityState(), packet);
207 
208                 // all EAP methods should be handled by MethodState
209                 default:
210                     return transitionAndProcess(new MethodState(), packet);
211             }
212         }
213     }
214 
215     protected class IdentityState extends EapState {
216         private final String mTAG = IdentityState.class.getSimpleName();
217 
process(@onNull byte[] packet)218         public EapResult process(@NonNull byte[] packet) {
219             DecodeResult decodeResult = decode(packet);
220             if (!decodeResult.isValidEapMessage()) {
221                 return decodeResult.eapResult;
222             }
223             EapMessage message = decodeResult.eapMessage;
224 
225             if (message.eapCode != EAP_CODE_REQUEST) {
226                 return new EapError(new EapInvalidRequestException(
227                         "Received non EAP-Request in IdentityState"));
228             }
229 
230             // EapMessage#validate verifies that all EapMessage objects representing
231             // EAP-Request packets have a Type value
232             switch (message.eapData.eapType) {
233                 case EAP_NOTIFICATION:
234                     return handleNotification(mTAG, message);
235 
236                 case EAP_IDENTITY:
237                     return getIdentityResponse(message.eapIdentifier);
238 
239                 // all EAP methods should be handled by MethodState
240                 default:
241                     return transitionAndProcess(new MethodState(), packet);
242             }
243         }
244 
245         @VisibleForTesting
getIdentityResponse(int eapIdentifier)246         EapResult getIdentityResponse(int eapIdentifier) {
247             try {
248                 LOG.d(mTAG, "Returning EAP-Identity: " + LOG.pii(mEapSessionConfig.eapIdentity));
249                 EapData identityData = new EapData(EAP_IDENTITY, mEapSessionConfig.eapIdentity);
250                 return EapResponse.getEapResponse(
251                         new EapMessage(EAP_CODE_RESPONSE, eapIdentifier, identityData));
252             } catch (EapSilentException ex) {
253                 // this should never happen - only identifier and identity bytes are variable
254                 LOG.wtf(mTAG,  "Failed to create Identity response for message with identifier="
255                         + LOG.pii(eapIdentifier));
256                 return new EapError(ex);
257             }
258         }
259     }
260 
261     protected class MethodState extends EapState {
262         private final String mTAG = MethodState.class.getSimpleName();
263 
264         @VisibleForTesting
265         EapMethodStateMachine mEapMethodStateMachine;
266 
267         // Not all EAP Method implementations may support EAP-Notifications, so allow the EAP-Method
268         // to handle any EAP-REQUEST/Notification messages (RFC 3748 Section 5.2)
process(@onNull byte[] packet)269         public EapResult process(@NonNull byte[] packet) {
270             DecodeResult decodeResult = decode(packet);
271             if (!decodeResult.isValidEapMessage()) {
272                 return decodeResult.eapResult;
273             }
274             EapMessage eapMessage = decodeResult.eapMessage;
275 
276             if (mEapMethodStateMachine == null) {
277                 if (eapMessage.eapCode == EAP_CODE_SUCCESS) {
278                     // EAP-SUCCESS is required to be the last EAP message sent during the EAP
279                     // protocol, so receiving a premature SUCCESS message is an unrecoverable error
280                     return new EapError(
281                             new EapInvalidRequestException(
282                                     "Received an EAP-Success in the MethodState"));
283                 } else if (eapMessage.eapCode == EAP_CODE_FAILURE) {
284                     transitionTo(new FailureState());
285                     return new EapFailure();
286                 } else if (eapMessage.eapData.eapType == EAP_NOTIFICATION) {
287                     // if no EapMethodStateMachine has been assigned and we receive an
288                     // EAP-Notification, we should log it and respond
289                     return handleNotification(mTAG, eapMessage);
290                 }
291 
292                 int eapType = eapMessage.eapData.eapType;
293                 mEapMethodStateMachine = buildEapMethodStateMachine(eapType);
294 
295                 if (mEapMethodStateMachine == null) {
296                     return EapMessage.getNakResponse(
297                             eapMessage.eapIdentifier,
298                             mEapSessionConfig.eapConfigs.keySet());
299                 }
300             }
301 
302             EapResult result = mEapMethodStateMachine.process(decodeResult.eapMessage);
303             if (result instanceof EapSuccess) {
304                 transitionTo(new SuccessState());
305             } else if (result instanceof EapFailure) {
306                 transitionTo(new FailureState());
307             }
308             return result;
309         }
310 
311         @Nullable
buildEapMethodStateMachine(@apMethod int eapType)312         private EapMethodStateMachine buildEapMethodStateMachine(@EapMethod int eapType) {
313             EapMethodConfig eapMethodConfig = mEapSessionConfig.eapConfigs.get(eapType);
314             if (eapMethodConfig == null) {
315                 LOG.e(
316                         mTAG,
317                         "No configs provided for method: "
318                                 + EAP_TYPE_STRING.getOrDefault(
319                                         eapType, "Unknown (" + eapType + ")"));
320                 return null;
321             }
322 
323             switch (eapType) {
324                 case EAP_TYPE_SIM:
325                     EapSimConfig eapSimConfig = (EapSimConfig) eapMethodConfig;
326                     return new EapSimMethodStateMachine(
327                             mContext, mEapSessionConfig.eapIdentity, eapSimConfig, mSecureRandom);
328                 case EAP_TYPE_AKA:
329                     EapAkaConfig eapAkaConfig = (EapAkaConfig) eapMethodConfig;
330                     boolean supportsEapAkaPrime =
331                             mEapSessionConfig.eapConfigs.containsKey(EAP_TYPE_AKA_PRIME);
332                     return new EapAkaMethodStateMachine(
333                             mContext,
334                             mEapSessionConfig.eapIdentity,
335                             eapAkaConfig,
336                             supportsEapAkaPrime);
337                 case EAP_TYPE_AKA_PRIME:
338                     EapAkaPrimeConfig eapAkaPrimeConfig = (EapAkaPrimeConfig) eapMethodConfig;
339                     return new EapAkaPrimeMethodStateMachine(
340                             mContext, mEapSessionConfig.eapIdentity, eapAkaPrimeConfig);
341                 case EAP_TYPE_MSCHAP_V2:
342                     EapMsChapV2Config eapMsChapV2Config = (EapMsChapV2Config) eapMethodConfig;
343                     return new EapMsChapV2MethodStateMachine(eapMsChapV2Config, mSecureRandom);
344                 case EAP_TYPE_TTLS:
345                     EapTtlsConfig eapTtlsConfig = (EapTtlsConfig) eapMethodConfig;
346                     return new EapTtlsMethodStateMachine(
347                             mContext, mEapSessionConfig, eapTtlsConfig, mSecureRandom);
348                 default:
349                     // received unsupported EAP Type. This should never happen.
350                     LOG.e(mTAG, "Received unsupported EAP Type=" + eapType);
351                     throw new IllegalArgumentException(
352                             "Received unsupported EAP Type in MethodState constructor");
353             }
354         }
355     }
356 
357     protected class SuccessState extends EapState {
process(byte[] packet)358         public EapResult process(byte[] packet) {
359             return new EapError(new EapInvalidRequestException(
360                     "Not possible to process messages in Success State"));
361         }
362     }
363 
364     protected class FailureState extends EapState {
process(byte[] message)365         public EapResult process(byte[] message) {
366             return new EapError(new EapInvalidRequestException(
367                     "Not possible to process messages in Failure State"));
368         }
369     }
370 
handleNotification(String tag, EapMessage message)371     protected static EapResult handleNotification(String tag, EapMessage message) {
372         // Type-Data will be UTF-8 encoded ISO 10646 characters (RFC 3748 Section 5.2)
373         String content = new String(message.eapData.eapTypeData, StandardCharsets.UTF_8);
374         LOG.i(tag, "Received EAP-Request/Notification: [" + content + "]");
375         return EapMessage.getNotificationResponse(message.eapIdentifier);
376     }
377 }
378