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