blob: 77cc9f4ea5bd0de7e1368b581805a2cd2e55fbf2 [file] [log] [blame]
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.internal.net.ipsec.ike.message;
import android.net.ipsec.ike.exceptions.IkeProtocolException;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.net.ipsec.ike.crypto.IkeCipher;
import com.android.internal.net.ipsec.ike.crypto.IkeCombinedModeCipher;
import com.android.internal.net.ipsec.ike.crypto.IkeMacIntegrity;
import com.android.internal.net.ipsec.ike.crypto.IkeNormalModeCipher;
import java.nio.ByteBuffer;
import java.security.GeneralSecurityException;
import java.security.SecureRandom;
import java.util.Arrays;
import javax.crypto.AEADBadTagException;
import javax.crypto.IllegalBlockSizeException;
/**
* IkeEncryptedPayloadBody is a package private class that represents an IKE payload substructure
* that contains initialization vector, encrypted content, padding, pad length and integrity
* checksum.
*
* <p>Both an Encrypted Payload (IkeSkPayload) and an EncryptedFragmentPayload (IkeSkfPayload)
* consists of an IkeEncryptedPayloadBody instance.
*
* <p>When using normal cipher with separate integrity algorithm, data to authenticate includes
* bytes from beginning of IKE header to the pad length, which are concatenation of IKE header,
* current payload header, iv and encrypted and padded data.
*
* <p>When using AEAD, additional authentication data(also known as) associated data is required. It
* MUST include bytes from beginning of IKE header to the last octet of the Payload Header of the
* Encrypted Payload. Note fragment number and total fragments are also included if Encrypted
* Payload is SKF.
*
* @see <a href="https://tools.ietf.org/html/rfc7296#page-105">RFC 7296, Internet Key Exchange
* Protocol Version 2 (IKEv2)</a>
* @see <a href="https://tools.ietf.org/html/rfc7383#page-6">RFC 7383, Internet Key Exchange
* Protocol Version 2 (IKEv2) Message Fragmentation</a>
*/
final class IkeEncryptedPayloadBody {
// Length of pad length field.
private static final int PAD_LEN_LEN = 1;
private final byte[] mUnencryptedData;
private final byte[] mEncryptedAndPaddedData;
private final byte[] mIv;
private final byte[] mIntegrityChecksum;
/**
* Package private constructor for constructing an instance of IkeEncryptedPayloadBody from
* decrypting an incoming packet.
*/
IkeEncryptedPayloadBody(
byte[] message,
int encryptedBodyOffset,
IkeMacIntegrity integrityMac,
IkeCipher decryptCipher,
byte[] integrityKey,
byte[] decryptionKey)
throws IkeProtocolException, GeneralSecurityException {
ByteBuffer inputBuffer = ByteBuffer.wrap(message);
// Skip IKE header and generic payload header (and SKF header)
inputBuffer.get(new byte[encryptedBodyOffset]);
// Extract bytes for authentication and decryption.
int expectedIvLen = decryptCipher.getIvLen();
mIv = new byte[expectedIvLen];
int checksumLen = getChecksum(integrityMac, decryptCipher);
int encryptedDataLen = message.length - (encryptedBodyOffset + expectedIvLen + checksumLen);
// IkeMessage will catch exception if encryptedDataLen is negative.
mEncryptedAndPaddedData = new byte[encryptedDataLen];
mIntegrityChecksum = new byte[checksumLen];
inputBuffer.get(mIv).get(mEncryptedAndPaddedData).get(mIntegrityChecksum);
if (decryptCipher.isAead()) {
byte[] dataToAuthenticate = Arrays.copyOfRange(message, 0, encryptedBodyOffset);
mUnencryptedData =
combinedModeDecrypt(
(IkeCombinedModeCipher) decryptCipher,
mEncryptedAndPaddedData,
mIntegrityChecksum,
dataToAuthenticate,
decryptionKey,
mIv);
} else {
byte[] dataToAuthenticate =
Arrays.copyOfRange(message, 0, message.length - checksumLen);
validateInboundChecksumOrThrow(
dataToAuthenticate, integrityMac, integrityKey, mIntegrityChecksum);
mUnencryptedData =
normalModeDecrypt(
mEncryptedAndPaddedData,
(IkeNormalModeCipher) decryptCipher,
decryptionKey,
mIv);
}
}
/**
* Package private constructor for constructing an instance of IkeEncryptedPayloadBody for
* building an outbound packet.
*/
IkeEncryptedPayloadBody(
IkeHeader ikeHeader,
@IkePayload.PayloadType int firstPayloadType,
byte[] skfHeaderBytes,
byte[] unencryptedPayloads,
IkeMacIntegrity integrityMac,
IkeCipher encryptCipher,
byte[] integrityKey,
byte[] encryptionKey) {
this(
ikeHeader,
firstPayloadType,
skfHeaderBytes,
unencryptedPayloads,
integrityMac,
encryptCipher,
integrityKey,
encryptionKey,
encryptCipher.generateIv(),
calculatePadding(unencryptedPayloads.length, encryptCipher.getBlockSize()));
}
/** Package private constructor only for testing. */
@VisibleForTesting
IkeEncryptedPayloadBody(
IkeHeader ikeHeader,
@IkePayload.PayloadType int firstPayloadType,
byte[] skfHeaderBytes,
byte[] unencryptedPayloads,
IkeMacIntegrity integrityMac,
IkeCipher encryptCipher,
byte[] integrityKey,
byte[] encryptionKey,
byte[] iv,
byte[] padding) {
mUnencryptedData = unencryptedPayloads;
mIv = iv;
if (encryptCipher.isAead()) {
byte[] paddedDataWithChecksum =
combinedModeEncrypt(
(IkeCombinedModeCipher) encryptCipher,
ikeHeader,
firstPayloadType,
skfHeaderBytes,
unencryptedPayloads,
encryptionKey,
iv,
padding);
int checkSumLen = ((IkeCombinedModeCipher) encryptCipher).getChecksumLen();
mIntegrityChecksum = new byte[checkSumLen];
mEncryptedAndPaddedData = new byte[paddedDataWithChecksum.length - checkSumLen];
ByteBuffer buffer = ByteBuffer.wrap(paddedDataWithChecksum);
buffer.get(mEncryptedAndPaddedData);
buffer.get(mIntegrityChecksum);
} else {
// Encrypt data
mEncryptedAndPaddedData =
normalModeEncrypt(
unencryptedPayloads,
(IkeNormalModeCipher) encryptCipher,
encryptionKey,
iv,
padding);
// Calculate checksum
mIntegrityChecksum =
generateOutboundChecksum(
ikeHeader,
firstPayloadType,
skfHeaderBytes,
integrityMac,
iv,
mEncryptedAndPaddedData,
integrityKey);
}
}
private int getChecksum(IkeMacIntegrity integrityMac, IkeCipher decryptCipher) {
if (decryptCipher.isAead()) {
return ((IkeCombinedModeCipher) decryptCipher).getChecksumLen();
} else {
return integrityMac.getChecksumLen();
}
}
/** Package private for testing */
@VisibleForTesting
static byte[] generateOutboundChecksum(
IkeHeader ikeHeader,
@IkePayload.PayloadType int firstPayloadType,
byte[] skfHeaderBytes,
IkeMacIntegrity integrityMac,
byte[] iv,
byte[] encryptedAndPaddedData,
byte[] integrityKey) {
// Length from encrypted payload header to the Pad Length field
int encryptedPayloadHeaderToPadLen =
IkePayload.GENERIC_HEADER_LENGTH
+ skfHeaderBytes.length
+ iv.length
+ encryptedAndPaddedData.length;
// Calculate length of authentication data and allocate ByteBuffer.
int dataToAuthenticateLength = IkeHeader.IKE_HEADER_LENGTH + encryptedPayloadHeaderToPadLen;
ByteBuffer authenticatedSectionBuffer = ByteBuffer.allocate(dataToAuthenticateLength);
// Build data to authenticate.
int encryptedPayloadLength = encryptedPayloadHeaderToPadLen + integrityMac.getChecksumLen();
ikeHeader.encodeToByteBuffer(authenticatedSectionBuffer, encryptedPayloadLength);
IkePayload.encodePayloadHeaderToByteBuffer(
firstPayloadType, encryptedPayloadLength, authenticatedSectionBuffer);
authenticatedSectionBuffer.put(skfHeaderBytes).put(iv).put(encryptedAndPaddedData);
// Calculate checksum
return integrityMac.generateChecksum(integrityKey, authenticatedSectionBuffer.array());
}
/** Package private for testing */
@VisibleForTesting
static void validateInboundChecksumOrThrow(
byte[] dataToAuthenticate,
IkeMacIntegrity integrityMac,
byte[] integrityKey,
byte[] integrityChecksum)
throws GeneralSecurityException {
// TODO: Make it package private and add test.
int checkSumLen = integrityChecksum.length;
byte[] calculatedChecksum = integrityMac.generateChecksum(integrityKey, dataToAuthenticate);
if (!Arrays.equals(integrityChecksum, calculatedChecksum)) {
throw new GeneralSecurityException("Message authentication failed.");
}
}
/** Package private for testing */
@VisibleForTesting
static byte[] normalModeEncrypt(
byte[] dataToEncrypt,
IkeNormalModeCipher encryptCipher,
byte[] encryptionKey,
byte[] iv,
byte[] padding) {
byte[] paddedData = getPaddedData(dataToEncrypt, padding);
// Encrypt data.
return encryptCipher.encrypt(paddedData, encryptionKey, iv);
}
/** Package private for testing */
@VisibleForTesting
static byte[] normalModeDecrypt(
byte[] encryptedData,
IkeNormalModeCipher decryptCipher,
byte[] decryptionKey,
byte[] iv)
throws IllegalBlockSizeException {
byte[] paddedPlaintext = decryptCipher.decrypt(encryptedData, decryptionKey, iv);
return stripPadding(paddedPlaintext);
}
/** Package private for testing */
@VisibleForTesting
static byte[] combinedModeEncrypt(
IkeCombinedModeCipher encryptCipher,
IkeHeader ikeHeader,
@IkePayload.PayloadType int firstPayloadType,
byte[] skfHeaderBytes,
byte[] dataToEncrypt,
byte[] encryptionKey,
byte[] iv,
byte[] padding) {
int dataToAuthenticateLength =
IkeHeader.IKE_HEADER_LENGTH
+ IkePayload.GENERIC_HEADER_LENGTH
+ skfHeaderBytes.length;
ByteBuffer authenticatedSectionBuffer = ByteBuffer.allocate(dataToAuthenticateLength);
byte[] paddedData = getPaddedData(dataToEncrypt, padding);
int encryptedPayloadLength =
IkePayload.GENERIC_HEADER_LENGTH
+ skfHeaderBytes.length
+ iv.length
+ paddedData.length
+ encryptCipher.getChecksumLen();
ikeHeader.encodeToByteBuffer(authenticatedSectionBuffer, encryptedPayloadLength);
IkePayload.encodePayloadHeaderToByteBuffer(
firstPayloadType, encryptedPayloadLength, authenticatedSectionBuffer);
authenticatedSectionBuffer.put(skfHeaderBytes);
return encryptCipher.encrypt(
paddedData, authenticatedSectionBuffer.array(), encryptionKey, iv);
}
/** Package private for testing */
@VisibleForTesting
static byte[] combinedModeDecrypt(
IkeCombinedModeCipher decryptCipher,
byte[] encryptedData,
byte[] checksum,
byte[] dataToAuthenticate,
byte[] decryptionKey,
byte[] iv)
throws AEADBadTagException {
ByteBuffer dataWithChecksumBuffer =
ByteBuffer.allocate(encryptedData.length + checksum.length);
dataWithChecksumBuffer.put(encryptedData);
dataWithChecksumBuffer.put(checksum);
dataWithChecksumBuffer.rewind();
byte[] paddedPlaintext =
decryptCipher.decrypt(
dataWithChecksumBuffer.array(), dataToAuthenticate, decryptionKey, iv);
return stripPadding(paddedPlaintext);
}
/** Package private for testing */
@VisibleForTesting
static byte[] calculatePadding(int dataToEncryptLength, int blockSize) {
// Sum of dataToEncryptLength, PAD_LEN_LEN and padLength should be aligned with block size.
int unpaddedLen = dataToEncryptLength + PAD_LEN_LEN;
int padLength = (unpaddedLen + blockSize - 1) / blockSize * blockSize - unpaddedLen;
byte[] padding = new byte[padLength];
// According to RFC 7296, "Padding MAY contain any value".
new SecureRandom().nextBytes(padding);
return padding;
}
private static byte[] getPaddedData(byte[] data, byte[] padding) {
int padLength = padding.length;
int paddedDataLength = data.length + padLength + PAD_LEN_LEN;
ByteBuffer padBuffer = ByteBuffer.allocate(paddedDataLength);
padBuffer.put(data).put(padding).put((byte) padLength);
return padBuffer.array();
}
private static byte[] stripPadding(byte[] paddedPlaintext) {
// Remove padding. Pad length value is the last byte of the padded unencrypted data.
int padLength = Byte.toUnsignedInt(paddedPlaintext[paddedPlaintext.length - 1]);
int decryptedDataLen = paddedPlaintext.length - padLength - PAD_LEN_LEN;
return Arrays.copyOfRange(paddedPlaintext, 0, decryptedDataLen);
}
/** Package private */
byte[] getUnencryptedData() {
return mUnencryptedData;
}
/** Package private */
int getLength() {
return (mIv.length + mEncryptedAndPaddedData.length + mIntegrityChecksum.length);
}
/** Package private */
byte[] encode() {
ByteBuffer buffer = ByteBuffer.allocate(getLength());
buffer.put(mIv).put(mEncryptedAndPaddedData).put(mIntegrityChecksum);
return buffer.array();
}
}