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