diff --git a/.github/workflows/library_examples.yml b/.github/workflows/library_examples.yml
new file mode 100644
index 000000000..69fd6ce12
--- /dev/null
+++ b/.github/workflows/library_examples.yml
@@ -0,0 +1,58 @@
+# "Copyright Amazon.com Inc. or its affiliates. All Rights Reserved."
+# "SPDX-License-Identifier: CC-BY-SA-4.0"
+# This workflow runs any examples.
+name: Library Examples
+on:
+ workflow_call:
+ inputs:
+ dafny:
+ description: "The Dafny version to run"
+ required: true
+ type: string
+
+jobs:
+ java:
+ runs-on: ubuntu-latest
+ permissions:
+ id-token: write
+ contents: read
+ defaults:
+ run:
+ shell: bash
+ steps:
+ - name: Support longpaths on Git checkout
+ run: |
+ git config --global core.longpaths true
+ - name: Configure AWS Credentials for Tests
+ uses: aws-actions/configure-aws-credentials@v4
+ with:
+ aws-region: us-west-2
+ role-to-assume: arn:aws:iam::370957321024:role/GitHub-CI-MPL-Dafny-Role-us-west-2
+ role-session-name: JavaExampleTests
+
+ - uses: actions/checkout@v4
+ - run: git submodule update --init libraries
+ - run: git submodule update --init smithy-dafny
+
+ - name: Setup Dafny
+ uses: dafny-lang/setup-dafny-action@v1.7.0
+ with:
+ dafny-version: ${{ inputs.dafny }}
+
+ - name: Setup Java 8
+ uses: actions/setup-java@v3
+ with:
+ distribution: "corretto"
+ java-version: 8
+
+ - name: Build AwsCryptographicMaterialProviders Java implementation
+ working-directory: ./AwsCryptographicMaterialProviders
+ run: |
+ # This works because `node` is installed by default on GHA runners
+ CORES=$(node -e 'console.log(os.cpus().length)')
+ make build_java CORES=$CORES
+
+ - name: Test AwsCryptographicMaterialProviders Java Examples
+ working-directory: ./AwsCryptographicMaterialProviders
+ run: |
+ make test_example_java
diff --git a/.github/workflows/pull.yml b/.github/workflows/pull.yml
index 5756acd1d..9abca7ccf 100644
--- a/.github/workflows/pull.yml
+++ b/.github/workflows/pull.yml
@@ -39,6 +39,11 @@ jobs:
uses: ./.github/workflows/library_python_tests.yml
with:
dafny: ${{needs.getVersion.outputs.version}}
+ pr-ci-examples:
+ needs: getVersion
+ uses: ./.github/workflows/library_examples.yml
+ with:
+ dafny: ${{needs.getVersion.outputs.version}}
pr-interop-test:
needs: getVersion
uses: ./.github/workflows/library_interop_tests.yml
@@ -55,6 +60,7 @@ jobs:
- pr-ci-java
- pr-ci-net
- pr-interop-test
+ - pr-ci-examples
runs-on: ubuntu-latest
steps:
- name: Verify all required jobs passed
diff --git a/AwsCryptographicMaterialProviders/Makefile b/AwsCryptographicMaterialProviders/Makefile
index f6828ec81..8fb3fc1ca 100644
--- a/AwsCryptographicMaterialProviders/Makefile
+++ b/AwsCryptographicMaterialProviders/Makefile
@@ -107,4 +107,7 @@ PYTHON_DEPENDENCY_MODULE_NAMES := \
--dependency-library-name=com.amazonaws.kms=aws_cryptography_internal_kms \
--dependency-library-name=com.amazonaws.dynamodb=aws_cryptography_internal_dynamodb \
--dependency-library-name=aws.cryptography.materialProviders=aws_cryptographic_material_providers \
- --dependency-library-name=aws.cryptography.keyStore=aws_cryptographic_material_providers \
\ No newline at end of file
+ --dependency-library-name=aws.cryptography.keyStore=aws_cryptographic_material_providers \
+
+test_example_java:
+ $(GRADLEW) -p runtimes/java testExamples
diff --git a/AwsCryptographicMaterialProviders/runtimes/java/build.gradle.kts b/AwsCryptographicMaterialProviders/runtimes/java/build.gradle.kts
index 8b4871408..71ad39c0a 100644
--- a/AwsCryptographicMaterialProviders/runtimes/java/build.gradle.kts
+++ b/AwsCryptographicMaterialProviders/runtimes/java/build.gradle.kts
@@ -18,9 +18,33 @@ var props = Properties().apply {
var dafnyVersion = props.getProperty("dafnyVersion")
group = "software.amazon.cryptography"
+// version = props.getProperty("mplVersion")
version = "1.7.3-SNAPSHOT"
description = "AWS Cryptographic Material Providers Library"
+sourceSets {
+ create("examples") {
+ compileClasspath += sourceSets.main.get().output
+ runtimeClasspath += sourceSets.main.get().output
+ }
+ create("testExamples") {
+ compileClasspath += sourceSets.test.get().output + sourceSets["examples"].output + sourceSets.main.get().output
+ runtimeClasspath += sourceSets.test.get().output + sourceSets["examples"].output + sourceSets.main.get().output
+ }
+}
+val examplesImplementation: Configuration by configurations.getting{
+ extendsFrom(configurations.testImplementation.get())
+}
+configurations.add(examplesImplementation)
+val examplesAnnotationProcessor: Configuration by configurations.getting{
+ extendsFrom(configurations.testAnnotationProcessor.get())
+}
+configurations.add(examplesAnnotationProcessor)
+val testExamplesImplementation: Configuration by configurations.getting{
+ extendsFrom(configurations["examplesImplementation"])
+}
+configurations.add(testExamplesImplementation)
+
java {
toolchain.languageVersion.set(JavaLanguageVersion.of(8))
sourceSets["main"].java {
@@ -29,6 +53,12 @@ java {
sourceSets["test"].java {
srcDir("src/test")
}
+ sourceSets["examples"].java {
+ srcDir("src/examples")
+ }
+ sourceSets["testExamples"].java {
+ srcDir("src/testExamples")
+ }
withJavadocJar()
withSourcesJar()
}
@@ -83,6 +113,15 @@ dependencies {
// https://mvnrepository.com/artifact/org.testng/testng
testImplementation("org.testng:testng:7.5")
+
+ // Example Dependencies
+ examplesImplementation("software.amazon.awssdk:arns")
+ examplesImplementation("software.amazon.awssdk:auth")
+ examplesImplementation("software.amazon.awssdk:sts")
+ examplesImplementation("software.amazon.awssdk:utils")
+ examplesImplementation("software.amazon.awssdk:apache-client")
+ examplesImplementation("com.google.code.findbugs:jsr305:3.0.2")
+ examplesImplementation("com.google.guava:guava:33.3.1-jre")
}
publishing {
@@ -224,18 +263,18 @@ tasks.test {
// This will show System.out.println statements
testLogging.showStandardStreams = true
- testLogging {
- lifecycle {
- events = mutableSetOf(org.gradle.api.tasks.testing.logging.TestLogEvent.FAILED, org.gradle.api.tasks.testing.logging.TestLogEvent.PASSED, org.gradle.api.tasks.testing.logging.TestLogEvent.SKIPPED)
- exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL
- showExceptions = true
- showCauses = true
- showStackTraces = true
- showStandardStreams = true
- }
- info.events = lifecycle.events
- info.exceptionFormat = lifecycle.exceptionFormat
- }
+ // testLogging {
+ // lifecycle {
+ // events = mutableSetOf(org.gradle.api.tasks.testing.logging.TestLogEvent.FAILED, org.gradle.api.tasks.testing.logging.TestLogEvent.PASSED, org.gradle.api.tasks.testing.logging.TestLogEvent.SKIPPED)
+ // exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL
+ // showExceptions = true
+ // showCauses = true
+ // showStackTraces = true
+ // showStandardStreams = true
+ // }
+ // info.events = lifecycle.events
+ // info.exceptionFormat = lifecycle.exceptionFormat
+ // }
// See https://github.com/gradle/kotlin-dsl/issues/836
addTestListener(object : TestListener {
@@ -256,6 +295,22 @@ tasks.test {
})
}
+val testExamples = task("testExamples") {
+ description = "Runs examples tests."
+ group = "verification"
+
+ testClassesDirs = sourceSets["testExamples"].output.classesDirs
+ classpath = sourceSets["testExamples"].runtimeClasspath + sourceSets["examples"].output + sourceSets.main.get().output
+ shouldRunAfter("compileJava", "compileExamplesJava", "test")
+ // This will show System.out.println statements
+ testLogging.showStandardStreams = true
+ useTestNG()
+
+ testLogging {
+ events("passed")
+ }
+}
+
fun buildPom(mavenPublication: MavenPublication) {
mavenPublication.pom.withXml {
var dependencyManagementNode = asNode().appendNode("dependencyManagement").appendNode("dependencies").appendNode("dependency")
diff --git a/AwsCryptographicMaterialProviders/runtimes/java/src/examples/README.md b/AwsCryptographicMaterialProviders/runtimes/java/src/examples/README.md
new file mode 100644
index 000000000..82184f05d
--- /dev/null
+++ b/AwsCryptographicMaterialProviders/runtimes/java/src/examples/README.md
@@ -0,0 +1,12 @@
+[//]: # "Copyright Amazon.com Inc. or its affiliates. All Rights Reserved."
+[//]: # "SPDX-License-Identifier: CC-BY-SA-4.0"
+
+## Examples (Java)
+
+This project contains examples demonstrating how to use the
+AWS Cryptographic Material Providers Library (MPL) in Java.
+
+```
+├── ..
+└── CMC: Example for implemeting a custom Concurrent Cryptographic Materials Cache (CMC)
+```
diff --git a/AwsCryptographicMaterialProviders/runtimes/java/src/examples/java/software/amazon/cryptography/example/ClientProvider.java b/AwsCryptographicMaterialProviders/runtimes/java/src/examples/java/software/amazon/cryptography/example/ClientProvider.java
new file mode 100644
index 000000000..e005c48b6
--- /dev/null
+++ b/AwsCryptographicMaterialProviders/runtimes/java/src/examples/java/software/amazon/cryptography/example/ClientProvider.java
@@ -0,0 +1,42 @@
+package software.amazon.cryptography.example;
+
+import javax.annotation.Nullable;
+
+import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
+import software.amazon.awssdk.services.kms.KmsClient;
+import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
+import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider;
+import software.amazon.awssdk.http.SdkHttpClient;
+import software.amazon.awssdk.http.apache.ApacheHttpClient;
+
+public class ClientProvider {
+ private static final AwsCredentialsProvider defaultCreds =
+ DefaultCredentialsProvider.create();
+ private static final SdkHttpClient httpClient = ApacheHttpClient.create();
+
+ public static DynamoDbClient dynamoDB(
+ @Nullable DynamoDbClient dynamoDbClient
+ ) {
+ if (dynamoDbClient == null) {
+ //noinspection resource
+ dynamoDbClient = DynamoDbClient.builder()
+ .httpClient(httpClient)
+ .credentialsProvider(defaultCreds)
+ .build();
+ }
+ return dynamoDbClient;
+ }
+
+ public static KmsClient kms(
+ @Nullable KmsClient kmsClient
+ ) {
+ if (kmsClient == null) {
+ //noinspection resource
+ kmsClient = KmsClient.builder()
+ .httpClient(httpClient)
+ .credentialsProvider(defaultCreds)
+ .build();
+ }
+ return kmsClient;
+ }
+}
diff --git a/AwsCryptographicMaterialProviders/runtimes/java/src/examples/java/software/amazon/cryptography/example/cmc/GuavaCMC.java b/AwsCryptographicMaterialProviders/runtimes/java/src/examples/java/software/amazon/cryptography/example/cmc/GuavaCMC.java
new file mode 100644
index 000000000..6fa736960
--- /dev/null
+++ b/AwsCryptographicMaterialProviders/runtimes/java/src/examples/java/software/amazon/cryptography/example/cmc/GuavaCMC.java
@@ -0,0 +1,189 @@
+package software.amazon.cryptography.example.cmc;
+
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
+import java.nio.ByteBuffer;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.NotThreadSafe;
+import javax.annotation.concurrent.ThreadSafe;
+import software.amazon.cryptography.materialproviders.ICryptographicMaterialsCache;
+import software.amazon.cryptography.materialproviders.model.CacheType;
+import software.amazon.cryptography.materialproviders.model.DeleteCacheEntryInput;
+import software.amazon.cryptography.materialproviders.model.EntryDoesNotExist;
+import software.amazon.cryptography.materialproviders.model.GetCacheEntryInput;
+import software.amazon.cryptography.materialproviders.model.GetCacheEntryOutput;
+import software.amazon.cryptography.materialproviders.model.Materials;
+import software.amazon.cryptography.materialproviders.model.PutCacheEntryInput;
+import software.amazon.cryptography.materialproviders.model.UpdateUsageMetadataInput;
+
+/**
+ * Utilize Google's Guava Cache
+ * to implement a
+ * Cryptographic Materials Cache
+ *
+ * At this time,
+ * we cannot utilize the loader functionality,
+ * as the Keyrings and CMMs that utilize the Cache
+ * expect an EntryDoesNotExist exception on a cache miss.
+ */
+public class GuavaCMC {
+
+ /**
+ * Guava is responsible for the LRU logic, but the TTL logic is still handled by us.
+ * @param threads Guides the allowed concurrency among update operations.
+ * @param initialCapacity Sets the minimum total size for the internal hash tables.
+ * @param maximumCapacity Specifies the maximum number of entries the cache may contain.
+ * @param trackUsage Not all applications need to track bytes or messages used. Doing so may help some security engineers, but at a performance cost.
+ * @return CacheType holding Guava Cache
+ */
+ public static CacheType create(
+ final int threads,
+ final int initialCapacity,
+ final int maximumCapacity,
+ boolean trackUsage
+ ) {
+ if (threads < 1 || maximumCapacity < 1 || initialCapacity < 1) {
+ throw new IllegalArgumentException(
+ "threads and maximumCapacity must be greater than 0"
+ );
+ }
+ Cache guaveCache = CacheBuilder
+ .newBuilder()
+ .concurrencyLevel(threads)
+ .maximumSize(maximumCapacity)
+ .initialCapacity(initialCapacity)
+ .build();
+ ConcurrentCMC cmc = new ConcurrentCMC(guaveCache, trackUsage);
+ return CacheType.builder().Shared(cmc).build();
+ }
+
+ @ThreadSafe
+ public static class ConcurrentCMC implements ICryptographicMaterialsCache {
+
+ private final Cache cache;
+ private final boolean trackUsage;
+
+ public ConcurrentCMC(
+ Cache cache,
+ boolean trackUsage
+ ) {
+ this.cache = cache;
+ this.trackUsage = trackUsage;
+ }
+
+ @Override
+ public void DeleteCacheEntry(DeleteCacheEntryInput input) {
+ this.cache.invalidate(input.identifier());
+ }
+
+ @Override
+ public GetCacheEntryOutput GetCacheEntry(GetCacheEntryInput input) {
+ @Nullable
+ CacheEntry entry = this.cache.getIfPresent(input.identifier());
+ //= aws-encryption-sdk-specification/framework/cryptographic-materials-cache.md#time-to-live-ttl
+ //# After a cache entry's TTL has elapsed,
+ //# we say that the entry is _TTL-expired_,
+ //# and a CMC MUST NOT return the entry to any caller.
+ //
+ //= aws-encryption-sdk-specification/framework/cryptographic-materials-cache.md#get-cache-entry
+ //# The CMC MUST validate that the cache entry
+ //# has not exceeded it's stored [TTL](#time-to-live-ttl).
+ //
+ //= aws-encryption-sdk-specification/framework/local-cryptographic-materials-cache.md#get-cache-entry
+ //# The local CMC MUST NOT return any TTL-expired entry.
+ if (
+ entry != null && Time.__default.CurrentRelativeTime() < entry.expiryTime
+ ) {
+ if (trackUsage) {
+ // Not all applications need to track bytes or messages used.
+ // Doing so may help some security engineers, but at a performance cost.
+ entry.updateUsage(input.bytesUsed(), 1);
+ }
+ return entry.toGetCacheEntryOutput();
+ } else if (entry != null) {
+ this.cache.invalidate(input.identifier());
+ // It is CRITICAL that Expired entries are not returned.
+ // Additionally, it is CRITICAL that an EntryDoesNotExist exception is thrown
+ throw EntryDoesNotExist.builder().message("Entry past TTL.").build();
+ }
+ // It is CRITICAL that an EntryDoesNotExist exception is thrown for cache miss
+ throw EntryDoesNotExist
+ .builder()
+ .message("Entry does not exist.")
+ .build();
+ }
+
+ @Override
+ public void PutCacheEntry(PutCacheEntryInput input) {
+ this.cache.put(input.identifier(), new CacheEntry(input));
+ }
+
+ @Override
+ public void UpdateUsageMetadata(UpdateUsageMetadataInput input) {
+ @Nullable
+ CacheEntry entry = this.cache.getIfPresent(input.identifier());
+ if (entry != null) {
+ entry.updateUsage(input.bytesUsed(), 1);
+ this.cache.put(input.identifier(), entry);
+ }
+ }
+ }
+
+ @NotThreadSafe
+ public static class CacheEntry {
+
+ private final Materials materials;
+ private final Long creationTime;
+ private final Long expiryTime;
+ private Integer messagesUsed;
+ private Integer bytesUsed;
+
+ public CacheEntry(PutCacheEntryInput input) {
+ this.materials = input.materials();
+ this.creationTime = input.creationTime();
+ this.expiryTime = input.expiryTime();
+ this.messagesUsed = input.messagesUsed();
+ this.bytesUsed = input.bytesUsed();
+ }
+
+ public GetCacheEntryOutput toGetCacheEntryOutput() {
+ return GetCacheEntryOutput
+ .builder()
+ .materials(this.materials)
+ .creationTime(this.creationTime)
+ .expiryTime(this.expiryTime)
+ .messagesUsed(Math.toIntExact(this.messagesUsed))
+ .bytesUsed(Math.toIntExact(this.bytesUsed))
+ .build();
+ }
+
+ public void updateUsage(long bytesUsed, int messagesUsed) {
+ this.bytesUsed = addLongToInt(bytesUsed, this.bytesUsed);
+ this.messagesUsed = addIntToInt(messagesUsed, this.messagesUsed);
+ }
+
+ // Edge case where a well-used item exceeds int max.
+ private static int addLongToInt(long _long, int _int) {
+ if (
+ _int == Integer.MAX_VALUE ||
+ _long >= Integer.MAX_VALUE ||
+ _long + _int >= Integer.MAX_VALUE
+ ) {
+ return Integer.MAX_VALUE;
+ }
+ return _int + (int) _long;
+ }
+
+ // Edge case where a well-used item exceeds int max.
+ private static int addIntToInt(int _int, int _int2) {
+ if (
+ _int == Integer.MAX_VALUE ||
+ _int2 == Integer.MAX_VALUE ||
+ (long) _int2 + _int >= Integer.MAX_VALUE
+ ) {
+ return Integer.MAX_VALUE;
+ }
+ return _int + _int2;
+ }
+ }
+}
diff --git a/AwsCryptographicMaterialProviders/runtimes/java/src/examples/java/software/amazon/cryptography/example/hierarchy/BranchKeyReadExample.java b/AwsCryptographicMaterialProviders/runtimes/java/src/examples/java/software/amazon/cryptography/example/hierarchy/BranchKeyReadExample.java
new file mode 100644
index 000000000..de07d18d1
--- /dev/null
+++ b/AwsCryptographicMaterialProviders/runtimes/java/src/examples/java/software/amazon/cryptography/example/hierarchy/BranchKeyReadExample.java
@@ -0,0 +1,99 @@
+// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+package software.amazon.cryptography.example.hierarchy;
+
+import java.util.List;
+import javax.annotation.Nullable;
+import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
+import software.amazon.awssdk.services.kms.KmsClient;
+import software.amazon.cryptography.keystore.KeyStore;
+import software.amazon.cryptography.keystore.model.BeaconKeyMaterials;
+import software.amazon.cryptography.keystore.model.BranchKeyMaterials;
+import software.amazon.cryptography.keystore.model.GetActiveBranchKeyInput;
+import software.amazon.cryptography.keystore.model.GetBeaconKeyInput;
+import software.amazon.cryptography.keystore.model.GetBranchKeyVersionInput;
+
+public class BranchKeyReadExample {
+
+ public static String BranchKey(
+ String branchKeyId,
+ String kmsArn,
+ String physicalName,
+ String logicalName,
+ List grantTokens,
+ @Nullable KmsClient kmsClient,
+ @Nullable DynamoDbClient dynamoDbClient
+ ) {
+ // 1. Configure your Key Store resource.
+ KeyStore strict = KeyStoreProvider.StrictKeyStore(
+ kmsArn,
+ physicalName,
+ logicalName,
+ null,
+ kmsClient,
+ dynamoDbClient
+ );
+ BranchKeyMaterials branchKeyMaterials;
+
+ // 2. Get the Active Branch Key
+ branchKeyMaterials =
+ strict
+ .GetActiveBranchKey(
+ GetActiveBranchKeyInput
+ .builder()
+ .branchKeyIdentifier(branchKeyId)
+ .build()
+ )
+ .branchKeyMaterials();
+ // Print the Encryption Context
+ System.out.println(branchKeyMaterials.encryptionContext().toString());
+
+ // 3. Get the related Branch Key Version
+ branchKeyMaterials =
+ strict
+ .GetBranchKeyVersion(
+ GetBranchKeyVersionInput
+ .builder()
+ .branchKeyIdentifier(branchKeyId)
+ .branchKeyVersion(branchKeyMaterials.branchKeyVersion())
+ .build()
+ )
+ .branchKeyMaterials();
+
+ // Print the Encryption Context
+ System.out.println(branchKeyMaterials.encryptionContext().toString());
+
+ // 3. Get the Beacon Key
+ BeaconKeyMaterials beaconKeyMaterials = strict
+ .GetBeaconKey(
+ GetBeaconKeyInput.builder().branchKeyIdentifier(branchKeyId).build()
+ )
+ .beaconKeyMaterials();
+
+ // Print the Encryption Context
+ System.out.println(beaconKeyMaterials.encryptionContext().toString());
+ return branchKeyId;
+ }
+
+ public static void main(final String[] args) {
+ if (args.length <= 1) {
+ throw new IllegalArgumentException(
+ "To run this example, include the keyStoreTableName, logicalKeyStoreName, and kmsKeyArn in args"
+ );
+ }
+ final String keyStoreTableName = args[0];
+ final String logicalKeyStoreName = args[1];
+ final String kmsKeyArn = args[2];
+ final String branchKeyId = args[3];
+
+ BranchKey(
+ branchKeyId,
+ kmsKeyArn,
+ keyStoreTableName,
+ logicalKeyStoreName,
+ null,
+ null,
+ null
+ );
+ }
+}
diff --git a/AwsCryptographicMaterialProviders/runtimes/java/src/examples/java/software/amazon/cryptography/example/hierarchy/CreateKeyExample.java b/AwsCryptographicMaterialProviders/runtimes/java/src/examples/java/software/amazon/cryptography/example/hierarchy/CreateKeyExample.java
new file mode 100644
index 000000000..312ed69bf
--- /dev/null
+++ b/AwsCryptographicMaterialProviders/runtimes/java/src/examples/java/software/amazon/cryptography/example/hierarchy/CreateKeyExample.java
@@ -0,0 +1,104 @@
+// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+package software.amazon.cryptography.example.hierarchy;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
+import software.amazon.awssdk.utils.StringUtils;
+import software.amazon.cryptography.keystore.KeyStore;
+import software.amazon.awssdk.services.kms.KmsClient;
+import software.amazon.cryptography.keystore.model.CreateKeyInput;
+
+
+/*
+ The Hierarchical Keyring Example relies on the existence of a
+ key store with pre-existing branch key material or beacon key material.
+
+ This example demonstrates configuring a Key Store Admin and then
+ using a helper method to create a branch key and beacon key
+ that share the same Id, then return that Id.
+ We will always create a new beacon key alongside a new branch key,
+ even if you are not using searchable encryption.
+
+ This key creation should occur within your control plane.
+ */
+public class CreateKeyExample {
+
+ public static String CreateKey(
+ @Nullable String branchKeyId,
+ @Nonnull final String kmsArn,
+ @Nonnull final String physicalName,
+ @Nonnull final String logicalName,
+ @Nullable final List grantTokens,
+ @Nullable KmsClient kmsClient,
+ @Nullable DynamoDbClient dynamoDbClient
+ ) {
+ // 1. Configure your Key Store resource.
+ KeyStore strict = KeyStoreProvider.StrictKeyStore(
+ kmsArn,
+ physicalName,
+ logicalName,
+ null,
+ kmsClient,
+ dynamoDbClient
+ );
+
+ // 2. If you need to specify the Identifier for a Branch Key, you may.
+ // This is an optional argument.
+ // If an Identifier is not provided, a v4 UUID will be generated and used.
+ // This example provides a combination of a fixed string and a v4 UUID;
+ // this makes it easy for Crypto Tools to clean up these Example Branch Keys.
+ branchKeyId =
+ StringUtils.isBlank(branchKeyId)
+ ? "mpl-java-example-" + java.util.UUID.randomUUID().toString()
+ : branchKeyId;
+
+ // 3. Create a custom encryption context for the Branch Key.
+ // Most encrypted data should have an associated encryption context
+ // to protect integrity. This sample uses placeholder values.
+ // Note that the custom encryption context for a Branch Key is
+ // prefixed by the library with `aws-crypto-ec:`.
+ // For more information see:
+ // blogs.aws.amazon.com/security/post/Tx2LZ6WBJJANTNW/How-to-Protect-the-Integrity-of-Your-Encrypted-Data-by-Using-AWS-Key-Management
+ final Map encryptionContext = Collections.singletonMap(
+ "ExampleContextKey",
+ "ExampleContextValue"
+ );
+
+ // 4. Create a new branch key and beacon key in our KeyStore.
+ // Both the branch key and the beacon key will share an Id.
+ // This creation is eventually consistent.
+ final String actualBranchKeyId = strict
+ .CreateKey(
+ CreateKeyInput
+ .builder()
+ // If you need to specify the Identifier for a Branch Key, you may.
+ // This is an optional argument.
+ .branchKeyIdentifier(branchKeyId)
+ // If a branch key Identifier is provided,
+ // custom encryption context MUST be provided as well.
+ .encryptionContext(encryptionContext)
+ .build()
+ )
+ .branchKeyIdentifier();
+
+ assert actualBranchKeyId.equals(branchKeyId);
+ return branchKeyId;
+ }
+
+ public static void main(final String[] args) {
+ if (args.length <= 1) {
+ throw new IllegalArgumentException(
+ "To run this example, include the keyStoreTableName, logicalKeyStoreName, and kmsKeyArn in args"
+ );
+ }
+ final String physical = args[0];
+ final String logical = args[1];
+ final String kmsKeyArn = args[2];
+ CreateKey(null, kmsKeyArn, physical, logical, null, null, null);
+ }
+}
diff --git a/AwsCryptographicMaterialProviders/runtimes/java/src/examples/java/software/amazon/cryptography/example/hierarchy/KeyStoreProvider.java b/AwsCryptographicMaterialProviders/runtimes/java/src/examples/java/software/amazon/cryptography/example/hierarchy/KeyStoreProvider.java
new file mode 100644
index 000000000..5824337ee
--- /dev/null
+++ b/AwsCryptographicMaterialProviders/runtimes/java/src/examples/java/software/amazon/cryptography/example/hierarchy/KeyStoreProvider.java
@@ -0,0 +1,77 @@
+package software.amazon.cryptography.example.hierarchy;
+
+import java.util.List;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
+import software.amazon.awssdk.services.kms.KmsClient;
+import software.amazon.cryptography.example.ClientProvider;
+import software.amazon.cryptography.keystore.KeyStore;
+import software.amazon.cryptography.keystore.model.Discovery;
+import software.amazon.cryptography.keystore.model.KMSConfiguration;
+import software.amazon.cryptography.keystore.model.KeyStoreConfig;
+
+public class KeyStoreProvider {
+ public static KeyStore DiscoveryKeyStore(
+ @Nonnull final String physicalName,
+ @Nonnull final String logicalName,
+ @Nullable final List grantTokens,
+ @Nullable KmsClient kmsClient,
+ @Nullable DynamoDbClient dynamoDbClient
+ ) {
+ KMSConfiguration kmsConfig = KMSConfiguration.builder()
+ .discovery(Discovery.builder().build())
+ .build();
+ KeyStoreConfig.Builder builder = KeyStoreConfig(
+ physicalName, logicalName, grantTokens, kmsClient, dynamoDbClient);
+ return buildIt(builder, kmsConfig);
+ }
+
+ public static KeyStore StrictKeyStore(
+ @Nonnull final String kmsArn,
+ @Nonnull final String physicalName,
+ @Nonnull final String logicalName,
+ @Nullable final List grantTokens,
+ @Nullable KmsClient kmsClient,
+ @Nullable DynamoDbClient dynamoDbClient
+ ) {
+ KMSConfiguration kmsConfig = KMSConfiguration.builder()
+ .kmsMRKeyArn(kmsArn)
+ .build();
+ KeyStoreConfig.Builder builder = KeyStoreConfig(
+ physicalName, logicalName, grantTokens, kmsClient, dynamoDbClient);
+ return buildIt(builder, kmsConfig);
+ }
+
+ private static KeyStoreConfig.Builder KeyStoreConfig(
+ @Nonnull final String physicalName,
+ @Nonnull final String logicalName,
+ @Nullable final List grantTokens,
+ @Nullable KmsClient kmsClient,
+ @Nullable DynamoDbClient dynamoDbClient
+ ) {
+ kmsClient = ClientProvider.kms(kmsClient);
+ dynamoDbClient = ClientProvider.dynamoDB(dynamoDbClient);
+ KeyStoreConfig.Builder builder = KeyStoreConfig.builder()
+ .ddbTableName(physicalName)
+ .logicalKeyStoreName(logicalName)
+ .ddbClient(dynamoDbClient)
+ .kmsClient(kmsClient);
+ if (grantTokens != null && !grantTokens.isEmpty()) {
+ builder.grantTokens(grantTokens);
+ }
+ return builder;
+ }
+
+ private static KeyStore buildIt(
+ KeyStoreConfig.Builder builder,
+ KMSConfiguration kmsConfig
+ ) {
+ return KeyStore.builder()
+ .KeyStoreConfig(
+ builder.kmsConfiguration(kmsConfig).build())
+ .build();
+ }
+}
diff --git a/AwsCryptographicMaterialProviders/runtimes/java/src/examples/java/software/amazon/cryptography/example/hierarchy/KeyringProvider.java b/AwsCryptographicMaterialProviders/runtimes/java/src/examples/java/software/amazon/cryptography/example/hierarchy/KeyringProvider.java
new file mode 100644
index 000000000..360317975
--- /dev/null
+++ b/AwsCryptographicMaterialProviders/runtimes/java/src/examples/java/software/amazon/cryptography/example/hierarchy/KeyringProvider.java
@@ -0,0 +1,45 @@
+// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+package software.amazon.cryptography.example.hierarchy;
+
+import java.util.List;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
+import software.amazon.awssdk.services.kms.KmsClient;
+import software.amazon.cryptography.example.cmc.GuavaCMC;
+import software.amazon.cryptography.materialproviders.IKeyring;
+import software.amazon.cryptography.materialproviders.MaterialProviders;
+import software.amazon.cryptography.materialproviders.model.CreateAwsKmsHierarchicalKeyringInput;
+import software.amazon.cryptography.materialproviders.model.MaterialProvidersConfig;
+
+import static software.amazon.cryptography.example.hierarchy.KeyStoreProvider.DiscoveryKeyStore;
+
+public class KeyringProvider {
+ public static final long FIVE_MINUTES_IN_SECONDS = 5 * 60;
+
+ public static IKeyring HierarchyWithGuavaCMC(
+ final String branchKeyId,
+ @Nonnull final String physicalName,
+ @Nonnull final String logicalName,
+ @Nullable final List grantTokens,
+ @Nullable KmsClient kmsClient,
+ @Nullable DynamoDbClient dynamoDbClient,
+ @Nullable String partitionId
+ ) {
+ final MaterialProviders mpl = MaterialProviders.builder()
+ .MaterialProvidersConfig(MaterialProvidersConfig.builder().build())
+ .build();
+ CreateAwsKmsHierarchicalKeyringInput.Builder input = CreateAwsKmsHierarchicalKeyringInput.builder()
+ .cache(GuavaCMC.create(5, 100, 1000, false))
+ .branchKeyId(branchKeyId)
+ .keyStore(DiscoveryKeyStore(physicalName, logicalName, grantTokens, kmsClient, dynamoDbClient))
+ .ttlSeconds(FIVE_MINUTES_IN_SECONDS);
+ if (partitionId != null) {
+ input.partitionId(partitionId);
+ }
+ return mpl.CreateAwsKmsHierarchicalKeyring(input.build());
+ }
+}
diff --git a/AwsCryptographicMaterialProviders/runtimes/java/src/examples/java/software/amazon/cryptography/example/hierarchy/VersionKeyExample.java b/AwsCryptographicMaterialProviders/runtimes/java/src/examples/java/software/amazon/cryptography/example/hierarchy/VersionKeyExample.java
new file mode 100644
index 000000000..c4d146e03
--- /dev/null
+++ b/AwsCryptographicMaterialProviders/runtimes/java/src/examples/java/software/amazon/cryptography/example/hierarchy/VersionKeyExample.java
@@ -0,0 +1,94 @@
+// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+package software.amazon.cryptography.example.hierarchy;
+
+import java.util.List;
+
+import javax.annotation.Nullable;
+import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
+import software.amazon.awssdk.services.kms.KmsClient;
+import software.amazon.cryptography.keystore.KeyStore;
+import software.amazon.cryptography.keystore.model.GetActiveBranchKeyInput;
+import software.amazon.cryptography.keystore.model.GetActiveBranchKeyOutput;
+import software.amazon.cryptography.keystore.model.VersionKeyInput;
+
+/*
+ There can only be one active version for each branch key at a time.
+ The Hierarchical keyring typically uses each active branch key version
+ to satisfy multiple requests.
+ But you control the extent to which active branch keys are reused
+ and determine how often the active branch key is rotated.
+
+ Branch keys are not used to encrypt plaintext data keys.
+ They are used to derive the unique wrapping keys
+ that encrypt plaintext data keys.
+ The wrapping key derivation process produces a
+ unique 32 byte wrapping key with 28 bytes of randomness.
+ This means that a branch key can derive more than 79 octillion, or 296,
+ unique wrapping keys before cryptographic wear-out occurs.
+ Despite this very low exhaustion risk,
+ you might be required to rotate your active branch keys more often.
+
+ The active version of the branch key remains active until you rotate it.
+ Previous versions of the active branch key will not
+ be used to perform encrypt operations and
+ cannot be used to derive new wrapping keys.
+ But they can still be queried and provide wrapping keys
+ to decrypt the data keys that they encrypted while active.
+
+ Use the Key Store's VersionKey operation to
+ rotate your active branch key.
+ When you rotate the active branch key,
+ a new branch key is created to replace the previous version.
+ The branch-key-id does not change when you rotate the active branch key.
+ You must specify the branch-key-id that identifies
+ the current active branch key when you call VersionKey.
+ */
+public class VersionKeyExample {
+
+ public static String VersionKey(
+ String branchKeyId,
+ String kmsArn,
+ String physicalName,
+ String logicalName,
+ List grantTokens,
+ @Nullable KmsClient kmsClient,
+ @Nullable DynamoDbClient dynamoDbClient
+ ) {
+ // 1. Configure your Key Store resource.
+ KeyStore strict = KeyStoreProvider.StrictKeyStore(
+ kmsArn,
+ physicalName,
+ logicalName,
+ null,
+ kmsClient,
+ dynamoDbClient
+ );
+
+ strict.VersionKey(
+ VersionKeyInput
+ .builder()
+ // This the Identifier for the Branch Key that is being rotated/versioned.
+ .branchKeyIdentifier(branchKeyId)
+ .build()
+ );
+ return branchKeyId;
+ }
+
+ public static void main(final String[] args) {
+ if (args.length <= 1) {
+ throw new IllegalArgumentException(
+ "To run this example, include the keyStoreTableName, logicalKeyStoreName, and kmsKeyArn in args"
+ );
+ }
+ final String keyStoreTableName = args[0];
+ final String logicalKeyStoreName = args[1];
+ final String kmsKeyArn = args[2];
+ final String branchKeyId = args[3];
+ VersionKey(
+ branchKeyId, kmsKeyArn, keyStoreTableName,
+ logicalKeyStoreName,
+ null, null, null
+ );
+ }
+}
diff --git a/AwsCryptographicMaterialProviders/runtimes/java/src/testExamples/java/software/amazon/cryptography/example/Fixtures.java b/AwsCryptographicMaterialProviders/runtimes/java/src/testExamples/java/software/amazon/cryptography/example/Fixtures.java
new file mode 100644
index 000000000..74a3b4315
--- /dev/null
+++ b/AwsCryptographicMaterialProviders/runtimes/java/src/testExamples/java/software/amazon/cryptography/example/Fixtures.java
@@ -0,0 +1,104 @@
+// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+package software.amazon.cryptography.example;
+
+import java.time.Duration;
+import java.util.HashMap;
+import java.util.Map;
+import javax.annotation.Nullable;
+import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
+import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider;
+import software.amazon.awssdk.http.SdkHttpClient;
+import software.amazon.awssdk.http.apache.ApacheHttpClient;
+import software.amazon.awssdk.regions.Region;
+import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
+import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
+import software.amazon.awssdk.services.dynamodb.model.GetItemResponse;
+import software.amazon.awssdk.services.kms.KmsClient;
+
+public class Fixtures {
+
+ public static final String BRANCH_KEY_ID_WITH_EC =
+ "4bb57643-07c1-419e-92ad-0df0df149d7c";
+ public static final String TEST_KEYSTORE_NAME = "KeyStoreDdbTable";
+ public static final String TEST_LOGICAL_KEYSTORE_NAME = "KeyStoreDdbTable";
+ public static final String BRANCH_KEY_ID_WITH_UN_MODELED_EC =
+ "test-un-modeled-encryption-context-is-usable-8da85fa0-a46a-4140-a13e-e0a5cd9a2be3";
+
+ public static final String POSTAL_HORN_KEY_ARN =
+ "arn:aws:kms:us-west-2:370957321024:key/bc127593-f7da-452c-a1f3-cd34c46f81f8";
+ public static final String KEYSTORE_KMS_ARN =
+ "arn:aws:kms:us-west-2:370957321024:key/9d989aa2-2f9c-438c-a745-cc57d3ad0126";
+ public static final String MRK_ARN_EAST =
+ "arn:aws:kms:us-east-1:658956600833:key/mrk-80bd8ecdcd4342aebd84b7dc9da498a7";
+ public static final String MRK_ARN_WEST =
+ "arn:aws:kms:us-west-2:658956600833:key/mrk-80bd8ecdcd4342aebd84b7dc9da498a7";
+ // Key MUST NOT exist in ap-south-2
+ public static final String MRK_ARN_AP =
+ "arn:aws:kms:ap-south-2:658956600833:key/mrk-80bd8ecdcd4342aebd84b7dc9da498a7";
+ public static final String LIMITED_KMS_ACCESS_IAM_ROLE =
+ "arn:aws:iam::370957321024:role/GitHub-CI-MPL-Limited-KMS-us-west-2";
+ public static final String NO_KMS_ACCESS_IAM_ROLE =
+ "arn:aws:iam::370957321024:role/GitHub-CI-MPL-No-KMS-us-west-2";
+
+ public static final AwsCredentialsProvider defaultCreds =
+ DefaultCredentialsProvider.create();
+ public static final SdkHttpClient httpClient = ApacheHttpClient
+ .builder()
+ .connectionTimeToLive(Duration.ofSeconds(5))
+ .build();
+ public static final DynamoDbClient ddbClientWest2 = DynamoDbClient
+ .builder()
+ .httpClient(httpClient)
+ .credentialsProvider(defaultCreds)
+ .region(Region.US_WEST_2)
+ .build();
+ public static final KmsClient kmsClientWest2 = KmsClient
+ .builder()
+ .httpClient(httpClient)
+ .credentialsProvider(defaultCreds)
+ .region(Region.US_WEST_2)
+ .build();
+ public static final KmsClient kmsClientEast1 = KmsClient
+ .builder()
+ .httpClient(httpClient)
+ .credentialsProvider(defaultCreds)
+ .region(Region.US_EAST_1)
+ .build();
+
+ public static void deleteKeyStoreDdbItem(
+ final String branchKeyId,
+ final String branchKeyType,
+ final String physicalName,
+ @Nullable DynamoDbClient dynamoDbClient
+ ) {
+ Map ddbKey = new HashMap<>(3);
+ ddbKey.put(
+ "branch-key-id",
+ AttributeValue.builder().s(branchKeyId).build()
+ );
+ ddbKey.put("type", AttributeValue.builder().s(branchKeyType).build());
+ dynamoDbClient = ClientProvider.dynamoDB(dynamoDbClient);
+ dynamoDbClient.deleteItem(builder ->
+ builder.tableName(physicalName).key(ddbKey)
+ );
+ }
+
+ public static GetItemResponse getKeyStoreDdbItem(
+ final String branchKeyId,
+ final String branchKeyType,
+ final String physicalName,
+ @Nullable DynamoDbClient dynamoDbClient
+ ) {
+ Map ddbKey = new HashMap<>(3);
+ ddbKey.put(
+ "branch-key-id",
+ AttributeValue.builder().s(branchKeyId).build()
+ );
+ ddbKey.put("type", AttributeValue.builder().s(branchKeyType).build());
+ dynamoDbClient = ClientProvider.dynamoDB(dynamoDbClient);
+ return dynamoDbClient.getItem(builder ->
+ builder.tableName(physicalName).key(ddbKey)
+ );
+ }
+}
diff --git a/AwsCryptographicMaterialProviders/runtimes/java/src/testExamples/java/software/amazon/cryptography/example/cmc/ConcurrentGuavaTest.java b/AwsCryptographicMaterialProviders/runtimes/java/src/testExamples/java/software/amazon/cryptography/example/cmc/ConcurrentGuavaTest.java
new file mode 100644
index 000000000..e77a66905
--- /dev/null
+++ b/AwsCryptographicMaterialProviders/runtimes/java/src/testExamples/java/software/amazon/cryptography/example/cmc/ConcurrentGuavaTest.java
@@ -0,0 +1,162 @@
+package software.amazon.cryptography.example.cmc;
+
+import Time.__default;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentLinkedDeque;
+
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+import software.amazon.cryptography.keystore.model.BranchKeyMaterials;
+import software.amazon.cryptography.materialproviders.ICryptographicMaterialsCache;
+import software.amazon.cryptography.materialproviders.model.CacheType;
+import software.amazon.cryptography.materialproviders.model.EntryDoesNotExist;
+import software.amazon.cryptography.materialproviders.model.GetCacheEntryInput;
+import software.amazon.cryptography.materialproviders.model.GetCacheEntryOutput;
+import software.amazon.cryptography.materialproviders.model.Materials;
+import software.amazon.cryptography.materialproviders.model.PutCacheEntryInput;
+
+public class ConcurrentGuavaTest {
+
+ public static final long FIVE_MINUTES_IN_SECONDS = 5 * 60;
+ private static final List identifies = Collections.unmodifiableList(
+ Arrays.asList("1", "2", "3", "4", "5")
+ );
+ private Map indexToThreadId;
+ private ConcurrentLinkedDeque unpickedIndexes;
+ private ICryptographicMaterialsCache underTest;
+
+ @BeforeClass
+ public void setup() {
+ // Keep threads, threadPoolSize, & |identifies| equal
+ final int threads = 5;
+ indexToThreadId = new ConcurrentHashMap<>(threads + 1, 1, threads);
+ unpickedIndexes = new ConcurrentLinkedDeque<>(identifies);
+ CacheType cmc = GuavaCMC.create(threads, 10, 10, false);
+ underTest = cmc.Shared();
+ // GuavaCMC.ConcurrentCMC;
+ System.out.println(
+ "Thread ID: " +
+ Thread.currentThread().getId() +
+ " Cache: " +
+ underTest.getClass().getSimpleName()
+ );
+ // assert underTest instanceof GuavaCMC.ConcurrentCMC;
+ identifies.forEach(id -> createEntry(id, 0, underTest));
+ }
+
+ private void createEntry(
+ String index,
+ @SuppressWarnings("SameParameterValue") int value,
+ ICryptographicMaterialsCache underTest
+ ) {
+ PutCacheEntryInput input = PutCacheEntryInput
+ .builder()
+ .identifier(identifier(index))
+ .creationTime(__default.CurrentRelativeTime())
+ .expiryTime(__default.CurrentRelativeTime() + FIVE_MINUTES_IN_SECONDS)
+ .messagesUsed(0)
+ .bytesUsed(0)
+ .materials(
+ Materials
+ .builder()
+ .BranchKey(
+ BranchKeyMaterials
+ .builder()
+ .branchKeyIdentifier(index)
+ .branchKeyVersion("0")
+ .encryptionContext(
+ Collections.singletonMap("Robbie", Integer.toString(value))
+ )
+ .branchKey(identifier("4"))
+ .build()
+ )
+ .build()
+ )
+ .build();
+ underTest.PutCacheEntry(input);
+ }
+
+ public ByteBuffer identifier(String index) {
+ return ByteBuffer.wrap(index.getBytes(StandardCharsets.UTF_8));
+ }
+
+ @AfterClass
+ public void teardown() {}
+
+ private String indexForThread(final String threadId) {
+ return indexToThreadId.computeIfAbsent(
+ threadId,
+ str -> unpickedIndexes.pop()
+ );
+ }
+
+ @Test(threadPoolSize = 5, invocationCount = 300, timeOut = 1000)
+ public void TestConcurrentCMC() {
+ String index = indexForThread(
+ String.valueOf(Thread.currentThread().getId())
+ );
+ ByteBuffer identifier = identifier(index);
+ GetCacheEntryInput input = GetCacheEntryInput
+ .builder()
+ .identifier(identifier)
+ .bytesUsed(0L)
+ .build();
+ try {
+ GetCacheEntryOutput output = underTest.GetCacheEntry(input);
+ String oldValue = output
+ .materials()
+ .BranchKey()
+ .encryptionContext()
+ .get("Robbie");
+ String newValue = Integer.toString(Integer.parseInt(oldValue) + 1);
+ System.out.println(
+ "Thread ID: " +
+ Thread.currentThread().getId() +
+ " Index: " +
+ index +
+ " newValue: " +
+ newValue +
+ " oldValue: " +
+ oldValue
+ );
+ output
+ .materials()
+ .BranchKey()
+ .encryptionContext()
+ .put("Robbie", newValue);
+ PutCacheEntryInput putInput = fromCacheOutput(output, identifier);
+ underTest.PutCacheEntry(putInput);
+ } catch (EntryDoesNotExist exception) {
+ System.out.println(
+ "Thread ID: " +
+ Thread.currentThread().getId() +
+ " Index: " +
+ index +
+ " Got EntryDoesNotExist exception"
+ );
+ }
+ }
+
+ public PutCacheEntryInput fromCacheOutput(
+ GetCacheEntryOutput output,
+ ByteBuffer identifier
+ ) {
+ return PutCacheEntryInput
+ .builder()
+ .identifier(identifier)
+ .materials(output.materials())
+ .creationTime(output.creationTime())
+ .expiryTime(output.expiryTime())
+ .bytesUsed(output.bytesUsed())
+ .messagesUsed(output.messagesUsed())
+ .build();
+ }
+}
diff --git a/AwsCryptographicMaterialProviders/runtimes/java/src/testExamples/java/software/amazon/cryptography/example/hierarchy/BranchKeyReadTest.java b/AwsCryptographicMaterialProviders/runtimes/java/src/testExamples/java/software/amazon/cryptography/example/hierarchy/BranchKeyReadTest.java
new file mode 100644
index 000000000..df8a9625e
--- /dev/null
+++ b/AwsCryptographicMaterialProviders/runtimes/java/src/testExamples/java/software/amazon/cryptography/example/hierarchy/BranchKeyReadTest.java
@@ -0,0 +1,22 @@
+package software.amazon.cryptography.example.hierarchy;
+
+import java.util.Collections;
+import org.testng.annotations.Test;
+import software.amazon.cryptography.example.Fixtures;
+
+public class BranchKeyReadTest {
+
+ @Test
+ public void test() {
+ //noinspection unchecked
+ BranchKeyReadExample.BranchKey(
+ Fixtures.BRANCH_KEY_ID_WITH_UN_MODELED_EC,
+ Fixtures.KEYSTORE_KMS_ARN,
+ Fixtures.TEST_KEYSTORE_NAME,
+ Fixtures.TEST_LOGICAL_KEYSTORE_NAME,
+ Collections.EMPTY_LIST,
+ Fixtures.kmsClientWest2,
+ Fixtures.ddbClientWest2
+ );
+ }
+}