| /* |
| * Copyright (C) 2019 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.internal.net.eap.statemachine; |
| |
| import static com.android.internal.net.eap.EapAuthenticator.LOG; |
| import static com.android.internal.net.eap.message.EapData.EAP_IDENTITY; |
| import static com.android.internal.net.eap.message.EapData.EAP_NAK; |
| import static com.android.internal.net.eap.message.EapData.EAP_NOTIFICATION; |
| import static com.android.internal.net.eap.message.EapData.EAP_TYPE_AKA; |
| import static com.android.internal.net.eap.message.EapData.EAP_TYPE_AKA_PRIME; |
| import static com.android.internal.net.eap.message.EapData.EAP_TYPE_MSCHAP_V2; |
| import static com.android.internal.net.eap.message.EapData.EAP_TYPE_SIM; |
| import static com.android.internal.net.eap.message.EapData.EAP_TYPE_STRING; |
| import static com.android.internal.net.eap.message.EapMessage.EAP_CODE_FAILURE; |
| import static com.android.internal.net.eap.message.EapMessage.EAP_CODE_REQUEST; |
| import static com.android.internal.net.eap.message.EapMessage.EAP_CODE_RESPONSE; |
| import static com.android.internal.net.eap.message.EapMessage.EAP_CODE_STRING; |
| import static com.android.internal.net.eap.message.EapMessage.EAP_CODE_SUCCESS; |
| |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.content.Context; |
| import android.net.eap.EapSessionConfig; |
| import android.net.eap.EapSessionConfig.EapAkaConfig; |
| import android.net.eap.EapSessionConfig.EapAkaPrimeConfig; |
| import android.net.eap.EapSessionConfig.EapMethodConfig; |
| import android.net.eap.EapSessionConfig.EapMsChapV2Config; |
| import android.net.eap.EapSessionConfig.EapSimConfig; |
| |
| import com.android.internal.annotations.VisibleForTesting; |
| import com.android.internal.net.eap.EapResult; |
| import com.android.internal.net.eap.EapResult.EapError; |
| import com.android.internal.net.eap.EapResult.EapFailure; |
| import com.android.internal.net.eap.EapResult.EapResponse; |
| import com.android.internal.net.eap.EapResult.EapSuccess; |
| import com.android.internal.net.eap.exceptions.EapInvalidRequestException; |
| import com.android.internal.net.eap.exceptions.EapSilentException; |
| import com.android.internal.net.eap.exceptions.UnsupportedEapTypeException; |
| import com.android.internal.net.eap.message.EapData; |
| import com.android.internal.net.eap.message.EapData.EapMethod; |
| import com.android.internal.net.eap.message.EapMessage; |
| import com.android.internal.net.utils.SimpleStateMachine; |
| |
| import java.nio.charset.StandardCharsets; |
| import java.security.SecureRandom; |
| |
| /** |
| * EapStateMachine represents the valid paths for a single EAP Authentication procedure. |
| * |
| * <p>EAP Authentication procedures will always follow the path: |
| * |
| * CreatedState --> IdentityState --> Method State --+--> SuccessState |
| * | ^ | |
| * +---------------------------------+ +--> FailureState |
| * |
| */ |
| public class EapStateMachine extends SimpleStateMachine<byte[], EapResult> { |
| private static final String TAG = EapStateMachine.class.getSimpleName(); |
| |
| private final Context mContext; |
| private final EapSessionConfig mEapSessionConfig; |
| private final SecureRandom mSecureRandom; |
| |
| public EapStateMachine( |
| @NonNull Context context, |
| @NonNull EapSessionConfig eapSessionConfig, |
| @NonNull SecureRandom secureRandom) { |
| this.mContext = context; |
| this.mEapSessionConfig = eapSessionConfig; |
| this.mSecureRandom = secureRandom; |
| |
| LOG.d( |
| TAG, |
| "Starting EapStateMachine with EAP-Identity=" |
| + LOG.pii(eapSessionConfig.eapIdentity) |
| + " and configs=" + eapSessionConfig.eapConfigs.keySet()); |
| |
| transitionTo(new CreatedState()); |
| } |
| |
| @VisibleForTesting |
| protected SimpleStateMachine.SimpleState getState() { |
| return mState; |
| } |
| |
| @VisibleForTesting |
| protected void transitionTo(EapState newState) { |
| LOG.d( |
| TAG, |
| "Transitioning from " + mState.getClass().getSimpleName() |
| + " to " + newState.getClass().getSimpleName()); |
| super.transitionTo(newState); |
| } |
| |
| @VisibleForTesting |
| protected EapResult transitionAndProcess(EapState newState, byte[] packet) { |
| return super.transitionAndProcess(newState, packet); |
| } |
| |
| protected abstract class EapState extends SimpleState { |
| protected DecodeResult decode(@NonNull byte[] packet) { |
| LOG.d(getClass().getSimpleName(), |
| "Received packet=[" + LOG.pii(packet) + "]"); |
| |
| if (packet == null) { |
| return new DecodeResult(new EapError( |
| new IllegalArgumentException("Attempting to decode null packet"))); |
| } |
| |
| try { |
| EapMessage eapMessage = EapMessage.decode(packet); |
| |
| // Log inbound message in the format "EAP-<Code>/<Type>" |
| String eapDataString = |
| (eapMessage.eapData == null) |
| ? "" |
| : "/" + EAP_TYPE_STRING.getOrDefault( |
| eapMessage.eapData.eapType, |
| "UNKNOWN (" + eapMessage.eapData.eapType + ")"); |
| String msg = "Decoded message: EAP-" |
| + EAP_CODE_STRING.getOrDefault(eapMessage.eapCode, "UNKNOWN") |
| + eapDataString; |
| LOG.i(getClass().getSimpleName(), msg); |
| |
| if (eapMessage.eapCode == EAP_CODE_RESPONSE) { |
| EapInvalidRequestException cause = |
| new EapInvalidRequestException("Received an EAP-Response message"); |
| return new DecodeResult(new EapError(cause)); |
| } else if (eapMessage.eapCode == EAP_CODE_REQUEST |
| && eapMessage.eapData.eapType == EAP_NAK) { |
| // RFC 3748 Section 5.3.1 states that Nak type is only valid in responses |
| EapInvalidRequestException cause = |
| new EapInvalidRequestException("Received an EAP-Request of type Nak"); |
| return new DecodeResult(new EapError(cause)); |
| } |
| |
| return new DecodeResult(eapMessage); |
| } catch (UnsupportedEapTypeException ex) { |
| return new DecodeResult( |
| EapMessage.getNakResponse( |
| ex.eapIdentifier, |
| mEapSessionConfig.eapConfigs.keySet())); |
| } catch (EapSilentException ex) { |
| return new DecodeResult(new EapError(ex)); |
| } |
| } |
| |
| protected final class DecodeResult { |
| public final EapMessage eapMessage; |
| public final EapResult eapResult; |
| |
| public DecodeResult(EapMessage eapMessage) { |
| this.eapMessage = eapMessage; |
| this.eapResult = null; |
| } |
| |
| public DecodeResult(EapResult eapResult) { |
| this.eapMessage = null; |
| this.eapResult = eapResult; |
| } |
| |
| public boolean isValidEapMessage() { |
| return eapMessage != null; |
| } |
| } |
| } |
| |
| protected class CreatedState extends EapState { |
| private final String mTAG = CreatedState.class.getSimpleName(); |
| |
| public EapResult process(@NonNull byte[] packet) { |
| DecodeResult decodeResult = decode(packet); |
| if (!decodeResult.isValidEapMessage()) { |
| return decodeResult.eapResult; |
| } |
| EapMessage message = decodeResult.eapMessage; |
| |
| if (message.eapCode != EAP_CODE_REQUEST) { |
| return new EapError( |
| new EapInvalidRequestException("Received non EAP-Request in CreatedState")); |
| } |
| |
| // EapMessage#validate verifies that all EapMessage objects representing |
| // EAP-Request packets have a Type value |
| switch (message.eapData.eapType) { |
| case EAP_NOTIFICATION: |
| return handleNotification(mTAG, message); |
| |
| case EAP_IDENTITY: |
| return transitionAndProcess(new IdentityState(), packet); |
| |
| // all EAP methods should be handled by MethodState |
| default: |
| return transitionAndProcess(new MethodState(), packet); |
| } |
| } |
| } |
| |
| protected class IdentityState extends EapState { |
| private final String mTAG = IdentityState.class.getSimpleName(); |
| |
| public EapResult process(@NonNull byte[] packet) { |
| DecodeResult decodeResult = decode(packet); |
| if (!decodeResult.isValidEapMessage()) { |
| return decodeResult.eapResult; |
| } |
| EapMessage message = decodeResult.eapMessage; |
| |
| if (message.eapCode != EAP_CODE_REQUEST) { |
| return new EapError(new EapInvalidRequestException( |
| "Received non EAP-Request in IdentityState")); |
| } |
| |
| // EapMessage#validate verifies that all EapMessage objects representing |
| // EAP-Request packets have a Type value |
| switch (message.eapData.eapType) { |
| case EAP_NOTIFICATION: |
| return handleNotification(mTAG, message); |
| |
| case EAP_IDENTITY: |
| return getIdentityResponse(message.eapIdentifier); |
| |
| // all EAP methods should be handled by MethodState |
| default: |
| return transitionAndProcess(new MethodState(), packet); |
| } |
| } |
| |
| @VisibleForTesting |
| EapResult getIdentityResponse(int eapIdentifier) { |
| try { |
| LOG.d(mTAG, "Returning EAP-Identity: " + LOG.pii(mEapSessionConfig.eapIdentity)); |
| EapData identityData = new EapData(EAP_IDENTITY, mEapSessionConfig.eapIdentity); |
| return EapResponse.getEapResponse( |
| new EapMessage(EAP_CODE_RESPONSE, eapIdentifier, identityData)); |
| } catch (EapSilentException ex) { |
| // this should never happen - only identifier and identity bytes are variable |
| LOG.wtf(mTAG, "Failed to create Identity response for message with identifier=" |
| + LOG.pii(eapIdentifier)); |
| return new EapError(ex); |
| } |
| } |
| } |
| |
| protected class MethodState extends EapState { |
| private final String mTAG = MethodState.class.getSimpleName(); |
| |
| @VisibleForTesting |
| EapMethodStateMachine mEapMethodStateMachine; |
| |
| // Not all EAP Method implementations may support EAP-Notifications, so allow the EAP-Method |
| // to handle any EAP-REQUEST/Notification messages (RFC 3748 Section 5.2) |
| public EapResult process(@NonNull byte[] packet) { |
| DecodeResult decodeResult = decode(packet); |
| if (!decodeResult.isValidEapMessage()) { |
| return decodeResult.eapResult; |
| } |
| EapMessage eapMessage = decodeResult.eapMessage; |
| |
| if (mEapMethodStateMachine == null) { |
| if (eapMessage.eapCode == EAP_CODE_SUCCESS) { |
| // EAP-SUCCESS is required to be the last EAP message sent during the EAP |
| // protocol, so receiving a premature SUCCESS message is an unrecoverable error |
| return new EapError( |
| new EapInvalidRequestException( |
| "Received an EAP-Success in the MethodState")); |
| } else if (eapMessage.eapCode == EAP_CODE_FAILURE) { |
| transitionTo(new FailureState()); |
| return new EapFailure(); |
| } else if (eapMessage.eapData.eapType == EAP_NOTIFICATION) { |
| // if no EapMethodStateMachine has been assigned and we receive an |
| // EAP-Notification, we should log it and respond |
| return handleNotification(mTAG, eapMessage); |
| } |
| |
| int eapType = eapMessage.eapData.eapType; |
| mEapMethodStateMachine = buildEapMethodStateMachine(eapType); |
| |
| if (mEapMethodStateMachine == null) { |
| return EapMessage.getNakResponse( |
| eapMessage.eapIdentifier, |
| mEapSessionConfig.eapConfigs.keySet()); |
| } |
| } |
| |
| EapResult result = mEapMethodStateMachine.process(decodeResult.eapMessage); |
| if (result instanceof EapSuccess) { |
| transitionTo(new SuccessState()); |
| } else if (result instanceof EapFailure) { |
| transitionTo(new FailureState()); |
| } |
| return result; |
| } |
| |
| @Nullable |
| private EapMethodStateMachine buildEapMethodStateMachine(@EapMethod int eapType) { |
| EapMethodConfig eapMethodConfig = mEapSessionConfig.eapConfigs.get(eapType); |
| if (eapMethodConfig == null) { |
| LOG.e( |
| mTAG, |
| "No configs provided for method: " |
| + EAP_TYPE_STRING.getOrDefault( |
| eapType, "Unknown (" + eapType + ")")); |
| return null; |
| } |
| |
| switch (eapType) { |
| case EAP_TYPE_SIM: |
| EapSimConfig eapSimConfig = (EapSimConfig) eapMethodConfig; |
| return new EapSimMethodStateMachine( |
| mContext, mEapSessionConfig.eapIdentity, eapSimConfig, mSecureRandom); |
| case EAP_TYPE_AKA: |
| EapAkaConfig eapAkaConfig = (EapAkaConfig) eapMethodConfig; |
| boolean supportsEapAkaPrime = |
| mEapSessionConfig.eapConfigs.containsKey(EAP_TYPE_AKA_PRIME); |
| return new EapAkaMethodStateMachine( |
| mContext, |
| mEapSessionConfig.eapIdentity, |
| eapAkaConfig, |
| supportsEapAkaPrime); |
| case EAP_TYPE_AKA_PRIME: |
| EapAkaPrimeConfig eapAkaPrimeConfig = (EapAkaPrimeConfig) eapMethodConfig; |
| return new EapAkaPrimeMethodStateMachine( |
| mContext, mEapSessionConfig.eapIdentity, eapAkaPrimeConfig); |
| case EAP_TYPE_MSCHAP_V2: |
| EapMsChapV2Config eapMsChapV2Config = (EapMsChapV2Config) eapMethodConfig; |
| return new EapMsChapV2MethodStateMachine(eapMsChapV2Config, mSecureRandom); |
| default: |
| // received unsupported EAP Type. This should never happen. |
| LOG.e(mTAG, "Received unsupported EAP Type=" + eapType); |
| throw new IllegalArgumentException( |
| "Received unsupported EAP Type in MethodState constructor"); |
| } |
| } |
| } |
| |
| protected class SuccessState extends EapState { |
| public EapResult process(byte[] packet) { |
| return new EapError(new EapInvalidRequestException( |
| "Not possible to process messages in Success State")); |
| } |
| } |
| |
| protected class FailureState extends EapState { |
| public EapResult process(byte[] message) { |
| return new EapError(new EapInvalidRequestException( |
| "Not possible to process messages in Failure State")); |
| } |
| } |
| |
| protected static EapResult handleNotification(String tag, EapMessage message) { |
| // Type-Data will be UTF-8 encoded ISO 10646 characters (RFC 3748 Section 5.2) |
| String content = new String(message.eapData.eapTypeData, StandardCharsets.UTF_8); |
| LOG.i(tag, "Received EAP-Request/Notification: [" + content + "]"); |
| return EapMessage.getNotificationResponse(message.eapIdentifier); |
| } |
| } |