diff options
6 files changed, 350 insertions, 45 deletions
diff --git a/src/java/com/android/ike/ikev2/message/IkeHeader.java b/src/java/com/android/ike/ikev2/message/IkeHeader.java index 851442b9..d2277cef 100644 --- a/src/java/com/android/ike/ikev2/message/IkeHeader.java +++ b/src/java/com/android/ike/ikev2/message/IkeHeader.java @@ -138,7 +138,7 @@ public final class IkeHeader { } /** Validate syntax and major version. */ - public void validate() throws IkeException { + public void checkValidOrThrow(int packetLength) throws IkeException { if (majorVersion > 2) { // Receive higher version of protocol. Stop parsing. throw new InvalidMajorVersionException(majorVersion); @@ -154,6 +154,9 @@ public final class IkeHeader { || exchangeType > EXCHANGE_TYPE_INFORMATIONAL) { throw new InvalidSyntaxException("Invalid IKE Exchange Type."); } + if (messageLength != packetLength) { + throw new InvalidSyntaxException("Invalid IKE Message Length."); + } } /** Encode IKE header to ByteBuffer */ diff --git a/src/java/com/android/ike/ikev2/message/IkeMessage.java b/src/java/com/android/ike/ikev2/message/IkeMessage.java index 0261ec2b..17bd5c0b 100644 --- a/src/java/com/android/ike/ikev2/message/IkeMessage.java +++ b/src/java/com/android/ike/ikev2/message/IkeMessage.java @@ -25,12 +25,18 @@ import com.android.ike.ikev2.exceptions.InvalidSyntaxException; import com.android.ike.ikev2.exceptions.UnsupportedCriticalPayloadException; import com.android.org.bouncycastle.jce.provider.BouncyCastleProvider; +import java.io.IOException; import java.nio.BufferUnderflowException; import java.nio.ByteBuffer; import java.security.Provider; +import java.util.Arrays; import java.util.LinkedList; import java.util.List; +import javax.crypto.Cipher; +import javax.crypto.Mac; +import javax.crypto.SecretKey; + /** * IkeMessage represents an IKE message. * @@ -60,22 +66,66 @@ public final class IkeMessage { } /** - * Decode unenrypted IKE message body and create an instance of IkeMessage. + * Decrypt and decode encrypted IKE message body and create an instance of IkeMessage. + * + * @param header the IKE header that is decoded but not validated. + * @param inputPacket the byte array containing the whole IKE message. + * @param integrityMac the initialized Message Authentication Code (MAC) for integrity check. + * @param checksumLen the length of integrity checksum. + * @param decryptCipher the uninitialized Cipher for doing decryption. + * @param dKey the decryption key. + * @param ivLen the length of Initialization Vector. + * @return the IkeMessage instance. + * @throws IkeException if there is any protocol error. + * @throws IOException if there is any error during integrity check or decryption. + */ + public static IkeMessage decode( + IkeHeader header, + byte[] inputPacket, + Mac integrityMac, + int checksumLen, + Cipher decryptCipher, + SecretKey dKey, + int ivLen) + throws IkeException, IOException { + + header.checkValidOrThrow(inputPacket.length); + + Pair<IkeSkPayload, Integer> pair = + IkePayloadFactory.getIkeSkPayload( + inputPacket, integrityMac, checksumLen, decryptCipher, dKey, ivLen); + IkeSkPayload skPayload = pair.first; + int firstPayloadType = pair.second; + + List<IkePayload> supportedPayloadList = + decodePayloadList(firstPayloadType, skPayload.unencryptedPayloads); + return new IkeMessage(header, supportedPayloadList); + } + + /** + * Decode unencrypted IKE message body and create an instance of IkeMessage. * - * @param header the IKE header that is decoded but not validated - * @param inputPacket the byte array contains the whole IKE message - * @throws IkeException if there is any error + * @param header the IKE header that is decoded but not validated. + * @param inputPacket the byte array contains the whole IKE message. + * @return the IkeMessage instance. + * @throws IkeException if there is any protocol error. */ public static IkeMessage decode(IkeHeader header, byte[] inputPacket) throws IkeException { - header.validate(); + header.checkValidOrThrow(inputPacket.length); + + byte[] unencryptedPayloads = + Arrays.copyOfRange(inputPacket, IkeHeader.IKE_HEADER_LENGTH, inputPacket.length); - ByteBuffer inputBuffer = - ByteBuffer.wrap( - inputPacket, - IkeHeader.IKE_HEADER_LENGTH, - inputPacket.length - IkeHeader.IKE_HEADER_LENGTH); - @PayloadType int currentPayloadType = header.nextPayloadType; + List<IkePayload> supportedPayloadList = + decodePayloadList(header.nextPayloadType, unencryptedPayloads); + return new IkeMessage(header, supportedPayloadList); + } + + private static List<IkePayload> decodePayloadList( + @PayloadType int firstPayloadType, byte[] unencryptedPayloads) throws IkeException { + ByteBuffer inputBuffer = ByteBuffer.wrap(unencryptedPayloads); + int currentPayloadType = firstPayloadType; // For supported payload List<IkePayload> supportedPayloadList = new LinkedList<>(); // For unsupported critical payload @@ -96,14 +146,23 @@ public final class IkeMessage { currentPayloadType = pair.second; } catch (NegativeArraySizeException | BufferUnderflowException e) { + // TODO: b/119791832. Add length check in each payload before getting data from + // ByteBuffer. + + // Invalid length error when parsing payload bodies. throw new InvalidSyntaxException("Malformed IKE Payload"); } } + if (inputBuffer.remaining() > 0) { + throw new InvalidSyntaxException( + "Malformed IKE Payload: Unexpected bytes at the end of packet."); + } + if (unsupportedCriticalPayloadList.size() > 0) { throw new UnsupportedCriticalPayloadException(unsupportedCriticalPayloadList); } - return new IkeMessage(header, supportedPayloadList); + return supportedPayloadList; } static Provider getSecurityProvider() { diff --git a/src/java/com/android/ike/ikev2/message/IkePayloadFactory.java b/src/java/com/android/ike/ikev2/message/IkePayloadFactory.java index 86edb585..ced76d1b 100644 --- a/src/java/com/android/ike/ikev2/message/IkePayloadFactory.java +++ b/src/java/com/android/ike/ikev2/message/IkePayloadFactory.java @@ -19,10 +19,16 @@ package com.android.ike.ikev2.message; import android.util.Pair; import com.android.ike.ikev2.exceptions.IkeException; +import com.android.ike.ikev2.exceptions.InvalidSyntaxException; import com.android.internal.annotations.VisibleForTesting; +import java.io.IOException; import java.nio.ByteBuffer; +import javax.crypto.Cipher; +import javax.crypto.Mac; +import javax.crypto.SecretKey; + /** * IkePayloadFactory is used for creating IkePayload according to is type. * @@ -31,6 +37,14 @@ import java.nio.ByteBuffer; */ final class IkePayloadFactory { + // Critical bit is set and following reserved 7 bits are unset. + private static final byte PAYLOAD_HEADER_CRITICAL_BIT_SET = (byte) 0x80; + + private static boolean isCriticalPayload(byte flagByte) { + // Reserved 7 bits following critical bit must be ignore on receipt. + return (flagByte & PAYLOAD_HEADER_CRITICAL_BIT_SET) == PAYLOAD_HEADER_CRITICAL_BIT_SET; + } + /** Default instance used for constructing IkePayload */ @VisibleForTesting static IkePayloadDecoder sDecoderInstance = @@ -68,15 +82,25 @@ final class IkePayloadFactory { * IkePayload.PayloadType} * @param input the encoded IKE message body containing all payloads. Position of it will * increment. + * @return a Pair including IkePayload and next payload type. */ - static Pair<IkePayload, Integer> getIkePayload(int payloadType, ByteBuffer input) + protected static Pair<IkePayload, Integer> getIkePayload(int payloadType, ByteBuffer input) throws IkeException { int nextPayloadType = (int) input.get(); // read critical bit - boolean isCritical = ((input.get() & 0x80) == 0x80); + boolean isCritical = isCriticalPayload(input.get()); int payloadLength = Short.toUnsignedInt(input.getShort()); + if (payloadLength <= IkePayload.GENERIC_HEADER_LENGTH) { + throw new InvalidSyntaxException( + "Invalid Payload Length: Payload length is too short."); + } int bodyLength = payloadLength - IkePayload.GENERIC_HEADER_LENGTH; + if (bodyLength > input.remaining()) { + // It is not clear whether previous payloads or current payload has invalid payload + // length. + throw new InvalidSyntaxException("Invalid Payload Length: Payload length is too long."); + } byte[] payloadBody = new byte[bodyLength]; input.get(payloadBody); @@ -85,6 +109,59 @@ final class IkePayloadFactory { return new Pair(payload, nextPayloadType); } + /** + * Construct an instance of IkeSkPayload by decrypting the received message. + * + * @param message the byte array contains the whole IKE message. + * @param integrityMac the initialized Mac for integrity check. + * @param checksumLen the length of integrity checksum. + * @param decryptCipher the uninitialized Cipher for doing decryption. + * @param dKey the decryption key. + * @param ivLen the length of Initialization Vector. + * @return a pair including IkePayload and next payload type. + * @throws IOException + */ + protected static Pair<IkeSkPayload, Integer> getIkeSkPayload( + byte[] message, + Mac integrityMac, + int checksumLen, + Cipher decryptCipher, + SecretKey dKey, + int ivLen) + throws IkeException { + ByteBuffer input = + ByteBuffer.wrap( + message, + IkeHeader.IKE_HEADER_LENGTH, + message.length - IkeHeader.IKE_HEADER_LENGTH); + + int nextPayloadType = (int) input.get(); + // read critical bit + boolean isCritical = isCriticalPayload(input.get()); + + int payloadLength = Short.toUnsignedInt(input.getShort()); + + int bodyLength = message.length - IkeHeader.IKE_HEADER_LENGTH; + if (bodyLength < payloadLength) { + throw new InvalidSyntaxException( + "Invalid length of SK Payload: Payload length is too long."); + } else if (bodyLength > payloadLength) { + // According to RFC 7296, SK Payload must be the last payload and for CREATE_CHILD_SA, + // IKE_AUTH and INFORMATIONAL exchanges, message following the header is encrypted. Thus + // this implementaion only accepts that SK Payload to be the only payload. Any IKE + // packet violating this format will be treated as invalid. A request violating this + // format will be rejected and replied with an error notification. + throw new InvalidSyntaxException( + "Invalid length of SK Payload: Payload length is too short" + + " or SK Payload is not the only payload."); + } + + IkeSkPayload payload = + new IkeSkPayload( + isCritical, message, integrityMac, checksumLen, decryptCipher, dKey, ivLen); + return new Pair(payload, nextPayloadType); + } + @VisibleForTesting interface IkePayloadDecoder { IkePayload decodeIkePayload(int payloadType, boolean isCritical, byte[] payloadBody) diff --git a/src/java/com/android/ike/ikev2/message/IkeSkPayload.java b/src/java/com/android/ike/ikev2/message/IkeSkPayload.java new file mode 100644 index 00000000..65c021a7 --- /dev/null +++ b/src/java/com/android/ike/ikev2/message/IkeSkPayload.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2018 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.ikev2.message; + +import com.android.ike.ikev2.message.IkePayload.PayloadType; + +import java.nio.ByteBuffer; + +import javax.crypto.Cipher; +import javax.crypto.Mac; +import javax.crypto.SecretKey; + +/** + * IkeSkPayload represents a Encrypted Payload. + * + * <p>It contains other payloads in encrypted form. It is must be the last payload in the message. + * It should be the only payload in this implementation. + * + * <p>Critical bit must be ignored when doing decoding. + * + * @see <a href="https://tools.ietf.org/html/rfc7296#page-105">RFC 7296, Internet Key Exchange + * Protocol Version 2 (IKEv2). + */ +public final class IkeSkPayload extends IkePayload { + + public final byte[] unencryptedPayloads; + + /** + * Construct an instance of IkeSkPayload in the context of {@link IkePayloadFactory}. + * + * @param critical indicates if it is a critical payload. + * @param message the byte array contains the whole IKE message. + * @param integrityMac the initialized Mac for integrity check. + * @param checksumLen the length of integrity checksum. + * @param decryptCipher the uninitialized Cipher for doing decryption. + * @param dKey the decryption key. + * @param ivLen the length of Initialization Vector. + */ + IkeSkPayload( + boolean critical, + byte[] message, + Mac integrityMac, + int checksumLen, + Cipher decryptCipher, + SecretKey dKey, + int ivLen) { + super(PAYLOAD_TYPE_SK, critical); + // TODO:Check integrity and decrypt SkPayload body. + throw new UnsupportedOperationException("It is not supported to construct a SkPayload."); + } + + //TODO: Add another constructor for AEAD protected payload. + + /** + * Throw an Exception when trying to encode this payload. + * + * @throws UnsupportedOperationException for this payload. + */ + @Override + protected void encodeToByteBuffer(@PayloadType int nextPayload, ByteBuffer byteBuffer) { + // TODO: Implement thie method + throw new UnsupportedOperationException( + "It is not supported to encode a " + getTypeString()); + } + + /** + * Get entire payload length. + * + * @return entire payload length. + */ + @Override + protected int getPayloadLength() { + // TODO: Implement thie method + throw new UnsupportedOperationException( + "It is not supported to get length of a " + getTypeString()); + } + + /** + * Return the payload type as a String. + * + * @return the payload type as a String. + */ + @Override + public String getTypeString() { + return "Encrypted and Authenticated Payload"; + } +} diff --git a/tests/iketests/src/java/com/android/ike/ikev2/message/IkeHeaderTest.java b/tests/iketests/src/java/com/android/ike/ikev2/message/IkeHeaderTest.java index 70b2d651..1bc52c3b 100644 --- a/tests/iketests/src/java/com/android/ike/ikev2/message/IkeHeaderTest.java +++ b/tests/iketests/src/java/com/android/ike/ikev2/message/IkeHeaderTest.java @@ -20,6 +20,10 @@ import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import com.android.ike.ikev2.exceptions.InvalidMajorVersionException; +import com.android.ike.ikev2.exceptions.InvalidSyntaxException; import org.junit.Test; @@ -59,6 +63,13 @@ public final class IkeHeaderTest { private static final int IKE_MSG_ID = 0; private static final int IKE_MSG_LENGTH = 336; + // Byte offsets of version field in IKE message header. + private static final int VERSION_OFFSET = 17; + // Byte offsets of exchange type in IKE message header. + private static final int EXCHANGE_TYPE_OFFSET = 18; + // Byte offsets of message length in IKE message header. + private static final int MESSAGE_LENGTH_OFFSET = 24; + @Test public void testDecodeIkeHeader() throws Exception { byte[] inputPacket = TestUtils.hexStringToByteArray(IKE_SA_INIT_RAW_PACKET); @@ -82,6 +93,50 @@ public final class IkeHeaderTest { } @Test + public void testDecodeIkeHeaderWithInvalidMajorVersion() throws Exception { + byte[] inputPacket = TestUtils.hexStringToByteArray(IKE_SA_INIT_RAW_PACKET); + // Set major version 3. + inputPacket[VERSION_OFFSET] = (byte) 0x30; + // Set Exchange type 0 + inputPacket[EXCHANGE_TYPE_OFFSET] = (byte) 0x00; + IkeHeader header = new IkeHeader(inputPacket); + try { + IkeMessage.decode(header, inputPacket); + fail( + "Expected InvalidMajorVersionException: major version is 3" + + "and exchange type is 0"); + } catch (InvalidMajorVersionException expected) { + assertEquals(3, expected.receivedMajorVersion); + } + } + + @Test + public void testDecodeIkeHeaderWithInvalidExchangeType() throws Exception { + byte[] inputPacket = TestUtils.hexStringToByteArray(IKE_SA_INIT_RAW_PACKET); + // Set Exchange type 0 + inputPacket[EXCHANGE_TYPE_OFFSET] = (byte) 0x00; + IkeHeader header = new IkeHeader(inputPacket); + try { + IkeMessage.decode(header, inputPacket); + fail("Expected InvalidSyntaxException: exchange type is 0"); + } catch (InvalidSyntaxException expected) { + } + } + + @Test + public void testDecodeIkeHeaderWithInvalidPacketLength() throws Exception { + byte[] inputPacket = TestUtils.hexStringToByteArray(IKE_SA_INIT_RAW_PACKET); + // Set Exchange type 0 + inputPacket[MESSAGE_LENGTH_OFFSET] = (byte) 0x01; + IkeHeader header = new IkeHeader(inputPacket); + try { + IkeMessage.decode(header, inputPacket); + fail("Expected InvalidSyntaxException: IKE message length."); + } catch (InvalidSyntaxException expected) { + } + } + + @Test public void testEncodeIkeHeader() throws Exception { byte[] inputPacket = TestUtils.hexStringToByteArray(IKE_SA_INIT_RAW_PACKET); IkeHeader header = new IkeHeader(inputPacket); diff --git a/tests/iketests/src/java/com/android/ike/ikev2/message/IkeMessageTest.java b/tests/iketests/src/java/com/android/ike/ikev2/message/IkeMessageTest.java index e2a1c9e1..8ba35560 100644 --- a/tests/iketests/src/java/com/android/ike/ikev2/message/IkeMessageTest.java +++ b/tests/iketests/src/java/com/android/ike/ikev2/message/IkeMessageTest.java @@ -21,7 +21,6 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; import com.android.ike.ikev2.exceptions.IkeException; -import com.android.ike.ikev2.exceptions.InvalidMajorVersionException; import com.android.ike.ikev2.exceptions.InvalidSyntaxException; import com.android.ike.ikev2.exceptions.UnsupportedCriticalPayloadException; @@ -50,10 +49,14 @@ public final class IkeMessageTest { private static final String IKE_SA_INIT_RAW_PACKET = IKE_SA_INIT_HEADER_RAW_PACKET + IKE_SA_INIT_BODY_RAW_PACKET; - private static final int FIRST_PAYLOAD_TYPE_POSITION = 16; - private static final int VERSION_POSITION = 17; - private static final int EXCHANGE_TYPE_POSITION = 18; - private static final int PAYLOAD_CRITICAL_BIT_POSITION = 1; + // Byte offsets of first payload type in IKE message header. + private static final int FIRST_PAYLOAD_TYPE_OFFSET = 16; + // Byte offsets of first payload's critical bit in IKE message body. + private static final int PAYLOAD_CRITICAL_BIT_OFFSET = 1; + // Byte offsets of first payload length in IKE message body. + private static final int FIRST_PAYLOAD_LENGTH_OFFSET = 2; + // Byte offsets of last payload length in IKE message body. + private static final int LAST_PAYLOAD_LENGTH_OFFSET = 278; private static final int[] SUPPORTED_PAYLOAD_LIST = { IkePayload.PAYLOAD_TYPE_SA, @@ -120,7 +123,7 @@ public final class IkeMessageTest { public void testDecodeMessageWithUnsupportedUncriticalPayload() throws Exception { byte[] inputPacket = TestUtils.hexStringToByteArray(IKE_SA_INIT_RAW_PACKET); // Set first payload unsupported uncritical - inputPacket[FIRST_PAYLOAD_TYPE_POSITION] = (byte) 0xff; + inputPacket[FIRST_PAYLOAD_TYPE_OFFSET] = (byte) 0xff; IkeHeader header = new IkeHeader(inputPacket); IkeMessage message = IkeMessage.decode(header, inputPacket); assertEquals(SUPPORTED_PAYLOAD_LIST.length - 1, message.ikePayloadList.size()); @@ -130,51 +133,58 @@ public final class IkeMessageTest { } @Test - public void testThrowInvalidMajorVersionException() throws Exception { + public void testThrowUnsupportedCriticalPayloadException() throws Exception { byte[] inputPacket = TestUtils.hexStringToByteArray(IKE_SA_INIT_RAW_PACKET); - // Set major version 3. - inputPacket[VERSION_POSITION] = (byte) 0x30; - // Set Exchange type 0 - inputPacket[EXCHANGE_TYPE_POSITION] = (byte) 0x00; + // Set first payload unsupported critical + inputPacket[FIRST_PAYLOAD_TYPE_OFFSET] = (byte) 0xff; + inputPacket[IkeHeader.IKE_HEADER_LENGTH + PAYLOAD_CRITICAL_BIT_OFFSET] = (byte) 0x80; + IkeHeader header = new IkeHeader(inputPacket); try { IkeMessage.decode(header, inputPacket); fail( - "Expected InvalidMajorVersionException: major version is 3" - + "and packet length is 0"); - } catch (InvalidMajorVersionException expected) { - assertEquals(3, expected.receivedMajorVersion); + "Expected UnsupportedCriticalPayloadException: first" + + "payload is unsupported critical."); + } catch (UnsupportedCriticalPayloadException expected) { + assertEquals(1, expected.payloadTypeList.size()); } } @Test - public void testThrowInvalidSyntaxException() throws Exception { + public void testDecodeMessageWithTooShortPayloadLength() throws Exception { byte[] inputPacket = TestUtils.hexStringToByteArray(IKE_SA_INIT_RAW_PACKET); - // Set Exchange type 0 - inputPacket[EXCHANGE_TYPE_POSITION] = (byte) 0x00; + // Set first payload length to 0 + inputPacket[IkeHeader.IKE_HEADER_LENGTH + FIRST_PAYLOAD_LENGTH_OFFSET] = (byte) 0; + inputPacket[IkeHeader.IKE_HEADER_LENGTH + FIRST_PAYLOAD_LENGTH_OFFSET + 1] = (byte) 0; IkeHeader header = new IkeHeader(inputPacket); try { - IkeMessage.decode(header, inputPacket); - fail("Expected InvalidSyntaxException: packet length is 0"); + IkeMessage message = IkeMessage.decode(header, inputPacket); + fail("Expected InvalidSyntaxException: Payload length is too short."); } catch (InvalidSyntaxException expected) { } } @Test - public void testThrowUnsupportedCriticalPayloadException() throws Exception { + public void testDecodeMessageWithTooLongPayloadLength() throws Exception { byte[] inputPacket = TestUtils.hexStringToByteArray(IKE_SA_INIT_RAW_PACKET); - // Set first payload unsupported critical - inputPacket[FIRST_PAYLOAD_TYPE_POSITION] = (byte) 0xff; - inputPacket[IkeHeader.IKE_HEADER_LENGTH + PAYLOAD_CRITICAL_BIT_POSITION] = (byte) 0x80; + // Increase last payload length by one byte + inputPacket[IkeHeader.IKE_HEADER_LENGTH + LAST_PAYLOAD_LENGTH_OFFSET]++; + IkeHeader header = new IkeHeader(inputPacket); + try { + IkeMessage message = IkeMessage.decode(header, inputPacket); + fail("Expected InvalidSyntaxException: Payload length is too long."); + } catch (InvalidSyntaxException expected) { + } + } + @Test + public void testDecodeMessageWithExpectedBytesInTheEnd() throws Exception { + byte[] inputPacket = TestUtils.hexStringToByteArray(IKE_SA_INIT_RAW_PACKET + "0000"); IkeHeader header = new IkeHeader(inputPacket); try { - IkeMessage.decode(header, inputPacket); - fail( - "Expected UnsupportedCriticalPayloadException: first" - + "payload is unsupported critical."); - } catch (UnsupportedCriticalPayloadException expected) { - assertEquals(1, expected.payloadTypeList.size()); + IkeMessage message = IkeMessage.decode(header, inputPacket); + fail("Expected InvalidSyntaxException: Unexpected bytes in the end of packet."); + } catch (InvalidSyntaxException expected) { } } |