blob: a31f4a0c7bb2eff39a8196c0b3fd0826b97d9f6f [file] [log] [blame]
/*
* 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.ike.eap.message.mschapv2;
import static com.android.ike.eap.EapAuthenticator.LOG;
import com.android.ike.eap.EapResult.EapError;
import com.android.ike.eap.exceptions.mschapv2.EapMsChapV2ParsingException;
import com.android.ike.eap.message.EapMessage;
import com.android.internal.annotations.VisibleForTesting;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
* EapMsChapV2TypeData represents the Type Data for an {@link EapMessage} during an EAP MSCHAPv2
* session.
*/
public class EapMsChapV2TypeData {
private static final int LABEL_VALUE_LENGTH = 2;
private static final String ASCII_CHARSET_NAME = "US-ASCII";
private static final String MESSAGE_PREFIX = "M=";
private static final String MESSAGE_LABEL = "M";
// EAP MSCHAPv2 OpCode values (EAP MSCHAPv2#2)
public static final int EAP_MSCHAP_V2_CHALLENGE = 1;
public static final int EAP_MSCHAP_V2_RESPONSE = 2;
public static final int EAP_MSCHAP_V2_SUCCESS = 3;
public static final int EAP_MSCHAP_V2_FAILURE = 4;
public static final int EAP_MSCHAP_V2_CHANGE_PASSWORD = 7;
public static final Map<Integer, String> EAP_OP_CODE_STRING = new HashMap<>();
static {
EAP_OP_CODE_STRING.put(EAP_MSCHAP_V2_CHALLENGE, "Challenge");
EAP_OP_CODE_STRING.put(EAP_MSCHAP_V2_RESPONSE, "Response");
EAP_OP_CODE_STRING.put(EAP_MSCHAP_V2_SUCCESS, "Success");
EAP_OP_CODE_STRING.put(EAP_MSCHAP_V2_FAILURE, "Failure");
EAP_OP_CODE_STRING.put(EAP_MSCHAP_V2_CHANGE_PASSWORD, "Change-Password");
}
private static final Set<Integer> SUPPORTED_OP_CODES = new HashSet<>();
static {
SUPPORTED_OP_CODES.add(EAP_MSCHAP_V2_CHALLENGE);
SUPPORTED_OP_CODES.add(EAP_MSCHAP_V2_RESPONSE);
SUPPORTED_OP_CODES.add(EAP_MSCHAP_V2_SUCCESS);
SUPPORTED_OP_CODES.add(EAP_MSCHAP_V2_FAILURE);
}
public final int opCode;
EapMsChapV2TypeData(int opCode) throws EapMsChapV2ParsingException {
this.opCode = opCode;
if (!SUPPORTED_OP_CODES.contains(opCode)) {
throw new EapMsChapV2ParsingException("Unsupported opCode provided: " + opCode);
}
}
/**
* Encodes this EapMsChapV2TypeData instance as a byte[].
*
* @return byte[] representing the encoded value of this EapMsChapV2TypeData instance.
*/
public byte[] encode() {
throw new UnsupportedOperationException(
"encode() not supported by " + this.getClass().getSimpleName());
}
abstract static class EapMsChapV2VariableTypeData extends EapMsChapV2TypeData {
public final int msChapV2Id;
public final int msLength;
EapMsChapV2VariableTypeData(int opCode, int msChapV2Id, int msLength)
throws EapMsChapV2ParsingException {
super(opCode);
this.msChapV2Id = msChapV2Id;
this.msLength = msLength;
}
}
/**
* EapMsChapV2ChallengeRequest represents the EAP MSCHAPv2 Challenge Packet (EAP MSCHAPv2#2.1).
*/
public static class EapMsChapV2ChallengeRequest extends EapMsChapV2VariableTypeData {
public static final int VALUE_SIZE = 16;
public static final int TYPE_DATA_HEADER_SIZE = 5;
public final byte[] challenge = new byte[VALUE_SIZE];
public final byte[] name;
EapMsChapV2ChallengeRequest(ByteBuffer buffer) throws EapMsChapV2ParsingException {
super(
EAP_MSCHAP_V2_CHALLENGE,
Byte.toUnsignedInt(buffer.get()),
Short.toUnsignedInt(buffer.getShort()));
int valueSize = Byte.toUnsignedInt(buffer.get());
if (valueSize != VALUE_SIZE) {
throw new EapMsChapV2ParsingException("Challenge Value-Size must be 16");
}
buffer.get(challenge);
int nameLenBytes = msLength - VALUE_SIZE - TYPE_DATA_HEADER_SIZE;
if (nameLenBytes < 0) {
throw new EapMsChapV2ParsingException("Invalid MS-Length specified");
}
name = new byte[nameLenBytes];
buffer.get(name);
}
@VisibleForTesting
public EapMsChapV2ChallengeRequest(
int msChapV2Id, int msLength, byte[] challenge, byte[] name)
throws EapMsChapV2ParsingException {
super(EAP_MSCHAP_V2_CHALLENGE, msChapV2Id, msLength);
if (challenge.length != VALUE_SIZE) {
throw new EapMsChapV2ParsingException("Challenge length must be 16");
}
System.arraycopy(challenge, 0, this.challenge, 0, VALUE_SIZE);
this.name = name;
}
}
/**
* EapMsChapV2ChallengeResponse represents the EAP MSCHAPv2 Response Packet (EAP MSCHAPv2#2.2).
*/
public static class EapMsChapV2ChallengeResponse extends EapMsChapV2VariableTypeData {
public static final int VALUE_SIZE = 49;
public static final int PEER_CHALLENGE_SIZE = 16;
public static final int RESERVED_BYTES = 8;
public static final int NT_RESPONSE_SIZE = 24;
public static final int TYPE_DATA_HEADER_SIZE = 5;
public final byte[] peerChallenge = new byte[PEER_CHALLENGE_SIZE];
public final byte[] ntResponse = new byte[NT_RESPONSE_SIZE];
public final int flags;
public final byte[] name;
public EapMsChapV2ChallengeResponse(
int msChapV2Id, byte[] peerChallenge, byte[] ntResponse, int flags, byte[] name)
throws EapMsChapV2ParsingException {
super(
EAP_MSCHAP_V2_RESPONSE,
msChapV2Id,
TYPE_DATA_HEADER_SIZE + VALUE_SIZE + name.length);
if (peerChallenge.length != PEER_CHALLENGE_SIZE) {
throw new EapMsChapV2ParsingException("Peer-Challenge must be 16B");
} else if (ntResponse.length != NT_RESPONSE_SIZE) {
throw new EapMsChapV2ParsingException("NT-Response must be 24B");
} else if (flags != 0) {
throw new EapMsChapV2ParsingException("Flags must be 0x00");
}
System.arraycopy(peerChallenge, 0, this.peerChallenge, 0, PEER_CHALLENGE_SIZE);
System.arraycopy(ntResponse, 0, this.ntResponse, 0, NT_RESPONSE_SIZE);
this.flags = flags;
this.name = name;
}
@Override
public byte[] encode() {
ByteBuffer buffer = ByteBuffer.allocate(msLength);
buffer.put((byte) EAP_MSCHAP_V2_RESPONSE);
buffer.put((byte) msChapV2Id);
buffer.putShort((short) msLength);
buffer.put((byte) VALUE_SIZE);
buffer.put(peerChallenge);
buffer.put(new byte[RESERVED_BYTES]);
buffer.put(ntResponse);
buffer.put((byte) flags);
buffer.put(name);
return buffer.array();
}
}
/**
* EapMsChapV2SuccessRequest represents the EAP MSCHAPv2 Success Request Packet
* (EAP MSCHAPv2#2.3).
*/
public static class EapMsChapV2SuccessRequest extends EapMsChapV2VariableTypeData {
private static final int AUTH_STRING_LEN_HEX = 40;
private static final int AUTH_STRING_LEN_BYTES = 20;
private static final int NUM_REQUIRED_ATTRIBUTES = 2;
private static final String AUTH_STRING_LABEL = "S";
public final byte[] authBytes = new byte[AUTH_STRING_LEN_BYTES];
public final String message;
EapMsChapV2SuccessRequest(ByteBuffer buffer) throws EapMsChapV2ParsingException {
super(
EAP_MSCHAP_V2_SUCCESS,
Byte.toUnsignedInt(buffer.get()),
Short.toUnsignedInt(buffer.getShort()));
byte[] message = new byte[buffer.remaining()];
buffer.get(message);
// message formatting: "S=<auth_string> M=<message>"
Map<String, String> mappings =
getMessageMappings(new String(message, Charset.forName(ASCII_CHARSET_NAME)));
if (!mappings.containsKey(AUTH_STRING_LABEL)
|| mappings.size() != NUM_REQUIRED_ATTRIBUTES) {
throw new EapMsChapV2ParsingException(
"Auth message must be in the format: 'S=<auth_string> M=<message>'");
}
String authStringHex = mappings.get(AUTH_STRING_LABEL);
if (authStringHex.length() != AUTH_STRING_LEN_HEX) {
throw new EapMsChapV2ParsingException("Auth String must be 40 hex chars (20B)");
}
byte[] authBytes = hexStringToByteArray(authStringHex);
System.arraycopy(authBytes, 0, this.authBytes, 0, AUTH_STRING_LEN_BYTES);
this.message = mappings.get(MESSAGE_LABEL);
}
@VisibleForTesting
public EapMsChapV2SuccessRequest(
int msChapV2Id, int msLength, byte[] authBytes, String message)
throws EapMsChapV2ParsingException {
super(EAP_MSCHAP_V2_SUCCESS, msChapV2Id, msLength);
if (authBytes.length != AUTH_STRING_LEN_BYTES) {
throw new EapMsChapV2ParsingException("Auth String must be 20B");
}
System.arraycopy(authBytes, 0, this.authBytes, 0, AUTH_STRING_LEN_BYTES);
this.message = message;
}
}
/**
* EapMsChapV2SuccessResponse represents the EAP MSCHAPv2 Success Response Packet
* (EAP MSCHAPv2#2.4).
*/
public static class EapMsChapV2SuccessResponse extends EapMsChapV2TypeData {
private EapMsChapV2SuccessResponse() throws EapMsChapV2ParsingException {
super(EAP_MSCHAP_V2_SUCCESS);
}
/**
* Constructs and returns a new EAP MSCHAPv2 Success Response type data.
*
* @return a new EapMsChapV2SuccessResponse instance
*/
public static EapMsChapV2SuccessResponse getEapMsChapV2SuccessResponse() {
try {
return new EapMsChapV2SuccessResponse();
} catch (EapMsChapV2ParsingException ex) {
// This should never happen
LOG.wtf(
EapMsChapV2SuccessResponse.class.getSimpleName(),
"ParsingException thrown while creating EapMsChapV2SuccessResponse",
ex);
return null;
}
}
@Override
public byte[] encode() {
return new byte[] {(byte) EAP_MSCHAP_V2_SUCCESS};
}
}
/**
* EapMsChapV2FailureRequest represents the EAP MSCHAPv2 Failure Request Packet
* (EAP MSCHAPv2#2.5).
*/
public static class EapMsChapV2FailureRequest extends EapMsChapV2VariableTypeData {
private static final int NUM_REQUIRED_ATTRIBUTES = 5;
private static final int CHALLENGE_LENGTH = 16;
private static final String ERROR_LABEL = "E";
private static final String RETRY_LABEL = "R";
private static final String IS_RETRYABLE_FLAG = "1";
private static final String CHALLENGE_LABEL = "C";
private static final String PASSWORD_CHANGE_PROTOCOL_LABEL = "V";
// Error codes defined in EAP MSCHAPv2#2.5
public static final Map<Integer, String> EAP_ERROR_CODE_STRING = new HashMap<>();
static {
EAP_ERROR_CODE_STRING.put(646, "ERROR_RESTRICTED_LOGON_HOURS");
EAP_ERROR_CODE_STRING.put(647, "ERROR_ACCT_DISABLED");
EAP_ERROR_CODE_STRING.put(648, "ERROR_PASSWD_EXPIRED");
EAP_ERROR_CODE_STRING.put(649, "ERROR_NO_DIALIN_PERMISSION");
EAP_ERROR_CODE_STRING.put(691, "ERROR_AUTHENTICATION_FAILURE");
EAP_ERROR_CODE_STRING.put(709, "ERROR_CHANGING_PASSWORD");
}
public final int errorCode;
public final boolean isRetryable;
public final byte[] challenge;
public final int passwordChangeProtocol;
public final String message;
EapMsChapV2FailureRequest(ByteBuffer buffer)
throws EapMsChapV2ParsingException, NumberFormatException {
super(
EAP_MSCHAP_V2_FAILURE,
Byte.toUnsignedInt(buffer.get()),
Short.toUnsignedInt(buffer.getShort()));
byte[] message = new byte[buffer.remaining()];
buffer.get(message);
// message formatting:
// "E=<error_code> R=<retry bit> C=<challenge> V=<password_change_protocol> M=<message>"
Map<String, String> mappings =
getMessageMappings(new String(message, Charset.forName(ASCII_CHARSET_NAME)));
if (!mappings.containsKey(ERROR_LABEL)
|| !mappings.containsKey(RETRY_LABEL)
|| !mappings.containsKey(CHALLENGE_LABEL)
|| !mappings.containsKey(PASSWORD_CHANGE_PROTOCOL_LABEL)
|| mappings.size() != NUM_REQUIRED_ATTRIBUTES) {
throw new EapMsChapV2ParsingException(
"Message must be formatted as: E=<error_code> R=<retry bit> C=<challenge>"
+ " V=<password_change_protocol> M=<message>");
}
this.errorCode = Integer.parseInt(mappings.get(ERROR_LABEL));
this.isRetryable = IS_RETRYABLE_FLAG.equals(mappings.get(RETRY_LABEL));
this.challenge = hexStringToByteArray(mappings.get(CHALLENGE_LABEL));
this.passwordChangeProtocol = Integer.parseInt(mappings.get(
PASSWORD_CHANGE_PROTOCOL_LABEL));
this.message = mappings.get("M");
if (challenge.length != CHALLENGE_LENGTH) {
throw new EapMsChapV2ParsingException("Challenge must be 16B long");
}
}
@VisibleForTesting
public EapMsChapV2FailureRequest(
int msChapV2Id,
int msLength,
int errorCode,
boolean isRetryable,
byte[] challenge,
int passwordChangeProtocol,
String message)
throws EapMsChapV2ParsingException {
super(EAP_MSCHAP_V2_FAILURE, msChapV2Id, msLength);
this.errorCode = errorCode;
this.isRetryable = isRetryable;
this.challenge = challenge;
this.passwordChangeProtocol = passwordChangeProtocol;
this.message = message;
if (challenge.length != CHALLENGE_LENGTH) {
throw new EapMsChapV2ParsingException("Challenge length must be 16B");
}
}
}
/**
* EapMsChapV2FailureResponse represents the EAP MSCHAPv2 Failure Response Packet
* (EAP MSCHAPv2#2.6).
*/
public static class EapMsChapV2FailureResponse extends EapMsChapV2TypeData {
private EapMsChapV2FailureResponse() throws EapMsChapV2ParsingException {
super(EAP_MSCHAP_V2_FAILURE);
}
/**
* Constructs and returns a new EAP MSCHAPv2 Failure Response type data.
*
* @return a new EapMsChapV2FailureResponse instance
*/
public static EapMsChapV2FailureResponse getEapMsChapV2FailureResponse() {
try {
return new EapMsChapV2FailureResponse();
} catch (EapMsChapV2ParsingException ex) {
// This should never happen
LOG.wtf(
EapMsChapV2SuccessResponse.class.getSimpleName(),
"ParsingException thrown while creating EapMsChapV2FailureResponse",
ex);
return null;
}
}
@Override
public byte[] encode() {
return new byte[] {(byte) EAP_MSCHAP_V2_FAILURE};
}
}
@VisibleForTesting
static Map<String, String> getMessageMappings(String message)
throws EapMsChapV2ParsingException {
Map<String, String> messageMappings = new HashMap<>();
int mPos = message.indexOf(MESSAGE_PREFIX);
String preMString;
if (mPos == -1) {
preMString = message;
messageMappings.put(MESSAGE_LABEL, "<omitted by authenticator>");
} else {
preMString = message.substring(0, mPos);
messageMappings.put(MESSAGE_LABEL, message.substring(mPos + MESSAGE_PREFIX.length()));
}
// preMString: "S=<auth string> " or "E=<error> R=r C=<challenge> V=<version> "
for (String value : preMString.split(" ")) {
String[] keyValue = value.split("=");
if (keyValue.length != LABEL_VALUE_LENGTH) {
throw new EapMsChapV2ParsingException(
"Message must be formatted <label character>=<value>");
} else if (messageMappings.containsKey(keyValue[0])) {
throw new EapMsChapV2ParsingException(
"Duplicated key-value pair in message: " + LOG.pii(message));
}
messageMappings.put(keyValue[0], keyValue[1]);
}
return messageMappings;
}
@VisibleForTesting
static byte[] hexStringToByteArray(String hexString)
throws EapMsChapV2ParsingException, NumberFormatException {
if (hexString.length() % 2 != 0) {
throw new EapMsChapV2ParsingException(
"Hex string must contain an even number of characters");
}
byte[] dataBytes = new byte[hexString.length() / 2];
for (int i = 0; i < hexString.length(); i += 2) {
dataBytes[i / 2] = (byte) Integer.parseInt(hexString.substring(i, i + 2), 16);
}
return dataBytes;
}
/** Class for decoding EAP MSCHAPv2 type data. */
public static class EapMsChapV2TypeDataDecoder {
/**
* Returns the EAP MSCHAPv2 Op Code for the given EAP type data.
*
* @param eapTypeData byte[] type data to read the Op Code from
* @return the EAP MSCHAPv2 Op Code for the current type data
* @throws BufferUnderflowException iff eapTypeData.length == 0
*/
public int getOpCode(byte[] eapTypeData) throws BufferUnderflowException {
return Byte.toUnsignedInt(ByteBuffer.wrap(eapTypeData).get());
}
/**
* Decodes and returns an EapMsChapV2ChallengeRequest for the specified eapTypeData.
*
* @param tag String for logging tag
* @param eapTypeData byte[] to be decoded as an EapMsChapV2ChallengeRequest instance
* @return DecodeResult wrapping an EapMsChapV2ChallengeRequest instance for the given
* eapTypeData iff the eapTypeData is formatted correctly. Otherwise, the DecodeResult
* wraps the appropriate EapError.
*/
public DecodeResult<EapMsChapV2ChallengeRequest> decodeChallengeRequest(
String tag, byte[] eapTypeData) {
try {
ByteBuffer buffer = ByteBuffer.wrap(eapTypeData);
int opCode = Byte.toUnsignedInt(buffer.get());
if (opCode != EAP_MSCHAP_V2_CHALLENGE) {
return new DecodeResult<>(
new EapError(
new EapMsChapV2ParsingException(
"Received type data with invalid opCode: "
+ EAP_OP_CODE_STRING.getOrDefault(
opCode, "Unknown (" + opCode + ")"))));
}
return new DecodeResult<>(new EapMsChapV2ChallengeRequest(buffer));
} catch (BufferUnderflowException | EapMsChapV2ParsingException ex) {
LOG.e(tag, "Error parsing EAP MSCHAPv2 Challenge Request type data", ex);
return new DecodeResult<>(new EapError(ex));
}
}
/**
* Decodes and returns an EapMsChapV2SuccessRequest for the specified eapTypeData.
*
* @param tag String for logging tag
* @param eapTypeData byte[] to be decoded as an EapMsChapV2SuccessRequest instance
* @return DecodeResult wrapping an EapMsChapV2SuccessRequest instance for the given
* eapTypeData iff the eapTypeData is formatted correctly. Otherwise, the DecodeResult
* wraps the appropriate EapError.
*/
public DecodeResult<EapMsChapV2SuccessRequest> decodeSuccessRequest(
String tag, byte[] eapTypeData) {
try {
ByteBuffer buffer = ByteBuffer.wrap(eapTypeData);
int opCode = Byte.toUnsignedInt(buffer.get());
if (opCode != EAP_MSCHAP_V2_SUCCESS) {
return new DecodeResult<>(
new EapError(
new EapMsChapV2ParsingException(
"Received type data with invalid opCode: "
+ EAP_OP_CODE_STRING.getOrDefault(
opCode, "Unknown (" + opCode + ")"))));
}
return new DecodeResult<>(new EapMsChapV2SuccessRequest(buffer));
} catch (BufferUnderflowException
| NumberFormatException
| EapMsChapV2ParsingException ex) {
LOG.e(tag, "Error parsing EAP MSCHAPv2 Success Request type data", ex);
return new DecodeResult<>(new EapError(ex));
}
}
/**
* Decodes and returns an EapMsChapV2FailureRequest for the specified eapTypeData.
*
* @param tag String for logging tag
* @param eapTypeData byte[] to be decoded as an EapMsChapV2FailureRequest instance
* @return DecodeResult wrapping an EapMsChapV2FailureRequest instance for the given
* eapTypeData iff the eapTypeData is formatted correctly. Otherwise, the DecodeResult
* wraps the appropriate EapError.
*/
public DecodeResult<EapMsChapV2FailureRequest> decodeFailureRequest(
String tag, byte[] eapTypeData) {
try {
ByteBuffer buffer = ByteBuffer.wrap(eapTypeData);
int opCode = Byte.toUnsignedInt(buffer.get());
if (opCode != EAP_MSCHAP_V2_FAILURE) {
return new DecodeResult<>(
new EapError(
new EapMsChapV2ParsingException(
"Received type data with invalid opCode: "
+ EAP_OP_CODE_STRING.getOrDefault(
opCode, "Unknown (" + opCode + ")"))));
}
return new DecodeResult<>(new EapMsChapV2FailureRequest(buffer));
} catch (BufferUnderflowException
| NumberFormatException
| EapMsChapV2ParsingException ex) {
LOG.e(tag, "Error parsing EAP MSCHAPv2 Failure Request type data", ex);
return new DecodeResult<>(new EapError(ex));
}
}
/**
* DecodeResult represents the result from calling a decode method within
* EapMsChapV2TypeDataDecoder. It will contain either an EapMsChapV2TypeData or an EapError.
*
* @param <T> The EapMsChapV2TypeData type that is wrapped in this DecodeResult
*/
public static class DecodeResult<T extends EapMsChapV2TypeData> {
public final T eapTypeData;
public final EapError eapError;
public DecodeResult(T eapTypeData) {
this.eapTypeData = eapTypeData;
this.eapError = null;
}
public DecodeResult(EapError eapError) {
this.eapTypeData = null;
this.eapError = eapError;
}
/**
* Checks whether this instance represents a successful decode operation.
*
* @return true iff this DecodeResult represents a successfully decoded Type Data
*/
public boolean isSuccessfulDecode() {
return eapTypeData != null;
}
}
}
}