diff options
31 files changed, 2322 insertions, 328 deletions
@@ -20,7 +20,7 @@ LOCAL_AIDL_INCLUDES := $(LOCAL_PATH)/src/java LOCAL_SRC_FILES := \ $(call all-java-files-under, src/java) -LOCAL_JAVA_LIBRARIES := bouncycastle +LOCAL_JAVA_LIBRARIES := bouncycastle NetworkStackBase LOCAL_MODULE_TAGS := optional LOCAL_MODULE := ike diff --git a/src/java/com/android/ike/ikev2/ChildSessionOptions.java b/src/java/com/android/ike/ikev2/ChildSessionOptions.java index cbca9f46..b311c666 100644 --- a/src/java/com/android/ike/ikev2/ChildSessionOptions.java +++ b/src/java/com/android/ike/ikev2/ChildSessionOptions.java @@ -20,6 +20,6 @@ package com.android.ike.ikev2; * ChildSessionOptions contains user-provided Child SA proposals and negotiated Child SA * information. */ -public class ChildSessionOptions { +public final class ChildSessionOptions { // TODO: Implement it. } diff --git a/src/java/com/android/ike/ikev2/IkeSessionOptions.java b/src/java/com/android/ike/ikev2/IkeSessionOptions.java index 66ef94d6..9f2094f2 100644 --- a/src/java/com/android/ike/ikev2/IkeSessionOptions.java +++ b/src/java/com/android/ike/ikev2/IkeSessionOptions.java @@ -16,9 +16,107 @@ package com.android.ike.ikev2; +import android.net.IpSecManager.UdpEncapsulationSocket; + +import com.android.ike.ikev2.message.IkePayload; + +import java.net.InetAddress; +import java.util.LinkedList; +import java.util.List; + /** - * IkeSessionOptions contains all configurations including cryptographic algorithm set of an IKE SA. + * IkeSessionOptions contains all user provided configurations for negotiating an IKE SA. + * + * <p>TODO: Make this doc more user-friendly. */ -public class IkeSessionOptions { - // TODO: Implement it. +public final class IkeSessionOptions { + private final InetAddress mServerAddress; + private final UdpEncapsulationSocket mUdpEncapSocket; + private final SaProposal[] mSaProposals; + private final boolean mIsIkeFragmentationSupported; + + private IkeSessionOptions( + InetAddress serverAddress, + UdpEncapsulationSocket udpEncapsulationSocket, + SaProposal[] proposals, + boolean isIkeFragmentationSupported) { + mServerAddress = serverAddress; + mUdpEncapSocket = udpEncapsulationSocket; + mSaProposals = proposals; + mIsIkeFragmentationSupported = isIkeFragmentationSupported; + } + + /** Package private */ + InetAddress getServerAddress() { + return mServerAddress; + } + /** Package private */ + UdpEncapsulationSocket getUdpEncapsulationSocket() { + return mUdpEncapSocket; + } + /** Package private */ + SaProposal[] getSaProposals() { + return mSaProposals; + } + /** Package private */ + boolean isIkeFragmentationSupported() { + return mIsIkeFragmentationSupported; + } + + /** This class can be used to incrementally construct a IkeSessionOptions. */ + public static final class Builder { + private final InetAddress mServerAddress; + private final UdpEncapsulationSocket mUdpEncapSocket; + private final List<SaProposal> mSaProposalList = new LinkedList<>(); + + private boolean mIsIkeFragmentationSupported = false; + + /** + * Returns a new Builder for an IkeSessionOptions. + * + * @param serverAddress IP address of remote IKE server. + * @param udpEncapsulationSocket {@link IpSecManager.UdpEncapsulationSocket} for sending and + * receiving IKE message. + * @return Builder for an IkeSessionOptions. + */ + public Builder(InetAddress serverAddress, UdpEncapsulationSocket udpEncapsulationSocket) { + mServerAddress = serverAddress; + mUdpEncapSocket = udpEncapsulationSocket; + } + + /** + * Adds an IKE SA proposal to IkeSessionOptions being built. + * + * @param proposal IKE SA proposal. + * @return Builder for an IkeSessionOptions. + * @throws IllegalArgumentException if input proposal is not IKE SA proposal. + */ + public Builder addSaProposal(SaProposal proposal) { + if (proposal.getProtocolId() != IkePayload.PROTOCOL_ID_IKE) { + throw new IllegalArgumentException( + "Expected IKE SA Proposal but received Child SA proposal"); + } + mSaProposalList.add(proposal); + return this; + } + + /** + * Validates, builds and returns the IkeSessionOptions + * + * @return IkeSessionOptions the validated IkeSessionOptions + * @throws IllegalStateException if no IKE SA proposal is provided + */ + public IkeSessionOptions build() { + if (mSaProposalList.isEmpty()) { + throw new IllegalArgumentException("IKE SA proposal not found"); + } + return new IkeSessionOptions( + mServerAddress, + mUdpEncapSocket, + mSaProposalList.toArray(new SaProposal[mSaProposalList.size()]), + mIsIkeFragmentationSupported); + } + + // TODO: add methods for supporting IKE fragmentation. + } } diff --git a/src/java/com/android/ike/ikev2/IkeSessionStateMachine.java b/src/java/com/android/ike/ikev2/IkeSessionStateMachine.java index c887ecb0..b4a7b9c6 100644 --- a/src/java/com/android/ike/ikev2/IkeSessionStateMachine.java +++ b/src/java/com/android/ike/ikev2/IkeSessionStateMachine.java @@ -17,20 +17,30 @@ package com.android.ike.ikev2; import android.os.Looper; import android.os.Message; +import android.system.ErrnoException; import android.util.LongSparseArray; -import android.util.Pair; import android.util.SparseArray; import com.android.ike.ikev2.SaRecord.IkeSaRecord; import com.android.ike.ikev2.exceptions.IkeException; import com.android.ike.ikev2.message.IkeHeader; +import com.android.ike.ikev2.message.IkeKePayload; import com.android.ike.ikev2.message.IkeMessage; +import com.android.ike.ikev2.message.IkeNoncePayload; import com.android.ike.ikev2.message.IkeNotifyPayload; +import com.android.ike.ikev2.message.IkePayload; +import com.android.ike.ikev2.message.IkeSaPayload; +import com.android.ike.ikev2.message.IkeSaPayload.DhGroupTransform; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.State; import com.android.internal.util.StateMachine; import java.security.GeneralSecurityException; +import java.security.SecureRandom; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; /** * IkeSessionStateMachine tracks states and manages exchanges of this IKE session. @@ -74,6 +84,11 @@ public class IkeSessionStateMachine extends StateMachine { static final int CMD_LOCAL_REQUEST_REKEY_CHILD = CMD_LOCAL_REQUEST_BASE + 7; // TODO: Add signals for other procedure types and notificaitons. + // Remember locally assigned IKE SPIs to avoid SPI collision. + private static final Set<Long> ASSIGNED_LOCAL_IKE_SPI_SET = new HashSet<>(); + private static final int MAX_ASSIGN_IKE_SPI_ATTEMPTS = 100; + private static final SecureRandom IKE_SPI_RANDOM = new SecureRandom(); + private final IkeSessionOptions mIkeSessionOptions; private final ChildSessionOptions mFirstChildSessionOptions; /** Map that stores all IkeSaRecords, keyed by remotely generated IKE SPI. */ @@ -85,6 +100,12 @@ public class IkeSessionStateMachine extends StateMachine { */ private final SparseArray<ChildSessionStateMachine> mSpiToChildSessionMap; + /** + * Package private socket that sends and receives encoded IKE message. Initialized in Initial + * State. + */ + @VisibleForTesting IkeSocket mIkeSocket; + /** Package */ @VisibleForTesting IkeSaRecord mCurrentIkeSaRecord; /** Package */ @@ -146,9 +167,55 @@ public class IkeSessionStateMachine extends StateMachine { setInitialState(mInitial); } + // Generate IKE SPI. Throw an exception if it failed and handle this exception in current State. + private static Long getIkeSpiOrThrow() { + for (int i = 0; i < MAX_ASSIGN_IKE_SPI_ATTEMPTS; i++) { + long spi = IKE_SPI_RANDOM.nextLong(); + if (ASSIGNED_LOCAL_IKE_SPI_SET.add(spi)) return spi; + } + throw new IllegalStateException("Failed to generate IKE SPI."); + } + private IkeMessage buildIkeInitReq() { - // TODO:Build packet according to mIkeSessionOptions. - return null; + // TODO: Handle IKE SPI assigning error in CreateIkeLocalIkeInit State. + + List<IkePayload> payloadList = new LinkedList<>(); + + // Generate IKE SPI + long initSpi = getIkeSpiOrThrow(); + long respSpi = 0; + + // It is validated in IkeSessionOptions.Builder to ensure IkeSessionOptions has at least one + // SaProposal and all SaProposals are valid for IKE SA negotiation. + SaProposal[] saProposals = mIkeSessionOptions.getSaProposals(); + + // Build SA Payload + IkeSaPayload saPayload = new IkeSaPayload(saProposals); + payloadList.add(saPayload); + + // Build KE Payload using the first DH group number in the first SaProposal. + DhGroupTransform dhGroupTransform = saProposals[0].getDhGroupTransforms()[0]; + IkeKePayload kePayload = new IkeKePayload(dhGroupTransform.id); + payloadList.add(kePayload); + + // Build Nonce Payload + IkeNoncePayload noncePayload = new IkeNoncePayload(); + payloadList.add(noncePayload); + + // TODO: Add Notification Payloads according to user configurations. + + // Build IKE header + IkeHeader ikeHeader = + new IkeHeader( + initSpi, + respSpi, + IkePayload.PAYLOAD_TYPE_SA, + IkeHeader.EXCHANGE_TYPE_IKE_SA_INIT, + false /*isResponseMsg*/, + true /*fromIkeInitiator*/, + 0 /*messageId*/); + + return new IkeMessage(ikeHeader, payloadList); } private IkeMessage buildIkeAuthReq() { @@ -214,6 +281,19 @@ public class IkeSessionStateMachine extends StateMachine { } /** + * Receive IKE packet from remote server. + * + * <p>This method is called synchronously from IkeSocket. It proxies the synchronous call as an + * asynchronous job to the IkeSessionStateMachine handler. + * + * @param ikeHeader the decoded IKE header. + * @param ikePacketBytes the byte array of the entire received IKE packet. + */ + public void receiveIkePacket(IkeHeader ikeHeader, byte[] ikePacketBytes) { + sendMessage(CMD_RECEIVE_IKE_PACKET, new ReceivedIkePacket(ikeHeader, ikePacketBytes)); + } + + /** * ReceivedIkePacket is a package private data container consists of decoded IkeHeader and * encoded IKE packet in a byte array. */ @@ -223,9 +303,9 @@ public class IkeSessionStateMachine extends StateMachine { /** Entire encoded IKE message including IKE header */ public final byte[] ikePacketBytes; - ReceivedIkePacket(Pair<IkeHeader, byte[]> ikePacketPair) { - ikeHeader = ikePacketPair.first; - ikePacketBytes = ikePacketPair.second; + ReceivedIkePacket(IkeHeader ikeHeader, byte[] ikePacketBytes) { + this.ikeHeader = ikeHeader; + this.ikePacketBytes = ikePacketBytes; } } @@ -260,6 +340,15 @@ public class IkeSessionStateMachine extends StateMachine { /** Initial state of IkeSessionStateMachine. */ class Initial extends State { @Override + public void enter() { + try { + mIkeSocket = IkeSocket.getIkeSocket(mIkeSessionOptions.getUdpEncapsulationSocket()); + } catch (ErrnoException e) { + // TODO: handle exception and close IkeSession. + } + } + + @Override public boolean processMessage(Message message) { switch (message.what) { case CMD_LOCAL_REQUEST_CREATE_IKE: @@ -417,6 +506,7 @@ public class IkeSessionStateMachine extends StateMachine { public void enter() { mRequestMsg = buildRequest(); mRequestPacket = encodeRequest(); + mIkeSocket.sendIkePacket(mRequestPacket, mIkeSessionOptions.getServerAddress()); // TODO: Send out packet and start retransmission timer. } @@ -432,12 +522,20 @@ public class IkeSessionStateMachine extends StateMachine { // CreateIkeLocalInit should override encodeRequest() to encode unencrypted packet protected byte[] encodeRequest() { // TODO: encrypt and encode mRequestMsg - return null; - }; + return new byte[0]; + } } /** CreateIkeLocalIkeInit represents state when IKE library initiates IKE_INIT exchange. */ class CreateIkeLocalIkeInit extends LocalNewExchangeBase { + + @Override + public void enter() { + super.enter(); + mIkeSocket.registerIke( + mRequestMsg.ikeHeader.ikeInitiatorSpi, IkeSessionStateMachine.this); + } + @Override protected IkeMessage buildRequest() { return buildIkeInitReq(); @@ -445,8 +543,7 @@ public class IkeSessionStateMachine extends StateMachine { @Override protected byte[] encodeRequest() { - // TODO: Encode an unencrypted IKE packet. - return null; + return mRequestMsg.encode(); } @Override @@ -522,8 +619,7 @@ public class IkeSessionStateMachine extends StateMachine { mFirstChildSessionOptions); // TODO: Replace null input params to payload lists in IKE_AUTH request and // IKE_AUTH response for negotiating Child SA. - firstChild.handleFirstChildExchange( - null, null, new ChildSessionCallback()); + firstChild.handleFirstChildExchange(null, null, new ChildSessionCallback()); transitionTo(mIdle); } catch (IkeException e) { diff --git a/src/java/com/android/ike/ikev2/IkeSocket.java b/src/java/com/android/ike/ikev2/IkeSocket.java new file mode 100644 index 00000000..e235ab88 --- /dev/null +++ b/src/java/com/android/ike/ikev2/IkeSocket.java @@ -0,0 +1,268 @@ +/* + * 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.ikev2; + +import static android.system.OsConstants.F_SETFL; +import static android.system.OsConstants.SOCK_DGRAM; +import static android.system.OsConstants.SOCK_NONBLOCK; + +import android.net.IpSecManager.UdpEncapsulationSocket; +import android.net.util.PacketReader; +import android.os.Handler; +import android.system.ErrnoException; +import android.system.Os; +import android.util.Log; +import android.util.LongSparseArray; + +import com.android.ike.ikev2.exceptions.IkeException; +import com.android.ike.ikev2.message.IkeHeader; +import com.android.internal.annotations.VisibleForTesting; + +import java.io.FileDescriptor; +import java.io.IOException; +import java.net.InetAddress; +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +/** + * IkeSocket sends and receives IKE packets via the user provided {@link UdpEncapsulationSocket}. + * + * <p>One UdpEncapsulationSocket instance can only be bound to one IkeSocket instance. IkeSocket + * maintains a static map to cache all bound UdpEncapsulationSockets and their IkeSocket instances. + * It returns the existing IkeSocket when it has been bound with user provided {@link + * UdpEncapsulationSocket}. + * + * <p>As a packet receiver, IkeSocket registers a file descriptor with a thread's Looper and handles + * read events (and errors). Users can expect a call life-cycle like the following: + * + * <pre> + * [1] when user gets a new initiated IkeSocket, start() is called and followed by createFd(). + * [2] yield, waiting for a read event which will invoke handlePacket() + * [3] when user closes this IkeSocket, its reference count decreases. Then stop() is called when + * there is no reference of this instance. + * </pre> + * + * <p>IkeSocket is constructed and called only on a single IKE working thread by {@link + * IkeSessionStateMachine}. Since all {@link IkeSessionStateMachine}s run on the same working + * thread, there will not be concurrent modification problems. + */ +public final class IkeSocket extends PacketReader implements AutoCloseable { + private static final String TAG = "IkeSocket"; + + // TODO: b/129358324 Consider supporting IKE exchange without UDP Encapsulation. + // UDP-encapsulated IKE packets MUST be sent to 4500. + @VisibleForTesting static final int IKE_SERVER_PORT = 4500; + + // A Non-ESP marker helps the recipient to distinguish IKE packets from ESP packets. + @VisibleForTesting static final int NON_ESP_MARKER_LEN = 4; + @VisibleForTesting static final byte[] NON_ESP_MARKER = new byte[NON_ESP_MARKER_LEN]; + + // Map from UdpEncapsulationSocket to IkeSocket instances. + private static Map<UdpEncapsulationSocket, IkeSocket> sFdToIkeSocketMap = new HashMap<>(); + + private static IPacketReceiver sPacketReceiver = new PacketReceiver(); + + // Package private map from locally generated IKE SPI to IkeSessionStateMachine instances. + @VisibleForTesting + final LongSparseArray<IkeSessionStateMachine> mSpiToIkeSession = + new LongSparseArray<>(); + // UdpEncapsulationSocket for sending and receving IKE packet. + private final UdpEncapsulationSocket mUdpEncapSocket; + + /** Package private */ + @VisibleForTesting + int mRefCount; + + private IkeSocket(UdpEncapsulationSocket udpEncapSocket, Handler handler) { + super(handler); + mRefCount = 1; + mUdpEncapSocket = udpEncapSocket; + } + + /** + * Get an IkeSocket instance. + * + * <p>Return the existing IkeSocket instance if it has been created for the input + * udpEncapSocket. Otherwise, create and return a new IkeSocket instance. + * + * @param udpEncapSocket user provided UdpEncapsulationSocket + * @return an IkSocket instance + */ + public static IkeSocket getIkeSocket(UdpEncapsulationSocket udpEncapSocket) + throws ErrnoException { + FileDescriptor fd = udpEncapSocket.getFileDescriptor(); + // All created IkeSocket has modified its FileDescriptor to non-blocking type for handling + // read events in a non-blocking way. + Os.fcntlInt(fd, F_SETFL, SOCK_DGRAM | SOCK_NONBLOCK); + + if (sFdToIkeSocketMap.containsKey(udpEncapSocket)) { + IkeSocket ikeSocket = sFdToIkeSocketMap.get(udpEncapSocket); + ikeSocket.mRefCount++; + return ikeSocket; + } else { + IkeSocket ikeSocket = new IkeSocket(udpEncapSocket, new Handler()); + // Create and register FileDescriptor for receiving IKE packet on current thread. + ikeSocket.start(); + + sFdToIkeSocketMap.put(udpEncapSocket, ikeSocket); + return ikeSocket; + } + } + + /** + * Get FileDecriptor of mUdpEncapSocket. + * + * <p>PacketReader registers a listener for this file descriptor on the thread where IkeSocket + * is constructed. When there is a read event, this listener is invoked and then calls {@link + * handlePacket} to handle the received packet. + */ + @Override + protected FileDescriptor createFd() { + return mUdpEncapSocket.getFileDescriptor(); + } + + /** + * IPacketReceiver provides a package private interface for handling received packet. + * + * <p>IPacketReceiver exists so that the interface is injectable for testing. + */ + interface IPacketReceiver { + void handlePacket(byte[] recvbuf, LongSparseArray<IkeSessionStateMachine> spiToIkeSession); + } + + /** Package private */ + @VisibleForTesting + static final class PacketReceiver implements IPacketReceiver { + public void handlePacket( + byte[] recvbuf, LongSparseArray<IkeSessionStateMachine> spiToIkeSession) { + // TODO: b/129708574 Consider only logging the error some % of the time it happens, or + // only logging the error the first time it happens and then keep a count to prevent + // logspam. + + ByteBuffer byteBuffer = ByteBuffer.wrap(recvbuf); + + // Check the existence of the Non-ESP Marker. A received packet can be either an IKE + // packet starts with 4 zero-valued bytes Non-ESP Marker or an ESP packet starts with 4 + // bytes ESP SPI. ESP SPI value can never be zero. + byte[] espMarker = new byte[NON_ESP_MARKER_LEN]; + byteBuffer.get(espMarker); + if (!Arrays.equals(NON_ESP_MARKER, espMarker)) { + // Drop the received ESP packet. + Log.e(TAG, "Receive an ESP packet."); + return; + } + + try { + // Re-direct IKE packet to IkeSessionStateMachine according to the locally generated + // IKE SPI. + byte[] ikePacketBytes = new byte[byteBuffer.remaining()]; + byteBuffer.get(ikePacketBytes); + IkeHeader ikeHeader = new IkeHeader(ikePacketBytes); + + long localGeneratedSpi = + ikeHeader.fromIkeInitiator + ? ikeHeader.ikeResponderSpi + : ikeHeader.ikeInitiatorSpi; + + IkeSessionStateMachine ikeStateMachine = spiToIkeSession.get(localGeneratedSpi); + if (ikeStateMachine == null) { + Log.e(TAG, "Unrecognized IKE SPI."); + // TODO: Handle invalid IKE SPI error + } else { + ikeStateMachine.receiveIkePacket(ikeHeader, ikePacketBytes); + } + } catch (IkeException e) { + // Handle invalid IKE header + Log.e(TAG, "Can't parse malformed IKE packet header."); + } + } + } + + /** Package private */ + @VisibleForTesting + static void setPacketReceiver(IPacketReceiver receiver) { + sPacketReceiver = receiver; + } + + /** + * Handle received IKE packet. Invoked when there is a read event. Any desired copies of + * |recvbuf| should be made in here, as the underlying byte array is reused across all reads. + */ + @Override + protected void handlePacket(byte[] recvbuf, int length) { + sPacketReceiver.handlePacket(Arrays.copyOfRange(recvbuf, 0, length), mSpiToIkeSession); + } + + /** + * Send encoded IKE packet to destination address + * + * @param ikePacket encoded IKE packet + * @param serverAddress IP address of remote server + */ + public void sendIkePacket(byte[] ikePacket, InetAddress serverAddress) { + try { + ByteBuffer buffer = ByteBuffer.allocate(NON_ESP_MARKER_LEN + ikePacket.length); + + // Build outbound UDP Encapsulation packet body for sending IKE message. + buffer.put(NON_ESP_MARKER).put(ikePacket); + buffer.rewind(); + + // Use unconnected UDP socket because one {@UdpEncapsulationSocket} may be shared by + // multiple IKE sessions that send messages to different destinations. + Os.sendto( + mUdpEncapSocket.getFileDescriptor(), buffer, 0, serverAddress, IKE_SERVER_PORT); + } catch (ErrnoException | IOException e) { + // TODO: Handle exception + } + } + + /** + * Register new created IKE SA + * + * @param spi the locally generated IKE SPI + * @param ikeSession the IKE session this IKE SA belongs to + */ + public void registerIke(long spi, IkeSessionStateMachine ikeSession) { + mSpiToIkeSession.put(spi, ikeSession); + } + + /** + * Unregister a deleted IKE SA + * + * @param spi the locally generated IKE SPI + */ + public void unregisterIke(long spi) { + mSpiToIkeSession.remove(spi); + } + + /** Release reference of current IkeSocket when the IKE session is closed. */ + public void releaseReference() { + mRefCount--; + if (mRefCount == 0) close(); + } + + /** Implement {@link AutoCloseable#close()} */ + @Override + public void close() { + sFdToIkeSocketMap.remove(mUdpEncapSocket); + // PackeReader unregisters file descriptor on thread with which the Handler constructor + // argument is associated. + stop(); + } +} diff --git a/src/java/com/android/ike/ikev2/SaProposal.java b/src/java/com/android/ike/ikev2/SaProposal.java index f932be24..53206cc9 100644 --- a/src/java/com/android/ike/ikev2/SaProposal.java +++ b/src/java/com/android/ike/ikev2/SaProposal.java @@ -29,7 +29,9 @@ import com.android.ike.ikev2.message.IkeSaPayload.Transform; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; import java.util.Arrays; +import java.util.List; import java.util.Set; /** @@ -137,17 +139,17 @@ public final class SaProposal { } /** Package private */ - @IkePayload.ProtocolId final int mProtocolId; + @IkePayload.ProtocolId private final int mProtocolId; /** Package private */ - final EncryptionTransform[] mEncryptionAlgorithms; + private final EncryptionTransform[] mEncryptionAlgorithms; /** Package private */ - final PrfTransform[] mPseudorandomFunctions; + private final PrfTransform[] mPseudorandomFunctions; /** Package private */ - final IntegrityTransform[] mIntegrityAlgorithms; + private final IntegrityTransform[] mIntegrityAlgorithms; /** Package private */ - final DhGroupTransform[] mDhGroups; + private final DhGroupTransform[] mDhGroups; /** Package private */ - final EsnTransform[] mEsns; + private final EsnTransform[] mEsns; private SaProposal( @IkePayload.ProtocolId int protocol, @@ -225,6 +227,63 @@ public final class SaProposal { return Arrays.asList(selectFrom).contains(selected[0]); } + /*Package private*/ + @IkePayload.ProtocolId + int getProtocolId() { + return mProtocolId; + } + + /*Package private*/ + EncryptionTransform[] getEncryptionTransforms() { + return mEncryptionAlgorithms; + } + + /*Package private*/ + PrfTransform[] getPrfTransforms() { + return mPseudorandomFunctions; + } + + /*Package private*/ + IntegrityTransform[] getIntegrityTransforms() { + return mIntegrityAlgorithms; + } + + /*Package private*/ + DhGroupTransform[] getDhGroupTransforms() { + return mDhGroups; + } + + /*Package private*/ + EsnTransform[] getEsnTransforms() { + return mEsns; + } + + /** + * Return all SA Transforms in this SaProposal to be encoded for building an outbound IKE + * message. + * + * <p>This method can be called by only IKE library. + * + * @return Array of Transforms to be encoded. + */ + public Transform[] getAllTransforms() { + int encodedNumTransforms = + mEncryptionAlgorithms.length + + mPseudorandomFunctions.length + + mIntegrityAlgorithms.length + + mDhGroups.length + + mEsns.length; + + List<Transform> transformList = new ArrayList<Transform>(encodedNumTransforms); + transformList.addAll(Arrays.asList(mEncryptionAlgorithms)); + transformList.addAll(Arrays.asList(mPseudorandomFunctions)); + transformList.addAll(Arrays.asList(mIntegrityAlgorithms)); + transformList.addAll(Arrays.asList(mDhGroups)); + transformList.addAll(Arrays.asList(mEsns)); + + return transformList.toArray(new Transform[encodedNumTransforms]); + } + /** * This class can be used to incrementally construct a SaProposal. SaProposal instances are * immutable once built. @@ -474,7 +533,7 @@ public final class SaProposal { * @return SaProposal the validated SaProposal. * @throws IllegalArgumentException if SaProposal is invalid. */ - public SaProposal buildOrThrow() { + public SaProposal build() { EncryptionTransform[] encryptionTransforms = buildEncryptAlgosOrThrow(); PrfTransform[] prfTransforms = buildPrfsOrThrow(); IntegrityTransform[] integrityTransforms = diff --git a/src/java/com/android/ike/ikev2/SaRecord.java b/src/java/com/android/ike/ikev2/SaRecord.java index 355d41d4..4dcddec4 100644 --- a/src/java/com/android/ike/ikev2/SaRecord.java +++ b/src/java/com/android/ike/ikev2/SaRecord.java @@ -17,9 +17,16 @@ package com.android.ike.ikev2; import com.android.ike.ikev2.message.IkeMessage; import com.android.ike.ikev2.message.IkePayload; +import com.android.internal.annotations.VisibleForTesting; +import java.nio.ByteBuffer; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; import java.util.List; +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; + /** * SaRecord represents common information of an IKE SA and a Child SA. * @@ -199,4 +206,71 @@ public abstract class SaRecord { ChildSaRecord makeChildSaRecord( List<IkePayload> reqPayloads, List<IkePayload> respPayloads); } + + /** Generate SKEYSEED using negotiated PRF. */ + @VisibleForTesting + static byte[] generateSKeySeed( + String prfAlgorithm, byte[] nonceInit, byte[] nonceResp, byte[] sharedDhKey) { + try { + ByteBuffer keyBuffer = ByteBuffer.allocate(nonceInit.length + nonceResp.length); + keyBuffer.put(nonceInit).put(nonceResp); + SecretKeySpec prfKeySpec = new SecretKeySpec(keyBuffer.array(), prfAlgorithm); + + Mac prfMac = Mac.getInstance(prfAlgorithm, IkeMessage.getSecurityProvider()); + prfMac.init(prfKeySpec); + + ByteBuffer sharedKeyBuffer = ByteBuffer.wrap(sharedDhKey); + prfMac.update(sharedKeyBuffer); + + return prfMac.doFinal(); + } catch (InvalidKeyException | NoSuchAlgorithmException e) { + throw new IllegalArgumentException("Failed to generate SKEYSEED", e); + } + } + + /** + * Derives key materials using negotiated PRF. + * + * <p>prf+(K, S) outputs a pseudorandom stream by using negotiated PRF iteratively. In this way + * it can generate long enough keying material containing all the keys for this IKE/Child SA. + * + * @see <a href="https://tools.ietf.org/html/rfc7296#section-2.13">RFC 7296 nternet Key Exchange + * Protocol Version 2 (IKEv2) 2.13. Generating Keying Material </a> + */ + @VisibleForTesting + static byte[] generateKeyMat( + String prfAlgorithm, byte[] prfKey, byte[] dataToSign, int keyMaterialLen) + throws InvalidKeyException { + try { + SecretKeySpec prfKeySpec = new SecretKeySpec(prfKey, prfAlgorithm); + Mac prfMac = Mac.getInstance(prfAlgorithm, IkeMessage.getSecurityProvider()); + + ByteBuffer keyMatBuffer = ByteBuffer.allocate(keyMaterialLen); + + byte[] previousMac = new byte[0]; + final int padLen = 1; + byte padValue = 1; + + while (keyMatBuffer.remaining() > 0) { + prfMac.init(prfKeySpec); + + ByteBuffer dataToSignBuffer = + ByteBuffer.allocate(previousMac.length + dataToSign.length + padLen); + dataToSignBuffer.put(previousMac).put(dataToSign).put(padValue); + dataToSignBuffer.rewind(); + + prfMac.update(dataToSignBuffer); + + previousMac = prfMac.doFinal(); + keyMatBuffer.put( + previousMac, 0, Math.min(previousMac.length, keyMatBuffer.remaining())); + + padValue++; + } + + return keyMatBuffer.array(); + } catch (InvalidKeyException | NoSuchAlgorithmException e) { + throw new IllegalArgumentException("Failed to generate keying material", e); + } + } } diff --git a/src/java/com/android/ike/ikev2/message/IkeDeletePayload.java b/src/java/com/android/ike/ikev2/message/IkeDeletePayload.java new file mode 100644 index 00000000..fd4f3644 --- /dev/null +++ b/src/java/com/android/ike/ikev2/message/IkeDeletePayload.java @@ -0,0 +1,123 @@ +/* + * 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.ikev2.message; + +import com.android.ike.ikev2.exceptions.IkeException; +import com.android.ike.ikev2.exceptions.InvalidSyntaxException; + +import java.nio.ByteBuffer; + +/** + * IkeDeletePayload represents a Delete Payload. + * + * <p>As instructed in RFC 7296, deletion of the IKE SA is indicated by a protocol ID of 1 (IKE) but + * no SPIs. Deletion of a Child SA will contain the IPsec protocol ID and SPIs of inbound IPsec + * packets. Since IKE library only supports negotiating Child SA using ESP, only the protocol ID of + * 3 (ESP) is used for deleting Child SA. + * + * @see <a href="https://tools.ietf.org/html/rfc7296#section-3.11">RFC 7296, Internet Key Exchange + * Protocol Version 2 (IKEv2)</a> + */ +public final class IkeDeletePayload extends IkePayload { + + @ProtocolId public final int protocolId; + public final byte spiSize; + public final int numSpi; + public final int[] spisToDelete; + + /** + * Construct an instance of IkeDeletePayload from decoding inbound IKE packet. + * + * <p>NegativeArraySizeException and BufferUnderflowException will be caught in {@link + * IkeMessage} + * + * @param critical indicates if this payload is critical. Ignored in supported payload as + * instructed by the RFC 7296. + * @param payloadBody payload body in byte array + * @throws IkeException if there is any error + */ + IkeDeletePayload(boolean critical, byte[] payloadBody) throws IkeException { + super(PAYLOAD_TYPE_DELETE, critical); + + ByteBuffer inputBuffer = ByteBuffer.wrap(payloadBody); + + protocolId = Byte.toUnsignedInt(inputBuffer.get()); + spiSize = inputBuffer.get(); + numSpi = Short.toUnsignedInt(inputBuffer.getShort()); + spisToDelete = new int[numSpi]; + + switch (protocolId) { + case PROTOCOL_ID_IKE: + // Delete payload for IKE SA must not include SPI. + if (spiSize != SPI_LEN_NOT_INCLUDED + || numSpi != 0 + || inputBuffer.remaining() != 0) { + throw new InvalidSyntaxException("Invalid Delete IKE Payload."); + } + break; + case PROTOCOL_ID_ESP: + // Delete payload for Child SA must include SPI + if (spiSize != SPI_LEN_IPSEC + || numSpi == 0 + || inputBuffer.remaining() != SPI_LEN_IPSEC * numSpi) { + throw new InvalidSyntaxException("Invalid Delete Child Payload."); + } + + for (int i = 0; i < numSpi; i++) { + spisToDelete[i] = inputBuffer.getInt(); + } + break; + default: + throw new InvalidSyntaxException("Unrecognized protocol in Delete Payload."); + } + } + + // TODO: Add a constructor for building outbound IKE message. + + /** + * Encode Delete Payload to ByteBuffer. + * + * @param nextPayload type of payload that follows this payload. + * @param byteBuffer destination ByteBuffer that stores encoded payload. + */ + @Override + protected void encodeToByteBuffer(@PayloadType int nextPayload, ByteBuffer byteBuffer) { + throw new UnsupportedOperationException("Operation not supported."); + // TODO: Implement it. + } + + /** + * Get entire payload length. + * + * @return entire payload length. + */ + @Override + protected int getPayloadLength() { + throw new UnsupportedOperationException("Operation not supported."); + // TODO: Implement it. + } + + /** + * Return the payload type as a String. + * + * @return the payload type as a String. + */ + @Override + public String getTypeString() { + return "Delete Payload"; + } +} diff --git a/src/java/com/android/ike/ikev2/message/IkeEncryptedPayloadBody.java b/src/java/com/android/ike/ikev2/message/IkeEncryptedPayloadBody.java index 52ca2a76..a41d8555 100644 --- a/src/java/com/android/ike/ikev2/message/IkeEncryptedPayloadBody.java +++ b/src/java/com/android/ike/ikev2/message/IkeEncryptedPayloadBody.java @@ -56,11 +56,7 @@ final class IkeEncryptedPayloadBody { * decrypting an incoming packet. */ IkeEncryptedPayloadBody( - byte[] message, - Mac integrityMac, - int expectedChecksumLen, - Cipher decryptCipher, - SecretKey dKey) + byte[] message, Mac integrityMac, int checksumLen, Cipher decryptCipher, SecretKey dKey) throws IkeException, GeneralSecurityException { ByteBuffer inputBuffer = ByteBuffer.wrap(message); @@ -77,16 +73,15 @@ final class IkeEncryptedPayloadBody { - (IkeHeader.IKE_HEADER_LENGTH + IkePayload.GENERIC_HEADER_LENGTH + expectedIvLen - + expectedChecksumLen); + + checksumLen); // IkeMessage will catch exception if encryptedDataLen is negative. mEncryptedAndPaddedData = new byte[encryptedDataLen]; - mIntegrityChecksum = new byte[expectedChecksumLen]; + mIntegrityChecksum = new byte[checksumLen]; inputBuffer.get(mIv).get(mEncryptedAndPaddedData).get(mIntegrityChecksum); // Authenticate and decrypt. - byte[] dataToAuthenticate = - Arrays.copyOfRange(message, 0, message.length - expectedChecksumLen); + byte[] dataToAuthenticate = Arrays.copyOfRange(message, 0, message.length - checksumLen); validateChecksumOrThrow(dataToAuthenticate, integrityMac, mIntegrityChecksum); mUnencryptedData = decrypt(mEncryptedAndPaddedData, decryptCipher, dKey, mIv); } @@ -96,17 +91,19 @@ final class IkeEncryptedPayloadBody { * building an outbound packet. */ IkeEncryptedPayloadBody( - byte[] ikeAndPayloadHeader, + IkeHeader ikeHeader, + @IkePayload.PayloadType int firstPayloadType, byte[] unencryptedPayloads, Mac integrityMac, - int expectedChecksumLen, + int checksumLen, Cipher encryptCipher, SecretKey eKey) { this( - ikeAndPayloadHeader, + ikeHeader, + firstPayloadType, unencryptedPayloads, integrityMac, - expectedChecksumLen, + checksumLen, encryptCipher, eKey, encryptCipher.getIV(), @@ -116,10 +113,11 @@ final class IkeEncryptedPayloadBody { /** Package private constructor only for testing. */ @VisibleForTesting IkeEncryptedPayloadBody( - byte[] ikeAndPayloadHeader, + IkeHeader ikeHeader, + @IkePayload.PayloadType int firstPayloadType, byte[] unencryptedPayloads, Mac integrityMac, - int expectedChecksumLen, + int checksumLen, Cipher encryptCipher, SecretKey eKey, byte[] iv, @@ -130,13 +128,40 @@ final class IkeEncryptedPayloadBody { mIv = iv; mEncryptedAndPaddedData = encrypt(unencryptedPayloads, encryptCipher, eKey, iv, padding); + // Build authenticated section using ByteBuffer. Authenticated section includes bytes from + // beginning of IKE header to the pad length, which are concatenation of IKE header, current + // payload header, iv and encrypted and padded data. + int dataToAuthenticateLength = + IkeHeader.IKE_HEADER_LENGTH + + IkePayload.GENERIC_HEADER_LENGTH + + iv.length + + mEncryptedAndPaddedData.length; + ByteBuffer authenticatedSectionBuffer = ByteBuffer.allocate(dataToAuthenticateLength); + + // Encode IKE header + int encryptedPayloadLength = + IkePayload.GENERIC_HEADER_LENGTH + + iv.length + + mEncryptedAndPaddedData.length + + checksumLen; + ikeHeader.encodeToByteBuffer(authenticatedSectionBuffer, encryptedPayloadLength); + + // Encode payload header. The next payload type field indicates the first payload nested in + // this SkPayload/SkfPayload. + int payloadLength = + IkePayload.GENERIC_HEADER_LENGTH + + iv.length + + mEncryptedAndPaddedData.length + + checksumLen; + IkePayload.encodePayloadHeaderToByteBuffer( + firstPayloadType, payloadLength, authenticatedSectionBuffer); + + // Encode iv and padded encrypted data. + authenticatedSectionBuffer.put(iv).put(mEncryptedAndPaddedData); + // Calculate checksum - ByteBuffer inputBuffer = - ByteBuffer.allocate( - ikeAndPayloadHeader.length + iv.length + mEncryptedAndPaddedData.length); - inputBuffer.put(ikeAndPayloadHeader).put(iv).put(mEncryptedAndPaddedData); mIntegrityChecksum = - calculateChecksum(inputBuffer.array(), integrityMac, expectedChecksumLen); + calculateChecksum(authenticatedSectionBuffer.array(), integrityMac, checksumLen); } // TODO: Add another constructor for AEAD protected payload. @@ -145,16 +170,16 @@ final class IkeEncryptedPayloadBody { /** Package private for testing */ @VisibleForTesting - static byte[] calculateChecksum( - byte[] dataToAuthenticate, Mac integrityMac, int expectedChecksumLen) { + static byte[] calculateChecksum(byte[] dataToAuthenticate, Mac integrityMac, int checksumLen) { ByteBuffer inputBuffer = ByteBuffer.wrap(dataToAuthenticate); integrityMac.update(inputBuffer); - byte[] calculatedChecksum = - Arrays.copyOfRange(integrityMac.doFinal(), 0, expectedChecksumLen); + byte[] calculatedChecksum = Arrays.copyOfRange(integrityMac.doFinal(), 0, checksumLen); return calculatedChecksum; } - private static void validateChecksumOrThrow( + /** Package private for testing */ + @VisibleForTesting + static void validateChecksumOrThrow( byte[] dataToAuthenticate, Mac integrityMac, byte[] integrityChecksum) throws GeneralSecurityException { // TODO: Make it package private and add test. @@ -188,8 +213,9 @@ final class IkeEncryptedPayloadBody { } } - private static byte[] decrypt( - byte[] encryptedData, Cipher decryptCipher, SecretKey dKey, byte[] iv) + /** Package private for testing */ + @VisibleForTesting + static byte[] decrypt(byte[] encryptedData, Cipher decryptCipher, SecretKey dKey, byte[] iv) throws GeneralSecurityException { // TODO: Make it package private and add test. decryptCipher.init(Cipher.DECRYPT_MODE, dKey, new IvParameterSpec(iv)); diff --git a/src/java/com/android/ike/ikev2/message/IkeHeader.java b/src/java/com/android/ike/ikev2/message/IkeHeader.java index ebd3553f..c4f215ca 100644 --- a/src/java/com/android/ike/ikev2/message/IkeHeader.java +++ b/src/java/com/android/ike/ikev2/message/IkeHeader.java @@ -23,6 +23,7 @@ import android.annotation.IntDef; import com.android.ike.ikev2.exceptions.IkeException; import com.android.ike.ikev2.exceptions.InvalidMajorVersionException; import com.android.ike.ikev2.exceptions.InvalidSyntaxException; +import com.android.internal.annotations.VisibleForTesting; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -36,7 +37,7 @@ import java.nio.ByteBuffer; * Protocol Version 2 (IKEv2)</a> */ public final class IkeHeader { - //TODO: b/122838549 Change IkeHeader to static inner class of IkeMessage. + // TODO: b/122838549 Change IkeHeader to static inner class of IkeMessage. private static final byte IKE_HEADER_VERSION_INFO = (byte) 0x20; // Indicate whether this message is a response message @@ -69,7 +70,14 @@ public final class IkeHeader { public final boolean isResponseMsg; public final boolean fromIkeInitiator; public final int messageId; - public final int messageLength; + + // Cannot assign encoded message length value for an outbound IKE message before it's encoded. + private static final int ENCODED_MESSAGE_LEN_UNAVAILABLE = -1; + + // mEncodedMessageLength is only set for an inbound IkeMessage. When building an outbound + // IkeMessage, message length is not set because message body length is unknown until it gets + // encrypted and encoded. + private final int mEncodedMessageLength; /** * Construct an instance of IkeHeader. It is only called in the process of building outbound @@ -82,7 +90,6 @@ public final class IkeHeader { * @param isResp indicates if this message is a response or a request * @param fromInit indictaes if this message is sent from the IKE initiator or the IKE responder * @param msgId the message identifier - * @param length the length of the total message in octets */ public IkeHeader( long iSpi, @@ -91,8 +98,7 @@ public final class IkeHeader { @ExchangeType int eType, boolean isResp, boolean fromInit, - int msgId, - int length) { + int msgId) { ikeInitiatorSpi = iSpi; ikeResponderSpi = rSpi; nextPayloadType = nextPType; @@ -100,7 +106,8 @@ public final class IkeHeader { isResponseMsg = isResp; fromIkeInitiator = fromInit; messageId = msgId; - messageLength = length; + + mEncodedMessageLength = ENCODED_MESSAGE_LEN_UNAVAILABLE; // Major version of IKE protocol in use; it must be set to 2 when building an IKEv2 message. majorVersion = 2; @@ -135,11 +142,21 @@ public final class IkeHeader { fromIkeInitiator = ((flagsByte & 0x08) != 0); messageId = buffer.getInt(); - messageLength = buffer.getInt(); + mEncodedMessageLength = buffer.getInt(); + } + + /*Package private*/ + @VisibleForTesting + int getInboundMessageLength() { + if (mEncodedMessageLength == ENCODED_MESSAGE_LEN_UNAVAILABLE) { + throw new UnsupportedOperationException( + "It is not supported to get encoded message length from an outbound message."); + } + return mEncodedMessageLength; } - /** Validate syntax and major version. */ - public void checkValidOrThrow(int packetLength) throws IkeException { + /** Validate syntax and major version of inbound IKE header. */ + public void checkInboundValidOrThrow(int packetLength) throws IkeException { if (majorVersion > 2) { // Receive higher version of protocol. Stop parsing. throw new InvalidMajorVersionException(majorVersion); @@ -155,13 +172,13 @@ public final class IkeHeader { || exchangeType > EXCHANGE_TYPE_INFORMATIONAL) { throw new InvalidSyntaxException("Invalid IKE Exchange Type."); } - if (messageLength != packetLength) { + if (mEncodedMessageLength != packetLength) { throw new InvalidSyntaxException("Invalid IKE Message Length."); } } /** Encode IKE header to ByteBuffer */ - public void encodeToByteBuffer(ByteBuffer byteBuffer) { + public void encodeToByteBuffer(ByteBuffer byteBuffer, int encodedMessageBodyLen) { byteBuffer .putLong(ikeInitiatorSpi) .putLong(ikeResponderSpi) @@ -177,6 +194,6 @@ public final class IkeHeader { flag |= IKE_HEADER_FLAG_FROM_IKE_INITIATOR; } - byteBuffer.put(flag).putInt(messageId).putInt(messageLength); + byteBuffer.put(flag).putInt(messageId).putInt(IKE_HEADER_LENGTH + encodedMessageBodyLen); } } diff --git a/src/java/com/android/ike/ikev2/message/IkeKePayload.java b/src/java/com/android/ike/ikev2/message/IkeKePayload.java index a742d942..92e22381 100644 --- a/src/java/com/android/ike/ikev2/message/IkeKePayload.java +++ b/src/java/com/android/ike/ikev2/message/IkeKePayload.java @@ -16,7 +16,7 @@ package com.android.ike.ikev2.message; -import android.util.Pair; +import android.annotation.Nullable; import com.android.ike.ikev2.IkeDhParams; import com.android.ike.ikev2.SaProposal; @@ -27,9 +27,12 @@ import com.android.ike.ikev2.utils.BigIntegerUtils; import java.math.BigInteger; import java.nio.ByteBuffer; import java.security.GeneralSecurityException; +import java.security.InvalidAlgorithmParameterException; import java.security.KeyFactory; import java.security.KeyPair; import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.ProviderException; import java.security.SecureRandom; import javax.crypto.KeyAgreement; @@ -64,8 +67,22 @@ public final class IkeKePayload extends IkePayload { /** Supported dhGroup falls into {@link DhGroup} */ public final int dhGroup; + /** Public DH key for the recipient to calculate shared key. */ public final byte[] keyExchangeData; + /** Flag indicates if this is an outbound payload. */ + public final boolean isOutbound; + + /** + * localPrivateKey caches the locally generated private key when building an outbound KE + * payload. It will not be sent out. It is only used to calculate DH shared + * key when IKE library receives a public key from the remote server. + * + * <p>localPrivateKey of a inbound payload will be set to null. Caller MUST ensure its an + * outbound payload before using localPrivateKey. + */ + @Nullable public final DHPrivateKeySpec localPrivateKey; + /** * Construct an instance of IkeKePayload in the context of IkePayloadFactory * @@ -79,6 +96,9 @@ public final class IkeKePayload extends IkePayload { IkeKePayload(boolean critical, byte[] payloadBody) throws IkeException { super(PAYLOAD_TYPE_KE, critical); + isOutbound = false; + localPrivateKey = null; + ByteBuffer inputBuffer = ByteBuffer.wrap(payloadBody); dhGroup = Short.toUnsignedInt(inputBuffer.getShort()); @@ -110,17 +130,67 @@ public final class IkeKePayload extends IkePayload { /** * Construct an instance of IkeKePayload for building an outbound packet. * + * <p>Generate a DH key pair. Cache the private key and and send out the public key as + * keyExchangeData. + * * <p>Critical bit in this payload must not be set as instructed in RFC 7296. * * @param dh DH group for this KE payload - * @param keData the Key Exchange data * @see <a href="https://tools.ietf.org/html/rfc7296#page-76">RFC 7296, Internet Key Exchange * Protocol Version 2 (IKEv2), Critical. */ - private IkeKePayload(@SaProposal.DhGroup int dh, byte[] keData) { + public IkeKePayload(@SaProposal.DhGroup int dh) { super(PAYLOAD_TYPE_KE, false); + dhGroup = dh; - keyExchangeData = keData; + isOutbound = true; + + BigInteger prime = BigInteger.ZERO; + int keySize = 0; + switch (dhGroup) { + case SaProposal.DH_GROUP_1024_BIT_MODP: + prime = + BigIntegerUtils.unsignedHexStringToBigInteger( + IkeDhParams.PRIME_1024_BIT_MODP); + keySize = DH_GROUP_1024_BIT_MODP_DATA_LEN; + break; + case SaProposal.DH_GROUP_2048_BIT_MODP: + prime = + BigIntegerUtils.unsignedHexStringToBigInteger( + IkeDhParams.PRIME_2048_BIT_MODP); + keySize = DH_GROUP_2048_BIT_MODP_DATA_LEN; + break; + default: + throw new IllegalArgumentException("DH group not supported: " + dh); + } + + try { + BigInteger baseGen = BigInteger.valueOf(IkeDhParams.BASE_GENERATOR_MODP); + DHParameterSpec dhParams = new DHParameterSpec(prime, baseGen); + + KeyPairGenerator dhKeyPairGen = + KeyPairGenerator.getInstance( + KEY_EXCHANGE_ALGORITHM, IkeMessage.getSecurityProvider()); + // By default SecureRandom uses AndroidOpenSSL provided SHA1PRNG Algorithm, which takes + // /dev/urandom as seed source. + dhKeyPairGen.initialize(dhParams, new SecureRandom()); + + KeyPair keyPair = dhKeyPairGen.generateKeyPair(); + + DHPrivateKey privateKey = (DHPrivateKey) keyPair.getPrivate(); + DHPrivateKeySpec dhPrivateKeyspec = + new DHPrivateKeySpec(privateKey.getX(), prime, baseGen); + DHPublicKey publicKey = (DHPublicKey) keyPair.getPublic(); + + // Zero-pad the public key without the sign bit + keyExchangeData = + BigIntegerUtils.bigIntegerToUnsignedByteArray(publicKey.getY(), keySize); + localPrivateKey = dhPrivateKeyspec; + } catch (NoSuchAlgorithmException e) { + throw new ProviderException("Failed to obtain " + KEY_EXCHANGE_ALGORITHM, e); + } catch (InvalidAlgorithmParameterException e) { + throw new IllegalArgumentException("Failed to initialize key generator", e); + } } /** @@ -149,56 +219,6 @@ public final class IkeKePayload extends IkePayload { } /** - * Construct an instance of IkeKePayload according to its {@link DhGroup}. - * - * @param dh the Dh-Group. It should be in {@link DhGroup} - * @return Pair of generated private key and an instance of IkeKePayload with key exchange data. - * @throws GeneralSecurityException for security-related exception. - */ - public static Pair<DHPrivateKeySpec, IkeKePayload> getKePayload(@SaProposal.DhGroup int dh) - throws GeneralSecurityException { - BigInteger baseGen = BigInteger.valueOf(IkeDhParams.BASE_GENERATOR_MODP); - BigInteger prime = BigInteger.ZERO; - int keySize = 0; - switch (dh) { - case SaProposal.DH_GROUP_1024_BIT_MODP: - prime = - BigIntegerUtils.unsignedHexStringToBigInteger( - IkeDhParams.PRIME_1024_BIT_MODP); - keySize = DH_GROUP_1024_BIT_MODP_DATA_LEN; - break; - case SaProposal.DH_GROUP_2048_BIT_MODP: - prime = - BigIntegerUtils.unsignedHexStringToBigInteger( - IkeDhParams.PRIME_2048_BIT_MODP); - keySize = DH_GROUP_2048_BIT_MODP_DATA_LEN; - break; - default: - throw new IllegalArgumentException("DH group not supported: " + dh); - } - - DHParameterSpec dhParams = new DHParameterSpec(prime, baseGen); - - KeyPairGenerator dhKeyPairGen = - KeyPairGenerator.getInstance( - KEY_EXCHANGE_ALGORITHM, IkeMessage.getSecurityProvider()); - // By default SecureRandom uses AndroidOpenSSL provided SHA1PRNG Algorithm, which takes - // /dev/urandom as seed source. - dhKeyPairGen.initialize(dhParams, new SecureRandom()); - - KeyPair keyPair = dhKeyPairGen.generateKeyPair(); - - DHPrivateKey privateKey = (DHPrivateKey) keyPair.getPrivate(); - DHPrivateKeySpec dhPrivateKeyspec = new DHPrivateKeySpec(privateKey.getX(), prime, baseGen); - DHPublicKey publicKey = (DHPublicKey) keyPair.getPublic(); - - // Zero-pad the public key without the sign bit - byte[] keData = BigIntegerUtils.bigIntegerToUnsignedByteArray(publicKey.getY(), keySize); - - return new Pair(dhPrivateKeyspec, new IkeKePayload(dh, keData)); - } - - /** * Calculate the shared secret. * * @param privateKeySpec contains the local private key, DH prime and DH base generator. diff --git a/src/java/com/android/ike/ikev2/message/IkeMessage.java b/src/java/com/android/ike/ikev2/message/IkeMessage.java index 15949674..5b63ee7f 100644 --- a/src/java/com/android/ike/ikev2/message/IkeMessage.java +++ b/src/java/com/android/ike/ikev2/message/IkeMessage.java @@ -103,7 +103,15 @@ public final class IkeMessage { ikePayloadList = payloadList; } - static Provider getSecurityProvider() { + /** + * Get security provider for IKE library + * + * <p>Use BouncyCastleProvider as the default security provider. + * + * @return the security provider of IKE library. + */ + public static Provider getSecurityProvider() { + // TODO: Move this getter out of IKE message package since not only this package uses it. return SECURITY_PROVIDER; } @@ -231,7 +239,7 @@ public final class IkeMessage { byte[] attachEncodedHeader(byte[] encodedIkeBody) { ByteBuffer outputBuffer = ByteBuffer.allocate(IkeHeader.IKE_HEADER_LENGTH + encodedIkeBody.length); - ikeHeader.encodeToByteBuffer(outputBuffer); + ikeHeader.encodeToByteBuffer(outputBuffer, encodedIkeBody.length); outputBuffer.put(encodedIkeBody); return outputBuffer.array(); } @@ -319,13 +327,40 @@ public final class IkeMessage { IkeSessionOptions ikeSessionOptions, IkeSaRecord ikeSaRecord, IkeMessage ikeMessage) { - // TODO: Implement it. + // TODO: Extract crypto attributes and call encrypt() return null; } + //TODO: Create and use a container class for crypto algorithms and keys. + private byte[] encryptAndEncode( + IkeHeader ikeHeader, + @PayloadType int firstPayload, + byte[] unencryptedPayloads, + Mac integrityMac, + int checksumLen, + Cipher encryptCipher, + SecretKey eKey) { + IkeSkPayload skPayload = + new IkeSkPayload( + ikeHeader, + firstPayload, + unencryptedPayloads, + integrityMac, + checksumLen, + encryptCipher, + eKey); + + ByteBuffer outputBuffer = + ByteBuffer.allocate(IkeHeader.IKE_HEADER_LENGTH + skPayload.getPayloadLength()); + ikeHeader.encodeToByteBuffer(outputBuffer, skPayload.getPayloadLength()); + skPayload.encodeToByteBuffer(firstPayload, outputBuffer); + + return outputBuffer.array(); + } + @Override public IkeMessage decode(IkeHeader header, byte[] inputPacket) throws IkeException { - header.checkValidOrThrow(inputPacket.length); + header.checkInboundValidOrThrow(inputPacket.length); byte[] unencryptedPayloads = Arrays.copyOfRange( @@ -362,7 +397,7 @@ public final class IkeMessage { SecretKey dKey) throws IkeException, GeneralSecurityException { - header.checkValidOrThrow(inputPacket.length); + header.checkInboundValidOrThrow(inputPacket.length); if (header.nextPayloadType != IkePayload.PAYLOAD_TYPE_SK) { // TODO: b/123372339 Handle message containing unprotected payloads. diff --git a/src/java/com/android/ike/ikev2/message/IkeNotifyPayload.java b/src/java/com/android/ike/ikev2/message/IkeNotifyPayload.java index 4b581e05..ee6fe47a 100644 --- a/src/java/com/android/ike/ikev2/message/IkeNotifyPayload.java +++ b/src/java/com/android/ike/ikev2/message/IkeNotifyPayload.java @@ -40,7 +40,7 @@ import java.util.Set; * outbound packet. * * @see <a href="https://tools.ietf.org/html/rfc7296">RFC 7296, Internet Key Exchange Protocol - * Version 2 (IKEv2). + * Version 2 (IKEv2)</a> */ public final class IkeNotifyPayload extends IkePayload { diff --git a/src/java/com/android/ike/ikev2/message/IkePayload.java b/src/java/com/android/ike/ikev2/message/IkePayload.java index a9d9f06a..2474bb64 100644 --- a/src/java/com/android/ike/ikev2/message/IkePayload.java +++ b/src/java/com/android/ike/ikev2/message/IkePayload.java @@ -53,6 +53,7 @@ public abstract class IkePayload { PAYLOAD_TYPE_ID_RESPONDER, PAYLOAD_TYPE_NONCE, PAYLOAD_TYPE_NOTIFY, + PAYLOAD_TYPE_DELETE, PAYLOAD_TYPE_VENDOR, PAYLOAD_TYPE_TS_INITIATOR, PAYLOAD_TYPE_TS_RESPONDER, @@ -78,6 +79,8 @@ public abstract class IkePayload { public static final int PAYLOAD_TYPE_NONCE = 40; /** Notify Payload */ public static final int PAYLOAD_TYPE_NOTIFY = 41; + /** Delete Payload */ + public static final int PAYLOAD_TYPE_DELETE = 42; /** Vendor Payload */ public static final int PAYLOAD_TYPE_VENDOR = 43; /** Traffic Selector Payload of Child SA Initiator */ @@ -137,7 +140,7 @@ public abstract class IkePayload { * @param payloadLength length of the entire payload * @param byteBuffer destination ByteBuffer that stores encoded payload header */ - protected void encodePayloadHeaderToByteBuffer( + protected static void encodePayloadHeaderToByteBuffer( @PayloadType int nextPayload, int payloadLength, ByteBuffer byteBuffer) { byteBuffer .put((byte) nextPayload) diff --git a/src/java/com/android/ike/ikev2/message/IkePayloadFactory.java b/src/java/com/android/ike/ikev2/message/IkePayloadFactory.java index 8f530187..bea87cad 100644 --- a/src/java/com/android/ike/ikev2/message/IkePayloadFactory.java +++ b/src/java/com/android/ike/ikev2/message/IkePayloadFactory.java @@ -78,6 +78,8 @@ final class IkePayloadFactory { return new IkeNoncePayload(isCritical, payloadBody); case IkePayload.PAYLOAD_TYPE_NOTIFY: return new IkeNotifyPayload(isCritical, payloadBody); + case IkePayload.PAYLOAD_TYPE_DELETE: + return new IkeDeletePayload(isCritical, payloadBody); case IkePayload.PAYLOAD_TYPE_VENDOR: return new IkeVendorPayload(isCritical, payloadBody); case IkePayload.PAYLOAD_TYPE_TS_INITIATOR: @@ -129,7 +131,7 @@ final class IkePayloadFactory { * * @param message the byte array contains the whole IKE message. * @param integrityMac the initialized Mac for integrity check. - * @param expectedChecksumLen the expected length of integrity checksum. + * @param checksumLen the checksum length of negotiated integrity algorithm. * @param decryptCipher the uninitialized Cipher for doing decryption. * @param dKey the decryption key. * @return a pair including IkePayload and next payload type. @@ -139,7 +141,7 @@ final class IkePayloadFactory { protected static Pair<IkeSkPayload, Integer> getIkeSkPayload( byte[] message, Mac integrityMac, - int expectedChecksumLen, + int checksumLen, Cipher decryptCipher, SecretKey dKey) throws IkeException, GeneralSecurityException { @@ -175,7 +177,7 @@ final class IkePayloadFactory { isCritical, message, integrityMac, - expectedChecksumLen, + checksumLen, decryptCipher, dKey); return new Pair(payload, nextPayloadType); diff --git a/src/java/com/android/ike/ikev2/message/IkeSaPayload.java b/src/java/com/android/ike/ikev2/message/IkeSaPayload.java index 70280843..c0651ae0 100644 --- a/src/java/com/android/ike/ikev2/message/IkeSaPayload.java +++ b/src/java/com/android/ike/ikev2/message/IkeSaPayload.java @@ -192,6 +192,9 @@ public final class IkeSaPayload extends IkePayload { private static final byte LAST_PROPOSAL = 0; private static final byte NOT_LAST_PROPOSAL = 2; + private static final int PROPOSAL_RESERVED_FIELD_LEN = 1; + private static final int PROPOSAL_HEADER_LEN = 8; + @VisibleForTesting static TransformDecoder sTransformDecoder = new TransformDecoder() { @@ -246,7 +249,7 @@ public final class IkeSaPayload extends IkePayload { "Invalid value of Last Proposal Substructure: " + isLast); } // Skip RESERVED byte - inputBuffer.get(); + inputBuffer.get(new byte[PROPOSAL_RESERVED_FIELD_LEN]); int length = Short.toUnsignedInt(inputBuffer.getShort()); byte number = inputBuffer.get(); @@ -259,8 +262,8 @@ public final class IkeSaPayload extends IkePayload { // spiSize should be either 8 for IKE or 4 for IPsec. long spi = SPI_NOT_INCLUDED; switch (spiSize) { - case 0: - // No SPI field here. + case SPI_LEN_NOT_INCLUDED: + // No SPI attached for IKE initial exchange. break; case SPI_LEN_IPSEC: spi = Integer.toUnsignedLong(inputBuffer.getInt()); @@ -330,6 +333,49 @@ public final class IkeSaPayload extends IkePayload { } return saProposal.isNegotiatedFrom(reqProposal.saProposal); } + + protected void encodeToByteBuffer(boolean isLast, ByteBuffer byteBuffer) { + Transform[] allTransforms = saProposal.getAllTransforms(); + byte isLastIndicator = isLast ? LAST_PROPOSAL : NOT_LAST_PROPOSAL; + + byteBuffer + .put(isLastIndicator) + .put(new byte[PROPOSAL_RESERVED_FIELD_LEN]) + .putShort((short) getProposalLength()) + .put(number) + .put((byte) protocolId) + .put(spiSize) + .put((byte) allTransforms.length); + + switch (spiSize) { + case SPI_LEN_NOT_INCLUDED: + // No SPI attached for IKE initial exchange. + break; + case SPI_LEN_IPSEC: + byteBuffer.putInt((int) spi); + break; + case SPI_LEN_IKE: + byteBuffer.putLong((long) spi); + break; + default: + throw new IllegalArgumentException( + "Invalid value of spiSize in Proposal Substructure: " + spiSize); + } + + // Encode all Transform. + for (int i = 0; i < allTransforms.length; i++) { + // The last transform has the isLast flag set to true. + allTransforms[i].encodeToByteBuffer(i == allTransforms.length - 1, byteBuffer); + } + } + + protected int getProposalLength() { + int len = PROPOSAL_HEADER_LEN + spiSize; + + Transform[] allTransforms = saProposal.getAllTransforms(); + for (Transform t : allTransforms) len += t.getTransformLength(); + return len; + } } @VisibleForTesting @@ -366,7 +412,12 @@ public final class IkeSaPayload extends IkePayload { private static final byte LAST_TRANSFORM = 0; private static final byte NOT_LAST_TRANSFORM = 3; - private static final int TRANSFORM_HEADER_LEN = 8; + + // Length of reserved field of a Transform. + private static final int TRANSFORM_RESERVED_FIELD_LEN = 1; + + // Length of the Transform that with no Attribute. + protected static final int BASIC_TRANSFORM_LEN = 8; // TODO: Add constants for supported algorithms @@ -376,7 +427,7 @@ public final class IkeSaPayload extends IkePayload { public List<Attribute> decodeAttributes(int length, ByteBuffer inputBuffer) throws IkeException { List<Attribute> list = new LinkedList<>(); - int parsedLength = TRANSFORM_HEADER_LEN; + int parsedLength = BASIC_TRANSFORM_LEN; while (parsedLength < length) { Pair<Attribute, Integer> pair = Attribute.readFrom(inputBuffer); parsedLength += pair.second; @@ -420,13 +471,13 @@ public final class IkeSaPayload extends IkePayload { } // Skip RESERVED byte - inputBuffer.get(); + inputBuffer.get(new byte[TRANSFORM_RESERVED_FIELD_LEN]); int length = Short.toUnsignedInt(inputBuffer.getShort()); int type = Byte.toUnsignedInt(inputBuffer.get()); // Skip RESERVED byte - inputBuffer.get(); + inputBuffer.get(new byte[TRANSFORM_RESERVED_FIELD_LEN]); int id = Short.toUnsignedInt(inputBuffer.getShort()); @@ -469,6 +520,23 @@ public final class IkeSaPayload extends IkePayload { // Check if this Transform ID is supported. protected abstract boolean isSupportedTransformId(int id); + // Encode Transform to a ByteBuffer. + protected abstract void encodeToByteBuffer(boolean isLast, ByteBuffer byteBuffer); + + // Get entire Transform length. + protected abstract int getTransformLength(); + + protected void encodeBasicTransformToByteBuffer(boolean isLast, ByteBuffer byteBuffer) { + byte isLastIndicator = isLast ? LAST_TRANSFORM : NOT_LAST_TRANSFORM; + byteBuffer + .put(isLastIndicator) + .put(new byte[TRANSFORM_RESERVED_FIELD_LEN]) + .putShort((short) getTransformLength()) + .put((byte) type) + .put(new byte[TRANSFORM_RESERVED_FIELD_LEN]) + .putShort((short) id); + } + /** * Get Tranform Type as a String. * @@ -487,9 +555,12 @@ public final class IkeSaPayload extends IkePayload { * Exchange Protocol Version 2 (IKEv2)</a> */ public static final class EncryptionTransform extends Transform { - private static final int KEY_LEN_UNASSIGNED = 0; + private static final int KEY_LEN_UNSPECIFIED = 0; - public final int keyLength; + // When using encryption algorithm with variable-length keys, mSpecifiedKeyLength MUST be + // set and a KeyLengthAttribute MUST be attached. Otherwise, mSpecifiedKeyLength MUST NOT be + // set and KeyLengthAttribute MUST NOT be attached. + private final int mSpecifiedKeyLength; /** * Contruct an instance of EncryptionTransform with fixed key length for building an @@ -498,7 +569,7 @@ public final class IkeSaPayload extends IkePayload { * @param id the IKE standard Transform ID. */ public EncryptionTransform(@EncryptionAlgorithm int id) { - this(id, KEY_LEN_UNASSIGNED); + this(id, KEY_LEN_UNSPECIFIED); } /** @@ -506,12 +577,12 @@ public final class IkeSaPayload extends IkePayload { * outbound packet. * * @param id the IKE standard Transform ID. - * @param keyLength the specified key length of this encryption algorithm. + * @param specifiedKeyLength the specified key length of this encryption algorithm. */ - public EncryptionTransform(@EncryptionAlgorithm int id, int keyLength) { + public EncryptionTransform(@EncryptionAlgorithm int id, int specifiedKeyLength) { super(Transform.TRANSFORM_TYPE_ENCR, id); - this.keyLength = keyLength; + mSpecifiedKeyLength = specifiedKeyLength; try { validateKeyLength(); } catch (InvalidSyntaxException e) { @@ -530,13 +601,13 @@ public final class IkeSaPayload extends IkePayload { throws InvalidSyntaxException { super(Transform.TRANSFORM_TYPE_ENCR, id, attributeList); if (!isSupported) { - keyLength = KEY_LEN_UNASSIGNED; + mSpecifiedKeyLength = KEY_LEN_UNSPECIFIED; } else { if (attributeList.size() == 0) { - keyLength = KEY_LEN_UNASSIGNED; + mSpecifiedKeyLength = KEY_LEN_UNSPECIFIED; } else { KeyLengthAttribute attr = getKeyLengthAttribute(attributeList); - keyLength = attr.keyLength; + mSpecifiedKeyLength = attr.keyLength; } validateKeyLength(); } @@ -544,7 +615,7 @@ public final class IkeSaPayload extends IkePayload { @Override public int hashCode() { - return Objects.hash(type, id, keyLength); + return Objects.hash(type, id, mSpecifiedKeyLength); } @Override @@ -552,7 +623,9 @@ public final class IkeSaPayload extends IkePayload { if (!(o instanceof EncryptionTransform)) return false; EncryptionTransform other = (EncryptionTransform) o; - return (type == other.type && id == other.id && keyLength == other.keyLength); + return (type == other.type + && id == other.id + && mSpecifiedKeyLength == other.mSpecifiedKeyLength); } @Override @@ -582,7 +655,7 @@ public final class IkeSaPayload extends IkePayload { private void validateKeyLength() throws InvalidSyntaxException { switch (id) { case SaProposal.ENCRYPTION_ALGORITHM_3DES: - if (keyLength != KEY_LEN_UNASSIGNED) { + if (mSpecifiedKeyLength != KEY_LEN_UNSPECIFIED) { throw new InvalidSyntaxException( "Must not set Key Length value for this " + getTransformTypeString() @@ -597,16 +670,16 @@ public final class IkeSaPayload extends IkePayload { case SaProposal.ENCRYPTION_ALGORITHM_AES_GCM_12: /* fall through */ case SaProposal.ENCRYPTION_ALGORITHM_AES_GCM_16: - if (keyLength == KEY_LEN_UNASSIGNED) { + if (mSpecifiedKeyLength == KEY_LEN_UNSPECIFIED) { throw new InvalidSyntaxException( "Must set Key Length value for this " + getTransformTypeString() + " Algorithm ID: " + id); } - if (keyLength != SaProposal.KEY_LEN_AES_128 - && keyLength != SaProposal.KEY_LEN_AES_192 - && keyLength != SaProposal.KEY_LEN_AES_256) { + if (mSpecifiedKeyLength != SaProposal.KEY_LEN_AES_128 + && mSpecifiedKeyLength != SaProposal.KEY_LEN_AES_192 + && mSpecifiedKeyLength != SaProposal.KEY_LEN_AES_256) { throw new InvalidSyntaxException( "Invalid key length for this " + getTransformTypeString() @@ -622,6 +695,26 @@ public final class IkeSaPayload extends IkePayload { } @Override + protected void encodeToByteBuffer(boolean isLast, ByteBuffer byteBuffer) { + encodeBasicTransformToByteBuffer(isLast, byteBuffer); + + if (mSpecifiedKeyLength != KEY_LEN_UNSPECIFIED) { + new KeyLengthAttribute(mSpecifiedKeyLength).encodeToByteBuffer(byteBuffer); + } + } + + @Override + protected int getTransformLength() { + int len = BASIC_TRANSFORM_LEN; + + if (mSpecifiedKeyLength != KEY_LEN_UNSPECIFIED) { + len += new KeyLengthAttribute(mSpecifiedKeyLength).getAttributeLength(); + } + + return len; + } + + @Override public String getTransformTypeString() { return "Encryption Algorithm"; } @@ -679,6 +772,16 @@ public final class IkeSaPayload extends IkePayload { } @Override + protected void encodeToByteBuffer(boolean isLast, ByteBuffer byteBuffer) { + encodeBasicTransformToByteBuffer(isLast, byteBuffer); + } + + @Override + protected int getTransformLength() { + return BASIC_TRANSFORM_LEN; + } + + @Override public String getTransformTypeString() { return "Pseudorandom Function"; } @@ -740,6 +843,16 @@ public final class IkeSaPayload extends IkePayload { } @Override + protected void encodeToByteBuffer(boolean isLast, ByteBuffer byteBuffer) { + encodeBasicTransformToByteBuffer(isLast, byteBuffer); + } + + @Override + protected int getTransformLength() { + return BASIC_TRANSFORM_LEN; + } + + @Override public String getTransformTypeString() { return "Integrity Algorithm"; } @@ -801,6 +914,16 @@ public final class IkeSaPayload extends IkePayload { } @Override + protected void encodeToByteBuffer(boolean isLast, ByteBuffer byteBuffer) { + encodeBasicTransformToByteBuffer(isLast, byteBuffer); + } + + @Override + protected int getTransformLength() { + return BASIC_TRANSFORM_LEN; + } + + @Override public String getTransformTypeString() { return "Diffie-Hellman Group"; } @@ -868,6 +991,16 @@ public final class IkeSaPayload extends IkePayload { } @Override + protected void encodeToByteBuffer(boolean isLast, ByteBuffer byteBuffer) { + encodeBasicTransformToByteBuffer(isLast, byteBuffer); + } + + @Override + protected int getTransformLength() { + return BASIC_TRANSFORM_LEN; + } + + @Override public String getTransformTypeString() { return "Extended Sequence Numbers"; } @@ -893,6 +1026,19 @@ public final class IkeSaPayload extends IkePayload { return !attributeList.isEmpty(); } + @Override + protected void encodeToByteBuffer(boolean isLast, ByteBuffer byteBuffer) { + throw new UnsupportedOperationException( + "It is not supported to encode a Transform with" + getTransformTypeString()); + } + + @Override + protected int getTransformLength() { + throw new UnsupportedOperationException( + "It is not supported to get length of a Transform with " + + getTransformTypeString()); + } + /** * Return Tranform Type of Unrecognized Transform as a String. * @@ -900,7 +1046,7 @@ public final class IkeSaPayload extends IkePayload { */ @Override public String getTransformTypeString() { - return "Unrecognized Transform Type"; + return "Unrecognized Transform Type."; } } @@ -925,9 +1071,18 @@ public final class IkeSaPayload extends IkePayload { // Support only one Attribute type: Key Length. Should use Type/Value format. public static final int ATTRIBUTE_TYPE_KEY_LENGTH = 14; - private static final int TV_ATTRIBUTE_VALUE_LEN = 2; - private static final int TV_ATTRIBUTE_TOTAL_LEN = 4; - private static final int TVL_ATTRIBUTE_HEADER_LEN = TV_ATTRIBUTE_TOTAL_LEN; + // Mask to extract the left most AF bit to indicate Attribute Format. + private static final int ATTRIBUTE_FORMAT_MASK = 0x8000; + // Mask to extract 15 bits after the AF bit to indicate Attribute Type. + private static final int ATTRIBUTE_TYPE_MASK = 0x7fff; + + // Package private mask to indicate that Type-Value (TV) Attribute Format is used. + static final int ATTRIBUTE_FORMAT_TV = ATTRIBUTE_FORMAT_MASK; + + // Package private + static final int TV_ATTRIBUTE_VALUE_LEN = 2; + static final int TV_ATTRIBUTE_TOTAL_LEN = 4; + static final int TVL_ATTRIBUTE_HEADER_LEN = TV_ATTRIBUTE_TOTAL_LEN; // Only Key Length type belongs to AttributeType public final int type; @@ -940,11 +1095,12 @@ public final class IkeSaPayload extends IkePayload { @VisibleForTesting static Pair<Attribute, Integer> readFrom(ByteBuffer inputBuffer) throws IkeException { short formatAndType = inputBuffer.getShort(); - int type = formatAndType & 0x7fff; + int format = formatAndType & ATTRIBUTE_FORMAT_MASK; + int type = formatAndType & ATTRIBUTE_TYPE_MASK; int length = 0; byte[] value = new byte[0]; - if ((formatAndType & 0x8000) == 0x8000) { + if (format == ATTRIBUTE_FORMAT_TV) { // Type/Value format length = TV_ATTRIBUTE_TOTAL_LEN; value = new byte[TV_ATTRIBUTE_VALUE_LEN]; @@ -969,6 +1125,12 @@ public final class IkeSaPayload extends IkePayload { return new Pair(new UnrecognizedAttribute(type, value), length); } } + + // Encode Attribute to a ByteBuffer. + protected abstract void encodeToByteBuffer(ByteBuffer byteBuffer); + + // Get entire Attribute length. + protected abstract int getAttributeLength(); } /** KeyLengthAttribute represents a Key Length type Attribute */ @@ -983,6 +1145,18 @@ public final class IkeSaPayload extends IkePayload { super(ATTRIBUTE_TYPE_KEY_LENGTH); this.keyLength = keyLength; } + + @Override + protected void encodeToByteBuffer(ByteBuffer byteBuffer) { + byteBuffer + .putShort((short) (ATTRIBUTE_FORMAT_TV | ATTRIBUTE_TYPE_KEY_LENGTH)) + .putShort((short) keyLength); + } + + @Override + protected int getAttributeLength() { + return TV_ATTRIBUTE_TOTAL_LEN; + } } /** @@ -994,6 +1168,18 @@ public final class IkeSaPayload extends IkePayload { protected UnrecognizedAttribute(int type, byte[] value) { super(type); } + + @Override + protected void encodeToByteBuffer(ByteBuffer byteBuffer) { + throw new UnsupportedOperationException( + "It is not supported to encode an unrecognized Attribute."); + } + + @Override + protected int getAttributeLength() { + throw new UnsupportedOperationException( + "It is not supported to get length of an unrecognized Attribute."); + } } /** @@ -1004,9 +1190,12 @@ public final class IkeSaPayload extends IkePayload { */ @Override protected void encodeToByteBuffer(@PayloadType int nextPayload, ByteBuffer byteBuffer) { - throw new UnsupportedOperationException( - "It is not supported to encode a " + getTypeString()); - // TODO: Implement encoding SA payload. + encodePayloadHeaderToByteBuffer(nextPayload, getPayloadLength(), byteBuffer); + + for (int i = 0; i < proposalList.size(); i++) { + // The last proposal has the isLast flag set to true. + proposalList.get(i).encodeToByteBuffer(i == proposalList.size() - 1, byteBuffer); + } } /** @@ -1016,9 +1205,11 @@ public final class IkeSaPayload extends IkePayload { */ @Override protected int getPayloadLength() { - throw new UnsupportedOperationException( - "It is not supported to get payload length of " + getTypeString()); - // TODO: Implement this method for SA payload. + int len = GENERIC_HEADER_LENGTH; + + for (Proposal p : proposalList) len += p.getProposalLength(); + + return len; } /** diff --git a/src/java/com/android/ike/ikev2/message/IkeSkPayload.java b/src/java/com/android/ike/ikev2/message/IkeSkPayload.java index 6a89a6aa..d753ee83 100644 --- a/src/java/com/android/ike/ikev2/message/IkeSkPayload.java +++ b/src/java/com/android/ike/ikev2/message/IkeSkPayload.java @@ -17,7 +17,6 @@ package com.android.ike.ikev2.message; import com.android.ike.ikev2.exceptions.IkeException; -import com.android.ike.ikev2.message.IkePayload.PayloadType; import java.nio.ByteBuffer; import java.security.GeneralSecurityException; @@ -47,7 +46,7 @@ public final class IkeSkPayload extends IkePayload { * @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 expectedChecksumLen the expected length of integrity checksum. + * @param checksumLen the checksum length of negotiated integrity algorithm. * @param decryptCipher the uninitialized Cipher for doing decryption. * @param dKey the decryption key. */ @@ -55,7 +54,7 @@ public final class IkeSkPayload extends IkePayload { boolean critical, byte[] message, Mac integrityMac, - int expectedChecksumLen, + int checksumLen, Cipher decryptCipher, SecretKey dKey) throws IkeException, GeneralSecurityException { @@ -63,7 +62,39 @@ public final class IkeSkPayload extends IkePayload { mIkeEncryptedPayloadBody = new IkeEncryptedPayloadBody( - message, integrityMac, expectedChecksumLen, decryptCipher, dKey); + message, integrityMac, checksumLen, decryptCipher, dKey); + } + + /** + * Construct an instance of IkeSkPayload for building outbound packet. + * + * @param ikeHeader the IKE header. + * @param firstPayloadType the type of first payload nested in SkPayload. + * @param unencryptedPayloads the encoded payload list to protect. + * @param integrityMac the initialized Mac for calculating integrity checksum + * @param checksumLen the checksum length of negotiated integrity algorithm. + * @param encryptCipher the uninitialized Cipher for doing encryption. + * @param eKey the encryption key. + */ + IkeSkPayload( + IkeHeader ikeHeader, + @PayloadType int firstPayloadType, + byte[] unencryptedPayloads, + Mac integrityMac, + int checksumLen, + Cipher encryptCipher, + SecretKey eKey) { + super(PAYLOAD_TYPE_SK, false); + + mIkeEncryptedPayloadBody = + new IkeEncryptedPayloadBody( + ikeHeader, + firstPayloadType, + unencryptedPayloads, + integrityMac, + checksumLen, + encryptCipher, + eKey); } /** diff --git a/tests/iketests/Android.mk b/tests/iketests/Android.mk index a45703c1..0da49b47 100644 --- a/tests/iketests/Android.mk +++ b/tests/iketests/Android.mk @@ -29,6 +29,7 @@ LOCAL_JAVA_LIBRARIES := android.test.runner LOCAL_STATIC_JAVA_LIBRARIES := ike \ androidx.test.rules \ frameworks-base-testutils \ - mockito-target-minus-junit4 + mockito-target-minus-junit4 \ + NetworkStackBase include $(BUILD_PACKAGE)
\ No newline at end of file diff --git a/tests/iketests/AndroidManifest.xml b/tests/iketests/AndroidManifest.xml index 80f54e8d..d69cbc8f 100644 --- a/tests/iketests/AndroidManifest.xml +++ b/tests/iketests/AndroidManifest.xml @@ -18,6 +18,8 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.android.ike.tests"> + <uses-permission android:name="android.permission.INTERNET"/> + <application android:label="FrameworksIkeTests"> <uses-library android:name="android.test.runner" /> </application> diff --git a/tests/iketests/src/java/com/android/ike/ikev2/ChildSessionStateMachineTest.java b/tests/iketests/src/java/com/android/ike/ikev2/ChildSessionStateMachineTest.java index e81b17aa..1d760b7e 100644 --- a/tests/iketests/src/java/com/android/ike/ikev2/ChildSessionStateMachineTest.java +++ b/tests/iketests/src/java/com/android/ike/ikev2/ChildSessionStateMachineTest.java @@ -63,12 +63,13 @@ public final class ChildSessionStateMachineTest { private ISaRecordHelper mMockSaRecordHelper; private IChildSessionCallback mMockChildSessionCallback; - private ChildSessionOptions mMockChildSessionOptions; + private ChildSessionOptions mChildSessionOptions; public ChildSessionStateMachineTest() { mMockSaRecordHelper = mock(SaRecord.ISaRecordHelper.class); mMockChildSessionCallback = mock(IChildSessionCallback.class); - mMockChildSessionOptions = mock(ChildSessionOptions.class); + + mChildSessionOptions = new ChildSessionOptions(); } @Before @@ -77,7 +78,7 @@ public final class ChildSessionStateMachineTest { mLooper = new TestLooper(); mChildSessionStateMachine = new ChildSessionStateMachine( - "ChildSessionStateMachine", mLooper.getLooper(), mMockChildSessionOptions); + "ChildSessionStateMachine", mLooper.getLooper(), mChildSessionOptions); mChildSessionStateMachine.setDbg(true); SaRecord.setSaRecordHelper(mMockSaRecordHelper); diff --git a/tests/iketests/src/java/com/android/ike/ikev2/IkeSessionOptionsTest.java b/tests/iketests/src/java/com/android/ike/ikev2/IkeSessionOptionsTest.java new file mode 100644 index 00000000..bdaf135a --- /dev/null +++ b/tests/iketests/src/java/com/android/ike/ikev2/IkeSessionOptionsTest.java @@ -0,0 +1,103 @@ +/* + * 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.ikev2; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.fail; + +import android.content.Context; +import android.net.IpSecManager; +import android.net.IpSecManager.UdpEncapsulationSocket; + +import androidx.test.InstrumentationRegistry; + +import libcore.net.InetAddressUtils; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.net.Inet4Address; + +public final class IkeSessionOptionsTest { + private static final Inet4Address IPV4_ADDRESS = + (Inet4Address) (InetAddressUtils.parseNumericAddress("192.0.2.100")); + + private UdpEncapsulationSocket mUdpEncapSocket; + + @Before + public void setUp() throws Exception { + Context context = InstrumentationRegistry.getContext(); + IpSecManager ipSecManager = (IpSecManager) context.getSystemService(Context.IPSEC_SERVICE); + mUdpEncapSocket = ipSecManager.openUdpEncapsulationSocket(); + } + + @After + public void tearDown() throws Exception { + mUdpEncapSocket.close(); + } + + @Test + public void testBuild() throws Exception { + SaProposal saProposal = + SaProposal.Builder.newIkeSaProposalBuilder() + .addEncryptionAlgorithm( + SaProposal.ENCRYPTION_ALGORITHM_AES_GCM_8, + SaProposal.KEY_LEN_AES_128) + .addPseudorandomFunction(SaProposal.PSEUDORANDOM_FUNCTION_AES128_XCBC) + .addDhGroup(SaProposal.DH_GROUP_1024_BIT_MODP) + .build(); + + IkeSessionOptions sessionOptions = + new IkeSessionOptions.Builder(IPV4_ADDRESS, mUdpEncapSocket) + .addSaProposal(saProposal) + .build(); + + assertEquals(IPV4_ADDRESS, sessionOptions.getServerAddress()); + assertEquals(mUdpEncapSocket, sessionOptions.getUdpEncapsulationSocket()); + assertArrayEquals(new SaProposal[] {saProposal}, sessionOptions.getSaProposals()); + assertFalse(sessionOptions.isIkeFragmentationSupported()); + } + + @Test + public void testBuildWithoutSaProposal() throws Exception { + try { + new IkeSessionOptions.Builder(IPV4_ADDRESS, mUdpEncapSocket).build(); + fail("Expected to fail due to absence of SA proposal."); + } catch (IllegalArgumentException expected) { + } + } + + @Test + public void testBuildWithChildSaProposal() throws Exception { + SaProposal saProposal = + SaProposal.Builder.newChildSaProposalBuilder(true) + .addEncryptionAlgorithm( + SaProposal.ENCRYPTION_ALGORITHM_AES_GCM_8, + SaProposal.KEY_LEN_AES_128) + .build(); + try { + new IkeSessionOptions.Builder(IPV4_ADDRESS, mUdpEncapSocket) + .addSaProposal(saProposal) + .build(); + fail("Expected to fail due to wrong type of SA proposal."); + } catch (IllegalArgumentException expected) { + } + } +} diff --git a/tests/iketests/src/java/com/android/ike/ikev2/IkeSessionStateMachineTest.java b/tests/iketests/src/java/com/android/ike/ikev2/IkeSessionStateMachineTest.java index 02aaa4c3..1dcf5b82 100644 --- a/tests/iketests/src/java/com/android/ike/ikev2/IkeSessionStateMachineTest.java +++ b/tests/iketests/src/java/com/android/ike/ikev2/IkeSessionStateMachineTest.java @@ -17,6 +17,9 @@ package com.android.ike.ikev2; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.mockito.Matchers.any; import static org.mockito.Matchers.eq; @@ -26,8 +29,13 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.content.Context; +import android.net.IpSecManager; +import android.net.IpSecManager.UdpEncapsulationSocket; +import android.os.Looper; import android.os.test.TestLooper; -import android.util.Pair; + +import androidx.test.InstrumentationRegistry; import com.android.ike.ikev2.ChildSessionStateMachineFactory.ChildSessionFactoryHelper; import com.android.ike.ikev2.ChildSessionStateMachineFactory.IChildSessionFactoryHelper; @@ -44,35 +52,46 @@ import com.android.ike.ikev2.message.IkePayload; import org.junit.After; import org.junit.Before; import org.junit.Test; +import org.mockito.ArgumentCaptor; +import java.net.InetAddress; import java.util.LinkedList; +import java.util.List; public final class IkeSessionStateMachineTest { + private static final String SERVER_ADDRESS = "192.0.2.100"; + + private UdpEncapsulationSocket mUdpEncapSocket; + private TestLooper mLooper; private IkeSessionStateMachine mIkeSessionStateMachine; + private IkeSessionOptions mIkeSessionOptions; + private ChildSessionOptions mChildSessionOptions; + private IIkeMessageHelper mMockIkeMessageHelper; private ISaRecordHelper mMockSaRecordHelper; - private IkeSessionOptions mMockIkeSessionOptions; private ChildSessionStateMachine mMockChildSessionStateMachine; - private ChildSessionOptions mMockChildSessionOptions; private IChildSessionFactoryHelper mMockChildSessionFactoryHelper; private IkeSaRecord mSpyCurrentIkeSaRecord; private IkeSaRecord mSpyLocalInitIkeSaRecord; private IkeSaRecord mSpyRemoteInitIkeSaRecord; + private ArgumentCaptor<IkeMessage> mIkeMessageCaptor = + ArgumentCaptor.forClass(IkeMessage.class); + private ReceivedIkePacket makeDummyUnencryptedReceivedIkePacket(int packetType) throws Exception { IkeMessage dummyIkeMessage = makeDummyIkeMessageForTest(0, 0, false, false); - Pair<IkeHeader, byte[]> dummyIkePacketPair = - new Pair<>(dummyIkeMessage.ikeHeader, new byte[0]); - when(mMockIkeMessageHelper.decode(dummyIkePacketPair.first, dummyIkePacketPair.second)) + byte[] dummyIkePacketBytes = new byte[0]; + + when(mMockIkeMessageHelper.decode(dummyIkeMessage.ikeHeader, dummyIkePacketBytes)) .thenReturn(dummyIkeMessage); when(mMockIkeMessageHelper.getMessageType(dummyIkeMessage)).thenReturn(packetType); - return new ReceivedIkePacket(dummyIkePacketPair); + return new ReceivedIkePacket(dummyIkeMessage.ikeHeader, dummyIkePacketBytes); } private ReceivedIkePacket makeDummyEncryptedReceivedIkePacket( @@ -81,16 +100,16 @@ public final class IkeSessionStateMachineTest { IkeMessage dummyIkeMessage = makeDummyIkeMessageForTest( ikeSaRecord.initiatorSpi, ikeSaRecord.responderSpi, fromIkeInit, true); - Pair<IkeHeader, byte[]> dummyIkePacketPair = - new Pair<>(dummyIkeMessage.ikeHeader, new byte[0]); + byte[] dummyIkePacketBytes = new byte[0]; + when(mMockIkeMessageHelper.decode( - mMockIkeSessionOptions, + mIkeSessionOptions, ikeSaRecord, - dummyIkePacketPair.first, - dummyIkePacketPair.second)) + dummyIkeMessage.ikeHeader, + dummyIkePacketBytes)) .thenReturn(dummyIkeMessage); when(mMockIkeMessageHelper.getMessageType(dummyIkeMessage)).thenReturn(packetType); - return new ReceivedIkePacket(dummyIkePacketPair); + return new ReceivedIkePacket(dummyIkeMessage.ikeHeader, dummyIkePacketBytes); } private IkeMessage makeDummyIkeMessageForTest( @@ -98,27 +117,21 @@ public final class IkeSessionStateMachineTest { int firstPayloadType = isEncrypted ? IkePayload.PAYLOAD_TYPE_SK : IkePayload.PAYLOAD_TYPE_NO_NEXT; IkeHeader header = - new IkeHeader(initSpi, respSpi, firstPayloadType, 0, true, fromikeInit, 0, 0); + new IkeHeader(initSpi, respSpi, firstPayloadType, 0, true, fromikeInit, 0); return new IkeMessage(header, new LinkedList<IkePayload>()); } private void verifyDecodeEncryptedMessage(IkeSaRecord record, ReceivedIkePacket rcvPacket) throws Exception { verify(mMockIkeMessageHelper) - .decode( - mMockIkeSessionOptions, - record, - rcvPacket.ikeHeader, - rcvPacket.ikePacketBytes); + .decode(mIkeSessionOptions, record, rcvPacket.ikeHeader, rcvPacket.ikePacketBytes); } public IkeSessionStateMachineTest() { mMockIkeMessageHelper = mock(IkeMessage.IIkeMessageHelper.class); mMockSaRecordHelper = mock(SaRecord.ISaRecordHelper.class); - mMockIkeSessionOptions = mock(IkeSessionOptions.class); mMockChildSessionStateMachine = mock(ChildSessionStateMachine.class); - mMockChildSessionOptions = mock(ChildSessionOptions.class); mMockChildSessionFactoryHelper = mock(IChildSessionFactoryHelper.class); mSpyCurrentIkeSaRecord = spy(new IkeSaRecord(11, 12, true, null, null)); @@ -132,17 +145,25 @@ public final class IkeSessionStateMachineTest { } @Before - public void setUp() { + public void setUp() throws Exception { + Context context = InstrumentationRegistry.getContext(); + IpSecManager ipSecManager = (IpSecManager) context.getSystemService(Context.IPSEC_SERVICE); + mUdpEncapSocket = ipSecManager.openUdpEncapsulationSocket(); + + mIkeSessionOptions = buildIkeSessionOptions(); + mChildSessionOptions = new ChildSessionOptions(); + // Setup thread and looper mLooper = new TestLooper(); mIkeSessionStateMachine = new IkeSessionStateMachine( "IkeSessionStateMachine", mLooper.getLooper(), - mMockIkeSessionOptions, - mMockChildSessionOptions); + mIkeSessionOptions, + mChildSessionOptions); mIkeSessionStateMachine.setDbg(true); mIkeSessionStateMachine.start(); + IkeMessage.setIkeMessageHelper(mMockIkeMessageHelper); SaRecord.setSaRecordHelper(mMockSaRecordHelper); ChildSessionStateMachineFactory.setChildSessionFactoryHelper( @@ -150,17 +171,46 @@ public final class IkeSessionStateMachineTest { } @After - public void tearDown() { + public void tearDown() throws Exception { mIkeSessionStateMachine.quit(); mIkeSessionStateMachine.setDbg(false); + mUdpEncapSocket.close(); + IkeMessage.setIkeMessageHelper(new IkeMessageHelper()); SaRecord.setSaRecordHelper(new SaRecordHelper()); ChildSessionStateMachineFactory.setChildSessionFactoryHelper( new ChildSessionFactoryHelper()); } + private IkeSessionOptions buildIkeSessionOptions() throws Exception { + SaProposal saProposal = + SaProposal.Builder.newIkeSaProposalBuilder() + .addEncryptionAlgorithm( + SaProposal.ENCRYPTION_ALGORITHM_AES_CBC, SaProposal.KEY_LEN_AES_128) + .addIntegrityAlgorithm(SaProposal.INTEGRITY_ALGORITHM_HMAC_SHA1_96) + .addPseudorandomFunction(SaProposal.PSEUDORANDOM_FUNCTION_HMAC_SHA1) + .addDhGroup(SaProposal.DH_GROUP_1024_BIT_MODP) + .build(); + + InetAddress serveAddress = InetAddress.getByName(SERVER_ADDRESS); + IkeSessionOptions sessionOptions = + new IkeSessionOptions.Builder(serveAddress, mUdpEncapSocket) + .addSaProposal(saProposal) + .build(); + return sessionOptions; + } + + private static boolean isIkePayloadExist( + List<IkePayload> payloadList, @IkePayload.PayloadType int payloadType) { + for (IkePayload payload : payloadList) { + if (payload.payloadType == payloadType) return true; + } + return false; + } + @Test public void testCreateIkeLocalIkeInit() throws Exception { + if (Looper.myLooper() == null) Looper.myLooper().prepare(); // Mock IKE_INIT response. ReceivedIkePacket dummyReceivedIkePacket = makeDummyUnencryptedReceivedIkePacket(IkeMessage.MESSAGE_TYPE_IKE_INIT_RESP); @@ -172,15 +222,37 @@ public final class IkeSessionStateMachineTest { IkeSessionStateMachine.CMD_RECEIVE_IKE_PACKET, dummyReceivedIkePacket); mLooper.dispatchAll(); + + // Validate outbound IKE INIT request + verify(mMockIkeMessageHelper).encode(mIkeMessageCaptor.capture()); + IkeMessage ikeInitReqMessage = mIkeMessageCaptor.getValue(); + + IkeHeader ikeHeader = ikeInitReqMessage.ikeHeader; + assertEquals(IkeHeader.EXCHANGE_TYPE_IKE_SA_INIT, ikeHeader.exchangeType); + assertFalse(ikeHeader.isResponseMsg); + assertTrue(ikeHeader.fromIkeInitiator); + + List<IkePayload> payloadList = ikeInitReqMessage.ikePayloadList; + assertTrue(isIkePayloadExist(payloadList, IkePayload.PAYLOAD_TYPE_SA)); + assertTrue(isIkePayloadExist(payloadList, IkePayload.PAYLOAD_TYPE_KE)); + assertTrue(isIkePayloadExist(payloadList, IkePayload.PAYLOAD_TYPE_NONCE)); + + IkeSocket ikeSocket = mIkeSessionStateMachine.mIkeSocket; + assertNotNull(ikeSocket); + assertNotEquals( + -1 /*not found*/, ikeSocket.mSpiToIkeSession.indexOfValue(mIkeSessionStateMachine)); + verify(mMockIkeMessageHelper) .decode(dummyReceivedIkePacket.ikeHeader, dummyReceivedIkePacket.ikePacketBytes); verify(mMockIkeMessageHelper).getMessageType(any()); + assertTrue( mIkeSessionStateMachine.getCurrentState() instanceof IkeSessionStateMachine.CreateIkeLocalIkeAuth); } private void mockIkeSetup() throws Exception { + if (Looper.myLooper() == null) Looper.myLooper().prepare(); // Mock IKE_INIT response ReceivedIkePacket dummyIkeInitRespReceivedPacket = makeDummyUnencryptedReceivedIkePacket(IkeMessage.MESSAGE_TYPE_IKE_INIT_RESP); diff --git a/tests/iketests/src/java/com/android/ike/ikev2/IkeSocketTest.java b/tests/iketests/src/java/com/android/ike/ikev2/IkeSocketTest.java new file mode 100644 index 00000000..5f15f554 --- /dev/null +++ b/tests/iketests/src/java/com/android/ike/ikev2/IkeSocketTest.java @@ -0,0 +1,376 @@ +/* + * 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.ikev2; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +import android.content.Context; +import android.net.IpSecManager; +import android.net.IpSecManager.UdpEncapsulationSocket; +import android.os.HandlerThread; +import android.os.Looper; +import android.system.ErrnoException; +import android.system.Os; +import android.system.OsConstants; +import android.util.Log; +import android.util.LongSparseArray; + +import androidx.test.InstrumentationRegistry; + +import com.android.ike.ikev2.IkeSocket.PacketReceiver; +import com.android.ike.ikev2.message.IkeHeader; +import com.android.ike.ikev2.message.TestUtils; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; + +import java.io.FileDescriptor; +import java.net.InetAddress; +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +public final class IkeSocketTest { + private static final int REMOTE_RECV_BUFF_SIZE = 2048; + private static final int TIMEOUT = 1000; + + private static final String NON_ESP_MARKER_HEX_STRING = "00000000"; + private static final String IKE_REQ_MESSAGE_HEX_STRING = + "5f54bf6d8b48e6e100000000000000002120220800000000" + + "00000150220000300000002c010100040300000c0100000c" + + "800e00800300000803000002030000080400000200000008" + + "020000022800008800020000b4a2faf4bb54878ae21d6385" + + "12ece55d9236fc5046ab6cef82220f421f3ce6361faf3656" + + "4ecb6d28798a94aad7b2b4b603ddeaaa5630adb9ece8ac37" + + "534036040610ebdd92f46bef84f0be7db860351843858f8a" + + "cf87056e272377f70c9f2d81e29c7b0ce4f291a3a72476bb" + + "0b278fd4b7b0a4c26bbeb08214c707137607958729000024" + + "c39b7f368f4681b89fa9b7be6465abd7c5f68b6ed5d3b4c7" + + "2cb4240eb5c464122900001c00004004e54f73b7d83f6beb" + + "881eab2051d8663f421d10b02b00001c00004005d915368c" + + "a036004cb578ae3e3fb268509aeab1900000002069936922" + + "8741c6d4ca094c93e242c9de19e7b7c60000000500000500"; + + private static final String LOCAL_SPI = "0000000000000000"; + private static final String REMOTE_SPI = "5f54bf6d8b48e6e1"; + + private static final String DATA_ONE = "one 1"; + private static final String DATA_TWO = "two 2"; + + private static final String IPV4_LOOPBACK = "127.0.0.1"; + + private byte[] mDataOne; + private byte[] mDataTwo; + + private long mLocalSpi; + private long mRemoteSpi; + + private LongSparseArray mSpiToIkeStateMachineMap; + private PacketReceiver mPacketReceiver; + + private UdpEncapsulationSocket mClientUdpEncapSocket; + private InetAddress mLocalAddress; + private FileDescriptor mDummyRemoteServerFd; + + private IkeSessionStateMachine mMockIkeSessionStateMachine; + + @Before + public void setUp() throws Exception { + Context context = InstrumentationRegistry.getContext(); + IpSecManager ipSecManager = (IpSecManager) context.getSystemService(Context.IPSEC_SERVICE); + mClientUdpEncapSocket = ipSecManager.openUdpEncapsulationSocket(); + + mLocalAddress = InetAddress.getByName(IPV4_LOOPBACK); + mDummyRemoteServerFd = getBoundUdpSocket(mLocalAddress); + + mDataOne = DATA_ONE.getBytes("UTF-8"); + mDataTwo = DATA_TWO.getBytes("UTF-8"); + + ByteBuffer localSpiBuffer = ByteBuffer.wrap(TestUtils.hexStringToByteArray(LOCAL_SPI)); + mLocalSpi = localSpiBuffer.getLong(); + ByteBuffer remoteSpiBuffer = ByteBuffer.wrap(TestUtils.hexStringToByteArray(REMOTE_SPI)); + mRemoteSpi = remoteSpiBuffer.getLong(); + + mMockIkeSessionStateMachine = mock(IkeSessionStateMachine.class); + + mSpiToIkeStateMachineMap = new LongSparseArray<IkeSessionStateMachine>(); + mSpiToIkeStateMachineMap.put(mLocalSpi, mMockIkeSessionStateMachine); + + mPacketReceiver = new IkeSocket.PacketReceiver(); + } + + @After + public void tearDown() throws Exception { + mClientUdpEncapSocket.close(); + IkeSocket.setPacketReceiver(mPacketReceiver); + Os.close(mDummyRemoteServerFd); + } + + private static FileDescriptor getBoundUdpSocket(InetAddress address) throws Exception { + FileDescriptor sock = + Os.socket(OsConstants.AF_INET, OsConstants.SOCK_DGRAM, OsConstants.IPPROTO_UDP); + Os.bind(sock, address, IkeSocket.IKE_SERVER_PORT); + return sock; + } + + @Test + public void testGetAndCloseIkeSocket() throws Exception { + if (Looper.myLooper() == null) Looper.myLooper().prepare(); + + IkeSocket ikeSocketOne = IkeSocket.getIkeSocket(mClientUdpEncapSocket); + assertEquals(1, ikeSocketOne.mRefCount); + + IkeSocket ikeSocketTwo = IkeSocket.getIkeSocket(mClientUdpEncapSocket); + assertEquals(ikeSocketOne, ikeSocketTwo); + assertEquals(2, ikeSocketTwo.mRefCount); + + ikeSocketOne.releaseReference(); + assertEquals(1, ikeSocketOne.mRefCount); + + ikeSocketTwo.releaseReference(); + assertEquals(0, ikeSocketTwo.mRefCount); + } + + @Test + public void testSendIkePacket() throws Exception { + if (Looper.myLooper() == null) Looper.myLooper().prepare(); + + // Send IKE packet + IkeSocket ikeSocket = IkeSocket.getIkeSocket(mClientUdpEncapSocket); + ikeSocket.sendIkePacket(mDataOne, mLocalAddress); + + byte[] receivedData = receive(mDummyRemoteServerFd); + + // Verify received data + ByteBuffer expectedBuffer = + ByteBuffer.allocate(IkeSocket.NON_ESP_MARKER_LEN + mDataOne.length); + expectedBuffer.put(IkeSocket.NON_ESP_MARKER).put(mDataOne); + + assertArrayEquals(expectedBuffer.array(), receivedData); + + ikeSocket.releaseReference(); + } + + @Test + public void testReceiveIkePacket() throws Exception { + // Create working thread. + HandlerThread mIkeThread = new HandlerThread("IkeSocketTest"); + mIkeThread.start(); + + // Create IkeSocket on working thread. + IkeSocketReceiver socketReceiver = new IkeSocketReceiver(); + TestCountDownLatch createLatch = new TestCountDownLatch(); + mIkeThread + .getThreadHandler() + .post( + () -> { + try { + socketReceiver.setIkeSocket( + IkeSocket.getIkeSocket(mClientUdpEncapSocket)); + createLatch.countDown(); + Log.d("IkeSocketTest", "IkeSocket created."); + } catch (ErrnoException e) { + Log.e("IkeSocketTest", "error encountered creating IkeSocket ", e); + } + }); + createLatch.await(); + + IkeSocket ikeSocket = socketReceiver.getIkeSocket(); + assertNotNull(ikeSocket); + + // Configure IkeSocket + TestCountDownLatch receiveLatch = new TestCountDownLatch(); + DummyPacketReceiver packetReceiver = new DummyPacketReceiver(receiveLatch); + IkeSocket.setPacketReceiver(packetReceiver); + + // Send first packet. + sendToIkeSocket(mDummyRemoteServerFd, mDataOne, mLocalAddress); + receiveLatch.await(); + + assertEquals(1, ikeSocket.numPacketsReceived()); + assertArrayEquals(mDataOne, packetReceiver.mReceivedData); + + // Send second packet. + sendToIkeSocket(mDummyRemoteServerFd, mDataTwo, mLocalAddress); + receiveLatch.await(); + + assertEquals(2, ikeSocket.numPacketsReceived()); + assertArrayEquals(mDataTwo, packetReceiver.mReceivedData); + + // Close IkeSocket. + TestCountDownLatch closeLatch = new TestCountDownLatch(); + ikeSocket + .getHandler() + .post( + () -> { + ikeSocket.releaseReference(); + closeLatch.countDown(); + }); + closeLatch.await(); + + mIkeThread.quitSafely(); + } + + @Test + public void testHandlePacket() throws Exception { + byte[] recvBuf = + TestUtils.hexStringToByteArray( + NON_ESP_MARKER_HEX_STRING + IKE_REQ_MESSAGE_HEX_STRING); + + mPacketReceiver.handlePacket(recvBuf, mSpiToIkeStateMachineMap); + + byte[] expectedIkePacketBytes = TestUtils.hexStringToByteArray(IKE_REQ_MESSAGE_HEX_STRING); + ArgumentCaptor<IkeHeader> ikeHeaderCaptor = ArgumentCaptor.forClass(IkeHeader.class); + verify(mMockIkeSessionStateMachine) + .receiveIkePacket(ikeHeaderCaptor.capture(), eq(expectedIkePacketBytes)); + + IkeHeader capturedIkeHeader = ikeHeaderCaptor.getValue(); + assertEquals(mRemoteSpi, capturedIkeHeader.ikeInitiatorSpi); + assertEquals(mLocalSpi, capturedIkeHeader.ikeResponderSpi); + } + + @Test + public void testHandleEspPacket() throws Exception { + byte[] recvBuf = + TestUtils.hexStringToByteArray( + NON_ESP_MARKER_HEX_STRING + IKE_REQ_MESSAGE_HEX_STRING); + // Modify Non-ESP Marker + recvBuf[0] = 1; + + mPacketReceiver.handlePacket(recvBuf, mSpiToIkeStateMachineMap); + + verify(mMockIkeSessionStateMachine, never()).receiveIkePacket(any(), any()); + } + + @Test + public void testHandlePacketWithMalformedHeader() throws Exception { + String malformedIkePacketHexString = "5f54bf6d8b48e6e100000000000000002120220800000000"; + byte[] recvBuf = + TestUtils.hexStringToByteArray( + NON_ESP_MARKER_HEX_STRING + malformedIkePacketHexString); + + mPacketReceiver.handlePacket(recvBuf, mSpiToIkeStateMachineMap); + + verify(mMockIkeSessionStateMachine, never()).receiveIkePacket(any(), any()); + } + + private byte[] receive(FileDescriptor mfd) throws Exception { + byte[] receiveBuffer = new byte[REMOTE_RECV_BUFF_SIZE]; + AtomicInteger bytesRead = new AtomicInteger(-1); + Thread receiveThread = + new Thread( + () -> { + while (bytesRead.get() < 0) { + try { + bytesRead.set( + Os.recvfrom( + mDummyRemoteServerFd, + receiveBuffer, + 0, + REMOTE_RECV_BUFF_SIZE, + 0, + null)); + } catch (Exception e) { + Log.e( + "IkeSocketTest", + "Error encountered reading from socket", + e); + } + } + Log.d( + "IkeSocketTest", + "Packet received with size of " + bytesRead.get()); + }); + + receiveThread.start(); + receiveThread.join(TIMEOUT); + + return Arrays.copyOfRange(receiveBuffer, 0, bytesRead.get()); + } + + private void sendToIkeSocket(FileDescriptor fd, byte[] data, InetAddress destAddress) + throws Exception { + Os.sendto(fd, data, 0, data.length, 0, destAddress, mClientUdpEncapSocket.getPort()); + } + + private static class IkeSocketReceiver { + private IkeSocket mIkeSocket; + + void setIkeSocket(IkeSocket ikeSocket) { + mIkeSocket = ikeSocket; + } + + IkeSocket getIkeSocket() { + return mIkeSocket; + } + } + + private static class DummyPacketReceiver implements IkeSocket.IPacketReceiver { + byte[] mReceivedData = null; + final TestCountDownLatch mLatch; + + DummyPacketReceiver(TestCountDownLatch latch) { + mLatch = latch; + } + + public void handlePacket( + byte[] revbuf, LongSparseArray<IkeSessionStateMachine> spiToIkeSession) { + mReceivedData = Arrays.copyOfRange(revbuf, 0, revbuf.length); + mLatch.countDown(); + Log.d("IkeSocketTest", "Packet received"); + } + } + + private static class TestCountDownLatch { + private CountDownLatch mLatch; + + TestCountDownLatch() { + reset(); + } + + private void reset() { + mLatch = new CountDownLatch(1); + } + + void countDown() { + mLatch.countDown(); + } + + void await() { + try { + if (!mLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)) { + fail("Time out"); + } + } catch (InterruptedException e) { + fail(e.toString()); + } + reset(); + } + } +} diff --git a/tests/iketests/src/java/com/android/ike/ikev2/SaProposalTest.java b/tests/iketests/src/java/com/android/ike/ikev2/SaProposalTest.java index 2ce2a9d9..7f40d729 100644 --- a/tests/iketests/src/java/com/android/ike/ikev2/SaProposalTest.java +++ b/tests/iketests/src/java/com/android/ike/ikev2/SaProposalTest.java @@ -64,18 +64,19 @@ public final class SaProposalTest { .addIntegrityAlgorithm(SaProposal.INTEGRITY_ALGORITHM_HMAC_SHA1_96) .addPseudorandomFunction(SaProposal.PSEUDORANDOM_FUNCTION_AES128_XCBC) .addDhGroup(SaProposal.DH_GROUP_1024_BIT_MODP) - .buildOrThrow(); + .build(); - assertEquals(IkePayload.PROTOCOL_ID_IKE, proposal.mProtocolId); + assertEquals(IkePayload.PROTOCOL_ID_IKE, proposal.getProtocolId()); assertArrayEquals( new EncryptionTransform[] {mEncryption3DesTransform}, - proposal.mEncryptionAlgorithms); + proposal.getEncryptionTransforms()); assertArrayEquals( new IntegrityTransform[] {mIntegrityHmacSha1Transform}, - proposal.mIntegrityAlgorithms); + proposal.getIntegrityTransforms()); assertArrayEquals( - new PrfTransform[] {mPrfAes128XCbcTransform}, proposal.mPseudorandomFunctions); - assertArrayEquals(new DhGroupTransform[] {mDhGroup1024Transform}, proposal.mDhGroups); + new PrfTransform[] {mPrfAes128XCbcTransform}, proposal.getPrfTransforms()); + assertArrayEquals( + new DhGroupTransform[] {mDhGroup1024Transform}, proposal.getDhGroupTransforms()); } @Test @@ -87,16 +88,17 @@ public final class SaProposalTest { SaProposal.KEY_LEN_AES_128) .addPseudorandomFunction(SaProposal.PSEUDORANDOM_FUNCTION_AES128_XCBC) .addDhGroup(SaProposal.DH_GROUP_1024_BIT_MODP) - .buildOrThrow(); + .build(); - assertEquals(IkePayload.PROTOCOL_ID_IKE, proposal.mProtocolId); + assertEquals(IkePayload.PROTOCOL_ID_IKE, proposal.getProtocolId()); assertArrayEquals( new EncryptionTransform[] {mEncryptionAesGcm8Transform}, - proposal.mEncryptionAlgorithms); + proposal.getEncryptionTransforms()); + assertArrayEquals( + new PrfTransform[] {mPrfAes128XCbcTransform}, proposal.getPrfTransforms()); assertArrayEquals( - new PrfTransform[] {mPrfAes128XCbcTransform}, proposal.mPseudorandomFunctions); - assertArrayEquals(new DhGroupTransform[] {mDhGroup1024Transform}, proposal.mDhGroups); - assertTrue(proposal.mIntegrityAlgorithms.length == 0); + new DhGroupTransform[] {mDhGroup1024Transform}, proposal.getDhGroupTransforms()); + assertTrue(proposal.getIntegrityTransforms().length == 0); } @Test @@ -107,16 +109,17 @@ public final class SaProposalTest { SaProposal.ENCRYPTION_ALGORITHM_AES_GCM_8, SaProposal.KEY_LEN_AES_128) .addIntegrityAlgorithm(SaProposal.INTEGRITY_ALGORITHM_NONE) - .buildOrThrow(); + .build(); - assertEquals(IkePayload.PROTOCOL_ID_ESP, proposal.mProtocolId); + assertEquals(IkePayload.PROTOCOL_ID_ESP, proposal.getProtocolId()); assertArrayEquals( new EncryptionTransform[] {mEncryptionAesGcm8Transform}, - proposal.mEncryptionAlgorithms); + proposal.getEncryptionTransforms()); assertArrayEquals( - new IntegrityTransform[] {mIntegrityNoneTransform}, proposal.mIntegrityAlgorithms); - assertTrue(proposal.mPseudorandomFunctions.length == 0); - assertTrue(proposal.mDhGroups.length == 0); + new IntegrityTransform[] {mIntegrityNoneTransform}, + proposal.getIntegrityTransforms()); + assertTrue(proposal.getPrfTransforms().length == 0); + assertTrue(proposal.getDhGroupTransforms().length == 0); } @Test @@ -127,23 +130,25 @@ public final class SaProposalTest { builder.addEncryptionAlgorithm(SaProposal.ENCRYPTION_ALGORITHM_3DES) .addIntegrityAlgorithm(SaProposal.INTEGRITY_ALGORITHM_NONE) .addDhGroup(SaProposal.DH_GROUP_1024_BIT_MODP) - .buildOrThrow(); + .build(); - assertEquals(IkePayload.PROTOCOL_ID_ESP, proposal.mProtocolId); + assertEquals(IkePayload.PROTOCOL_ID_ESP, proposal.getProtocolId()); assertArrayEquals( new EncryptionTransform[] {mEncryption3DesTransform}, - proposal.mEncryptionAlgorithms); + proposal.getEncryptionTransforms()); + assertArrayEquals( + new IntegrityTransform[] {mIntegrityNoneTransform}, + proposal.getIntegrityTransforms()); assertArrayEquals( - new IntegrityTransform[] {mIntegrityNoneTransform}, proposal.mIntegrityAlgorithms); - assertArrayEquals(new DhGroupTransform[] {mDhGroup1024Transform}, proposal.mDhGroups); - assertTrue(proposal.mPseudorandomFunctions.length == 0); + new DhGroupTransform[] {mDhGroup1024Transform}, proposal.getDhGroupTransforms()); + assertTrue(proposal.getPrfTransforms().length == 0); } @Test public void testBuildEncryptAlgosWithNoAlgorithm() throws Exception { Builder builder = Builder.newIkeSaProposalBuilder(); try { - builder.buildOrThrow(); + builder.build(); fail("Expected to fail when no encryption algorithm is proposed."); } catch (IllegalArgumentException expected) { @@ -179,7 +184,7 @@ public final class SaProposalTest { public void testBuildIkeProposalWithoutPrf() throws Exception { Builder builder = Builder.newIkeSaProposalBuilder(); try { - builder.addEncryptionAlgorithm(SaProposal.ENCRYPTION_ALGORITHM_3DES).buildOrThrow(); + builder.addEncryptionAlgorithm(SaProposal.ENCRYPTION_ALGORITHM_3DES).build(); fail("Expected to fail when PRF is not provided in IKE SA proposal."); } catch (IllegalArgumentException expected) { @@ -192,7 +197,7 @@ public final class SaProposalTest { try { builder.addEncryptionAlgorithm(SaProposal.ENCRYPTION_ALGORITHM_3DES) .addPseudorandomFunction(SaProposal.PSEUDORANDOM_FUNCTION_HMAC_SHA1) - .buildOrThrow(); + .build(); fail("Expected to fail when PRF is provided in Child SA proposal."); } catch (IllegalArgumentException expected) { @@ -209,7 +214,7 @@ public final class SaProposalTest { builder.addEncryptionAlgorithm(SaProposal.ENCRYPTION_ALGORITHM_AES_GCM_12) .addIntegrityAlgorithm(SaProposal.INTEGRITY_ALGORITHM_NONE) .addIntegrityAlgorithm(SaProposal.INTEGRITY_ALGORITHM_HMAC_SHA1_96) - .buildOrThrow(); + .build(); fail("Expected to fail when not-none integrity algorithm is proposed with AEAD"); } catch (IllegalArgumentException expected) { @@ -225,7 +230,7 @@ public final class SaProposalTest { try { builder.addEncryptionAlgorithm(SaProposal.ENCRYPTION_ALGORITHM_3DES) .addPseudorandomFunction(SaProposal.PSEUDORANDOM_FUNCTION_HMAC_SHA1) - .buildOrThrow(); + .build(); fail( "Expected to fail when" @@ -245,7 +250,7 @@ public final class SaProposalTest { .addPseudorandomFunction(SaProposal.PSEUDORANDOM_FUNCTION_HMAC_SHA1) .addIntegrityAlgorithm(SaProposal.INTEGRITY_ALGORITHM_NONE) .addIntegrityAlgorithm(SaProposal.INTEGRITY_ALGORITHM_HMAC_SHA1_96) - .buildOrThrow(); + .build(); fail( "Expected to fail when none-value integrity algorithm is proposed" @@ -262,7 +267,7 @@ public final class SaProposalTest { builder.addEncryptionAlgorithm(SaProposal.ENCRYPTION_ALGORITHM_3DES) .addIntegrityAlgorithm(SaProposal.INTEGRITY_ALGORITHM_HMAC_SHA1_96) .addPseudorandomFunction(SaProposal.PSEUDORANDOM_FUNCTION_AES128_XCBC) - .buildOrThrow(); + .build(); fail("Expected to fail when no DH Group is proposed in IKE SA proposal."); } catch (IllegalArgumentException expected) { @@ -279,7 +284,7 @@ public final class SaProposalTest { .addPseudorandomFunction(SaProposal.PSEUDORANDOM_FUNCTION_AES128_XCBC) .addDhGroup(SaProposal.DH_GROUP_1024_BIT_MODP) .addDhGroup(SaProposal.DH_GROUP_NONE) - .buildOrThrow(); + .build(); fail("Expected to fail when none-value DH Group is proposed in IKE SA proposal."); } catch (IllegalArgumentException expected) { @@ -295,7 +300,7 @@ public final class SaProposalTest { builder.addEncryptionAlgorithm(SaProposal.ENCRYPTION_ALGORITHM_3DES) .addIntegrityAlgorithm(SaProposal.INTEGRITY_ALGORITHM_HMAC_SHA1_96) .addDhGroup(SaProposal.DH_GROUP_1024_BIT_MODP) - .buildOrThrow(); + .build(); fail( "Expected to fail when" diff --git a/tests/iketests/src/java/com/android/ike/ikev2/SaRecordTest.java b/tests/iketests/src/java/com/android/ike/ikev2/SaRecordTest.java new file mode 100644 index 00000000..5c61105c --- /dev/null +++ b/tests/iketests/src/java/com/android/ike/ikev2/SaRecordTest.java @@ -0,0 +1,138 @@ +/* + * 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.ikev2; + +import static org.junit.Assert.assertArrayEquals; + +import com.android.ike.ikev2.message.TestUtils; + +import org.junit.Test; + +public final class SaRecordTest { + private static final String IKE_INIT_SPI = "5F54BF6D8B48E6E1"; + private static final String IKE_RESP_SPI = "909232B3D1EDCB5C"; + + private static final String IKE_NONCE_INIT_HEX_STRING = + "C39B7F368F4681B89FA9B7BE6465ABD7C5F68B6ED5D3B4C72CB4240EB5C46412"; + private static final String IKE_NONCE_RESP_HEX_STRING = + "9756112CA539F5C25ABACC7EE92B73091942A9C06950F98848F1AF1694C4DDFF"; + + private static final String IKE_SHARED_DH_KEY_HEX_STRING = + "C14155DEA40056BD9C76FB4819687B7A397582F4CD5AFF4B" + + "8F441C56E0C08C84234147A0BA249A555835A048E3CA2980" + + "7D057A61DD26EEFAD9AF9C01497005E52858E29FB42EB849" + + "6731DF96A11CCE1F51137A9A1B900FA81AEE7898E373D4E4" + + "8B899BBECA091314ECD4B6E412EF4B0FEF798F54735F3180" + + "7424A318287F20E8"; + + private static final String IKE_SKEYSEED_HEX_STRING = + "8C42F3B1F5F81C7BAAC5F33E9A4F01987B2F9657"; + private static final String IKE_SK_D_HEX_STRING = "C86B56EFCF684DCC2877578AEF3137167FE0EBF6"; + private static final String IKE_SK_AUTH_INIT_HEX_STRING = + "554FBF5A05B7F511E05A30CE23D874DB9EF55E51"; + private static final String IKE_SK_AUTH_RESP_HEX_STRING = + "36D83420788337CA32ECAA46892C48808DCD58B1"; + private static final String IKE_SK_ENCR_INIT_HEX_STRING = "5CBFD33F75796C0188C4A3A546AEC4A1"; + private static final String IKE_SK_ENCR_RESP_HEX_STRING = "C33B35FCF29514CD9D8B4A695E1A816E"; + private static final String IKE_SK_PRF_INIT_HEX_STRING = + "094787780EE466E2CB049FA327B43908BC57E485"; + private static final String IKE_SK_PRF_RESP_HEX_STRING = + "A30E6B08BE56C0E6BFF4744143C75219299E1BEB"; + private static final String IKE_KEY_MAT = + IKE_SK_D_HEX_STRING + + IKE_SK_AUTH_INIT_HEX_STRING + + IKE_SK_AUTH_RESP_HEX_STRING + + IKE_SK_ENCR_INIT_HEX_STRING + + IKE_SK_ENCR_RESP_HEX_STRING + + IKE_SK_PRF_INIT_HEX_STRING + + IKE_SK_PRF_RESP_HEX_STRING; + + private static final int IKE_AUTH_ALGO_KEY_LEN = 20; + private static final int IKE_ENCR_ALGO_KEY_LEN = 16; + private static final int IKE_PRF_KEY_LEN = 20; + private static final int IKE_SK_D_KEY_LEN = IKE_PRF_KEY_LEN; + + private static final String FIRST_CHILD_ENCR_INIT_HEX_STRING = + "1B865CEA6E2C23973E8C5452ADC5CD7D"; + private static final String FIRST_CHILD_ENCR_RESP_HEX_STRING = + "5E82FEDACC6DCB0756DDD7553907EBD1"; + private static final String FIRST_CHILD_AUTH_INIT_HEX_STRING = + "A7A5A44F7EF4409657206C7DC52B7E692593B51E"; + private static final String FIRST_CHILD_AUTH_RESP_HEX_STRING = + "CDE612189FD46DE870FAEC04F92B40B0BFDBD9E1"; + private static final String FIRST_CHILD_KEY_MAT = + FIRST_CHILD_ENCR_INIT_HEX_STRING + + FIRST_CHILD_AUTH_INIT_HEX_STRING + + FIRST_CHILD_ENCR_RESP_HEX_STRING + + FIRST_CHILD_AUTH_RESP_HEX_STRING; + + private static final int FIRST_CHILD_AUTH_ALGO_KEY_LEN = 20; + private static final int FIRST_CHILD_ENCR_ALGO_KEY_LEN = 16; + + private static final String PRF_HMAC_SHA1_ALGO_NAME = "HmacSHA1"; + + @Test + public void testCalculateSKeySeed() throws Exception { + byte[] nonceInit = TestUtils.hexStringToByteArray(IKE_NONCE_INIT_HEX_STRING); + byte[] nonceResp = TestUtils.hexStringToByteArray(IKE_NONCE_RESP_HEX_STRING); + byte[] sharedDhKey = TestUtils.hexStringToByteArray(IKE_SHARED_DH_KEY_HEX_STRING); + + byte[] calculatedSKeySeed = + SaRecord.generateSKeySeed( + PRF_HMAC_SHA1_ALGO_NAME, nonceInit, nonceResp, sharedDhKey); + + byte[] expectedSKeySeed = TestUtils.hexStringToByteArray(IKE_SKEYSEED_HEX_STRING); + assertArrayEquals(expectedSKeySeed, calculatedSKeySeed); + } + + @Test + public void testSignWithPrfPlusForIke() throws Exception { + byte[] prfKey = TestUtils.hexStringToByteArray(IKE_SKEYSEED_HEX_STRING); + byte[] prfData = + TestUtils.hexStringToByteArray( + IKE_NONCE_INIT_HEX_STRING + + IKE_NONCE_RESP_HEX_STRING + + IKE_INIT_SPI + + IKE_RESP_SPI); + int keyMaterialLen = + IKE_SK_D_KEY_LEN + + IKE_AUTH_ALGO_KEY_LEN * 2 + + IKE_ENCR_ALGO_KEY_LEN * 2 + + IKE_PRF_KEY_LEN * 2; + + byte[] calculatedKeyMat = + SaRecord.generateKeyMat(PRF_HMAC_SHA1_ALGO_NAME, prfKey, prfData, keyMaterialLen); + + byte[] expectedKeyMat = TestUtils.hexStringToByteArray(IKE_KEY_MAT); + assertArrayEquals(expectedKeyMat, calculatedKeyMat); + } + + @Test + public void testSignWithPrfPlusForFirstChild() throws Exception { + byte[] prfKey = TestUtils.hexStringToByteArray(IKE_SK_D_HEX_STRING); + byte[] prfData = + TestUtils.hexStringToByteArray( + IKE_NONCE_INIT_HEX_STRING + IKE_NONCE_RESP_HEX_STRING); + int keyMaterialLen = FIRST_CHILD_AUTH_ALGO_KEY_LEN * 2 + FIRST_CHILD_ENCR_ALGO_KEY_LEN * 2; + + byte[] calculatedKeyMat = + SaRecord.generateKeyMat(PRF_HMAC_SHA1_ALGO_NAME, prfKey, prfData, keyMaterialLen); + + byte[] expectedKeyMat = TestUtils.hexStringToByteArray(FIRST_CHILD_KEY_MAT); + assertArrayEquals(expectedKeyMat, calculatedKeyMat); + } +} diff --git a/tests/iketests/src/java/com/android/ike/ikev2/message/IkeDeletePayloadTest.java b/tests/iketests/src/java/com/android/ike/ikev2/message/IkeDeletePayloadTest.java new file mode 100644 index 00000000..1a7b9a29 --- /dev/null +++ b/tests/iketests/src/java/com/android/ike/ikev2/message/IkeDeletePayloadTest.java @@ -0,0 +1,144 @@ +/* + * 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.ikev2.message; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import com.android.ike.ikev2.exceptions.InvalidSyntaxException; + +import org.junit.Test; + +import java.nio.ByteBuffer; + +public final class IkeDeletePayloadTest { + private static final String DELETE_IKE_PAYLOAD_HEX_STRING = "0000000801000000"; + private static final String DELETE_CHILD_PAYLOAD_HEX_STRING = "0000000c030400012ad4c0a2"; + private static final String CHILD_SPI = "2ad4c0a2"; + + private static final int NUM_CHILD_SPI = 1; + + private static final int PROTOCOL_ID_OFFSET = 4; + private static final int SPI_SIZE_OFFSET = 5; + private static final int NUM_OF_SPI_OFFSET = 6; + + @Test + public void testDecodeDeleteIkePayload() throws Exception { + ByteBuffer inputBuffer = + ByteBuffer.wrap(TestUtils.hexStringToByteArray(DELETE_IKE_PAYLOAD_HEX_STRING)); + + IkePayload payload = + IkePayloadFactory.getIkePayload( + IkePayload.PAYLOAD_TYPE_DELETE, false /*is request*/, inputBuffer) + .first; + + assertTrue(payload instanceof IkeDeletePayload); + + IkeDeletePayload deletePayload = (IkeDeletePayload) payload; + assertEquals(IkePayload.PROTOCOL_ID_IKE, deletePayload.protocolId); + assertEquals(IkePayload.SPI_LEN_NOT_INCLUDED, deletePayload.spiSize); + assertEquals(0, deletePayload.numSpi); + assertArrayEquals(new int[0], deletePayload.spisToDelete); + } + + @Test + public void testDecodeDeleteChildPayload() throws Exception { + ByteBuffer inputBuffer = + ByteBuffer.wrap(TestUtils.hexStringToByteArray(DELETE_CHILD_PAYLOAD_HEX_STRING)); + + IkePayload payload = + IkePayloadFactory.getIkePayload( + IkePayload.PAYLOAD_TYPE_DELETE, false /*is request*/, inputBuffer) + .first; + + assertTrue(payload instanceof IkeDeletePayload); + + IkeDeletePayload deletePayload = (IkeDeletePayload) payload; + assertEquals(IkePayload.PROTOCOL_ID_ESP, deletePayload.protocolId); + assertEquals(IkePayload.SPI_LEN_IPSEC, deletePayload.spiSize); + assertEquals(NUM_CHILD_SPI, deletePayload.numSpi); + + byte[] childSpiBytes = TestUtils.hexStringToByteArray(CHILD_SPI); + ByteBuffer buffer = ByteBuffer.wrap(childSpiBytes); + int expectedChildSpi = buffer.getInt(); + + assertArrayEquals(new int[] {expectedChildSpi}, deletePayload.spisToDelete); + } + + @Test + public void testDecodeWithInvalidProtocol() throws Exception { + byte[] deletePayloadBytes = TestUtils.hexStringToByteArray(DELETE_IKE_PAYLOAD_HEX_STRING); + deletePayloadBytes[PROTOCOL_ID_OFFSET] = -1; + ByteBuffer inputBuffer = ByteBuffer.wrap(deletePayloadBytes); + + try { + IkePayloadFactory.getIkePayload( + IkePayload.PAYLOAD_TYPE_DELETE, false /*is request*/, inputBuffer); + fail("Expected to fail due to unrecognized protocol ID."); + } catch (InvalidSyntaxException expected) { + + } + } + + @Test + public void testDecodeWithInvalidSpiSize() throws Exception { + byte[] deletePayloadBytes = TestUtils.hexStringToByteArray(DELETE_IKE_PAYLOAD_HEX_STRING); + deletePayloadBytes[SPI_SIZE_OFFSET] = IkePayload.SPI_LEN_IPSEC; + ByteBuffer inputBuffer = ByteBuffer.wrap(deletePayloadBytes); + + try { + IkePayloadFactory.getIkePayload( + IkePayload.PAYLOAD_TYPE_DELETE, false /*is request*/, inputBuffer); + fail("Expected to fail due to invalid SPI size in Delete IKE Payload."); + } catch (InvalidSyntaxException expected) { + + } + } + + @Test + public void testDecodeWithInvalidNumSpi() throws Exception { + byte[] deletePayloadBytes = TestUtils.hexStringToByteArray(DELETE_IKE_PAYLOAD_HEX_STRING); + deletePayloadBytes[NUM_OF_SPI_OFFSET] = 1; + ByteBuffer inputBuffer = ByteBuffer.wrap(deletePayloadBytes); + + try { + IkePayloadFactory.getIkePayload( + IkePayload.PAYLOAD_TYPE_DELETE, false /*is request*/, inputBuffer); + fail("Expected to fail because number of SPI is not zero in Delete IKE Payload."); + } catch (InvalidSyntaxException expected) { + + } + } + + @Test + public void testDecodeWithInvalidNumSpiAndSpiSize() throws Exception { + byte[] deletePayloadBytes = TestUtils.hexStringToByteArray(DELETE_IKE_PAYLOAD_HEX_STRING); + deletePayloadBytes[SPI_SIZE_OFFSET] = 1; + deletePayloadBytes[NUM_CHILD_SPI] = 4; + ByteBuffer inputBuffer = ByteBuffer.wrap(deletePayloadBytes); + + try { + IkePayloadFactory.getIkePayload( + IkePayload.PAYLOAD_TYPE_DELETE, false /*is request*/, inputBuffer); + fail("Expected to fail due to invalid SPI size in Delete IKE Payload."); + } catch (InvalidSyntaxException expected) { + + } + } +} diff --git a/tests/iketests/src/java/com/android/ike/ikev2/message/IkeEncryptedPayloadBodyTest.java b/tests/iketests/src/java/com/android/ike/ikev2/message/IkeEncryptedPayloadBodyTest.java index 48669e89..fcb3eff6 100644 --- a/tests/iketests/src/java/com/android/ike/ikev2/message/IkeEncryptedPayloadBodyTest.java +++ b/tests/iketests/src/java/com/android/ike/ikev2/message/IkeEncryptedPayloadBodyTest.java @@ -17,10 +17,14 @@ package com.android.ike.ikev2.message; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; import org.junit.Before; import org.junit.Test; +import java.security.GeneralSecurityException; +import java.util.Arrays; + import javax.crypto.Cipher; import javax.crypto.Mac; import javax.crypto.SecretKey; @@ -65,9 +69,40 @@ public final class IkeEncryptedPayloadBodyTest { private SecretKey mAesCbcKey; private Mac mHmacSha1IntegrityMac; + private byte[] mDataToPadAndEncrypt; + private byte[] mDataToAuthenticate; + private byte[] mEncryptedPaddedData; + private byte[] mIkeMessage; + + private byte[] mChecksum; + private byte[] mIv; + private byte[] mPadding; + // TODO: Add tests for authenticating and decrypting received message. @Before public void setUp() throws Exception { + mDataToPadAndEncrypt = + TestUtils.hexStringToByteArray(IKE_AUTH_INIT_REQUEST_UNENCRYPTED_DATA); + String hexStringToAuthenticate = + IKE_AUTH_INIT_REQUEST_HEADER + + IKE_AUTH_INIT_REQUEST_SK_HEADER + + IKE_AUTH_INIT_REQUEST_IV + + IKE_AUTH_INIT_REQUEST_ENCRYPT_PADDED_DATA; + mDataToAuthenticate = TestUtils.hexStringToByteArray(hexStringToAuthenticate); + mEncryptedPaddedData = + TestUtils.hexStringToByteArray(IKE_AUTH_INIT_REQUEST_ENCRYPT_PADDED_DATA); + mIkeMessage = + TestUtils.hexStringToByteArray( + IKE_AUTH_INIT_REQUEST_HEADER + + IKE_AUTH_INIT_REQUEST_SK_HEADER + + IKE_AUTH_INIT_REQUEST_IV + + IKE_AUTH_INIT_REQUEST_ENCRYPT_PADDED_DATA + + IKE_AUTH_INIT_REQUEST_CHECKSUM); + + mChecksum = TestUtils.hexStringToByteArray(IKE_AUTH_INIT_REQUEST_CHECKSUM); + mIv = TestUtils.hexStringToByteArray(IKE_AUTH_INIT_REQUEST_IV); + mPadding = TestUtils.hexStringToByteArray(IKE_AUTH_INIT_REQUEST_PADDING); + mAesCbcCipher = Cipher.getInstance(ENCR_ALGO_AES_CBC, IkeMessage.getSecurityProvider()); byte[] encryptKeyBytes = TestUtils.hexStringToByteArray(ENCR_KEY_FROM_INIT_TO_RESP); mAesCbcKey = new SecretKeySpec(encryptKeyBytes, ENCR_ALGO_AES_CBC); @@ -81,20 +116,30 @@ public final class IkeEncryptedPayloadBodyTest { @Test public void testCalculateChecksum() throws Exception { - String hexStringToAuthenticate = - IKE_AUTH_INIT_REQUEST_HEADER - + IKE_AUTH_INIT_REQUEST_SK_HEADER - + IKE_AUTH_INIT_REQUEST_IV - + IKE_AUTH_INIT_REQUEST_ENCRYPT_PADDED_DATA; - byte[] byteToAuthenticate = TestUtils.hexStringToByteArray(hexStringToAuthenticate); - - byte[] expectedCheckSum = TestUtils.hexStringToByteArray(IKE_AUTH_INIT_REQUEST_CHECKSUM); - byte[] calculatedChecksum = IkeEncryptedPayloadBody.calculateChecksum( - byteToAuthenticate, mHmacSha1IntegrityMac, HMAC_SHA1_CHECKSUM_LEN); + mDataToAuthenticate, mHmacSha1IntegrityMac, HMAC_SHA1_CHECKSUM_LEN); + + assertArrayEquals(mChecksum, calculatedChecksum); + } + + @Test + public void testValidateChecksum() throws Exception { + IkeEncryptedPayloadBody.validateChecksumOrThrow( + mDataToAuthenticate, mHmacSha1IntegrityMac, mChecksum); + } - assertArrayEquals(expectedCheckSum, calculatedChecksum); + @Test + public void testThrowForInvalidChecksum() throws Exception { + byte[] dataToAuthenticate = Arrays.copyOf(mDataToAuthenticate, mDataToAuthenticate.length); + dataToAuthenticate[0]++; + + try { + IkeEncryptedPayloadBody.validateChecksumOrThrow( + dataToAuthenticate, mHmacSha1IntegrityMac, mChecksum); + fail("Expected GeneralSecurityException due to mismatched checksum."); + } catch (GeneralSecurityException expected) { + } } @Test @@ -132,40 +177,37 @@ public final class IkeEncryptedPayloadBodyTest { @Test public void testEncrypt() throws Exception { - byte[] dataToEncrypt = - TestUtils.hexStringToByteArray(IKE_AUTH_INIT_REQUEST_UNENCRYPTED_DATA); - byte[] iv = TestUtils.hexStringToByteArray(IKE_AUTH_INIT_REQUEST_IV); - byte[] padding = TestUtils.hexStringToByteArray(IKE_AUTH_INIT_REQUEST_PADDING); - byte[] calculatedData = IkeEncryptedPayloadBody.encrypt( - dataToEncrypt, mAesCbcCipher, mAesCbcKey, iv, padding); - byte[] expectedData = - TestUtils.hexStringToByteArray(IKE_AUTH_INIT_REQUEST_ENCRYPT_PADDED_DATA); + mDataToPadAndEncrypt, mAesCbcCipher, mAesCbcKey, mIv, mPadding); + + assertArrayEquals(mEncryptedPaddedData, calculatedData); + } + + @Test + public void testDecrypt() throws Exception { + byte[] calculatedPlainText = + IkeEncryptedPayloadBody.decrypt( + mEncryptedPaddedData, mAesCbcCipher, mAesCbcKey, mIv); - assertArrayEquals(expectedData, calculatedData); + assertArrayEquals(mDataToPadAndEncrypt, calculatedPlainText); } @Test public void testBuildAndEncodeOutboundIkeEncryptedPayloadBody() throws Exception { - byte[] ikeAndPayloadHeader = - TestUtils.hexStringToByteArray( - IKE_AUTH_INIT_REQUEST_HEADER + IKE_AUTH_INIT_REQUEST_SK_HEADER); - byte[] unencryptedPayloads = - TestUtils.hexStringToByteArray(IKE_AUTH_INIT_REQUEST_UNENCRYPTED_DATA); - byte[] iv = TestUtils.hexStringToByteArray(IKE_AUTH_INIT_REQUEST_IV); - byte[] padding = TestUtils.hexStringToByteArray(IKE_AUTH_INIT_REQUEST_PADDING); + IkeHeader ikeHeader = new IkeHeader(mIkeMessage); IkeEncryptedPayloadBody paylaodBody = new IkeEncryptedPayloadBody( - ikeAndPayloadHeader, - unencryptedPayloads, + ikeHeader, + IkePayload.PAYLOAD_TYPE_ID_INITIATOR, + mDataToPadAndEncrypt, mHmacSha1IntegrityMac, HMAC_SHA1_CHECKSUM_LEN, mAesCbcCipher, mAesCbcKey, - iv, - padding); + mIv, + mPadding); byte[] expectedEncodedData = TestUtils.hexStringToByteArray( @@ -174,4 +216,17 @@ public final class IkeEncryptedPayloadBodyTest { + IKE_AUTH_INIT_REQUEST_CHECKSUM); assertArrayEquals(expectedEncodedData, paylaodBody.encode()); } + + @Test + public void testAuthenticateAndDecryptInboundIkeEncryptedPayloadBody() throws Exception { + IkeEncryptedPayloadBody paylaodBody = + new IkeEncryptedPayloadBody( + mIkeMessage, + mHmacSha1IntegrityMac, + HMAC_SHA1_CHECKSUM_LEN, + mAesCbcCipher, + mAesCbcKey); + + assertArrayEquals(mDataToPadAndEncrypt, paylaodBody.getUnencryptedData()); + } } 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 1bc52c3b..08b1612b 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 @@ -62,6 +62,7 @@ public final class IkeHeaderTest { private static final int IKE_MSG_ID = 0; private static final int IKE_MSG_LENGTH = 336; + private static final int IKE_MSG_BODY_LENGTH = IKE_MSG_LENGTH - IkeHeader.IKE_HEADER_LENGTH; // Byte offsets of version field in IKE message header. private static final int VERSION_OFFSET = 17; @@ -89,7 +90,7 @@ public final class IkeHeaderTest { assertFalse(header.isResponseMsg); assertTrue(header.fromIkeInitiator); assertEquals(IKE_MSG_ID, header.messageId); - assertEquals(IKE_MSG_LENGTH, header.messageLength); + assertEquals(IKE_MSG_LENGTH, header.getInboundMessageLength()); } @Test @@ -142,7 +143,7 @@ public final class IkeHeaderTest { IkeHeader header = new IkeHeader(inputPacket); ByteBuffer byteBuffer = ByteBuffer.allocate(IkeHeader.IKE_HEADER_LENGTH); - header.encodeToByteBuffer(byteBuffer); + header.encodeToByteBuffer(byteBuffer, IKE_MSG_BODY_LENGTH); byte[] expectedPacket = TestUtils.hexStringToByteArray(IKE_HEADER_HEX_STRING); assertArrayEquals(expectedPacket, byteBuffer.array()); diff --git a/tests/iketests/src/java/com/android/ike/ikev2/message/IkeKePayloadTest.java b/tests/iketests/src/java/com/android/ike/ikev2/message/IkeKePayloadTest.java index 4f45f26d..1bb0b709 100644 --- a/tests/iketests/src/java/com/android/ike/ikev2/message/IkeKePayloadTest.java +++ b/tests/iketests/src/java/com/android/ike/ikev2/message/IkeKePayloadTest.java @@ -18,11 +18,10 @@ package com.android.ike.ikev2.message; 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 android.util.Pair; - import com.android.ike.ikev2.IkeDhParams; import com.android.ike.ikev2.SaProposal; import com.android.ike.ikev2.exceptions.InvalidSyntaxException; @@ -100,6 +99,7 @@ public final class IkeKePayloadTest { IkeKePayload payload = new IkeKePayload(CRITICAL_BIT, inputPacket); + assertFalse(payload.isOutbound); assertEquals(EXPECTED_DH_GROUP, payload.dhGroup); byte[] keyExchangeData = TestUtils.hexStringToByteArray(KEY_EXCHANGE_DATA_RAW_PACKET); @@ -138,11 +138,11 @@ public final class IkeKePayloadTest { @Test public void testGetIkeKePayload() throws Exception { - Pair<DHPrivateKeySpec, IkeKePayload> pair = - IkeKePayload.getKePayload(SaProposal.DH_GROUP_1024_BIT_MODP); + IkeKePayload payload = new IkeKePayload(SaProposal.DH_GROUP_1024_BIT_MODP); // Test DHPrivateKeySpec - DHPrivateKeySpec privateKeySpec = pair.first; + assertTrue(payload.isOutbound); + DHPrivateKeySpec privateKeySpec = payload.localPrivateKey; BigInteger primeValue = privateKeySpec.getP(); BigInteger expectedPrimeValue = new BigInteger(IkeDhParams.PRIME_1024_BIT_MODP, 16); @@ -153,8 +153,6 @@ public final class IkeKePayloadTest { assertEquals(0, expectedGenValue.compareTo(genValue)); // Test IkeKePayload - IkeKePayload payload = pair.second; - assertEquals(EXPECTED_DH_GROUP, payload.dhGroup); assertEquals(EXPECTED_KE_DATA_LEN, payload.keyExchangeData.length); } diff --git a/tests/iketests/src/java/com/android/ike/ikev2/message/IkeSaPayloadTest.java b/tests/iketests/src/java/com/android/ike/ikev2/message/IkeSaPayloadTest.java index b0d927d8..afb3a810 100644 --- a/tests/iketests/src/java/com/android/ike/ikev2/message/IkeSaPayloadTest.java +++ b/tests/iketests/src/java/com/android/ike/ikev2/message/IkeSaPayloadTest.java @@ -16,6 +16,7 @@ package com.android.ike.ikev2.message; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; @@ -57,11 +58,14 @@ import java.util.List; import java.util.Random; public final class IkeSaPayloadTest { - private static final String PROPOSAL_RAW_PACKET = + private static final String OUTBOUND_SA_PAYLOAD_HEADER = "22000030"; + private static final String OUTBOUND_PROPOSAL_RAW_PACKET = + "0000002C010100040300000C0100000C800E0080030000080200000203000008030" + + "000020000000804000002"; + private static final String INBOUND_PROPOSAL_RAW_PACKET = "0000002c010100040300000c0100000c800e0080030000080300000203000008040" + "000020000000802000002"; - - private static final String TWO_PROPOSAL_RAW_PACKET = + private static final String INBOUND_TWO_PROPOSAL_RAW_PACKET = "020000dc010100190300000c0100000c800e00800300000c0100000c800e00c0030" + "0000c0100000c800e01000300000801000003030000080300000c0300" + "00080300000d030000080300000e03000008030000020300000803000" @@ -91,16 +95,10 @@ public final class IkeSaPayloadTest { private static final String ATTRIBUTE_RAW_PACKET = "800e0080"; private static final int PROPOSAL_NUMBER = 1; - private static final int PROPOSAL_NUMBER_OFFSET = 4; - - @IkePayload.ProtocolId - private static final int PROPOSAL_PROTOCOL_ID = IkePayload.PROTOCOL_ID_IKE; + private static final int PROPOSAL_NUMBER_OFFSET = 4; private static final int PROTOCOL_ID_OFFSET = 5; - private static final byte PROPOSAL_SPI_SIZE = 0; - private static final byte PROPOSAL_SPI = 0; - // Constants for multiple proposals test private static final byte[] PROPOSAL_NUMBER_LIST = {1, 2}; @@ -153,7 +151,7 @@ public final class IkeSaPayloadTest { .addIntegrityAlgorithm(SaProposal.INTEGRITY_ALGORITHM_HMAC_SHA1_96) .addDhGroup(SaProposal.DH_GROUP_1024_BIT_MODP) .addPseudorandomFunction(SaProposal.PSEUDORANDOM_FUNCTION_HMAC_SHA1) - .buildOrThrow(); + .build(); mSaProposalTwo = SaProposal.Builder.newIkeSaProposalBuilder() @@ -166,7 +164,7 @@ public final class IkeSaPayloadTest { .addPseudorandomFunction(SaProposal.PSEUDORANDOM_FUNCTION_AES128_XCBC) .addDhGroup(SaProposal.DH_GROUP_1024_BIT_MODP) .addDhGroup(SaProposal.DH_GROUP_2048_BIT_MODP) - .buildOrThrow(); + .build(); mTwoSaProposalsArray = new SaProposal[] {mSaProposalOne, mSaProposalTwo}; } @@ -186,6 +184,16 @@ public final class IkeSaPayloadTest { } @Test + public void testEncodeAttribute() throws Exception { + ByteBuffer byteBuffer = ByteBuffer.allocate(mAttributeKeyLength128.getAttributeLength()); + mAttributeKeyLength128.encodeToByteBuffer(byteBuffer); + + byte[] expectedBytes = TestUtils.hexStringToByteArray(ATTRIBUTE_RAW_PACKET); + + assertArrayEquals(expectedBytes, byteBuffer.array()); + } + + @Test public void testDecodeEncryptionTransform() throws Exception { byte[] inputPacket = TestUtils.hexStringToByteArray(ENCR_TRANSFORM_RAW_PACKET); ByteBuffer inputBuffer = ByteBuffer.wrap(inputPacket); @@ -221,6 +229,16 @@ public final class IkeSaPayloadTest { } @Test + public void testEncodeEncryptionTransform() throws Exception { + ByteBuffer byteBuffer = ByteBuffer.allocate(mEncrAesCbc128Transform.getTransformLength()); + mEncrAesCbc128Transform.encodeToByteBuffer(false, byteBuffer); + + byte[] expectedBytes = TestUtils.hexStringToByteArray(ENCR_TRANSFORM_RAW_PACKET); + + assertArrayEquals(expectedBytes, byteBuffer.array()); + } + + @Test public void testConstructEncryptionTransformWithUnsupportedId() throws Exception { try { new EncryptionTransform(-1); @@ -255,6 +273,16 @@ public final class IkeSaPayloadTest { } @Test + public void testEncodePrfTransform() throws Exception { + ByteBuffer byteBuffer = ByteBuffer.allocate(mPrfHmacSha1Transform.getTransformLength()); + mPrfHmacSha1Transform.encodeToByteBuffer(true, byteBuffer); + + byte[] expectedBytes = TestUtils.hexStringToByteArray(PRF_TRANSFORM_RAW_PACKET); + + assertArrayEquals(expectedBytes, byteBuffer.array()); + } + + @Test public void testConstructPrfTransformWithUnsupportedId() throws Exception { try { new PrfTransform(-1); @@ -296,6 +324,16 @@ public final class IkeSaPayloadTest { } @Test + public void testEncodeIntegrityTransform() throws Exception { + ByteBuffer byteBuffer = ByteBuffer.allocate(mIntegHmacSha1Transform.getTransformLength()); + mIntegHmacSha1Transform.encodeToByteBuffer(false, byteBuffer); + + byte[] expectedBytes = TestUtils.hexStringToByteArray(INTEG_TRANSFORM_RAW_PACKET); + + assertArrayEquals(expectedBytes, byteBuffer.array()); + } + + @Test public void testConstructIntegrityTransformWithUnsupportedId() throws Exception { try { new IntegrityTransform(-1); @@ -321,6 +359,16 @@ public final class IkeSaPayloadTest { } @Test + public void testEncodeDhGroupTransform() throws Exception { + ByteBuffer byteBuffer = ByteBuffer.allocate(mDhGroup1024Transform.getTransformLength()); + mDhGroup1024Transform.encodeToByteBuffer(false, byteBuffer); + + byte[] expectedBytes = TestUtils.hexStringToByteArray(DH_GROUP_TRANSFORM_RAW_PACKET); + + assertArrayEquals(expectedBytes, byteBuffer.array()); + } + + @Test public void testConstructDhGroupTransformWithUnsupportedId() throws Exception { try { new DhGroupTransform(-1); @@ -378,6 +426,17 @@ public final class IkeSaPayloadTest { } @Test + public void testEncodeEsnTransform() throws Exception { + EsnTransform mEsnTransform = new EsnTransform(); + ByteBuffer byteBuffer = ByteBuffer.allocate(mEsnTransform.getTransformLength()); + mEsnTransform.encodeToByteBuffer(true, byteBuffer); + + byte[] expectedBytes = TestUtils.hexStringToByteArray(ESN_TRANSFORM_RAW_PACKET); + + assertArrayEquals(expectedBytes, byteBuffer.array()); + } + + @Test public void testDecodeUnrecognizedTransform() throws Exception { byte[] inputPacket = TestUtils.hexStringToByteArray(ENCR_TRANSFORM_RAW_PACKET); inputPacket[TRANSFORM_TYPE_OFFSET] = 6; @@ -484,23 +543,23 @@ public final class IkeSaPayloadTest { @Test public void testDecodeSingleProposal() throws Exception { - byte[] inputPacket = TestUtils.hexStringToByteArray(PROPOSAL_RAW_PACKET); + byte[] inputPacket = TestUtils.hexStringToByteArray(INBOUND_PROPOSAL_RAW_PACKET); ByteBuffer inputBuffer = ByteBuffer.wrap(inputPacket); Proposal.sTransformDecoder = getDummyTransformDecoder(new Transform[0]); Proposal proposal = Proposal.readFrom(inputBuffer); assertEquals(PROPOSAL_NUMBER, proposal.number); - assertEquals(PROPOSAL_PROTOCOL_ID, proposal.protocolId); - assertEquals(PROPOSAL_SPI_SIZE, proposal.spiSize); - assertEquals(PROPOSAL_SPI, proposal.spi); + assertEquals(IkePayload.PROTOCOL_ID_IKE, proposal.protocolId); + assertEquals(IkePayload.SPI_LEN_NOT_INCLUDED, proposal.spiSize); + assertEquals(IkePayload.SPI_NOT_INCLUDED, proposal.spi); assertFalse(proposal.hasUnrecognizedTransform); assertNotNull(proposal.saProposal); } @Test public void testDecodeSaRequestWithMultipleProposal() throws Exception { - byte[] inputPacket = TestUtils.hexStringToByteArray(TWO_PROPOSAL_RAW_PACKET); + byte[] inputPacket = TestUtils.hexStringToByteArray(INBOUND_TWO_PROPOSAL_RAW_PACKET); Proposal.sTransformDecoder = getDummyTransformDecoder(new Transform[0]); IkeSaPayload payload = new IkeSaPayload(false, false, inputPacket); @@ -515,8 +574,26 @@ public final class IkeSaPayloadTest { } @Test + public void testEncodeProposal() throws Exception { + Proposal proposal = + new Proposal( + (byte) PROPOSAL_NUMBER, + IkePayload.PROTOCOL_ID_IKE, + IkePayload.SPI_LEN_NOT_INCLUDED, + IkePayload.SPI_NOT_INCLUDED, + mSaProposalOne, + false /*has no unrecognized Tramsform*/); + + ByteBuffer byteBuffer = ByteBuffer.allocate(proposal.getProposalLength()); + proposal.encodeToByteBuffer(true /*is the last*/, byteBuffer); + + byte[] expectedBytes = TestUtils.hexStringToByteArray(OUTBOUND_PROPOSAL_RAW_PACKET); + assertArrayEquals(expectedBytes, byteBuffer.array()); + } + + @Test public void testDecodeSaResponseWithMultipleProposal() throws Exception { - byte[] inputPacket = TestUtils.hexStringToByteArray(TWO_PROPOSAL_RAW_PACKET); + byte[] inputPacket = TestUtils.hexStringToByteArray(INBOUND_TWO_PROPOSAL_RAW_PACKET); Proposal.sTransformDecoder = getDummyTransformDecoder(new Transform[0]); try { @@ -561,6 +638,19 @@ public final class IkeSaPayloadTest { } } + @Test + public void testEncodeIkeSaPayload() throws Exception { + IkeSaPayload saPayload = new IkeSaPayload(new SaProposal[] {mSaProposalOne}); + + ByteBuffer byteBuffer = ByteBuffer.allocate(saPayload.getPayloadLength()); + saPayload.encodeToByteBuffer(IkePayload.PAYLOAD_TYPE_KE, byteBuffer); + + byte[] expectedBytes = + TestUtils.hexStringToByteArray( + OUTBOUND_SA_PAYLOAD_HEADER + OUTBOUND_PROPOSAL_RAW_PACKET); + assertArrayEquals(expectedBytes, byteBuffer.array()); + } + private void buildAndVerifySaRespProposal(byte[] saResponseBytes, Transform[] decodedTransforms) throws Exception { // Build response SA payload from decoding bytes. @@ -577,7 +667,7 @@ public final class IkeSaPayloadTest { @Test public void testGetVerifiedNegotiatedProposal() throws Exception { - byte[] inputPacket = TestUtils.hexStringToByteArray(PROPOSAL_RAW_PACKET); + byte[] inputPacket = TestUtils.hexStringToByteArray(INBOUND_PROPOSAL_RAW_PACKET); buildAndVerifySaRespProposal(inputPacket, mValidNegotiatedTransformSet); } @@ -585,7 +675,7 @@ public final class IkeSaPayloadTest { // Test throwing when negotiated proposal in SA response payload has unrecognized Transform. @Test public void testGetVerifiedNegotiatedProposalWithUnrecogTransform() throws Exception { - byte[] inputPacket = TestUtils.hexStringToByteArray(PROPOSAL_RAW_PACKET); + byte[] inputPacket = TestUtils.hexStringToByteArray(INBOUND_PROPOSAL_RAW_PACKET); Transform[] negotiatedTransformSet = Arrays.copyOfRange( @@ -602,7 +692,7 @@ public final class IkeSaPayloadTest { // Test throwing when negotiated proposal has invalid proposal number. @Test public void testGetVerifiedNegotiatedProposalWithInvalidNumber() throws Exception { - byte[] inputPacket = TestUtils.hexStringToByteArray(PROPOSAL_RAW_PACKET); + byte[] inputPacket = TestUtils.hexStringToByteArray(INBOUND_PROPOSAL_RAW_PACKET); inputPacket[PROPOSAL_NUMBER_OFFSET] = (byte) 10; try { @@ -615,7 +705,7 @@ public final class IkeSaPayloadTest { // Test throwing when negotiated proposal has mismatched protocol ID. @Test public void testGetVerifiedNegotiatedProposalWithMisMatchedProtocol() throws Exception { - byte[] inputPacket = TestUtils.hexStringToByteArray(PROPOSAL_RAW_PACKET); + byte[] inputPacket = TestUtils.hexStringToByteArray(INBOUND_PROPOSAL_RAW_PACKET); inputPacket[PROTOCOL_ID_OFFSET] = IkePayload.PROTOCOL_ID_ESP; try { @@ -628,7 +718,7 @@ public final class IkeSaPayloadTest { // Test throwing when negotiated proposal has Transform that was not proposed in request. @Test public void testGetVerifiedNegotiatedProposalWithMismatchedTransform() throws Exception { - byte[] inputPacket = TestUtils.hexStringToByteArray(PROPOSAL_RAW_PACKET); + byte[] inputPacket = TestUtils.hexStringToByteArray(INBOUND_PROPOSAL_RAW_PACKET); Transform[] negotiatedTransformSet = Arrays.copyOfRange( @@ -645,7 +735,7 @@ public final class IkeSaPayloadTest { // Test throwing when negotiated proposal is lack of a certain type Transform. @Test public void testGetVerifiedNegotiatedProposalWithoutTransform() throws Exception { - byte[] inputPacket = TestUtils.hexStringToByteArray(PROPOSAL_RAW_PACKET); + byte[] inputPacket = TestUtils.hexStringToByteArray(INBOUND_PROPOSAL_RAW_PACKET); try { buildAndVerifySaRespProposal(inputPacket, new Transform[0]); diff --git a/tests/iketests/src/java/com/android/ike/ikev2/message/IkeSkPayloadTest.java b/tests/iketests/src/java/com/android/ike/ikev2/message/IkeSkPayloadTest.java index aebd1194..13866fb6 100644 --- a/tests/iketests/src/java/com/android/ike/ikev2/message/IkeSkPayloadTest.java +++ b/tests/iketests/src/java/com/android/ike/ikev2/message/IkeSkPayloadTest.java @@ -17,13 +17,11 @@ package com.android.ike.ikev2.message; import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.fail; import org.junit.Before; import org.junit.Test; import java.nio.ByteBuffer; -import java.security.GeneralSecurityException; import java.util.Arrays; import javax.crypto.Cipher; @@ -82,44 +80,11 @@ public final class IkeSkPayloadTest { } @Test - public void testAuthenticateAndDecryptMessage() throws Exception { - byte[] message = TestUtils.hexStringToByteArray(IKE_AUTH_INIT_REQUEST_HEX_STRING); - - IkeSkPayload payload = - IkePayloadFactory.getIkeSkPayload( - message, - mHmacSha1IntegrityMac, - CHECKSUM_LEN, - mAesCbcDecryptCipher, - mAesCbcDecryptKey) - .first; - byte[] expectedPlaintext = - TestUtils.hexStringToByteArray(IKE_AUTH_INIT_REQUEST_DECRYPTED_BODY_HEX_STRING); - assertArrayEquals(expectedPlaintext, payload.getUnencryptedPayloads()); - } - - @Test - public void testThrowExceptionForInvalidChecksum() throws Exception { - byte[] message = TestUtils.hexStringToByteArray(IKE_AUTH_INIT_REQUEST_HEX_STRING); - // Change last bit of checksum. - message[message.length - 1]++; - try { - IkePayloadFactory.getIkeSkPayload( - message, - mHmacSha1IntegrityMac, - CHECKSUM_LEN, - mAesCbcDecryptCipher, - mAesCbcDecryptKey); - fail("Expected GeneralSecurityException: Invalid checksum."); - } catch (GeneralSecurityException expected) { - } - } - - @Test public void testEncode() throws Exception { byte[] message = TestUtils.hexStringToByteArray(IKE_AUTH_INIT_REQUEST_HEX_STRING); byte[] payloadBytes = Arrays.copyOfRange(message, IkeHeader.IKE_HEADER_LENGTH, message.length); + IkeSkPayload payload = IkePayloadFactory.getIkeSkPayload( message, |