Skip to content

Commit

Permalink
Allow users to control the release of EVP context for AES-GCM
Browse files Browse the repository at this point in the history
  • Loading branch information
amirhosv committed Nov 7, 2023
1 parent fffbf92 commit 6275528
Show file tree
Hide file tree
Showing 11 changed files with 575 additions and 269 deletions.
28 changes: 27 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
18 changes: 16 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions benchmarks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 8 additions & 1 deletion benchmarks/lib/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -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`
Expand Down Expand Up @@ -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)
Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
112 changes: 50 additions & 62 deletions csrc/aes_gcm.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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<EVP_CIPHER_CTX*>(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;
Expand Down Expand Up @@ -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,
Expand All @@ -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<EVP_CIPHER_CTX*>(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)
Expand All @@ -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<EVP_CIPHER_CTX*>(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) {
Expand Down Expand Up @@ -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,
Expand All @@ -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<EVP_CIPHER_CTX*>(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)) {
Expand Down
Loading

0 comments on commit 6275528

Please sign in to comment.