diff --git a/CMakeLists.txt b/CMakeLists.txt index 670e836b..4f3279c9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -666,6 +666,24 @@ add_custom_target(check-junit-SecurityManager DEPENDS accp-jar tests-jar) +add_custom_target(check-junit-AesGcmLazy + 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 + + DEPENDS accp-jar tests-jar) + +add_custom_target(check-junit-AesGcmEager + COMMAND ${TEST_JAVA_EXECUTABLE} + -Dcom.amazon.corretto.crypto.provider.nativeContextReleaseStrategy=EAGER + ${TEST_RUNNER_ARGUMENTS} + --select-class=com.amazon.corretto.crypto.provider.test.AesTest + --select-class=com.amazon.corretto.crypto.provider.test.AesGcmKatTest + + DEPENDS accp-jar tests-jar) + add_custom_target(check-junit-extra-checks COMMAND ${TEST_JAVA_EXECUTABLE} -Dcom.amazon.corretto.crypto.provider.extrachecks=ALL @@ -758,7 +776,15 @@ add_custom_target(check-install-via-properties-with-debug DEPENDS accp-jar tests-jar) add_custom_target(check - DEPENDS check-recursive-init check-install-via-properties check-install-via-properties-with-debug check-junit check-junit-SecurityManager check-external-lib check-libaccp-rpath) + DEPENDS check-recursive-init + check-install-via-properties + check-install-via-properties-with-debug + check-junit + check-junit-SecurityManager + check-external-lib + check-libaccp-rpath + check-junit-AesGcmLazy + check-junit-AesGcmEager) if(ENABLE_NATIVE_TEST_HOOKS) add_custom_target(check-keyutils diff --git a/README.md b/README.md index 23bd514b..89681d26 100644 --- a/README.md +++ b/README.md @@ -272,7 +272,7 @@ AmazonCorrettoCryptoProvider.INSTANCE.assertHealthy(); ### Other system properties ACCP can be configured via several system properties. -None of these should be needed for standard deployments and we recommend not touching them. +None of these should be needed for standard deployments, and we recommend not touching them. They are of most use to developers needing to test ACCP or experiment with benchmarking. These are all read early in the load process and may be cached so any changes to them made from within Java may not be respected. Thus, these should all be set on the JVM command line using `-D`. @@ -314,7 +314,21 @@ Thus, these should all be set on the JVM command line using `-D`. This means that there is a slight "pause" before ACCP FIPS's SecureRandom can produce pseudo-random bytes in highly threaded environments. Because, in extreme cases this could present an availability risk, we do not register LibCryptoRng by default in configurations where this initialization cost is incurred (i.e. FIPS mode). Non-FIPS AWS-LC does not use CPU jitter for its DRBG seed's entropy, and therefore does not incur this initialization cost, therefore we register LibCryptoRng by default when not in FIPS mode. - +* `com.amazon.corretto.crypto.provider.nativeContextReleaseStrategy` + Takes in `HYBRID`, `LAZY`, or `EAGER` (defaults ot `HYBRID`). This property only affects + AES-GCM cipher for now. AES-GCM associates a native object of type `EVP_CIPHER_CTX` + to each `Cipher` object. This property allows users to control the strategy for releasing + the native object. + * `HYBRID` (default): the structure is released eagerly, unless the same AES key is used. This is the + default behavior, and it is consistent with prior releases of ACCP. + * `LAZY`: preserve the native object and do not release while the `Cipher` object is not garbage collected. + * `EAGER`: release the native object as soon as possible, regardless of using the same key or not. + Our recommendation is to set this property to `EAGER` if `Cipher` objects are discarded + after use and caching of `Cipher` objects is not needed. When reusing the same `Cipher` + object, it would be beneficial to set this system property to `LAZY` so that different + encryption/decryption operations would not require allocation and release of `EVP_CIPHER_CTX` + structure. A common use case would be having long-running threads that each would get its + own instance of `Cipher` class. # License This library is licensed under the Apache 2.0 license although portions of this diff --git a/benchmarks/README.md b/benchmarks/README.md index 224ba6dc..0f77a844 100644 --- a/benchmarks/README.md +++ b/benchmarks/README.md @@ -6,6 +6,8 @@ The benchmarks can use locally built ACCP or published ACCP. The `lib:jmh` Gradle task runs the benchmarks and generates reports in JSON and HTML. The reports are saved under `lib/build/results/jmh`. +* `-PincludeBenchmark="INCLUDE_BENCHMARK"` would only run the specified benchmark. + ### Benchmarking published ACCP to Maven ```bash diff --git a/benchmarks/lib/build.gradle.kts b/benchmarks/lib/build.gradle.kts index d54098b9..0f2b2970 100644 --- a/benchmarks/lib/build.gradle.kts +++ b/benchmarks/lib/build.gradle.kts @@ -1,6 +1,8 @@ val accpVersion: String? by project val accpLocalJar: String by project val fips: Boolean by project +val includeBenchmark: String by project +val nativeContextReleaseStrategy: String by project plugins { `java-library` @@ -46,7 +48,9 @@ java { } jmh { - // includes.add("AesXts") // can be used to run a subset of benchmarks + if (project.hasProperty("includeBenchmark")) { + includes.add(includeBenchmark) + } fork.set(1) benchmarkMode.add("thrpt") threads.set(1) @@ -59,6 +63,9 @@ jmh { duplicateClassesStrategy.set(DuplicatesStrategy.WARN) jvmArgs.add("-DversionStr=${accpVersion}") jvmArgs.add("-Dcom.amazon.corretto.crypto.provider.registerSecureRandom=true") + if (project.hasProperty("nativeContextReleaseStrategy")) { + jvmArgs.add("-Dcom.amazon.corretto.crypto.provider.nativeContextReleaseStrategy=${nativeContextReleaseStrategy}") + } } jmhReport { diff --git a/benchmarks/lib/src/jmh/java/com/amazon/corretto/crypto/provider/benchmarks/CipherReuse.java b/benchmarks/lib/src/jmh/java/com/amazon/corretto/crypto/provider/benchmarks/CipherReuse.java new file mode 100644 index 00000000..f01c5192 --- /dev/null +++ b/benchmarks/lib/src/jmh/java/com/amazon/corretto/crypto/provider/benchmarks/CipherReuse.java @@ -0,0 +1,75 @@ +package com.amazon.corretto.crypto.provider.benchmarks; + +import com.amazon.corretto.crypto.provider.AmazonCorrettoCryptoProvider; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.infra.Blackhole; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import java.util.Random; + +@State(Scope.Thread) +public class CipherReuse { + private final static String AES = "AES"; + private final static String AES_GCM = "AES/GCM/NoPadding"; + Cipher shared; + SecretKeySpec key; + Random random = new Random(); + byte[] iv = new byte[12]; + byte[] input = new byte[1000]; + byte[] keyb = new byte[32]; + byte[] aad = new byte[100]; + + @Param({"true", "false"}) + private boolean newKey; + + @Setup + public void setup() throws Exception { + shared = getAesGcmCipherFromAccp(); + random.nextBytes(keyb); + key = new SecretKeySpec(keyb, AES); + random.nextBytes(input); + random.nextBytes(aad); + } + + @Benchmark + public void newInstance(Blackhole blackhole) throws Exception { + blackhole.consume(encrypt(getAesGcmCipherFromAccp())); + } + + @Benchmark + public void reuse(Blackhole blackhole) throws Exception { + blackhole.consume(encrypt(shared)); + } + + byte[] encrypt(Cipher cipher) throws Exception { + iv[0]++; + final SecretKey sk; + if (newKey) { + keyb[0]++; + sk = new SecretKeySpec(keyb, AES); + } else { + sk = key; + } + cipher.init(Cipher.ENCRYPT_MODE, sk, new GCMParameterSpec(128, iv)); + cipher.updateAAD(aad); + final byte[] cipherText = cipher.doFinal(input); + cipher.init(Cipher.DECRYPT_MODE, sk, new GCMParameterSpec(128, iv)); + cipher.updateAAD(aad); + return cipher.doFinal(cipherText); + } + + private Cipher getAesGcmCipherFromAccp() { + try { + return Cipher.getInstance(AES_GCM, AmazonCorrettoCryptoProvider.INSTANCE); + } catch (final Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/csrc/aes_gcm.cpp b/csrc/aes_gcm.cpp index 131739f2..e2f68741 100644 --- a/csrc/aes_gcm.cpp +++ b/csrc/aes_gcm.cpp @@ -28,7 +28,7 @@ using namespace AmazonCorrettoCryptoProvider; -static void initContext(raii_env& env, raii_cipher_ctx& ctx, jint opMode, java_buffer key, java_buffer iv) +static void initContext(raii_env& env, raii_cipher_ctx& ctx, jint opMode, java_buffer& key, java_buffer& iv) { const EVP_CIPHER* cipher; @@ -66,6 +66,43 @@ static void initContext(raii_env& env, raii_cipher_ctx& ctx, jint opMode, java_b } } +static void initializeContext(raii_env& env, + jlong ctxPtr, + raii_cipher_ctx& ctx, + jboolean sameKey, + jbyteArray keyArray, + jbyteArray ivArray, + int enc) +{ + // There are three possible cases: + // 1) there is no context: in this case, we need to create a context and initialize both key and iv + // 2) there is a context, and the key is the same: in this case, we borrow the context and only initialize iv + // 3) there is a context, but the key is not the same: in this case, we borrow the context and intialize it with + // both key and iv + if (ctxPtr == 0) { + // Case 1 + ctx.init(); + EVP_CIPHER_CTX_init(ctx); + } else { + // Case 2 or 3 + ctx.borrow(reinterpret_cast(ctxPtr)); + } + + java_buffer iv = java_buffer::from_array(env, ivArray); + + if (ctxPtr != 0 && sameKey == JNI_TRUE) { + // Case 2 + jni_borrow ivBorrow(env, iv, "iv"); + if (unlikely(!EVP_CipherInit_ex(ctx, NULL, NULL, NULL, ivBorrow.data(), enc))) { + throw java_ex::from_openssl(EX_RUNTIME_CRYPTO, "Failed to set IV"); + } + } else { + // Case 1 or 3 + java_buffer key = java_buffer::from_array(env, keyArray); + initContext(env, ctx, enc, key, iv); + } +} + static int updateLoop(raii_env& env, java_buffer out, java_buffer in, EVP_CIPHER_CTX* ctx) { int total_output = 0; @@ -145,6 +182,7 @@ static int cryptFinish(raii_env& env, int opMode, java_buffer resultBuf, unsigne JNIEXPORT int JNICALL Java_com_amazon_corretto_crypto_provider_AesGcmSpi_oneShotEncrypt(JNIEnv* pEnv, jclass, jlong ctxPtr, + jboolean sameKey, jlongArray ctxOut, jbyteArray inputArray, jint inoffset, @@ -157,25 +195,12 @@ JNIEXPORT int JNICALL Java_com_amazon_corretto_crypto_provider_AesGcmSpi_oneShot { try { raii_env env(pEnv); + raii_cipher_ctx ctx; + + initializeContext(env, ctxPtr, ctx, sameKey, keyArray, ivArray, NATIVE_MODE_ENCRYPT); java_buffer input = java_buffer::from_array(env, inputArray, inoffset, inlen); java_buffer result = java_buffer::from_array(env, resultArray, resultOffset); - java_buffer iv = java_buffer::from_array(env, ivArray); - - raii_cipher_ctx ctx; - if (ctxPtr) { - ctx.borrow(reinterpret_cast(ctxPtr)); - - jni_borrow ivBorrow(env, iv, "iv"); - if (unlikely(!EVP_CipherInit_ex(ctx, NULL, NULL, NULL, ivBorrow.data(), NATIVE_MODE_ENCRYPT))) { - throw java_ex::from_openssl(EX_RUNTIME_CRYPTO, "Failed to set IV"); - } - } else { - ctx.init(); - EVP_CIPHER_CTX_init(ctx); - java_buffer key = java_buffer::from_array(env, keyArray); - initContext(env, ctx, NATIVE_MODE_ENCRYPT, key, iv); - } int outoffset = updateLoop(env, result, input, ctx); if (outoffset < 0) @@ -197,41 +222,16 @@ JNIEXPORT int JNICALL Java_com_amazon_corretto_crypto_provider_AesGcmSpi_oneShot } } -JNIEXPORT void JNICALL Java_com_amazon_corretto_crypto_provider_AesGcmSpi_encryptInit__J_3B( - JNIEnv* pEnv, jclass, jlong ctxPtr, jbyteArray ivArray) -{ - try { - raii_env env(pEnv); - - if (!ctxPtr) - throw java_ex(EX_NPE, "Null context"); - - EVP_CIPHER_CTX* ctx = reinterpret_cast(ctxPtr); - java_buffer iv = java_buffer::from_array(env, ivArray); - - jni_borrow ivBorrow(env, iv, "iv"); - if (unlikely(!EVP_CipherInit_ex(ctx, NULL, NULL, NULL, ivBorrow.data(), NATIVE_MODE_ENCRYPT))) { - throw java_ex::from_openssl(EX_RUNTIME_CRYPTO, "Failed to set IV"); - } - } catch (java_ex& ex) { - ex.throw_to_java(pEnv); - } -} - -JNIEXPORT jlong JNICALL Java_com_amazon_corretto_crypto_provider_AesGcmSpi_encryptInit___3B_3B( - JNIEnv* pEnv, jclass, jbyteArray keyArray, jbyteArray ivArray) +JNIEXPORT jlong JNICALL Java_com_amazon_corretto_crypto_provider_AesGcmSpi_encryptInit( + JNIEnv* pEnv, jclass, jlong ctxPtr, jboolean sameKey, jbyteArray keyArray, jbyteArray ivArray) { raii_cipher_ctx ctx; - ctx.init(); - EVP_CIPHER_CTX_init(ctx); try { raii_env env(pEnv); + raii_cipher_ctx ctx; - java_buffer key = java_buffer::from_array(env, keyArray); - java_buffer iv = java_buffer::from_array(env, ivArray); - - initContext(env, ctx, NATIVE_MODE_ENCRYPT, key, iv); + initializeContext(env, ctxPtr, ctx, sameKey, keyArray, ivArray, NATIVE_MODE_ENCRYPT); return (jlong)ctx.take(); } catch (java_ex& ex) { @@ -348,6 +348,7 @@ JNIEXPORT jint JNICALL Java_com_amazon_corretto_crypto_provider_AesGcmSpi_encryp JNIEXPORT jint JNICALL Java_com_amazon_corretto_crypto_provider_AesGcmSpi_oneShotDecrypt(JNIEnv* pEnv, jclass, jlong ctxPtr, + jboolean sameKey, jlongArray ctxOut, jbyteArray inputArray, jint inoffset, @@ -362,25 +363,12 @@ JNIEXPORT jint JNICALL Java_com_amazon_corretto_crypto_provider_AesGcmSpi_oneSho { try { raii_env env(pEnv); + raii_cipher_ctx ctx; + + initializeContext(env, ctxPtr, ctx, sameKey, keyArray, ivArray, NATIVE_MODE_DECRYPT); java_buffer input = java_buffer::from_array(env, inputArray, inoffset, inlen); java_buffer result = java_buffer::from_array(env, resultArray, resultOffset); - java_buffer iv = java_buffer::from_array(env, ivArray); - - raii_cipher_ctx ctx; - if (ctxPtr) { - ctx.borrow(reinterpret_cast(ctxPtr)); - - jni_borrow ivBorrow(env, iv, "iv"); - if (unlikely(!EVP_CipherInit_ex(ctx, NULL, NULL, NULL, ivBorrow.data(), NATIVE_MODE_DECRYPT))) { - throw java_ex::from_openssl(EX_RUNTIME_CRYPTO, "Failed to set IV"); - } - } else { - ctx.init(); - EVP_CIPHER_CTX_init(ctx); - java_buffer key = java_buffer::from_array(env, keyArray); - initContext(env, ctx, NATIVE_MODE_DECRYPT, key, iv); - } // Decrypt mode: Set the tag before we decrypt if (unlikely(tagLen > 16 || tagLen < 0)) { diff --git a/src/com/amazon/corretto/crypto/provider/AesGcmSpi.java b/src/com/amazon/corretto/crypto/provider/AesGcmSpi.java index 466fe0dd..448ef907 100644 --- a/src/com/amazon/corretto/crypto/provider/AesGcmSpi.java +++ b/src/com/amazon/corretto/crypto/provider/AesGcmSpi.java @@ -32,12 +32,6 @@ final class AesGcmSpi extends CipherSpi { Loader.load(); } - /** - * The number a times a key must be reused prior to keeping it in native memory rather than - * freeing it each time. - */ - private static final int KEY_REUSE_THRESHOLD = 1; - private static final int DEFAULT_TAG_LENGTH = 16 * 8; /* Some random notes: @@ -69,6 +63,7 @@ final class AesGcmSpi extends CipherSpi { */ private static native int oneShotEncrypt( long ctxPtr, + boolean sameKey, long[] ctxPtrOut, byte[] input, int inputOffset, @@ -100,6 +95,7 @@ private static native int oneShotEncrypt( */ private static native int oneShotDecrypt( long ctxPtr, + boolean sameKey, long[] ctxPtrOut, byte[] input, int inoffset, @@ -116,20 +112,14 @@ private static native int oneShotDecrypt( /** * Initializes state for a non-one-shot encryption operation. * + * @param ptr Context pointer to be used if not zero + * @param sameKey true iff the key for initialization has been used before * @param key Encryption key * @param iv Initialization vector * @return Native pointer to context data structure, which must be freed using releaseContext() or * encryptDoFinal() */ - private static native long encryptInit(byte[] key, byte[] iv); - - /** - * Reuses an existing EVP context and initializes it for encryption given the new IV. - * - * @param ptr Context pointer - * @param iv Initialization vector - */ - private static native void encryptInit(long ptr, byte[] iv); + private static native long encryptInit(long ptr, boolean sameKey, byte[] key, byte[] iv); /** * Processes some plaintext during a non-one-shot encryption operation. This is essentially a @@ -191,7 +181,13 @@ private static native int encryptDoFinal( private final AmazonCorrettoCryptoProvider provider; private NativeResource context = null; - private SecretKey jceKey; + // If an EVP context exists (context != null), then sameKey determines if the EVP context needs to + // be initialized with both key and iv or if initialization with iv is sufficient: + // (sameKey == true) implies only iv, and (sameKey == false) implies both key and iv. + private boolean sameKey = false; + // A reference to the last Key that was used. This reference is used to optimize initialization + // when the same Java key object is used to initialize a Cipher that was previously used. + private Key lastKey = null; private byte[] iv, key; /** GCM tag length in bytes. */ private int tagLength; @@ -199,7 +195,6 @@ private static native int encryptDoFinal( private int opMode = -1; private boolean hasConsumedData = false; private boolean needReset = false; - private int keyUsageCount = 0; private boolean contextInitialized = false; private final AccessibleByteArrayOutputStream decryptInputBuf = @@ -212,6 +207,21 @@ private static native int encryptDoFinal( this.provider = provider; } + private boolean saveNativeContext() { + switch (provider.getNativeContextReleaseStrategy()) { + case HYBRID: + // In HYBRID strategy, the preservation of context depends on if the same key is used or + // not. + return sameKey; + case LAZY: + return true; + case EAGER: + return false; + default: + throw new AssertionError("This should not be reachable."); + } + } + @Override protected void engineSetMode(final String s) throws NoSuchAlgorithmException { if (!"GCM".equalsIgnoreCase(s)) { @@ -306,103 +316,119 @@ protected void engineInit( final AlgorithmParameterSpec algorithmParameterSpec, final SecureRandom secureRandom) throws InvalidKeyException, InvalidAlgorithmParameterException { - if (key == null) { - throw new InvalidKeyException("Key can't be null"); - } - final GCMParameterSpec spec; - if (algorithmParameterSpec instanceof GCMParameterSpec) { - spec = (GCMParameterSpec) algorithmParameterSpec; - } else if (algorithmParameterSpec instanceof IvParameterSpec) { - spec = - new GCMParameterSpec( - DEFAULT_TAG_LENGTH, ((IvParameterSpec) algorithmParameterSpec).getIV()); - } else { - throw new InvalidAlgorithmParameterException( - "I don't know how to handle a " + algorithmParameterSpec.getClass()); - } + final int opMode = checkOperation(jceOpMode); - byte[] encodedKey = null; - if (jceKey != key) { - if (!(key instanceof SecretKey)) { - throw new InvalidKeyException("Need a SecretKey"); - } - String keyAlgorithm = key.getAlgorithm(); - if (!"RAW".equalsIgnoreCase(key.getFormat())) { - throw new InvalidKeyException("Need a raw format key"); - } - if (!keyAlgorithm.equalsIgnoreCase("AES")) { - throw new InvalidKeyException("Expected an AES key"); - } - encodedKey = key.getEncoded(); - if (encodedKey == null) { - throw new InvalidKeyException("Key doesn't support encoding"); - } + final GCMParameterSpec spec = checkSpecAndTag(algorithmParameterSpec); - if (!ConstantTime.equals(this.key, encodedKey)) { - 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"); - } + final byte[] newIv = checkIv(spec); - keyUsageCount = 0; - if (context != null) { - context.release(); - } + final byte[] newKey = checkKey(key, lastKey, this.key); - context = null; - } else { - encodedKey = null; - } - } + final boolean sameKey = checkKeyIvPair(opMode, this.key, newKey, this.iv, newIv); - final byte[] iv = spec.getIV(); + this.opMode = opMode; + this.sameKey = sameKey; + this.iv = newIv; + this.tagLength = spec.getTLen() / 8; + this.key = newKey; + this.lastKey = key; + this.needReset = false; - if ((spec.getTLen() % 8 != 0) || spec.getTLen() > 128 || spec.getTLen() < 96) { - throw new InvalidAlgorithmParameterException( - "Unsupported TLen value; must be one of {128, 120, 112, 104, 96}"); + stateReset(); + } + + private static int checkOperation(final int opMode) throws InvalidAlgorithmParameterException { + switch (opMode) { + case Cipher.ENCRYPT_MODE: + case Cipher.WRAP_MODE: + return NATIVE_MODE_ENCRYPT; + case Cipher.DECRYPT_MODE: + case Cipher.UNWRAP_MODE: + return NATIVE_MODE_DECRYPT; + default: + throw new InvalidAlgorithmParameterException("Unsupported cipher mode " + opMode); } + } - if (this.iv != null - && this.key != null - && (jceOpMode == Cipher.ENCRYPT_MODE || jceOpMode == Cipher.WRAP_MODE)) { - if (Arrays.equals(this.iv, iv) - && (encodedKey == null || ConstantTime.equals(this.key, encodedKey))) { + private static GCMParameterSpec checkSpecAndTag( + final AlgorithmParameterSpec algorithmParameterSpec) + throws InvalidAlgorithmParameterException { + if (algorithmParameterSpec instanceof GCMParameterSpec) { + final GCMParameterSpec spec = (GCMParameterSpec) algorithmParameterSpec; + if ((spec.getTLen() % 8 != 0) || spec.getTLen() > 128 || spec.getTLen() < 96) { throw new InvalidAlgorithmParameterException( - "Cannot reuse same iv and key for GCM encryption"); + "Unsupported TLen value; must be one of {128, 120, 112, 104, 96}"); } + return spec; + } + if (algorithmParameterSpec instanceof IvParameterSpec) { + return new GCMParameterSpec( + DEFAULT_TAG_LENGTH, ((IvParameterSpec) algorithmParameterSpec).getIV()); } + throw new InvalidAlgorithmParameterException( + "I don't know how to handle a " + algorithmParameterSpec.getClass()); + } + private static byte[] checkIv(final GCMParameterSpec spec) + throws InvalidAlgorithmParameterException { + final byte[] iv = spec.getIV(); if (iv == null || iv.length == 0) { throw new InvalidAlgorithmParameterException("IV must be at least one byte long"); } + return iv; + } - switch (jceOpMode) { - case Cipher.ENCRYPT_MODE: - case Cipher.WRAP_MODE: - this.opMode = NATIVE_MODE_ENCRYPT; - break; - case Cipher.DECRYPT_MODE: - case Cipher.UNWRAP_MODE: - this.opMode = NATIVE_MODE_DECRYPT; - break; - default: - throw new InvalidAlgorithmParameterException("Unsupported cipher mode " + jceOpMode); + private static byte[] checkKey(final Key key, final Key lastKey, final byte[] lastKeyBytes) + throws InvalidKeyException { + if (key == null) { + throw new InvalidKeyException("Key can't be null"); + } + 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"); } - this.iv = iv; - this.tagLength = spec.getTLen() / 8; - if (encodedKey != null) { - this.key = encodedKey; - this.jceKey = (SecretKey) key; + final byte[] encodedKey = key.getEncoded(); + if (encodedKey == null) { + throw new InvalidKeyException("Key doesn't support encoding"); } - this.needReset = false; - stateReset(); + 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; + } + + private static boolean checkKeyIvPair( + final int jceOpMode, + final byte[] lastKey, + final byte[] newKey, + final byte[] lastIv, + final byte[] newIv) + throws InvalidAlgorithmParameterException { + + final boolean sameKey = ConstantTime.equals(lastKey, newKey); + + if (sameKey + && (jceOpMode == Cipher.ENCRYPT_MODE || jceOpMode == Cipher.WRAP_MODE) + && Arrays.equals(newIv, lastIv)) { + throw new InvalidAlgorithmParameterException( + "Cannot reuse same iv and key for GCM encryption"); + } + + return sameKey; } @Override @@ -575,29 +601,29 @@ private int engineEncryptFinal( // init(). In this case // we make a single native call to perform the encryption operation in one go. - keyUsageCount++; if (context != null) { - // Our key, but not our IV has been initialized return context.use( - ptr -> { - return oneShotEncrypt( - ptr, - null, - finalInput, - inputOffset, - finalInputLength, - output, - finalOutputOffset, - tagLength, - key, - iv); - }); - } else { - // We don't have an existing context, however we might want to save one - final long[] ptrOut = keyUsageCount > KEY_REUSE_THRESHOLD ? new long[1] : null; + ptr -> + oneShotEncrypt( + ptr, + sameKey, + null, + finalInput, + inputOffset, + finalInputLength, + output, + finalOutputOffset, + tagLength, + key, + iv)); + } + // We don't have an existing context, however we might want to save one + if (saveNativeContext()) { + final long[] ptrOut = new long[1]; final int outLen = oneShotEncrypt( 0, + false, ptrOut, finalInput, inputOffset, @@ -607,48 +633,59 @@ private int engineEncryptFinal( tagLength, key, iv); - if (ptrOut != null) { - context = new NativeContext(ptrOut[0]); - } + context = new NativeContext(ptrOut[0]); return outLen; } + // We don't need to save the context. + return oneShotEncrypt( + 0, + false, + null, + finalInput, + inputOffset, + finalInputLength, + output, + finalOutputOffset, + tagLength, + key, + iv); + } + // Context is initialized, which means either updateAAD or update has been invoked after init + + // We need to make sure to add resultLength here; engineUpdate in encrypt mode produces + // incremental output (unlike in decrypt mode) and so we need to carry forward whatever + // amount of data it produced in our return value. + final int finalOutputLen; + + // Should we preserve the context for the next operation? + if (saveNativeContext()) { + finalOutputLen = + context.use( + ptr -> + encryptDoFinal( + ptr, + false, // releaseContext + finalInput, + inputOffset, + finalInputLength, + output, + finalOutputOffset, + tagLength)); } else { - // We need to make sure to add resultLength here; engineUpdate in encrypt mode produces - // incremental output (unlike in decrypt mode) and so we need to carry forward whatever - // amount of data it produced in our return value. - - keyUsageCount++; - - final int finalOutputLen; - - if (keyUsageCount > KEY_REUSE_THRESHOLD) { - finalOutputLen = - context.use( - ptr -> - encryptDoFinal( - ptr, - false, // releaseContext - finalInput, - inputOffset, - finalInputLength, - output, - finalOutputOffset, - tagLength)); - } else { - finalOutputLen = - encryptDoFinal( - context.take(), - true, // releaseContext - input, - inputOffset, - finalInputLength, - output, - finalOutputOffset, - tagLength); - context = null; - } - return resultLength + finalOutputLen; + finalOutputLen = + encryptDoFinal( + context.take(), + true, // releaseContext + input, + inputOffset, + finalInputLength, + output, + finalOutputOffset, + tagLength); + context = null; } + + return resultLength + finalOutputLen; } finally { stateReset(); } @@ -694,38 +731,37 @@ private int engineDecryptFinal( throw new AEADBadTagException("Input too short - need tag"); } - keyUsageCount++; - final int outLen; if (context != null) { // We already have a context, so let's reuse it. - outLen = - context.use( - ptr -> { - return oneShotDecrypt( - ptr, - null, - workingInputArray, - workingInputOffset, - workingInputLength, - output, - outputOffset, - tagLength, - key, - iv, - - // The cost of calling decryptAADBuf.getDataBuffer() when its buffer is empty - // is significant for 16-byte decrypt operations (approximately a 7% - // performance hit). To avoid this, we reuse the same empty array instead in - // this common-case path. - decryptAADBuf.isEmpty() ? EMPTY_ARRAY : decryptAADBuf.getDataBuffer(), - decryptAADBuf.size()); - }); - } else { - // We don't have an existing context, however we might want to save one - final long[] ptrOut = keyUsageCount > KEY_REUSE_THRESHOLD ? new long[1] : null; - outLen = + return context.use( + ptr -> + oneShotDecrypt( + ptr, + sameKey, + null, + workingInputArray, + workingInputOffset, + workingInputLength, + output, + outputOffset, + tagLength, + key, + iv, + // The cost of calling decryptAADBuf.getDataBuffer() when its buffer is empty + // is significant for 16-byte decrypt operations (approximately a 7% + // performance hit). To avoid this, we reuse the same empty array instead in + // this common-case path. + decryptAADBuf.size() != 0 ? decryptAADBuf.getDataBuffer() : EMPTY_ARRAY, + decryptAADBuf.size())); + } + + // We don't have an existing context, however we might want to save one + if (saveNativeContext()) { + final long[] ptrOut = new long[1]; + final int outlen = oneShotDecrypt( 0, + false, ptrOut, workingInputArray, workingInputOffset, @@ -741,14 +777,29 @@ private int engineDecryptFinal( // To avoid this, we reuse the same empty array decryptAADBuf.size() != 0 ? decryptAADBuf.getDataBuffer() : EMPTY_ARRAY, decryptAADBuf.size()); - - if (ptrOut != null) { - context = new NativeContext(ptrOut[0]); - } + context = new NativeContext(ptrOut[0]); + return outlen; } - // Decryption completed successfully. - return outLen; - } catch (AEADBadTagException e) { + // We don't have a context, and we don't need to save it + return oneShotDecrypt( + 0, + false, + null, + workingInputArray, + workingInputOffset, + workingInputLength, + output, + outputOffset, + tagLength, + key, + iv, + + // The cost of calling decryptAADBuf.getDataBuffer() when its buffer is empty is + // significant for 16-byte decrypt operations (approximately a 7% performance hit). + // To avoid this, we reuse the same empty array + decryptAADBuf.size() != 0 ? decryptAADBuf.getDataBuffer() : EMPTY_ARRAY, + decryptAADBuf.size()); + } catch (final AEADBadTagException e) { final int maxFillSize = output.length - outputOffset; Arrays.fill( output, outputOffset, Math.min(maxFillSize, engineGetOutputSize(inputLen)), (byte) 0); @@ -971,11 +1022,9 @@ private void lazyInit() { checkNeedReset(); if (context != null) { - context.useVoid(ptr -> encryptInit(ptr, iv)); + context.useVoid(ptr -> encryptInit(ptr, sameKey, key, iv)); } else { - long ptr = encryptInit(key, iv); - - context = new NativeContext(ptr); + context = new NativeContext(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 5c4ea990..57a3a212 100644 --- a/src/com/amazon/corretto/crypto/provider/AmazonCorrettoCryptoProvider.java +++ b/src/com/amazon/corretto/crypto/provider/AmazonCorrettoCryptoProvider.java @@ -38,6 +38,7 @@ public final class AmazonCorrettoCryptoProvider extends java.security.Provider { private static final String PROPERTY_CACHE_SELF_TEST_RESULTS = "cacheselftestresults"; private static final String PROPERTY_REGISTER_EC_PARAMS = "registerEcParams"; private static final String PROPERTY_REGISTER_SECURE_RANDOM = "registerSecureRandom"; + private static final String PROPERTY_FREE_NATIVE_CONTEXT_EAGERLY = "nativeContextReleaseStrategy"; private static final long serialVersionUID = 1L; public static final AmazonCorrettoCryptoProvider INSTANCE; @@ -48,6 +49,7 @@ public final class AmazonCorrettoCryptoProvider extends java.security.Provider { private final boolean relyOnCachedSelfTestResults; private final boolean shouldRegisterEcParams; private final boolean shouldRegisterSecureRandom; + private final Utils.NativeContextReleaseStrategy nativeContextReleaseStrategy; private transient SelfTestSuite selfTestSuite = new SelfTestSuite(); @@ -391,6 +393,9 @@ public AmazonCorrettoCryptoProvider() { this.shouldRegisterSecureRandom = Utils.getBooleanProperty(PROPERTY_REGISTER_SECURE_RANDOM, !isFips()); + this.nativeContextReleaseStrategy = + Utils.getNativeContextReleaseStrategyProperty(PROPERTY_FREE_NATIVE_CONTEXT_EAGERLY); + Utils.optionsFromProperty(ExtraCheck.class, extraChecks, "extrachecks"); if (!Loader.IS_AVAILABLE) { @@ -407,6 +412,10 @@ public AmazonCorrettoCryptoProvider() { initializeSelfTests(); } + Utils.NativeContextReleaseStrategy getNativeContextReleaseStrategy() { + return nativeContextReleaseStrategy; + } + private synchronized void initializeSelfTests() { if (selfTestSuite == null) { selfTestSuite = new SelfTestSuite(); diff --git a/src/com/amazon/corretto/crypto/provider/Utils.java b/src/com/amazon/corretto/crypto/provider/Utils.java index dae2dad2..6e29d3f0 100644 --- a/src/com/amazon/corretto/crypto/provider/Utils.java +++ b/src/com/amazon/corretto/crypto/provider/Utils.java @@ -572,4 +572,27 @@ static T requireNonNull(final T obj, final String message) { static String requireNonNullString(final String s, final String message) { return requireNonNull(s, message); } + + enum NativeContextReleaseStrategy { + HYBRID, + LAZY, + EAGER + } + + static NativeContextReleaseStrategy getNativeContextReleaseStrategyProperty( + final String propertyName) { + final String propertyStr = Loader.getProperty(propertyName, "HYBRID").toUpperCase(); + if (propertyStr.equals("LAZY")) { + return NativeContextReleaseStrategy.LAZY; + } + if (propertyStr.equals("EAGER")) { + return NativeContextReleaseStrategy.EAGER; + } + if (!propertyStr.equals("HYBRID")) { + LOG.warning( + String.format( + "Valid values for %s are HYBRID, LAZY, EAGER, with HYBRID as default", propertyName)); + } + return NativeContextReleaseStrategy.HYBRID; + } } diff --git a/tst/com/amazon/corretto/crypto/provider/test/AesTest.java b/tst/com/amazon/corretto/crypto/provider/test/AesTest.java index 84095f61..8fa4fe4e 100644 --- a/tst/com/amazon/corretto/crypto/provider/test/AesTest.java +++ b/tst/com/amazon/corretto/crypto/provider/test/AesTest.java @@ -1120,12 +1120,58 @@ public void testEmptyPlaintext() throws Throwable { assertArrayEquals(new byte[0], plaintext); } + @Test + public void safeCipherReuse() throws Exception { + Cipher c1 = Cipher.getInstance(ALGO_NAME, NATIVE_PROVIDER); + Cipher c2 = Cipher.getInstance(ALGO_NAME, NATIVE_PROVIDER); + GCMParameterSpec spec1 = new GCMParameterSpec(128, randomIV()); + GCMParameterSpec spec2 = new GCMParameterSpec(128, randomIV()); + SecretKey key1 = new SecretKeySpec(TestUtil.getRandomBytes(16), "AES"); + SecretKey key2 = new SecretKeySpec(TestUtil.getRandomBytes(16), "AES"); + byte[] aad = TestUtil.getRandomBytes(100); + String message = "hello world!"; + + c1.init(Cipher.ENCRYPT_MODE, key1, spec1); + c1.updateAAD(aad); + byte[] cipherText1 = c1.doFinal(message.getBytes()); + c1.init(Cipher.DECRYPT_MODE, key1, spec1); + c1.updateAAD(aad); + assertEquals(message, new String(c1.doFinal(cipherText1))); + + c1.init(Cipher.ENCRYPT_MODE, key2, spec2); + c1.updateAAD(aad); + byte[] cipherText2 = c1.doFinal(message.getBytes()); + // Let's use a different context for decrypt + c2.init(Cipher.DECRYPT_MODE, key2, spec2); + c2.updateAAD(aad); + assertEquals(message, new String(c2.doFinal(cipherText2))); + + // Let's set AAD for encrypt but ignore it by another init + c1.init(Cipher.ENCRYPT_MODE, key2, spec1); + c1.updateAAD(aad); + // Initializing again and doFinal immediately after. + c1.init(Cipher.ENCRYPT_MODE, key2, spec2); + byte[] cipherText3 = c1.doFinal(message.getBytes()); + c2.init(Cipher.DECRYPT_MODE, key2, spec2); + assertEquals(message, new String(c2.doFinal(cipherText3))); + + // Let's set AAD for decrypt but ignore it by another init + c1.init(Cipher.ENCRYPT_MODE, key2, spec1); + byte[] cipherText4 = c1.doFinal(message.getBytes()); + c2.init(Cipher.DECRYPT_MODE, key2, spec1); + c2.updateAAD(aad); + c2.init(Cipher.DECRYPT_MODE, key2, spec1); + assertEquals(message, new String(c2.doFinal(cipherText4))); + } + + private static boolean saveNativeContext(Object obj) throws Throwable { + return ((Boolean) sneakyInvoke(obj, "saveNativeContext")).booleanValue(); + } + @Test public void safeReuse() throws Throwable { Cipher c = Cipher.getInstance(ALGO_NAME, NATIVE_PROVIDER); final Object spi = sneakyGetField(c, "spi"); - final int keyReuseThreshold = (int) sneakyGetField(spi.getClass(), "KEY_REUSE_THRESHOLD"); - assertEquals(1, keyReuseThreshold, "Test must be re-written for KEY_REUSE_THRESHOLD != 1"); GCMParameterSpec spec1 = new GCMParameterSpec(128, randomIV()); GCMParameterSpec spec2 = new GCMParameterSpec(128, randomIV()); @@ -1142,61 +1188,110 @@ public void safeReuse() throws Throwable { assertFalse((boolean) sneakyGetField(spi, "contextInitialized")); assertNull(sneakyGetField(spi, "context")); byte[] ciphertext1 = c.doFinal(plaintext1); + if (saveNativeContext(spi)) { + assertNotNull(sneakyGetField(spi, "context")); + } else { + assertNull(sneakyGetField(spi, "context")); + } c.init(Cipher.ENCRYPT_MODE, key, spec2); assertFalse((boolean) sneakyGetField(spi, "contextInitialized")); - assertNull(sneakyGetField(spi, "context")); byte[] ciphertext2 = c.doFinal(plaintext2); + if (saveNativeContext(spi)) { + assertNotNull(sneakyGetField(spi, "context")); + } else { + assertNull(sneakyGetField(spi, "context")); + } c.init(Cipher.ENCRYPT_MODE, key, spec3); assertFalse((boolean) sneakyGetField(spi, "contextInitialized")); - assertNotNull(sneakyGetField(spi, "context")); byte[] ciphertext3 = c.doFinal(plaintext3); - c.init(Cipher.DECRYPT_MODE, key, spec1); assertFalse((boolean) sneakyGetField(spi, "contextInitialized")); - assertNotNull(sneakyGetField(spi, "context")); assertArrayEquals(plaintext1, c.doFinal(ciphertext1)); + if (saveNativeContext(spi)) { + assertNotNull(sneakyGetField(spi, "context")); + } else { + assertNull(sneakyGetField(spi, "context")); + } + c.init(Cipher.DECRYPT_MODE, key, spec2); assertFalse((boolean) sneakyGetField(spi, "contextInitialized")); - assertNotNull(sneakyGetField(spi, "context")); assertArrayEquals(plaintext2, c.doFinal(ciphertext2)); + if (saveNativeContext(spi)) { + assertNotNull(sneakyGetField(spi, "context")); + } else { + assertNull(sneakyGetField(spi, "context")); + } + assertFalse((boolean) sneakyGetField(spi, "contextInitialized")); - assertNotNull(sneakyGetField(spi, "context")); c.init(Cipher.DECRYPT_MODE, key, spec3); assertArrayEquals(plaintext3, c.doFinal(ciphertext3)); + if (saveNativeContext(spi)) { + assertNotNull(sneakyGetField(spi, "context")); + } else { + assertNull(sneakyGetField(spi, "context")); + } // Interleaved c.init(Cipher.ENCRYPT_MODE, key, spec1); assertFalse((boolean) sneakyGetField(spi, "contextInitialized")); - assertNotNull(sneakyGetField(spi, "context")); ciphertext1 = c.doFinal(plaintext1); + if (saveNativeContext(spi)) { + assertNotNull(sneakyGetField(spi, "context")); + } else { + assertNull(sneakyGetField(spi, "context")); + } c.init(Cipher.DECRYPT_MODE, key, spec1); assertFalse((boolean) sneakyGetField(spi, "contextInitialized")); - assertNotNull(sneakyGetField(spi, "context")); assertArrayEquals(plaintext1, c.doFinal(ciphertext1)); + if (saveNativeContext(spi)) { + assertNotNull(sneakyGetField(spi, "context")); + } else { + assertNull(sneakyGetField(spi, "context")); + } c.init(Cipher.ENCRYPT_MODE, key, spec2); assertFalse((boolean) sneakyGetField(spi, "contextInitialized")); - assertNotNull(sneakyGetField(spi, "context")); ciphertext2 = c.doFinal(plaintext2); + if (saveNativeContext(spi)) { + assertNotNull(sneakyGetField(spi, "context")); + } else { + assertNull(sneakyGetField(spi, "context")); + } c.init(Cipher.DECRYPT_MODE, key, spec2); assertFalse((boolean) sneakyGetField(spi, "contextInitialized")); - assertNotNull(sneakyGetField(spi, "context")); assertArrayEquals(plaintext2, c.doFinal(ciphertext2)); + if (saveNativeContext(spi)) { + assertNotNull(sneakyGetField(spi, "context")); + } else { + assertNull(sneakyGetField(spi, "context")); + } c.init(Cipher.ENCRYPT_MODE, key, spec3); assertFalse((boolean) sneakyGetField(spi, "contextInitialized")); - assertNotNull(sneakyGetField(spi, "context")); ciphertext3 = c.doFinal(plaintext3); + if (saveNativeContext(spi)) { + assertNotNull(sneakyGetField(spi, "context")); + } else { + assertNull(sneakyGetField(spi, "context")); + } c.init(Cipher.DECRYPT_MODE, key, spec3); assertFalse((boolean) sneakyGetField(spi, "contextInitialized")); - assertNotNull(sneakyGetField(spi, "context")); assertArrayEquals(plaintext3, c.doFinal(ciphertext3)); + if (saveNativeContext(spi)) { + assertNotNull(sneakyGetField(spi, "context")); + } else { + assertNull(sneakyGetField(spi, "context")); + } // Try a decrypt with the same key bytes but a different key object c.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key.getEncoded(), key.getAlgorithm()), spec2); assertFalse((boolean) sneakyGetField(spi, "contextInitialized")); - assertNotNull(sneakyGetField(spi, "context")); assertArrayEquals(plaintext2, c.doFinal(ciphertext2)); + if (saveNativeContext(spi)) { + assertNotNull(sneakyGetField(spi, "context")); + } else { + assertNull(sneakyGetField(spi, "context")); + } } private byte[] randomIV() { diff --git a/tst/com/amazon/corretto/crypto/provider/test/UtilsTest.java b/tst/com/amazon/corretto/crypto/provider/test/UtilsTest.java index 4a4c5f65..978bd6a0 100644 --- a/tst/com/amazon/corretto/crypto/provider/test/UtilsTest.java +++ b/tst/com/amazon/corretto/crypto/provider/test/UtilsTest.java @@ -208,6 +208,24 @@ private static void getBooleanPropertyTest( .booleanValue()); } + @Test + public void getNativeContextReleaseStrategyPropertyTests() throws Throwable { + getNativeContextReleaseStrategyPropertyTest("accp.native.property1", "HYBRID", "HYBRID"); + getNativeContextReleaseStrategyPropertyTest("accp.native.property2", "dummy", "HYBRID"); + getNativeContextReleaseStrategyPropertyTest("accp.native.property3", "LAZY", "LAZY"); + getNativeContextReleaseStrategyPropertyTest("accp.native.property4", "EAGER", "EAGER"); + } + + private static void getNativeContextReleaseStrategyPropertyTest( + final String propertyName, final String value, final String expectedValue) throws Throwable { + final String fullPropertyName = TestUtil.NATIVE_PROVIDER_PACKAGE + "." + propertyName; + System.setProperty(fullPropertyName, value); + assertEquals( + expectedValue, + (sneakyInvoke(UTILS_CLASS, "getNativeContextReleaseStrategyProperty", propertyName) + .toString())); + } + @Test public void givenNull_whenCheckArrayLimits_expectException() { assertThrows(