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