/*
 * 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.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.net.ipsec.ike.exceptions.InvalidKeException;
import android.net.ipsec.ike.exceptions.InvalidSyntaxException;
import android.net.ipsec.ike.exceptions.NoValidProposalChosenException;
import android.os.PersistableBundle;
import android.util.ArraySet;
import android.util.Pair;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.net.ipsec.ike.utils.IkeSecurityParameterIndex;
import com.android.internal.net.ipsec.ike.utils.IkeSpiGenerator;
import com.android.internal.net.ipsec.ike.utils.IpSecSpiGenerator;

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,
            IkeSpiGenerator ikeSpiGenerator,
            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],
                            ikeSpiGenerator,
                            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,
            IkeSpiGenerator ikeSpiGenerator,
            InetAddress localAddress)
            throws IOException {
        this(isResp, spiSize, localAddress);

        proposalList.add(
                IkeProposal.createIkeProposal(
                        proposalNumber /* number */,
                        spiSize,
                        saProposal,
                        ikeSpiGenerator,
                        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 IkeSessionParams and ChildSessionParams
        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,
            IpSecSpiGenerator ipSecSpiGenerator,
            InetAddress localAddress)
            throws SpiUnavailableException, ResourceUnavailableException {
        this(false /* isResp */, ipSecSpiGenerator, localAddress);

        if (saProposals.length < 1) {
            throw new IllegalArgumentException("Invalid SA payload.");
        }

        // TODO: Check that saProposals.length <= 255 in IkeSessionParams and ChildSessionParams

        for (int i = 0; i < saProposals.length; i++) {
            // Proposal number must start from 1.
            proposalList.add(
                    ChildProposal.createChildProposal(
                            (byte) (i + 1) /* number */,
                            saProposals[i],
                            ipSecSpiGenerator,
                            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,
            IpSecSpiGenerator ipSecSpiGenerator,
            InetAddress localAddress)
            throws SpiUnavailableException, ResourceUnavailableException {
        this(true /* isResp */, ipSecSpiGenerator, localAddress);

        proposalList.add(
                ChildProposal.createChildProposal(
                        proposalNumber /* number */, saProposal, ipSecSpiGenerator, localAddress));

        getIkeLog().d(TAG, "Generate " + toString());
    }

    /** Constructor for building an outbound SA Payload for Child SA negotiation. */
    private IkeSaPayload(
            boolean isResp, IpSecSpiGenerator ipSecSpiGenerator, 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 /* ikeSpiGenerator unused */,
                null /* localAddress unused */);
    }

    /**
     * Construct an instance of IkeSaPayload for building an outbound request for Rekey IKE.
     *
     * @param saProposals the array of all IKE SA Proposals.
     * @param ikeSpiGenerator the IKE SPI generator.
     * @param localAddress the local address assigned on-device.
     */
    public static IkeSaPayload createRekeyIkeSaRequestPayload(
            IkeSaProposal[] saProposals, IkeSpiGenerator ikeSpiGenerator, InetAddress localAddress)
            throws IOException {
        return new IkeSaPayload(
                false /* isResp */, SPI_LEN_IKE, saProposals, ikeSpiGenerator, 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 ikeSpiGenerator the IKE SPI generator.
     * @param localAddress the local address assigned on-device.
     */
    public static IkeSaPayload createRekeyIkeSaResponsePayload(
            byte respProposalNumber,
            IkeSaProposal saProposal,
            IkeSpiGenerator ikeSpiGenerator,
            InetAddress localAddress)
            throws IOException {
        return new IkeSaPayload(
                true /* isResp */,
                SPI_LEN_IKE,
                respProposalNumber,
                saProposal,
                ikeSpiGenerator,
                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 ipSecSpiGenerator the IPsec SPI generator.
     * @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,
            IpSecSpiGenerator ipSecSpiGenerator,
            InetAddress localAddress)
            throws SpiUnavailableException, ResourceUnavailableException {

        return new IkeSaPayload(saProposals, ipSecSpiGenerator, 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 ipSecSpiGenerator the IPsec SPI generator.
     * @param localAddress the local address assigned on-device.
     */
    public static IkeSaPayload createChildSaResponsePayload(
            byte respProposalNumber,
            ChildSaProposal saProposal,
            IpSecSpiGenerator ipSecSpiGenerator,
            InetAddress localAddress)
            throws SpiUnavailableException, ResourceUnavailableException {
        return new IkeSaPayload(respProposalNumber, saProposal, ipSecSpiGenerator, 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");
    }

    /**
     * Finds or builds the negotiated Child proposal when there is a key exchange.
     *
     * <p>This method will be used in Remote Rekey Child. For better interoperability, IKE library
     * allows the server to set up new Child SA with a different DH group if (1) caller has
     * configured that DH group in the Child SA Proposal, or (2) that DH group is the DH group
     * negotiated as part of IKE Session.
     *
     * @param currentProposal the current negotiated Child SA Proposal
     * @param callerConfiguredProposals all caller configured Child SA Proposals
     * @param reqKePayloadDh the DH group in the request KE payload
     * @param ikeDh the DH group negotiated as part of IKE Session
     * @return the negotiated Child SA Proposal
     * @throws NoValidProposalChosenException when there is no acceptable proposal in the SA payload
     * @throws InvalidKeException when the request KE payload has a mismatched DH group
     */
    public ChildSaProposal getNegotiatedChildProposalWithDh(
            ChildSaProposal currentProposal,
            List<ChildSaProposal> callerConfiguredProposals,
            int reqKePayloadDh,
            int ikeDh)
            throws NoValidProposalChosenException, InvalidKeException {

        List<ChildSaProposal> proposalCandidates = new ArrayList<>();
        for (ChildSaProposal callerProposal : callerConfiguredProposals) {
            // Check if current proposal can be negotiated from the callerProposal.
            if (!currentProposal.isNegotiatedFromExceptDhGroup(callerProposal)) {
                continue;
            }

            // Check if current proposal can be negotiated from the Rekey Child request.
            // Try all DH groups in this caller configured proposal and see if current
            // proposal + the DH group can be negotiated from the Rekey request. For
            // better interoperability, if caller does not configure any DH group for
            // this proposal, try DH group negotiated as part of IKE Session. Some
            // implementation will request using the IKE DH group when rekeying the
            // Child SA which is built during IKE Auth
            if (callerProposal.getDhGroups().isEmpty()) {
                callerProposal = callerProposal.getCopyWithAdditionalDhTransform(ikeDh);
            }

            for (int callerDh : callerProposal.getDhGroups()) {
                ChildSaProposal negotiatedProposal =
                        currentProposal.getCopyWithAdditionalDhTransform(callerDh);
                try {
                    getNegotiatedProposalNumber(negotiatedProposal);
                    proposalCandidates.add(negotiatedProposal);
                } catch (NoValidProposalChosenException e) {
                    continue;
                }
            }
        }

        // Check if any negotiated proposal match reqKePayloadDh
        if (proposalCandidates.isEmpty()) {
            throw new NoValidProposalChosenException("No acceptable SA proposal in the request");
        } else {
            for (ChildSaProposal negotiatedProposal : proposalCandidates) {
                if (reqKePayloadDh == negotiatedProposal.getDhGroups().get(0)) {
                    return negotiatedProposal;
                }
            }
            throw new InvalidKeException(proposalCandidates.get(0).getDhGroups().get(0));
        }
    }

    /**
     * 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,
            IkeSpiGenerator ikeSpiGenerator,
            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(ikeSpiGenerator, 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(ikeSpiGenerator, 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 ipSecSpiGenerator the SPI generator 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,
            IpSecSpiGenerator ipSecSpiGenerator,
            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(ipSecSpiGenerator, remoteAddress);
            }
            // Allocate responder's inbound SPI as needed for locally initiated Child SA creation
            if (respProposal.getChildSpiResource() == null) {
                respProposal.allocateResourceForRemoteChildSpi(ipSecSpiGenerator, 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;
    }

    /**
     * Release SPI resources in the outbound Create IKE/Child request
     *
     * <p>This method is usually called when an IKE library fails to receive a Create IKE/Child
     * response before it is terminated. It is also safe to call after the Create IKE/Child exchange
     * has succeeded because the newly created IkeSaRecord or ChildSaRecord (IpSecTransform pair)
     * will hold the SPI resource.
     */
    public void releaseSpiResources() {
        for (Proposal proposal : proposalList) {
            proposal.releaseSpiResourceIfExists();
        }
    }

    /**
     * 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;

        private static TransformDecoder sTransformDecoder = new TransformDecoderImpl();

        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);
            }
        }

        private static class TransformDecoderImpl implements 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);
                    transformArray[i] = transform;
                }
                return transformArray;
            }
        }

        /** Package private method to set TransformDecoder for testing purposes */
        @VisibleForTesting
        static void setTransformDecoder(TransformDecoder decoder) {
            sTransformDecoder = decoder;
        }

        /** Package private method to reset TransformDecoder */
        @VisibleForTesting
        static void resetTransformDecoder() {
            sTransformDecoder = new TransformDecoderImpl();
        }

        /** 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,
                IkeSpiGenerator ikeSpiGenerator,
                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
                            : ikeSpiGenerator.allocateSpi(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(
                IkeSpiGenerator ikeSpiGenerator, InetAddress remoteAddress) throws IOException {
            mIkeSpiResource = ikeSpiGenerator.allocateSpi(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,
                IpSecSpiGenerator ipSecSpiGenerator,
                InetAddress localAddress)
                throws SpiUnavailableException, ResourceUnavailableException {
            return new ChildProposal(
                    number, ipSecSpiGenerator.allocateSpi(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(
                IpSecSpiGenerator ipSecSpiGenerator, InetAddress remoteAddress)
                throws ResourceUnavailableException, SpiUnavailableException {
            mChildSpiResource = ipSecSpiGenerator.allocateSpi(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

        private static AttributeDecoder sAttributeDecoder = new AttributeDecoderImpl();

        // 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);
            }
        }

        private static class AttributeDecoderImpl implements AttributeDecoder {
            @Override
            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; // Increase parsedLength by the Atrribute length
                    list.add(pair.first);
                }
                // TODO: Validate that parsedLength equals to length.
                return list;
            }
        }

        /** Package private method to set AttributeDecoder for testing purpose */
        @VisibleForTesting
        static void setAttributeDecoder(AttributeDecoder decoder) {
            sAttributeDecoder = decoder;
        }

        /** Package private method to reset AttributeDecoder */
        @VisibleForTesting
        static void resetAttributeDecoder() {
            sAttributeDecoder = new AttributeDecoderImpl();
        }

        // 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;

        private static final String ID_KEY = "id";
        private static final String SPECIFIED_KEY_LEN_KEY = "mSpecifiedKeyLength";

        // 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);
            }
        }

        /** Constructs this object by deserializing a PersistableBundle */
        public static EncryptionTransform fromPersistableBundle(@NonNull PersistableBundle in) {
            Objects.requireNonNull(in, "PersistableBundle is null");
            return new EncryptionTransform(in.getInt(ID_KEY), in.getInt(SPECIFIED_KEY_LEN_KEY));
        }

        /** Serializes this object to a PersistableBundle */
        public PersistableBundle toPersistableBundle() {
            final PersistableBundle result = new PersistableBundle();
            result.putInt(ID_KEY, id);
            result.putInt(SPECIFIED_KEY_LEN_KEY, mSpecifiedKeyLength);

            return result;
        }

        /**
         * 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 IkeSaProposal.getSupportedEncryptionAlgorithms().contains(id)
                    || ChildSaProposal.getSupportedEncryptionAlgorithms().contains(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:
                    /* fall through */
                case SaProposal.ENCRYPTION_ALGORITHM_CHACHA20_POLY1305:
                    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_CTR:
                    /* 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() {
            if (isSupported) {
                return SaProposal.getEncryptionAlgorithmString(id)
                        + "("
                        + getSpecifiedKeyLength()
                        + ")";
            } else {
                return "ENCR(" + id + ")";
            }
        }
    }

    /**
     * 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 IkeSaProposal.getSupportedPseudorandomFunctions().contains(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() {
            if (isSupported) {
                return SaProposal.getPseudorandomFunctionString(id);
            } else {
                return "PRF(" + 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 IkeSaProposal.getSupportedIntegrityAlgorithms().contains(id)
                    || ChildSaProposal.getSupportedIntegrityAlgorithms().contains(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() {
            if (isSupported) {
                return SaProposal.getIntegrityAlgorithmString(id);
            } else {
                return "AUTH(" + 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.getSupportedDhGroups().contains(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() {
            if (isSupported) {
                return SaProposal.getDhGroupString(id);
            } else {
                return "DH(" + 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();
    }
}
