aboutsummaryrefslogtreecommitdiff
path: root/src/java/com/android/internal/net/ipsec/ike/message/IkeSaPayload.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/java/com/android/internal/net/ipsec/ike/message/IkeSaPayload.java')
-rw-r--r--src/java/com/android/internal/net/ipsec/ike/message/IkeSaPayload.java1773
1 files changed, 1773 insertions, 0 deletions
diff --git a/src/java/com/android/internal/net/ipsec/ike/message/IkeSaPayload.java b/src/java/com/android/internal/net/ipsec/ike/message/IkeSaPayload.java
new file mode 100644
index 00000000..3cce6255
--- /dev/null
+++ b/src/java/com/android/internal/net/ipsec/ike/message/IkeSaPayload.java
@@ -0,0 +1,1773 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.net.ipsec.ike.message;
+
+import static android.net.ipsec.ike.IkeManager.getIkeLog;
+import static android.net.ipsec.ike.SaProposal.DhGroup;
+import static android.net.ipsec.ike.SaProposal.EncryptionAlgorithm;
+import static android.net.ipsec.ike.SaProposal.IntegrityAlgorithm;
+import static android.net.ipsec.ike.SaProposal.PseudorandomFunction;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.net.IpSecManager;
+import android.net.IpSecManager.ResourceUnavailableException;
+import android.net.IpSecManager.SecurityParameterIndex;
+import android.net.IpSecManager.SpiUnavailableException;
+import android.net.ipsec.ike.ChildSaProposal;
+import android.net.ipsec.ike.IkeSaProposal;
+import android.net.ipsec.ike.SaProposal;
+import android.net.ipsec.ike.exceptions.IkeProtocolException;
+import android.util.ArraySet;
+import android.util.Pair;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.net.ipsec.ike.IkeSessionStateMachine.IkeSecurityParameterIndex;
+import com.android.internal.net.ipsec.ike.exceptions.InvalidSyntaxException;
+import com.android.internal.net.ipsec.ike.exceptions.NoValidProposalChosenException;
+
+import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.net.InetAddress;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * IkeSaPayload represents a Security Association payload. It contains one or more {@link Proposal}.
+ *
+ * @see <a href="https://tools.ietf.org/html/rfc7296#section-3.3">RFC 7296, Internet Key Exchange
+ * Protocol Version 2 (IKEv2)</a>
+ */
+public final class IkeSaPayload extends IkePayload {
+ private static final String TAG = "IkeSaPayload";
+
+ public final boolean isSaResponse;
+ public final List<Proposal> proposalList;
+ /**
+ * Construct an instance of IkeSaPayload for decoding an inbound packet.
+ *
+ * @param critical indicates if this payload is critical. Ignored in supported payload as
+ * instructed by the RFC 7296.
+ * @param isResp indicates if this payload is in a response message.
+ * @param payloadBody the encoded payload body in byte array.
+ */
+ IkeSaPayload(boolean critical, boolean isResp, byte[] payloadBody) throws IkeProtocolException {
+ super(IkePayload.PAYLOAD_TYPE_SA, critical);
+
+ ByteBuffer inputBuffer = ByteBuffer.wrap(payloadBody);
+ proposalList = new LinkedList<>();
+ while (inputBuffer.hasRemaining()) {
+ Proposal proposal = Proposal.readFrom(inputBuffer);
+ proposalList.add(proposal);
+ }
+
+ if (proposalList.isEmpty()) {
+ throw new InvalidSyntaxException("Found no SA Proposal in this SA Payload.");
+ }
+
+ // An SA response must have exactly one SA proposal.
+ if (isResp && proposalList.size() != 1) {
+ throw new InvalidSyntaxException(
+ "Expected only one negotiated proposal from SA response: "
+ + "Multiple negotiated proposals found.");
+ }
+ isSaResponse = isResp;
+
+ boolean firstIsIkeProposal = (proposalList.get(0).protocolId == PROTOCOL_ID_IKE);
+ for (int i = 1; i < proposalList.size(); i++) {
+ boolean isIkeProposal = (proposalList.get(i).protocolId == PROTOCOL_ID_IKE);
+ if (firstIsIkeProposal != isIkeProposal) {
+ getIkeLog()
+ .w(TAG, "Found both IKE proposals and Child proposals in this SA Payload.");
+ break;
+ }
+ }
+
+ getIkeLog().d(TAG, "Receive " + toString());
+ }
+
+ /** Package private constructor for building a request for IKE SA initial creation or rekey */
+ @VisibleForTesting
+ IkeSaPayload(
+ boolean isResp, byte spiSize, IkeSaProposal[] saProposals, InetAddress localAddress)
+ throws IOException {
+ this(isResp, spiSize, localAddress);
+
+ if (saProposals.length < 1 || isResp && (saProposals.length > 1)) {
+ throw new IllegalArgumentException("Invalid SA payload.");
+ }
+
+ for (int i = 0; i < saProposals.length; i++) {
+ // Proposal number must start from 1.
+ proposalList.add(
+ IkeProposal.createIkeProposal(
+ (byte) (i + 1) /*number*/, spiSize, saProposals[i], localAddress));
+ }
+
+ getIkeLog().d(TAG, "Generate " + toString());
+ }
+
+ /** Package private constructor for building an response SA Payload for IKE SA rekeys. */
+ @VisibleForTesting
+ IkeSaPayload(
+ boolean isResp,
+ byte spiSize,
+ byte proposalNumber,
+ IkeSaProposal saProposal,
+ InetAddress localAddress)
+ throws IOException {
+ this(isResp, spiSize, localAddress);
+
+ proposalList.add(
+ IkeProposal.createIkeProposal(
+ proposalNumber /*number*/, spiSize, saProposal, localAddress));
+
+ getIkeLog().d(TAG, "Generate " + toString());
+ }
+
+ private IkeSaPayload(boolean isResp, byte spiSize, InetAddress localAddress)
+ throws IOException {
+ super(IkePayload.PAYLOAD_TYPE_SA, false);
+
+ // TODO: Check that proposals.length <= 255 in IkeSessionOptions and ChildSessionOptions
+ isSaResponse = isResp;
+
+ // TODO: Allocate IKE SPI and pass to IkeProposal.createIkeProposal()
+
+ // ProposalList populated in other constructors
+ proposalList = new ArrayList<Proposal>();
+ }
+
+ /**
+ * Package private constructor for building an outbound request SA Payload for Child SA
+ * negotiation.
+ */
+ @VisibleForTesting
+ IkeSaPayload(ChildSaProposal[] saProposals, IpSecManager ipSecManager, InetAddress localAddress)
+ throws ResourceUnavailableException {
+ this(false /*isResp*/, ipSecManager, localAddress);
+
+ if (saProposals.length < 1) {
+ throw new IllegalArgumentException("Invalid SA payload.");
+ }
+
+ // TODO: Check that saProposals.length <= 255 in IkeSessionOptions and ChildSessionOptions
+
+ for (int i = 0; i < saProposals.length; i++) {
+ // Proposal number must start from 1.
+ proposalList.add(
+ ChildProposal.createChildProposal(
+ (byte) (i + 1) /*number*/, saProposals[i], ipSecManager, localAddress));
+ }
+
+ getIkeLog().d(TAG, "Generate " + toString());
+ }
+
+ /**
+ * Package private constructor for building an outbound response SA Payload for Child SA
+ * negotiation.
+ */
+ @VisibleForTesting
+ IkeSaPayload(
+ byte proposalNumber,
+ ChildSaProposal saProposal,
+ IpSecManager ipSecManager,
+ InetAddress localAddress)
+ throws ResourceUnavailableException {
+ this(true /*isResp*/, ipSecManager, localAddress);
+
+ proposalList.add(
+ ChildProposal.createChildProposal(
+ proposalNumber /*number*/, saProposal, ipSecManager, localAddress));
+
+ getIkeLog().d(TAG, "Generate " + toString());
+ }
+
+ /** Constructor for building an outbound SA Payload for Child SA negotiation. */
+ private IkeSaPayload(boolean isResp, IpSecManager ipSecManager, InetAddress localAddress) {
+ super(IkePayload.PAYLOAD_TYPE_SA, false);
+
+ isSaResponse = isResp;
+
+ // TODO: Allocate Child SPI and pass to ChildProposal.createChildProposal()
+
+ // ProposalList populated in other constructors
+ proposalList = new ArrayList<Proposal>();
+ }
+
+ /**
+ * Construct an instance of IkeSaPayload for building an outbound IKE initial setup request.
+ *
+ * <p>According to RFC 7296, for an initial IKE SA negotiation, no SPI is included in SA
+ * Proposal. IKE library, as a client, only supports requesting this initial negotiation.
+ *
+ * @param saProposals the array of all SA Proposals.
+ */
+ public static IkeSaPayload createInitialIkeSaPayload(IkeSaProposal[] saProposals)
+ throws IOException {
+ return new IkeSaPayload(false /*isResp*/, SPI_LEN_NOT_INCLUDED, saProposals, null);
+ }
+
+ /**
+ * Construct an instance of IkeSaPayload for building an outbound request for Rekey IKE.
+ *
+ * @param saProposals the array of all IKE SA Proposals.
+ * @param localAddress the local address assigned on-device.
+ */
+ public static IkeSaPayload createRekeyIkeSaRequestPayload(
+ IkeSaProposal[] saProposals, InetAddress localAddress) throws IOException {
+ return new IkeSaPayload(false /*isResp*/, SPI_LEN_IKE, saProposals, localAddress);
+ }
+
+ /**
+ * Construct an instance of IkeSaPayload for building an outbound response for Rekey IKE.
+ *
+ * @param respProposalNumber the selected proposal's number.
+ * @param saProposal the expected selected IKE SA Proposal.
+ * @param localAddress the local address assigned on-device.
+ */
+ public static IkeSaPayload createRekeyIkeSaResponsePayload(
+ byte respProposalNumber, IkeSaProposal saProposal, InetAddress localAddress)
+ throws IOException {
+ return new IkeSaPayload(
+ true /*isResp*/, SPI_LEN_IKE, respProposalNumber, saProposal, localAddress);
+ }
+
+ /**
+ * Construct an instance of IkeSaPayload for building an outbound request for Child SA
+ * negotiation.
+ *
+ * @param saProposals the array of all Child SA Proposals.
+ * @param ipSecManager the IpSecManager for generating IPsec SPIs.
+ * @param localAddress the local address assigned on-device.
+ * @throws ResourceUnavailableException if too many SPIs are currently allocated for this user.
+ */
+ public static IkeSaPayload createChildSaRequestPayload(
+ ChildSaProposal[] saProposals, IpSecManager ipSecManager, InetAddress localAddress)
+ throws ResourceUnavailableException {
+
+ return new IkeSaPayload(saProposals, ipSecManager, localAddress);
+ }
+
+ /**
+ * Construct an instance of IkeSaPayload for building an outbound response for Child SA
+ * negotiation.
+ *
+ * @param respProposalNumber the selected proposal's number.
+ * @param saProposal the expected selected Child SA Proposal.
+ * @param ipSecManager the IpSecManager for generating IPsec SPIs.
+ * @param localAddress the local address assigned on-device.
+ */
+ public static IkeSaPayload createChildSaResponsePayload(
+ byte respProposalNumber,
+ ChildSaProposal saProposal,
+ IpSecManager ipSecManager,
+ InetAddress localAddress)
+ throws ResourceUnavailableException {
+ return new IkeSaPayload(respProposalNumber, saProposal, ipSecManager, localAddress);
+ }
+
+ /**
+ * Finds the proposal in this (request) payload that matches the response proposal.
+ *
+ * @param respProposal the Proposal to match against.
+ * @return the byte-value proposal number of the selected proposal
+ * @throws NoValidProposalChosenException if no matching proposal was found.
+ */
+ public byte getNegotiatedProposalNumber(SaProposal respProposal)
+ throws NoValidProposalChosenException {
+ for (int i = 0; i < proposalList.size(); i++) {
+ Proposal reqProposal = proposalList.get(i);
+ if (respProposal.isNegotiatedFrom(reqProposal.getSaProposal())
+ && reqProposal.getSaProposal().getProtocolId()
+ == respProposal.getProtocolId()) {
+ return reqProposal.number;
+ }
+ }
+ throw new NoValidProposalChosenException("No remotely proposed protocol acceptable");
+ }
+
+ /**
+ * Validate the IKE SA Payload pair (request/response) and return the IKE SA negotiation result.
+ *
+ * <p>Caller is able to extract the negotiated IKE SA Proposal from the response Proposal and
+ * the IKE SPI pair generated by both sides.
+ *
+ * <p>In a locally-initiated case all IKE SA proposals (from users in initial creation or from
+ * previously negotiated proposal in rekey creation) in the locally generated reqSaPayload have
+ * been validated during building and are unmodified. All Transform combinations in these SA
+ * proposals are valid for IKE SA negotiation. It means each IKE SA request proposal MUST have
+ * Encryption algorithms, DH group configurations and PRFs. Integrity algorithms can only be
+ * omitted when AEAD is used.
+ *
+ * <p>In a remotely-initiated case the locally generated respSaPayload has exactly one SA
+ * proposal. It is validated during building and are unmodified. This proposal has a valid
+ * Transform combination for an IKE SA and has at most one value for each Transform type.
+ *
+ * <p>The response IKE SA proposal is validated against one of the request IKE SA proposals. It
+ * is guaranteed that for each Transform type that the request proposal has provided options,
+ * the response proposal has exact one Transform value.
+ *
+ * @param reqSaPayload the request payload.
+ * @param respSaPayload the response payload.
+ * @param remoteAddress the address of the remote IKE peer.
+ * @return the Pair of selected IkeProposal in request and the IkeProposal in response.
+ * @throws NoValidProposalChosenException if the response SA Payload cannot be negotiated from
+ * the request SA Payload.
+ */
+ public static Pair<IkeProposal, IkeProposal> getVerifiedNegotiatedIkeProposalPair(
+ IkeSaPayload reqSaPayload, IkeSaPayload respSaPayload, InetAddress remoteAddress)
+ throws NoValidProposalChosenException, IOException {
+ Pair<Proposal, Proposal> proposalPair =
+ getVerifiedNegotiatedProposalPair(reqSaPayload, respSaPayload);
+ IkeProposal reqProposal = (IkeProposal) proposalPair.first;
+ IkeProposal respProposal = (IkeProposal) proposalPair.second;
+
+ try {
+ // Allocate initiator's inbound SPI as needed for remotely initiated IKE SA creation
+ if (reqProposal.spiSize != SPI_NOT_INCLUDED
+ && reqProposal.getIkeSpiResource() == null) {
+ reqProposal.allocateResourceForRemoteIkeSpi(remoteAddress);
+ }
+ // Allocate responder's inbound SPI as needed for locally initiated IKE SA creation
+ if (respProposal.spiSize != SPI_NOT_INCLUDED
+ && respProposal.getIkeSpiResource() == null) {
+ respProposal.allocateResourceForRemoteIkeSpi(remoteAddress);
+ }
+
+ return new Pair(reqProposal, respProposal);
+ } catch (Exception e) {
+ reqProposal.releaseSpiResourceIfExists();
+ respProposal.releaseSpiResourceIfExists();
+ throw e;
+ }
+ }
+
+ /**
+ * Validate the SA Payload pair (request/response) and return the Child SA negotiation result.
+ *
+ * <p>Caller is able to extract the negotiated SA Proposal from the response Proposal and the
+ * IPsec SPI pair generated by both sides.
+ *
+ * <p>In a locally-initiated case all Child SA proposals (from users in initial creation or from
+ * previously negotiated proposal in rekey creation) in the locally generated reqSaPayload have
+ * been validated during building and are unmodified. All Transform combinations in these SA
+ * proposals are valid for Child SA negotiation. It means each request SA proposal MUST have
+ * Encryption algorithms and ESN configurations.
+ *
+ * <p>In a remotely-initiated case the locally generated respSapayload has exactly one SA
+ * proposal. It is validated during building and are unmodified. This proposal has a valid
+ * Transform combination for an Child SA and has at most one value for each Transform type.
+ *
+ * <p>The response Child SA proposal is validated against one of the request SA proposals. It is
+ * guaranteed that for each Transform type that the request proposal has provided options, the
+ * response proposal has exact one Transform value.
+ *
+ * @param reqSaPayload the request payload.
+ * @param respSaPayload the response payload.
+ * @param ipSecManager the IpSecManager to allocate SPI resource for the Proposal in this
+ * inbound SA Payload.
+ * @param remoteAddress the address of the remote IKE peer.
+ * @return the Pair of selected ChildProposal in the locally generated request and the
+ * ChildProposal in this response.
+ * @throws NoValidProposalChosenException if the response SA Payload cannot be negotiated from
+ * the request SA Payload.
+ * @throws ResourceUnavailableException if too many SPIs are currently allocated for this user.
+ * @throws SpiUnavailableException if the remotely generated SPI is in use.
+ */
+ public static Pair<ChildProposal, ChildProposal> getVerifiedNegotiatedChildProposalPair(
+ IkeSaPayload reqSaPayload,
+ IkeSaPayload respSaPayload,
+ IpSecManager ipSecManager,
+ InetAddress remoteAddress)
+ throws NoValidProposalChosenException, ResourceUnavailableException,
+ SpiUnavailableException {
+ Pair<Proposal, Proposal> proposalPair =
+ getVerifiedNegotiatedProposalPair(reqSaPayload, respSaPayload);
+ ChildProposal reqProposal = (ChildProposal) proposalPair.first;
+ ChildProposal respProposal = (ChildProposal) proposalPair.second;
+
+ try {
+ // Allocate initiator's inbound SPI as needed for remotely initiated Child SA creation
+ if (reqProposal.getChildSpiResource() == null) {
+ reqProposal.allocateResourceForRemoteChildSpi(ipSecManager, remoteAddress);
+ }
+ // Allocate responder's inbound SPI as needed for locally initiated Child SA creation
+ if (respProposal.getChildSpiResource() == null) {
+ respProposal.allocateResourceForRemoteChildSpi(ipSecManager, remoteAddress);
+ }
+
+ return new Pair(reqProposal, respProposal);
+ } catch (Exception e) {
+ reqProposal.releaseSpiResourceIfExists();
+ respProposal.releaseSpiResourceIfExists();
+ throw e;
+ }
+ }
+
+ private static Pair<Proposal, Proposal> getVerifiedNegotiatedProposalPair(
+ IkeSaPayload reqSaPayload, IkeSaPayload respSaPayload)
+ throws NoValidProposalChosenException {
+ try {
+ // If negotiated proposal has an unrecognized Transform, throw an exception.
+ Proposal respProposal = respSaPayload.proposalList.get(0);
+ if (respProposal.hasUnrecognizedTransform) {
+ throw new NoValidProposalChosenException(
+ "Negotiated proposal has unrecognized Transform.");
+ }
+
+ // In SA request payload, the first proposal MUST be 1, and subsequent proposals MUST be
+ // one more than the previous proposal. In SA response payload, the negotiated proposal
+ // number MUST match the selected proposal number in SA request Payload.
+ int negotiatedProposalNum = respProposal.number;
+ List<Proposal> reqProposalList = reqSaPayload.proposalList;
+ if (negotiatedProposalNum < 1 || negotiatedProposalNum > reqProposalList.size()) {
+ throw new NoValidProposalChosenException(
+ "Negotiated proposal has invalid proposal number.");
+ }
+
+ Proposal reqProposal = reqProposalList.get(negotiatedProposalNum - 1);
+ if (!respProposal.isNegotiatedFrom(reqProposal)) {
+ throw new NoValidProposalChosenException("Invalid negotiated proposal.");
+ }
+
+ // In a locally-initiated creation, release locally generated SPIs in unselected request
+ // Proposals. In remotely-initiated SA creation, unused proposals do not have SPIs, and
+ // will silently succeed.
+ for (Proposal p : reqProposalList) {
+ if (reqProposal != p) p.releaseSpiResourceIfExists();
+ }
+
+ return new Pair<Proposal, Proposal>(reqProposal, respProposal);
+ } catch (Exception e) {
+ // In a locally-initiated case, release all locally generated SPIs in the SA request
+ // payload.
+ for (Proposal p : reqSaPayload.proposalList) p.releaseSpiResourceIfExists();
+ throw e;
+ }
+ }
+
+ @VisibleForTesting
+ interface TransformDecoder {
+ Transform[] decodeTransforms(int count, ByteBuffer inputBuffer) throws IkeProtocolException;
+ }
+
+ // TODO: Add another constructor for building outbound message.
+
+ /**
+ * This class represents the common information of an IKE Proposal and a Child Proposal.
+ *
+ * <p>Proposal represents a set contains cryptographic algorithms and key generating materials.
+ * It contains multiple {@link Transform}.
+ *
+ * @see <a href="https://tools.ietf.org/html/rfc7296#section-3.3.1">RFC 7296, Internet Key
+ * Exchange Protocol Version 2 (IKEv2)</a>
+ * <p>Proposals with an unrecognized Protocol ID, containing an unrecognized Transform Type
+ * or lacking a necessary Transform Type shall be ignored when processing a received SA
+ * Payload.
+ */
+ public abstract static class Proposal {
+ 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() {
+ @Override
+ public Transform[] decodeTransforms(int count, ByteBuffer inputBuffer)
+ throws IkeProtocolException {
+ Transform[] transformArray = new Transform[count];
+ for (int i = 0; i < count; i++) {
+ Transform transform = Transform.readFrom(inputBuffer);
+ if (transform.isSupported) {
+ transformArray[i] = transform;
+ }
+ }
+ return transformArray;
+ }
+ };
+
+ public final byte number;
+ /** All supported protocol will fall into {@link ProtocolId} */
+ public final int protocolId;
+
+ public final byte spiSize;
+ public final long spi;
+
+ public final boolean hasUnrecognizedTransform;
+
+ @VisibleForTesting
+ Proposal(
+ byte number,
+ int protocolId,
+ byte spiSize,
+ long spi,
+ boolean hasUnrecognizedTransform) {
+ this.number = number;
+ this.protocolId = protocolId;
+ this.spiSize = spiSize;
+ this.spi = spi;
+ this.hasUnrecognizedTransform = hasUnrecognizedTransform;
+ }
+
+ @VisibleForTesting
+ static Proposal readFrom(ByteBuffer inputBuffer) throws IkeProtocolException {
+ byte isLast = inputBuffer.get();
+ if (isLast != LAST_PROPOSAL && isLast != NOT_LAST_PROPOSAL) {
+ throw new InvalidSyntaxException(
+ "Invalid value of Last Proposal Substructure: " + isLast);
+ }
+ // Skip RESERVED byte
+ inputBuffer.get(new byte[PROPOSAL_RESERVED_FIELD_LEN]);
+
+ int length = Short.toUnsignedInt(inputBuffer.getShort());
+ byte number = inputBuffer.get();
+ int protocolId = Byte.toUnsignedInt(inputBuffer.get());
+
+ byte spiSize = inputBuffer.get();
+ int transformCount = Byte.toUnsignedInt(inputBuffer.get());
+
+ // TODO: Add check: spiSize must be 0 in initial IKE SA negotiation
+ // spiSize should be either 8 for IKE or 4 for IPsec.
+ long spi = SPI_NOT_INCLUDED;
+ switch (spiSize) {
+ case SPI_LEN_NOT_INCLUDED:
+ // No SPI attached for IKE initial exchange.
+ break;
+ case SPI_LEN_IPSEC:
+ spi = Integer.toUnsignedLong(inputBuffer.getInt());
+ break;
+ case SPI_LEN_IKE:
+ spi = inputBuffer.getLong();
+ break;
+ default:
+ throw new InvalidSyntaxException(
+ "Invalid value of spiSize in Proposal Substructure: " + spiSize);
+ }
+
+ Transform[] transformArray =
+ sTransformDecoder.decodeTransforms(transformCount, inputBuffer);
+ // TODO: Validate that sum of all Transforms' lengths plus Proposal header length equals
+ // to Proposal's length.
+
+ List<EncryptionTransform> encryptAlgoList = new LinkedList<>();
+ List<PrfTransform> prfList = new LinkedList<>();
+ List<IntegrityTransform> integAlgoList = new LinkedList<>();
+ List<DhGroupTransform> dhGroupList = new LinkedList<>();
+ List<EsnTransform> esnList = new LinkedList<>();
+
+ boolean hasUnrecognizedTransform = false;
+
+ for (Transform transform : transformArray) {
+ switch (transform.type) {
+ case Transform.TRANSFORM_TYPE_ENCR:
+ encryptAlgoList.add((EncryptionTransform) transform);
+ break;
+ case Transform.TRANSFORM_TYPE_PRF:
+ prfList.add((PrfTransform) transform);
+ break;
+ case Transform.TRANSFORM_TYPE_INTEG:
+ integAlgoList.add((IntegrityTransform) transform);
+ break;
+ case Transform.TRANSFORM_TYPE_DH:
+ dhGroupList.add((DhGroupTransform) transform);
+ break;
+ case Transform.TRANSFORM_TYPE_ESN:
+ esnList.add((EsnTransform) transform);
+ break;
+ default:
+ hasUnrecognizedTransform = true;
+ }
+ }
+
+ if (protocolId == PROTOCOL_ID_IKE) {
+ IkeSaProposal saProposal =
+ new IkeSaProposal(
+ encryptAlgoList.toArray(
+ new EncryptionTransform[encryptAlgoList.size()]),
+ prfList.toArray(new PrfTransform[prfList.size()]),
+ integAlgoList.toArray(new IntegrityTransform[integAlgoList.size()]),
+ dhGroupList.toArray(new DhGroupTransform[dhGroupList.size()]));
+ return new IkeProposal(number, spiSize, spi, saProposal, hasUnrecognizedTransform);
+ } else {
+ ChildSaProposal saProposal =
+ new ChildSaProposal(
+ encryptAlgoList.toArray(
+ new EncryptionTransform[encryptAlgoList.size()]),
+ integAlgoList.toArray(new IntegrityTransform[integAlgoList.size()]),
+ dhGroupList.toArray(new DhGroupTransform[dhGroupList.size()]),
+ esnList.toArray(new EsnTransform[esnList.size()]));
+ return new ChildProposal(number, spi, saProposal, hasUnrecognizedTransform);
+ }
+ }
+
+ /** Package private */
+ boolean isNegotiatedFrom(Proposal reqProposal) {
+ if (protocolId != reqProposal.protocolId || number != reqProposal.number) {
+ return false;
+ }
+ return getSaProposal().isNegotiatedFrom(reqProposal.getSaProposal());
+ }
+
+ protected void encodeToByteBuffer(boolean isLast, ByteBuffer byteBuffer) {
+ Transform[] allTransforms = getSaProposal().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 = getSaProposal().getAllTransforms();
+ for (Transform t : allTransforms) len += t.getTransformLength();
+ return len;
+ }
+
+ @Override
+ @NonNull
+ public String toString() {
+ return "Proposal(" + number + ") " + getSaProposal().toString();
+ }
+
+ /** Package private method for releasing SPI resource in this unselected Proposal. */
+ abstract void releaseSpiResourceIfExists();
+
+ /** Package private method for getting SaProposal */
+ abstract SaProposal getSaProposal();
+ }
+
+ /** This class represents a Proposal for IKE SA negotiation. */
+ public static final class IkeProposal extends Proposal {
+ private IkeSecurityParameterIndex mIkeSpiResource;
+
+ public final IkeSaProposal saProposal;
+
+ /**
+ * Construct IkeProposal from a decoded inbound message for IKE negotiation.
+ *
+ * <p>Package private
+ */
+ IkeProposal(
+ byte number,
+ byte spiSize,
+ long spi,
+ IkeSaProposal saProposal,
+ boolean hasUnrecognizedTransform) {
+ super(number, PROTOCOL_ID_IKE, spiSize, spi, hasUnrecognizedTransform);
+ this.saProposal = saProposal;
+ }
+
+ /** Construct IkeProposal for an outbound message for IKE negotiation. */
+ private IkeProposal(
+ byte number,
+ byte spiSize,
+ IkeSecurityParameterIndex ikeSpiResource,
+ IkeSaProposal saProposal) {
+ super(
+ number,
+ PROTOCOL_ID_IKE,
+ spiSize,
+ ikeSpiResource == null ? SPI_NOT_INCLUDED : ikeSpiResource.getSpi(),
+ false /*hasUnrecognizedTransform*/);
+ mIkeSpiResource = ikeSpiResource;
+ this.saProposal = saProposal;
+ }
+
+ /**
+ * Construct IkeProposal for an outbound message for IKE negotiation.
+ *
+ * <p>Package private
+ */
+ @VisibleForTesting
+ static IkeProposal createIkeProposal(
+ byte number, byte spiSize, IkeSaProposal saProposal, InetAddress localAddress)
+ throws IOException {
+ // IKE_INIT uses SPI_LEN_NOT_INCLUDED, while rekeys use SPI_LEN_IKE
+ IkeSecurityParameterIndex spiResource =
+ (spiSize == SPI_LEN_NOT_INCLUDED
+ ? null
+ : IkeSecurityParameterIndex.allocateSecurityParameterIndex(
+ localAddress));
+ return new IkeProposal(number, spiSize, spiResource, saProposal);
+ }
+
+ /** Package private method for releasing SPI resource in this unselected Proposal. */
+ void releaseSpiResourceIfExists() {
+ // mIkeSpiResource is null when doing IKE initial exchanges.
+ if (mIkeSpiResource == null) return;
+ mIkeSpiResource.close();
+ mIkeSpiResource = null;
+ }
+
+ /**
+ * Package private method for allocating SPI resource for a validated remotely generated IKE
+ * SA proposal.
+ */
+ void allocateResourceForRemoteIkeSpi(InetAddress remoteAddress) throws IOException {
+ mIkeSpiResource =
+ IkeSecurityParameterIndex.allocateSecurityParameterIndex(remoteAddress, spi);
+ }
+
+ @Override
+ public SaProposal getSaProposal() {
+ return saProposal;
+ }
+
+ /**
+ * Get the IKE SPI resource.
+ *
+ * @return the IKE SPI resource or null for IKE initial exchanges.
+ */
+ public IkeSecurityParameterIndex getIkeSpiResource() {
+ return mIkeSpiResource;
+ }
+ }
+
+ /** This class represents a Proposal for Child SA negotiation. */
+ public static final class ChildProposal extends Proposal {
+ private SecurityParameterIndex mChildSpiResource;
+
+ public final ChildSaProposal saProposal;
+
+ /**
+ * Construct ChildProposal from a decoded inbound message for Child SA negotiation.
+ *
+ * <p>Package private
+ */
+ ChildProposal(
+ byte number,
+ long spi,
+ ChildSaProposal saProposal,
+ boolean hasUnrecognizedTransform) {
+ super(
+ number,
+ PROTOCOL_ID_ESP,
+ SPI_LEN_IPSEC,
+ spi,
+ hasUnrecognizedTransform);
+ this.saProposal = saProposal;
+ }
+
+ /** Construct ChildProposal for an outbound message for Child SA negotiation. */
+ private ChildProposal(
+ byte number, SecurityParameterIndex childSpiResource, ChildSaProposal saProposal) {
+ super(
+ number,
+ PROTOCOL_ID_ESP,
+ SPI_LEN_IPSEC,
+ (long) childSpiResource.getSpi(),
+ false /*hasUnrecognizedTransform*/);
+ mChildSpiResource = childSpiResource;
+ this.saProposal = saProposal;
+ }
+
+ /**
+ * Construct ChildProposal for an outbound message for Child SA negotiation.
+ *
+ * <p>Package private
+ */
+ @VisibleForTesting
+ static ChildProposal createChildProposal(
+ byte number,
+ ChildSaProposal saProposal,
+ IpSecManager ipSecManager,
+ InetAddress localAddress)
+ throws ResourceUnavailableException {
+ return new ChildProposal(
+ number, ipSecManager.allocateSecurityParameterIndex(localAddress), saProposal);
+ }
+
+ /** Package private method for releasing SPI resource in this unselected Proposal. */
+ void releaseSpiResourceIfExists() {
+ if (mChildSpiResource == null) return;
+
+ mChildSpiResource.close();
+ mChildSpiResource = null;
+ }
+
+ /**
+ * Package private method for allocating SPI resource for a validated remotely generated
+ * Child SA proposal.
+ */
+ void allocateResourceForRemoteChildSpi(IpSecManager ipSecManager, InetAddress remoteAddress)
+ throws ResourceUnavailableException, SpiUnavailableException {
+ mChildSpiResource =
+ ipSecManager.allocateSecurityParameterIndex(remoteAddress, (int) spi);
+ }
+
+ @Override
+ public SaProposal getSaProposal() {
+ return saProposal;
+ }
+
+ /**
+ * Get the IPsec SPI resource.
+ *
+ * @return the IPsec SPI resource.
+ */
+ public SecurityParameterIndex getChildSpiResource() {
+ return mChildSpiResource;
+ }
+ }
+
+ @VisibleForTesting
+ interface AttributeDecoder {
+ List<Attribute> decodeAttributes(int length, ByteBuffer inputBuffer)
+ throws IkeProtocolException;
+ }
+
+ /**
+ * Transform is an abstract base class that represents the common information for all Transform
+ * types. It may contain one or more {@link Attribute}.
+ *
+ * @see <a href="https://tools.ietf.org/html/rfc7296#section-3.3.2">RFC 7296, Internet Key
+ * Exchange Protocol Version 2 (IKEv2)</a>
+ * <p>Transforms with unrecognized Transform ID or containing unrecognized Attribute Type
+ * shall be ignored when processing received SA payload.
+ */
+ public abstract static class Transform {
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ TRANSFORM_TYPE_ENCR,
+ TRANSFORM_TYPE_PRF,
+ TRANSFORM_TYPE_INTEG,
+ TRANSFORM_TYPE_DH,
+ TRANSFORM_TYPE_ESN
+ })
+ public @interface TransformType {}
+
+ public static final int TRANSFORM_TYPE_ENCR = 1;
+ public static final int TRANSFORM_TYPE_PRF = 2;
+ public static final int TRANSFORM_TYPE_INTEG = 3;
+ public static final int TRANSFORM_TYPE_DH = 4;
+ public static final int TRANSFORM_TYPE_ESN = 5;
+
+ private static final byte LAST_TRANSFORM = 0;
+ private static final byte NOT_LAST_TRANSFORM = 3;
+
+ // 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
+
+ @VisibleForTesting
+ static AttributeDecoder sAttributeDecoder =
+ new AttributeDecoder() {
+ public List<Attribute> decodeAttributes(int length, ByteBuffer inputBuffer)
+ throws IkeProtocolException {
+ List<Attribute> list = new LinkedList<>();
+ int parsedLength = BASIC_TRANSFORM_LEN;
+ while (parsedLength < length) {
+ Pair<Attribute, Integer> pair = Attribute.readFrom(inputBuffer);
+ parsedLength += pair.second;
+ list.add(pair.first);
+ }
+ // TODO: Validate that parsedLength equals to length.
+ return list;
+ }
+ };
+
+ // Only supported type falls into {@link TransformType}
+ public final int type;
+ public final int id;
+ public final boolean isSupported;
+
+ /** Construct an instance of Transform for building an outbound packet. */
+ protected Transform(int type, int id) {
+ this.type = type;
+ this.id = id;
+ if (!isSupportedTransformId(id)) {
+ throw new IllegalArgumentException(
+ "Unsupported " + getTransformTypeString() + " Algorithm ID: " + id);
+ }
+ this.isSupported = true;
+ }
+
+ /** Construct an instance of Transform for decoding an inbound packet. */
+ protected Transform(int type, int id, List<Attribute> attributeList) {
+ this.type = type;
+ this.id = id;
+ this.isSupported =
+ isSupportedTransformId(id) && !hasUnrecognizedAttribute(attributeList);
+ }
+
+ @VisibleForTesting
+ static Transform readFrom(ByteBuffer inputBuffer) throws IkeProtocolException {
+ byte isLast = inputBuffer.get();
+ if (isLast != LAST_TRANSFORM && isLast != NOT_LAST_TRANSFORM) {
+ throw new InvalidSyntaxException(
+ "Invalid value of Last Transform Substructure: " + isLast);
+ }
+
+ // Skip RESERVED byte
+ 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(new byte[TRANSFORM_RESERVED_FIELD_LEN]);
+
+ int id = Short.toUnsignedInt(inputBuffer.getShort());
+
+ // Decode attributes
+ List<Attribute> attributeList = sAttributeDecoder.decodeAttributes(length, inputBuffer);
+
+ validateAttributeUniqueness(attributeList);
+
+ switch (type) {
+ case TRANSFORM_TYPE_ENCR:
+ return new EncryptionTransform(id, attributeList);
+ case TRANSFORM_TYPE_PRF:
+ return new PrfTransform(id, attributeList);
+ case TRANSFORM_TYPE_INTEG:
+ return new IntegrityTransform(id, attributeList);
+ case TRANSFORM_TYPE_DH:
+ return new DhGroupTransform(id, attributeList);
+ case TRANSFORM_TYPE_ESN:
+ return new EsnTransform(id, attributeList);
+ default:
+ return new UnrecognizedTransform(type, id, attributeList);
+ }
+ }
+
+ // Throw InvalidSyntaxException if there are multiple Attributes of the same type
+ private static void validateAttributeUniqueness(List<Attribute> attributeList)
+ throws IkeProtocolException {
+ Set<Integer> foundTypes = new ArraySet<>();
+ for (Attribute attr : attributeList) {
+ if (!foundTypes.add(attr.type)) {
+ throw new InvalidSyntaxException(
+ "There are multiple Attributes of the same type. ");
+ }
+ }
+ }
+
+ // Check if there is Attribute with unrecognized type.
+ protected abstract boolean hasUnrecognizedAttribute(List<Attribute> attributeList);
+
+ // 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.
+ *
+ * @return Tranform Type as a String.
+ */
+ public abstract String getTransformTypeString();
+
+ // TODO: Add abstract getTransformIdString() to return specific algorithm/dhGroup name
+ }
+
+ /**
+ * EncryptionTransform represents an encryption algorithm. It may contain an Atrribute
+ * specifying the key length.
+ *
+ * @see <a href="https://tools.ietf.org/html/rfc7296#section-3.3.2">RFC 7296, Internet Key
+ * Exchange Protocol Version 2 (IKEv2)</a>
+ */
+ public static final class EncryptionTransform extends Transform {
+ public static final int KEY_LEN_UNSPECIFIED = 0;
+
+ // 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
+ * outbound packet.
+ *
+ * @param id the IKE standard Transform ID.
+ */
+ public EncryptionTransform(@EncryptionAlgorithm int id) {
+ this(id, KEY_LEN_UNSPECIFIED);
+ }
+
+ /**
+ * Contruct an instance of EncryptionTransform with variable key length for building an
+ * outbound packet.
+ *
+ * @param id the IKE standard Transform ID.
+ * @param specifiedKeyLength the specified key length of this encryption algorithm.
+ */
+ public EncryptionTransform(@EncryptionAlgorithm int id, int specifiedKeyLength) {
+ super(Transform.TRANSFORM_TYPE_ENCR, id);
+
+ mSpecifiedKeyLength = specifiedKeyLength;
+ try {
+ validateKeyLength();
+ } catch (InvalidSyntaxException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ /**
+ * Contruct an instance of EncryptionTransform for decoding an inbound packet.
+ *
+ * @param id the IKE standard Transform ID.
+ * @param attributeList the decoded list of Attribute.
+ * @throws InvalidSyntaxException for syntax error.
+ */
+ protected EncryptionTransform(int id, List<Attribute> attributeList)
+ throws InvalidSyntaxException {
+ super(Transform.TRANSFORM_TYPE_ENCR, id, attributeList);
+ if (!isSupported) {
+ mSpecifiedKeyLength = KEY_LEN_UNSPECIFIED;
+ } else {
+ if (attributeList.size() == 0) {
+ mSpecifiedKeyLength = KEY_LEN_UNSPECIFIED;
+ } else {
+ KeyLengthAttribute attr = getKeyLengthAttribute(attributeList);
+ mSpecifiedKeyLength = attr.keyLength;
+ }
+ validateKeyLength();
+ }
+ }
+
+ /**
+ * Get the specified key length.
+ *
+ * @return the specified key length.
+ */
+ public int getSpecifiedKeyLength() {
+ return mSpecifiedKeyLength;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(type, id, mSpecifiedKeyLength);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof EncryptionTransform)) return false;
+
+ EncryptionTransform other = (EncryptionTransform) o;
+ return (type == other.type
+ && id == other.id
+ && mSpecifiedKeyLength == other.mSpecifiedKeyLength);
+ }
+
+ @Override
+ protected boolean isSupportedTransformId(int id) {
+ return SaProposal.isSupportedEncryptionAlgorithm(id);
+ }
+
+ @Override
+ protected boolean hasUnrecognizedAttribute(List<Attribute> attributeList) {
+ for (Attribute attr : attributeList) {
+ if (attr instanceof UnrecognizedAttribute) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private KeyLengthAttribute getKeyLengthAttribute(List<Attribute> attributeList) {
+ for (Attribute attr : attributeList) {
+ if (attr.type == Attribute.ATTRIBUTE_TYPE_KEY_LENGTH) {
+ return (KeyLengthAttribute) attr;
+ }
+ }
+ throw new IllegalArgumentException("Cannot find Attribute with Key Length type");
+ }
+
+ private void validateKeyLength() throws InvalidSyntaxException {
+ switch (id) {
+ case SaProposal.ENCRYPTION_ALGORITHM_3DES:
+ if (mSpecifiedKeyLength != KEY_LEN_UNSPECIFIED) {
+ throw new InvalidSyntaxException(
+ "Must not set Key Length value for this "
+ + getTransformTypeString()
+ + " Algorithm ID: "
+ + id);
+ }
+ return;
+ case SaProposal.ENCRYPTION_ALGORITHM_AES_CBC:
+ /* fall through */
+ case SaProposal.ENCRYPTION_ALGORITHM_AES_GCM_8:
+ /* fall through */
+ case SaProposal.ENCRYPTION_ALGORITHM_AES_GCM_12:
+ /* fall through */
+ case SaProposal.ENCRYPTION_ALGORITHM_AES_GCM_16:
+ if (mSpecifiedKeyLength == KEY_LEN_UNSPECIFIED) {
+ throw new InvalidSyntaxException(
+ "Must set Key Length value for this "
+ + getTransformTypeString()
+ + " Algorithm ID: "
+ + id);
+ }
+ 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()
+ + " Algorithm ID: "
+ + id);
+ }
+ return;
+ default:
+ // Won't hit here.
+ throw new IllegalArgumentException(
+ "Unrecognized Encryption Algorithm ID: " + id);
+ }
+ }
+
+ @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";
+ }
+
+ @Override
+ @NonNull
+ public String toString() {
+ return SaProposal.getEncryptionAlgorithmString(id)
+ + "("
+ + getSpecifiedKeyLength()
+ + ")";
+ }
+ }
+
+ /**
+ * PrfTransform represents an pseudorandom function.
+ *
+ * @see <a href="https://tools.ietf.org/html/rfc7296#section-3.3.2">RFC 7296, Internet Key
+ * Exchange Protocol Version 2 (IKEv2)</a>
+ */
+ public static final class PrfTransform extends Transform {
+ /**
+ * Contruct an instance of PrfTransform for building an outbound packet.
+ *
+ * @param id the IKE standard Transform ID.
+ */
+ public PrfTransform(@PseudorandomFunction int id) {
+ super(Transform.TRANSFORM_TYPE_PRF, id);
+ }
+
+ /**
+ * Contruct an instance of PrfTransform for decoding an inbound packet.
+ *
+ * @param id the IKE standard Transform ID.
+ * @param attributeList the decoded list of Attribute.
+ * @throws InvalidSyntaxException for syntax error.
+ */
+ protected PrfTransform(int id, List<Attribute> attributeList)
+ throws InvalidSyntaxException {
+ super(Transform.TRANSFORM_TYPE_PRF, id, attributeList);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(type, id);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof PrfTransform)) return false;
+
+ PrfTransform other = (PrfTransform) o;
+ return (type == other.type && id == other.id);
+ }
+
+ @Override
+ protected boolean isSupportedTransformId(int id) {
+ return SaProposal.isSupportedPseudorandomFunction(id);
+ }
+
+ @Override
+ protected boolean hasUnrecognizedAttribute(List<Attribute> attributeList) {
+ return !attributeList.isEmpty();
+ }
+
+ @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";
+ }
+
+ @Override
+ @NonNull
+ public String toString() {
+ return SaProposal.getPseudorandomFunctionString(id);
+ }
+ }
+
+ /**
+ * IntegrityTransform represents an integrity algorithm.
+ *
+ * <p>Proposing integrity algorithm for ESP SA is optional. Omitting the IntegrityTransform is
+ * equivalent to including it with a value of NONE. When multiple integrity algorithms are
+ * provided, choosing any of them are acceptable.
+ *
+ * @see <a href="https://tools.ietf.org/html/rfc7296#section-3.3.2">RFC 7296, Internet Key
+ * Exchange Protocol Version 2 (IKEv2)</a>
+ */
+ public static final class IntegrityTransform extends Transform {
+ /**
+ * Contruct an instance of IntegrityTransform for building an outbound packet.
+ *
+ * @param id the IKE standard Transform ID.
+ */
+ public IntegrityTransform(@IntegrityAlgorithm int id) {
+ super(Transform.TRANSFORM_TYPE_INTEG, id);
+ }
+
+ /**
+ * Contruct an instance of IntegrityTransform for decoding an inbound packet.
+ *
+ * @param id the IKE standard Transform ID.
+ * @param attributeList the decoded list of Attribute.
+ * @throws InvalidSyntaxException for syntax error.
+ */
+ protected IntegrityTransform(int id, List<Attribute> attributeList)
+ throws InvalidSyntaxException {
+ super(Transform.TRANSFORM_TYPE_INTEG, id, attributeList);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(type, id);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof IntegrityTransform)) return false;
+
+ IntegrityTransform other = (IntegrityTransform) o;
+ return (type == other.type && id == other.id);
+ }
+
+ @Override
+ protected boolean isSupportedTransformId(int id) {
+ return SaProposal.isSupportedIntegrityAlgorithm(id);
+ }
+
+ @Override
+ protected boolean hasUnrecognizedAttribute(List<Attribute> attributeList) {
+ return !attributeList.isEmpty();
+ }
+
+ @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";
+ }
+
+ @Override
+ @NonNull
+ public String toString() {
+ return SaProposal.getIntegrityAlgorithmString(id);
+ }
+ }
+
+ /**
+ * DhGroupTransform represents a Diffie-Hellman Group
+ *
+ * <p>Proposing DH group for non-first Child SA is optional. Omitting the DhGroupTransform is
+ * equivalent to including it with a value of NONE. When multiple DH groups are provided,
+ * choosing any of them are acceptable.
+ *
+ * @see <a href="https://tools.ietf.org/html/rfc7296#section-3.3.2">RFC 7296, Internet Key
+ * Exchange Protocol Version 2 (IKEv2)</a>
+ */
+ public static final class DhGroupTransform extends Transform {
+ /**
+ * Contruct an instance of DhGroupTransform for building an outbound packet.
+ *
+ * @param id the IKE standard Transform ID.
+ */
+ public DhGroupTransform(@DhGroup int id) {
+ super(Transform.TRANSFORM_TYPE_DH, id);
+ }
+
+ /**
+ * Contruct an instance of DhGroupTransform for decoding an inbound packet.
+ *
+ * @param id the IKE standard Transform ID.
+ * @param attributeList the decoded list of Attribute.
+ * @throws InvalidSyntaxException for syntax error.
+ */
+ protected DhGroupTransform(int id, List<Attribute> attributeList)
+ throws InvalidSyntaxException {
+ super(Transform.TRANSFORM_TYPE_DH, id, attributeList);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(type, id);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof DhGroupTransform)) return false;
+
+ DhGroupTransform other = (DhGroupTransform) o;
+ return (type == other.type && id == other.id);
+ }
+
+ @Override
+ protected boolean isSupportedTransformId(int id) {
+ return SaProposal.isSupportedDhGroup(id);
+ }
+
+ @Override
+ protected boolean hasUnrecognizedAttribute(List<Attribute> attributeList) {
+ return !attributeList.isEmpty();
+ }
+
+ @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";
+ }
+
+ @Override
+ @NonNull
+ public String toString() {
+ return SaProposal.getDhGroupString(id);
+ }
+ }
+
+ /**
+ * EsnTransform represents ESN policy that indicates if IPsec SA uses tranditional 32-bit
+ * sequence numbers or extended(64-bit) sequence numbers.
+ *
+ * <p>Currently IKE library only supports negotiating IPsec SA that do not use extended sequence
+ * numbers. The Transform ID of EsnTransform in outbound packets is not user configurable.
+ *
+ * @see <a href="https://tools.ietf.org/html/rfc7296#section-3.3.2">RFC 7296, Internet Key
+ * Exchange Protocol Version 2 (IKEv2)</a>
+ */
+ public static final class EsnTransform extends Transform {
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({ESN_POLICY_NO_EXTENDED, ESN_POLICY_EXTENDED})
+ public @interface EsnPolicy {}
+
+ public static final int ESN_POLICY_NO_EXTENDED = 0;
+ public static final int ESN_POLICY_EXTENDED = 1;
+
+ /**
+ * Construct an instance of EsnTransform indicates using no-extended sequence numbers for
+ * building an outbound packet.
+ */
+ public EsnTransform() {
+ super(Transform.TRANSFORM_TYPE_ESN, ESN_POLICY_NO_EXTENDED);
+ }
+
+ /**
+ * Contruct an instance of EsnTransform for decoding an inbound packet.
+ *
+ * @param id the IKE standard Transform ID.
+ * @param attributeList the decoded list of Attribute.
+ * @throws InvalidSyntaxException for syntax error.
+ */
+ protected EsnTransform(int id, List<Attribute> attributeList)
+ throws InvalidSyntaxException {
+ super(Transform.TRANSFORM_TYPE_ESN, id, attributeList);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(type, id);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof EsnTransform)) return false;
+
+ EsnTransform other = (EsnTransform) o;
+ return (type == other.type && id == other.id);
+ }
+
+ @Override
+ protected boolean isSupportedTransformId(int id) {
+ return (id == ESN_POLICY_NO_EXTENDED || id == ESN_POLICY_EXTENDED);
+ }
+
+ @Override
+ protected boolean hasUnrecognizedAttribute(List<Attribute> attributeList) {
+ return !attributeList.isEmpty();
+ }
+
+ @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";
+ }
+
+ @Override
+ @NonNull
+ public String toString() {
+ if (id == ESN_POLICY_NO_EXTENDED) {
+ return "ESN_No_Extended";
+ }
+ return "ESN_Extended";
+ }
+ }
+
+ /**
+ * UnrecognizedTransform represents a Transform with unrecognized Transform Type.
+ *
+ * <p>Proposals containing an UnrecognizedTransform should be ignored.
+ */
+ protected static final class UnrecognizedTransform extends Transform {
+ protected UnrecognizedTransform(int type, int id, List<Attribute> attributeList) {
+ super(type, id, attributeList);
+ }
+
+ @Override
+ protected boolean isSupportedTransformId(int id) {
+ return false;
+ }
+
+ @Override
+ protected boolean hasUnrecognizedAttribute(List<Attribute> attributeList) {
+ 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.
+ *
+ * @return Tranform Type of Unrecognized Transform as a String.
+ */
+ @Override
+ public String getTransformTypeString() {
+ return "Unrecognized Transform Type.";
+ }
+ }
+
+ /**
+ * Attribute is an abtract base class for completing the specification of some {@link
+ * Transform}.
+ *
+ * <p>Attribute is either in Type/Value format or Type/Length/Value format. For TV format,
+ * Attribute length is always 4 bytes containing value for 2 bytes. While for TLV format,
+ * Attribute length is determined by length field.
+ *
+ * <p>Currently only Key Length type is supported
+ *
+ * @see <a href="https://tools.ietf.org/html/rfc7296#section-3.3.5">RFC 7296, Internet Key
+ * Exchange Protocol Version 2 (IKEv2)</a>
+ */
+ public abstract static class Attribute {
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({ATTRIBUTE_TYPE_KEY_LENGTH})
+ public @interface AttributeType {}
+
+ // Support only one Attribute type: Key Length. Should use Type/Value format.
+ public static final int ATTRIBUTE_TYPE_KEY_LENGTH = 14;
+
+ // 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;
+
+ /** Construct an instance of an Attribute when decoding message. */
+ protected Attribute(int type) {
+ this.type = type;
+ }
+
+ @VisibleForTesting
+ static Pair<Attribute, Integer> readFrom(ByteBuffer inputBuffer)
+ throws IkeProtocolException {
+ short formatAndType = inputBuffer.getShort();
+ int format = formatAndType & ATTRIBUTE_FORMAT_MASK;
+ int type = formatAndType & ATTRIBUTE_TYPE_MASK;
+
+ int length = 0;
+ byte[] value = new byte[0];
+ if (format == ATTRIBUTE_FORMAT_TV) {
+ // Type/Value format
+ length = TV_ATTRIBUTE_TOTAL_LEN;
+ value = new byte[TV_ATTRIBUTE_VALUE_LEN];
+ } else {
+ // Type/Length/Value format
+ if (type == ATTRIBUTE_TYPE_KEY_LENGTH) {
+ throw new InvalidSyntaxException("Wrong format in Transform Attribute");
+ }
+
+ length = Short.toUnsignedInt(inputBuffer.getShort());
+ int valueLen = length - TVL_ATTRIBUTE_HEADER_LEN;
+ // IkeMessage will catch exception if valueLen is negative.
+ value = new byte[valueLen];
+ }
+
+ inputBuffer.get(value);
+
+ switch (type) {
+ case ATTRIBUTE_TYPE_KEY_LENGTH:
+ return new Pair(new KeyLengthAttribute(value), length);
+ default:
+ 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 */
+ public static final class KeyLengthAttribute extends Attribute {
+ public final int keyLength;
+
+ protected KeyLengthAttribute(byte[] value) {
+ this(Short.toUnsignedInt(ByteBuffer.wrap(value).getShort()));
+ }
+
+ protected KeyLengthAttribute(int keyLength) {
+ 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;
+ }
+ }
+
+ /**
+ * UnrecognizedAttribute represents a Attribute with unrecoginzed Attribute Type.
+ *
+ * <p>Transforms containing UnrecognizedAttribute should be ignored.
+ */
+ protected static final class UnrecognizedAttribute extends Attribute {
+ 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.");
+ }
+ }
+
+ /**
+ * Encode SA 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) {
+ 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);
+ }
+ }
+
+ /**
+ * Get entire payload length.
+ *
+ * @return entire payload length.
+ */
+ @Override
+ protected int getPayloadLength() {
+ int len = GENERIC_HEADER_LENGTH;
+
+ for (Proposal p : proposalList) len += p.getProposalLength();
+
+ return len;
+ }
+
+ /**
+ * Return the payload type as a String.
+ *
+ * @return the payload type as a String.
+ */
+ @Override
+ public String getTypeString() {
+ return "SA";
+ }
+
+ @Override
+ @NonNull
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ if (isSaResponse) {
+ sb.append("SA Response: ");
+ } else {
+ sb.append("SA Request: ");
+ }
+
+ int len = proposalList.size();
+ for (int i = 0; i < len; i++) {
+ sb.append(proposalList.get(i).toString());
+ if (i < len - 1) sb.append(", ");
+ }
+
+ return sb.toString();
+ }
+}