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