| /* |
| * 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_NOTIFICATION; |
| import static com.android.internal.net.eap.message.EapData.EAP_TYPE_MSCHAP_V2; |
| 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_RESPONSE; |
| import static com.android.internal.net.eap.message.EapMessage.EAP_CODE_SUCCESS; |
| import static com.android.internal.net.eap.message.mschapv2.EapMsChapV2TypeData.EAP_MSCHAP_V2_FAILURE; |
| import static com.android.internal.net.eap.message.mschapv2.EapMsChapV2TypeData.EAP_MSCHAP_V2_SUCCESS; |
| import static com.android.internal.net.eap.message.mschapv2.EapMsChapV2TypeData.EAP_OP_CODE_STRING; |
| import static com.android.internal.net.eap.message.mschapv2.EapMsChapV2TypeData.EapMsChapV2FailureRequest.EAP_ERROR_CODE_STRING; |
| import static com.android.internal.net.eap.message.mschapv2.EapMsChapV2TypeData.EapMsChapV2FailureResponse.getEapMsChapV2FailureResponse; |
| import static com.android.internal.net.eap.message.mschapv2.EapMsChapV2TypeData.EapMsChapV2SuccessResponse.getEapMsChapV2SuccessResponse; |
| |
| import android.net.eap.EapSessionConfig.EapMsChapV2Config; |
| |
| 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.crypto.ParityBitUtil; |
| import com.android.internal.net.eap.exceptions.EapInvalidRequestException; |
| import com.android.internal.net.eap.exceptions.EapSilentException; |
| import com.android.internal.net.eap.exceptions.mschapv2.EapMsChapV2ParsingException; |
| 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.eap.message.mschapv2.EapMsChapV2TypeData; |
| import com.android.internal.net.eap.message.mschapv2.EapMsChapV2TypeData.EapMsChapV2ChallengeRequest; |
| import com.android.internal.net.eap.message.mschapv2.EapMsChapV2TypeData.EapMsChapV2ChallengeResponse; |
| import com.android.internal.net.eap.message.mschapv2.EapMsChapV2TypeData.EapMsChapV2FailureRequest; |
| import com.android.internal.net.eap.message.mschapv2.EapMsChapV2TypeData.EapMsChapV2SuccessRequest; |
| import com.android.internal.net.eap.message.mschapv2.EapMsChapV2TypeData.EapMsChapV2TypeDataDecoder; |
| import com.android.internal.net.eap.message.mschapv2.EapMsChapV2TypeData.EapMsChapV2TypeDataDecoder.DecodeResult; |
| import com.android.internal.net.utils.Log; |
| |
| import org.bouncycastle.crypto.digests.MD4Digest; |
| |
| import java.io.UnsupportedEncodingException; |
| import java.nio.BufferUnderflowException; |
| import java.nio.ByteBuffer; |
| import java.nio.charset.StandardCharsets; |
| import java.security.GeneralSecurityException; |
| import java.security.MessageDigest; |
| import java.security.SecureRandom; |
| import java.util.Arrays; |
| |
| import javax.crypto.Cipher; |
| import javax.crypto.SecretKey; |
| import javax.crypto.SecretKeyFactory; |
| import javax.crypto.spec.DESKeySpec; |
| |
| /** |
| * EapMsChapV2MethodStateMachine represents the valid paths possible for the EAP MSCHAPv2 protocol. |
| * |
| * <p>EAP MSCHAPv2 sessions will always follow the path: |
| * |
| * <p>CreatedState |
| * | |
| * +--> ChallengeState |
| * | |
| * +--> ValidateAuthenticatorState --+--> AwaitingEapSuccessState --> FinalState |
| * | |
| * +--> AwaitingEapFailureState --> FinalState |
| * |
| * <p>Note: All Failure-Request messages received in the PostChallenge state will be responded to |
| * with Failure-Response messages. That is, retryable failures <i>will not</i> be retried. |
| * |
| * <p>Note: The EAP standard states that EAP methods may disallow EAP Notification messages for the |
| * duration of the method (RFC 3748#5.2). EAP MSCHAPv2 does not explicitly ban these packets, so |
| * they are allowed at any time (except once a terminal state is reached). |
| * |
| * @see <a href="https://tools.ietf.org/html/draft-kamath-pppext-eap-mschapv2-02">Microsoft EAP CHAP |
| * Extensions Draft (EAP MSCHAPv2)</a> |
| * @see <a href="https://tools.ietf.org/html/rfc2759">RFC 2759, Microsoft PPP CHAP Extensions, |
| * Version 2 (MSCHAPv2)</a> |
| * @see <a href="https://tools.ietf.org/html/rfc3079">RFC 3079, Deriving Keys for use with Microsoft |
| * Point-to-Point Encryption (MPPE)</a> |
| * @hide |
| */ |
| public class EapMsChapV2MethodStateMachine extends EapMethodStateMachine { |
| private static final String SHA_ALG = "SHA-1"; |
| private static final String DES_ALG = "DES/ECB/NoPadding"; |
| private static final String DES_KEY_FACTORY = "DES"; |
| private static final int PEER_CHALLENGE_SIZE = 16; |
| private static final int CHALLENGE_HASH_LEN = 8; |
| private static final int PASSWORD_HASH_LEN = 16; |
| private static final int PASSWORD_HASH_HASH_LEN = 16; |
| private static final int RESPONSE_LEN = 24; |
| private static final int Z_PASSWORD_HASH_LEN = 21; |
| private static final int Z_PASSWORD_SECTION_LEN = 7; |
| private static final int RESPONSE_SECTION_LEN = 8; |
| private static final int SHS_PAD_LEN = 40; |
| private static final int MASTER_KEY_LEN = 16; |
| private static final int SESSION_KEY_LEN = 16; |
| private static final int MASTER_SESSION_KEY_LEN = 2 * SESSION_KEY_LEN; |
| |
| // Reserved for future use and must be 0 (EAP MSCHAPv2#2.2) |
| private static final int FLAGS = 0; |
| |
| // we all need a little magic in our lives |
| // Defined in RFC 2759#8.7. Constants used for Success response generation. |
| private static final byte[] CHALLENGE_MAGIC_1 = { |
| (byte) 0x4D, (byte) 0x61, (byte) 0x67, (byte) 0x69, (byte) 0x63, (byte) 0x20, (byte) 0x73, |
| (byte) 0x65, (byte) 0x72, (byte) 0x76, (byte) 0x65, (byte) 0x72, (byte) 0x20, (byte) 0x74, |
| (byte) 0x6F, (byte) 0x20, (byte) 0x63, (byte) 0x6C, (byte) 0x69, (byte) 0x65, (byte) 0x6E, |
| (byte) 0x74, (byte) 0x20, (byte) 0x73, (byte) 0x69, (byte) 0x67, (byte) 0x6E, (byte) 0x69, |
| (byte) 0x6E, (byte) 0x67, (byte) 0x20, (byte) 0x63, (byte) 0x6F, (byte) 0x6E, (byte) 0x73, |
| (byte) 0x74, (byte) 0x61, (byte) 0x6E, (byte) 0x74 |
| }; |
| private static final byte[] CHALLENGE_MAGIC_2 = { |
| (byte) 0x50, (byte) 0x61, (byte) 0x64, (byte) 0x20, (byte) 0x74, (byte) 0x6F, (byte) 0x20, |
| (byte) 0x6D, (byte) 0x61, (byte) 0x6B, (byte) 0x65, (byte) 0x20, (byte) 0x69, (byte) 0x74, |
| (byte) 0x20, (byte) 0x64, (byte) 0x6F, (byte) 0x20, (byte) 0x6D, (byte) 0x6F, (byte) 0x72, |
| (byte) 0x65, (byte) 0x20, (byte) 0x74, (byte) 0x68, (byte) 0x61, (byte) 0x6E, (byte) 0x20, |
| (byte) 0x6F, (byte) 0x6E, (byte) 0x65, (byte) 0x20, (byte) 0x69, (byte) 0x74, (byte) 0x65, |
| (byte) 0x72, (byte) 0x61, (byte) 0x74, (byte) 0x69, (byte) 0x6F, (byte) 0x6E |
| }; |
| |
| // Defined in RFC 3079#3.4. Constants used for Master Session Key (MSK) generation |
| private static final byte[] SHS_PAD_1 = new byte[SHS_PAD_LEN]; |
| private static final byte[] SHS_PAD_2 = new byte[SHS_PAD_LEN]; |
| |
| static { |
| Arrays.fill(SHS_PAD_2, (byte) 0xF2); |
| } |
| |
| private static final byte[] MSK_MAGIC_1 = { |
| (byte) 0x54, (byte) 0x68, (byte) 0x69, (byte) 0x73, (byte) 0x20, (byte) 0x69, |
| (byte) 0x73, (byte) 0x20, (byte) 0x74, (byte) 0x68, (byte) 0x65, (byte) 0x20, |
| (byte) 0x4D, (byte) 0x50, (byte) 0x50, (byte) 0x45, (byte) 0x20, (byte) 0x4D, |
| (byte) 0x61, (byte) 0x73, (byte) 0x74, (byte) 0x65, (byte) 0x72, (byte) 0x20, |
| (byte) 0x4B, (byte) 0x65, (byte) 0x79 |
| }; |
| private static final byte[] MSK_MAGIC_2 = { |
| (byte) 0x4F, (byte) 0x6E, (byte) 0x20, (byte) 0x74, (byte) 0x68, (byte) 0x65, |
| (byte) 0x20, (byte) 0x63, (byte) 0x6C, (byte) 0x69, (byte) 0x65, (byte) 0x6E, |
| (byte) 0x74, (byte) 0x20, (byte) 0x73, (byte) 0x69, (byte) 0x64, (byte) 0x65, |
| (byte) 0x2C, (byte) 0x20, (byte) 0x74, (byte) 0x68, (byte) 0x69, (byte) 0x73, |
| (byte) 0x20, (byte) 0x69, (byte) 0x73, (byte) 0x20, (byte) 0x74, (byte) 0x68, |
| (byte) 0x65, (byte) 0x20, (byte) 0x73, (byte) 0x65, (byte) 0x6E, (byte) 0x64, |
| (byte) 0x20, (byte) 0x6B, (byte) 0x65, (byte) 0x79, (byte) 0x3B, (byte) 0x20, |
| (byte) 0x6F, (byte) 0x6E, (byte) 0x20, (byte) 0x74, (byte) 0x68, (byte) 0x65, |
| (byte) 0x20, (byte) 0x73, (byte) 0x65, (byte) 0x72, (byte) 0x76, (byte) 0x65, |
| (byte) 0x72, (byte) 0x20, (byte) 0x73, (byte) 0x69, (byte) 0x64, (byte) 0x65, |
| (byte) 0x2C, (byte) 0x20, (byte) 0x69, (byte) 0x74, (byte) 0x20, (byte) 0x69, |
| (byte) 0x73, (byte) 0x20, (byte) 0x74, (byte) 0x68, (byte) 0x65, (byte) 0x20, |
| (byte) 0x72, (byte) 0x65, (byte) 0x63, (byte) 0x65, (byte) 0x69, (byte) 0x76, |
| (byte) 0x65, (byte) 0x20, (byte) 0x6B, (byte) 0x65, (byte) 0x79, (byte) 0x2E |
| }; |
| private static final byte[] MSK_MAGIC_3 = { |
| (byte) 0x4F, (byte) 0x6E, (byte) 0x20, (byte) 0x74, (byte) 0x68, (byte) 0x65, |
| (byte) 0x20, (byte) 0x63, (byte) 0x6C, (byte) 0x69, (byte) 0x65, (byte) 0x6E, |
| (byte) 0x74, (byte) 0x20, (byte) 0x73, (byte) 0x69, (byte) 0x64, (byte) 0x65, |
| (byte) 0x2C, (byte) 0x20, (byte) 0x74, (byte) 0x68, (byte) 0x69, (byte) 0x73, |
| (byte) 0x20, (byte) 0x69, (byte) 0x73, (byte) 0x20, (byte) 0x74, (byte) 0x68, |
| (byte) 0x65, (byte) 0x20, (byte) 0x72, (byte) 0x65, (byte) 0x63, (byte) 0x65, |
| (byte) 0x69, (byte) 0x76, (byte) 0x65, (byte) 0x20, (byte) 0x6B, (byte) 0x65, |
| (byte) 0x79, (byte) 0x3B, (byte) 0x20, (byte) 0x6F, (byte) 0x6E, (byte) 0x20, |
| (byte) 0x74, (byte) 0x68, (byte) 0x65, (byte) 0x20, (byte) 0x73, (byte) 0x65, |
| (byte) 0x72, (byte) 0x76, (byte) 0x65, (byte) 0x72, (byte) 0x20, (byte) 0x73, |
| (byte) 0x69, (byte) 0x64, (byte) 0x65, (byte) 0x2C, (byte) 0x20, (byte) 0x69, |
| (byte) 0x74, (byte) 0x20, (byte) 0x69, (byte) 0x73, (byte) 0x20, (byte) 0x74, |
| (byte) 0x68, (byte) 0x65, (byte) 0x20, (byte) 0x73, (byte) 0x65, (byte) 0x6E, |
| (byte) 0x64, (byte) 0x20, (byte) 0x6B, (byte) 0x65, (byte) 0x79, (byte) 0x2E |
| }; |
| |
| private final EapMsChapV2Config mEapMsChapV2Config; |
| private final SecureRandom mSecureRandom; |
| private final EapMsChapV2TypeDataDecoder mTypeDataDecoder; |
| |
| public EapMsChapV2MethodStateMachine( |
| EapMsChapV2Config eapMsChapV2Config, SecureRandom secureRandom) { |
| this(eapMsChapV2Config, secureRandom, new EapMsChapV2TypeDataDecoder()); |
| } |
| |
| @VisibleForTesting |
| EapMsChapV2MethodStateMachine( |
| EapMsChapV2Config eapMsChapV2Config, |
| SecureRandom secureRandom, |
| EapMsChapV2TypeDataDecoder eapMsChapV2TypeDataDecoder) { |
| this.mEapMsChapV2Config = eapMsChapV2Config; |
| this.mSecureRandom = secureRandom; |
| this.mTypeDataDecoder = eapMsChapV2TypeDataDecoder; |
| |
| transitionTo(new CreatedState()); |
| } |
| |
| @Override |
| @EapMethod |
| int getEapMethod() { |
| return EAP_TYPE_MSCHAP_V2; |
| } |
| |
| @Override |
| EapResult handleEapNotification(String tag, EapMessage message) { |
| return EapStateMachine.handleNotification(tag, message); |
| } |
| |
| protected class CreatedState extends EapMethodState { |
| private final String mTAG = this.getClass().getSimpleName(); |
| |
| @Override |
| public EapResult process(EapMessage message) { |
| EapResult result = handleEapSuccessFailureNotification(mTAG, message); |
| if (result != null) { |
| return result; |
| } |
| |
| DecodeResult<EapMsChapV2ChallengeRequest> decodeResult = |
| mTypeDataDecoder.decodeChallengeRequest(mTAG, message.eapData.eapTypeData); |
| if (!decodeResult.isSuccessfulDecode()) { |
| return decodeResult.eapError; |
| } |
| |
| return transitionAndProcess(new ChallengeState(), message); |
| } |
| } |
| |
| protected class ChallengeState extends EapMethodState { |
| private final String mTAG = this.getClass().getSimpleName(); |
| |
| @Override |
| public EapResult process(EapMessage message) { |
| EapResult result = handleEapSuccessFailureNotification(mTAG, message); |
| if (result != null) { |
| return result; |
| } |
| |
| DecodeResult<EapMsChapV2ChallengeRequest> decodeResult = |
| mTypeDataDecoder.decodeChallengeRequest(mTAG, message.eapData.eapTypeData); |
| if (!decodeResult.isSuccessfulDecode()) { |
| return decodeResult.eapError; |
| } |
| |
| EapMsChapV2ChallengeRequest challengeRequest = decodeResult.eapTypeData; |
| LOG.d( |
| mTAG, |
| "Received Challenge Request:" |
| + " Challenge=" + LOG.pii(challengeRequest.challenge) |
| + " Server-Name=" + Log.byteArrayToHexString(challengeRequest.name)); |
| |
| byte[] peerChallenge = new byte[PEER_CHALLENGE_SIZE]; |
| mSecureRandom.nextBytes(peerChallenge); |
| |
| byte[] ntResponse; |
| try { |
| ntResponse = |
| generateNtResponse( |
| challengeRequest.challenge, |
| peerChallenge, |
| mEapMsChapV2Config.username, |
| mEapMsChapV2Config.password); |
| } catch (GeneralSecurityException ex) { |
| LOG.e(mTAG, "Error generating EAP MSCHAPv2 Challenge response", ex); |
| return new EapError(ex); |
| } |
| |
| LOG.d( |
| mTAG, |
| "Generating Challenge Response:" |
| + " Username=" + LOG.pii(mEapMsChapV2Config.username) |
| + " Peer-Challenge=" + LOG.pii(peerChallenge) |
| + " NT-Response=" + LOG.pii(ntResponse)); |
| |
| try { |
| EapMsChapV2ChallengeResponse challengeResponse = |
| new EapMsChapV2ChallengeResponse( |
| challengeRequest.msChapV2Id, |
| peerChallenge, |
| ntResponse, |
| FLAGS, |
| usernameToBytes(mEapMsChapV2Config.username)); |
| transitionTo( |
| new ValidateAuthenticatorState( |
| challengeRequest.challenge, peerChallenge, ntResponse)); |
| |
| return buildEapMessageResponse(mTAG, message.eapIdentifier, challengeResponse); |
| } catch (EapMsChapV2ParsingException ex) { |
| LOG.e(mTAG, "Error building response type data", ex); |
| return new EapError(ex); |
| } |
| } |
| } |
| |
| protected class ValidateAuthenticatorState extends EapMethodState { |
| private final String mTAG = this.getClass().getSimpleName(); |
| |
| private final byte[] mAuthenticatorChallenge; |
| private final byte[] mPeerChallenge; |
| private final byte[] mNtResponse; |
| |
| @VisibleForTesting |
| ValidateAuthenticatorState( |
| byte[] authenticatorChallenge, byte[] peerChallenge, byte[] ntResponse) { |
| this.mAuthenticatorChallenge = authenticatorChallenge; |
| this.mPeerChallenge = peerChallenge; |
| this.mNtResponse = ntResponse; |
| } |
| |
| @Override |
| public EapResult process(EapMessage message) { |
| EapResult result = handleEapSuccessFailureNotification(mTAG, message); |
| if (result != null) { |
| return result; |
| } |
| |
| int opCode; |
| try { |
| opCode = mTypeDataDecoder.getOpCode(message.eapData.eapTypeData); |
| } catch (BufferUnderflowException ex) { |
| LOG.e(mTAG, "Empty type data received in ValidateAuthenticatorState", ex); |
| return new EapError(ex); |
| } |
| |
| LOG.d( |
| mTAG, |
| "Received Op Code: " |
| + EAP_OP_CODE_STRING.getOrDefault(opCode, "Unknown") |
| + " (" + opCode + ")"); |
| |
| switch (opCode) { |
| case EAP_MSCHAP_V2_SUCCESS: |
| DecodeResult<EapMsChapV2SuccessRequest> successDecodeResult = |
| mTypeDataDecoder.decodeSuccessRequest( |
| mTAG, message.eapData.eapTypeData); |
| if (!successDecodeResult.isSuccessfulDecode()) { |
| return successDecodeResult.eapError; |
| } |
| |
| EapMsChapV2SuccessRequest successRequest = successDecodeResult.eapTypeData; |
| LOG.d( |
| mTAG, |
| "Received SuccessRequest:" |
| + " Auth-String=" + LOG.pii(successRequest.authBytes) |
| + " Message=" + successRequest.message); |
| |
| boolean isSuccessfulAuth; |
| try { |
| isSuccessfulAuth = |
| checkAuthenticatorResponse( |
| mEapMsChapV2Config.password, |
| mNtResponse, |
| mPeerChallenge, |
| mAuthenticatorChallenge, |
| mEapMsChapV2Config.username, |
| successRequest.authBytes); |
| } catch (GeneralSecurityException | UnsupportedEncodingException ex) { |
| LOG.e(mTAG, "Error validating MSCHAPv2 Authenticator Response", ex); |
| return new EapError(ex); |
| } |
| |
| if (!isSuccessfulAuth) { |
| LOG.e( |
| mTAG, |
| "Authenticator Response does not match expected response value"); |
| transitionTo(new FinalState()); |
| return new EapFailure(); |
| } |
| |
| transitionTo(new AwaitingEapSuccessState(mNtResponse)); |
| return buildEapMessageResponse( |
| mTAG, message.eapIdentifier, getEapMsChapV2SuccessResponse()); |
| |
| case EAP_MSCHAP_V2_FAILURE: |
| DecodeResult<EapMsChapV2FailureRequest> failureDecodeResult = |
| mTypeDataDecoder.decodeFailureRequest( |
| mTAG, message.eapData.eapTypeData); |
| if (!failureDecodeResult.isSuccessfulDecode()) { |
| return failureDecodeResult.eapError; |
| } |
| |
| EapMsChapV2FailureRequest failureRequest = failureDecodeResult.eapTypeData; |
| int errorCode = failureRequest.errorCode; |
| LOG.e( |
| mTAG, |
| String.format( |
| "Received MSCHAPv2 Failure-Request: E=%s (%d) R=%b V=%d M=%s", |
| EAP_ERROR_CODE_STRING.getOrDefault(errorCode, "UNKNOWN"), |
| errorCode, |
| failureRequest.isRetryable, |
| failureRequest.passwordChangeProtocol, |
| failureRequest.message)); |
| transitionTo(new AwaitingEapFailureState()); |
| return buildEapMessageResponse( |
| mTAG, message.eapIdentifier, getEapMsChapV2FailureResponse()); |
| |
| default: |
| LOG.e(mTAG, "Invalid OpCode: " + opCode); |
| return new EapError( |
| new EapInvalidRequestException( |
| "Unexpected request received in EAP MSCHAPv2")); |
| } |
| } |
| } |
| |
| protected class AwaitingEapSuccessState extends EapMethodState { |
| private final String mTAG = this.getClass().getSimpleName(); |
| |
| private final byte[] mNtResponse; |
| |
| AwaitingEapSuccessState(byte[] ntResponse) { |
| this.mNtResponse = ntResponse; |
| } |
| |
| @Override |
| public EapResult process(EapMessage message) { |
| if (message.eapCode == EAP_CODE_FAILURE) { |
| LOG.e(mTAG, "Received EAP-Failure in PreSuccessState"); |
| transitionTo(new FinalState()); |
| return new EapFailure(); |
| } else if (message.eapCode != EAP_CODE_SUCCESS) { |
| int eapType = message.eapData.eapType; |
| if (eapType == EAP_NOTIFICATION) { |
| return handleEapNotification(mTAG, message); |
| } else { |
| LOG.e( |
| mTAG, |
| "Received unexpected EAP message. Type=" |
| + EAP_TYPE_STRING.getOrDefault( |
| eapType, "UNKNOWN (" + eapType + ")")); |
| return new EapError( |
| new EapInvalidRequestException( |
| "Expected EAP Type " |
| + getEapMethod() |
| + ", received " |
| + eapType)); |
| } |
| } |
| |
| try { |
| byte[] msk = generateMsk(mEapMsChapV2Config.password, mNtResponse); |
| transitionTo(new FinalState()); |
| return new EapSuccess(msk, new byte[0]); |
| } catch (GeneralSecurityException | UnsupportedEncodingException ex) { |
| LOG.e(mTAG, "Error generating MSK for EAP MSCHAPv2", ex); |
| return new EapError(ex); |
| } |
| } |
| } |
| |
| protected class AwaitingEapFailureState extends EapMethodState { |
| private final String mTAG = this.getClass().getSimpleName(); |
| |
| @Override |
| public EapResult process(EapMessage message) { |
| EapResult result = handleEapSuccessFailureNotification(mTAG, message); |
| if (result != null) { |
| return result; |
| } |
| int eapType = message.eapData.eapType; |
| LOG.e( |
| mTAG, |
| "Received unexpected EAP message. Type=" |
| + EAP_TYPE_STRING.getOrDefault(eapType, "UNKNOWN (" + eapType + ")")); |
| return new EapError( |
| new EapInvalidRequestException( |
| "Expected EAP Type " + getEapMethod() + ", received " + eapType)); |
| } |
| } |
| |
| private EapResult buildEapMessageResponse( |
| String tag, int eapIdentifier, EapMsChapV2TypeData typeData) { |
| try { |
| EapData eapData = new EapData(getEapMethod(), typeData.encode()); |
| EapMessage eapMessage = new EapMessage(EAP_CODE_RESPONSE, eapIdentifier, eapData); |
| return EapResponse.getEapResponse(eapMessage); |
| } catch (EapSilentException ex) { |
| LOG.e(tag, "Error building response EapMessage", ex); |
| return new EapError(ex); |
| } |
| } |
| |
| /** Util for converting String username to "0-to-256 char username", as used in RFC 2759#8. */ |
| @VisibleForTesting |
| static byte[] usernameToBytes(String username) { |
| return username.getBytes(StandardCharsets.US_ASCII); |
| } |
| |
| /** |
| * Util for converting String password to "0-to-256-unicode-char password", as used in |
| * RFC 2759#8. |
| */ |
| @VisibleForTesting |
| static byte[] passwordToBytes(String password) { |
| return password.getBytes(StandardCharsets.UTF_16LE); |
| } |
| |
| /* Implementation of RFC 2759#8.1: GenerateNTResponse() */ |
| @VisibleForTesting |
| static byte[] generateNtResponse( |
| byte[] authenticatorChallenge, byte[] peerChallenge, String username, String password) |
| throws GeneralSecurityException { |
| byte[] challenge = challengeHash(peerChallenge, authenticatorChallenge, username); |
| byte[] passwordHash = ntPasswordHash(password); |
| return challengeResponse(challenge, passwordHash); |
| } |
| |
| /* Implementation of RFC 2759#8.2: ChallengeHash() */ |
| @VisibleForTesting |
| static byte[] challengeHash( |
| byte[] peerChallenge, byte[] authenticatorChallenge, String username) |
| throws GeneralSecurityException { |
| MessageDigest sha1 = MessageDigest.getInstance(SHA_ALG); |
| sha1.update(peerChallenge); |
| sha1.update(authenticatorChallenge); |
| sha1.update(usernameToBytes(username)); |
| return Arrays.copyOf(sha1.digest(), CHALLENGE_HASH_LEN); |
| } |
| |
| /* Implementation of RFC 2759#8.3: NtPasswordHash() */ |
| @VisibleForTesting |
| static byte[] ntPasswordHash(String password) { |
| MD4Digest md4 = new MD4Digest(); |
| byte[] passwordBytes = passwordToBytes(password); |
| md4.update(passwordBytes, 0, passwordBytes.length); |
| |
| byte[] passwordHash = new byte[PASSWORD_HASH_LEN]; |
| md4.doFinal(passwordHash, 0); |
| return passwordHash; |
| } |
| |
| /* Implementation of RFC 2759#8.4: HashNtPasswordHash() */ |
| @VisibleForTesting |
| static byte[] hashNtPasswordHash(byte[] passwordHash) { |
| MD4Digest md4 = new MD4Digest(); |
| md4.update(passwordHash, 0, passwordHash.length); |
| |
| byte[] passwordHashHash = new byte[PASSWORD_HASH_HASH_LEN]; |
| md4.doFinal(passwordHashHash, 0); |
| return passwordHashHash; |
| } |
| |
| /* Implementation of RFC 2759#8.5: ChallengeResponse() */ |
| @VisibleForTesting |
| static byte[] challengeResponse(byte[] challenge, byte[] passwordHash) |
| throws GeneralSecurityException { |
| byte[] zPasswordHash = Arrays.copyOf(passwordHash, Z_PASSWORD_HASH_LEN); |
| |
| ByteBuffer response = ByteBuffer.allocate(RESPONSE_LEN); |
| for (int i = 0; i < 3; i++) { |
| int from = i * Z_PASSWORD_SECTION_LEN; |
| int to = from + Z_PASSWORD_SECTION_LEN; |
| byte[] zPasswordSection = Arrays.copyOfRange(zPasswordHash, from, to); |
| response.put(desEncrypt(challenge, zPasswordSection)); |
| } |
| return response.array(); |
| } |
| |
| /* Implementation of RFC 2759#8.6: DesEncrypt() */ |
| @VisibleForTesting |
| static byte[] desEncrypt(byte[] clear, byte[] key) throws GeneralSecurityException { |
| if (key.length != Z_PASSWORD_SECTION_LEN) { |
| throw new IllegalArgumentException("DES Key must be 7B before parity-bits are added"); |
| } |
| |
| key = ParityBitUtil.addParityBits(key); |
| SecretKey secretKey = |
| SecretKeyFactory.getInstance(DES_KEY_FACTORY).generateSecret(new DESKeySpec(key)); |
| |
| Cipher des = Cipher.getInstance(DES_ALG); |
| des.init(Cipher.ENCRYPT_MODE, secretKey); |
| byte[] output = des.doFinal(clear); |
| |
| // RFC 2759#8.6 specifies 8B outputs for DesEncrypt() |
| return Arrays.copyOf(output, RESPONSE_SECTION_LEN); |
| } |
| |
| /** |
| * Implementation of RFC 2759#8.7: GenerateAuthenticatorResponse() |
| * |
| * <p>Keep response as byte[] so checkAuthenticatorResponse() can easily compare byte[]'s |
| */ |
| @VisibleForTesting |
| static byte[] generateAuthenticatorResponse( |
| String password, |
| byte[] ntResponse, |
| byte[] peerChallenge, |
| byte[] authenticatorChallenge, |
| String username) |
| throws GeneralSecurityException, UnsupportedEncodingException { |
| byte[] passwordHash = ntPasswordHash(password); |
| byte[] passwordHashHash = hashNtPasswordHash(passwordHash); |
| |
| MessageDigest sha1 = MessageDigest.getInstance(SHA_ALG); |
| sha1.update(passwordHashHash); |
| sha1.update(ntResponse); |
| sha1.update(CHALLENGE_MAGIC_1); // add just a dash of magic |
| byte[] digest = sha1.digest(); |
| |
| byte[] challenge = challengeHash(peerChallenge, authenticatorChallenge, username); |
| |
| sha1.update(digest); |
| sha1.update(challenge); |
| sha1.update(CHALLENGE_MAGIC_2); |
| |
| return sha1.digest(); |
| } |
| |
| /* Implementation of RFC 2759#8.8: CheckAuthenticatorResponse() */ |
| @VisibleForTesting |
| static boolean checkAuthenticatorResponse( |
| String password, |
| byte[] ntResponse, |
| byte[] peerChallenge, |
| byte[] authenticatorChallenge, |
| String userName, |
| byte[] receivedResponse) |
| throws GeneralSecurityException, UnsupportedEncodingException { |
| byte[] myResponse = |
| generateAuthenticatorResponse( |
| password, ntResponse, peerChallenge, authenticatorChallenge, userName); |
| return Arrays.equals(myResponse, receivedResponse); |
| } |
| |
| /* Implementation of RFC 3079#3.4: GetMasterKey() */ |
| @VisibleForTesting |
| static byte[] getMasterKey(byte[] passwordHashHash, byte[] ntResponse) |
| throws GeneralSecurityException { |
| MessageDigest sha1 = MessageDigest.getInstance(SHA_ALG); |
| sha1.update(passwordHashHash); |
| sha1.update(ntResponse); |
| sha1.update(MSK_MAGIC_1); |
| return Arrays.copyOf(sha1.digest(), MASTER_KEY_LEN); |
| } |
| |
| /* Implementation of RFC 3079#3.4: GetAsymmetricStartKey() */ |
| @VisibleForTesting |
| static byte[] getAsymmetricStartKey(byte[] masterKey, boolean isSend) |
| throws GeneralSecurityException { |
| // salt: referred to as 's' in RFC 3079#3.4 GetAsymmetricStartKey() |
| byte[] salt = isSend ? MSK_MAGIC_2 : MSK_MAGIC_3; |
| MessageDigest sha1 = MessageDigest.getInstance(SHA_ALG); |
| sha1.update(masterKey); |
| sha1.update(SHS_PAD_1); |
| sha1.update(salt); |
| sha1.update(SHS_PAD_2); |
| return Arrays.copyOf(sha1.digest(), SESSION_KEY_LEN); |
| } |
| |
| @VisibleForTesting |
| static byte[] generateMsk(String password, byte[] ntResponse) |
| throws GeneralSecurityException, UnsupportedEncodingException { |
| byte[] passwordHash = ntPasswordHash(password); |
| byte[] passwordHashHash = hashNtPasswordHash(passwordHash); |
| byte[] masterKey = getMasterKey(passwordHashHash, ntResponse); |
| |
| // MSK: SendKey + ReceiveKey |
| ByteBuffer msk = ByteBuffer.allocate(MASTER_SESSION_KEY_LEN); |
| msk.put(getAsymmetricStartKey(masterKey, true /* isSend */)); |
| msk.put(getAsymmetricStartKey(masterKey, false /* isSend */)); |
| |
| return msk.array(); |
| } |
| } |