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