diff --git a/.github/workflows/macos_ci_jobs.yml b/.github/workflows/macos_ci_jobs.yml index 57a902fe..b2135238 100644 --- a/.github/workflows/macos_ci_jobs.yml +++ b/.github/workflows/macos_ci_jobs.yml @@ -22,9 +22,9 @@ jobs: - uses: actions/checkout@v3 with: submodules: true - - name: Install lcov + - name: Install lcov golang run: | - env HOMEBREW_NO_AUTO_UPDATE=1 brew install lcov + env HOMEBREW_NO_AUTO_UPDATE=1 brew install lcov golang - name: Install clang-format run: | env HOMEBREW_NO_AUTO_UPDATE=1 brew install clang-format @@ -70,9 +70,9 @@ jobs: - uses: actions/checkout@v3 with: submodules: true - - name: Install lcov + - name: Install lcov golang run: | - env HOMEBREW_NO_AUTO_UPDATE=1 brew install lcov + env HOMEBREW_NO_AUTO_UPDATE=1 brew install lcov golang - name: Install clang-format run: | env HOMEBREW_NO_AUTO_UPDATE=1 brew install clang-format @@ -107,9 +107,9 @@ jobs: - uses: actions/checkout@v3 with: submodules: true - - name: Install lcov + - name: Install lcov golang run: | - env HOMEBREW_NO_AUTO_UPDATE=1 brew install lcov + env HOMEBREW_NO_AUTO_UPDATE=1 brew install lcov golang - name: Install clang-format run: | env HOMEBREW_NO_AUTO_UPDATE=1 brew install clang-format diff --git a/CMakeLists.txt b/CMakeLists.txt index a7f14ad6..99026395 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -251,6 +251,7 @@ add_library( amazonCorrettoCryptoProvider SHARED csrc/aes_gcm.cpp csrc/aes_xts.cpp + csrc/aes_cbc.cpp csrc/aes_kwp.cpp csrc/agreement.cpp csrc/bn.cpp @@ -682,16 +683,18 @@ add_custom_target(check-junit-SecurityManager DEPENDS accp-jar tests-jar) -add_custom_target(check-junit-AesGcmLazy +add_custom_target(check-junit-AesLazy COMMAND ${TEST_JAVA_EXECUTABLE} -Dcom.amazon.corretto.crypto.provider.nativeContextReleaseStrategy=LAZY ${TEST_RUNNER_ARGUMENTS} --select-class=com.amazon.corretto.crypto.provider.test.AesTest --select-class=com.amazon.corretto.crypto.provider.test.AesGcmKatTest + --select-class=com.amazon.corretto.crypto.provider.test.AesCbcTest + --select-class=com.amazon.corretto.crypto.provider.test.AesCbcNistTest DEPENDS accp-jar tests-jar) -add_custom_target(check-junit-AesGcmEager +add_custom_target(check-junit-AesEager COMMAND ${TEST_JAVA_EXECUTABLE} -Dcom.amazon.corretto.crypto.provider.nativeContextReleaseStrategy=EAGER ${TEST_RUNNER_ARGUMENTS} @@ -807,9 +810,10 @@ add_custom_target(check check-junit check-junit-SecurityManager check-external-lib - check-junit-AesGcmLazy - check-junit-AesGcmEager - check-junit-DifferentTempDir) + check-junit-AesLazy + check-junit-AesEager + check-junit-DifferentTempDir +) if(ENABLE_NATIVE_TEST_HOOKS) add_custom_target(check-keyutils diff --git a/csrc/aes_cbc.cpp b/csrc/aes_cbc.cpp new file mode 100644 index 00000000..e23dd51c --- /dev/null +++ b/csrc/aes_cbc.cpp @@ -0,0 +1,297 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +#include "buffer.h" +#include "env.h" +#include +#include +#include +#include + +#define AWS_LC_BAD_PADDING_ERROR_CODE 0x1e000065 + +#define EX_BAD_PADDING "javax/crypto/BadPaddingException" + +#define AES_CBC_BLOCK_SIZE_IN_BYTES 16 +#define KEY_LEN_AES128 16 +#define KEY_LEN_AES192 24 +#define KEY_LEN_AES256 32 + +namespace AmazonCorrettoCryptoProvider { + +class AesCbcCipher { + JNIEnv* jenv_; + EVP_CIPHER_CTX* ctx_; + bool own_ctx_; + + static bool output_clobbers_input(uint8_t const* input, int input_len, uint8_t const* output, int unprocessed_input) + { + // Let's say we have 5 unprocessed bytes. The first 11 (16 - 5) bytes of input would produce 16 bytes of output. + // To avoid overwriting the input, output must be at least 11 bytes behind input. + int delta = (unprocessed_input == 0) || (unprocessed_input == AES_CBC_BLOCK_SIZE_IN_BYTES) + ? unprocessed_input + : (AES_CBC_BLOCK_SIZE_IN_BYTES - unprocessed_input); + if ((output + delta) <= input) { + return false; + } + + // If the output starts after the input ends, then we're not clobbering anything. + if ((input + input_len) <= output) { + return false; + } + + return true; + } + + static void check_unprocessed_input(int unprocessed_input) + { + if (unprocessed_input < 0 || unprocessed_input > 16) { + // This should not be reachable since we check this in Java. + throw java_ex(EX_ERROR, "unprocessed_input is not in [0, 16] range."); + } + } + +public: + AesCbcCipher(JNIEnv* jenv, jlongArray ctx_container, jlong ctx_ptr, bool save_ctx) + : jenv_(jenv) + , ctx_(reinterpret_cast(ctx_ptr)) + , own_ctx_(!save_ctx) + { + if (ctx_ == nullptr) { + // There is no context, so we need to create one. + ctx_ = EVP_CIPHER_CTX_new(); + if (ctx_ == nullptr) { + throw_openssl(EX_RUNTIME_CRYPTO, "EVP_CIPHER_CTX_new failed."); + } + + if (!own_ctx_) { + // We need to return the context. + jlong tmpPtr = reinterpret_cast(ctx_); + jenv_->SetLongArrayRegion(ctx_container, 0, 1, &tmpPtr); + } + } + } + + ~AesCbcCipher() + { + if (own_ctx_) { + EVP_CIPHER_CTX_free(ctx_); + } + } + + void init(int op_mode, int padding, uint8_t const* key, int key_len, uint8_t const* iv) + { + EVP_CIPHER const* cipher; + switch (key_len) { + case KEY_LEN_AES128: + cipher = EVP_aes_128_cbc(); + break; + case KEY_LEN_AES192: + cipher = EVP_aes_192_cbc(); + break; + case KEY_LEN_AES256: + cipher = EVP_aes_256_cbc(); + break; + default: + // This should not happen since we check this in the Java layer. + throw java_ex(EX_ERROR, "THIS SHOULD NOT BE REACHABLE. Invalid AES key size."); + } + + if (EVP_CipherInit_ex(ctx_, cipher, nullptr, key, iv, op_mode) != 1) { + throw_openssl(EX_RUNTIME_CRYPTO, "EVP_CipherInit_ex failed."); + } + + // This method always returns 1 and succeeds. + if (EVP_CIPHER_CTX_set_padding(ctx_, padding) != 1) { + throw_openssl(EX_RUNTIME_CRYPTO, "EVP_CIPHER_CTX_set_padding failed."); + } + } + + int update(uint8_t const* input, int input_len, uint8_t* output, int unprocessed_input) + { + check_unprocessed_input(unprocessed_input); + int result = 0; + if (output_clobbers_input(input, input_len, output, unprocessed_input)) { + SimpleBuffer temp(input_len + unprocessed_input); + + if (EVP_CipherUpdate(ctx_, temp.get_buffer(), &result, input, input_len) != 1) { + throw_openssl(EX_RUNTIME_CRYPTO, "EVP_CipherUpdate failed."); + } + + std::memcpy(output, temp.get_buffer(), result); + } else { + if (EVP_CipherUpdate(ctx_, output, &result, input, input_len) != 1) { + throw_openssl(EX_RUNTIME_CRYPTO, "EVP_CipherUpdate failed."); + } + } + + return result; + } + + int do_final(uint8_t* output) + { + int result = 0; + if (EVP_CipherFinal_ex(ctx_, output, &result) != 1) { + if (ERR_get_error() == AWS_LC_BAD_PADDING_ERROR_CODE) { + throw java_ex(EX_BAD_PADDING, "Bad padding"); + } else { + throw_openssl(EX_RUNTIME_CRYPTO, "EVP_CipherFinal_ex failed."); + } + } + return result; + } +}; + +} + +using namespace AmazonCorrettoCryptoProvider; + +extern "C" JNIEXPORT jint JNICALL Java_com_amazon_corretto_crypto_provider_AesCbcSpi_nInitUpdateFinal(JNIEnv* env, + jclass, + jint opMod, + jint padding, + jbyteArray key, + jint keyLen, + jbyteArray iv, + jlongArray ctxContainer, + jlong ctxPtr, + jboolean saveCtx, + jobject inputDirect, + jbyteArray inputArray, + jint inputOffset, + jint inputLen, + jobject outputDirect, + jbyteArray outputArray, + jint outputOffset) +{ + try { + AesCbcCipher aes_cbc_cipher(env, ctxContainer, ctxPtr, saveCtx); + // init + { + JBinaryBlob j_key(env, nullptr, key); + JBinaryBlob j_iv(env, nullptr, iv); + aes_cbc_cipher.init(opMod, padding, j_key.get(), keyLen, j_iv.get()); + } + + int result = 0; + + // update + JBinaryBlob output(env, outputDirect, outputArray); + + { + JBinaryBlob input(env, inputDirect, inputArray); + result = aes_cbc_cipher.update(input.get() + inputOffset, inputLen, output.get() + outputOffset, 0); + } + + // final + result += aes_cbc_cipher.do_final(output.get() + outputOffset + result); + + return result; + } catch (java_ex& ex) { + ex.throw_to_java(env); + return -1; + } +} + +extern "C" JNIEXPORT jint JNICALL Java_com_amazon_corretto_crypto_provider_AesCbcSpi_nInitUpdate(JNIEnv* env, + jclass, + jint opMod, + jint padding, + jbyteArray key, + jint keyLen, + jbyteArray iv, + jlongArray ctxContainer, + jlong ctxPtr, + jobject inputDirect, + jbyteArray inputArray, + jint inputOffset, + jint inputLen, + jobject outputDirect, + jbyteArray outputArray, + jint outputOffset) +{ + try { + AesCbcCipher aes_cbc_cipher(env, ctxContainer, ctxPtr, true); + // init + { + JBinaryBlob j_key(env, nullptr, key); + JBinaryBlob j_iv(env, nullptr, iv); + aes_cbc_cipher.init(opMod, padding, j_key.get(), keyLen, j_iv.get()); + } + + // update + JBinaryBlob output(env, outputDirect, outputArray); + JBinaryBlob input(env, inputDirect, inputArray); + + return aes_cbc_cipher.update(input.get() + inputOffset, inputLen, output.get() + outputOffset, 0); + + } catch (java_ex& ex) { + ex.throw_to_java(env); + return -1; + } +} + +extern "C" JNIEXPORT jint JNICALL Java_com_amazon_corretto_crypto_provider_AesCbcSpi_nUpdate(JNIEnv* env, + jclass, + jlong ctxPtr, + jobject inputDirect, + jbyteArray inputArray, + jint inputOffset, + jint inputLen, + jint unprocessed_input, + jobject outputDirect, + jbyteArray outputArray, + jint outputOffset) +{ + try { + AesCbcCipher aes_cbc_cipher(env, nullptr, ctxPtr, true); + + // update + JBinaryBlob output(env, outputDirect, outputArray); + JBinaryBlob input(env, inputDirect, inputArray); + + return aes_cbc_cipher.update( + input.get() + inputOffset, inputLen, output.get() + outputOffset, unprocessed_input); + + } catch (java_ex& ex) { + ex.throw_to_java(env); + return -1; + } +} + +extern "C" JNIEXPORT jint JNICALL Java_com_amazon_corretto_crypto_provider_AesCbcSpi_nUpdateFinal(JNIEnv* env, + jclass, + jlongArray ctxContainer, + jlong ctxPtr, + jboolean saveCtx, + jobject inputDirect, + jbyteArray inputArray, + jint inputOffset, + jint inputLen, + jint unprocessedInput, + jobject outputDirect, + jbyteArray outputArray, + jint outputOffset) +{ + try { + AesCbcCipher aes_cbc_cipher(env, ctxContainer, ctxPtr, saveCtx); + + int result = 0; + + // update + JBinaryBlob output(env, outputDirect, outputArray); + + { + JBinaryBlob input(env, inputDirect, inputArray); + result = aes_cbc_cipher.update( + input.get() + inputOffset, inputLen, output.get() + outputOffset, unprocessedInput); + } + + // final + result += aes_cbc_cipher.do_final(output.get() + outputOffset + result); + + return result; + } catch (java_ex& ex) { + ex.throw_to_java(env); + return -1; + } +} diff --git a/csrc/aes_gcm.cpp b/csrc/aes_gcm.cpp index e2f68741..b2862a01 100644 --- a/csrc/aes_gcm.cpp +++ b/csrc/aes_gcm.cpp @@ -241,13 +241,6 @@ JNIEXPORT jlong JNICALL Java_com_amazon_corretto_crypto_provider_AesGcmSpi_encry } } -JNIEXPORT void JNICALL Java_com_amazon_corretto_crypto_provider_AesGcmSpi_releaseContext(JNIEnv*, jclass, jlong ctxPtr) -{ - EVP_CIPHER_CTX* ctx = (EVP_CIPHER_CTX*)ctxPtr; - - EVP_CIPHER_CTX_free(ctx); -} - JNIEXPORT jint JNICALL Java_com_amazon_corretto_crypto_provider_AesGcmSpi_encryptUpdate(JNIEnv* pEnv, jclass, jlong ctxPtr, diff --git a/csrc/buffer.cpp b/csrc/buffer.cpp index aedfeb00..c0ab6b46 100644 --- a/csrc/buffer.cpp +++ b/csrc/buffer.cpp @@ -1,6 +1,7 @@ // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 #include "buffer.h" +#include namespace AmazonCorrettoCryptoProvider { @@ -49,4 +50,53 @@ JByteArrayCritical::~JByteArrayCritical() { env_->ReleasePrimitiveArrayCritical( unsigned char* JByteArrayCritical::get() { return (unsigned char*)ptr_; } +SimpleBuffer::SimpleBuffer(int size) + : buffer_(nullptr) +{ + buffer_ = (uint8_t*)malloc(size); + if (buffer_ == nullptr) { + throw java_ex(EX_ERROR, "malloc failed."); + } } + +SimpleBuffer::~SimpleBuffer() { free(buffer_); } + +uint8_t* SimpleBuffer::get_buffer() { return buffer_; } + +JBinaryBlob::JBinaryBlob(JNIEnv* env, jobject directByteBuffer, jbyteArray array) + : env_(env) + , array_(array) +{ + if ((array_ != nullptr) && (directByteBuffer != nullptr)) { + // One should be null. In the Java layer, we ensure this. + throw java_ex(EX_ERROR, "THIS SHOULD NOT BE REACHABLE. BOTH directByteBuffer and array cannot be provided."); + } + if (array_ != nullptr) { + ptr_ = (uint8_t*)env->GetPrimitiveArrayCritical(array, nullptr); + if (ptr_ == nullptr) { + throw java_ex(EX_ERROR, "GetPrimitiveArrayCritical failed."); + } + return; + } + if (directByteBuffer != nullptr) { + ptr_ = (uint8_t*)env->GetDirectBufferAddress(directByteBuffer); + if (ptr_ == nullptr) { + throw java_ex(EX_ERROR, "GetDirectBufferAddress failed."); + } + return; + } + // In the Java layer, we must ensure that exactly one of them is not null. + throw java_ex(EX_ERROR, "THIS SHOULD NOT BE REACHABLE. directByteBuffer or array must be provided."); +} + +JBinaryBlob::~JBinaryBlob() +{ + if (array_ != nullptr) { + env_->ReleasePrimitiveArrayCritical(array_, ptr_, 0); + } + // For direct ByteBuffers, there is no cleaning up. +} + +uint8_t* JBinaryBlob::get() { return ptr_; } + +} // end of namespace AmazonCorrettoCryptoProvider diff --git a/csrc/buffer.h b/csrc/buffer.h index 9970edd2..79758e1f 100644 --- a/csrc/buffer.h +++ b/csrc/buffer.h @@ -603,5 +603,30 @@ class JByteArrayCritical { jbyteArray jarray_; }; +class SimpleBuffer { +public: + SimpleBuffer(int len); + ~SimpleBuffer(); + uint8_t* get_buffer(); + +private: + uint8_t* buffer_; +}; + +// This class can be used to represent either byte arrays or direct byte buffers. It's best to follow the guidelines +// outlined in {Get,Release}PrimitiveArrayCritical when using this class even if it's representing a direct ByteBuffer: +// https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/functions.html#GetPrimitiveArrayCritical_ReleasePrimitiveArrayCritical +class JBinaryBlob { +public: + JBinaryBlob(JNIEnv* env, jobject directByteBuffer, jbyteArray array); + ~JBinaryBlob(); + uint8_t* get(); + +private: + uint8_t* ptr_; + JNIEnv* env_; + jbyteArray array_; +}; + } #endif diff --git a/csrc/util.cpp b/csrc/util.cpp index 6d68df78..c4753a85 100644 --- a/csrc/util.cpp +++ b/csrc/util.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 #include "generated-headers.h" #include +#include #include #include #include @@ -40,4 +41,10 @@ std::string formatOpensslError(unsigned long errCode, const char* fallback) return std::string(fallback); } } + +JNIEXPORT void JNICALL Java_com_amazon_corretto_crypto_provider_Utils_releaseEvpCipherCtx(JNIEnv*, jclass, jlong ctxPtr) +{ + EVP_CIPHER_CTX_free(reinterpret_cast(ctxPtr)); +} + } // namespace diff --git a/src/com/amazon/corretto/crypto/provider/AesCbcSpi.java b/src/com/amazon/corretto/crypto/provider/AesCbcSpi.java new file mode 100644 index 00000000..49e1edf6 --- /dev/null +++ b/src/com/amazon/corretto/crypto/provider/AesCbcSpi.java @@ -0,0 +1,685 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package com.amazon.corretto.crypto.provider; + +import static com.amazon.corretto.crypto.provider.Utils.checkAesKey; + +import java.nio.ByteBuffer; +import java.security.AlgorithmParameters; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.InvalidParameterException; +import java.security.Key; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.spec.AlgorithmParameterSpec; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.InvalidParameterSpecException; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.CipherSpi; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.ShortBufferException; +import javax.crypto.spec.IvParameterSpec; + +class AesCbcSpi extends CipherSpi { + public static final Set AES_CBC_NO_PADDING_NAMES; + public static final Set AES_CBC_PKCS7_PADDING_NAMES; + + static { + AES_CBC_NO_PADDING_NAMES = new HashSet<>(); + AES_CBC_NO_PADDING_NAMES.add("AES/CBC/NoPadding".toLowerCase()); + AES_CBC_NO_PADDING_NAMES.add("AES_128/CBC/NoPadding".toLowerCase()); + AES_CBC_NO_PADDING_NAMES.add("AES_192/CBC/NoPadding".toLowerCase()); + AES_CBC_NO_PADDING_NAMES.add("AES_256/CBC/NoPadding".toLowerCase()); + + AES_CBC_PKCS7_PADDING_NAMES = new HashSet<>(); + AES_CBC_PKCS7_PADDING_NAMES.add("AES/CBC/PKCS7Padding".toLowerCase()); + AES_CBC_PKCS7_PADDING_NAMES.add("AES_128/CBC/PKCS7Padding".toLowerCase()); + AES_CBC_PKCS7_PADDING_NAMES.add("AES_192/CBC/PKCS7Padding".toLowerCase()); + AES_CBC_PKCS7_PADDING_NAMES.add("AES_256/CBC/PKCS7Padding".toLowerCase()); + // PKCS5Padding with AES/CBC should be treated as PKCS7Padding + AES_CBC_PKCS7_PADDING_NAMES.add("AES/CBC/PKCS5Padding".toLowerCase()); + AES_CBC_PKCS7_PADDING_NAMES.add("AES_128/CBC/PKCS5Padding".toLowerCase()); + AES_CBC_PKCS7_PADDING_NAMES.add("AES_192/CBC/PKCS5Padding".toLowerCase()); + AES_CBC_PKCS7_PADDING_NAMES.add("AES_256/CBC/PKCS5Padding".toLowerCase()); + } + + private static final byte[] EMPTY_ARRAY = new byte[0]; + private static final int NO_PADDING = 0; + private static final int PKCS7_PADDING = 1; + private static final int BLOCK_SIZE_IN_BYTES = 128 / 8; + private static final int MODE_NOT_SET = -1; + private static final int ENC_MODE = 1; + private static final int DEC_MODE = 0; + + private enum CipherState { + NEEDS_INITIALIZATION, + INITIALIZED, + UPDATED, + } + + // State + private CipherState cipherState; + private final int padding; + // CBC processes data one block at a time. There are two scenarios where not all the input passed + // to engineUpdate is processed: + // 1. Input length is not a multiple of the block size, + // 2. Padding is enabled and cipher is configured for decryption. + // This variable keeps track of the unprocessed bytes. + private int unprocessedInput; + private int opMode; + private byte[] key; + private byte[] iv; + // nativeCtx is used to avoid memory leaks in case of multi-step operations or when the + // EVP_CIPHER_CTX needs to be preserved. + private NativeEvpCipherCtx nativeCtx; + // Determines if the EVP_CIPHER_CTX used should be released after doFinal or not. This is + // controlled by a system property. + private final boolean saveContext; + + AesCbcSpi(final boolean paddingEnabled, final boolean saveContext) { + this.padding = paddingEnabled ? PKCS7_PADDING : NO_PADDING; + this.cipherState = CipherState.NEEDS_INITIALIZATION; + this.unprocessedInput = 0; + this.opMode = MODE_NOT_SET; + this.key = null; + this.iv = null; + this.nativeCtx = null; + this.saveContext = saveContext; + } + + @Override + protected void engineSetMode(final String mode) throws NoSuchAlgorithmException { + // no op. One only needs to provide an implementation if the same Spi class instance can be used + // for different modes. + } + + @Override + protected void engineSetPadding(final String padding) throws NoSuchPaddingException { + // no op. One only needs to provide an implementation if the same Spi class instance is used for + // different paddings. + } + + @Override + protected int engineGetBlockSize() { + return BLOCK_SIZE_IN_BYTES; + } + + @Override + protected int engineGetOutputSize(final int inputLen) { + // There is no need to check if the Cipher is initialized since + // javax.crypto.Cipher::getOutputSize checks that. + final long all = inputLen + unprocessedInput; + + final long rem = all % BLOCK_SIZE_IN_BYTES; + + // When there is no padding, the output size for enc/dec is at most all. + if (padding == NO_PADDING) { + return (int) (all); + } + + // If padding is enabled and encrypting, the largest output size is during doFinal + if (opMode == ENC_MODE) { + return (int) ((all + BLOCK_SIZE_IN_BYTES) - rem); + } + + // If padding is enabled and decrypting, the largest output size is during doFinal + return (int) all; + } + + @Override + protected byte[] engineGetIV() { + return iv == null ? null : iv.clone(); + } + + @Override + protected AlgorithmParameters engineGetParameters() { + try { + AlgorithmParameters parameters = AlgorithmParameters.getInstance("AES"); + byte[] ivForParams = iv; + if (ivForParams == null) { + // We aren't initialized, so we return default and random values + ivForParams = new byte[BLOCK_SIZE_IN_BYTES]; + new LibCryptoRng().nextBytes(ivForParams); + } + parameters.init(new IvParameterSpec(ivForParams)); + return parameters; + } catch (final InvalidParameterSpecException | NoSuchAlgorithmException e) { + throw new Error("Unexpected error", e); + } + } + + @Override + protected void engineInit(final int opmode, final Key key, final SecureRandom random) + throws InvalidKeyException { + if (opmode != Cipher.ENCRYPT_MODE && opmode != Cipher.WRAP_MODE) { + throw new InvalidKeyException("IV required for decrypt"); + } + + final byte[] iv = new byte[BLOCK_SIZE_IN_BYTES]; + random.nextBytes(iv); + + try { + engineInit(opmode, key, new IvParameterSpec(iv), null); + } catch (final InvalidAlgorithmParameterException e) { + throw new RuntimeCryptoException(e); + } + } + + @Override + protected void engineInit( + final int opmode, final Key key, final AlgorithmParameters params, final SecureRandom random) + throws InvalidKeyException, InvalidAlgorithmParameterException { + try { + engineInit(opmode, key, params.getParameterSpec(IvParameterSpec.class), null); + } catch (final InvalidParameterSpecException e) { + throw new InvalidAlgorithmParameterException(e); + } + } + + @Override + protected void engineInit( + final int opmode, + final Key key, + final AlgorithmParameterSpec params, + final SecureRandom random) + throws InvalidKeyException, InvalidAlgorithmParameterException { + final int opMode = checkOperation(opmode); + final byte[] iv = checkAesCbcIv(params); + final byte[] keyBytes = checkAesKey(key); + + // All checks passes, so we update the state: + this.cipherState = CipherState.INITIALIZED; + this.opMode = opMode; + this.iv = iv; + this.key = keyBytes; + this.unprocessedInput = 0; + } + + private static int checkOperation(final int opMode) throws InvalidParameterException { + return ((opMode == Cipher.ENCRYPT_MODE) || (opMode == Cipher.WRAP_MODE)) ? ENC_MODE : DEC_MODE; + } + + private static byte[] checkAesCbcIv(final AlgorithmParameterSpec params) + throws InvalidAlgorithmParameterException { + if (!(params instanceof IvParameterSpec)) { + throw new InvalidAlgorithmParameterException( + "I don't know how to handle a " + params.getClass()); + } + + final IvParameterSpec ivParameterSpec = (IvParameterSpec) params; + final byte[] iv = ivParameterSpec.getIV(); + if (iv.length != BLOCK_SIZE_IN_BYTES) { + throw new InvalidAlgorithmParameterException("Invalid IV for AES/CBC"); + } + + return iv; + } + + @Override + protected byte[] engineUpdate(final byte[] input, final int inputOffset, final int inputLen) { + Utils.checkArrayLimits(input, inputOffset, inputLen); + finalOrUpdateStateCheck(); + final byte[] result = new byte[getOutputSizeUpdate(inputLen)]; + update(null, input, inputOffset, inputLen, null, result, 0); + // For update, getOutputSizeUpdate returns the exact required size, therefore there is no need + // for trimming the result; + return result; + } + + @Override + protected int engineUpdate( + final byte[] input, + final int inputOffset, + final int inputLen, + final byte[] output, + final int outputOffset) + throws ShortBufferException { + Utils.checkArrayLimits(input, inputOffset, inputLen); + Utils.checkArrayLimits(output, outputOffset, output.length - outputOffset); + updateChecks(inputLen, output.length - outputOffset); + return update(null, input, inputOffset, inputLen, null, output, outputOffset); + } + + @Override + protected int engineUpdate(final ByteBuffer input, final ByteBuffer output) + throws ShortBufferException { + updateChecks(input.remaining(), output.remaining()); + + final ShimByteBuffer inputShimByteBuffer = new ShimByteBuffer(input, true); + final ShimByteBuffer outputShimByteBuffer = new ShimByteBuffer(output, false); + + final int result = + update( + inputShimByteBuffer.directByteBuffer, + inputShimByteBuffer.array, + inputShimByteBuffer.offset, + input.remaining(), + outputShimByteBuffer.directByteBuffer, + outputShimByteBuffer.array, + outputShimByteBuffer.offset); + + outputShimByteBuffer.writeBack(result); + + input.position(input.limit()); + output.position(output.position() + result); + + return result; + } + + private void finalOrUpdateStateCheck() { + if (cipherState == CipherState.NEEDS_INITIALIZATION) { + throw new IllegalStateException("Cipher needs initialization."); + } + } + + private void updateChecks(final int inputLen, final int outputLen) throws ShortBufferException { + finalOrUpdateStateCheck(); + if (outputLen < getOutputSizeUpdate(inputLen)) { + throw new ShortBufferException(); + } + } + + private int getOutputSizeUpdate(final int inputLen) { + final long all = ((long) inputLen) + ((long) unprocessedInput); + if (all == 0) { + return 0; + } + final long rem = all % BLOCK_SIZE_IN_BYTES; + if (padding == NO_PADDING || opMode == ENC_MODE || rem != 0) { + return (int) (all - rem); + } + // When all data (inputLen + unprocessedInput) is block-size aligned, padding is enabled, and we + // are decrypting, the cipher does not decrypt the last block until doFinal. + return (int) (all - BLOCK_SIZE_IN_BYTES); + } + + private int update( + final ByteBuffer inputDirect, + final byte[] inputArray, + final int inputOffset, + final int inputLen, + final ByteBuffer outputDirect, + final byte[] outputArray, + final int outputOffset) { + + // Unlike, doFinal (which needs to decide if a context should be released or not), update always + // has to save the context. + + final long[] ctxContainer = new long[] {0}; + try { + final int result; + if (cipherState == CipherState.INITIALIZED) { + if (nativeCtx != null) { + result = + nativeCtx.use( + ctxPtr -> + nInitUpdate( + opMode, + padding, + key, + key.length, + iv, + null, + ctxPtr, + inputDirect, + inputArray, + inputOffset, + inputLen, + outputDirect, + outputArray, + outputOffset)); + } else { + result = + nInitUpdate( + opMode, + padding, + key, + key.length, + iv, + ctxContainer, + 0, + inputDirect, + inputArray, + inputOffset, + inputLen, + outputDirect, + outputArray, + outputOffset); + nativeCtx = new NativeEvpCipherCtx(ctxContainer[0]); + } + cipherState = CipherState.UPDATED; + } else { + // Cipher is in UPDATED state: this is not the first time update is being invoked. + result = + nativeCtx.use( + ctxPtr -> + nUpdate( + ctxPtr, + inputDirect, + inputArray, + inputOffset, + inputLen, + unprocessedInput, + outputDirect, + outputArray, + outputOffset)); + // No need to update the cipherState since it's already in UPDATED state. + } + final long all = inputLen + unprocessedInput; + unprocessedInput = (int) (all - result); + return result; + } catch (final Exception e) { + cipherState = CipherState.NEEDS_INITIALIZATION; + saveNativeContextIfNeeded(ctxContainer[0]); + throw e; + } + } + + private static native int nInitUpdate( + int opMode, + int padding, + byte[] key, + int keyLen, + byte[] iv, + long[] ctxContainer, + long ctxPtr, + ByteBuffer inputDirect, + byte[] inputArray, + int inputOffset, + int inputLen, + ByteBuffer outputDirect, + byte[] outputArray, + int outputOffset); + + private static native int nUpdate( + long ctxPtr, + ByteBuffer inputDirect, + byte[] inputArray, + int inputOffset, + int inputLen, + int unprocessedInput, + ByteBuffer outputDirect, + byte[] outputArray, + int outputOffset); + + @Override + protected byte[] engineDoFinal(byte[] input, int inputOffset, int inputLen) + throws IllegalBlockSizeException, BadPaddingException { + Utils.checkArrayLimits(emptyIfNull(input), inputOffset, inputLen); + finalOrUpdateStateCheck(); + final byte[] result = new byte[getOutputSizeFinal(inputLen)]; + final int resultLen = doFinal(null, emptyIfNull(input), inputOffset, inputLen, null, result, 0); + return resultLen == result.length ? result : Arrays.copyOf(result, resultLen); + } + + @Override + protected int engineDoFinal( + byte[] input, int inputOffset, int inputLen, byte[] output, int outputOffset) + throws ShortBufferException, IllegalBlockSizeException, BadPaddingException { + Utils.checkArrayLimits(emptyIfNull(input), inputOffset, inputLen); + Utils.checkArrayLimits(output, outputOffset, output.length - outputOffset); + finalChecks(inputLen, output.length - outputOffset); + + return doFinal(null, emptyIfNull(input), inputOffset, inputLen, null, output, outputOffset); + } + + @Override + protected int engineDoFinal(ByteBuffer input, ByteBuffer output) + throws ShortBufferException, IllegalBlockSizeException, BadPaddingException { + finalChecks(input.remaining(), output.remaining()); + + final ShimByteBuffer inputShimByteBuffer = new ShimByteBuffer(input, true); + final ShimByteBuffer outputShimByteBuffer = new ShimByteBuffer(output, false); + + final int result = + doFinal( + inputShimByteBuffer.directByteBuffer, + inputShimByteBuffer.array, + inputShimByteBuffer.offset, + input.remaining(), + outputShimByteBuffer.directByteBuffer, + outputShimByteBuffer.array, + outputShimByteBuffer.offset); + + outputShimByteBuffer.writeBack(result); + + input.position(input.limit()); + output.position(output.position() + result); + + return result; + } + + private void finalChecks(final int inputLen, final int outputLen) + throws IllegalBlockSizeException, ShortBufferException { + finalOrUpdateStateCheck(); + if (outputLen < getOutputSizeFinal(inputLen)) { + throw new ShortBufferException(outputLen + "<" + getOutputSizeFinal(inputLen)); + } + } + + private int getOutputSizeFinal(final int inputLen) throws IllegalBlockSizeException { + final long all = ((long) inputLen) + ((long) unprocessedInput); + final long rem = all % BLOCK_SIZE_IN_BYTES; + // If there is no padding or if we are decrypting, all the data must be aligned with block size. + if ((opMode == DEC_MODE || padding == NO_PADDING) && rem != 0) { + throw new IllegalBlockSizeException(); + } + if (padding == NO_PADDING) { + return (int) all; + } + // When encrypting with padding enabled ... + if (opMode == ENC_MODE) { + return (int) ((all + BLOCK_SIZE_IN_BYTES) - rem); + } + // When decrypting with padding enabled, we don't know exactly how many bytes the input has + // without decrypting first. + return (int) all; + } + + private int doFinal( + final ByteBuffer inputDirect, + final byte[] inputArray, + final int inputOffset, + final int inputLen, + final ByteBuffer outputDirect, + final byte[] outputArray, + final int outputOffset) { + + // There are four possibilities: + // 1. Save context AND Cipher is in INITIALIZED state => nInitUpdateFinal(saveContext == true) + // 2. Save context AND Cipher is in UPDATED state => nUpdateFinal(saveContext == true) + // 3. Don't save context AND Cipher is in INITIALIZED state => nInitUpdateFinal(saveContext == + // false) + // 4. Don't save context AND Cipher is in UPDATED state => nUpdateFinal(saveContext == false) + + final long[] ctxContainer = new long[] {0}; + try { + final int result; + if (saveContext) { + if (cipherState == CipherState.INITIALIZED) { + if (nativeCtx != null) { + result = + nativeCtx.use( + ctxPtr -> + nInitUpdateFinal( + opMode, + padding, + key, + key.length, + iv, + null, + ctxPtr, + true, + inputDirect, + inputArray, + inputOffset, + inputLen, + outputDirect, + outputArray, + outputOffset)); + } else { + result = + nInitUpdateFinal( + opMode, + padding, + key, + key.length, + iv, + ctxContainer, + 0, + true, + inputDirect, + inputArray, + inputOffset, + inputLen, + outputDirect, + outputArray, + outputOffset); + nativeCtx = new NativeEvpCipherCtx(ctxContainer[0]); + } + } else { + // Cipher is in UPDATE state, which means update was called at least once, and it needs to + // save the context. No need to call registerMess since the first update has already done + // this. + result = + nativeCtx.use( + ctxPtr -> + nUpdateFinal( + null, + ctxPtr, + true, + inputDirect, + inputArray, + inputOffset, + inputLen, + unprocessedInput, + outputDirect, + outputArray, + outputOffset)); + } + } else { + // Don't need to save the context + if (cipherState == CipherState.INITIALIZED) { + result = + nInitUpdateFinal( + opMode, + padding, + key, + key.length, + iv, + null, + nativeCtx == null ? 0 : nativeCtx.take(), + false, + inputDirect, + inputArray, + inputOffset, + inputLen, + outputDirect, + outputArray, + outputOffset); + } else { + // Cipher is in UPDATE state and don't need to save the context + result = + nUpdateFinal( + null, + nativeCtx.take(), + false, + inputDirect, + inputArray, + inputOffset, + inputLen, + unprocessedInput, + outputDirect, + outputArray, + outputOffset); + } + nativeCtx = null; + } + + cipherState = CipherState.INITIALIZED; + unprocessedInput = 0; + + return result; + + } catch (final Exception e) { + cipherState = CipherState.NEEDS_INITIALIZATION; + if (saveContext) { + saveNativeContextIfNeeded(ctxContainer[0]); + } else { + nativeCtx = null; + } + throw e; + } + } + + private void saveNativeContextIfNeeded(final long ctxPtr) { + if (nativeCtx == null && ctxPtr != 0) { + nativeCtx = new NativeEvpCipherCtx(ctxPtr); + } + } + + private static native int nInitUpdateFinal( + int opMode, + int padding, + byte[] key, + int keyLen, + byte[] iv, + long[] ctxContainer, + long ctxPtr, + boolean saveCtx, + ByteBuffer inputDirect, + byte[] inputArray, + int inputOffset, + int inputLen, + ByteBuffer outputDirect, + byte[] outputArray, + int outputOffset); + + private static native int nUpdateFinal( + long[] ctxContainer, + long ctxPtr, + boolean saveCtx, + ByteBuffer inputDirect, + byte[] inputArray, + int inputOffset, + int inputLen, + int unprocessedInput, + ByteBuffer outputDirect, + byte[] outputArray, + int outputOffset); + + @Override + protected byte[] engineWrap(final Key key) throws IllegalBlockSizeException, InvalidKeyException { + try { + final byte[] encoded = Utils.encodeForWrapping(key); + return engineDoFinal(encoded, 0, encoded.length); + } catch (final BadPaddingException ex) { + // This is not reachable when encrypting. + throw new InvalidKeyException("Wrapping failed", ex); + } + } + + @Override + protected Key engineUnwrap( + final byte[] wrappedKey, final String wrappedKeyAlgorithm, final int wrappedKeyType) + throws InvalidKeyException, NoSuchAlgorithmException { + try { + final byte[] unwrappedKey = engineDoFinal(wrappedKey, 0, wrappedKey.length); + return Utils.buildUnwrappedKey(unwrappedKey, wrappedKeyAlgorithm, wrappedKeyType); + } catch (final BadPaddingException | IllegalBlockSizeException | InvalidKeySpecException ex) { + // BadPaddingException and IllegalBlockSizeException can happen for AES/CBC/PKCS7Padding, but + // the JCA spec only allows throwing InvalidKeyException for engineUnwrap. + throw new InvalidKeyException("Unwrapping failed", ex); + } + } + + private static byte[] emptyIfNull(final byte[] array) { + return array == null ? EMPTY_ARRAY : array; + } +} diff --git a/src/com/amazon/corretto/crypto/provider/AesGcmSpi.java b/src/com/amazon/corretto/crypto/provider/AesGcmSpi.java index e4ccd38d..ce1f2b86 100644 --- a/src/com/amazon/corretto/crypto/provider/AesGcmSpi.java +++ b/src/com/amazon/corretto/crypto/provider/AesGcmSpi.java @@ -3,6 +3,7 @@ package com.amazon.corretto.crypto.provider; import static com.amazon.corretto.crypto.provider.Utils.EMPTY_ARRAY; +import static com.amazon.corretto.crypto.provider.Utils.checkAesKey; import static com.amazon.corretto.crypto.provider.Utils.checkArrayLimits; import java.nio.ByteBuffer; @@ -22,7 +23,6 @@ import javax.crypto.CipherSpi; import javax.crypto.IllegalBlockSizeException; import javax.crypto.NoSuchPaddingException; -import javax.crypto.SecretKey; import javax.crypto.ShortBufferException; import javax.crypto.spec.GCMParameterSpec; import javax.crypto.spec.IvParameterSpec; @@ -171,13 +171,6 @@ private static native int encryptDoFinal( int outputOffset, int tagLen); - /** - * Aborts an encryption operation and releases native resources associated with it. - * - * @param ptr Native context pointer - */ - private static native void releaseContext(long ptr); - private static final int BLOCK_SIZE = 128 / 8; private final AmazonCorrettoCryptoProvider provider; @@ -394,28 +387,7 @@ private static byte[] checkKey(final Key key, final Key lastKey, final byte[] la if (key == lastKey) { return lastKeyBytes; } - if (!(key instanceof SecretKey)) { - throw new InvalidKeyException("Need a SecretKey"); - } - if (!"RAW".equalsIgnoreCase(key.getFormat())) { - throw new InvalidKeyException("Need a raw format key"); - } - if (!"AES".equalsIgnoreCase(key.getAlgorithm())) { - throw new InvalidKeyException("Expected an AES key"); - } - - final byte[] encodedKey = key.getEncoded(); - if (encodedKey == null) { - throw new InvalidKeyException("Key doesn't support encoding"); - } - - if (encodedKey.length != 128 / 8 - && encodedKey.length != 192 / 8 - && encodedKey.length != 256 / 8) { - throw new InvalidKeyException( - "Bad key length of " + (encodedKey.length * 8) + " bits; expected 128, 192, or 256 bits"); - } - return encodedKey; + return checkAesKey(key); } private static boolean checkKeyIvPair( @@ -642,7 +614,7 @@ private int engineEncryptFinal( tagLength, key, iv); - context = new NativeContext(ptrOut[0]); + context = new NativeEvpCipherCtx(ptrOut[0]); return outLen; } // We don't need to save the context. @@ -788,7 +760,7 @@ private int engineDecryptFinal( // To avoid this, we reuse the same empty array decryptAADBuf.isEmpty() ? EMPTY_ARRAY : decryptAADBuf.getDataBuffer(), decryptAADBuf.size()); - context = new NativeContext(ptrOut[0]); + context = new NativeEvpCipherCtx(ptrOut[0]); return outlen; } // We don't have a context, and we don't need to save it @@ -889,12 +861,6 @@ protected Key engineUnwrap( } } - private static final class NativeContext extends NativeResource { - private NativeContext(final long ptr) { - super(ptr, AesGcmSpi::releaseContext); - } - } - /** * An array view over a bytebuffer - either directly aliasing the underlying bytebuffer, or a copy * of the byte buffer's data. In the latter case, writeback() will copy the data back to the @@ -1035,7 +1001,7 @@ private void lazyInit() { if (context != null) { context.useVoid(ptr -> encryptInit(ptr, sameKey, key, iv)); } else { - context = new NativeContext(encryptInit(0, false, key, iv)); + context = new NativeEvpCipherCtx(encryptInit(0, false, key, iv)); } } diff --git a/src/com/amazon/corretto/crypto/provider/AmazonCorrettoCryptoProvider.java b/src/com/amazon/corretto/crypto/provider/AmazonCorrettoCryptoProvider.java index 45f3e4e8..afeda337 100644 --- a/src/com/amazon/corretto/crypto/provider/AmazonCorrettoCryptoProvider.java +++ b/src/com/amazon/corretto/crypto/provider/AmazonCorrettoCryptoProvider.java @@ -2,6 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 package com.amazon.corretto.crypto.provider; +import static com.amazon.corretto.crypto.provider.AesCbcSpi.AES_CBC_NO_PADDING_NAMES; +import static com.amazon.corretto.crypto.provider.AesCbcSpi.AES_CBC_PKCS7_PADDING_NAMES; import static com.amazon.corretto.crypto.provider.HkdfSecretKeyFactorySpi.HKDF_WITH_SHA1; import static com.amazon.corretto.crypto.provider.HkdfSecretKeyFactorySpi.HKDF_WITH_SHA256; import static com.amazon.corretto.crypto.provider.HkdfSecretKeyFactorySpi.HKDF_WITH_SHA384; @@ -91,6 +93,21 @@ private void buildServiceMap() { addService("Cipher", "AES/XTS/NoPadding", "AesXtsSpi", false); + addService("Cipher", "AES/CBC/NoPadding", "AesCbcSpi", false); + addService("Cipher", "AES_128/CBC/NoPadding", "AesCbcSpi", false); + addService("Cipher", "AES_192/CBC/NoPadding", "AesCbcSpi", false); + addService("Cipher", "AES_256/CBC/NoPadding", "AesCbcSpi", false); + + addService("Cipher", "AES/CBC/PKCS5Padding", "AesCbcSpi", false); + addService("Cipher", "AES_128/CBC/PKCS5Padding", "AesCbcSpi", false); + addService("Cipher", "AES_192/CBC/PKCS5Padding", "AesCbcSpi", false); + addService("Cipher", "AES_256/CBC/PKCS5Padding", "AesCbcSpi", false); + + addService("Cipher", "AES/CBC/PKCS7Padding", "AesCbcSpi", false); + addService("Cipher", "AES_128/CBC/PKCS7Padding", "AesCbcSpi", false); + addService("Cipher", "AES_192/CBC/PKCS7Padding", "AesCbcSpi", false); + addService("Cipher", "AES_256/CBC/PKCS7Padding", "AesCbcSpi", false); + addService("Cipher", "RSA/ECB/NoPadding", "RsaCipher$NoPadding"); addService("Cipher", "RSA/ECB/Pkcs1Padding", "RsaCipher$Pkcs1"); addService("Cipher", "RSA/ECB/OAEPPadding", "RsaCipher$OAEP"); @@ -309,6 +326,22 @@ public Object newInstance(final Object constructorParameter) throws NoSuchAlgori return new AesXtsSpi(); } + if ("Cipher".equalsIgnoreCase(type) + && AES_CBC_PKCS7_PADDING_NAMES.contains(algo.toLowerCase())) { + final boolean saveContext = + AmazonCorrettoCryptoProvider.this.nativeContextReleaseStrategy + == Utils.NativeContextReleaseStrategy.LAZY; + return new AesCbcSpi(true, saveContext); + } + + if ("Cipher".equalsIgnoreCase(type) + && AES_CBC_NO_PADDING_NAMES.contains(algo.toLowerCase())) { + final boolean saveContext = + AmazonCorrettoCryptoProvider.this.nativeContextReleaseStrategy + == Utils.NativeContextReleaseStrategy.LAZY; + return new AesCbcSpi(false, saveContext); + } + throw new NoSuchAlgorithmException(String.format("No service class for %s/%s", type, algo)); } diff --git a/src/com/amazon/corretto/crypto/provider/NativeEvpCipherCtx.java b/src/com/amazon/corretto/crypto/provider/NativeEvpCipherCtx.java new file mode 100644 index 00000000..0f6ec4bb --- /dev/null +++ b/src/com/amazon/corretto/crypto/provider/NativeEvpCipherCtx.java @@ -0,0 +1,9 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package com.amazon.corretto.crypto.provider; + +final class NativeEvpCipherCtx extends NativeResource { + NativeEvpCipherCtx(final long ptr) { + super(ptr, Utils::releaseEvpCipherCtx); + } +} diff --git a/src/com/amazon/corretto/crypto/provider/ShimByteBuffer.java b/src/com/amazon/corretto/crypto/provider/ShimByteBuffer.java new file mode 100644 index 00000000..546eea5d --- /dev/null +++ b/src/com/amazon/corretto/crypto/provider/ShimByteBuffer.java @@ -0,0 +1,51 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package com.amazon.corretto.crypto.provider; + +import java.nio.ByteBuffer; + +// TODO: merge this class and AesGcmSpi.ShimArray +class ShimByteBuffer { + public final ByteBuffer directByteBuffer; + public final byte[] array; + public final int offset; + + // backingByteBuffer is only used when the byteBuffer is not direct nor has a backing array. + private final ByteBuffer backingByteBuffer; + + ShimByteBuffer(final ByteBuffer byteBuffer, final boolean isInput) { + if (byteBuffer.hasArray()) { + backingByteBuffer = null; + + directByteBuffer = null; + array = byteBuffer.array(); + offset = byteBuffer.position(); + return; + } + + if (byteBuffer.isDirect()) { + backingByteBuffer = null; + + directByteBuffer = byteBuffer; + array = null; + offset = byteBuffer.position(); + return; + } + // The original ByteBuffer is not direct and its backing array is not accessible. An example is + // a read-only ByteBuffer wrapping a byte[]. + backingByteBuffer = byteBuffer.duplicate(); + + directByteBuffer = null; + array = new byte[byteBuffer.remaining()]; + if (isInput) { + backingByteBuffer.duplicate().get(array); + } + offset = 0; + } + + void writeBack(final int len) { + if (backingByteBuffer != null) { + backingByteBuffer.put(array, 0, len); + } + } +} diff --git a/src/com/amazon/corretto/crypto/provider/Utils.java b/src/com/amazon/corretto/crypto/provider/Utils.java index a730c591..337d7268 100644 --- a/src/com/amazon/corretto/crypto/provider/Utils.java +++ b/src/com/amazon/corretto/crypto/provider/Utils.java @@ -198,6 +198,10 @@ static byte[] encodeForWrapping(final AmazonCorrettoCryptoProvider provider, fin } } + static byte[] encodeForWrapping(final Key key) throws InvalidKeyException { + return encodeForWrapping(AmazonCorrettoCryptoProvider.INSTANCE, key); + } + static Key buildUnwrappedKey( final AmazonCorrettoCryptoProvider provider, final byte[] rawKey, @@ -216,6 +220,11 @@ static Key buildUnwrappedKey( } } + static Key buildUnwrappedKey(final byte[] rawKey, final String algorithm, final int keyType) + throws NoSuchAlgorithmException, InvalidKeySpecException { + return buildUnwrappedKey(AmazonCorrettoCryptoProvider.INSTANCE, rawKey, algorithm, keyType); + } + static SecretKey buildUnwrappedSecretKey(final byte[] rawKey, final String algorithm) { return new SecretKeySpec(rawKey, algorithm); } @@ -563,7 +572,13 @@ static void checkArrayLimits(final byte[] bytes, final int offset, final int len } if ((long) offset + (long) length > bytes.length) { - throw new ArrayIndexOutOfBoundsException("Requested range is outside of buffer limits"); + throw new ArrayIndexOutOfBoundsException( + "Requested range is outside of buffer limits" + + bytes.length + + ":" + + offset + + ":" + + length); } } @@ -602,4 +617,34 @@ private static NativeContextReleaseStrategy getNativeContextReleaseStrategyPrope static NativeContextReleaseStrategy getNativeContextReleaseStrategyProperty() { return getNativeContextReleaseStrategyProperty(PROPERTY_NATIVE_CONTEXT_RELEASE_STRATEGY); } + + static native void releaseEvpCipherCtx(long ctxPtr); + + public static byte[] checkAesKey(final Key key) throws InvalidKeyException { + if (key == null) { + throw new InvalidKeyException("Key can't be null"); + } + if (!(key instanceof SecretKey)) { + throw new InvalidKeyException("Need a SecretKey"); + } + if (!"RAW".equalsIgnoreCase(key.getFormat())) { + throw new InvalidKeyException("Need a raw format key"); + } + if (!"AES".equalsIgnoreCase(key.getAlgorithm())) { + throw new InvalidKeyException("Expected an AES key"); + } + + final byte[] encodedKey = key.getEncoded(); + if (encodedKey == null) { + throw new InvalidKeyException("Key doesn't support encoding"); + } + + if (encodedKey.length != 128 / 8 + && encodedKey.length != 192 / 8 + && encodedKey.length != 256 / 8) { + throw new InvalidKeyException( + "Bad key length of " + (encodedKey.length * 8) + " bits; expected 128, 192, or 256 bits"); + } + return encodedKey; + } } diff --git a/test-data/CBCGFSbox128.rsp.gz b/test-data/CBCGFSbox128.rsp.gz new file mode 100644 index 00000000..84fde322 Binary files /dev/null and b/test-data/CBCGFSbox128.rsp.gz differ diff --git a/test-data/CBCGFSbox192.rsp.gz b/test-data/CBCGFSbox192.rsp.gz new file mode 100644 index 00000000..35630992 Binary files /dev/null and b/test-data/CBCGFSbox192.rsp.gz differ diff --git a/test-data/CBCGFSbox256.rsp.gz b/test-data/CBCGFSbox256.rsp.gz new file mode 100644 index 00000000..8c41f017 Binary files /dev/null and b/test-data/CBCGFSbox256.rsp.gz differ diff --git a/test-data/CBCKeySbox128.rsp.gz b/test-data/CBCKeySbox128.rsp.gz new file mode 100644 index 00000000..90d5d557 Binary files /dev/null and b/test-data/CBCKeySbox128.rsp.gz differ diff --git a/test-data/CBCKeySbox192.rsp.gz b/test-data/CBCKeySbox192.rsp.gz new file mode 100644 index 00000000..1fc34f71 Binary files /dev/null and b/test-data/CBCKeySbox192.rsp.gz differ diff --git a/test-data/CBCKeySbox256.rsp.gz b/test-data/CBCKeySbox256.rsp.gz new file mode 100644 index 00000000..998baafc Binary files /dev/null and b/test-data/CBCKeySbox256.rsp.gz differ diff --git a/test-data/CBCMMT128.rsp.gz b/test-data/CBCMMT128.rsp.gz new file mode 100644 index 00000000..15b2c804 Binary files /dev/null and b/test-data/CBCMMT128.rsp.gz differ diff --git a/test-data/CBCMMT192.rsp.gz b/test-data/CBCMMT192.rsp.gz new file mode 100644 index 00000000..83572aa3 Binary files /dev/null and b/test-data/CBCMMT192.rsp.gz differ diff --git a/test-data/CBCMMT256.rsp.gz b/test-data/CBCMMT256.rsp.gz new file mode 100644 index 00000000..cfe29a96 Binary files /dev/null and b/test-data/CBCMMT256.rsp.gz differ diff --git a/test-data/CBCVarKey128.rsp.gz b/test-data/CBCVarKey128.rsp.gz new file mode 100644 index 00000000..a366f1e4 Binary files /dev/null and b/test-data/CBCVarKey128.rsp.gz differ diff --git a/test-data/CBCVarKey192.rsp.gz b/test-data/CBCVarKey192.rsp.gz new file mode 100644 index 00000000..6181131d Binary files /dev/null and b/test-data/CBCVarKey192.rsp.gz differ diff --git a/test-data/CBCVarKey256.rsp.gz b/test-data/CBCVarKey256.rsp.gz new file mode 100644 index 00000000..3b47635d Binary files /dev/null and b/test-data/CBCVarKey256.rsp.gz differ diff --git a/test-data/CBCVarTxt128.rsp.gz b/test-data/CBCVarTxt128.rsp.gz new file mode 100644 index 00000000..6a5b5928 Binary files /dev/null and b/test-data/CBCVarTxt128.rsp.gz differ diff --git a/test-data/CBCVarTxt192.rsp.gz b/test-data/CBCVarTxt192.rsp.gz new file mode 100644 index 00000000..c6b7782a Binary files /dev/null and b/test-data/CBCVarTxt192.rsp.gz differ diff --git a/test-data/CBCVarTxt256.rsp.gz b/test-data/CBCVarTxt256.rsp.gz new file mode 100644 index 00000000..d307a1e9 Binary files /dev/null and b/test-data/CBCVarTxt256.rsp.gz differ diff --git a/tst/com/amazon/corretto/crypto/provider/test/AesCbcNistTest.java b/tst/com/amazon/corretto/crypto/provider/test/AesCbcNistTest.java new file mode 100644 index 00000000..888e1984 --- /dev/null +++ b/tst/com/amazon/corretto/crypto/provider/test/AesCbcNistTest.java @@ -0,0 +1,77 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package com.amazon.corretto.crypto.provider.test; + +import static com.amazon.corretto.crypto.provider.test.TestUtil.getEntriesFromFile; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; + +import java.util.function.Function; +import java.util.stream.Stream; +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; +import org.junit.jupiter.api.parallel.ResourceAccessMode; +import org.junit.jupiter.api.parallel.ResourceLock; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +@ExtendWith(TestResultLogger.class) +@Execution(ExecutionMode.CONCURRENT) +@ResourceLock(value = TestUtil.RESOURCE_GLOBAL, mode = ResourceAccessMode.READ) +public class AesCbcNistTest { + + @ParameterizedTest(name = "{0}") + @MethodSource("allCbcKatTests") + public void cbcKatTests(final RspTestEntry entry) throws Exception { + singleTest(entry); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("allCbcMmtTests") + public void cbcMmtTests(final RspTestEntry entry) throws Exception { + singleTest(entry); + } + + // These tests are coming from the following URL: + // https://csrc.nist.gov/Projects/cryptographic-algorithm-validation-program/Block-Ciphers + private static Stream allCbcKatTests() throws Exception { + return Stream.of( + getEntriesFromFile("CBCGFSbox128.rsp.gz"), + getEntriesFromFile("CBCGFSbox192.rsp.gz"), + getEntriesFromFile("CBCGFSbox256.rsp.gz"), + getEntriesFromFile("CBCKeySbox128.rsp.gz"), + getEntriesFromFile("CBCKeySbox192.rsp.gz"), + getEntriesFromFile("CBCKeySbox256.rsp.gz"), + getEntriesFromFile("CBCVarKey128.rsp.gz"), + getEntriesFromFile("CBCVarKey192.rsp.gz"), + getEntriesFromFile("CBCVarKey256.rsp.gz"), + getEntriesFromFile("CBCVarTxt128.rsp.gz"), + getEntriesFromFile("CBCVarTxt192.rsp.gz"), + getEntriesFromFile("CBCVarTxt256.rsp.gz")) + .flatMap(Function.identity()); + } + + private static Stream allCbcMmtTests() throws Exception { + return Stream.of( + getEntriesFromFile("CBCMMT128.rsp.gz"), + getEntriesFromFile("CBCMMT192.rsp.gz"), + getEntriesFromFile("CBCMMT256.rsp.gz")) + .flatMap(Function.identity()); + } + + private static void singleTest(final RspTestEntry entry) throws Exception { + final SecretKey key = new SecretKeySpec(entry.getInstanceFromHex("KEY"), "AES"); + final IvParameterSpec iv = new IvParameterSpec(entry.getInstanceFromHex("IV")); + final byte[] plainText = entry.getInstanceFromHex("PLAINTEXT"); + final byte[] ciphertexts = entry.getInstanceFromHex("CIPHERTEXT"); + final Cipher cipher = AesCbcTest.accpAesCbcCipher(false); + cipher.init(Cipher.ENCRYPT_MODE, key, iv); + assertArrayEquals(ciphertexts, cipher.doFinal(plainText)); + cipher.init(Cipher.DECRYPT_MODE, key, iv); + assertArrayEquals(plainText, cipher.doFinal(ciphertexts)); + } +} diff --git a/tst/com/amazon/corretto/crypto/provider/test/AesCbcTest.java b/tst/com/amazon/corretto/crypto/provider/test/AesCbcTest.java new file mode 100644 index 00000000..97f17c74 --- /dev/null +++ b/tst/com/amazon/corretto/crypto/provider/test/AesCbcTest.java @@ -0,0 +1,888 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package com.amazon.corretto.crypto.provider.test; + +import static com.amazon.corretto.crypto.provider.test.TestUtil.ascendingPattern; +import static com.amazon.corretto.crypto.provider.test.TestUtil.byteBuffersAreEqual; +import static com.amazon.corretto.crypto.provider.test.TestUtil.fixPattern; +import static com.amazon.corretto.crypto.provider.test.TestUtil.genAesKey; +import static com.amazon.corretto.crypto.provider.test.TestUtil.genData; +import static com.amazon.corretto.crypto.provider.test.TestUtil.genIv; +import static com.amazon.corretto.crypto.provider.test.TestUtil.multiStepArray; +import static com.amazon.corretto.crypto.provider.test.TestUtil.multiStepArrayMultiAllocationExplicit; +import static com.amazon.corretto.crypto.provider.test.TestUtil.multiStepArrayMultiAllocationImplicit; +import static com.amazon.corretto.crypto.provider.test.TestUtil.multiStepByteBuffer; +import static com.amazon.corretto.crypto.provider.test.TestUtil.multiStepByteBufferMultiAllocation; +import static com.amazon.corretto.crypto.provider.test.TestUtil.randomPattern; +import static org.junit.Assert.assertFalse; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.ByteBuffer; +import java.security.AlgorithmParameters; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.InvalidParameterException; +import java.security.Key; +import java.security.SecureRandom; +import java.security.spec.AlgorithmParameterSpec; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.ShortBufferException; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import org.apache.commons.codec.binary.Hex; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; +import org.junit.jupiter.api.parallel.ResourceAccessMode; +import org.junit.jupiter.api.parallel.ResourceLock; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +@ExtendWith(TestResultLogger.class) +@Execution(ExecutionMode.SAME_THREAD) +@ResourceLock(value = TestUtil.RESOURCE_GLOBAL, mode = ResourceAccessMode.READ) +public class AesCbcTest { + static Cipher accpAesCbcCipher(final boolean paddingEnabled) { + try { + return paddingEnabled + ? Cipher.getInstance("AES/CBC/PKCS5Padding", TestUtil.NATIVE_PROVIDER) + : Cipher.getInstance("AES/CBC/NoPadding", TestUtil.NATIVE_PROVIDER); + } catch (final Exception e) { + throw new RuntimeException(e); + } + } + + static Cipher sunAesCbcCipher(final boolean paddingEnabled) { + try { + return paddingEnabled + ? Cipher.getInstance("AES/CBC/PKCS5Padding", "SunJCE") + : Cipher.getInstance("AES/CBC/NoPadding", "SunJCE"); + } catch (final Exception e) { + throw new RuntimeException(e); + } + } + + @ParameterizedTest + @MethodSource("arrayTestParams") + public void testOneShotArray( + final int keySize, final long seed, final boolean isPaddingEnabled, final int inputLen) + throws Exception { + final Cipher accpCipher = accpAesCbcCipher(isPaddingEnabled); + final Cipher sunCipher = sunAesCbcCipher(isPaddingEnabled); + final byte[] data = genData(seed, inputLen); + final SecretKeySpec aesKey = genAesKey(seed, keySize); + final IvParameterSpec iv = genIv(seed, 16); + + accpCipher.init(Cipher.ENCRYPT_MODE, aesKey, iv); + sunCipher.init(Cipher.ENCRYPT_MODE, aesKey, iv); + final byte[] cipherText = accpCipher.doFinal(data); + assertArrayEquals(sunCipher.doFinal(data), cipherText); + + accpCipher.init(Cipher.DECRYPT_MODE, aesKey, iv); + sunCipher.init(Cipher.DECRYPT_MODE, aesKey, iv); + final byte[] plainText = accpCipher.doFinal(cipherText); + assertArrayEquals(sunCipher.doFinal(cipherText), plainText); + assertArrayEquals(data, plainText); + } + + @ParameterizedTest + @MethodSource("arrayTestParams") + public void testMultiStepArray( + final int keySize, final long seed, final boolean isPaddingEnabled, final int inputLen) + throws Exception { + final Cipher accpCipher = accpAesCbcCipher(isPaddingEnabled); + + final byte[] data = genData(seed, inputLen); + final SecretKeySpec aesKey = genAesKey(seed, keySize); + final IvParameterSpec iv = genIv(seed, 16); + + final List> processingPatterns = + Stream.of(-1, 0, 16, 20, 32) + .map(c -> genPattern(seed, c, inputLen)) + .collect(Collectors.toList()); + + final Cipher sunCipher = sunAesCbcCipher(isPaddingEnabled); + sunCipher.init(Cipher.ENCRYPT_MODE, aesKey, iv); + final byte[] sunCipherText = sunCipher.doFinal(data); + + accpCipher.init(Cipher.ENCRYPT_MODE, aesKey, iv); + for (final List processingPattern : processingPatterns) { + assertArrayEquals(sunCipherText, multiStepArray(accpCipher, processingPattern, data)); + assertArrayEquals( + sunCipherText, + multiStepArrayMultiAllocationImplicit(accpCipher, processingPattern, data)); + assertArrayEquals( + sunCipherText, + multiStepArrayMultiAllocationExplicit(accpCipher, processingPattern, data)); + } + + accpCipher.init(Cipher.DECRYPT_MODE, aesKey, iv); + for (final List processingPattern : processingPatterns) { + assertArrayEquals(data, multiStepArray(accpCipher, processingPattern, sunCipherText)); + assertArrayEquals( + data, + multiStepArrayMultiAllocationImplicit(accpCipher, processingPattern, sunCipherText)); + assertArrayEquals( + data, + multiStepArrayMultiAllocationExplicit(accpCipher, processingPattern, sunCipherText)); + } + } + + private static List genPattern(final long seed, final int choice, final int inputLen) { + if (choice < 0) { + return randomPattern(inputLen, seed); + } + if (choice == 0) { + return ascendingPattern(inputLen); + } + return fixPattern(inputLen, choice); + } + + private static Stream arrayTestParams() { + final List result = new ArrayList<>(); + for (final int keySize : new int[] {128, 192, 256}) { + for (final boolean isPaddingEnabled : new boolean[] {false, true}) { + for (int i = 0; i != 32; i++) { + if (!isPaddingEnabled && (i % 16 != 0)) continue; + result.add(Arguments.of(keySize, (long) i, isPaddingEnabled, i)); + } + } + } + return result.stream(); + } + + @ParameterizedTest + @MethodSource("byteBufferTestParams") + public void testOneShotByteBuffer( + final int keySize, + final long seed, + final boolean isPaddingEnabled, + final int inputLen, + final boolean inputReadOnly, + final boolean inputDirect, + final boolean outputDirect) + throws Exception { + + final Cipher accpCipher = accpAesCbcCipher(isPaddingEnabled); + final Cipher sunCipher = sunAesCbcCipher(isPaddingEnabled); + final ByteBuffer input = genData(seed, inputLen, inputDirect); + final SecretKeySpec aesKey = genAesKey(seed, keySize); + final IvParameterSpec iv = genIv(seed, 16); + + accpCipher.init(Cipher.ENCRYPT_MODE, aesKey, iv); + final ByteBuffer accpCipherText = + genData(seed, accpCipher.getOutputSize(input.remaining()), outputDirect); + sunCipher.init(Cipher.ENCRYPT_MODE, aesKey, iv); + final ByteBuffer sunCipherText = + genData(seed, sunCipher.getOutputSize(input.remaining()), false); + final int accpOutputLimit = accpCipherText.limit(); + final int accpOutputPosition = accpCipherText.position(); + final ByteBuffer accpInput = + inputReadOnly ? input.duplicate().asReadOnlyBuffer() : input.duplicate(); + final int accpCipherLen = accpCipher.doFinal(accpInput, accpCipherText); + // all the input must have been processed. + assertEquals(0, accpInput.remaining()); + assertEquals(accpInput.limit(), accpInput.position()); + // limit for input should not change + assertEquals(input.limit(), accpInput.limit()); + // limit should not change for output + assertEquals(accpOutputLimit, accpCipherText.limit()); + // position of the output should advance by the length of the cipher + assertEquals(accpOutputPosition + accpCipherLen, accpCipherText.position()); + + final ByteBuffer sunInput = input.duplicate(); + final int sunCipherLen = sunCipher.doFinal(sunInput, sunCipherText); + assertEquals(sunCipherLen, accpCipherLen); + + sunCipherText.flip(); + accpCipherText.flip(); + assertTrue(byteBuffersAreEqual(sunCipherText, accpCipherText)); + + accpCipher.init(Cipher.DECRYPT_MODE, aesKey, iv); + final ByteBuffer accpPlainText = + ByteBuffer.allocate(accpCipher.getOutputSize(accpCipherText.remaining())); + assertEquals(inputLen, accpCipher.doFinal(accpCipherText, accpPlainText)); + accpPlainText.flip(); + assertTrue(byteBuffersAreEqual(input, accpPlainText)); + + sunCipher.init(Cipher.DECRYPT_MODE, aesKey, iv); + final ByteBuffer sunPlainText = + ByteBuffer.allocate(sunCipher.getOutputSize(sunCipherText.remaining())); + sunCipher.doFinal(sunCipherText, sunPlainText); + sunPlainText.flip(); + assertTrue(byteBuffersAreEqual(sunPlainText, accpPlainText)); + } + + @ParameterizedTest + @MethodSource("byteBufferTestParams") + public void testMultiStepByteBuffer( + final int keySize, + final long seed, + final boolean isPaddingEnabled, + final int inputLen, + final boolean inputReadOnly, + final boolean inputDirect, + final boolean outputDirect) + throws Exception { + + final Cipher accpCipher = accpAesCbcCipher(isPaddingEnabled); + final ByteBuffer input = genData(seed, inputLen, inputDirect); + final SecretKeySpec aesKey = genAesKey(seed, keySize); + final IvParameterSpec iv = genIv(seed, 16); + + final Cipher sunCipher = sunAesCbcCipher(isPaddingEnabled); + sunCipher.init(Cipher.ENCRYPT_MODE, aesKey, iv); + final ByteBuffer sunCipherText = + genData(seed, sunCipher.getOutputSize(input.remaining()), false); + final ByteBuffer sunInput = input.duplicate(); + sunCipher.doFinal(sunInput, sunCipherText); + sunCipherText.flip(); + + final List> processingPatterns = + Stream.of(-1, 0, 16, 20, 32) + .map(c -> genPattern(seed, c, inputLen)) + .collect(Collectors.toList()); + + accpCipher.init(Cipher.ENCRYPT_MODE, aesKey, iv); + for (final List processingPattern : processingPatterns) { + final ByteBuffer accpInput = + inputReadOnly ? input.duplicate().asReadOnlyBuffer() : input.duplicate(); + final ByteBuffer accpCipherText = + multiStepByteBuffer(accpCipher, processingPattern, accpInput, outputDirect); + // all the input must have been processed. + assertEquals(0, accpInput.remaining()); + assertEquals(accpInput.limit(), accpInput.position()); + // limit for input should not change + assertEquals(input.limit(), accpInput.limit()); + + assertTrue(byteBuffersAreEqual(sunCipherText, accpCipherText)); + } + for (final List processingPattern : processingPatterns) { + final ByteBuffer accpInput = + inputReadOnly ? input.duplicate().asReadOnlyBuffer() : input.duplicate(); + final ByteBuffer accpCipherText = + multiStepByteBufferMultiAllocation( + accpCipher, processingPattern, accpInput, outputDirect); + // all the input must have been processed. + assertEquals(0, accpInput.remaining()); + assertEquals(accpInput.limit(), accpInput.position()); + // limit for input should not change + assertEquals(input.limit(), accpInput.limit()); + + assertTrue(byteBuffersAreEqual(sunCipherText, accpCipherText)); + } + + accpCipher.init(Cipher.DECRYPT_MODE, aesKey, iv); + for (final List processingPattern : processingPatterns) { + final ByteBuffer cipherText = sunCipherText.duplicate(); + final ByteBuffer accpPlainText = + multiStepByteBuffer(accpCipher, processingPattern, cipherText, outputDirect); + // all the input must have been processed. + assertEquals(0, cipherText.remaining()); + assertEquals(cipherText.limit(), cipherText.position()); + + assertTrue(byteBuffersAreEqual(input, accpPlainText)); + } + for (final List processingPattern : processingPatterns) { + final ByteBuffer cipherText = sunCipherText.duplicate(); + final ByteBuffer accpPlainText = + multiStepByteBufferMultiAllocation( + accpCipher, processingPattern, cipherText, outputDirect); + // all the input must have been processed. + assertEquals(0, cipherText.remaining()); + assertEquals(cipherText.limit(), cipherText.position()); + + assertTrue(byteBuffersAreEqual(input, accpPlainText)); + } + } + + private static Stream byteBufferTestParams() { + final List result = new ArrayList<>(); + for (final int keySize : new int[] {256}) { + for (int i = 0; i != 1024; i++) { + for (final boolean isPaddingEnabled : new boolean[] {true, false}) { + for (final boolean inputReadOnly : new boolean[] {true, false}) { + for (final boolean inputDirect : new boolean[] {true, false}) { + for (final boolean outputDirect : new boolean[] {true, false}) { + if (!isPaddingEnabled && (i % 16 != 0)) continue; + result.add( + Arguments.of( + keySize, + (long) i, + isPaddingEnabled, + i, + inputReadOnly, + inputDirect, + outputDirect)); + } + } + } + } + } + } + return result.stream(); + } + + @ParameterizedTest + @MethodSource("paddings") + public void usingSameKeyIvIsAllowed(final boolean isPaddingEnabled) throws Exception { + final SecretKeySpec key = genAesKey(564, 256); + final IvParameterSpec iv = genIv(644, 16); + final byte[] input1 = genData(0, 256); + final byte[] input2 = genData(1, 256); + + final Cipher accp = accpAesCbcCipher(isPaddingEnabled); + final Cipher sun = sunAesCbcCipher(isPaddingEnabled); + + accp.init(Cipher.ENCRYPT_MODE, key, iv); + sun.init(Cipher.ENCRYPT_MODE, key, iv); + assertArrayEquals(sun.doFinal(input1), accp.doFinal(input1)); + assertArrayEquals(sun.doFinal(input2), accp.doFinal(input2)); + + accp.init(Cipher.ENCRYPT_MODE, key, iv); + sun.init(Cipher.ENCRYPT_MODE, key, iv); + assertArrayEquals(sun.doFinal(input1), accp.doFinal(input1)); + accp.init(Cipher.ENCRYPT_MODE, key, iv); + sun.init(Cipher.ENCRYPT_MODE, key, iv); + assertArrayEquals(sun.doFinal(input2), accp.doFinal(input2)); + } + + @ParameterizedTest + @MethodSource("clobberingParams") + public void outputClobberingInput_expectSuccess( + final long seed, + final boolean isPaddingEnabled, + final int inputLen, + final int clobberingIndex) + throws Exception { + final SecretKeySpec key = genAesKey(seed, 256); + final IvParameterSpec iv = genIv(seed, 16); + + final Cipher accp = accpAesCbcCipher(isPaddingEnabled); + accp.init(Cipher.ENCRYPT_MODE, key, iv); + final Cipher sun = sunAesCbcCipher(isPaddingEnabled); + sun.init(Cipher.ENCRYPT_MODE, key, iv); + + final int cipherLen = accp.getOutputSize(inputLen); + final byte[] buffer = genData(seed, clobberingIndex + cipherLen + clobberingIndex); + + final byte[] prefix = Arrays.copyOf(buffer, clobberingIndex); + final byte[] postfix = Arrays.copyOfRange(buffer, clobberingIndex + cipherLen, buffer.length); + + final byte[] sunCipherText = sun.doFinal(buffer, 0, inputLen); + + assertEquals(sunCipherText.length, cipherLen); + // output clobbers input + assertEquals(sunCipherText.length, accp.doFinal(buffer, 0, inputLen, buffer, clobberingIndex)); + // prefix and postfix should be untouched + assertTrue(Arrays.equals(prefix, Arrays.copyOf(buffer, clobberingIndex))); + assertTrue( + Arrays.equals( + postfix, Arrays.copyOfRange(buffer, clobberingIndex + cipherLen, buffer.length))); + // cipherText matches what we get from sun + assertArrayEquals( + sunCipherText, Arrays.copyOfRange(buffer, clobberingIndex, clobberingIndex + cipherLen)); + } + + private static Stream clobberingParams() { + final List result = new ArrayList<>(); + for (int clobberingIndex = 1; clobberingIndex != 17; clobberingIndex++) { + for (int i = 0; i != 1024; i++) { + for (final boolean isPaddingEnabled : new boolean[] {true, false}) { + if (!isPaddingEnabled && (i % 16 != 0)) continue; + result.add(Arguments.of((long) i, isPaddingEnabled, i, clobberingIndex)); + } + } + } + return result.stream(); + } + + @Test + public void testOneShotPadding() throws Exception { + final SecretKeySpec key = + new SecretKeySpec( + TestUtil.decodeHex("000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F"), + "AES"); + final byte[] input = + TestUtil.decodeHex( + "000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F3031"); + final IvParameterSpec iv = + new IvParameterSpec(TestUtil.decodeHex("000102030405060708090A0B0C0D0E0F")); + final Cipher cbcPadding = accpAesCbcCipher(true); + cbcPadding.init(Cipher.ENCRYPT_MODE, key, iv); + // final byte[] cipherText = cbcPadding.doFinal(input); + final byte[] cipherText = + TestUtil.multiStepArrayMultiAllocationImplicit( + cbcPadding, Arrays.asList(0, 1, 1, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5), input); + assertEquals( + "F29000B62A499FD0A9F39A6ADD2E77809543B86FC046FA883A9446B82E47D12DA144FC255AAD45BF681D3A3773A325C275C285C2760F0ED66EB65CFBEED8781D", + Hex.encodeHexString(cipherText, false)); + cbcPadding.init(Cipher.DECRYPT_MODE, key, iv); + assertArrayEquals( + input, + TestUtil.multiStepArray( + cbcPadding, Arrays.asList(0, 1, 1, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5), cipherText)); + } + + @Test + public void testOneShotPaddingDirect() throws Exception { + final SecretKeySpec key = + new SecretKeySpec( + TestUtil.decodeHex("000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F"), + "AES"); + final byte[] input = + TestUtil.decodeHex( + "000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F3031"); + final ByteBuffer directInput = ByteBuffer.allocateDirect(input.length); + directInput.put(input).flip(); + final IvParameterSpec iv = + new IvParameterSpec(TestUtil.decodeHex("000102030405060708090A0B0C0D0E0F")); + final Cipher cbcPadding = accpAesCbcCipher(true); + cbcPadding.init(Cipher.ENCRYPT_MODE, key, iv); + // final ByteBuffer cipherText = ByteBuffer.allocate(input.length + 16); + // final int cipherLen = cbcPadding.doFinal(directInput, cipherText); + // cipherText.flip(); + // assertEquals(cipherLen, cipherText.remaining()); + final ByteBuffer cipherText = + multiStepByteBufferMultiAllocation( + cbcPadding, Arrays.asList(0, 5, 5, 5, 5, 20), directInput, false); + assertEquals( + "F29000B62A499FD0A9F39A6ADD2E77809543B86FC046FA883A9446B82E47D12DA144FC255AAD45BF681D3A3773A325C275C285C2760F0ED66EB65CFBEED8781D", + Hex.encodeHexString(Arrays.copyOf(cipherText.array(), cipherText.remaining()), false)); + } + + @Test + public void testOneShotPaddingReadOnly() throws Exception { + final SecretKeySpec key = + new SecretKeySpec( + TestUtil.decodeHex("000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F"), + "AES"); + final byte[] input = + TestUtil.decodeHex( + "000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F3031"); + final ByteBuffer inputBuffer = ByteBuffer.allocate(input.length); + inputBuffer.put(input).flip(); + final IvParameterSpec iv = + new IvParameterSpec(TestUtil.decodeHex("000102030405060708090A0B0C0D0E0F")); + final Cipher cbcPadding = accpAesCbcCipher(true); + cbcPadding.init(Cipher.ENCRYPT_MODE, key, iv); + // final ByteBuffer cipherText = ByteBuffer.allocate(input.length + 16); + // final int cipherLen = cbcPadding.doFinal(directInput, cipherText); + // cipherText.flip(); + // assertEquals(cipherLen, cipherText.remaining()); + final ByteBuffer cipherText = + TestUtil.multiStepByteBuffer( + cbcPadding, Arrays.asList(0, 5, 5, 5, 5, 20), inputBuffer, false); + assertEquals( + "F29000B62A499FD0A9F39A6ADD2E77809543B86FC046FA883A9446B82E47D12DA144FC255AAD45BF681D3A3773A325C275C285C2760F0ED66EB65CFBEED8781D", + Hex.encodeHexString(Arrays.copyOf(cipherText.array(), cipherText.remaining()), false)); + } + + @ParameterizedTest + @MethodSource("wrapUnwrapParams") + public void wrapUnwrapIsCompatibleWithSun( + final long seed, final boolean isPaddingEnabled, final int keySize, final int inputLen) + throws Exception { + final String alg = "SECRET_KEY"; + final SecretKeySpec wrappingKey = genAesKey(seed, keySize); + final IvParameterSpec iv = genIv(seed, 16); + + final Cipher accp = accpAesCbcCipher(isPaddingEnabled); + accp.init(Cipher.WRAP_MODE, wrappingKey, iv); + final Cipher sun = sunAesCbcCipher(isPaddingEnabled); + sun.init(Cipher.WRAP_MODE, wrappingKey, iv); + + final SecretKeySpec keyToBeWrapped = new SecretKeySpec(genData(seed, inputLen), alg); + + final byte[] sunWrappedKey = sun.wrap(keyToBeWrapped); + final byte[] accpWrappedKey = accp.wrap(keyToBeWrapped); + + assertArrayEquals(sunWrappedKey, accpWrappedKey); + + accp.init(Cipher.UNWRAP_MODE, wrappingKey, iv); + sun.init(Cipher.UNWRAP_MODE, wrappingKey, iv); + + final Key sunUnwrappedKey = sun.unwrap(sunWrappedKey, alg, Cipher.SECRET_KEY); + final Key accpUnwrappedKey = accp.unwrap(sunWrappedKey, alg, Cipher.SECRET_KEY); + + assertEquals(sunUnwrappedKey.getAlgorithm(), accpUnwrappedKey.getAlgorithm()); + assertEquals(sunUnwrappedKey.getFormat(), accpUnwrappedKey.getFormat()); + assertArrayEquals(keyToBeWrapped.getEncoded(), sunUnwrappedKey.getEncoded()); + assertArrayEquals(keyToBeWrapped.getEncoded(), accpUnwrappedKey.getEncoded()); + } + + private static Stream wrapUnwrapParams() { + final List result = new ArrayList<>(); + for (int keySize : new int[] {128, 192, 256}) { + for (final boolean isPaddingEnabled : new boolean[] {true, false}) { + for (int i = 16; i != 128; i++) { + if (!isPaddingEnabled && (i % 16 != 0)) continue; + result.add(Arguments.of((long) i, isPaddingEnabled, keySize, i)); + } + } + } + return result.stream(); + } + + @Test + public void whenBadPaddingWithUnwrap_expectException() throws Exception { + final SecretKeySpec wrappingKey = genAesKey(0, 128); + final IvParameterSpec iv = genIv(0, 16); + final Cipher accp = accpAesCbcCipher(true); + accp.init(Cipher.UNWRAP_MODE, wrappingKey, iv); + assertThrows( + InvalidKeyException.class, + () -> accp.unwrap(genData(0, 16), "SECRET_KEY", Cipher.SECRET_KEY)); + + accp.init(Cipher.UNWRAP_MODE, wrappingKey, iv); + assertThrows( + InvalidKeyException.class, + () -> accp.unwrap(genData(0, 17), "SECRET_KEY", Cipher.SECRET_KEY)); + } + + @ParameterizedTest + @MethodSource("paddings") + public void whenWrappedKeyIsNotAligned_expectException(final boolean isPaddingEnabled) + throws Exception { + final SecretKeySpec wrappingKey = genAesKey(0, 128); + final IvParameterSpec iv = genIv(0, 16); + final Cipher accp = accpAesCbcCipher(isPaddingEnabled); + accp.init(Cipher.UNWRAP_MODE, wrappingKey, iv); + assertThrows( + InvalidKeyException.class, + () -> accp.unwrap(genData(0, 17), "SECRET_KEY", Cipher.SECRET_KEY)); + } + + @ParameterizedTest + @MethodSource("paddings") + public void wrapUnwrapCanOnlyBeAfterInitialization(final boolean isPaddingEnabled) { + final String alg = "SECRET_KEY"; + final SecretKeySpec keyToBeWrapped = new SecretKeySpec(genData(0, 16), alg); + + final Cipher accp = accpAesCbcCipher(isPaddingEnabled); + + // Cipher must be initialized before wrap/unwrap + assertThrows(IllegalStateException.class, () -> accp.wrap(keyToBeWrapped)); + assertThrows( + IllegalStateException.class, + () -> accp.unwrap(keyToBeWrapped.getEncoded(), alg, Cipher.SECRET_KEY)); + } + + @ParameterizedTest + @MethodSource("paddings") + public void wrapUnwrapCanOnlyBeUsedIfCipherIsInitializedForWrapUnwrap( + final boolean isPaddingEnabled) throws Exception { + final SecretKeySpec wrappingKey = genAesKey(0, 128); + final IvParameterSpec iv = genIv(0, 16); + final String alg = "SECRET_KEY"; + final SecretKeySpec keyToBeWrapped = new SecretKeySpec(genData(0, 16), alg); + + final Cipher accp = accpAesCbcCipher(isPaddingEnabled); + + accp.init(Cipher.ENCRYPT_MODE, wrappingKey, iv); + assertThrows(IllegalStateException.class, () -> accp.wrap(keyToBeWrapped)); + + accp.init(Cipher.DECRYPT_MODE, wrappingKey, iv); + assertThrows( + IllegalStateException.class, + () -> accp.unwrap(keyToBeWrapped.getEncoded(), alg, Cipher.SECRET_KEY)); + + accp.init(Cipher.WRAP_MODE, wrappingKey, iv); + assertThrows(IllegalStateException.class, () -> accp.update(wrappingKey.getEncoded())); + + accp.init(Cipher.UNWRAP_MODE, wrappingKey, iv); + assertThrows(IllegalStateException.class, () -> accp.update(wrappingKey.getEncoded())); + + accp.init(Cipher.WRAP_MODE, wrappingKey, iv); + assertThrows(IllegalStateException.class, () -> accp.doFinal(wrappingKey.getEncoded())); + + accp.init(Cipher.UNWRAP_MODE, wrappingKey, iv); + assertThrows(IllegalStateException.class, () -> accp.doFinal(wrappingKey.getEncoded())); + } + + @Test + public void whenBadPaddingDuringDecryptOneShot_expectException() throws Exception { + final SecretKeySpec key = + new SecretKeySpec( + TestUtil.decodeHex("000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F"), + "AES"); + final byte[] input = + TestUtil.decodeHex( + "000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F3031"); + final IvParameterSpec iv = + new IvParameterSpec(TestUtil.decodeHex("000102030405060708090A0B0C0D0E0F")); + final Cipher cbcPadding = accpAesCbcCipher(true); + cbcPadding.init(Cipher.ENCRYPT_MODE, key, iv); + final byte[] cipherText = cbcPadding.doFinal(input); + assertEquals( + "F29000B62A499FD0A9F39A6ADD2E77809543B86FC046FA883A9446B82E47D12DA144FC255AAD45BF681D3A3773A325C275C285C2760F0ED66EB65CFBEED8781D", + Hex.encodeHexString(cipherText, false)); + cipherText[cipherText.length - 1] = (byte) (0xFF ^ cipherText[cipherText.length - 1]); + cbcPadding.init(Cipher.DECRYPT_MODE, key, iv); + assertThrows(BadPaddingException.class, () -> cbcPadding.doFinal(cipherText)); + // The cipher will need to be initialized again. + assertThrows(IllegalStateException.class, () -> cbcPadding.doFinal(cipherText)); + + final Cipher cipherFresh = accpAesCbcCipher(true); + cipherFresh.init(Cipher.DECRYPT_MODE, key, iv); + assertThrows(BadPaddingException.class, () -> cipherFresh.doFinal(cipherText)); + // The cipher will need to be initialized again. + assertThrows(IllegalStateException.class, () -> cipherFresh.doFinal(cipherText)); + } + + @Test + public void whenBadPaddingDuringDecryptMultiStep_expectException() throws Exception { + final SecretKeySpec key = + new SecretKeySpec( + TestUtil.decodeHex("000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F"), + "AES"); + final byte[] input = + TestUtil.decodeHex( + "000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F3031"); + final IvParameterSpec iv = + new IvParameterSpec(TestUtil.decodeHex("000102030405060708090A0B0C0D0E0F")); + final Cipher cbcPadding = accpAesCbcCipher(true); + cbcPadding.init(Cipher.ENCRYPT_MODE, key, iv); + final byte[] cipherText = cbcPadding.doFinal(input); + assertEquals( + "F29000B62A499FD0A9F39A6ADD2E77809543B86FC046FA883A9446B82E47D12DA144FC255AAD45BF681D3A3773A325C275C285C2760F0ED66EB65CFBEED8781D", + Hex.encodeHexString(cipherText, false)); + cipherText[cipherText.length - 1] = (byte) (0xFF ^ cipherText[cipherText.length - 1]); + cbcPadding.init(Cipher.DECRYPT_MODE, key, iv); + assertDoesNotThrow(() -> cbcPadding.update(cipherText, 0, 16)); + assertThrows( + BadPaddingException.class, + () -> cbcPadding.doFinal(cipherText, 16, cipherText.length - 16)); + // The cipher will need to be initialized again. + assertThrows(IllegalStateException.class, () -> cbcPadding.doFinal(cipherText)); + } + + private static Stream paddings() { + return Stream.of(Arguments.of(true), Arguments.of(false)); + } + + @ParameterizedTest + @MethodSource("paddings") + public void aesCbcBlockSizeIs16(final boolean isPaddingEnabled) { + assertEquals(16, accpAesCbcCipher(isPaddingEnabled).getBlockSize()); + } + + @ParameterizedTest + @MethodSource("paddings") + public void whenGetOutputSizeWithUninitializedCipher_expectException( + final boolean isPaddingEnabled) { + // The exceptions are not thrown by ACCP. + assertThrows( + IllegalStateException.class, () -> accpAesCbcCipher(isPaddingEnabled).getOutputSize(10)); + } + + @ParameterizedTest + @MethodSource("paddings") + public void whenGetIVOnUninitializedCipher_expectNull(final boolean isPaddingEnabled) { + assertNull(accpAesCbcCipher(isPaddingEnabled).getIV()); + } + + @ParameterizedTest + @MethodSource("paddings") + public void whenGetParametersOnInitializedCipher_expectSameIv(final boolean isPaddingEnabled) + throws Exception { + final IvParameterSpec iv = genIv(1, 16); + final SecretKeySpec key = genAesKey(1, 128); + final Cipher cipher = accpAesCbcCipher(isPaddingEnabled); + cipher.init(Cipher.ENCRYPT_MODE, key, iv); + final IvParameterSpec ivFromParams = + cipher.getParameters().getParameterSpec(IvParameterSpec.class); + assertArrayEquals(cipher.getIV(), ivFromParams.getIV()); + } + + @ParameterizedTest + @MethodSource("paddings") + public void whenGetParametersOnUninitializedCipher_expectDifferentIvs( + final boolean isPaddingEnabled) throws Exception { + final Cipher cipher = accpAesCbcCipher(isPaddingEnabled); + final IvParameterSpec iv1 = cipher.getParameters().getParameterSpec(IvParameterSpec.class); + final IvParameterSpec iv2 = cipher.getParameters().getParameterSpec(IvParameterSpec.class); + assertFalse(Arrays.equals(iv1.getIV(), iv2.getIV())); + } + + @ParameterizedTest + @MethodSource("paddings") + public void whenInitializingWithNoIvDuringDecrypt_expectException( + final boolean isPaddingEnabled) { + final SecretKeySpec key = genAesKey(1, 128); + final SecureRandom random = new SecureRandom(); + final Cipher cipher = accpAesCbcCipher(isPaddingEnabled); + assertThrows(InvalidKeyException.class, () -> cipher.init(Cipher.DECRYPT_MODE, key, random)); + } + + @ParameterizedTest + @MethodSource("paddings") + public void whenInitializingWithNoIvForEncryptionOrWrap_expectSuccess( + final boolean isPaddingEnabled) { + final SecretKeySpec key = genAesKey(1, 128); + final SecureRandom random = new SecureRandom(); + for (final int mode : new int[] {Cipher.ENCRYPT_MODE, Cipher.WRAP_MODE}) { + final Cipher cipher = accpAesCbcCipher(isPaddingEnabled); + assertDoesNotThrow(() -> cipher.init(mode, key, random)); + } + } + + @ParameterizedTest + @MethodSource("paddings") + public void whenInitializedWithParam_expectSameIv(final boolean isPaddingEnabled) + throws Exception { + final SecretKeySpec key = genAesKey(1, 128); + final Cipher cipher = accpAesCbcCipher(isPaddingEnabled); + final AlgorithmParameters params = cipher.getParameters(); + cipher.init(Cipher.ENCRYPT_MODE, key, params, null); + assertArrayEquals(cipher.getIV(), params.getParameterSpec(IvParameterSpec.class).getIV()); + } + + @ParameterizedTest + @MethodSource("paddings") + public void whenInitializingWithInvalidMode_expectException(final boolean isPaddingEnabled) { + final SecretKeySpec key = genAesKey(1, 128); + final IvParameterSpec iv = genIv(1, 16); + final Cipher cipher = accpAesCbcCipher(isPaddingEnabled); + assertThrows(InvalidParameterException.class, () -> cipher.init(0, key, iv)); + assertThrows(InvalidParameterException.class, () -> cipher.init(5, key, iv)); + } + + @ParameterizedTest + @MethodSource("paddings") + public void whenInitializingWithBadIv_expectException(final boolean isPaddingEnabled) { + final SecretKeySpec key = genAesKey(1, 128); + + final Cipher cipher = accpAesCbcCipher(isPaddingEnabled); + // Only IvParameterSpec is supported. + assertThrows( + InvalidAlgorithmParameterException.class, + () -> cipher.init(Cipher.ENCRYPT_MODE, key, new AlgorithmParameterSpec() {})); + + // Iv cannot be null. We don't need to check for IvParameterSpec.getIV() == null + assertThrows(NullPointerException.class, () -> new IvParameterSpec(null)); + + // Iv's length must be 16 + assertThrows( + InvalidAlgorithmParameterException.class, + () -> cipher.init(Cipher.ENCRYPT_MODE, key, genIv(1, 10))); + } + + @ParameterizedTest + @MethodSource("paddings") + public void whenUpdateOrFinalWithShortBuffer_expectException(final boolean isPaddingEnabled) + throws Exception { + final IvParameterSpec iv = genIv(1, 16); + final SecretKeySpec key = genAesKey(1, 128); + final ByteBuffer input = ByteBuffer.allocate(16); + final ByteBuffer output = ByteBuffer.allocate(5); + final Cipher cipher = accpAesCbcCipher(isPaddingEnabled); + cipher.init(Cipher.ENCRYPT_MODE, key, iv); + assertThrows(ShortBufferException.class, () -> cipher.doFinal(input, output)); + assertThrows(ShortBufferException.class, () -> cipher.update(input, output)); + } + + @ParameterizedTest + @MethodSource("aesCbcKatFromOpenSSL") + public void aesCbcKnownAnswerTests( + final String keyStr, final String ivStr, final String plainText, final String cipherText) + throws Exception { + final SecretKeySpec key = new SecretKeySpec(TestUtil.decodeHex(keyStr), "AES"); + final IvParameterSpec iv = new IvParameterSpec(TestUtil.decodeHex(ivStr)); + final Cipher cipher = accpAesCbcCipher(false); + cipher.init(Cipher.ENCRYPT_MODE, key, iv); + assertEquals( + cipherText.toUpperCase(), + Hex.encodeHexString(cipher.doFinal(TestUtil.decodeHex(plainText)), false)); + + cipher.init(Cipher.DECRYPT_MODE, key, iv); + assertEquals( + plainText.toUpperCase(), + Hex.encodeHexString(cipher.doFinal(TestUtil.decodeHex(cipherText)), false)); + } + + private static Stream aesCbcKatFromOpenSSL() { + // These tests come from the following URL: + // https://github.com/majek/openssl/blob/master/crypto/evp/evptests.txt + // cipher:key:iv:plaintext:ciphertext + return Stream.of( + "AES-128-CBC:00000000000000000000000000000000:00000000000000000000000000000000:f34481ec3cc627bacd5dc3fb08f273e6:0336763e966d92595a567cc9ce537f5e", + "AES-128-CBC:2B7E151628AED2A6ABF7158809CF4F3C:000102030405060708090A0B0C0D0E0F:6BC1BEE22E409F96E93D7E117393172A:7649ABAC8119B246CEE98E9B12E9197D", + "AES-128-CBC:2B7E151628AED2A6ABF7158809CF4F3C:7649ABAC8119B246CEE98E9B12E9197D:AE2D8A571E03AC9C9EB76FAC45AF8E51:5086CB9B507219EE95DB113A917678B2", + "AES-128-CBC:2B7E151628AED2A6ABF7158809CF4F3C:5086CB9B507219EE95DB113A917678B2:30C81C46A35CE411E5FBC1191A0A52EF:73BED6B8E3C1743B7116E69E22229516", + "AES-128-CBC:2B7E151628AED2A6ABF7158809CF4F3C:73BED6B8E3C1743B7116E69E22229516:F69F2445DF4F9B17AD2B417BE66C3710:3FF1CAA1681FAC09120ECA307586E1A7", + "AES-192-CBC:8E73B0F7DA0E6452C810F32B809079E562F8EAD2522C6B7B:000102030405060708090A0B0C0D0E0F:6BC1BEE22E409F96E93D7E117393172A:4F021DB243BC633D7178183A9FA071E8", + "AES-192-CBC:8E73B0F7DA0E6452C810F32B809079E562F8EAD2522C6B7B:4F021DB243BC633D7178183A9FA071E8:AE2D8A571E03AC9C9EB76FAC45AF8E51:B4D9ADA9AD7DEDF4E5E738763F69145A", + "AES-192-CBC:8E73B0F7DA0E6452C810F32B809079E562F8EAD2522C6B7B:B4D9ADA9AD7DEDF4E5E738763F69145A:30C81C46A35CE411E5FBC1191A0A52EF:571B242012FB7AE07FA9BAAC3DF102E0", + "AES-192-CBC:8E73B0F7DA0E6452C810F32B809079E562F8EAD2522C6B7B:571B242012FB7AE07FA9BAAC3DF102E0:F69F2445DF4F9B17AD2B417BE66C3710:08B0E27988598881D920A9E64F5615CD", + "AES-256-CBC:603DEB1015CA71BE2B73AEF0857D77811F352C073B6108D72D9810A30914DFF4:000102030405060708090A0B0C0D0E0F:6BC1BEE22E409F96E93D7E117393172A:F58C4C04D6E5F1BA779EABFB5F7BFBD6", + "AES-256-CBC:603DEB1015CA71BE2B73AEF0857D77811F352C073B6108D72D9810A30914DFF4:F58C4C04D6E5F1BA779EABFB5F7BFBD6:AE2D8A571E03AC9C9EB76FAC45AF8E51:9CFC4E967EDB808D679F777BC6702C7D", + "AES-256-CBC:603DEB1015CA71BE2B73AEF0857D77811F352C073B6108D72D9810A30914DFF4:9CFC4E967EDB808D679F777BC6702C7D:30C81C46A35CE411E5FBC1191A0A52EF:39F23369A9D9BACFA530E26304231461", + "AES-256-CBC:603DEB1015CA71BE2B73AEF0857D77811F352C073B6108D72D9810A30914DFF4:39F23369A9D9BACFA530E26304231461:F69F2445DF4F9B17AD2B417BE66C3710:B2EB05E2C39BE9FCDA6C19078C6A9D1B") + .map( + testCase -> { + final String[] rawTestCase = testCase.split(":"); + return Arguments.of(rawTestCase[1], rawTestCase[2], rawTestCase[3], rawTestCase[4]); + }); + } + + @Test + public void whenNoPaddingOrDecryptingWithUnalignedInput_expectException() throws Exception { + final IvParameterSpec iv = genIv(1, 16); + final SecretKeySpec key = genAesKey(1, 128); + final byte[] input = new byte[23]; + + final Cipher cipherEnc = accpAesCbcCipher(false); + cipherEnc.init(Cipher.ENCRYPT_MODE, key, iv); + assertThrows(IllegalBlockSizeException.class, () -> cipherEnc.doFinal(input)); + + final Cipher cipherDec = accpAesCbcCipher(true); + cipherDec.init(Cipher.DECRYPT_MODE, key, iv); + assertThrows(IllegalBlockSizeException.class, () -> cipherDec.doFinal(input)); + + // When performing multistep operations, unalignment is only detected during final + assertDoesNotThrow(() -> cipherEnc.update(input, 0, 20)); + assertThrows(IllegalBlockSizeException.class, () -> cipherEnc.doFinal(input, 20, 3)); + + assertDoesNotThrow(() -> cipherDec.update(input, 0, 20)); + assertThrows(IllegalBlockSizeException.class, () -> cipherDec.doFinal(input, 20, 3)); + } + + @ParameterizedTest + @MethodSource("paddings") + public void whenInitUpdateInitDoFinal_expectSuccess(final boolean isPaddingEnabled) + throws Exception { + final IvParameterSpec iv = genIv(1, 16); + final SecretKeySpec key = genAesKey(1, 128); + final byte[] input = new byte[16]; + + final Cipher sunCipher = sunAesCbcCipher(isPaddingEnabled); + sunCipher.init(Cipher.ENCRYPT_MODE, key, iv); + final byte[] cipherText = sunCipher.doFinal(input); + + // This pattern of invocation is strange; however, nothing in the spec forbids it. + final Cipher cipher = accpAesCbcCipher(isPaddingEnabled); + cipher.init(Cipher.ENCRYPT_MODE, key, iv); + cipher.update(input); + // Let's forget what we were doing and do init followed by final. + cipher.init(Cipher.ENCRYPT_MODE, key, iv); + assertArrayEquals(cipherText, cipher.doFinal(input)); + + cipher.init(Cipher.DECRYPT_MODE, key, iv); + assertArrayEquals(input, cipher.update(cipherText)); + } +} diff --git a/tst/com/amazon/corretto/crypto/provider/test/AesKwpKatTest.java b/tst/com/amazon/corretto/crypto/provider/test/AesKwpKatTest.java index 369e44b3..6e985890 100644 --- a/tst/com/amazon/corretto/crypto/provider/test/AesKwpKatTest.java +++ b/tst/com/amazon/corretto/crypto/provider/test/AesKwpKatTest.java @@ -7,19 +7,11 @@ import static org.junit.Assert.assertTrue; import com.amazon.corretto.crypto.provider.RuntimeCryptoException; -import java.io.File; -import java.io.FileInputStream; import java.io.IOException; -import java.io.InputStream; import java.security.GeneralSecurityException; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; -import java.util.Iterator; -import java.util.Spliterator; -import java.util.Spliterators; import java.util.stream.Stream; -import java.util.stream.StreamSupport; -import java.util.zip.GZIPInputStream; import javax.crypto.BadPaddingException; import javax.crypto.Cipher; import javax.crypto.IllegalBlockSizeException; @@ -38,38 +30,28 @@ @ResourceLock(value = TestUtil.RESOURCE_GLOBAL, mode = ResourceAccessMode.READ) public final class AesKwpKatTest { - private static Stream getEntriesFromFile(final String fileName) throws IOException { - final File rsp = new File(System.getProperty("test.data.dir"), fileName); - final InputStream is = new GZIPInputStream(new FileInputStream(rsp)); - final Iterator iterator = - RspTestEntry.iterateOverResource(is, true); // Auto-closes stream - final Spliterator split = - Spliterators.spliteratorUnknownSize(iterator, Spliterator.ORDERED); - return StreamSupport.stream(split, false); - } - public static Stream encrypt128Params() throws IOException { - return getEntriesFromFile("kwpEncrypt128.rsp.gz"); + return TestUtil.getEntriesFromFile("kwpEncrypt128.rsp.gz"); } public static Stream decrypt128Params() throws IOException { - return getEntriesFromFile("kwpDecrypt128.rsp.gz"); + return TestUtil.getEntriesFromFile("kwpDecrypt128.rsp.gz"); } public static Stream encrypt192Params() throws IOException { - return getEntriesFromFile("kwpEncrypt192.rsp.gz"); + return TestUtil.getEntriesFromFile("kwpEncrypt192.rsp.gz"); } public static Stream decrypt192Params() throws IOException { - return getEntriesFromFile("kwpDecrypt192.rsp.gz"); + return TestUtil.getEntriesFromFile("kwpDecrypt192.rsp.gz"); } public static Stream encrypt256Params() throws IOException { - return getEntriesFromFile("kwpEncrypt256.rsp.gz"); + return TestUtil.getEntriesFromFile("kwpEncrypt256.rsp.gz"); } public static Stream decrypt256Params() throws IOException { - return getEntriesFromFile("kwpDecrypt256.rsp.gz"); + return TestUtil.getEntriesFromFile("kwpDecrypt256.rsp.gz"); } @ParameterizedTest(name = "{0}") diff --git a/tst/com/amazon/corretto/crypto/provider/test/TestUtil.java b/tst/com/amazon/corretto/crypto/provider/test/TestUtil.java index 0b7ade3d..f87225fe 100644 --- a/tst/com/amazon/corretto/crypto/provider/test/TestUtil.java +++ b/tst/com/amazon/corretto/crypto/provider/test/TestUtil.java @@ -14,12 +14,25 @@ import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.nio.ByteBuffer; import java.security.NoSuchAlgorithmException; import java.security.Provider; import java.security.SecureRandom; import java.security.Security; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.Random; +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; +import java.util.zip.GZIPInputStream; +import javax.crypto.Cipher; import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; import org.apache.commons.codec.binary.Hex; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.junit.jupiter.api.Assumptions; @@ -448,4 +461,261 @@ public static synchronized void restoreProviders(final Provider[] providers) { Security.addProvider(provider); } } + + public static Stream getEntriesFromFile(final String fileName) throws IOException { + final File rsp = new File(System.getProperty("test.data.dir"), fileName); + final InputStream is = new GZIPInputStream(new FileInputStream(rsp)); + final Iterator iterator = + RspTestEntry.iterateOverResource(is, true); // Auto-closes stream + final Spliterator split = + Spliterators.spliteratorUnknownSize(iterator, Spliterator.ORDERED); + return StreamSupport.stream(split, false); + } + + public static int roundUp(final int i, final int m) { + final int d = m - (i % m); + return d == m ? i : (i + d); + } + + public static byte[] genData(final long seed, final int len) { + final byte[] result = new byte[len]; + final Random rand = new Random(seed); + rand.nextBytes(result); + return result; + } + + public static ByteBuffer genData( + final long seed, final int offset, final int len, boolean isDirect) { + final byte[] data = genData(seed, offset + len); + final ByteBuffer result = + isDirect ? ByteBuffer.allocateDirect(data.length) : ByteBuffer.allocate(data.length); + return (ByteBuffer) result.put(data).position(offset); + } + + public static ByteBuffer genData(final long seed, final int len, boolean isDirect) { + return genData(seed, 0, len, isDirect); + } + + public static IvParameterSpec genIv(final long seed, final int len) { + return new IvParameterSpec(genData(seed, len)); + } + + public static SecretKeySpec genAesKey(final long seed, final int len) { + return new SecretKeySpec(genData(seed, len / 8), "AES"); + } + + public static boolean byteBuffersAreEqual(final ByteBuffer a, final ByteBuffer b) { + if (a.remaining() != b.remaining()) { + return false; + } + final int len = a.remaining(); + for (int i = 0; i != len; i++) { + if (a.get(a.position() + i) != b.get(b.position() + i)) { + return false; + } + } + return true; + } + + public static byte[] mergeByteArrays(final List chunks) { + final int len = chunks.stream().map(c -> c == null ? 0 : c.length).reduce(0, Integer::sum); + + final byte[] result = new byte[len]; + + int offset = 0; + for (final byte[] chunk : chunks) { + if (chunk == null) continue; + System.arraycopy(chunk, 0, result, offset, chunk.length); + offset += chunk.length; + } + + return result; + } + + public static ByteBuffer mergeByteBuffers(final List chunks) { + final int len = chunks.stream().map(c -> c == null ? 0 : c.remaining()).reduce(0, Integer::sum); + + final ByteBuffer result = ByteBuffer.allocate(len); + + for (final ByteBuffer chunk : chunks) { + if (chunk == null) continue; + result.put(chunk); + } + + result.flip(); + + return result; + } + + public static List fixPattern(final int inputLen, final int c) { + final List result = new ArrayList<>(); + int total = 0; + while (total < inputLen) { + result.add(c); + total += c; + } + return result; + } + + public static List ascendingPattern(final int inputLen) { + final List result = new ArrayList<>(); + int i = 0; + int total = 0; + while (total < inputLen) { + result.add(i); + i++; + total += i; + } + return result; + } + + public static List randomPattern(final int inputLen, final long seed) { + final List result = new ArrayList<>(); + final Random random = new Random(seed); + int total = 0; + while (total < inputLen) { + final int c = random.nextInt(inputLen + 1); + result.add(c); + total += c; + } + return result; + } + + public static byte[] multiStepArray( + final Cipher cipher, final List process, final byte[] input) throws Exception { + + final byte[] output = new byte[cipher.getOutputSize(input.length)]; + + int inputOffset = 0; + int outputOffset = 0; + + for (final Integer p : process) { + if (inputOffset == input.length) break; + final int toBeProcessed = (p + inputOffset) > input.length ? (input.length - inputOffset) : p; + outputOffset += cipher.update(input, inputOffset, toBeProcessed, output, outputOffset); + inputOffset += toBeProcessed; + } + + if (inputOffset == input.length) { + outputOffset += cipher.doFinal(output, outputOffset); + } else { + outputOffset += + cipher.doFinal(input, inputOffset, input.length - inputOffset, output, outputOffset); + } + + return Arrays.copyOf(output, outputOffset); + } + + public static byte[] multiStepArrayMultiAllocationImplicit( + final Cipher cipher, final List process, final byte[] input) throws Exception { + final List outputChunks = new ArrayList<>(); + + int inputOffset = 0; + + for (final Integer p : process) { + if (inputOffset == input.length) break; + final int toBeProcessed = (p + inputOffset) > input.length ? (input.length - inputOffset) : p; + final byte[] chunk = cipher.update(input, inputOffset, toBeProcessed); + // If input.length == 0, then javax.crypto.Cipher::update returns null. + if (chunk != null) { + outputChunks.add(chunk); + } + inputOffset += toBeProcessed; + } + + if (inputOffset == input.length) { + outputChunks.add(cipher.doFinal()); + } else { + outputChunks.add(cipher.doFinal(input, inputOffset, input.length - inputOffset)); + } + return mergeByteArrays(outputChunks); + } + + public static byte[] multiStepArrayMultiAllocationExplicit( + final Cipher cipher, final List process, final byte[] input) throws Exception { + final List outputChunks = new ArrayList<>(); + + int inputOffset = 0; + + for (final Integer p : process) { + if (inputOffset == input.length) break; + final int toBeProcessed = (p + inputOffset) > input.length ? (input.length - inputOffset) : p; + final byte[] temp = new byte[cipher.getOutputSize(toBeProcessed)]; + final byte[] chunk = + Arrays.copyOf(temp, cipher.update(input, inputOffset, toBeProcessed, temp, 0)); + outputChunks.add(chunk); + inputOffset += toBeProcessed; + } + + final byte[] temp = new byte[cipher.getOutputSize(input.length - inputOffset)]; + + if (inputOffset == input.length) { + outputChunks.add(Arrays.copyOf(temp, cipher.doFinal(temp, 0))); + } else { + outputChunks.add( + Arrays.copyOf( + temp, cipher.doFinal(input, inputOffset, input.length - inputOffset, temp, 0))); + } + return mergeByteArrays(outputChunks); + } + + public static ByteBuffer multiStepByteBuffer( + final Cipher cipher, + final List process, + final ByteBuffer input, + final boolean outputDirect) + throws Exception { + final int cipherSize = cipher.getOutputSize(input.remaining()); + + final ByteBuffer output = + outputDirect ? ByteBuffer.allocateDirect(cipherSize) : ByteBuffer.allocate(cipherSize); + + for (final Integer p : process) { + if (!input.hasRemaining()) break; + final int toBeProcessed = p > input.remaining() ? input.remaining() : p; + final ByteBuffer temp = input.duplicate(); + temp.limit(input.position() + toBeProcessed); + cipher.update(temp, output); + input.position(input.position() + toBeProcessed); + } + + cipher.doFinal(input, output); + + output.flip(); + + return output; + } + + public static ByteBuffer multiStepByteBufferMultiAllocation( + final Cipher cipher, + final List process, + final ByteBuffer input, + final boolean outputDirect) + throws Exception { + + final List outputChunks = new ArrayList<>(); + + for (final Integer p : process) { + if (!input.hasRemaining()) break; + final int toBeProcessed = p > input.remaining() ? input.remaining() : p; + final int cipherSize = cipher.getOutputSize(toBeProcessed); + final ByteBuffer output = + outputDirect ? ByteBuffer.allocateDirect(cipherSize) : ByteBuffer.allocate(cipherSize); + final ByteBuffer temp = input.duplicate(); + temp.limit(input.position() + toBeProcessed); + cipher.update(temp, output); + output.flip(); + outputChunks.add(output); + input.position(input.position() + toBeProcessed); + } + + final int cipherSize = cipher.getOutputSize(input.remaining()); + final ByteBuffer output = + outputDirect ? ByteBuffer.allocateDirect(cipherSize) : ByteBuffer.allocate(cipherSize); + cipher.doFinal(input, output); + output.flip(); + outputChunks.add(output); + + return mergeByteBuffers(outputChunks); + } }