blob: 7f3529ac2322561990d45d663b102a2450f28135 [file] [log] [blame]
/*
* 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.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");
}
/**
* 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 IPsec SPI resources in the outbound Create Child request
*
* <p>This method is usually called when an IKE library fails to receive a Create Child response
* before it is terminated. It is also safe to call after the Create Child exchange has
* succeeded because the newly created IpSecTransform pair will hold the IPsec SPI resource.
*/
public void releaseChildSpiResourcesIfExists() {
for (Proposal proposal : proposalList) {
if (proposal instanceof ChildProposal) {
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();
}
}