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.message.mschapv2;
18 
19 import static com.android.internal.net.eap.EapAuthenticator.LOG;
20 
21 import com.android.internal.annotations.VisibleForTesting;
22 import com.android.internal.net.eap.EapResult.EapError;
23 import com.android.internal.net.eap.exceptions.mschapv2.EapMsChapV2ParsingException;
24 import com.android.internal.net.eap.message.EapMessage;
25 
26 import java.nio.BufferUnderflowException;
27 import java.nio.ByteBuffer;
28 import java.nio.charset.Charset;
29 import java.util.HashMap;
30 import java.util.HashSet;
31 import java.util.Map;
32 import java.util.Set;
33 
34 /**
35  * EapMsChapV2TypeData represents the Type Data for an {@link EapMessage} during an EAP MSCHAPv2
36  * session.
37  */
38 public class EapMsChapV2TypeData {
39     private static final int LABEL_VALUE_LENGTH = 2;
40     private static final String ASCII_CHARSET_NAME = "US-ASCII";
41     private static final String MESSAGE_PREFIX = "M=";
42     private static final String MESSAGE_LABEL = "M";
43 
44     // EAP MSCHAPv2 OpCode values (EAP MSCHAPv2#2)
45     public static final int EAP_MSCHAP_V2_CHALLENGE = 1;
46     public static final int EAP_MSCHAP_V2_RESPONSE = 2;
47     public static final int EAP_MSCHAP_V2_SUCCESS = 3;
48     public static final int EAP_MSCHAP_V2_FAILURE = 4;
49     public static final int EAP_MSCHAP_V2_CHANGE_PASSWORD = 7;
50 
51     public static final Map<Integer, String> EAP_OP_CODE_STRING = new HashMap<>();
52     static {
EAP_OP_CODE_STRING.put(EAP_MSCHAP_V2_CHALLENGE, "Challenge")53         EAP_OP_CODE_STRING.put(EAP_MSCHAP_V2_CHALLENGE, "Challenge");
EAP_OP_CODE_STRING.put(EAP_MSCHAP_V2_RESPONSE, "Response")54         EAP_OP_CODE_STRING.put(EAP_MSCHAP_V2_RESPONSE, "Response");
EAP_OP_CODE_STRING.put(EAP_MSCHAP_V2_SUCCESS, "Success")55         EAP_OP_CODE_STRING.put(EAP_MSCHAP_V2_SUCCESS, "Success");
EAP_OP_CODE_STRING.put(EAP_MSCHAP_V2_FAILURE, "Failure")56         EAP_OP_CODE_STRING.put(EAP_MSCHAP_V2_FAILURE, "Failure");
EAP_OP_CODE_STRING.put(EAP_MSCHAP_V2_CHANGE_PASSWORD, "Change-Password")57         EAP_OP_CODE_STRING.put(EAP_MSCHAP_V2_CHANGE_PASSWORD, "Change-Password");
58     }
59 
60     private static final Set<Integer> SUPPORTED_OP_CODES = new HashSet<>();
61     static {
62         SUPPORTED_OP_CODES.add(EAP_MSCHAP_V2_CHALLENGE);
63         SUPPORTED_OP_CODES.add(EAP_MSCHAP_V2_RESPONSE);
64         SUPPORTED_OP_CODES.add(EAP_MSCHAP_V2_SUCCESS);
65         SUPPORTED_OP_CODES.add(EAP_MSCHAP_V2_FAILURE);
66     }
67 
68     public final int opCode;
69 
EapMsChapV2TypeData(int opCode)70     EapMsChapV2TypeData(int opCode) throws EapMsChapV2ParsingException {
71         this.opCode = opCode;
72 
73         if (!SUPPORTED_OP_CODES.contains(opCode)) {
74             throw new EapMsChapV2ParsingException("Unsupported opCode provided: " + opCode);
75         }
76     }
77 
78     /**
79      * Encodes this EapMsChapV2TypeData instance as a byte[].
80      *
81      * @return byte[] representing the encoded value of this EapMsChapV2TypeData instance.
82      */
encode()83     public byte[] encode() {
84         throw new UnsupportedOperationException(
85                 "encode() not supported by " + this.getClass().getSimpleName());
86     }
87 
88     abstract static class EapMsChapV2VariableTypeData extends EapMsChapV2TypeData {
89         public final int msChapV2Id;
90         public final int msLength;
91 
EapMsChapV2VariableTypeData(int opCode, int msChapV2Id, int msLength)92         EapMsChapV2VariableTypeData(int opCode, int msChapV2Id, int msLength)
93                 throws EapMsChapV2ParsingException {
94             super(opCode);
95 
96             this.msChapV2Id = msChapV2Id;
97             this.msLength = msLength;
98         }
99     }
100 
101     /**
102      * EapMsChapV2ChallengeRequest represents the EAP MSCHAPv2 Challenge Packet (EAP MSCHAPv2#2.1).
103      */
104     public static class EapMsChapV2ChallengeRequest extends EapMsChapV2VariableTypeData {
105         public static final int VALUE_SIZE = 16;
106         public static final int TYPE_DATA_HEADER_SIZE = 5;
107 
108         public final byte[] challenge = new byte[VALUE_SIZE];
109         public final byte[] name;
110 
EapMsChapV2ChallengeRequest(ByteBuffer buffer)111         EapMsChapV2ChallengeRequest(ByteBuffer buffer) throws EapMsChapV2ParsingException {
112             super(
113                     EAP_MSCHAP_V2_CHALLENGE,
114                     Byte.toUnsignedInt(buffer.get()),
115                     Short.toUnsignedInt(buffer.getShort()));
116 
117             int valueSize = Byte.toUnsignedInt(buffer.get());
118             if (valueSize != VALUE_SIZE) {
119                 throw new EapMsChapV2ParsingException("Challenge Value-Size must be 16");
120             }
121             buffer.get(challenge);
122 
123             int nameLenBytes = msLength - VALUE_SIZE - TYPE_DATA_HEADER_SIZE;
124             if (nameLenBytes < 0) {
125                 throw new EapMsChapV2ParsingException("Invalid MS-Length specified");
126             }
127 
128             name = new byte[nameLenBytes];
129             buffer.get(name);
130         }
131 
132         @VisibleForTesting
EapMsChapV2ChallengeRequest( int msChapV2Id, int msLength, byte[] challenge, byte[] name)133         public EapMsChapV2ChallengeRequest(
134                 int msChapV2Id, int msLength, byte[] challenge, byte[] name)
135                 throws EapMsChapV2ParsingException {
136             super(EAP_MSCHAP_V2_CHALLENGE, msChapV2Id, msLength);
137 
138             if (challenge.length != VALUE_SIZE) {
139                 throw new EapMsChapV2ParsingException("Challenge length must be 16");
140             }
141 
142             System.arraycopy(challenge, 0, this.challenge, 0, VALUE_SIZE);
143             this.name = name;
144         }
145     }
146 
147     /**
148      * EapMsChapV2ChallengeResponse represents the EAP MSCHAPv2 Response Packet (EAP MSCHAPv2#2.2).
149      */
150     public static class EapMsChapV2ChallengeResponse extends EapMsChapV2VariableTypeData {
151         public static final int VALUE_SIZE = 49;
152         public static final int PEER_CHALLENGE_SIZE = 16;
153         public static final int RESERVED_BYTES = 8;
154         public static final int NT_RESPONSE_SIZE = 24;
155         public static final int TYPE_DATA_HEADER_SIZE = 5;
156 
157         public final byte[] peerChallenge = new byte[PEER_CHALLENGE_SIZE];
158         public final byte[] ntResponse = new byte[NT_RESPONSE_SIZE];
159         public final int flags;
160         public final byte[] name;
161 
EapMsChapV2ChallengeResponse( int msChapV2Id, byte[] peerChallenge, byte[] ntResponse, int flags, byte[] name)162         public EapMsChapV2ChallengeResponse(
163                 int msChapV2Id, byte[] peerChallenge, byte[] ntResponse, int flags, byte[] name)
164                 throws EapMsChapV2ParsingException {
165             super(
166                     EAP_MSCHAP_V2_RESPONSE,
167                     msChapV2Id,
168                     TYPE_DATA_HEADER_SIZE + VALUE_SIZE + name.length);
169 
170             if (peerChallenge.length != PEER_CHALLENGE_SIZE) {
171                 throw new EapMsChapV2ParsingException("Peer-Challenge must be 16B");
172             } else if (ntResponse.length != NT_RESPONSE_SIZE) {
173                 throw new EapMsChapV2ParsingException("NT-Response must be 24B");
174             } else if (flags != 0) {
175                 throw new EapMsChapV2ParsingException("Flags must be 0x00");
176             }
177 
178             System.arraycopy(peerChallenge, 0, this.peerChallenge, 0, PEER_CHALLENGE_SIZE);
179             System.arraycopy(ntResponse, 0, this.ntResponse, 0, NT_RESPONSE_SIZE);
180             this.flags = flags;
181             this.name = name;
182         }
183 
184         @Override
encode()185         public byte[] encode() {
186             ByteBuffer buffer = ByteBuffer.allocate(msLength);
187             buffer.put((byte) EAP_MSCHAP_V2_RESPONSE);
188             buffer.put((byte) msChapV2Id);
189             buffer.putShort((short) msLength);
190             buffer.put((byte) VALUE_SIZE);
191             buffer.put(peerChallenge);
192             buffer.put(new byte[RESERVED_BYTES]);
193             buffer.put(ntResponse);
194             buffer.put((byte) flags);
195             buffer.put(name);
196 
197             return buffer.array();
198         }
199     }
200 
201     /**
202      * EapMsChapV2SuccessRequest represents the EAP MSCHAPv2 Success Request Packet
203      * (EAP MSCHAPv2#2.3).
204      */
205     public static class EapMsChapV2SuccessRequest extends EapMsChapV2VariableTypeData {
206         private static final int AUTH_STRING_LEN_HEX = 40;
207         private static final int AUTH_STRING_LEN_BYTES = 20;
208         private static final int NUM_REQUIRED_ATTRIBUTES = 2;
209         private static final String AUTH_STRING_LABEL = "S";
210 
211         public final byte[] authBytes = new byte[AUTH_STRING_LEN_BYTES];
212         public final String message;
213 
EapMsChapV2SuccessRequest(ByteBuffer buffer)214         EapMsChapV2SuccessRequest(ByteBuffer buffer) throws EapMsChapV2ParsingException {
215             super(
216                     EAP_MSCHAP_V2_SUCCESS,
217                     Byte.toUnsignedInt(buffer.get()),
218                     Short.toUnsignedInt(buffer.getShort()));
219 
220             byte[] message = new byte[buffer.remaining()];
221             buffer.get(message);
222 
223             // message formatting: "S=<auth_string> M=<message>"
224             Map<String, String> mappings =
225                     getMessageMappings(new String(message, Charset.forName(ASCII_CHARSET_NAME)));
226 
227             if (!mappings.containsKey(AUTH_STRING_LABEL)
228                     || mappings.size() != NUM_REQUIRED_ATTRIBUTES) {
229                 throw new EapMsChapV2ParsingException(
230                         "Auth message must be in the format: 'S=<auth_string> M=<message>'");
231             }
232 
233             String authStringHex = mappings.get(AUTH_STRING_LABEL);
234             if (authStringHex.length() != AUTH_STRING_LEN_HEX) {
235                 throw new EapMsChapV2ParsingException("Auth String must be 40 hex chars (20B)");
236             }
237             byte[] authBytes = hexStringToByteArray(authStringHex);
238             System.arraycopy(authBytes, 0, this.authBytes, 0, AUTH_STRING_LEN_BYTES);
239 
240             this.message = mappings.get(MESSAGE_LABEL);
241         }
242 
243         @VisibleForTesting
EapMsChapV2SuccessRequest( int msChapV2Id, int msLength, byte[] authBytes, String message)244         public EapMsChapV2SuccessRequest(
245                 int msChapV2Id, int msLength, byte[] authBytes, String message)
246                 throws EapMsChapV2ParsingException {
247             super(EAP_MSCHAP_V2_SUCCESS, msChapV2Id, msLength);
248 
249             if (authBytes.length != AUTH_STRING_LEN_BYTES) {
250                 throw new EapMsChapV2ParsingException("Auth String must be 20B");
251             }
252 
253             System.arraycopy(authBytes, 0, this.authBytes, 0, AUTH_STRING_LEN_BYTES);
254             this.message = message;
255         }
256     }
257 
258     /**
259      * EapMsChapV2SuccessResponse represents the EAP MSCHAPv2 Success Response Packet
260      * (EAP MSCHAPv2#2.4).
261      */
262     public static class EapMsChapV2SuccessResponse extends EapMsChapV2TypeData {
EapMsChapV2SuccessResponse()263         private EapMsChapV2SuccessResponse() throws EapMsChapV2ParsingException {
264             super(EAP_MSCHAP_V2_SUCCESS);
265         }
266 
267         /**
268          * Constructs and returns a new EAP MSCHAPv2 Success Response type data.
269          *
270          * @return a new EapMsChapV2SuccessResponse instance
271          */
getEapMsChapV2SuccessResponse()272         public static EapMsChapV2SuccessResponse getEapMsChapV2SuccessResponse() {
273             try {
274                 return new EapMsChapV2SuccessResponse();
275             } catch (EapMsChapV2ParsingException ex) {
276                 // This should never happen
277                 LOG.wtf(
278                         EapMsChapV2SuccessResponse.class.getSimpleName(),
279                         "ParsingException thrown while creating EapMsChapV2SuccessResponse",
280                         ex);
281                 return null;
282             }
283         }
284 
285         @Override
encode()286         public byte[] encode() {
287             return new byte[] {(byte) EAP_MSCHAP_V2_SUCCESS};
288         }
289     }
290 
291     /**
292      * EapMsChapV2FailureRequest represents the EAP MSCHAPv2 Failure Request Packet
293      * (EAP MSCHAPv2#2.5).
294      */
295     public static class EapMsChapV2FailureRequest extends EapMsChapV2VariableTypeData {
296         private static final int NUM_REQUIRED_ATTRIBUTES = 5;
297         private static final int CHALLENGE_LENGTH = 16;
298         private static final String ERROR_LABEL = "E";
299         private static final String RETRY_LABEL = "R";
300         private static final String IS_RETRYABLE_FLAG = "1";
301         private static final String CHALLENGE_LABEL = "C";
302         private static final String PASSWORD_CHANGE_PROTOCOL_LABEL = "V";
303 
304         // Error codes defined in EAP MSCHAPv2#2.5
305         public static final Map<Integer, String> EAP_ERROR_CODE_STRING = new HashMap<>();
306         static {
307             EAP_ERROR_CODE_STRING.put(646, "ERROR_RESTRICTED_LOGON_HOURS");
308             EAP_ERROR_CODE_STRING.put(647, "ERROR_ACCT_DISABLED");
309             EAP_ERROR_CODE_STRING.put(648, "ERROR_PASSWD_EXPIRED");
310             EAP_ERROR_CODE_STRING.put(649, "ERROR_NO_DIALIN_PERMISSION");
311             EAP_ERROR_CODE_STRING.put(691, "ERROR_AUTHENTICATION_FAILURE");
312             EAP_ERROR_CODE_STRING.put(709, "ERROR_CHANGING_PASSWORD");
313         }
314 
315         public final int errorCode;
316         public final boolean isRetryable;
317         public final byte[] challenge;
318         public final int passwordChangeProtocol;
319         public final String message;
320 
EapMsChapV2FailureRequest(ByteBuffer buffer)321         EapMsChapV2FailureRequest(ByteBuffer buffer)
322                 throws EapMsChapV2ParsingException, NumberFormatException {
323             super(
324                     EAP_MSCHAP_V2_FAILURE,
325                     Byte.toUnsignedInt(buffer.get()),
326                     Short.toUnsignedInt(buffer.getShort()));
327 
328             byte[] message = new byte[buffer.remaining()];
329             buffer.get(message);
330 
331             // message formatting:
332             // "E=<error_code> R=<retry bit> C=<challenge> V=<password_change_protocol> M=<message>"
333             Map<String, String> mappings =
334                     getMessageMappings(new String(message, Charset.forName(ASCII_CHARSET_NAME)));
335             if (!mappings.containsKey(ERROR_LABEL)
336                     || !mappings.containsKey(RETRY_LABEL)
337                     || !mappings.containsKey(CHALLENGE_LABEL)
338                     || !mappings.containsKey(PASSWORD_CHANGE_PROTOCOL_LABEL)
339                     || mappings.size() != NUM_REQUIRED_ATTRIBUTES) {
340                 throw new EapMsChapV2ParsingException(
341                         "Message must be formatted as: E=<error_code> R=<retry bit> C=<challenge>"
342                             + " V=<password_change_protocol> M=<message>");
343             }
344 
345             this.errorCode = Integer.parseInt(mappings.get(ERROR_LABEL));
346             this.isRetryable = IS_RETRYABLE_FLAG.equals(mappings.get(RETRY_LABEL));
347             this.challenge = hexStringToByteArray(mappings.get(CHALLENGE_LABEL));
348             this.passwordChangeProtocol = Integer.parseInt(mappings.get(
349                     PASSWORD_CHANGE_PROTOCOL_LABEL));
350             this.message = mappings.get("M");
351 
352             if (challenge.length != CHALLENGE_LENGTH) {
353                 throw new EapMsChapV2ParsingException("Challenge must be 16B long");
354             }
355         }
356 
357         @VisibleForTesting
EapMsChapV2FailureRequest( int msChapV2Id, int msLength, int errorCode, boolean isRetryable, byte[] challenge, int passwordChangeProtocol, String message)358         public EapMsChapV2FailureRequest(
359                 int msChapV2Id,
360                 int msLength,
361                 int errorCode,
362                 boolean isRetryable,
363                 byte[] challenge,
364                 int passwordChangeProtocol,
365                 String message)
366                 throws EapMsChapV2ParsingException {
367             super(EAP_MSCHAP_V2_FAILURE, msChapV2Id, msLength);
368 
369             this.errorCode = errorCode;
370             this.isRetryable = isRetryable;
371             this.challenge = challenge;
372             this.passwordChangeProtocol = passwordChangeProtocol;
373             this.message = message;
374 
375             if (challenge.length != CHALLENGE_LENGTH) {
376                 throw new EapMsChapV2ParsingException("Challenge length must be 16B");
377             }
378         }
379     }
380 
381     /**
382      * EapMsChapV2FailureResponse represents the EAP MSCHAPv2 Failure Response Packet
383      * (EAP MSCHAPv2#2.6).
384      */
385     public static class EapMsChapV2FailureResponse extends EapMsChapV2TypeData {
EapMsChapV2FailureResponse()386         private EapMsChapV2FailureResponse() throws EapMsChapV2ParsingException {
387             super(EAP_MSCHAP_V2_FAILURE);
388         }
389 
390         /**
391          * Constructs and returns a new EAP MSCHAPv2 Failure Response type data.
392          *
393          * @return a new EapMsChapV2FailureResponse instance
394          */
getEapMsChapV2FailureResponse()395         public static EapMsChapV2FailureResponse getEapMsChapV2FailureResponse() {
396             try {
397                 return new EapMsChapV2FailureResponse();
398             } catch (EapMsChapV2ParsingException ex) {
399                 // This should never happen
400                 LOG.wtf(
401                         EapMsChapV2SuccessResponse.class.getSimpleName(),
402                         "ParsingException thrown while creating EapMsChapV2FailureResponse",
403                         ex);
404                 return null;
405             }
406         }
407 
408         @Override
encode()409         public byte[] encode() {
410             return new byte[] {(byte) EAP_MSCHAP_V2_FAILURE};
411         }
412     }
413 
414     @VisibleForTesting
getMessageMappings(String message)415     static Map<String, String> getMessageMappings(String message)
416             throws EapMsChapV2ParsingException {
417         Map<String, String> messageMappings = new HashMap<>();
418         int mPos = message.indexOf(MESSAGE_PREFIX);
419 
420         String preMString;
421         if (mPos == -1) {
422             preMString = message;
423             messageMappings.put(MESSAGE_LABEL, "<omitted by authenticator>");
424         } else {
425             preMString = message.substring(0, mPos);
426             messageMappings.put(MESSAGE_LABEL, message.substring(mPos + MESSAGE_PREFIX.length()));
427         }
428 
429         // preMString: "S=<auth string> " or "E=<error> R=r C=<challenge> V=<version> "
430         for (String value : preMString.split(" ")) {
431             String[] keyValue = value.split("=");
432             if (keyValue.length != LABEL_VALUE_LENGTH) {
433                 throw new EapMsChapV2ParsingException(
434                         "Message must be formatted <label character>=<value>");
435             } else if (messageMappings.containsKey(keyValue[0])) {
436                 throw new EapMsChapV2ParsingException(
437                         "Duplicated key-value pair in message: " + LOG.pii(message));
438             }
439             messageMappings.put(keyValue[0], keyValue[1]);
440         }
441 
442         return messageMappings;
443     }
444 
445     @VisibleForTesting
hexStringToByteArray(String hexString)446     static byte[] hexStringToByteArray(String hexString)
447             throws EapMsChapV2ParsingException, NumberFormatException {
448         if (hexString.length() % 2 != 0) {
449             throw new EapMsChapV2ParsingException(
450                     "Hex string must contain an even number of characters");
451         }
452 
453         byte[] dataBytes = new byte[hexString.length() / 2];
454         for (int i = 0; i < hexString.length(); i += 2) {
455             dataBytes[i / 2] = (byte) Integer.parseInt(hexString.substring(i, i + 2), 16);
456         }
457         return dataBytes;
458     }
459 
460     /** Class for decoding EAP MSCHAPv2 type data. */
461     public static class EapMsChapV2TypeDataDecoder {
462         /**
463          * Returns the EAP MSCHAPv2 Op Code for the given EAP type data.
464          *
465          * @param eapTypeData byte[] type data to read the Op Code from
466          * @return the EAP MSCHAPv2 Op Code for the current type data
467          * @throws BufferUnderflowException iff eapTypeData.length == 0
468          */
getOpCode(byte[] eapTypeData)469         public int getOpCode(byte[] eapTypeData) throws BufferUnderflowException {
470             return Byte.toUnsignedInt(ByteBuffer.wrap(eapTypeData).get());
471         }
472 
473         /**
474          * Decodes and returns an EapMsChapV2ChallengeRequest for the specified eapTypeData.
475          *
476          * @param tag String for logging tag
477          * @param eapTypeData byte[] to be decoded as an EapMsChapV2ChallengeRequest instance
478          * @return DecodeResult wrapping an EapMsChapV2ChallengeRequest instance for the given
479          *     eapTypeData iff the eapTypeData is formatted correctly. Otherwise, the DecodeResult
480          *     wraps the appropriate EapError.
481          */
decodeChallengeRequest( String tag, byte[] eapTypeData)482         public DecodeResult<EapMsChapV2ChallengeRequest> decodeChallengeRequest(
483                 String tag, byte[] eapTypeData) {
484             try {
485                 ByteBuffer buffer = ByteBuffer.wrap(eapTypeData);
486                 int opCode = Byte.toUnsignedInt(buffer.get());
487 
488                 if (opCode != EAP_MSCHAP_V2_CHALLENGE) {
489                     return new DecodeResult<>(
490                             new EapError(
491                                     new EapMsChapV2ParsingException(
492                                             "Received type data with invalid opCode: "
493                                                     + EAP_OP_CODE_STRING.getOrDefault(
494                                                             opCode, "Unknown (" + opCode + ")"))));
495                 }
496 
497                 return new DecodeResult<>(new EapMsChapV2ChallengeRequest(buffer));
498             } catch (BufferUnderflowException | EapMsChapV2ParsingException ex) {
499                 LOG.e(tag, "Error parsing EAP MSCHAPv2 Challenge Request type data", ex);
500                 return new DecodeResult<>(new EapError(ex));
501             }
502         }
503 
504         /**
505          * Decodes and returns an EapMsChapV2SuccessRequest for the specified eapTypeData.
506          *
507          * @param tag String for logging tag
508          * @param eapTypeData byte[] to be decoded as an EapMsChapV2SuccessRequest instance
509          * @return DecodeResult wrapping an EapMsChapV2SuccessRequest instance for the given
510          *     eapTypeData iff the eapTypeData is formatted correctly. Otherwise, the DecodeResult
511          *     wraps the appropriate EapError.
512          */
decodeSuccessRequest( String tag, byte[] eapTypeData)513         public DecodeResult<EapMsChapV2SuccessRequest> decodeSuccessRequest(
514                 String tag, byte[] eapTypeData) {
515             try {
516                 ByteBuffer buffer = ByteBuffer.wrap(eapTypeData);
517                 int opCode = Byte.toUnsignedInt(buffer.get());
518 
519                 if (opCode != EAP_MSCHAP_V2_SUCCESS) {
520                     return new DecodeResult<>(
521                             new EapError(
522                                     new EapMsChapV2ParsingException(
523                                             "Received type data with invalid opCode: "
524                                                     + EAP_OP_CODE_STRING.getOrDefault(
525                                                             opCode, "Unknown (" + opCode + ")"))));
526                 }
527 
528                 return new DecodeResult<>(new EapMsChapV2SuccessRequest(buffer));
529             } catch (BufferUnderflowException
530                     | NumberFormatException
531                     | EapMsChapV2ParsingException ex) {
532                 LOG.e(tag, "Error parsing EAP MSCHAPv2 Success Request type data", ex);
533                 return new DecodeResult<>(new EapError(ex));
534             }
535         }
536 
537         /**
538          * Decodes and returns an EapMsChapV2FailureRequest for the specified eapTypeData.
539          *
540          * @param tag String for logging tag
541          * @param eapTypeData byte[] to be decoded as an EapMsChapV2FailureRequest instance
542          * @return DecodeResult wrapping an EapMsChapV2FailureRequest instance for the given
543          *     eapTypeData iff the eapTypeData is formatted correctly. Otherwise, the DecodeResult
544          *     wraps the appropriate EapError.
545          */
decodeFailureRequest( String tag, byte[] eapTypeData)546         public DecodeResult<EapMsChapV2FailureRequest> decodeFailureRequest(
547                 String tag, byte[] eapTypeData) {
548             try {
549                 ByteBuffer buffer = ByteBuffer.wrap(eapTypeData);
550                 int opCode = Byte.toUnsignedInt(buffer.get());
551 
552                 if (opCode != EAP_MSCHAP_V2_FAILURE) {
553                     return new DecodeResult<>(
554                             new EapError(
555                                     new EapMsChapV2ParsingException(
556                                             "Received type data with invalid opCode: "
557                                                     + EAP_OP_CODE_STRING.getOrDefault(
558                                                             opCode, "Unknown (" + opCode + ")"))));
559                 }
560 
561                 return new DecodeResult<>(new EapMsChapV2FailureRequest(buffer));
562             } catch (BufferUnderflowException
563                     | NumberFormatException
564                     | EapMsChapV2ParsingException ex) {
565                 LOG.e(tag, "Error parsing EAP MSCHAPv2 Failure Request type data", ex);
566                 return new DecodeResult<>(new EapError(ex));
567             }
568         }
569 
570         /**
571          * DecodeResult represents the result from calling a decode method within
572          * EapMsChapV2TypeDataDecoder. It will contain either an EapMsChapV2TypeData or an EapError.
573          *
574          * @param <T> The EapMsChapV2TypeData type that is wrapped in this DecodeResult
575          */
576         public static class DecodeResult<T extends EapMsChapV2TypeData> {
577             public final T eapTypeData;
578             public final EapError eapError;
579 
DecodeResult(T eapTypeData)580             public DecodeResult(T eapTypeData) {
581                 this.eapTypeData = eapTypeData;
582                 this.eapError = null;
583             }
584 
DecodeResult(EapError eapError)585             public DecodeResult(EapError eapError) {
586                 this.eapTypeData = null;
587                 this.eapError = eapError;
588             }
589 
590             /**
591              * Checks whether this instance represents a successful decode operation.
592              *
593              * @return true iff this DecodeResult represents a successfully decoded Type Data
594              */
isSuccessfulDecode()595             public boolean isSuccessfulDecode() {
596                 return eapTypeData != null;
597             }
598         }
599     }
600 }
601