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 + ); + } +}