diff --git a/.gitignore b/.gitignore index 1da07539c..875661f53 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,7 @@ target/ .classpath /bin/ .idea/ -*.iml \ No newline at end of file +*.iml +/.history +/.DS_Store +/specification_compliance_report.html diff --git a/.gitmodules b/.gitmodules index 0b8f497e1..eb6b6a564 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "src/test/resources/aws-encryption-sdk-test-vectors"] path = src/test/resources/aws-encryption-sdk-test-vectors url = https://github.com/awslabs/private-aws-encryption-sdk-test-vectors-staging.git +[submodule "aws-encryption-sdk-specification"] + path = aws-encryption-sdk-specification + url = https://github.com/awslabs/private-aws-encryption-sdk-specification-staging.git diff --git a/CHANGELOG.md b/CHANGELOG.md index fc4cb2350..845c965fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ # Changelog +## 2.3.0 -- 2021-06-16 + +* feat: AWS KMS multi-Region Key support + + Added new the master key AwsKmsMrkAwareMasterKey + and the new master key provider AwsKmsMrkAwareMasterKeyProvider + that support AWS KMS multi-Region Keys. + + See https://docs.aws.amazon.com/kms/latest/developerguide/multi-region-keys-overview.html + for more details about AWS KMS multi-Region Keys. + + See https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/configure.html#config-mrks + for more details about how the AWS Encryption SDK interoperates + with AWS KMS multi-Region keys. + ## 2.2.0 -- 2021-05-27 * feat: Improvements to the message decryption process. diff --git a/NOTICE.txt b/NOTICE.txt index c95ec7e34..45ed0c46c 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -8,4 +8,4 @@ This software includes third party software subject to the following copyrights: -Cryptographic functions from Bouncy Castle Crypto APIs for Java - Copyright 2000-2013 The Legion of the Bouncy Castle -The licenses for these third party components are included in LICENSE.txt \ No newline at end of file +The licenses for these third party components are included in LICENSE.txt diff --git a/README-JML.md b/README-JML.md index 779042c0c..4cf01fb90 100644 --- a/README-JML.md +++ b/README-JML.md @@ -646,4 +646,4 @@ OpenJML distinguishes between an assertion or specification being found to be in ## Where to Find Java Standard Library Specifications -In the installation of OpenJML, the repo OpenJML/Specs (https://github.com/OpenJML/Specs) is downloaded. This contains specifications for a subset of Java's standard library, following the package structure of the JDK. Any missing specifications could be added into files in the projects contained. Note that the specifications provided for standard library classes and methods are assumed, rather than verified against particular implementations, so any added specifications should be carefully examined so as not to introduce potential unsoundness. If appropriate, additional standard library specifications could be merged into the official release of OpenJML by making a pull request to the development branch of OpenJML/Specs. \ No newline at end of file +In the installation of OpenJML, the repo OpenJML/Specs (https://github.com/OpenJML/Specs) is downloaded. This contains specifications for a subset of Java's standard library, following the package structure of the JDK. Any missing specifications could be added into files in the projects contained. Note that the specifications provided for standard library classes and methods are assumed, rather than verified against particular implementations, so any added specifications should be carefully examined so as not to introduce potential unsoundness. If appropriate, additional standard library specifications could be merged into the official release of OpenJML by making a pull request to the development branch of OpenJML/Specs. diff --git a/README.md b/README.md index 9398fbd41..7d8f2a5fc 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ You can get the latest release from Maven: com.amazonaws aws-encryption-sdk-java - 2.2.0 + 2.3.0 ``` diff --git a/aws-encryption-sdk-specification b/aws-encryption-sdk-specification new file mode 160000 index 000000000..ef3420d0f --- /dev/null +++ b/aws-encryption-sdk-specification @@ -0,0 +1 @@ +Subproject commit ef3420d0fa8740c4a98f2e9e976d75be185473e4 diff --git a/codebuild/compliance.yml b/codebuild/compliance.yml new file mode 100644 index 000000000..fe25a9c37 --- /dev/null +++ b/codebuild/compliance.yml @@ -0,0 +1,9 @@ +version: 0.2 + +phases: + install: + runtime-versions: + nodejs: 12 + build: + commands: + - ./util/test-conditions.sh diff --git a/codebuild/corretto11.yml b/codebuild/corretto11.yml index e065929cf..439323e07 100644 --- a/codebuild/corretto11.yml +++ b/codebuild/corretto11.yml @@ -6,4 +6,4 @@ phases: java: corretto11 build: commands: - - mvn install -Dgpg.skip=true "-DtestVectorZip=file://$CODEBUILD_SRC_DIR/src/test/resources/aws-encryption-sdk-test-vectors/vectors/awses-decrypt/python-2.2.0.zip" + - mvn install -Dgpg.skip=true "-DtestVectorZip=file://$CODEBUILD_SRC_DIR/src/test/resources/aws-encryption-sdk-test-vectors/vectors/awses-decrypt/python-2.3.0-mrks.zip" diff --git a/codebuild/corretto8.yml b/codebuild/corretto8.yml index 71e236f59..1d03f9733 100644 --- a/codebuild/corretto8.yml +++ b/codebuild/corretto8.yml @@ -6,4 +6,4 @@ phases: java: corretto8 build: commands: - - mvn install -Dgpg.skip=true "-DtestVectorZip=file://$CODEBUILD_SRC_DIR/src/test/resources/aws-encryption-sdk-test-vectors/vectors/awses-decrypt/python-2.2.0.zip" + - mvn install -Dgpg.skip=true "-DtestVectorZip=file://$CODEBUILD_SRC_DIR/src/test/resources/aws-encryption-sdk-test-vectors/vectors/awses-decrypt/python-2.3.0-mrks.zip" diff --git a/codebuild/openjdk11.yml b/codebuild/openjdk11.yml index 208f52a28..215db591b 100644 --- a/codebuild/openjdk11.yml +++ b/codebuild/openjdk11.yml @@ -6,4 +6,4 @@ phases: java: openjdk11 build: commands: - - mvn install -Dgpg.skip=true "-DtestVectorZip=file://$CODEBUILD_SRC_DIR/src/test/resources/aws-encryption-sdk-test-vectors/vectors/awses-decrypt/python-2.2.0.zip" + - mvn install -Dgpg.skip=true "-DtestVectorZip=file://$CODEBUILD_SRC_DIR/src/test/resources/aws-encryption-sdk-test-vectors/vectors/awses-decrypt/python-2.3.0-mrks.zip" diff --git a/codebuild/openjdk8.yml b/codebuild/openjdk8.yml index e80b43dd5..ef25414d1 100644 --- a/codebuild/openjdk8.yml +++ b/codebuild/openjdk8.yml @@ -6,4 +6,4 @@ phases: java: openjdk8 build: commands: - - mvn install -Dgpg.skip=true "-DtestVectorZip=file://$CODEBUILD_SRC_DIR/src/test/resources/aws-encryption-sdk-test-vectors/vectors/awses-decrypt/python-2.2.0.zip" + - mvn install -Dgpg.skip=true "-DtestVectorZip=file://$CODEBUILD_SRC_DIR/src/test/resources/aws-encryption-sdk-test-vectors/vectors/awses-decrypt/python-2.3.0-mrks.zip" diff --git a/compliance_exceptions/aws-kms-mrk-aware-multi-keyrings.java b/compliance_exceptions/aws-kms-mrk-aware-multi-keyrings.java new file mode 100644 index 000000000..e40f72c61 --- /dev/null +++ b/compliance_exceptions/aws-kms-mrk-aware-multi-keyrings.java @@ -0,0 +1,104 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// The AWS Encryption SDK - Java does not implement +// any of the Keyring interface at this time. + +//= compliance/framework/aws-kms/aws-kms-mrk-aware-multi-keyrings.txt#2.5 +//= type=exception +//# The caller MUST provide: +//# +//# * A set of Region strings +//# +//# * An optional discovery filter that is an AWS partition and a set of +//# AWS accounts +//# +//# * An optional method that can take a region string and return an AWS +//# KMS client e.g. a regional client supplier +//# +//# * An optional list of AWS KMS grant tokens +//# +//# If an empty set of Region is provided this function MUST fail. If +//# any element of the set of regions is null or an empty string this +//# function MUST fail. If a regional client supplier is not passed, +//# then a default MUST be created that takes a region string and +//# generates a default AWS SDK client for the given region. +//# +//# A set of AWS KMS clients MUST be created by calling regional client +//# supplier for each region in the input set of regions. +//# +//# Then a set of AWS KMS MRK Aware Symmetric Region Discovery Keyring +//# (aws-kms-mrk-aware-symmetric-region-discovery-keyring.md) MUST be +//# created for each AWS KMS client by initializing each keyring with +//# +//# * The AWS KMS client +//# +//# * The input discovery filter +//# +//# * The input AWS KMS grant tokens +//# +//# Then a Multi-Keyring (../multi-keyring.md#inputs) MUST be initialize +//# by using this set of discovery keyrings as the child keyrings +//# (../multi-keyring.md#child-keyrings). This Multi-Keyring MUST be +//# this functions output. + +//= compliance/framework/aws-kms/aws-kms-mrk-aware-multi-keyrings.txt#2.6 +//= type=exception +//# The caller MUST provide: +//# +//# * An optional AWS KMS key identifiers to use as the generator. +//# +//# * An optional set of AWS KMS key identifiers to us as child +//# keyrings. +//# +//# * An optional method that can take a region string and return an AWS +//# KMS client e.g. a regional client supplier +//# +//# * An optional list of AWS KMS grant tokens +//# +//# If any of the AWS KMS key identifiers is null or an empty string this +//# function MUST fail. At least one non-null or non-empty string AWS +//# KMS key identifiers exists in the input this function MUST fail. All +//# AWS KMS identifiers are passed to Assert AWS KMS MRK are unique (aws- +//# kms-mrk-are-unique.md#Implementation) and the function MUST return +//# success otherwise this MUST fail. If a regional client supplier is +//# not passed, then a default MUST be created that takes a region string +//# and generates a default AWS SDK client for the given region. +//# +//# If there is a generator input then the generator keyring MUST be a +//# AWS KMS MRK Aware Symmetric Keyring (aws-kms-mrk-aware-symmetric- +//# keyring.md) initialized with +//# +//# * The generator input. +//# +//# * The AWS KMS client that MUST be created by the regional client +//# supplier when called with the region part of the generator ARN or +//# a signal for the AWS SDK to select the default region. +//# +//# * The input list of AWS KMS grant tokens +//# +//# If there is a set of child identifiers then a set of AWS KMS MRK +//# Aware Symmetric Keyring (aws-kms-mrk-aware-symmetric-keyring.md) MUST +//# be created for each AWS KMS key identifier by initialized each +//# keyring with +//# +//# * AWS KMS key identifier. +//# +//# * The AWS KMS client that MUST be created by the regional client +//# supplier when called with the region part of the AWS KMS key +//# identifier or a signal for the AWS SDK to select the default +//# region. +//# +//# * The input list of AWS KMS grant tokens +//# +//# NOTE: The AWS Encryption SDK SHOULD NOT attempt to evaluate its own +//# default region. +//# +//# Then a Multi-Keyring (../multi-keyring.md#inputs) MUST be initialize +//# by using this generator keyring as the generator keyring (../multi- +//# keyring.md#generator-keyring) and this set of child keyrings as the +//# child keyrings (../multi-keyring.md#child-keyrings). This Multi- +//# Keyring MUST be this functions output. + + + diff --git a/compliance_exceptions/aws-kms-mrk-aware-symmetric-keyring.java b/compliance_exceptions/aws-kms-mrk-aware-symmetric-keyring.java new file mode 100644 index 000000000..e4937758d --- /dev/null +++ b/compliance_exceptions/aws-kms-mrk-aware-symmetric-keyring.java @@ -0,0 +1,226 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// The AWS Encryption SDK - Java does not implement +// any of the Keyring interface at this time. + +//= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.5 +//= type=exception +//# MUST implement the AWS Encryption SDK Keyring interface (../keyring- +//# interface.md#interface) + +//= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.6 +//= type=exception +//# On initialization the caller MUST provide: +//# +//# * An AWS KMS key identifier +//# +//# * An AWS KMS SDK client +//# +//# * An optional list of Grant Tokens +//# +//# The AWS KMS key identifier MUST NOT be null or empty. The AWS KMS +//# key identifier MUST be a valid identifier (aws-kms-key-arn.md#a- +//# valid-aws-kms-identifier). The AWS KMS SDK client MUST NOT be null. + +//= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.7 +//= type=exception +//# OnEncrypt MUST take encryption materials (structures.md#encryption- +//# materials) as input. +//# +//# If the input encryption materials (structures.md#encryption- +//# materials) do not contain a plaintext data key OnEncrypt MUST attempt +//# to generate a new plaintext data key by calling AWS KMS +//# GenerateDataKey (https://docs.aws.amazon.com/kms/latest/APIReference/ +//# API_GenerateDataKey.html). +//# +//# If the keyring calls AWS KMS GenerateDataKeys, it MUST use the +//# configured AWS KMS client to make the call. The keyring MUST call +//# AWS KMS GenerateDataKeys with a request constructed as follows: +//# +//# * "KeyId": this keyring's KMS key identifier. +//# +//# * "NumberOfBytes": the key derivation input length (algorithm- +//# suites.md#key-derivation-input-length) specified by the algorithm +//# suite (algorithm-suites.md) included in the input encryption +//# materials (structures.md#encryption-materials). +//# +//# * "EncryptionContext": the encryption context +//# (structures.md#encryption-context) included in the input +//# encryption materials (structures.md#encryption-materials). +//# +//# * "GrantTokens": this keyring's grant tokens +//# (https://docs.aws.amazon.com/kms/latest/developerguide/ +//# concepts.html#grant_token) +//# +//# If the call to AWS KMS GenerateDataKey +//# (https://docs.aws.amazon.com/kms/latest/APIReference/ +//# API_GenerateDataKey.html) does not succeed, OnEncrypt MUST NOT modify +//# the encryption materials (structures.md#encryption-materials) and +//# MUST fail. +//# +//# If the Generate Data Key call succeeds, OnEncrypt MUST verify that +//# the response "Plaintext" length matches the specification of the +//# algorithm suite (algorithm-suites.md)'s Key Derivation Input Length +//# field. The Generate Data Key response's "KeyId" MUST be A valid AWS +//# KMS key ARN (aws-kms-key-arn.md#identifying-an-aws-kms-multi-region- +//# key). If verified, OnEncrypt MUST do the following with the response +//# from AWS KMS GenerateDataKey +//# (https://docs.aws.amazon.com/kms/latest/APIReference/ +//# API_GenerateDataKey.html): +//# +//# * set the plaintext data key on the encryption materials +//# (structures.md#encryption-materials) as the response "Plaintext". +//# +//# * append a new encrypted data key (structures.md#encrypted-data-key) +//# to the encrypted data key list in the encryption materials +//# (structures.md#encryption-materials), constructed as follows: +//# +//# - the ciphertext (structures.md#ciphertext) is the response +//# "CiphertextBlob". +//# +//# - the key provider id (structures.md#key-provider-id) is "aws- +//# kms". +//# +//# - the key provider information (structures.md#key-provider- +//# information) is the response "KeyId". +//# +//# * OnEncrypt MUST output the modified encryption materials +//# (structures.md#encryption-materials) +//# +//# Given a plaintext data key in the encryption materials +//# (structures.md#encryption-materials), OnEncrypt MUST attempt to +//# encrypt the plaintext data key using the configured AWS KMS key +//# identifier. +//# +//# The keyring MUST call AWS KMS Encrypt +//# (https://docs.aws.amazon.com/kms/latest/APIReference/ +//# API_Encrypt.html) using the configured AWS KMS client. The keyring +//# MUST AWS KMS Encrypt call with a request constructed as follows: +//# +//# * "KeyId": The configured AWS KMS key identifier. +//# +//# * "PlaintextDataKey": the plaintext data key in the encryption +//# materials (structures.md#encryption-materials). +//# +//# * "EncryptionContext": the encryption context +//# (structures.md#encryption-context) included in the input +//# encryption materials (structures.md#encryption-materials). +//# +//# * "GrantTokens": this keyring's grant tokens +//# (https://docs.aws.amazon.com/kms/latest/developerguide/ +//# concepts.html#grant_token) +//# +//# If the call to AWS KMS Encrypt +//# (https://docs.aws.amazon.com/kms/latest/APIReference/ +//# API_Encrypt.html) does not succeed, OnEncrypt MUST fail. +//# +//# If the Encrypt call succeeds The response's "KeyId" MUST be A valid +//# AWS KMS key ARN (aws-kms-key-arn.md#identifying-an-aws-kms-multi- +//# region-key). If verified, OnEncrypt MUST do the following with the +//# response from AWS KMS Encrypt +//# (https://docs.aws.amazon.com/kms/latest/APIReference/ +//# API_Encrypt.html): +//# +//# * append a new encrypted data key (structures.md#encrypted-data-key) +//# to the encrypted data key list in the encryption materials +//# (structures.md#encryption-materials), constructed as follows: +//# +//# - The ciphertext (structures.md#ciphertext) is the response +//# "CiphertextBlob". +//# +//# - The key provider id (structures.md#key-provider-id) is "aws- +//# kms". +//# +//# - The key provider information (structures.md#key-provider- +//# information) is the response "KeyId". Note that the "KeyId" in +//# the response is always in key ARN format. +//# +//# If all Encrypt calls succeed, OnEncrypt MUST output the modified +//# encryption materials (structures.md#encryption-materials). + +//= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.8 +//= type=exception +//# OnDecrypt MUST take decryption materials (structures.md#decryption- +//# materials) and a list of encrypted data keys +//# (structures.md#encrypted-data-key) as input. +//# +//# If the decryption materials (structures.md#decryption-materials) +//# already contained a valid plaintext data key OnDecrypt MUST +//# immediately return the unmodified decryption materials +//# (structures.md#decryption-materials). +//# +//# The set of encrypted data keys MUST first be filtered to match this +//# keyring's configuration. For the encrypted data key to match +//# +//# * Its provider ID MUST exactly match the value "aws-kms". +//# +//# * The provider info MUST be a valid AWS KMS ARN (aws-kms-key- +//# arn.md#a-valid-aws-kms-arn) with a resource type of "key" or +//# OnDecrypt MUST fail. +//# +//# * The the function AWS KMS MRK Match for Decrypt (aws-kms-mrk-match- +//# for-decrypt.md#implementation) called with the configured AWS KMS +//# key identifier and the provider info MUST return "true". +//# +//# For each encrypted data key in the filtered set, one at a time, the +//# OnDecrypt MUST attempt to decrypt the data key. If this attempt +//# results in an error, then these errors MUST be collected. +//# +//# To attempt to decrypt a particular encrypted data key +//# (structures.md#encrypted-data-key), OnDecrypt MUST call AWS KMS +//# Decrypt (https://docs.aws.amazon.com/kms/latest/APIReference/ +//# API_Decrypt.html) with the configured AWS KMS client. +//# +//# When calling AWS KMS Decrypt +//# (https://docs.aws.amazon.com/kms/latest/APIReference/ +//# API_Decrypt.html), the keyring MUST call with a request constructed +//# as follows: +//# +//# * "KeyId": The configured AWS KMS key identifier. +//# +//# * "CiphertextBlob": the encrypted data key ciphertext +//# (structures.md#ciphertext). +//# +//# * "EncryptionContext": the encryption context +//# (structures.md#encryption-context) included in the input +//# decryption materials (structures.md#decryption-materials). +//# +//# * "GrantTokens": this keyring's grant tokens +//# (https://docs.aws.amazon.com/kms/latest/developerguide/ +//# concepts.html#grant_token) +//# +//# If the call to AWS KMS Decrypt +//# (https://docs.aws.amazon.com/kms/latest/APIReference/ +//# API_Decrypt.html) succeeds OnDecrypt verifies +//# +//# * The "KeyId" field in the response MUST equal the configured AWS +//# KMS key identifier. +//# +//# * The length of the response's "Plaintext" MUST equal the key +//# derivation input length (algorithm-suites.md#key-derivation-input- +//# length) specified by the algorithm suite (algorithm-suites.md) +//# included in the input decryption materials +//# (structures.md#decryption-materials). +//# +//# If the response does not satisfies these requirements then an error +//# MUST be collected and the next encrypted data key in the filtered set +//# MUST be attempted. +//# +//# If the response does satisfies these requirements then OnDecrypt MUST +//# do the following with the response: +//# +//# * set the plaintext data key on the decryption materials +//# (structures.md#decryption-materials) as the response "Plaintext". +//# +//# * immediately return the modified decryption materials +//# (structures.md#decryption-materials). +//# +//# If OnDecrypt fails to successfully decrypt any encrypted data key +//# (structures.md#encrypted-data-key), then it MUST yield an error that +//# includes all the collected errors. + + + + + diff --git a/compliance_exceptions/aws-kms-mrk-aware-symmetric-region-discovery-keyring.java b/compliance_exceptions/aws-kms-mrk-aware-symmetric-region-discovery-keyring.java new file mode 100644 index 000000000..f57c07fe5 --- /dev/null +++ b/compliance_exceptions/aws-kms-mrk-aware-symmetric-region-discovery-keyring.java @@ -0,0 +1,125 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// The AWS Encryption SDK - Java does not implement +// any of the Keyring interface at this time. + +//= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.5 +//= type=exception +//# MUST implement that AWS Encryption SDK Keyring interface (../keyring- +//# interface.md#interface) + +//= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.6 +//= type=exception +//# On initialization the caller MUST provide: +//# +//# * An AWS KMS client +//# +//# * An optional discovery filter that is an AWS partition and a set of +//# AWS accounts +//# +//# * An optional list of AWS KMS grant tokens +//# +//# The keyring MUST know what Region the AWS KMS client is in. It +//# SHOULD obtain this information directly from the client as opposed to +//# having an additional parameter. However if it can not, then it MUST +//# NOT create the client itself. It SHOULD have a Region parameter and +//# SHOULD try to identify mismatched configurations. i.e. The client is +//# in Region A and the Region parameter is B. + +//= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.7 +//= type=exception +//# This function MUST fail. + +//= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.8 +//= type=exception +//# OnDecrypt MUST take decryption materials (structures.md#decryption- +//# materials) and a list of encrypted data keys +//# (structures.md#encrypted-data-key) as input. +//# +//# If the decryption materials (structures.md#decryption-materials) +//# already contained a valid plaintext data key OnDecrypt MUST +//# immediately return the unmodified decryption materials +//# (structures.md#decryption-materials). +//# +//# The set of encrypted data keys MUST first be filtered to match this +//# keyring's configuration. For the encrypted data key to match +//# +//# * Its provider ID MUST exactly match the value "aws-kms". +//# +//# * The provider info MUST be a valid AWS KMS ARN (aws-kms-key- +//# arn.md#a-valid-aws-kms-arn) with a resource type of "key" or +//# OnDecrypt MUST fail. +//# +//# * If a discovery filter is configured, its partition and the +//# provider info partition MUST match. +//# +//# * If a discovery filter is configured, its set of accounts MUST +//# contain the provider info account. +//# +//# * If the provider info is not identified as a multi-Region key (aws- +//# kms-key-arn.md#identifying-an-aws-kms-multi-region-key), then the +//# provider info's Region MUST match the AWS KMS client region. +//# +//# For each encrypted data key in the filtered set, one at a time, the +//# OnDecrypt MUST attempt to decrypt the data key. If this attempt +//# results in an error, then these errors are collected. +//# +//# To attempt to decrypt a particular encrypted data key +//# (structures.md#encrypted-data-key), OnDecrypt MUST call AWS KMS +//# Decrypt (https://docs.aws.amazon.com/kms/latest/APIReference/ +//# API_Decrypt.html) with the configured AWS KMS client. +//# +//# When calling AWS KMS Decrypt +//# (https://docs.aws.amazon.com/kms/latest/APIReference/ +//# API_Decrypt.html), the keyring MUST call with a request constructed +//# as follows: +//# +//# * "KeyId": If the provider info's resource type is "key" and its +//# resource is a multi-Region key then a new ARN MUST be created +//# where the region part MUST equal the AWS KMS client region and +//# every other part MUST equal the provider info. Otherwise it MUST +//# be the provider info. +//# +//# * "CiphertextBlob": The encrypted data key ciphertext +//# (structures.md#ciphertext). +//# +//# * "EncryptionContext": The encryption context +//# (structures.md#encryption-context) included in the input +//# decryption materials (structures.md#decryption-materials). +//# +//# * "GrantTokens": this keyring's grant tokens +//# (https://docs.aws.amazon.com/kms/latest/developerguide/ +//# concepts.html#grant_token) +//# +//# If the call to AWS KMS Decrypt +//# (https://docs.aws.amazon.com/kms/latest/APIReference/ +//# API_Decrypt.html) succeeds OnDecrypt verifies +//# +//# * The "KeyId" field in the response MUST equal the requested "KeyId" +//# +//# * The length of the response's "Plaintext" MUST equal the key +//# derivation input length (algorithm-suites.md#key-derivation-input- +//# length) specified by the algorithm suite (algorithm-suites.md) +//# included in the input decryption materials +//# (structures.md#decryption-materials). +//# +//# If the response does not satisfies these requirements then an error +//# is collected and the next encrypted data key in the filtered set MUST +//# be attempted. +//# +//# Since the response does satisfies these requirements then OnDecrypt +//# MUST do the following with the response: +//# +//# * set the plaintext data key on the decryption materials +//# (structures.md#decryption-materials) as the response "Plaintext". +//# +//# * immediately return the modified decryption materials +//# (structures.md#decryption-materials). +//# +//# If OnDecrypt fails to successfully decrypt any encrypted data key +//# (structures.md#encrypted-data-key), then it MUST yield an error that +//# includes all collected errors. + + + diff --git a/pom.xml b/pom.xml index fdfd69543..9cce49b78 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ com.amazonaws aws-encryption-sdk-java - 2.2.0 + 2.3.0 jar aws-encryption-sdk-java @@ -55,10 +55,16 @@ org.mockito mockito-core - 2.28.1 + 3.8.0 test + + org.junit.jupiter + junit-jupiter-api + 5.7.1 + test + org.junit.vintage junit-vintage-engine @@ -96,6 +102,13 @@ + + + src/main/resources + true + + + org.apache.maven.plugins @@ -138,6 +151,54 @@ + + + org.jacoco + jacoco-maven-plugin + 0.8.6 + + + + prepare-agent + + + + report + prepare-package + + report + + + + check + + check + + + + + BUNDLE + + + INSTRUCTION + COVEREDRATIO + 0.88 + + + BRANCH + COVEREDRATIO + 0.80 + + + + com.xyz.ClassToExclude + + + + + + + @@ -279,4 +340,5 @@ + diff --git a/src/examples/java/com/amazonaws/crypto/examples/BasicMultiRegionKeyEncryptionExample.java b/src/examples/java/com/amazonaws/crypto/examples/BasicMultiRegionKeyEncryptionExample.java new file mode 100644 index 000000000..36381cead --- /dev/null +++ b/src/examples/java/com/amazonaws/crypto/examples/BasicMultiRegionKeyEncryptionExample.java @@ -0,0 +1,112 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.amazonaws.crypto.examples; + +import com.amazonaws.encryptionsdk.AwsCrypto; +import com.amazonaws.encryptionsdk.CommitmentPolicy; +import com.amazonaws.encryptionsdk.CryptoResult; +import com.amazonaws.encryptionsdk.kms.AwsKmsMrkAwareMasterKey; +import com.amazonaws.encryptionsdk.kms.AwsKmsMrkAwareMasterKeyProvider; + +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Collections; +import java.util.Map; + +/** + * Encrypts and then decrypts data using two related AWS KMS multi-Region keys. In this example, the + * AWS KMS multi-Region encrypt key is in a different region than the related AWS KMS multi-Region + * decrypt key. + * + *

This example demonstrates how you might use AWS KMS multi-Region keys and the AWS Encryption + * SDK client-side library to encrypt data in one region, move or copy it to a different region, and + * decrypt the data in the destination region. + * + *

Arguments: Two related AWS KMS multi-Region keys + * + *

    + *
  1. Encrypt multi-Region key ARN + *
  2. Decrypt multi-Region key ARN + *
+ * + * For help finding the key ARN of your multi-Region key, see "Finding the key ID and ARN" at + * https://docs.aws.amazon.com/kms/latest/developerguide/find-cmk-id-arn.html + */ +public class BasicMultiRegionKeyEncryptionExample { + + private static final byte[] EXAMPLE_DATA = "Hello World".getBytes(StandardCharsets.UTF_8); + + public static void main(final String[] args) { + final String multiRegionEncryptKey = args[0]; + final String multiRegionDecryptKey = args[1]; + + encryptAndDecrypt(multiRegionEncryptKey, multiRegionDecryptKey); + } + + static void encryptAndDecrypt( + final String multiRegionEncryptKey, + final String multiRegionDecryptKey + ) { + // 1. Instantiate the SDK + // This builds the AwsCrypto client with + // the RequireEncryptRequireDecrypt commitment policy, + // which encrypts and decrypts only with committing algorithm suites. + // This is the default commitment policy + // if you build the client with `AwsCrypto.builder().build()` + // or `AwsCrypto.standard()`. + final AwsCrypto crypto = AwsCrypto.builder() + .withCommitmentPolicy(CommitmentPolicy.RequireEncryptRequireDecrypt) + .build(); + + // 2. Instantiate an AWS KMS multi-Region optimized master key provider + // in strict mode using buildStrict(). + // This example uses two related multi-Region keys. + // In strict mode, the AWS KMS multi-Region optimized master key provider encrypts + // and decrypts only by using the key indicated + // by key ARN passed to `buildStrict`. + // To encrypt with this master key provider, + // use any valid AWS KMS key identifier to identify the CMKs. + // In strict mode, the decrypt operation requires a key ARN. + final AwsKmsMrkAwareMasterKeyProvider encryptingKeyProvider = AwsKmsMrkAwareMasterKeyProvider + .builder() + .buildStrict(multiRegionEncryptKey); + + // 3. Create an encryption context + // Most encrypted data + // should have an associated encryption context + // protect its integrity. + // This sample uses placeholder values. + // 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. Encrypt the data + final CryptoResult encryptResult = crypto.encryptData(encryptingKeyProvider, EXAMPLE_DATA, encryptionContext); + final byte[] ciphertext = encryptResult.getResult(); + + // 5. Instantiate an AWS KMS multi-Region optimized master key provider + // in strict mode using buildStrict(). + // This example uses two related multi-Region keys. + // Now decrypt with a related multi-Region key in a different region. + // In strict mode, the decrypt operation requires a key ARN. + final AwsKmsMrkAwareMasterKeyProvider decryptKeyProvider = AwsKmsMrkAwareMasterKeyProvider + .builder() + .buildStrict(multiRegionDecryptKey); + + // 6. Decrypt the data with a related multi-Region key in a different region. + final CryptoResult decryptResult = crypto.decryptData(decryptKeyProvider, ciphertext); + + // 7. Verify that the encryption context in the result contains + // the encryption context supplied to the encryptData method. + // Because the ESDK can add values to the encryption context, + // don't require that the entire context matches. + if (!encryptionContext.entrySet().stream() + .allMatch(e -> e.getValue().equals(decryptResult.getEncryptionContext().get(e.getKey())))) { + throw new IllegalStateException("Wrong Encryption Context!"); + } + + // 8. Verify that the decrypted plaintext matches the original plaintext + assert Arrays.equals(decryptResult.getResult(), EXAMPLE_DATA); + } +} diff --git a/src/examples/java/com/amazonaws/crypto/examples/DiscoveryMultiRegionDecryptionExample.java b/src/examples/java/com/amazonaws/crypto/examples/DiscoveryMultiRegionDecryptionExample.java new file mode 100644 index 000000000..98ffea359 --- /dev/null +++ b/src/examples/java/com/amazonaws/crypto/examples/DiscoveryMultiRegionDecryptionExample.java @@ -0,0 +1,137 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.amazonaws.crypto.examples; + +import com.amazonaws.encryptionsdk.AwsCrypto; +import com.amazonaws.encryptionsdk.CommitmentPolicy; +import com.amazonaws.encryptionsdk.CryptoResult; +import com.amazonaws.encryptionsdk.kms.DiscoveryFilter; +import com.amazonaws.encryptionsdk.kms.AwsKmsMrkAwareMasterKey; +import com.amazonaws.encryptionsdk.kms.AwsKmsMrkAwareMasterKeyProvider; + +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Collections; +import java.util.Map; + +/** + *

+ * Encrypts and then decrypts data using an AWS KMS customer master key in discovery mode. + * Discovery mode is useful when you can't or don't want to specify a CMK on decrypt. + *

+ * Arguments: + *

    + *
  1. Key Name: A key identifier for the AWS KMS customer master key (CMK). For example, + * a key ARN or a key alias. + * For details, see "Key identifiers" at https://docs.aws.amazon.com/kms/latest/developerguide/concepts.html#key-id + *
  2. Partition: The partition of the AWS KMS customer master key, which is usually "aws." + * A partition is a group of regions. The partition is the second element in the key ARN, e.g. "arn" in "aws:aws: ..." + * For details, see: https://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html#arns-syntax + *
  3. Account ID: The identifier for the account of the AWS KMS customer master key. + *
+ */ +public class DiscoveryMultiRegionDecryptionExample { + + private static final byte[] EXAMPLE_DATA = "Hello World".getBytes(StandardCharsets.UTF_8); + + public static void main(final String[] args) { + final String keyName = args[0]; + final String partition = args[1]; + final String accountId = args[2]; + final String discoveryMrkRegion = args[3]; + + encryptAndDecrypt(keyName, partition, accountId, discoveryMrkRegion); + } + + static void encryptAndDecrypt( + final String keyName, + final String partition, + final String accountId, + final String discoveryMrkRegion + ) { + // 1. Instantiate the SDK + // This builds the AwsCrypto client with + // the RequireEncryptRequireDecrypt commitment policy, + // which decrypts only with committing algorithm suites. + // This is the default commitment policy + // if you build the client with `AwsCrypto.builder().build()` + // or `AwsCrypto.standard()`. + final AwsCrypto crypto = AwsCrypto.builder() + .withCommitmentPolicy(CommitmentPolicy.RequireEncryptRequireDecrypt) + .build(); + + // 2. Instantiate an AWS KMS multi region optimized master key provider + // in strict mode using buildStrict(). + // In this example we are using + // two related multi region keys. + // we will encrypt with + // the encrypting in the encrypting region first. + // In strict mode, + // the AWS KMS multi region optimized master key provider encrypts + // and decrypts only by using the key indicated + // by key arn passed to `buildStrict`. + // To encrypt with this master key provider, + // use an AWS KMS key ARN to identify the CMKs. + // In strict mode, the decrypt operation requires a key ARN. + final AwsKmsMrkAwareMasterKeyProvider encryptingKeyProvider = AwsKmsMrkAwareMasterKeyProvider + .builder() + .buildStrict(keyName); + + // 3. Create an encryption context + // Most encrypted data + // should have an associated encryption context + // to protect integrity. + // This sample uses placeholder values. + // 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. Encrypt the data + final CryptoResult encryptResult = crypto + .encryptData(encryptingKeyProvider, EXAMPLE_DATA, encryptionContext); + final byte[] ciphertext = encryptResult.getResult(); + + // 5. Instantiate a discovery filter for decrypting. + // This filter limits the CMKs that the ESDK can use + // to those in the specified AWS partition and accounts. + // This filter is not required for discovery mode, + // but is a best practice. + final DiscoveryFilter discoveryFilter = new DiscoveryFilter(partition, accountId); + + // 6. Instantiate an AWS KMS multi region optimized master key provider + // for decryption in discovery mode (`buildDiscovery`) + // with a Discovery Mrk Region + // and with a discovery filter. + // + // In discovery mode, the AWS KMS multi region optimized master key provider + // attempts to decrypt only by using AWS KMS keys indicated in the encrypted message. + // By configuring the master key provider with a Discovery Mrk Region, + // this master key provider will only attempt to decrypt + // with AWS KMS multi-Region keys in the Discovery Mrk Region. + // If the Discovery Mrk Region is not configured, + // it is limited to the Region configured for the AWS SDK. + final AwsKmsMrkAwareMasterKeyProvider decryptingKeyProvider = AwsKmsMrkAwareMasterKeyProvider + .builder() + .withDiscoveryMrkRegion(discoveryMrkRegion) + .buildDiscovery(discoveryFilter); + + // 7. Decrypt the data + // Even though the message was encrypted with an AWS KMS key in one region + // the master key provider will attempt to decrypt with the discoveryMrkRegion. + final CryptoResult decryptResult = crypto + .decryptData(decryptingKeyProvider, ciphertext); + + // 8. Verify that the encryption context in the result contains + // the encryption context supplied to the encryptData method. + // Because the ESDK can add values to the encryption context, + // don't require that the entire context matches. + if (!encryptionContext.entrySet().stream() + .allMatch(e -> e.getValue().equals(decryptResult.getEncryptionContext().get(e.getKey())))) { + throw new IllegalStateException("Wrong Encryption Context!"); + } + + // 9. Verify that the decrypted plaintext matches the original plaintext + assert Arrays.equals(decryptResult.getResult(), EXAMPLE_DATA); + } +} diff --git a/src/examples/java/com/amazonaws/crypto/examples/FileStreamingExample.java b/src/examples/java/com/amazonaws/crypto/examples/FileStreamingExample.java index 8938baa21..7399cba29 100644 --- a/src/examples/java/com/amazonaws/crypto/examples/FileStreamingExample.java +++ b/src/examples/java/com/amazonaws/crypto/examples/FileStreamingExample.java @@ -101,4 +101,4 @@ private static SecretKey retrieveEncryptionKey() { rnd.nextBytes(rawKey); return new SecretKeySpec(rawKey, "AES"); } -} \ No newline at end of file +} diff --git a/src/main/java/com/amazonaws/encryptionsdk/CryptoOutputStream.java b/src/main/java/com/amazonaws/encryptionsdk/CryptoOutputStream.java index 785403152..1dbee7c46 100644 --- a/src/main/java/com/amazonaws/encryptionsdk/CryptoOutputStream.java +++ b/src/main/java/com/amazonaws/encryptionsdk/CryptoOutputStream.java @@ -183,4 +183,4 @@ public CryptoResult, K> getCryptoResult() { (List) cryptoHandler_.getMasterKeys(), cryptoHandler_.getHeaders()); } -} \ No newline at end of file +} diff --git a/src/main/java/com/amazonaws/encryptionsdk/internal/AwsKmsCmkArnInfo.java b/src/main/java/com/amazonaws/encryptionsdk/internal/AwsKmsCmkArnInfo.java index 52521b095..c1dddde1c 100644 --- a/src/main/java/com/amazonaws/encryptionsdk/internal/AwsKmsCmkArnInfo.java +++ b/src/main/java/com/amazonaws/encryptionsdk/internal/AwsKmsCmkArnInfo.java @@ -1,14 +1,235 @@ package com.amazonaws.encryptionsdk.internal; +import java.util.Arrays; + + +/** + * A class to parse and handle AWS KMS identifiers. + * Mostly AWS KMS ARNs but raw resources + * are also used in the AWS Encryption SDK. + */ public final class AwsKmsCmkArnInfo { + final private static String arnLiteral = "arn"; + final private static String kmsServiceName = "kms"; + + /** + * Takes an AWS KMS identifier that may or may not be an ARN + * and attempts to parse the identifier as an ARN. + * If the identifier is not an ARN, it returns + * null. This is an expected condition, not an error. + * + * @param keyArn The string to parse + */ + public static AwsKmsCmkArnInfo parseInfoFromKeyArn(final String keyArn) { + /* Precondition: keyArn must be a string. */ + if (keyArn == null || keyArn.isEmpty()) return null; + + final String[] parts = AwsKmsArnParts.splitArn(keyArn); + + //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.5 + //# MUST start with string "arn" + if (!arnLiteral.equals(parts[AwsKmsArnParts.ArnLiteral.index()])) { + return null; + } + + //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.5 + //# The service MUST be the string "kms" + if (!kmsServiceName.equals(parts[AwsKmsArnParts.Service.index()])) { + return null; + } + + //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.5 + //# The partition MUST be a non-empty + // + //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.5 + //# The region MUST be a non-empty string + // + //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.5 + //# The account MUST be a non-empty string + // + final boolean emptyParts = Arrays.stream(parts).anyMatch(String::isEmpty); + if (emptyParts || AwsKmsArnParts.values().length != parts.length) return null; + + //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.5 + //# The resource section MUST be non-empty and MUST be split by a + //# single "/" any additional "/" are included in the resource id + String[] resourceParts = AwsKmsArnParts + .Resource + .splitResourceParts(parts[AwsKmsArnParts.ResourceParts.index()]); + + //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.5 + //# The resource id MUST be a non-empty string + if (Arrays.stream(resourceParts).anyMatch(String::isEmpty) + || AwsKmsArnParts.Resource.values().length > resourceParts.length + ) { + return null; + } + + //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.5 + //# The resource type MUST be either "alias" or "key" + if (!("key".equals(resourceParts[AwsKmsArnParts.Resource.ResourceType.index()]) + || "alias".equals(resourceParts[AwsKmsArnParts.Resource.ResourceType.index()]))) { + return null; + } + + return new AwsKmsCmkArnInfo( + parts[AwsKmsArnParts.Partition.index()], + parts[AwsKmsArnParts.Region.index()], + parts[AwsKmsArnParts.Account.index()], + resourceParts[AwsKmsArnParts.Resource.ResourceType.index()], + resourceParts[AwsKmsArnParts.Resource.Resource.index()] + ); + } + + /** Takes a string an will throw if this identifier is invalid + * Raw resources like a key ID or alias + * `mrk-edb7fe6942894d32ac46dbb1c922d574`, `alias/my-alias` + * or ARNs like + * arn:aws:kms:us-west-2:111122223333:key/mrk-edb7fe6942894d32ac46dbb1c922d574 + * arn:aws:kms:us-west-2:111122223333:alias/my-alias + * + * @param identifier an identifier that is an ARN or raw resource + */ + public static void validAwsKmsIdentifier(final String identifier) { + /* Exceptional Postcondition: Null or empty string is not a valid identifier. */ + if (identifier == null || identifier.isEmpty()) { + throw new IllegalArgumentException("Null or empty string is not a valid Aws KMS identifier."); + } + + /* Exceptional Postcondition: Things that start with `arn:` MUST be ARNs. */ + if (identifier.startsWith("arn:") && parseInfoFromKeyArn(identifier) == null) { + throw new IllegalArgumentException("Invalid ARN used as an identifier."); + }; + /* Postcondition: Raw alias starts with `alias/`. */ + if (identifier.startsWith("alias/")) return; + + /* Postcondition: There are no requirements on key ids. + * Even thought they look like UUID, this is not required. + * Take multi region keys: mrk-edb7fe6942894d32ac46dbb1c922d574 + */ + return; + } + + //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.9 + //# This function MUST take a single AWS KMS identifier + /** + * Identifies Multi Region AWS KMS keys. + * This can misidentify an alias that starts with "mrk-". + * + */ + public static boolean isMRK(final String resource) { + //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.9 + //# If the input starts with "arn:", this MUST return the output of + //# identifying an an AWS KMS multi-Region ARN (aws-kms-key- + //# arn.md#identifying-an-an-aws-kms-multi-region-arn) called with this + //# input. + if (resource.startsWith("arn:")) return isMRK(parseInfoFromKeyArn(resource)); + //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.9 + //# If the input starts with "alias/", this an AWS KMS alias and + //# not a multi-Region key id and MUST return false. + if (resource.startsWith("alias/")) return false; + //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.9 + //# If the input starts + //# with "mrk-", this is a multi-Region key id and MUST return true. + // + //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.9 + //# If + //# the input does not start with any of the above, this is not a multi- + //# Region key id and MUST return false. + return resource.startsWith("mrk-"); + } + + //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.8 + //# This function MUST take a single AWS KMS ARN + /** + * Identifies Multi Region AWS KMS keys. + * The resource type check is to protect against the edge case where an alias starts with + * `mrk-` * e.g. arn:aws:kms:us-west-2:111122223333:alias/mrk-someOtherName + * + */ + public static boolean isMRK(final AwsKmsCmkArnInfo arn) { + + //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.8 + //# If the input is an invalid AWS KMS ARN this function MUST error. + if (arn == null) throw new Error("Invalid Arn"); + + //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.8 + //# If resource type is "alias", this is an AWS KMS alias ARN and MUST + //# return false. + // + //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.8 + //# If resource type is "key" and resource ID starts with + //# "mrk-", this is a AWS KMS multi-Region key ARN and MUST return true. + // + //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.8 + //# If resource type is "key" and resource ID does not start with "mrk-", + //# this is a (single-region) AWS KMS key ARN and MUST return false. + return isMRK(arn.getResource()) && arn.getResourceType().equals("key"); + } + + //= compliance/framework/aws-kms/aws-kms-mrk-match-for-decrypt.txt#2.5 + //# The caller MUST provide: + /** + * Tell if two different AWS KMS ARNs match. + * For identical keys this is trivial, + * but multi-Region keys can match across regions. + * + */ + public static boolean awsKmsArnMatchForDecrypt( + final String configuredKeyIdentifier, + final String providerInfoKeyIdentifier + ) { + //= compliance/framework/aws-kms/aws-kms-mrk-match-for-decrypt.txt#2.5 + //# If both identifiers are identical, this function MUST return "true". + if (configuredKeyIdentifier.equals(providerInfoKeyIdentifier)) return true; + + final AwsKmsCmkArnInfo configuredArnInfo = parseInfoFromKeyArn(configuredKeyIdentifier); + final AwsKmsCmkArnInfo providerInfoKeyArnInfo = parseInfoFromKeyArn(providerInfoKeyIdentifier); + + /* Check for early return (Postcondition): Both identifiers are not ARNs and not equal, therefore they can not match. */ + if (providerInfoKeyArnInfo == null || configuredArnInfo == null) return false; + + //= compliance/framework/aws-kms/aws-kms-mrk-match-for-decrypt.txt#2.5 + //# Otherwise if either input is not identified as a multi-Region key + //# (aws-kms-key-arn.md#identifying-an-aws-kms-multi-region-key), then + //# this function MUST return "false". + if (!isMRK(configuredArnInfo) || !isMRK(providerInfoKeyArnInfo)) return false; + + //= compliance/framework/aws-kms/aws-kms-mrk-match-for-decrypt.txt#2.5 + //# Otherwise if both inputs are + //# identified as a multi-Region keys (aws-kms-key-arn.md#identifying-an- + //# aws-kms-multi-region-key), this function MUST return the result of + //# comparing the "partition", "service", "accountId", "resourceType", + //# and "resource" parts of both ARN inputs. + //Service is not matched because AwsKmsCmkArnInfo only allows a service of `kms`. + return configuredArnInfo.getPartition().equals(providerInfoKeyArnInfo.getPartition()) && + configuredArnInfo.getAccountId().equals(providerInfoKeyArnInfo.getAccountId()) && + configuredArnInfo.getResourceType().equals(providerInfoKeyArnInfo.getResourceType()) && + configuredArnInfo.getResource().equals(providerInfoKeyArnInfo.getResource()); + } + private final String partition_; private final String accountId_; private final String region_; + private final String resource_; + private final String resourceType_; - public AwsKmsCmkArnInfo(String partition, String region, String accountId) { + /** + * Data structure to hold the parts of an AWS KMS ARN + * + */ + AwsKmsCmkArnInfo( + String partition, + String region, + String accountId, + String resourceType, + String resource + ) { partition_ = partition; region_ = region; accountId_ = accountId; + resourceType_ = resourceType; + resource_ = resource; } public String getPartition() { @@ -22,4 +243,104 @@ public String getAccountId() { public String getRegion() { return region_; } + + public String getResourceType() { return resourceType_; } + + public String getResource() { return resource_; } + + + /** + * Returns the well-formed ARN this object describes. + * + */ + @Override + public String toString() { + return toString(region_); + } + + /** + * AWS KMS multi-Region keys can have replicas in other region. + * A compatible ARN in a different Region may be required. + * + * @param mrkRegion The region to use instead of the region in the ARN + */ + public String toString(String mrkRegion) { + return String.join( + AwsKmsArnParts.Delimiter, + arnLiteral, + partition_, + kmsServiceName, + mrkRegion, + accountId_, + String.join( + AwsKmsArnParts.Resource.ResourceDelimiter, + resourceType_, + resource_)); + } + + /** + * Structure information about an ARN. + * This structure is only expecting + * to process AWS KMS ARNs + * see https://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html + * for more details. + * + */ + enum AwsKmsArnParts { + ArnLiteral(0), + Partition(1), + Service(2), + Region(3), + Account(4), + ResourceParts(5); + + int index_; + AwsKmsArnParts(int i) { + index_ = i; + } + int index() { + return index_; + } + + public static String[] splitArn(String arn) { + return arn.split( + AwsKmsArnParts.Delimiter, + AwsKmsArnParts.values().length); + } + + static String Delimiter = ":"; + + /** + * Structure information about the resource part of an ARN + * This structure is only expecting + * to process AWS KMS ARNs + * see https://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html + * for more details. + * + * Of note, is that the ARN specification lets the `/` also be a `:` + * however AWS KMS does not support this. + * AWS KMS _only_ uses `/` to delimit the resource type and resource. + * + */ + enum Resource { + ResourceType(0), + Resource(1); + + static String ResourceDelimiter = "/"; + + int index_; + Resource(int i) { + index_ = i; + } + int index() { + return index_; + } + + public static String[] splitResourceParts(String resource) { + return resource.split( + ResourceDelimiter, + 2); + } + } + } } diff --git a/src/main/java/com/amazonaws/encryptionsdk/internal/BlockEncryptionHandler.java b/src/main/java/com/amazonaws/encryptionsdk/internal/BlockEncryptionHandler.java index e40ac944c..d50b983ca 100644 --- a/src/main/java/com/amazonaws/encryptionsdk/internal/BlockEncryptionHandler.java +++ b/src/main/java/com/amazonaws/encryptionsdk/internal/BlockEncryptionHandler.java @@ -211,4 +211,4 @@ private byte[] getNonce() { public boolean isComplete() { return complete_; } -} \ No newline at end of file +} diff --git a/src/main/java/com/amazonaws/encryptionsdk/internal/EncryptionContextSerializer.java b/src/main/java/com/amazonaws/encryptionsdk/internal/EncryptionContextSerializer.java index 614ecd08e..d78864b6f 100644 --- a/src/main/java/com/amazonaws/encryptionsdk/internal/EncryptionContextSerializer.java +++ b/src/main/java/com/amazonaws/encryptionsdk/internal/EncryptionContextSerializer.java @@ -203,4 +203,4 @@ public static Map deserialize(final byte[] b) { throw new AwsCryptoException("Invalid encryption context. Expected more bytes.", e); } } -} \ No newline at end of file +} diff --git a/src/main/java/com/amazonaws/encryptionsdk/internal/HmacKeyDerivationFunction.java b/src/main/java/com/amazonaws/encryptionsdk/internal/HmacKeyDerivationFunction.java index ede4d8c46..ca2d7cc8a 100644 --- a/src/main/java/com/amazonaws/encryptionsdk/internal/HmacKeyDerivationFunction.java +++ b/src/main/java/com/amazonaws/encryptionsdk/internal/HmacKeyDerivationFunction.java @@ -168,4 +168,4 @@ private void assertInitialized() throws IllegalStateException { throw new IllegalStateException("Hkdf has not been initialized"); } } -} \ No newline at end of file +} diff --git a/src/main/java/com/amazonaws/encryptionsdk/internal/VersionInfo.java b/src/main/java/com/amazonaws/encryptionsdk/internal/VersionInfo.java index 8bc000fdd..496568360 100644 --- a/src/main/java/com/amazonaws/encryptionsdk/internal/VersionInfo.java +++ b/src/main/java/com/amazonaws/encryptionsdk/internal/VersionInfo.java @@ -12,20 +12,25 @@ */ package com.amazonaws.encryptionsdk.internal; +import java.util.Properties; +import java.io.IOException; /** * This class specifies the versioning system for the AWS KMS encryption client. */ public class VersionInfo { - // incremented for major changes to the implementation - public static final String MAJOR_REVISION_NUM = "2"; - // incremented for minor changes to the implementation - public static final String MINOR_REVISION_NUM = "2"; - // incremented for releases containing an immediate bug fix. - public static final String BUGFIX_REVISION_NUM = "0"; - - public static final String RELEASE_VERSION = MAJOR_REVISION_NUM + "." + MINOR_REVISION_NUM - + "." + BUGFIX_REVISION_NUM; - - public static final String USER_AGENT = "AwsCrypto/" + RELEASE_VERSION; + public static final String USER_AGENT_PREFIX = "AwsCrypto/"; + public static final String UNKNOWN_VERSION = "unknown"; + /* + * Loads the version of the library + */ + public static String loadUserAgent() { + try { + final Properties properties = new Properties(); + properties.load(ClassLoader.getSystemResourceAsStream("project.properties")); + return USER_AGENT_PREFIX + properties.getProperty("version"); + } catch (final IOException ex) { + return USER_AGENT_PREFIX + UNKNOWN_VERSION; + } + } } diff --git a/src/main/java/com/amazonaws/encryptionsdk/internal/package-info.java b/src/main/java/com/amazonaws/encryptionsdk/internal/package-info.java index cca0a6c4b..46d3fe389 100644 --- a/src/main/java/com/amazonaws/encryptionsdk/internal/package-info.java +++ b/src/main/java/com/amazonaws/encryptionsdk/internal/package-info.java @@ -59,4 +59,4 @@ * * */ -package com.amazonaws.encryptionsdk.internal; \ No newline at end of file +package com.amazonaws.encryptionsdk.internal; diff --git a/src/main/java/com/amazonaws/encryptionsdk/kms/AwsKmsMrkAwareMasterKey.java b/src/main/java/com/amazonaws/encryptionsdk/kms/AwsKmsMrkAwareMasterKey.java new file mode 100644 index 000000000..b970dbcb4 --- /dev/null +++ b/src/main/java/com/amazonaws/encryptionsdk/kms/AwsKmsMrkAwareMasterKey.java @@ -0,0 +1,440 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.amazonaws.encryptionsdk.kms; + +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.function.Supplier; + +import com.amazonaws.AmazonServiceException; +import com.amazonaws.AmazonWebServiceRequest; +import com.amazonaws.encryptionsdk.*; +import com.amazonaws.encryptionsdk.exception.AwsCryptoException; +import com.amazonaws.encryptionsdk.internal.AwsKmsCmkArnInfo; +import com.amazonaws.encryptionsdk.internal.VersionInfo; +import com.amazonaws.services.kms.AWSKMS; +import com.amazonaws.services.kms.model.DecryptRequest; +import com.amazonaws.services.kms.model.DecryptResult; +import com.amazonaws.services.kms.model.EncryptRequest; +import com.amazonaws.services.kms.model.EncryptResult; +import com.amazonaws.services.kms.model.GenerateDataKeyRequest; +import com.amazonaws.services.kms.model.GenerateDataKeyResult; + +import static com.amazonaws.encryptionsdk.internal.AwsKmsCmkArnInfo.*; + + +//= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.5 +//# MUST implement the Master Key Interface (../master-key- +//# interface.md#interface) +// +//= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.7 +//# MUST be unchanged from the Master Key interface. +// +//= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.8 +//# MUST be unchanged from the Master Key interface. +/** + * Represents a single Aws KMS key + * and is used to encrypt/decrypt data with + * {@link AwsCrypto}. + * This key may be a multi region key, + * in which case this component + * is able to recognize + * different regional replicas + * of this multi region key as the same. + */ +public final class AwsKmsMrkAwareMasterKey extends MasterKey implements KmsMethods { + private static final String USER_AGENT = VersionInfo.loadUserAgent(); + private final AWSKMS kmsClient_; + private final List grantTokens_ = new ArrayList<>(); + private final String awsKmsIdentifier_; + private final MasterKeyProvider sourceProvider_; + + private static T updateUserAgent(T request) { + request.getRequestClientOptions().appendUserAgent(USER_AGENT); + + return request; + } + + /** + * A light builder method. + * + * @see KmsMasterKey#getInstance(Supplier, String, MasterKeyProvider) + * @param kms An AWS KMS Client + * @param awsKmsIdentifier An identifier for an AWS KMS key. May be a raw resource. + */ + static AwsKmsMrkAwareMasterKey getInstance( + final AWSKMS kms, + final String awsKmsIdentifier, + final MasterKeyProvider provider + ) { + return new AwsKmsMrkAwareMasterKey(awsKmsIdentifier, kms, provider); + } + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.6 + //# On initialization, the caller MUST provide: + private AwsKmsMrkAwareMasterKey( + final String awsKmsIdentifier, + final AWSKMS kmsClient, + final MasterKeyProvider provider + ) { + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.6 + //# The AWS KMS key identifier MUST NOT be null or empty. + // + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.6 + //# The AWS KMS + //# key identifier MUST be a valid identifier (aws-kms-key-arn.md#a- + //# valid-aws-kms-identifier). + validAwsKmsIdentifier(awsKmsIdentifier); + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.6 + //# The AWS KMS SDK client MUST not be null. + if (kmsClient == null) { + throw new IllegalArgumentException("AwsKmsMrkAwareMasterKey must be configured with an AWS KMS client."); + } + + /* Precondition: A provider is required. */ + if (provider == null) { + throw new IllegalArgumentException("AwsKmsMrkAwareMasterKey must be configured with a source provider."); + } + + kmsClient_ = kmsClient; + awsKmsIdentifier_ = awsKmsIdentifier; + sourceProvider_ = provider; + } + + @Override + public String getProviderId() { + return sourceProvider_.getDefaultProviderId(); + } + + @Override + public String getKeyId() { + return awsKmsIdentifier_; + } + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.6 + //# The master key MUST be able to be configured with an optional list of + //# Grant Tokens. + // + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.6 + //= type=exception + //# This configuration SHOULD be on initialization and + //# SHOULD be immutable. + // The existing KMS Master Key + // sets grants in this way, so we continue this interface. + /** + * Clears and sets all grant tokens on this instance. + * This is not thread safe. + */ + @Override + public void setGrantTokens(final List grantTokens) { + grantTokens_.clear(); + grantTokens_.addAll(grantTokens); + } + + @Override + public List getGrantTokens() { + return grantTokens_; + } + + @Override + public void addGrantToken(final String grantToken) { + grantTokens_.add(grantToken); + } + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.10 + //# The inputs MUST be the same as the Master Key Generate Data Key + //# (../master-key-interface.md#generate-data-key) interface. + /** + * This is identical behavior to + * @see KmsMasterKey#generateDataKey(CryptoAlgorithm, Map) + */ + @Override + public DataKey generateDataKey(final CryptoAlgorithm algorithm, + final Map encryptionContext) { + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.10 + //# This + //# master key MUST use the configured AWS KMS client to make an AWS KMS + //# GenerateDatakey (https://docs.aws.amazon.com/kms/latest/APIReference/ + //# API_GenerateDataKey.html) request constructed as follows: + final GenerateDataKeyResult gdkResult = kmsClient_.generateDataKey(updateUserAgent( + new GenerateDataKeyRequest() + .withKeyId(awsKmsIdentifier_) + .withNumberOfBytes(algorithm.getDataKeyLength()) + .withEncryptionContext(encryptionContext) + .withGrantTokens(grantTokens_) + )); + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.10 + //# If the call succeeds the AWS KMS Generate Data Key response's + //# "Plaintext" MUST match the key derivation input length specified by + //# the algorithm suite included in the input. + if (gdkResult.getPlaintext().limit() != algorithm.getDataKeyLength()) { + throw new IllegalStateException("Received an unexpected number of bytes from KMS"); + } + + final byte[] rawKey = new byte[algorithm.getDataKeyLength()]; + gdkResult.getPlaintext().get(rawKey); + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.10 + //# The response's "KeyId" + //# MUST be valid. + final String gdkResultKeyId = gdkResult.getKeyId(); + /* Exceptional Postcondition: Must have an AWS KMS ARN from AWS KMS generateDataKey. */ + if (parseInfoFromKeyArn(gdkResultKeyId) == null) { + throw new IllegalStateException("Received an empty or invalid keyId from KMS"); + } + + final byte[] encryptedKey = new byte[gdkResult.getCiphertextBlob().remaining()]; + gdkResult.getCiphertextBlob().get(encryptedKey); + + final SecretKeySpec key = new SecretKeySpec(rawKey, algorithm.getDataKeyAlgo()); + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.10 + //# The output MUST be the same as the Master Key Generate Data Key + //# (../master-key-interface.md#generate-data-key) interface. + return new DataKey<>( + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.10 + //# The response's "Plaintext" MUST be the plaintext in + //# the output. + key, + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.10 + //# The response's cipher text blob MUST be used as the + //# returned as the ciphertext for the encrypted data key in the output. + encryptedKey, + gdkResultKeyId.getBytes(StandardCharsets.UTF_8), + this + ); + } + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.11 + //# The inputs MUST be the same as the Master Key Encrypt Data Key + //# (../master-key-interface.md#encrypt-data-key) interface. + /** + * @see KmsMasterKey#encryptDataKey(CryptoAlgorithm, Map, DataKey) + */ + @Override + public DataKey encryptDataKey(final CryptoAlgorithm algorithm, + final Map encryptionContext, + final DataKey dataKey) { + final SecretKey key = dataKey.getKey(); + /* Precondition: The key format MUST be RAW. */ + if (!key.getFormat().equals("RAW")) { + throw new IllegalArgumentException("Only RAW encoded keys are supported"); + } + + try { + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.11 + //# The master + //# key MUST use the configured AWS KMS client to make an AWS KMS Encrypt + //# (https://docs.aws.amazon.com/kms/latest/APIReference/ + //# API_Encrypt.html) request constructed as follows: + final EncryptResult encryptResult = kmsClient_.encrypt(updateUserAgent( + new EncryptRequest() + .withKeyId(awsKmsIdentifier_) + .withPlaintext(ByteBuffer.wrap(key.getEncoded())) + .withEncryptionContext(encryptionContext) + .withGrantTokens(grantTokens_))); + + final byte[] edk = new byte[encryptResult.getCiphertextBlob().remaining()]; + encryptResult.getCiphertextBlob().get(edk); + final String encryptResultKeyId = encryptResult.getKeyId(); + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.11 + //# The AWS KMS Encrypt response MUST contain a valid "KeyId". + /* Postcondition: Must have an AWS KMS ARN from AWS KMS encrypt. */ + if (parseInfoFromKeyArn(encryptResultKeyId) == null) { + throw new IllegalStateException("Received an empty or invalid keyId from KMS"); + } + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.11 + //# The output MUST be the same as the Master Key Encrypt Data Key + //# (../master-key-interface.md#encrypt-data-key) interface. + return new DataKey<>( + dataKey.getKey(), + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.11 + //# The + //# response's cipher text blob MUST be used as the "ciphertext" for the + //# encrypted data key. + edk, + encryptResultKeyId.getBytes(StandardCharsets.UTF_8), + this + ); + } catch (final AmazonServiceException asex) { + throw new AwsCryptoException(asex); + } + } + + /** + * Will attempt to decrypt if awsKmsArnMatchForDecrypt returns true in + * {@link AwsKmsMrkAwareMasterKey#filterEncryptedDataKeys(String, AwsKmsCmkArnInfo, EncryptedDataKey)}. + * An extension of + * {@link KmsMasterKey#decryptDataKey(CryptoAlgorithm, Collection, Map)} + * but with an awareness of the properties of multi-Region keys. + */ + @Override + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.9 + //# The inputs MUST be the same as the Master Key Decrypt Data Key + //# (../master-key-interface.md#decrypt-data-key) interface. + public DataKey decryptDataKey( + final CryptoAlgorithm algorithm, + final Collection encryptedDataKeys, + final Map encryptionContext + ) throws AwsCryptoException { + final List exceptions = new ArrayList<>(); + final String providerId = this.getProviderId(); + + return encryptedDataKeys + .stream() + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.9 + //# The set of encrypted data keys MUST first be filtered to match this + //# master key's configuration. + .filter(edk -> filterEncryptedDataKeys(providerId, awsKmsIdentifier_, edk)) + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.9 + //# For each encrypted data key in the filtered set, one at a time, the + //# master key MUST attempt to decrypt the data key. + .map(edk -> { + try { + return decryptSingleEncryptedDataKey( + this, + kmsClient_, + awsKmsIdentifier_, + grantTokens_, + algorithm, + edk, + encryptionContext + ); + } catch (final AmazonServiceException amazonServiceException) { + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.9 + //# If this attempt + //# results in an error, then these errors MUST be collected. + exceptions.add(amazonServiceException); + } + return null; + }) + /* Need to filter null + * because an Optional + * of a null is crazy. + * Therefore `findFirst` will throw + * if it sees `null`. + */ + .filter(Objects::nonNull) + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.9 + //# If the AWS KMS response satisfies the requirements then it MUST be + //# use and this function MUST return and not attempt to decrypt any more + //# encrypted data keys. + /* Order is important. + * Process the encrypted data keys in the order they exist in the encrypted message. + */ + .findFirst() + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.9 + //# If all the input encrypted data keys have been processed then this + //# function MUST yield an error that includes all the collected errors. + // + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.9 + //# The output MUST be the same as the Master Key Decrypt Data Key + //# (../master-key-interface.md#decrypt-data-key) interface. + /* Exceptional Postcondition: Master key was unable to decrypt. */ + .orElseThrow(() -> buildCannotDecryptDksException(exceptions)); + } + + /** + * Pure function for decrypting and encrypted data key. + * This is refactored out of `decryptDataKey` + * to facilitate testing to ensure correctness. + * + */ + static DataKey decryptSingleEncryptedDataKey( + final AwsKmsMrkAwareMasterKey masterKey, + final AWSKMS client, + final String awsKmsIdentifier, + final List grantTokens, + final CryptoAlgorithm algorithm, + final EncryptedDataKey edk, + final Map encryptionContext + ) { + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.9 + //# To decrypt the encrypted data key this master key MUST use the + //# configured AWS KMS client to make an AWS KMS Decrypt + //# (https://docs.aws.amazon.com/kms/latest/APIReference/ + //# API_Decrypt.html) request constructed as follows: + final DecryptResult decryptResult = client.decrypt(updateUserAgent( + new DecryptRequest() + .withCiphertextBlob(ByteBuffer.wrap(edk.getEncryptedDataKey())) + .withEncryptionContext(encryptionContext) + .withGrantTokens(grantTokens) + .withKeyId(awsKmsIdentifier))); + + final String decryptResultKeyId = decryptResult.getKeyId(); + /* Exceptional Postcondition: Must have a CMK ARN from AWS KMS to match. */ + if (decryptResultKeyId == null) { + throw new IllegalStateException("Received an empty keyId from KMS"); + } + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.9 + //# If the call succeeds then the response's "KeyId" MUST be equal to the + //# configured AWS KMS key identifier otherwise the function MUST collect + //# an error. + if (!awsKmsIdentifier.equals(decryptResultKeyId)) { + throw new IllegalStateException("Received an invalid response from KMS Decrypt call: Unexpected keyId."); + } + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.9 + //# The response's "Plaintext"'s length MUST equal the length + //# required by the requested algorithm suite otherwise the function MUST + //# collect an error. + if (decryptResult.getPlaintext().limit() != algorithm.getDataKeyLength()) { + throw new IllegalStateException("Received an unexpected number of bytes from KMS"); + } + + final byte[] rawKey = new byte[algorithm.getDataKeyLength()]; + decryptResult.getPlaintext().get(rawKey); + + return new DataKey<>( + new SecretKeySpec(rawKey, algorithm.getDataKeyAlgo()), + edk.getEncryptedDataKey(), + edk.getProviderInformation(), + masterKey); + } + + /** + * A pure function to filter encrypted data keys. + * This function is refactored out from `decryptDataKey` + * to facilitate testing and ensure correctness. + * + * An AWS KMS Master key should only attempt + * to process an Encrypted Data Key + * if the information in the Encrypted Data Key + * matches the master keys configuration. + * + */ + static boolean filterEncryptedDataKeys ( + final String providerId, + final String awsKmsIdentifier_, + final EncryptedDataKey edk + ) { + final String edkKeyId = new String(edk.getProviderInformation(), StandardCharsets.UTF_8); + + final AwsKmsCmkArnInfo providerArnInfo = parseInfoFromKeyArn(edkKeyId); + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.9 + //# Additionally each provider info MUST be a valid AWS KMS ARN + //# (aws-kms-key-arn.md#a-valid-aws-kms-arn) with a resource type of + //# "key". + if (providerArnInfo == null || !"key".equals(providerArnInfo.getResourceType())) { + throw new IllegalStateException("Invalid provider info in message."); + } + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.9 + //# To match the encrypted data key's + //# provider ID MUST exactly match the value "aws-kms" and the the + //# function AWS KMS MRK Match for Decrypt (aws-kms-mrk-match-for- + //# decrypt.md#implementation) called with the configured AWS KMS key + //# identifier and the encrypted data key's provider info MUST return + //# "true". + return edk.getProviderId().equals(providerId) && + awsKmsArnMatchForDecrypt(awsKmsIdentifier_, edkKeyId); + } +} diff --git a/src/main/java/com/amazonaws/encryptionsdk/kms/AwsKmsMrkAwareMasterKeyProvider.java b/src/main/java/com/amazonaws/encryptionsdk/kms/AwsKmsMrkAwareMasterKeyProvider.java new file mode 100644 index 000000000..8c0e2874f --- /dev/null +++ b/src/main/java/com/amazonaws/encryptionsdk/kms/AwsKmsMrkAwareMasterKeyProvider.java @@ -0,0 +1,831 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.amazonaws.encryptionsdk.kms; + +import com.amazonaws.SdkClientException; +import com.amazonaws.auth.AWSCredentials; +import com.amazonaws.auth.AWSCredentialsProvider; +import com.amazonaws.auth.AWSStaticCredentialsProvider; +import com.amazonaws.encryptionsdk.*; +import com.amazonaws.encryptionsdk.exception.AwsCryptoException; +import com.amazonaws.encryptionsdk.exception.NoSuchMasterKeyException; +import com.amazonaws.encryptionsdk.exception.UnsupportedProviderException; +import com.amazonaws.encryptionsdk.internal.AwsKmsCmkArnInfo; +import com.amazonaws.handlers.RequestHandler2; +import com.amazonaws.services.kms.AWSKMS; +import com.amazonaws.services.kms.AWSKMSClient; +import com.amazonaws.services.kms.AWSKMSClientBuilder; + +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +import static com.amazonaws.encryptionsdk.internal.AwsKmsCmkArnInfo.*; +import static com.amazonaws.encryptionsdk.internal.AwsKmsCmkArnInfo.parseInfoFromKeyArn; +import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; + +//= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.5 +//# MUST implement the Master Key Provider Interface (../master-key- +//# provider-interface.md#interface) +/** + * Represents a list Aws KMS keys + * and is used to encrypt/decrypt data with + * {@link AwsCrypto}. + * Some of these keys may be multi region keys, + * in which case this component + * is able to recognize + * different regional replicas + * of this multi region key as the same. + */ +public final class AwsKmsMrkAwareMasterKeyProvider extends MasterKeyProvider { + private static final String PROVIDER_NAME = "aws-kms"; + private final List keyIds_; + private final List grantTokens_; + + private final boolean isDiscovery_; + private final DiscoveryFilter discoveryFilter_; + private final String discoveryMrkRegion_; + + private final KmsMasterKeyProvider.RegionalClientSupplier regionalClientSupplier_; + private final String defaultRegion_; + + public static class Builder implements Cloneable { + private String defaultRegion_ = getSdkDefaultRegion(); + private Optional regionalClientSupplier_ = Optional.empty(); + private AWSKMSClientBuilder templateBuilder_ = null; + private DiscoveryFilter discoveryFilter_ = null; + private String discoveryMrkRegion_ = this.defaultRegion_; + + Builder() { + // Default access: Don't allow outside classes to extend this class + } + + public Builder clone() { + try { + AwsKmsMrkAwareMasterKeyProvider.Builder cloned = (AwsKmsMrkAwareMasterKeyProvider.Builder) super.clone(); + + if (templateBuilder_ != null) { + cloned.templateBuilder_ = cloneClientBuilder(templateBuilder_); + } + + return cloned; + } catch (CloneNotSupportedException e) { + throw new Error("Impossible: CloneNotSupportedException", e); + } + } + + /** + * Sets the default region. + * This region will be used when specifying key IDs for encryption or in + * {@link AwsKmsMrkAwareMasterKeyProvider#getMasterKey(String)} that are not full ARNs, but are instead bare key IDs or + * aliases. + *

+ * If the default region is not specified, + * the AWS SDK default region will be used. + * @see KmsMasterKeyProvider.Builder#withDefaultRegion(String) + * @param defaultRegion The default region to use. + */ + public AwsKmsMrkAwareMasterKeyProvider.Builder withDefaultRegion(String defaultRegion) { + this.defaultRegion_ = defaultRegion; + return this; + } + + /** + * Sets the region contacted for multi-region keys + * when in Discovery mode. + * This region will be used when a multi-region key is discovered + * on decrypt by {@link AwsKmsMrkAwareMasterKeyProvider#getMasterKey(String)}. + *

+ * + * @param discoveryMrkRegion The region to contact to attempt to decrypt multi-region keys. + */ + public AwsKmsMrkAwareMasterKeyProvider.Builder withDiscoveryMrkRegion(String discoveryMrkRegion) { + this.discoveryMrkRegion_ = discoveryMrkRegion; + return this; + } + + /** + * Provides a custom factory function that will vend KMS clients. This is provided for advanced use cases which + * require complete control over the client construction process. + *

+ * Because the regional client supplier fully controls the client construction process, it is not possible to + * configure the client through methods such as {@link #withCredentials(AWSCredentialsProvider)} or + * {@link #withClientBuilder(AWSKMSClientBuilder)}; if you try to use these in combination, an + * {@link IllegalStateException} will be thrown. + * + * @see KmsMasterKeyProvider.Builder#withCustomClientFactory(KmsMasterKeyProvider.RegionalClientSupplier) + */ + public AwsKmsMrkAwareMasterKeyProvider.Builder withCustomClientFactory(KmsMasterKeyProvider.RegionalClientSupplier regionalClientSupplier) { + if (templateBuilder_ != null) { + throw clientSupplierComboException(); + } + + regionalClientSupplier_ = Optional.of(regionalClientSupplier); + return this; + } + + private RuntimeException clientSupplierComboException() { + return new IllegalStateException("withCustomClientFactory cannot be used in conjunction with " + + "withCredentials or withClientBuilder"); + } + + /** + * Configures the {@link AwsKmsMrkAwareMasterKeyProvider} to use specific credentials. If a builder was previously set, + * this will override whatever credentials it set. + * + * @see KmsMasterKeyProvider.Builder#withCredentials(AWSCredentialsProvider) + */ + public AwsKmsMrkAwareMasterKeyProvider.Builder withCredentials(AWSCredentialsProvider credentialsProvider) { + if (regionalClientSupplier_.isPresent()) { + throw clientSupplierComboException(); + } + + if (templateBuilder_ == null) { + templateBuilder_ = AWSKMSClientBuilder.standard(); + } + + templateBuilder_.setCredentials(credentialsProvider); + + return this; + } + + /** + * Configures the {@link AwsKmsMrkAwareMasterKeyProvider} to use specific credentials. If a builder was previously set, + * this will override whatever credentials it set. + * + * @see KmsMasterKeyProvider.Builder#withCredentials(AWSCredentials) + */ + public AwsKmsMrkAwareMasterKeyProvider.Builder withCredentials(AWSCredentials credentials) { + return withCredentials(new AWSStaticCredentialsProvider(credentials)); + } + + /** + * Configures the {@link AwsKmsMrkAwareMasterKeyProvider} to use settings from this {@link AWSKMSClientBuilder} to + * configure KMS clients. Note that the region set on this builder will be ignored, but all other settings + * will be propagated into the regional clients. + *

+ * This method will overwrite any credentials set using {@link #withCredentials(AWSCredentialsProvider)}. + * + * @see KmsMasterKeyProvider.Builder#withClientBuilder(AWSKMSClientBuilder) + */ + public AwsKmsMrkAwareMasterKeyProvider.Builder withClientBuilder(AWSKMSClientBuilder builder) { + if (regionalClientSupplier_.isPresent()) { + throw clientSupplierComboException(); + } + final AWSKMSClientBuilder newBuilder = cloneClientBuilder(builder); + this.templateBuilder_ = newBuilder; + + return this; + } + + /** + * Builds the master key provider in Discovery Mode. + * In Discovery Mode the KMS Master Key Provider will attempt to decrypt using any + * key identifier it discovers in the encrypted message. + * KMS Master Key Providers in Discovery Mode will not encrypt data keys. + * + * @see KmsMasterKeyProvider.Builder#buildDiscovery() + * + */ + public AwsKmsMrkAwareMasterKeyProvider buildDiscovery() { + final boolean isDiscovery = true; + + return new AwsKmsMrkAwareMasterKeyProvider( + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.6 + //# The regional client + //# supplier MUST be defined in discovery mode. + regionalClientSupplier_.orElse(clientFactory(new ConcurrentHashMap<>(), templateBuilder_)), + defaultRegion_, + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.6 + //# The key id list MUST be empty in discovery mode. + emptyList(), + emptyList(), + isDiscovery, + discoveryFilter_, + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.6 + //# In + //# discovery mode if a default MRK Region is not configured the AWS SDK + //# Default Region MUST be used. + discoveryMrkRegion_ == null ? defaultRegion_ : discoveryMrkRegion_ + ); + } + + /** + * Builds the master key provider in Discovery Mode with a {@link DiscoveryFilter}. + * In Discovery Mode the KMS Master Key Provider will attempt to decrypt using any + * key identifier it discovers in the encrypted message that is accepted by the + * {@code filter}. + * KMS Master Key Providers in Discovery Mode will not encrypt data keys. + * + * @see KmsMasterKeyProvider.Builder#buildDiscovery(DiscoveryFilter) + */ + public AwsKmsMrkAwareMasterKeyProvider buildDiscovery(DiscoveryFilter filter) { + discoveryFilter_ = filter; + + return buildDiscovery(); + } + + /** + * Builds the master key provider in Strict Mode. + * KMS Master Key Providers in Strict Mode will only attempt to decrypt using + * key ARNs listed in {@code keyIds}. + * KMS Master Key Providers in Strict Mode will encrypt data keys using the keys + * listed in {@code keyIds} + *

+ * In Strict Mode, one or more CMKs must be provided. + * For Master Key Providers that will only be used for encryption, + * you can use any valid KMS key identifier. + * For providers that will be used for decryption, + * you must use the key ARN; + * key ids, alias names, and alias ARNs are not supported. + * + * @see KmsMasterKeyProvider.Builder#buildStrict(List) + */ + public AwsKmsMrkAwareMasterKeyProvider buildStrict(List keyIds) { + final boolean isDiscovery = false; + + return new AwsKmsMrkAwareMasterKeyProvider( + regionalClientSupplier_.orElse(clientFactory(new ConcurrentHashMap<>(), templateBuilder_)), + defaultRegion_, + new ArrayList(keyIds), + emptyList(), + isDiscovery, + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.6 + //# A discovery filter MUST NOT be configured in strict mode. + null, + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.6 + //# A default MRK Region MUST NOT be configured in strict mode. + null + ); + } + + /** + * Builds the master key provider in strict mode. + * KMS Master Key Providers in Strict Mode will only attempt to decrypt using + * key ARNs listed in {@code keyIds}. + * KMS Master Key Providers in Strict Mode will encrypt data keys using the keys + * listed in {@code keyIds} + *

+ * In Strict Mode, one or more CMKs must be provided. + * For Master Key Providers that will only be used for encryption, + * you can use any valid KMS key identifier. + * For providers that will be used for decryption, + * you must use the key ARN; + * key ids, alias names, and alias ARNs are not supported. + * + * @see KmsMasterKeyProvider.Builder#buildStrict(String...) + * + */ + public AwsKmsMrkAwareMasterKeyProvider buildStrict(String... keyIds) { + return buildStrict(asList(keyIds)); + } + + static KmsMasterKeyProvider.RegionalClientSupplier clientFactory( + ConcurrentHashMap clientCache, + AWSKMSClientBuilder templateBuilder + ) { + + // Clone again; this MKP builder might be reused to build a second MKP with different creds. + AWSKMSClientBuilder builder = templateBuilder != null ? cloneClientBuilder(templateBuilder) + : AWSKMSClientBuilder.standard(); + + return region -> { + /* Check for early return (Postcondition): If a client already exists, use that. */ + if (clientCache.containsKey(region)) { + return clientCache.get(region); + } + + // We can't just use computeIfAbsent as we need to avoid leaking KMS clients if we're asked to decrypt + // an EDK with a bogus region in its ARN. So we'll install a request handler to identify the first + // successful call, and cache it when we see that. + final KmsMasterKeyProvider.SuccessfulRequestCacher cacher = new KmsMasterKeyProvider.SuccessfulRequestCacher(clientCache, region); + final ArrayList handlers = new ArrayList<>(); + if (builder.getRequestHandlers() != null) { + handlers.addAll(builder.getRequestHandlers()); + } + handlers.add(cacher); + + final AWSKMS kms = cloneClientBuilder(builder) + .withRegion(region) + .withRequestHandlers(handlers.toArray(new RequestHandler2[handlers.size()])) + .build(); + return cacher.setClient(kms); + }; + } + + static AWSKMSClientBuilder cloneClientBuilder(final AWSKMSClientBuilder builder) { + // We need to copy all arguments out of the builder in case it's mutated later on. + // Unfortunately AWSKMSClientBuilder doesn't support .clone() so we'll have to do it by hand. + + if (builder.getEndpoint() != null) { + // We won't be able to set the region later if a custom endpoint is set. + throw new IllegalArgumentException("Setting endpoint configuration is not compatible with passing a " + + "builder to the KmsMasterKeyProvider. Use withCustomClientFactory" + + " instead."); + } + + final AWSKMSClientBuilder newBuilder = AWSKMSClient.builder(); + newBuilder.setClientConfiguration(builder.getClientConfiguration()); + newBuilder.setCredentials(builder.getCredentials()); + newBuilder.setEndpointConfiguration(builder.getEndpoint()); + newBuilder.setMetricsCollector(builder.getMetricsCollector()); + if (builder.getRequestHandlers() != null) { + newBuilder.setRequestHandlers(builder.getRequestHandlers().toArray(new RequestHandler2[0])); + } + return newBuilder; + } + + /** + * The AWS SDK has a default process for evaluating the default Region. + * This returns null if no default region is found. + * Because a default region _may_ not be needed. + * + */ + private static String getSdkDefaultRegion() { + try { + return new com.amazonaws.regions.DefaultAwsRegionProviderChain().getRegion(); + } catch (SdkClientException ex) { + return null; + } + } + } + + public static AwsKmsMrkAwareMasterKeyProvider.Builder builder() { + return new AwsKmsMrkAwareMasterKeyProvider.Builder(); + } + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.6 + //# On initialization the caller MUST provide: + private AwsKmsMrkAwareMasterKeyProvider( + + KmsMasterKeyProvider.RegionalClientSupplier supplier, + String defaultRegion, + List keyIds, + List grantTokens, + boolean isDiscovery, + DiscoveryFilter discoveryFilter, + String discoveryMrkRegion + ) { + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.6 + //# The key id list MUST NOT be empty or null in strict mode. + if (!isDiscovery && (keyIds == null || keyIds.isEmpty())) { + throw new IllegalArgumentException("Strict mode must be configured with a non-empty " + + "list of keyIds."); + } + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.6 + //# The key id + //# list MUST NOT contain any null or empty string values. + if (!isDiscovery && (keyIds.contains(null) || keyIds.contains(""))) { + throw new IllegalArgumentException("Strict mode cannot be configured with a " + + "null key identifier."); + } + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.6 + //# All AWS KMS + //# key identifiers are be passed to Assert AWS KMS MRK are unique (aws- + //# kms-mrk-are-unique.md#Implementation) and the function MUST return + //# success. + assertMrksAreUnique(keyIds); + /* Precondition: A region is required to contact AWS KMS. + * This is an edge case because the default region will be the same as the SDK default, + * but it is still possible. + */ + if ( + !isDiscovery && + defaultRegion == null && + keyIds + .stream() + .map(identifier -> parseInfoFromKeyArn(identifier)) + .anyMatch(info -> info == null) + ) { + throw new AwsCryptoException("Can't use non-ARN key identifiers or aliases when " + + "no default region is set"); + } + /* Precondition (untested): Discovery filter is only valid in discovery mode. */ + if (!isDiscovery && discoveryFilter != null) { + throw new IllegalArgumentException("Strict mode cannot be configured with a " + + "discovery filter."); + } + /* Precondition (untested): Discovery mode can not have any keys to filter. */ + if (isDiscovery && !keyIds.isEmpty()) { + throw new IllegalArgumentException("Discovery mode can not be configured with keys."); + } + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.6 + //# If an AWS SDK Default Region can not be + //# obtained initialization MUST fail. + if (isDiscovery && discoveryMrkRegion == null) { + throw new IllegalArgumentException("Discovery MRK region can not be null."); + } + + this.regionalClientSupplier_ = supplier; + this.defaultRegion_ = defaultRegion; + this.keyIds_ = Collections.unmodifiableList(new ArrayList<>(keyIds)); + + this.isDiscovery_ = isDiscovery; + this.discoveryFilter_ = discoveryFilter; + this.discoveryMrkRegion_ = discoveryMrkRegion; + this.grantTokens_ = grantTokens; + } + + //= compliance/framework/aws-kms/aws-kms-mrk-are-unique.txt#2.5 + //# The caller MUST provide: + /** + * Refactored into a pure function + * to facilitate testing and correctness. + * + */ + static void assertMrksAreUnique(List keyIdentifiers) { + + List duplicateMultiRegionKeyIdentifiers = keyIdentifiers + .stream() + /* Collect a map of resource to identifier. + * This lets me group duplicates by "resource". + * This is because the identifier can be either an ARN or a raw identifier. + * By having the both the key id and the identifier I can ensure the uniqueness of + * the key id and the error message to the caller can contain both identifiers + * to facilitate debugging. + */ + .collect(Collectors.groupingBy(AwsKmsMrkAwareMasterKeyProvider::getResourceForResourceTypeKey)) + .entrySet() + .stream() + //= compliance/framework/aws-kms/aws-kms-mrk-are-unique.txt#2.5 + //# If there are zero duplicate resource ids between the multi-region + //# keys, this function MUST exit successfully + .filter(maybeDuplicate -> maybeDuplicate.getValue().size() > 1) + //= compliance/framework/aws-kms/aws-kms-mrk-are-unique.txt#2.5 + //# If the list does not contain any multi-Region keys (aws-kms-key- + //# arn.md#identifying-an-aws-kms-multi-region-key) this function MUST + //# exit successfully. + // + /* Postcondition: Filter out duplicate resources that are not multi-region keys. + * I expect only have duplicates of specific multi-region keys. + * In JSON something like + * { + * "mrk-edb7fe6942894d32ac46dbb1c922d574" : [ + * "arn:aws:kms:us-west-2:111122223333:key/mrk-edb7fe6942894d32ac46dbb1c922d574", + * "arn:aws:kms:us-east-2:111122223333:key/mrk-edb7fe6942894d32ac46dbb1c922d574" + * ] + * } + */ + .filter(maybeMrk -> isMRK(maybeMrk.getKey())) + /* Flatten the duplicate identifiers into a single list. */ + .flatMap(mrkEntry -> mrkEntry.getValue().stream()) + .collect(Collectors.toList()); + + //= compliance/framework/aws-kms/aws-kms-mrk-are-unique.txt#2.5 + //# If any duplicate multi-region resource ids exist, this function MUST + //# yield an error that includes all identifiers with duplicate resource + //# ids not only the first duplicate found. + if (duplicateMultiRegionKeyIdentifiers.size() > 1) { + throw new IllegalArgumentException("Duplicate multi-region keys are not allowed:\n" + + String.join(", ", duplicateMultiRegionKeyIdentifiers)); + } + } + + /** + * Helper method for + * @see AwsKmsMrkAwareMasterKeyProvider#assertMrksAreUnique(List) + * + * Refoactored into a pure function + * to simplify testing and ensure correctness. + * + */ + static String getResourceForResourceTypeKey(String identifier) { + final AwsKmsCmkArnInfo info = parseInfoFromKeyArn(identifier); + /* Check for early return (Postcondition): Non-ARNs may be raw resources. + * Raw aliases ('alias/my-key') + * or key ids ('mrk-edb7fe6942894d32ac46dbb1c922d574'). + */ + if (info == null) return identifier; + + /* Check for early return (Postcondition): Return the identifier for non-key resource types. + * I only care about duplicate multi-region *keys*. + * Any other resource type + * should get filtered out. + * I return the entire identifier + * on the off chance that + * a customer has created + * an alias with a name `mrk-*`. + * This way such an alias + * can never accidentally + * collided with an existing multi-region key + * or a duplicate alias. + */ + if (!info.getResourceType().equals("key")) { + return identifier; + } + + /* Postcondition: Return the key id. + * This will be used + * to find different regional replicas of + * the same multi-region key + * because the key id for replicas is always the same. + */ + return info.getResource(); + } + + /** + * Returns "aws-kms" + */ + @Override + public String getDefaultProviderId() { + return PROVIDER_NAME; + } + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.7 + //# The input MUST be the same as the Master Key Provider Get Master Key + //# (../master-key-provider-interface.md#get-master-key) interface. + /** + * Added flexibility in matching multi-Region keys from different regions. + * + * @see KmsMasterKey#getMasterKey(String, String) + */ + @Override + public AwsKmsMrkAwareMasterKey getMasterKey( + final String providerId, + final String requestedKeyArn + ) throws UnsupportedProviderException, NoSuchMasterKeyException { + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.7 + //# The function MUST only provide master keys if the input provider id + //# equals "aws-kms". + if (!canProvide(providerId)) { + throw new UnsupportedProviderException(); + } + + /* There SHOULD only be one match. + * An unambiguous multi-region key for the family + * of related multi-region keys is required. + * See `assertMrksAreUnique`. + * However, in the case of single region keys or aliases, + * duplicates _are_ possible. + */ + Optional matchedArn = keyIds_ + .stream() + .filter(t -> awsKmsArnMatchForDecrypt(t, requestedKeyArn)) + .findFirst(); + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.7 + //# In strict mode, the requested AWS KMS key ARN MUST + //# match a member of the configured key ids by using AWS KMS MRK Match + //# for Decrypt (aws-kms-mrk-match-for-decrypt.md#implementation) + //# otherwise this function MUST error. + if (!isDiscovery_ && !matchedArn.isPresent()) { + throw new NoSuchMasterKeyException("Key must be in supplied list of keyIds."); + } + + final AwsKmsCmkArnInfo requestedKeyArnInfo = parseInfoFromKeyArn(requestedKeyArn); + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.7 + //# In discovery mode, the requested + //# AWS KMS key identifier MUST be a well formed AWS KMS ARN. + /* Precondition: Discovery mode requires requestedKeyArn be an ARN. + * This function is called on the encrypt path. + * It _may_ be the case that a raw key id, for example, was configured. + */ + if (isDiscovery_ && requestedKeyArnInfo == null) { + throw new NoSuchMasterKeyException("Cannot use AWS KMS identifiers " + + "when in discovery mode."); + } + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.7 + //# In + //# discovery mode if a discovery filter is configured the requested AWS + //# KMS key ARN's "partition" MUST match the discovery filter's + //# "partition" and the AWS KMS key ARN's "account" MUST exist in the + //# discovery filter's account id set. + if (isDiscovery_ && discoveryFilter_ != null && + !discoveryFilter_.allowsPartitionAndAccount(requestedKeyArnInfo.getPartition(), requestedKeyArnInfo.getAccountId()) + ) { + throw new NoSuchMasterKeyException("Cannot use key in partition " + requestedKeyArnInfo.getPartition() + + " with account id " + requestedKeyArnInfo.getAccountId() + " with configured discovery filter."); + } + + final String regionName_ = extractRegion( + defaultRegion_, + discoveryMrkRegion_, + matchedArn, + requestedKeyArnInfo, + isDiscovery_ + ); + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.7 + //# An AWS KMS client + //# MUST be obtained by calling the regional client supplier with this + //# AWS Region. + AWSKMS kms = regionalClientSupplier_.getClient(regionName_); + + String keyIdentifier = isDiscovery_ + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.7 + //# In discovery mode a AWS KMS MRK Aware Master Key (aws-kms-mrk-aware- + //# master-key.md) MUST be returned configured with + ? requestedKeyArnInfo.toString(regionName_) + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.7 + //# In strict mode a AWS KMS MRK Aware Master Key (aws-kms-mrk-aware- + //# master-key.md) MUST be returned configured with + : matchedArn.get(); + + final AwsKmsMrkAwareMasterKey result = AwsKmsMrkAwareMasterKey + .getInstance(kms, keyIdentifier, this); + result.setGrantTokens(grantTokens_); + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.7 + //# The output MUST be the same as the Master Key Provider Get Master Key + //# (../master-key-provider-interface.md#get-master-key) interface. + return result; + } + + /** + * Select the correct region from multiple default configurations + * and potentially related multi-Region keys from different regions. + * + * Refactored into a pure function to facilitate testing and ensure correctness. + * + */ + static String extractRegion( + final String defaultRegion, + final String discoveryMrkRegion, + final Optional matchedArn, + final AwsKmsCmkArnInfo requestedKeyArnInfo, + final boolean isDiscovery + ) { + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.7 + //# If the requested AWS KMS key identifier is not a well formed ARN the + //# AWS Region MUST be the configured default region this SHOULD be + //# obtained from the AWS SDK. + if (requestedKeyArnInfo == null) return defaultRegion; + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.7 + //# Otherwise if the requested AWS KMS key + //# identifier is identified as a multi-Region key (aws-kms-key- + //# arn.md#identifying-an-aws-kms-multi-region-key), then AWS Region MUST + //# be the region from the AWS KMS key ARN stored in the provider info + //# from the encrypted data key. + if ( + !isMRK(requestedKeyArnInfo.getResource()) || + !requestedKeyArnInfo.getResourceType().equals("key") + ) { + return requestedKeyArnInfo.getRegion(); + } + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.7 + //# Otherwise if the mode is discovery then + //# the AWS Region MUST be the discovery MRK region. + if (isDiscovery) return discoveryMrkRegion; + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.7 + //# Finally if the + //# provider info is identified as a multi-Region key (aws-kms-key- + //# arn.md#identifying-an-aws-kms-multi-region-key) the AWS Region MUST + //# be the region from the AWS KMS key in the configured key ids matched + //# to the requested AWS KMS key by using AWS KMS MRK Match for Decrypt + //# (aws-kms-mrk-match-for-decrypt.md#implementation). + return parseInfoFromKeyArn(matchedArn.get()).getRegion(); + } + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.8 + //# The input MUST be the same as the Master Key Provider Get Master Keys + //# For Encryption (../master-key-provider-interface.md#get-master-keys- + //# for-encryption) interface. + /** + * Returns all CMKs provided to the constructor of this object. + * @see KmsMasterKey#getMasterKeysForEncryption(MasterKeyRequest) + */ + @Override + public List getMasterKeysForEncryption(final MasterKeyRequest request) { + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.8 + //# If the configured mode is discovery the function MUST return an empty + //# list. + if (isDiscovery_) { + return emptyList(); + } + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.8 + //# If the configured mode is strict this function MUST return a + //# list of master keys obtained by calling Get Master Key (aws-kms-mrk- + //# aware-master-key-provider.md#get-master-key) for each AWS KMS key + //# identifier in the configured key ids + List result = new ArrayList<>(keyIds_.size()); + for (String id : keyIds_) { + result.add(getMasterKey(id)); + } + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.8 + //# The output MUST be the same as the Master Key Provider Get Master + //# Keys For Encryption (../master-key-provider-interface.md#get-master- + //# keys-for-encryption) interface. + return result; + } + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.9 + //# The input MUST be the same as the Master Key Provider Decrypt Data + //# Key (../master-key-provider-interface.md#decrypt-data-key) interface. + /** + * @see KmsMasterKey#decryptDataKey(CryptoAlgorithm, Collection, Map) + * @throws AwsCryptoException + */ + @Override + public DataKey decryptDataKey(final CryptoAlgorithm algorithm, + final Collection encryptedDataKeys, + final Map encryptionContext) + throws AwsCryptoException { + final List exceptions = new ArrayList<>(); + + return encryptedDataKeys + .stream() + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.9 + //# The set of encrypted data keys MUST first be filtered to match this + //# master key's configuration. + .filter(edk -> { + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.9 + //# To match the encrypted data key's + //# provider ID MUST exactly match the value "aws-kms". + if (!canProvide(edk.getProviderId())) return false; + + final String providerInfo = new String(edk.getProviderInformation(), StandardCharsets.UTF_8); + final AwsKmsCmkArnInfo providerArnInfo = parseInfoFromKeyArn(providerInfo); + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.9 + //# Additionally + //# each provider info MUST be a valid AWS KMS ARN (aws-kms-key-arn.md#a- + //# valid-aws-kms-arn) with a resource type of "key". + if (providerArnInfo == null || !"key".equals(providerArnInfo.getResourceType())) { + throw new IllegalStateException("Invalid provider info in message."); + } + return true; + }) + .map(edk -> { + try { + final String keyArn = new String(edk.getProviderInformation(), StandardCharsets.UTF_8); + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.9 + //# For each encrypted data key in the filtered set, one at a time, the + //# master key provider MUST call Get Master Key (aws-kms-mrk-aware- + //# master-key-provider.md#get-master-key) with the encrypted data key's + //# provider info as the AWS KMS key ARN. + // This will throw if we can't use this key for whatever reason + return getMasterKey( + edk.getProviderId(), + keyArn) + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.9 + //# It MUST call Decrypt Data Key + //# (aws-kms-mrk-aware-master-key.md#decrypt-data-key) on this master key + //# with the input algorithm, this single encrypted data key, and the + //# input encryption context. + .decryptDataKey(algorithm, singletonList(edk), encryptionContext); + } catch (final Exception ex) { + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.9 + //# If this attempt results in an error, then + //# these errors MUST be collected. + exceptions.add(ex); + return null; + } + }) + /* Need to filter null because an Optional of a null is crazy. + * `findFirst` will throw if it sees `null`. + */ + .filter(Objects::nonNull) + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.9 + //# If the decrypt data key call is + //# successful, then this function MUST return this result and not + //# attempt to decrypt any more encrypted data keys. + .findFirst() + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.9 + //# If all the input encrypted data keys have been processed then this + //# function MUST yield an error that includes all the collected errors. + // + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.9 + //# The output MUST be the same as the Master Key Provider Decrypt Data + //# Key (../master-key-provider-interface.md#decrypt-data-key) interface. + .orElseThrow(() -> buildCannotDecryptDksException(exceptions)); + } + + public List getGrantTokens() { + return new ArrayList<>(grantTokens_); + } + + /** + * Returns a new {@link AwsKmsMrkAwareMasterKeyProvider} that is configured identically to this one, except with the given list + * of grant tokens. The grant token list in the returned provider is immutable (but can be further overridden by + * invoking withGrantTokens again). + * + */ + public AwsKmsMrkAwareMasterKeyProvider withGrantTokens(List grantTokens) { + grantTokens = Collections.unmodifiableList(new ArrayList<>(grantTokens)); + + return new AwsKmsMrkAwareMasterKeyProvider( + regionalClientSupplier_, + defaultRegion_, + keyIds_, + grantTokens, + isDiscovery_, + discoveryFilter_, + discoveryMrkRegion_ + ); + } + + /** + * Returns a new {@link AwsKmsMrkAwareMasterKeyProvider} that is configured identically to this one, except with the given list + * of grant tokens. The grant token list in the returned provider is immutable (but can be further overridden by + * invoking withGrantTokens again). + * + */ + public AwsKmsMrkAwareMasterKeyProvider withGrantTokens(String... grantTokens) { + return withGrantTokens(asList(grantTokens)); + } + +} diff --git a/src/main/java/com/amazonaws/encryptionsdk/kms/KmsMasterKey.java b/src/main/java/com/amazonaws/encryptionsdk/kms/KmsMasterKey.java index 346314f68..e4783d6d2 100644 --- a/src/main/java/com/amazonaws/encryptionsdk/kms/KmsMasterKey.java +++ b/src/main/java/com/amazonaws/encryptionsdk/kms/KmsMasterKey.java @@ -47,15 +47,19 @@ /** * Represents a single Customer Master Key (CMK) and is used to encrypt/decrypt data with * {@link AwsCrypto}. + * + * This component is not multi-Region key aware, + * and will treat every AWS KMS identifier as regionally isolated. */ public final class KmsMasterKey extends MasterKey implements KmsMethods { + private static final String USER_AGENT = VersionInfo.loadUserAgent(); private final Supplier kms_; private final MasterKeyProvider sourceProvider_; private final String id_; private final List grantTokens_ = new ArrayList<>(); private T updateUserAgent(T request) { - request.getRequestClientOptions().appendUserAgent(VersionInfo.USER_AGENT); + request.getRequestClientOptions().appendUserAgent(USER_AGENT); return request; } diff --git a/src/main/java/com/amazonaws/encryptionsdk/kms/KmsMasterKeyProvider.java b/src/main/java/com/amazonaws/encryptionsdk/kms/KmsMasterKeyProvider.java index 62b3e7d8b..8a1837de7 100644 --- a/src/main/java/com/amazonaws/encryptionsdk/kms/KmsMasterKeyProvider.java +++ b/src/main/java/com/amazonaws/encryptionsdk/kms/KmsMasterKeyProvider.java @@ -41,10 +41,14 @@ import com.amazonaws.services.kms.AWSKMS; import com.amazonaws.services.kms.AWSKMSClient; import com.amazonaws.services.kms.AWSKMSClientBuilder; +import static com.amazonaws.encryptionsdk.internal.AwsKmsCmkArnInfo.parseInfoFromKeyArn; /** * Provides {@link MasterKey}s backed by the AWS Key Management Service. This object is regional and * if you want to use keys from multiple regions, you'll need multiple copies of this object. + * + * This component is not multi-Region key aware, and will treat every AWS KMS identifier as + * regionally isolated. */ public class KmsMasterKeyProvider extends MasterKeyProvider implements KmsMethods { private static final String PROVIDER_NAME = "aws-kms"; @@ -186,7 +190,7 @@ public Builder withClientBuilder(AWSKMSClientBuilder builder) { return this; } - private AWSKMSClientBuilder cloneClientBuilder(final AWSKMSClientBuilder builder) { + AWSKMSClientBuilder cloneClientBuilder(final AWSKMSClientBuilder builder) { // We need to copy all arguments out of the builder in case it's mutated later on. // Unfortunately AWSKMSClientBuilder doesn't support .clone() so we'll have to do it by hand. @@ -293,7 +297,7 @@ public KmsMasterKeyProvider buildStrict(String... keyIds) { return buildStrict(asList(keyIds)); } - private RegionalClientSupplier clientFactory() { + RegionalClientSupplier clientFactory() { if (regionalClientSupplier_ != null) { return regionalClientSupplier_; } @@ -324,9 +328,8 @@ private RegionalClientSupplier clientFactory() { .withRegion(region) .withRequestHandlers(handlers.toArray(new RequestHandler2[handlers.size()])) .build(); - cacher.client_ = kms; - return kms; + return cacher.setClient(kms); }; } @@ -335,14 +338,14 @@ protected void snoopClientCache(ConcurrentHashMap map) { } } - private static class SuccessfulRequestCacher extends RequestHandler2 { + static class SuccessfulRequestCacher extends RequestHandler2 { private final ConcurrentHashMap cache_; private final String region_; private AWSKMS client_; volatile boolean ranBefore_ = false; - private SuccessfulRequestCacher( + SuccessfulRequestCacher( final ConcurrentHashMap cache, final String region ) { @@ -350,6 +353,11 @@ private SuccessfulRequestCacher( this.cache_ = cache; } + public AWSKMS setClient(final AWSKMS client) { + client_ = client; + return client; + } + @Override public void afterResponse(final Request request, final Response response) { if (ranBefore_) return; ranBefore_ = true; @@ -370,7 +378,7 @@ public static Builder builder() { return new Builder(); } - private KmsMasterKeyProvider( + KmsMasterKeyProvider( RegionalClientSupplier supplier, String defaultRegion, List keyIds, @@ -491,8 +499,8 @@ public DataKey decryptDataKey(final CryptoAlgorithm algorithm, final String keyArn = new String(edk.getProviderInformation(), StandardCharsets.UTF_8); // This will throw if we can't use this key for whatever reason return getMasterKey(keyArn).decryptDataKey(algorithm, singletonList(edk), encryptionContext); - } catch (final Exception asex) { - exceptions.add(asex); + } catch (final Exception ex) { + exceptions.add(ex); } } } @@ -563,18 +571,4 @@ public KmsMasterKeyProvider withGrantTokens(String... grantTokens) { return withGrantTokens(asList(grantTokens)); } - private static AwsKmsCmkArnInfo parseInfoFromKeyArn(final String keyArn) { - final String[] parts = keyArn.split(":", 6); - if (!parts[0].equals("arn") || parts.length < 6) { - return null; - } - if (!parts[2].equals("kms")) { - return null; - } - if (parts[1].isEmpty() || parts[3].isEmpty() || parts[4].isEmpty()) { - return null; - } - - return new AwsKmsCmkArnInfo(parts[1], parts[3], parts[4]); - } } diff --git a/src/main/java/com/amazonaws/encryptionsdk/model/CipherFrameHeaders.java b/src/main/java/com/amazonaws/encryptionsdk/model/CipherFrameHeaders.java index 25cd140d4..10a3d0221 100644 --- a/src/main/java/com/amazonaws/encryptionsdk/model/CipherFrameHeaders.java +++ b/src/main/java/com/amazonaws/encryptionsdk/model/CipherFrameHeaders.java @@ -328,4 +328,4 @@ public void setNonceLength(final short nonceLength) { public void includeFrameSize(final boolean value) { includeFrameSize_ = true; } -} \ No newline at end of file +} diff --git a/src/main/java/com/amazonaws/encryptionsdk/model/CiphertextType.java b/src/main/java/com/amazonaws/encryptionsdk/model/CiphertextType.java index 8e6825436..aef722763 100644 --- a/src/main/java/com/amazonaws/encryptionsdk/model/CiphertextType.java +++ b/src/main/java/com/amazonaws/encryptionsdk/model/CiphertextType.java @@ -73,4 +73,4 @@ public static CiphertextType deserialize(final byte value) { final CiphertextType result = ID_MAPPING.get(valueByte); return result; } -} \ No newline at end of file +} diff --git a/src/main/java/com/amazonaws/encryptionsdk/model/package-info.java b/src/main/java/com/amazonaws/encryptionsdk/model/package-info.java index 745b703c4..897a15f3b 100644 --- a/src/main/java/com/amazonaws/encryptionsdk/model/package-info.java +++ b/src/main/java/com/amazonaws/encryptionsdk/model/package-info.java @@ -33,4 +33,4 @@ * that identify the key provider. * */ -package com.amazonaws.encryptionsdk.model; \ No newline at end of file +package com.amazonaws.encryptionsdk.model; diff --git a/src/main/resources/project.properties b/src/main/resources/project.properties new file mode 100644 index 000000000..defbd4820 --- /dev/null +++ b/src/main/resources/project.properties @@ -0,0 +1 @@ +version=${project.version} diff --git a/src/test/java/com/amazonaws/crypto/examples/BasicMultiRegionKeyEncryptionExampleTest.java b/src/test/java/com/amazonaws/crypto/examples/BasicMultiRegionKeyEncryptionExampleTest.java new file mode 100644 index 000000000..4a5e567b6 --- /dev/null +++ b/src/test/java/com/amazonaws/crypto/examples/BasicMultiRegionKeyEncryptionExampleTest.java @@ -0,0 +1,18 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.amazonaws.crypto.examples; + +import com.amazonaws.encryptionsdk.kms.KMSTestFixtures; +import org.junit.Test; + +public class BasicMultiRegionKeyEncryptionExampleTest { + + @Test + public void testEncryptAndDecrypt() { + BasicMultiRegionKeyEncryptionExample.encryptAndDecrypt( + KMSTestFixtures.US_EAST_1_MULTI_REGION_KEY_ID, + KMSTestFixtures.US_WEST_2_MULTI_REGION_KEY_ID + ); + } +} diff --git a/src/test/java/com/amazonaws/crypto/examples/DiscoveryMultiRegionDecryptionExampleTest.java b/src/test/java/com/amazonaws/crypto/examples/DiscoveryMultiRegionDecryptionExampleTest.java new file mode 100644 index 000000000..626d116db --- /dev/null +++ b/src/test/java/com/amazonaws/crypto/examples/DiscoveryMultiRegionDecryptionExampleTest.java @@ -0,0 +1,20 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.amazonaws.crypto.examples; + +import com.amazonaws.encryptionsdk.kms.KMSTestFixtures; +import org.junit.Test; + +public class DiscoveryMultiRegionDecryptionExampleTest { + + @Test + public void testEncryptAndDecrypt() { + DiscoveryMultiRegionDecryptionExample.encryptAndDecrypt( + KMSTestFixtures.US_EAST_1_MULTI_REGION_KEY_ID, + KMSTestFixtures.PARTITION, + KMSTestFixtures.ACCOUNT_ID, + KMSTestFixtures.US_WEST_2 + ); + } +} diff --git a/src/test/java/com/amazonaws/encryptionsdk/AllTestsSuite.java b/src/test/java/com/amazonaws/encryptionsdk/AllTestsSuite.java index 385bdc210..b53119f11 100644 --- a/src/test/java/com/amazonaws/encryptionsdk/AllTestsSuite.java +++ b/src/test/java/com/amazonaws/encryptionsdk/AllTestsSuite.java @@ -6,12 +6,15 @@ import com.amazonaws.crypto.examples.SetCommitmentPolicyExampleTest; import com.amazonaws.crypto.examples.SetEncryptionAlgorithmExampleTest; import com.amazonaws.crypto.examples.SimpleDataKeyCachingExampleTest; +import com.amazonaws.encryptionsdk.internal.*; import com.amazonaws.encryptionsdk.jce.JceMasterKeyTest; import org.junit.runner.RunWith; import org.junit.runners.Suite; import com.amazonaws.crypto.examples.BasicEncryptionExampleTest; +import com.amazonaws.crypto.examples.BasicMultiRegionKeyEncryptionExampleTest; import com.amazonaws.crypto.examples.DiscoveryDecryptionExampleTest; +import com.amazonaws.crypto.examples.DiscoveryMultiRegionDecryptionExampleTest; import com.amazonaws.crypto.examples.MultipleCmkEncryptExampleTest; import com.amazonaws.crypto.examples.RestrictRegionExampleTest; import com.amazonaws.encryptionsdk.caching.CacheIdentifierTests; @@ -19,17 +22,6 @@ import com.amazonaws.encryptionsdk.caching.LocalCryptoMaterialsCacheTest; import com.amazonaws.encryptionsdk.caching.LocalCryptoMaterialsCacheThreadStormTest; import com.amazonaws.encryptionsdk.caching.NullCryptoMaterialsCacheTest; -import com.amazonaws.encryptionsdk.internal.BlockDecryptionHandlerTest; -import com.amazonaws.encryptionsdk.internal.BlockEncryptionHandlerTest; -import com.amazonaws.encryptionsdk.internal.CipherHandlerTest; -import com.amazonaws.encryptionsdk.internal.CommittedKeyTest; -import com.amazonaws.encryptionsdk.internal.DecryptionHandlerTest; -import com.amazonaws.encryptionsdk.internal.EncContextSerializerTest; -import com.amazonaws.encryptionsdk.internal.EncryptionHandlerTest; -import com.amazonaws.encryptionsdk.internal.FrameDecryptionHandlerTest; -import com.amazonaws.encryptionsdk.internal.FrameEncryptionHandlerTest; -import com.amazonaws.encryptionsdk.internal.PrimitivesParserTest; -import com.amazonaws.encryptionsdk.internal.UtilsTest; import com.amazonaws.encryptionsdk.jce.KeyStoreProviderTest; import com.amazonaws.encryptionsdk.model.CipherBlockHeadersTest; import com.amazonaws.encryptionsdk.model.CipherFrameHeadersTest; @@ -42,6 +34,8 @@ import com.amazonaws.encryptionsdk.kms.KMSProviderBuilderMockTests; import com.amazonaws.encryptionsdk.kms.KmsMasterKeyProviderTest; import com.amazonaws.encryptionsdk.kms.KmsMasterKeyTest; +import com.amazonaws.encryptionsdk.kms.AwsKmsMrkAwareMasterKeyProviderTest; +import com.amazonaws.encryptionsdk.kms.AwsKmsMrkAwareMasterKeyTest; @RunWith(Suite.class) @Suite.SuiteClasses({ @@ -69,6 +63,7 @@ XCompatDecryptTest.class, DefaultCryptoMaterialsManagerTest.class, NullCryptoMaterialsCacheTest.class, + AwsKmsCmkArnInfoTest.class, CacheIdentifierTests.class, CachingCryptoMaterialsManagerTest.class, LocalCryptoMaterialsCacheTest.class, @@ -84,13 +79,18 @@ EncryptionMaterialsRequestTest.class, CommitmentKATRunner.class, BasicEncryptionExampleTest.class, + BasicMultiRegionKeyEncryptionExampleTest.class, DiscoveryDecryptionExampleTest.class, + DiscoveryMultiRegionDecryptionExampleTest.class, MultipleCmkEncryptExampleTest.class, RestrictRegionExampleTest.class, SimpleDataKeyCachingExampleTest.class, SetEncryptionAlgorithmExampleTest.class, SetCommitmentPolicyExampleTest.class, ParsedCiphertextTest.class, + AwsKmsMrkAwareMasterKeyProviderTest.class, + AwsKmsMrkAwareMasterKeyTest.class, + VersionInfoTest.class, }) public class AllTestsSuite { } diff --git a/src/test/java/com/amazonaws/encryptionsdk/TestVectorRunner.java b/src/test/java/com/amazonaws/encryptionsdk/TestVectorRunner.java index 8f1573258..e3110a36f 100644 --- a/src/test/java/com/amazonaws/encryptionsdk/TestVectorRunner.java +++ b/src/test/java/com/amazonaws/encryptionsdk/TestVectorRunner.java @@ -3,11 +3,11 @@ package com.amazonaws.encryptionsdk; -import static java.lang.String.format; - import com.amazonaws.auth.DefaultAWSCredentialsProviderChain; import com.amazonaws.encryptionsdk.internal.SignaturePolicy; import com.amazonaws.encryptionsdk.jce.JceMasterKey; +import com.amazonaws.encryptionsdk.kms.AwsKmsMrkAwareMasterKeyProvider; +import com.amazonaws.encryptionsdk.kms.DiscoveryFilter; import com.amazonaws.encryptionsdk.kms.KmsMasterKeyProvider; import com.amazonaws.encryptionsdk.multi.MultipleProviderFactory; import com.amazonaws.util.IOUtils; @@ -26,11 +26,7 @@ import java.io.InputStream; import java.net.JarURLConnection; import java.net.URL; -import java.security.GeneralSecurityException; -import java.security.Key; -import java.security.KeyFactory; -import java.security.PrivateKey; -import java.security.PublicKey; +import java.security.*; import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.X509EncodedKeySpec; import java.util.ArrayList; @@ -40,9 +36,12 @@ import java.util.List; import java.util.Map; import java.util.concurrent.Callable; +import java.util.function.Supplier; import java.util.jar.JarFile; import java.util.zip.ZipEntry; +import static java.lang.String.format; + @RunWith(Parameterized.class) public class TestVectorRunner { @@ -65,11 +64,11 @@ public TestVectorRunner(final String testName, TestCase testCase, DecryptionMeth @Test public void decrypt() throws Exception { AwsCrypto crypto = AwsCrypto.builder().withCommitmentPolicy(CommitmentPolicy.ForbidEncryptAllowDecrypt).build(); - Callable decryptor = () -> decryptionMethod.decryptMessage(crypto, testCase.mkp, cachedData.get(testCase.ciphertextPath)); + Callable decryptor = () -> decryptionMethod.decryptMessage(crypto, testCase.mkpSupplier.get(), cachedData.get(testCase.ciphertextPath)); testCase.matcher.Match(decryptor); } - @Parameterized.Parameters(name="Compatibility Test: {0} - {1}") + @Parameterized.Parameters(name="Compatibility Test: {0} - {2}") @SuppressWarnings("unchecked") public static Collection data() throws Exception { final String zipPath = System.getProperty("testVectorZip"); @@ -151,51 +150,72 @@ private static TestCase parseTest(String testName, Map data, Map final String ciphertextURL = (String) data.get("ciphertext"); cacheData(jar, ciphertextURL); - @SuppressWarnings("generic") - final List> mks = new ArrayList<>(); - - for (Map mkEntry : (List>) data.get("master-keys")) { - final String type = mkEntry.get("type"); - final String keyName = mkEntry.get("key"); - final KeyEntry key = keys.get(keyName); - - if ("aws-kms".equals(type)) { - mks.add(kmsProv.getMasterKey(key.keyId)); - } else if ("raw".equals(type)) { - final String provId = mkEntry.get("provider-id"); - final String algorithm = mkEntry.get("encryption-algorithm"); - if ("aes".equals(algorithm)) { - mks.add(JceMasterKey.getInstance((SecretKey) key.key, provId, key.keyId, "AES/GCM/NoPadding")); - } else if ("rsa".equals(algorithm)) { - String transformation = "RSA/ECB/"; - final String padding = mkEntry.get("padding-algorithm"); - if ("pkcs1".equals(padding)) { - transformation += "PKCS1Padding"; - } else if ("oaep-mgf1".equals(padding)) { - final String hashName = mkEntry.get("padding-hash") - .replace("sha", "sha-") - .toUpperCase(); - transformation += "OAEPWith" + hashName + "AndMGF1Padding"; + Supplier> mkpSupplier = () -> { + + @SuppressWarnings("generic") + final List> mks = new ArrayList<>(); + + for (Map mkEntry : (List>) data.get("master-keys")) { + final String type = (String) mkEntry.get("type"); + final String keyName =(String) mkEntry.get("key"); + final KeyEntry key = keys.get(keyName); + + if ("aws-kms".equals(type)) { + mks.add(kmsProv.getMasterKey(key.keyId)); + } else if ("aws-kms-mrk-aware".equals(type)) { + AwsKmsMrkAwareMasterKeyProvider provider = AwsKmsMrkAwareMasterKeyProvider.builder().buildStrict(key.keyId); + mks.add(provider.getMasterKey(key.keyId)); + } else if ("aws-kms-mrk-aware-discovery".equals(type)) { + final String defaultMrkRegion = (String) mkEntry.get("default-mrk-region"); + final Map discoveryFilterSpec = (Map) mkEntry.get("aws-kms-discovery-filter"); + final DiscoveryFilter discoveryFilter; + if (discoveryFilterSpec != null) { + discoveryFilter = new DiscoveryFilter((String) discoveryFilterSpec.get("partition"), + (List) discoveryFilterSpec.get("account-ids")); } else { - throw new IllegalArgumentException("Unsupported padding:" + padding); + discoveryFilter = null; } - final PublicKey wrappingKey; - final PrivateKey unwrappingKey; - if (key.key instanceof PublicKey) { - wrappingKey = (PublicKey) key.key; - unwrappingKey = null; + return AwsKmsMrkAwareMasterKeyProvider.builder() + .withDiscoveryMrkRegion(defaultMrkRegion) + .buildDiscovery(discoveryFilter); + } else if ("raw".equals(type)) { + final String provId = (String) mkEntry.get("provider-id"); + final String algorithm = (String) mkEntry.get("encryption-algorithm"); + if ("aes".equals(algorithm)) { + mks.add(JceMasterKey.getInstance((SecretKey) key.key, provId, key.keyId, "AES/GCM/NoPadding")); + } else if ("rsa".equals(algorithm)) { + String transformation = "RSA/ECB/"; + final String padding = (String) mkEntry.get("padding-algorithm"); + if ("pkcs1".equals(padding)) { + transformation += "PKCS1Padding"; + } else if ("oaep-mgf1".equals(padding)) { + final String hashName = ((String) mkEntry.get("padding-hash")) + .replace("sha", "sha-") + .toUpperCase(); + transformation += "OAEPWith" + hashName + "AndMGF1Padding"; + } else { + throw new IllegalArgumentException("Unsupported padding:" + padding); + } + final PublicKey wrappingKey; + final PrivateKey unwrappingKey; + if (key.key instanceof PublicKey) { + wrappingKey = (PublicKey) key.key; + unwrappingKey = null; + } else { + wrappingKey = null; + unwrappingKey = (PrivateKey) key.key; + } + mks.add(JceMasterKey.getInstance(wrappingKey, unwrappingKey, provId, key.keyId, transformation)); } else { - wrappingKey = null; - unwrappingKey = (PrivateKey) key.key; + throw new IllegalArgumentException("Unsupported algorithm: " + algorithm); } - mks.add(JceMasterKey.getInstance(wrappingKey, unwrappingKey, provId, key.keyId, transformation)); } else { - throw new IllegalArgumentException("Unsupported algorithm: " + algorithm); + throw new IllegalArgumentException("Unsupported Key Type: " + type); } - } else { - throw new IllegalArgumentException("Unsupported Key Type: " + type); } - } + + return MultipleProviderFactory.buildMultiProvider(mks); + }; @SuppressWarnings("unchecked") final Map resultSpec = (Map) data.get("result"); @@ -211,7 +231,7 @@ private static TestCase parseTest(String testName, Map data, Map } } - return new TestCase(testName, ciphertextURL, mks, matcher, signaturePolicy); + return new TestCase(testName, ciphertextURL, mkpSupplier, matcher, signaturePolicy); } private static ResultMatcher parseResultMatcher(final JarFile jar, final Map result) throws IOException { @@ -322,18 +342,14 @@ private static class TestCase { private final String name; private final String ciphertextPath; private final ResultMatcher matcher; - private final MasterKeyProvider mkp; + private final Supplier> mkpSupplier; private final SignaturePolicy signaturePolicy; - private TestCase(String name, String ciphertextPath, List> mks, ResultMatcher matcher, SignaturePolicy signaturePolicy) { - this(name, ciphertextPath, MultipleProviderFactory.buildMultiProvider(mks), matcher, signaturePolicy); - } - - private TestCase(String name, String ciphertextPath, MasterKeyProvider mkp, ResultMatcher matcher, SignaturePolicy signaturePolicy) { + private TestCase(String name, String ciphertextPath, Supplier> mkpSupplier, ResultMatcher matcher, SignaturePolicy signaturePolicy) { this.name = name; this.ciphertextPath = ciphertextPath; this.matcher = matcher; - this.mkp = mkp; + this.mkpSupplier = mkpSupplier; this.signaturePolicy = signaturePolicy; } } diff --git a/src/test/java/com/amazonaws/encryptionsdk/internal/AwsKmsCmkArnInfoTest.java b/src/test/java/com/amazonaws/encryptionsdk/internal/AwsKmsCmkArnInfoTest.java new file mode 100644 index 000000000..47f82253b --- /dev/null +++ b/src/test/java/com/amazonaws/encryptionsdk/internal/AwsKmsCmkArnInfoTest.java @@ -0,0 +1,489 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.amazonaws.encryptionsdk.internal; + +import org.junit.Test; +import org.junit.experimental.runners.Enclosed; +import org.junit.jupiter.api.DisplayName; +import org.junit.runner.RunWith; + +import static com.amazonaws.encryptionsdk.TestUtils.assertThrows; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +@RunWith(Enclosed.class) +public class AwsKmsCmkArnInfoTest { + + public static class splitArn { + @Test + public void basic_use() { + String[] test = AwsKmsCmkArnInfo.AwsKmsArnParts.splitArn("arn:aws:kms:us-west-2:111122223333:key/mrk-edb7fe6942894d32ac46dbb1c922d574"); + assertEquals(test.length, 6); + } + + @Test + public void with_fewer_elements() { + String[] test = AwsKmsCmkArnInfo.AwsKmsArnParts.splitArn("arn:aws:kms:us-west-2"); + assertEquals(test.length, 4); + } + + @Test + public void with_valid_arn_but_not_kms_valid() { + String[] test = AwsKmsCmkArnInfo.AwsKmsArnParts.splitArn("arn:aws:kms:us-west-2:111122223333:key:mrk-edb7fe6942894d32ac46dbb1c922d574"); + assertEquals(test.length, 6); + } + } + + public static class splitResourceParts { + @Test + public void basic_use() { + String[] test = AwsKmsCmkArnInfo.AwsKmsArnParts.Resource.splitResourceParts("key/mrk-edb7fe6942894d32ac46dbb1c922d574"); + assertEquals(test.length, 2); + } + } + + public static class parseInfoFromKeyArn { + @Test + public void basic_use() { + AwsKmsCmkArnInfo test = AwsKmsCmkArnInfo.parseInfoFromKeyArn("arn:aws:kms:us-west-2:111122223333:key/mrk-edb7fe6942894d32ac46dbb1c922d574"); + assertNotNull(test); + assertEquals(test.getPartition(), "aws"); + assertEquals(test.getRegion(), "us-west-2"); + assertEquals(test.getAccountId(), "111122223333"); + assertEquals(test.getAccountId(), "111122223333"); + assertEquals(test.getResourceType(), "key"); + assertEquals(test.getResource(), "mrk-edb7fe6942894d32ac46dbb1c922d574"); + } + + @Test + @DisplayName("Precondition: keyArn must be a string.") + public void keyArn_must_be_string_with_content() { + assertEquals( + AwsKmsCmkArnInfo.parseInfoFromKeyArn(""), + null + ); + assertEquals( + AwsKmsCmkArnInfo.parseInfoFromKeyArn(null), + null + ); + } + + @Test + //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.5 + //= type=test + //# MUST start with string "arn" + public void not_well_formed() { + assertEquals( + AwsKmsCmkArnInfo.parseInfoFromKeyArn("key/mrk-edb7fe6942894d32ac46dbb1c922d574"), + null + ); + assertEquals( + AwsKmsCmkArnInfo.parseInfoFromKeyArn("alias/my-key"), + null + ); + assertEquals( + AwsKmsCmkArnInfo.parseInfoFromKeyArn("not-an-arn:aws:kms:us-west-2:111122223333:key/mrk-edb7fe6942894d32ac46dbb1c922d574"), + null + ); + } + + @Test + //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.5 + //= type=test + //# The service MUST be the string "kms" + public void not_kms_service() { + assertEquals( + AwsKmsCmkArnInfo.parseInfoFromKeyArn("arn:aws:sqs:us-east-2:444455556666:queue1"), + null + ); + } + + @Test + //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.5 + //= type=test + //# The partition MUST be a non-empty + public void partition_non_empty() { + assertEquals( + AwsKmsCmkArnInfo.parseInfoFromKeyArn("arn::kms:us-west-2:111122223333:key/mrk-edb7fe6942894d32ac46dbb1c922d574"), + null + ); + } + + @Test + //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.5 + //= type=test + //# The region MUST be a non-empty string + public void region_non_empty() { + assertEquals( + AwsKmsCmkArnInfo.parseInfoFromKeyArn("arn:aws:kms::111122223333:key/mrk-edb7fe6942894d32ac46dbb1c922d574"), + null + ); + } + + @Test + //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.5 + //= type=test + //# The account MUST be a non-empty string + public void account_non_empty() { + assertEquals( + AwsKmsCmkArnInfo.parseInfoFromKeyArn("arn:aws:kms:us-west-2::key/mrk-edb7fe6942894d32ac46dbb1c922d574"), + null + ); + } + + @Test + //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.5 + //= type=test + //# The resource section MUST be non-empty and MUST be split by a + //# single "/" any additional "/" are included in the resource id + public void resource_non_empty() { + //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.5 + //= type=test + //# The resource id MUST be a non-empty string + assertEquals( + AwsKmsCmkArnInfo.parseInfoFromKeyArn("arn:aws:kms:us-west-2:111122223333:"), + null + ); + assertEquals( + // This is a valid ARN but not valid for AWS KMS + AwsKmsCmkArnInfo.parseInfoFromKeyArn("arn:aws:kms:us-west-2:111122223333:key:mrk-edb7fe6942894d32ac46dbb1c922d574"), + null + ); + final AwsKmsCmkArnInfo arn = AwsKmsCmkArnInfo.parseInfoFromKeyArn("arn:aws:kms:us-west-2:111122223333:alias/has/slashes"); + assertNotNull(arn); + assertEquals(arn.getResource(), "has/slashes"); + } + + //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.5 + //= type=test + //# The resource type MUST be either "alias" or "key" + public void resource_type_key_or_alias() { + assertEquals( + AwsKmsCmkArnInfo.parseInfoFromKeyArn("arn:aws:kms:us-west-2:111122223333:not-key/mrk-edb7fe6942894d32ac46dbb1c922d574"), + null + ); + } + } + + public static class validAwsKmsIdentifier { + + @Test + public void basic_use() { + AwsKmsCmkArnInfo.validAwsKmsIdentifier("mrk-edb7fe6942894d32ac46dbb1c922d574"); + AwsKmsCmkArnInfo.validAwsKmsIdentifier("alias/my-alias"); + AwsKmsCmkArnInfo.validAwsKmsIdentifier("arn:aws:kms:us-west-2:111122223333:key/mrk-edb7fe6942894d32ac46dbb1c922d574"); + AwsKmsCmkArnInfo.validAwsKmsIdentifier("arn:aws:kms:us-west-2:111122223333:alias/my-alias"); + } + + @Test + @DisplayName("Exceptional Postcondition: Null or empty string is not a valid identifier.") + public void must_have_content() { + assertThrows( + IllegalArgumentException.class, + "Null or empty string is not a valid Aws KMS identifier.", + () -> AwsKmsCmkArnInfo.validAwsKmsIdentifier("")); + assertThrows( + IllegalArgumentException.class, + "Null or empty string is not a valid Aws KMS identifier.", + () -> AwsKmsCmkArnInfo.validAwsKmsIdentifier(null)); + } + + @Test + @DisplayName("Exceptional Postcondition: Things that start with `arn:` MUST be ARNs.") + public void arn_must_be_arn() { + assertThrows( + IllegalArgumentException.class, + "Invalid ARN used as an identifier.", + () -> AwsKmsCmkArnInfo.validAwsKmsIdentifier("arn:aws:dynamodb:us-east-2:123456789012:table/myDynamoDBTable")); + } + + @Test + @DisplayName("Postcondition: Raw alias starts with `alias/`.") + public void alias_is_valid() { + AwsKmsCmkArnInfo.validAwsKmsIdentifier("alias/some/kind/of/alias"); + } + + @Test + @DisplayName("Postcondition: There are no requirements on key ids.") + public void anything_else_is_key_id() { + AwsKmsCmkArnInfo.validAwsKmsIdentifier("mrk-edb7fe6942894d32ac46dbb1c922d574"); + AwsKmsCmkArnInfo.validAwsKmsIdentifier("b3537ef1-d8dc-4780-9f5a-55776cbb2f7f"); + } + } + + public static class isMRK { + @Test + //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.9 + //= type=test + //# This function MUST take a single AWS KMS identifier + public void basic_use() { + //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.9 + //= type=test + //# If the input starts + //# with "mrk-", this is a multi-Region key id and MUST return true. + assertEquals( + AwsKmsCmkArnInfo.isMRK("mrk-edb7fe6942894d32ac46dbb1c922d574"), + true + ); + //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.9 + //= type=test + //# If the input starts with "alias/", this an AWS KMS alias and + //# not a multi-Region key id and MUST return false. + assertEquals( + AwsKmsCmkArnInfo.isMRK("alias/mrk-1234"), + false + ); + //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.9 + //= type=test + //# If + //# the input does not start with any of the above, this is not a multi- + //# Region key id and MUST return false. + assertEquals( + AwsKmsCmkArnInfo.isMRK("64339c87-2ae4-42b1-8875-c83fc47acc97"), + false + ); + //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.9 + //= type=test + //# If the input starts with "arn:", this MUST return the output of + //# identifying an an AWS KMS multi-Region ARN (aws-kms-key- + //# arn.md#identifying-an-an-aws-kms-multi-region-arn) called with this + //# input. + assertEquals( + AwsKmsCmkArnInfo.isMRK("arn:aws:kms:us-west-2:111122223333:alias/mrk-edb7fe6942894d32ac46dbb1c922d574"), + false + ); + } + + @Test + //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.8 + //= type=test + //# If the input is an invalid AWS KMS ARN this function MUST error. + public void invalid_arn() { + assertThrows( + () -> AwsKmsCmkArnInfo.isMRK(AwsKmsCmkArnInfo.parseInfoFromKeyArn("arn:aws:kms:us-west-2:111122223333:not-key/mrk-edb7fe6942894d32ac46dbb1c922d574")) + ); + } + + @Test + //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.8 + //= type=test + //# If resource type is "alias", this is an AWS KMS alias ARN and MUST + //# return false. + public void with_an_alias_AwsKmsCmkArnInfo() { + assertEquals( + AwsKmsCmkArnInfo.isMRK(AwsKmsCmkArnInfo.parseInfoFromKeyArn("arn:aws:kms:us-west-2:111122223333:alias/mrk-edb7fe6942894d32ac46dbb1c922d574")), + false + ); + } + + @Test + //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.8 + //= type=test + //# This function MUST take a single AWS KMS ARN + public void with_an_mrk_AwsKmsCmkArnInfo() { + //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.8 + //= type=test + //# If resource type is "key" and resource ID starts with + //# "mrk-", this is a AWS KMS multi-Region key ARN and MUST return true. + assertEquals( + AwsKmsCmkArnInfo.isMRK(AwsKmsCmkArnInfo.parseInfoFromKeyArn("arn:aws:kms:us-west-2:111122223333:key/mrk-edb7fe6942894d32ac46dbb1c922d574")), + true + ); + } + + @Test + //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.8 + //= type=test + //# If resource type is "key" and resource ID does not start with "mrk-", + //# this is a (single-region) AWS KMS key ARN and MUST return false. + public void with_an_srk_AwsKmsCmkArnInfo() { + assertEquals( + AwsKmsCmkArnInfo.isMRK(AwsKmsCmkArnInfo.parseInfoFromKeyArn("arn:aws:kms:us-west-2:111122223333:key/b3537ef1-d8dc-4780-9f5a-55776cbb2f7f")), + false + ); + } + } + + public static class awsKmsArnMatchForDecrypt { + @Test + //= compliance/framework/aws-kms/aws-kms-mrk-match-for-decrypt.txt#2.5 + //= type=test + //# The caller MUST provide: + public void basic_use() { + assertEquals( + AwsKmsCmkArnInfo.awsKmsArnMatchForDecrypt( + "arn:aws:kms:us-west-2:111122223333:key/mrk-edb7fe6942894d32ac46dbb1c922d574", + "arn:aws:kms:us-west-2:111122223333:key/mrk-edb7fe6942894d32ac46dbb1c922d574" + ), + true + ); + assertEquals( + AwsKmsCmkArnInfo.awsKmsArnMatchForDecrypt( + "arn:aws:kms:us-east-1:111122223333:key/mrk-edb7fe6942894d32ac46dbb1c922d574", + "arn:aws:kms:us-west-2:111122223333:key/mrk-edb7fe6942894d32ac46dbb1c922d574" + ), + true + ); + } + + @Test + //= compliance/framework/aws-kms/aws-kms-mrk-match-for-decrypt.txt#2.5 + //= type=test + //# If both identifiers are identical, this function MUST return "true". + public void string_match_cases() { + + assertEquals( + AwsKmsCmkArnInfo.awsKmsArnMatchForDecrypt( + "arn:aws:kms:us-west-2:111122223333:key/mrk-edb7fe6942894d32ac46dbb1c922d574", + "arn:aws:kms:us-west-2:111122223333:key/mrk-edb7fe6942894d32ac46dbb1c922d574" + ), + true + ); + + assertEquals( + AwsKmsCmkArnInfo.awsKmsArnMatchForDecrypt( + "arn:aws:kms:us-west-2:111122223333:key/64339c87-2ae4-42b1-8875-c83fc47acc97", + "arn:aws:kms:us-west-2:111122223333:key/64339c87-2ae4-42b1-8875-c83fc47acc97" + ), + true + ); + + assertEquals( + AwsKmsCmkArnInfo.awsKmsArnMatchForDecrypt( + "arn:aws:kms:us-west-2:111122223333:alias/my-name", + "arn:aws:kms:us-west-2:111122223333:alias/my-name" + ), + true + ); + + assertEquals( + AwsKmsCmkArnInfo.awsKmsArnMatchForDecrypt( + "alias/my-raw-alias", + "alias/my-raw-alias" + ), + true + ); + + assertEquals( + AwsKmsCmkArnInfo.awsKmsArnMatchForDecrypt( + "64339c87-2ae4-42b1-8875-c83fc47acc97", + "64339c87-2ae4-42b1-8875-c83fc47acc97" + ), + true + ); + + assertEquals( + AwsKmsCmkArnInfo.awsKmsArnMatchForDecrypt( + "c83fc47acc97", + "64339c87-2ae4-42b1-8875-c83fc47acc97" + ), + false + ); + } + + @Test + @DisplayName("Check for early return (Postcondition): Both identifiers are not ARNs and not equal, therefore they can not match.") + public void flexibility_for_only_arns() { + assertEquals( + AwsKmsCmkArnInfo.awsKmsArnMatchForDecrypt( + "arn:aws:kms:us-west-2:111122223333:key/mrk-edb7fe6942894d32ac46dbb1c922d574", + "mrk-edb7fe6942894d32ac46dbb1c922d574" + ), + false + ); + assertEquals( + AwsKmsCmkArnInfo.awsKmsArnMatchForDecrypt( + "mrk-edb7fe6942894d32ac46dbb1c922d574", + "arn:aws:kms:us-west-2:111122223333:key/mrk-edb7fe6942894d32ac46dbb1c922d574" + ), + false + ); + } + + + @Test + //= compliance/framework/aws-kms/aws-kms-mrk-match-for-decrypt.txt#2.5 + //= type=test + //# Otherwise if either input is not identified as a multi-Region key + //# (aws-kms-key-arn.md#identifying-an-aws-kms-multi-region-key), then + //# this function MUST return "false". + public void no_flexibility_for_non_mrks() { + assertEquals( + AwsKmsCmkArnInfo.awsKmsArnMatchForDecrypt( + "arn:aws:kms:us-west-2:111122223333:key/64339c87-2ae4-42b1-8875-c83fc47acc97", + "arn:aws:kms:us-east-1:111122223333:key/64339c87-2ae4-42b1-8875-c83fc47acc97" + ), + false + ); + assertEquals( + AwsKmsCmkArnInfo.awsKmsArnMatchForDecrypt( + "arn:aws:kms:us-west-2:111122223333:alias/mrk-someOtherName", + "arn:aws:kms:us-east-1:111122223333:alias/mrk-someOtherName" + ), + false + ); + } + + @Test + //= compliance/framework/aws-kms/aws-kms-mrk-match-for-decrypt.txt#2.5 + //= type=test + //# Otherwise if both inputs are + //# identified as a multi-Region keys (aws-kms-key-arn.md#identifying-an- + //# aws-kms-multi-region-key), this function MUST return the result of + //# comparing the "partition", "service", "accountId", "resourceType", + //# and "resource" parts of both ARN inputs. + public void all_elements_must_match() { + // Different partition + assertEquals( + AwsKmsCmkArnInfo.awsKmsArnMatchForDecrypt( + "arn:not-aws:kms:us-east-1:111122223333:key/mrk-edb7fe6942894d32ac46dbb1c922d574", + "arn:aws:kms:us-west-2:111122223333:key/mrk-edb7fe6942894d32ac46dbb1c922d574" + ), + false + ); + // Different account + assertEquals( + AwsKmsCmkArnInfo.awsKmsArnMatchForDecrypt( + "arn:aws:kms:us-east-1:333322221111:key/mrk-edb7fe6942894d32ac46dbb1c922d574", + "arn:aws:kms:us-west-2:111122223333:key/mrk-edb7fe6942894d32ac46dbb1c922d574" + ), + false + ); + // Different resource type + assertEquals( + AwsKmsCmkArnInfo.awsKmsArnMatchForDecrypt( + "arn:not-aws:kms:us-east-1:111122223333:not-key/mrk-edb7fe6942894d32ac46dbb1c922d574", + "arn:aws:kms:us-west-2:111122223333:key/mrk-edb7fe6942894d32ac46dbb1c922d574" + ), + false + ); + // Different resource + assertEquals( + AwsKmsCmkArnInfo.awsKmsArnMatchForDecrypt( + "arn:aws:kms:us-east-1:111122223333:key/mrk-475d229c1bbd64ca23d4982496ef7bde", + "arn:aws:kms:us-west-2:111122223333:key/mrk-edb7fe6942894d32ac46dbb1c922d574" + ), + false + ); + } + } + + public static class to_string_tests { + @Test + public void basic_use() { + final String arn = "arn:aws:kms:us-west-2:111122223333:key/mrk-edb7fe6942894d32ac46dbb1c922d574"; + final String region = "us-east-1"; + final AwsKmsCmkArnInfo test = AwsKmsCmkArnInfo.parseInfoFromKeyArn(arn); + + assertEquals(arn, test.toString()); + assertEquals( + "arn:aws:kms:us-east-1:111122223333:key/mrk-edb7fe6942894d32ac46dbb1c922d574", + test.toString("us-east-1") + ); + } + } + +} diff --git a/src/test/java/com/amazonaws/encryptionsdk/internal/BlockEncryptionHandlerTest.java b/src/test/java/com/amazonaws/encryptionsdk/internal/BlockEncryptionHandlerTest.java index ce3b07353..14c83a4f3 100644 --- a/src/test/java/com/amazonaws/encryptionsdk/internal/BlockEncryptionHandlerTest.java +++ b/src/test/java/com/amazonaws/encryptionsdk/internal/BlockEncryptionHandlerTest.java @@ -74,4 +74,4 @@ public void correctIVGenerated() throws Exception { headers.getNonce() ); } -} \ No newline at end of file +} diff --git a/src/test/java/com/amazonaws/encryptionsdk/internal/CipherHandlerTest.java b/src/test/java/com/amazonaws/encryptionsdk/internal/CipherHandlerTest.java index 18dc6dc77..adc1083f3 100644 --- a/src/test/java/com/amazonaws/encryptionsdk/internal/CipherHandlerTest.java +++ b/src/test/java/com/amazonaws/encryptionsdk/internal/CipherHandlerTest.java @@ -86,4 +86,4 @@ private byte[] encryptDecrypt(final byte[] content, final CryptoAlgorithm crypto private CipherHandler createCipherHandler(final SecretKey key, final CryptoAlgorithm cryptoAlgorithm, final int mode) { return new CipherHandler(key, mode, cryptoAlgorithm); } -} \ No newline at end of file +} diff --git a/src/test/java/com/amazonaws/encryptionsdk/internal/FrameEncryptionHandlerTest.java b/src/test/java/com/amazonaws/encryptionsdk/internal/FrameEncryptionHandlerTest.java index b8eca48a8..ed662f004 100644 --- a/src/test/java/com/amazonaws/encryptionsdk/internal/FrameEncryptionHandlerTest.java +++ b/src/test/java/com/amazonaws/encryptionsdk/internal/FrameEncryptionHandlerTest.java @@ -124,4 +124,4 @@ private void generateTestBlock(byte[] buf) { new byte[frameSize_], 0, frameSize_, buf, 0 ); } -} \ No newline at end of file +} diff --git a/src/test/java/com/amazonaws/encryptionsdk/internal/TrailingSignatureAlgorithmTest.java b/src/test/java/com/amazonaws/encryptionsdk/internal/TrailingSignatureAlgorithmTest.java index 3a4eda85a..babce1455 100644 --- a/src/test/java/com/amazonaws/encryptionsdk/internal/TrailingSignatureAlgorithmTest.java +++ b/src/test/java/com/amazonaws/encryptionsdk/internal/TrailingSignatureAlgorithmTest.java @@ -167,4 +167,4 @@ private void testDeserialization(CryptoAlgorithm algorithm, int[] compressedKey assertEquals(expectedXBigInteger, x); assertEquals(expectedYBigInteger, y); } -} \ No newline at end of file +} diff --git a/src/test/java/com/amazonaws/encryptionsdk/internal/VersionInfoTest.java b/src/test/java/com/amazonaws/encryptionsdk/internal/VersionInfoTest.java new file mode 100644 index 000000000..bfee709b8 --- /dev/null +++ b/src/test/java/com/amazonaws/encryptionsdk/internal/VersionInfoTest.java @@ -0,0 +1,28 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.amazonaws.encryptionsdk.internal; + +import org.junit.Test; +import org.junit.experimental.runners.Enclosed; +import org.junit.jupiter.api.DisplayName; +import org.junit.runner.RunWith; + +import static com.amazonaws.encryptionsdk.TestUtils.assertThrows; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +public class VersionInfoTest { + + @Test + public void basic_use() { + final String userAgent = VersionInfo.loadUserAgent(); + assertTrue(userAgent.startsWith(VersionInfo.USER_AGENT_PREFIX)); + assertTrue(!userAgent.equals(VersionInfo.USER_AGENT_PREFIX + VersionInfo.UNKNOWN_VERSION)); + } +} + + + diff --git a/src/test/java/com/amazonaws/encryptionsdk/kms/AwsKmsMrkAwareMasterKeyProviderTest.java b/src/test/java/com/amazonaws/encryptionsdk/kms/AwsKmsMrkAwareMasterKeyProviderTest.java new file mode 100644 index 000000000..3bc65a63c --- /dev/null +++ b/src/test/java/com/amazonaws/encryptionsdk/kms/AwsKmsMrkAwareMasterKeyProviderTest.java @@ -0,0 +1,885 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.amazonaws.encryptionsdk.kms; + +import com.amazonaws.AmazonServiceException; +import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.encryptionsdk.*; +import com.amazonaws.encryptionsdk.exception.AwsCryptoException; +import com.amazonaws.encryptionsdk.exception.CannotUnwrapDataKeyException; +import com.amazonaws.encryptionsdk.exception.NoSuchMasterKeyException; +import com.amazonaws.encryptionsdk.exception.UnsupportedProviderException; + +import static com.amazonaws.encryptionsdk.internal.AwsKmsCmkArnInfo.parseInfoFromKeyArn; + +import com.amazonaws.encryptionsdk.kms.KmsMasterKeyProvider.RegionalClientSupplier; +import com.amazonaws.encryptionsdk.model.KeyBlob; +import com.amazonaws.services.kms.AWSKMS; +import com.amazonaws.services.kms.AWSKMSClientBuilder; +import com.amazonaws.services.kms.model.DecryptRequest; +import com.amazonaws.services.kms.model.DecryptResult; +import com.amazonaws.services.kms.model.GenerateDataKeyRequest; +import org.junit.Test; +import org.junit.experimental.runners.Enclosed; +import org.junit.jupiter.api.DisplayName; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +import static org.junit.Assert.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; +import static org.mockito.Mockito.spy; + +@RunWith(Enclosed.class) +public class AwsKmsMrkAwareMasterKeyProviderTest { + + static public class getResourceForResourceTypeKey { + @Test + @DisplayName("Postcondition: Return the key id.") + public void basic_use() { + assertEquals( + "mrk-edb7fe6942894d32ac46dbb1c922d574", + AwsKmsMrkAwareMasterKeyProvider + .getResourceForResourceTypeKey("arn:aws:kms:us-west-2:111122223333:key/mrk-edb7fe6942894d32ac46dbb1c922d574")); + } + + @Test + @DisplayName("Check for early return (Postcondition): Non-ARNs may be raw resources.") + public void not_an_arn() { + assertEquals( + "mrk-edb7fe6942894d32ac46dbb1c922d574", + AwsKmsMrkAwareMasterKeyProvider + .getResourceForResourceTypeKey("mrk-edb7fe6942894d32ac46dbb1c922d574")); + final String malformed = "aws:kms:us-west-2::key/garbage"; + assertEquals( + malformed, + AwsKmsMrkAwareMasterKeyProvider + .getResourceForResourceTypeKey(malformed)); + } + + @Test + @DisplayName("Check for early return (Postcondition): Return the identifier for non-key resource types.") + public void not_a_key() { + final String alias = "arn:aws:kms:us-west-2:658956600833:alias/EncryptDecrypt"; + assertEquals( + alias, + AwsKmsMrkAwareMasterKeyProvider + .getResourceForResourceTypeKey(alias)); + } + } + + static public class assertMrksAreUnique { + @Test + //= compliance/framework/aws-kms/aws-kms-mrk-are-unique.txt#2.5 + //= type=test + //# The caller MUST provide: + public void basic_use() { + AwsKmsMrkAwareMasterKeyProvider + .assertMrksAreUnique(Arrays.asList( + "arn:aws:kms:us-west-2:111122223333:key/mrk-edb7fe6942894d32ac46dbb1c922d574" + )); + } + + @Test + public void no_duplicates() { + //= compliance/framework/aws-kms/aws-kms-mrk-are-unique.txt#2.5 + //= type=test + //# If there are zero duplicate resource ids between the multi-region + //# keys, this function MUST exit successfully + AwsKmsMrkAwareMasterKeyProvider + .assertMrksAreUnique(Arrays.asList( + "arn:aws:kms:us-west-2:111122223333:key/mrk-edb7fe6942894d32ac46dbb1c922d574", + "arn:aws:kms:us-west-2:658956600833:key/b3537ef1-d8dc-4780-9f5a-55776cbb2f7f" + )); + } + + @Test + //= compliance/framework/aws-kms/aws-kms-mrk-are-unique.txt#2.5 + //= type=test + //# If the list does not contain any multi-Region keys (aws-kms-key- + //# arn.md#identifying-an-aws-kms-multi-region-key) this function MUST + //# exit successfully. + public void no_mrks_at_all() { + AwsKmsMrkAwareMasterKeyProvider + .assertMrksAreUnique(Arrays.asList( + "arn:aws:kms:us-west-2:658956600833:key/b3537ef1-d8dc-4780-9f5a-55776cbb2f7f", + "arn:aws:kms:us-west-2:658956600833:key/b3537ef1-d8dc-4780-9f5a-55776cbb2f7f" + )); + } + + @Test + @DisplayName("Postcondition: Filter out duplicate resources that are not multi-region keys.") + public void non_mrk_duplicates_ok() { + AwsKmsMrkAwareMasterKeyProvider + .assertMrksAreUnique(Arrays.asList( + "arn:aws:kms:us-west-2:111122223333:key/mrk-edb7fe6942894d32ac46dbb1c922d574", + "arn:aws:kms:us-west-2:658956600833:key/b3537ef1-d8dc-4780-9f5a-55776cbb2f7f", + "arn:aws:kms:us-west-2:658956600833:key/b3537ef1-d8dc-4780-9f5a-55776cbb2f7f", + "arn:aws:kms:us-west-2:658956600833:alias/EncryptDecrypt", + "arn:aws:kms:us-west-2:658956600833:alias/EncryptDecrypt" + )); + } + + @Test + //= compliance/framework/aws-kms/aws-kms-mrk-are-unique.txt#2.5 + //= type=test + //# If any duplicate multi-region resource ids exist, this function MUST + //# yield an error that includes all identifiers with duplicate resource + //# ids not only the first duplicate found. + public void no_duplicate_mrks() { + assertThrows( + IllegalArgumentException.class, + () -> AwsKmsMrkAwareMasterKeyProvider + .assertMrksAreUnique(Arrays.asList( + "arn:aws:kms:us-west-2:111122223333:key/mrk-edb7fe6942894d32ac46dbb1c922d574", + "arn:aws:kms:us-east-1:111122223333:key/mrk-edb7fe6942894d32ac46dbb1c922d574" + ))); + } + } + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.6 + //= type=test + //# On initialization the caller MUST provide: + static public class AwsKmsMrkAwareMasterKeyProviderBuilderTests { + @Test + public void basic_use() { + final AwsKmsMrkAwareMasterKeyProvider strict = AwsKmsMrkAwareMasterKeyProvider + .builder() + .buildStrict("arn:aws:kms:us-west-2:111122223333:key/mrk-edb7fe6942894d32ac46dbb1c922d574"); + final AwsKmsMrkAwareMasterKeyProvider discovery = AwsKmsMrkAwareMasterKeyProvider + .builder() + .buildDiscovery(); + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.5 + //= type=test + //# MUST implement the Master Key Provider Interface (../master-key- + //# provider-interface.md#interface) + assertTrue(MasterKeyProvider.class.isInstance(strict)); + assertTrue(MasterKeyProvider.class.isInstance(discovery)); + + // These are not testable because of how the builder is structured. + // + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.6 + //= type=test + //# A discovery filter MUST NOT be configured in strict mode. + // + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.6 + //= type=test + //# A default MRK Region MUST NOT be configured in strict mode. + // + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.6 + //= type=test + //# In + //# discovery mode if a default MRK Region is not configured the AWS SDK + //# Default Region MUST be used. + // + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.6 + //= type=test + //# The key id list MUST be empty in discovery mode. + // + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.6 + //= type=test + //# The regional client + //# supplier MUST be defined in discovery mode. + } + + @Test + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.6 + //= type=test + //# The key id list MUST NOT be empty or null in strict mode. + public void no_noop() { + assertThrows(IllegalArgumentException.class, () -> AwsKmsMrkAwareMasterKeyProvider + .builder() + .buildStrict()); + assertThrows(IllegalArgumentException.class, () -> AwsKmsMrkAwareMasterKeyProvider + .builder() + .buildStrict(new ArrayList())); + } + + @Test + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.6 + //= type=test + //# The key id + //# list MUST NOT contain any null or empty string values. + public void no_null_identifiers() { + assertThrows(IllegalArgumentException.class, () -> AwsKmsMrkAwareMasterKeyProvider + .builder() + .buildStrict("arn:aws:kms:us-west-2:111122223333:key/mrk-edb7fe6942894d32ac46dbb1c922d574", "")); + + assertThrows(IllegalArgumentException.class, () -> AwsKmsMrkAwareMasterKeyProvider + .builder() + .buildStrict("arn:aws:kms:us-west-2:111122223333:key/mrk-edb7fe6942894d32ac46dbb1c922d574", null)); + + } + + @Test + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.6 + //= type=test + //# All AWS KMS + //# key identifiers are be passed to Assert AWS KMS MRK are unique (aws- + //# kms-mrk-are-unique.md#Implementation) and the function MUST return + //# success. + public void no_duplicate_mrks() { + assertThrows(IllegalArgumentException.class, () -> AwsKmsMrkAwareMasterKeyProvider + .builder() + .buildStrict( + "arn:aws:kms:us-west-2:111122223333:key/mrk-edb7fe6942894d32ac46dbb1c922d574", + "arn:aws:kms:us-east-1:111122223333:key/mrk-edb7fe6942894d32ac46dbb1c922d574")); + } + + @Test + @DisplayName("Precondition: A region is required to contact AWS KMS.") + public void always_need_a_region() { + assertThrows(AwsCryptoException.class, () -> AwsKmsMrkAwareMasterKeyProvider + .builder() + .withDefaultRegion(null) + .buildStrict( + "mrk-edb7fe6942894d32ac46dbb1c922d574")); + + AwsKmsMrkAwareMasterKeyProvider + .builder() + .withDefaultRegion("us-east-1") + .buildStrict( + "mrk-edb7fe6942894d32ac46dbb1c922d574"); + } + + @Test + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.6 + //= type=test + //# If an AWS SDK Default Region can not be + //# obtained initialization MUST fail. + public void discovery_region_can_not_be_null() { + assertThrows(IllegalArgumentException.class, () -> AwsKmsMrkAwareMasterKeyProvider + .builder() + // need to force the default region to `null` + // otherwise it may pick one up from the environment. + .withDefaultRegion(null) + .withDiscoveryMrkRegion(null) + .buildDiscovery()); + } + + @Test + public void basic_credentials_and_builder() { + BasicAWSCredentials creds = new BasicAWSCredentials("asdf", "qwer"); + AwsKmsMrkAwareMasterKeyProvider + .builder() + .withClientBuilder(AWSKMSClientBuilder.standard()) + .withCredentials(creds) + .buildDiscovery(); + } + } + + static public class extractRegion { + + @Test + public void basic_use() { + final String test = AwsKmsMrkAwareMasterKeyProvider + .extractRegion( + "us-east-1", + "us-east-2", + Optional.of("arn:aws:kms:us-west-2:111122223333:key/mrk-edb7fe6942894d32ac46dbb1c922d574"), + parseInfoFromKeyArn("arn:aws:kms:us-west-2:111122223333:key/mrk-edb7fe6942894d32ac46dbb1c922d574"), + false + ); + + assertEquals("us-west-2", test); + } + + @Test + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.7 + //= type=test + //# If the requested AWS KMS key identifier is not a well formed ARN the + //# AWS Region MUST be the configured default region this SHOULD be + //# obtained from the AWS SDK. + public void not_an_arn() { + final String test = AwsKmsMrkAwareMasterKeyProvider + .extractRegion( + "us-east-1", + "us-east-2", + Optional.empty(), + parseInfoFromKeyArn("mrk-edb7fe6942894d32ac46dbb1c922d574"), + false + ); + + assertEquals("us-east-1", test); + } + + @Test + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.7 + //= type=test + //# Otherwise if the requested AWS KMS key + //# identifier is identified as a multi-Region key (aws-kms-key- + //# arn.md#identifying-an-aws-kms-multi-region-key), then AWS Region MUST + //# be the region from the AWS KMS key ARN stored in the provider info + //# from the encrypted data key. + public void not_an_mrk() { + final String test = AwsKmsMrkAwareMasterKeyProvider + .extractRegion( + "us-east-1", + "us-east-2", + Optional.of("arn:aws:kms:us-west-2:658956600833:key/b3537ef1-d8dc-4780-9f5a-55776cbb2f7f"), + parseInfoFromKeyArn("arn:aws:kms:us-west-2:658956600833:key/b3537ef1-d8dc-4780-9f5a-55776cbb2f7f"), + false + ); + + assertEquals("us-west-2", test); + + final String test2 = AwsKmsMrkAwareMasterKeyProvider + .extractRegion( + "us-east-1", + "us-east-2", + Optional.of("arn:aws:kms:us-west-2:658956600833:alias/mrk-nasty"), + parseInfoFromKeyArn("arn:aws:kms:us-west-2:658956600833:alias/mrk-nasty"), + false + ); + + assertEquals("us-west-2", test2); + } + + @Test + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.7 + //= type=test + //# Otherwise if the mode is discovery then + //# the AWS Region MUST be the discovery MRK region. + public void mrk_in_discovery() { + final String test = AwsKmsMrkAwareMasterKeyProvider + .extractRegion( + "us-east-1", + "us-east-2", + Optional.empty(), + parseInfoFromKeyArn("arn:aws:kms:us-west-2:111122223333:key/mrk-edb7fe6942894d32ac46dbb1c922d574"), + true + ); + + assertEquals("us-east-2", test); + } + + @Test + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.7 + //= type=test + //# Finally if the + //# provider info is identified as a multi-Region key (aws-kms-key- + //# arn.md#identifying-an-aws-kms-multi-region-key) the AWS Region MUST + //# be the region from the AWS KMS key in the configured key ids matched + //# to the requested AWS KMS key by using AWS KMS MRK Match for Decrypt + //# (aws-kms-mrk-match-for-decrypt.md#implementation). + public void fuzzy_match_mrk() { + final String test = AwsKmsMrkAwareMasterKeyProvider + .extractRegion( + "us-east-1", + "us-east-2", + Optional.of("arn:aws:kms:us-west-2:111122223333:key/mrk-edb7fe6942894d32ac46dbb1c922d574"), + parseInfoFromKeyArn("arn:aws:kms:us-west-1:111122223333:key/mrk-edb7fe6942894d32ac46dbb1c922d574"), + false + ); + + assertEquals("us-west-2", test); + } + } + + static public class getMasterKey { + @Test + public void basic_use() { + final String identifier = "arn:aws:kms:us-west-2:111122223333:key/mrk-edb7fe6942894d32ac46dbb1c922d574"; + final AWSKMS client = spy(new MockKMSClient()); + final RegionalClientSupplier supplier = mock(RegionalClientSupplier.class); + when(supplier.getClient(any())).thenReturn(client); + + AwsKmsMrkAwareMasterKeyProvider mkp = AwsKmsMrkAwareMasterKeyProvider + .builder() + .withCustomClientFactory(supplier) + .buildStrict(identifier); + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.7 + //= type=test + //# The input MUST be the same as the Master Key Provider Get Master Key + //# (../master-key-provider-interface.md#get-master-key) interface. + AwsKmsMrkAwareMasterKey test = mkp.getMasterKey( + "aws-kms", + identifier); + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.7 + //= type=test + //# The output MUST be the same as the Master Key Provider Get Master Key + //# (../master-key-provider-interface.md#get-master-key) interface. + assertTrue(AwsKmsMrkAwareMasterKey.class.isInstance((test))); + + assertEquals(identifier, test.getKeyId()); + verify(supplier, times(1)).getClient("us-west-2"); + } + + @Test + public void basic_mrk_use() { + final String configuredIdentifier = "arn:aws:kms:us-west-2:111122223333:key/mrk-edb7fe6942894d32ac46dbb1c922d574"; + final String requestedIdentifier = "arn:aws:kms:us-east-1:111122223333:key/mrk-edb7fe6942894d32ac46dbb1c922d574"; + final AWSKMS client = spy(new MockKMSClient()); + final RegionalClientSupplier supplier = mock(RegionalClientSupplier.class); + when(supplier.getClient(any())).thenReturn(client); + + AwsKmsMrkAwareMasterKeyProvider mkp = AwsKmsMrkAwareMasterKeyProvider + .builder() + .withCustomClientFactory(supplier) + .buildStrict(configuredIdentifier); + + AwsKmsMrkAwareMasterKey test = mkp.getMasterKey( + "aws-kms", + requestedIdentifier); + + assertEquals(configuredIdentifier, test.getKeyId()); + verify(supplier, times(1)).getClient("us-west-2"); + } + + @Test + public void other_basic_uses() { + final AWSKMS client = spy(new MockKMSClient()); + final RegionalClientSupplier supplier = mock(RegionalClientSupplier.class); + when(supplier.getClient(any())).thenReturn(client); + + // A raw alias is a valid configuration for encryption + final String rawAliasIdentifier = "alias/my-alias"; + AwsKmsMrkAwareMasterKeyProvider + .builder() + .withCustomClientFactory(supplier) + .buildStrict(rawAliasIdentifier) + .getMasterKey( + "aws-kms", + rawAliasIdentifier); + + // A raw alias is a valid configuration for encryption + final String rawKeyIdentifier = "mrk-edb7fe6942894d32ac46dbb1c922d574"; + AwsKmsMrkAwareMasterKeyProvider + .builder() + .withCustomClientFactory(supplier) + .buildStrict(rawKeyIdentifier) + .getMasterKey( + "aws-kms", + rawKeyIdentifier); + } + + @Test + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.7 + //= type=test + //# The function MUST only provide master keys if the input provider id + //# equals "aws-kms". + public void only_this_provider() { + final String identifier = "arn:aws:kms:us-west-2:111122223333:key/mrk-edb7fe6942894d32ac46dbb1c922d574"; + final AWSKMS client = spy(new MockKMSClient()); + final RegionalClientSupplier supplier = mock(RegionalClientSupplier.class); + when(supplier.getClient(any())).thenReturn(client); + + AwsKmsMrkAwareMasterKeyProvider mkp = AwsKmsMrkAwareMasterKeyProvider + .builder() + .withCustomClientFactory(supplier) + .buildStrict(identifier); + + assertThrows(UnsupportedProviderException.class, () -> mkp.getMasterKey( + "not-aws-kms", + identifier)); + } + + @Test + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.7 + //= type=test + //# In strict mode, the requested AWS KMS key ARN MUST + //# match a member of the configured key ids by using AWS KMS MRK Match + //# for Decrypt (aws-kms-mrk-match-for-decrypt.md#implementation) + //# otherwise this function MUST error. + public void no_key_id_match() { + final String identifier = "arn:aws:kms:us-west-2:111122223333:key/mrk-edb7fe6942894d32ac46dbb1c922d574"; + final AWSKMS client = spy(new MockKMSClient()); + final RegionalClientSupplier supplier = mock(RegionalClientSupplier.class); + when(supplier.getClient(any())).thenReturn(client); + + final AwsKmsMrkAwareMasterKeyProvider mkp = AwsKmsMrkAwareMasterKeyProvider + .builder() + .withCustomClientFactory(supplier) + .buildStrict(identifier); + + assertThrows(NoSuchMasterKeyException.class, () -> mkp.getMasterKey( + "aws-kms", + "does-not-match-configured")); + } + + @Test + @DisplayName("Precondition: Discovery mode requires requestedKeyArn be an ARN.") + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.7 + //= type=test + //# In discovery mode, the requested + //# AWS KMS key identifier MUST be a well formed AWS KMS ARN. + public void discovery_request_must_be_arn() { + AwsKmsMrkAwareMasterKeyProvider mkp = AwsKmsMrkAwareMasterKeyProvider + .builder() + .buildDiscovery(); + + assertThrows(NoSuchMasterKeyException.class, + () -> mkp.getMasterKey( + "aws-kms", + "mrk-edb7fe6942894d32ac46dbb1c922d574")); + } + + @Test + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.7 + //= type=test + //# In + //# discovery mode if a discovery filter is configured the requested AWS + //# KMS key ARN's "partition" MUST match the discovery filter's + //# "partition" and the AWS KMS key ARN's "account" MUST exist in the + //# discovery filter's account id set. + public void discovery_filter_must_match() { + final String identifier = "arn:aws:kms:us-west-2:111122223333:key/mrk-edb7fe6942894d32ac46dbb1c922d574"; + final AWSKMS client = spy(new MockKMSClient()); + final RegionalClientSupplier supplier = mock(RegionalClientSupplier.class); + when(supplier.getClient(any())).thenReturn(client); + + assertThrows(NoSuchMasterKeyException.class, () -> AwsKmsMrkAwareMasterKeyProvider + .builder() + .buildDiscovery(new DiscoveryFilter("aws", Arrays.asList("not-111122223333"))) + .getMasterKey( + "aws-kms", + identifier) + ); + + assertThrows(NoSuchMasterKeyException.class, () -> AwsKmsMrkAwareMasterKeyProvider + .builder() + .buildDiscovery(new DiscoveryFilter("not-aws", Arrays.asList("111122223333"))) + .getMasterKey( + "aws-kms", + identifier) + ); + } + + @Test + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.7 + //= type=test + //# In discovery mode a AWS KMS MRK Aware Master Key (aws-kms-mrk-aware- + //# master-key.md) MUST be returned configured with + public void discovery_magic_to_make_the_region_match() { + final String identifier = "arn:aws:kms:us-west-2:111122223333:key/mrk-edb7fe6942894d32ac46dbb1c922d574"; + final AWSKMS client = spy(new MockKMSClient()); + final RegionalClientSupplier supplier = mock(RegionalClientSupplier.class); + when(supplier.getClient(any())).thenReturn(client); + + AwsKmsMrkAwareMasterKeyProvider mkp = AwsKmsMrkAwareMasterKeyProvider + .builder() + .withCustomClientFactory(supplier) + .withDiscoveryMrkRegion("my-region") + .buildDiscovery(); + + AwsKmsMrkAwareMasterKey test = mkp.getMasterKey( + "aws-kms", + identifier); + + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.7 + //= type=test + //# An AWS KMS client + //# MUST be obtained by calling the regional client supplier with this + //# AWS Region. + assertEquals( + "arn:aws:kms:my-region:111122223333:key/mrk-edb7fe6942894d32ac46dbb1c922d574", + test.getKeyId() + ); + verify(supplier, times(1)).getClient("my-region"); + } + + @Test + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.7 + //= type=test + //# In strict mode a AWS KMS MRK Aware Master Key (aws-kms-mrk-aware- + //# master-key.md) MUST be returned configured with + public void strict_mrk_region_match() { + final String identifier = "arn:aws:kms:us-west-2:111122223333:key/mrk-edb7fe6942894d32ac46dbb1c922d574"; + final String configIdentifier = "arn:aws:kms:us-east-1:111122223333:key/mrk-edb7fe6942894d32ac46dbb1c922d574"; + final AWSKMS client = spy(new MockKMSClient()); + final RegionalClientSupplier supplier = mock(RegionalClientSupplier.class); + when(supplier.getClient(any())).thenReturn(client); + + AwsKmsMrkAwareMasterKeyProvider mkp = AwsKmsMrkAwareMasterKeyProvider + .builder() + .withCustomClientFactory(supplier) + .buildStrict(configIdentifier); + + AwsKmsMrkAwareMasterKey test = mkp.getMasterKey( + "aws-kms", + identifier); + + assertEquals( + configIdentifier, + test.getKeyId() + ); + verify(supplier, times(1)).getClient("us-east-1"); + } + } + + static public class decryptDataKey { + + @Test + public void basic_use() { + final String identifier = "arn:aws:kms:us-west-2:111122223333:key/mrk-edb7fe6942894d32ac46dbb1c922d574"; + final CryptoAlgorithm ALGORITHM_SUITE = CryptoAlgorithm.ALG_AES_256_GCM_IV12_TAG16_HKDF_SHA256; + final List GRANT_TOKENS = Collections.singletonList("testGrantToken"); + final Map ENCRYPTION_CONTEXT = Collections.singletonMap("myKey", "myValue"); + final byte[] cipherText = new byte[10]; + final EncryptedDataKey edk1 = new KeyBlob( + "aws-kms", + identifier.getBytes(StandardCharsets.UTF_8), + cipherText); + final EncryptedDataKey edk2 = new KeyBlob( + "aws-kms", + identifier.getBytes(StandardCharsets.UTF_8), + cipherText); + + final RegionalClientSupplier supplier = mock(RegionalClientSupplier.class); + final AWSKMS client = mock(AWSKMS.class); + when(client.decrypt(any())) + .thenReturn(new DecryptResult() + .withKeyId(identifier) + .withPlaintext(ByteBuffer.allocate(ALGORITHM_SUITE.getDataKeyLength()))); + when(supplier.getClient(any())).thenReturn(client); + + AwsKmsMrkAwareMasterKeyProvider mkp = AwsKmsMrkAwareMasterKeyProvider + .builder() + .withCustomClientFactory(supplier) + .buildStrict(identifier) + .withGrantTokens(GRANT_TOKENS); + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.9 + //= type=test + //# The input MUST be the same as the Master Key Provider Decrypt Data + //# Key (../master-key-provider-interface.md#decrypt-data-key) interface. + final DataKey test = mkp + .decryptDataKey( + ALGORITHM_SUITE, + Arrays.asList(edk1, edk2), + ENCRYPTION_CONTEXT); + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.9 + //= type=test + //# For each encrypted data key in the filtered set, one at a time, the + //# master key provider MUST call Get Master Key (aws-kms-mrk-aware- + //# master-key-provider.md#get-master-key) with the encrypted data key's + //# provider info as the AWS KMS key ARN. + // + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.9 + //= type=test + //# It MUST call Decrypt Data Key + //# (aws-kms-mrk-aware-master-key.md#decrypt-data-key) on this master key + //# with the input algorithm, this single encrypted data key, and the + //# input encryption context. + // + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.9 + //= type=test + //# If the decrypt data key call is + //# successful, then this function MUST return this result and not + //# attempt to decrypt any more encrypted data keys. + verify(client, times((1))).decrypt(new DecryptRequest() + .withGrantTokens(GRANT_TOKENS) + .withEncryptionContext(ENCRYPTION_CONTEXT) + .withKeyId(identifier) + .withCiphertextBlob(ByteBuffer.wrap(cipherText)) + ); + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.9 + //= type=test + //# The output MUST be the same as the Master Key Provider Decrypt Data + //# Key (../master-key-provider-interface.md#decrypt-data-key) interface. + assertTrue(DataKey.class.isInstance(test)); + } + + @Test + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.9 + //= type=test + //# The set of encrypted data keys MUST first be filtered to match this + //# master key's configuration. + public void only_if_providers_match() { + final String identifier = "arn:aws:kms:us-west-2:111122223333:key/mrk-edb7fe6942894d32ac46dbb1c922d574"; + final CryptoAlgorithm ALGORITHM_SUITE = CryptoAlgorithm.ALG_AES_256_GCM_IV12_TAG16_HKDF_SHA256; + final Map ENCRYPTION_CONTEXT = Collections.singletonMap("myKey", "myValue"); + final EncryptedDataKey edk = new KeyBlob( + "not-aws-kms", + "not the identifier".getBytes(StandardCharsets.UTF_8), + new byte[10]); + + AwsKmsMrkAwareMasterKeyProvider mkp = AwsKmsMrkAwareMasterKeyProvider + .builder() + .buildStrict(identifier); + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.9 + //= type=test + //# To match the encrypted data key's + //# provider ID MUST exactly match the value "aws-kms". + final CannotUnwrapDataKeyException test = assertThrows( + "Unable to decrypt any data keys", + CannotUnwrapDataKeyException.class, () -> mkp + .decryptDataKey( + ALGORITHM_SUITE, + Arrays.asList(edk), + ENCRYPTION_CONTEXT)); + assertEquals(0, test.getSuppressed().length); + } + + @Test + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.9 + //= type=test + //# Additionally + //# each provider info MUST be a valid AWS KMS ARN (aws-kms-key-arn.md#a- + //# valid-aws-kms-arn) with a resource type of "key". + public void provider_info_must_be_arn() { + final String identifier = "arn:aws:kms:us-west-2:111122223333:key/mrk-edb7fe6942894d32ac46dbb1c922d574"; + final String aliasArn = "arn:aws:kms:us-west-2:111122223333:alias/mrk-edb7fe6942894d32ac46dbb1c922d574"; + final CryptoAlgorithm ALGORITHM_SUITE = CryptoAlgorithm.ALG_AES_256_GCM_IV12_TAG16_HKDF_SHA256; + final Map ENCRYPTION_CONTEXT = Collections.singletonMap("myKey", "myValue"); + final EncryptedDataKey edk = new KeyBlob( + "aws-kms", + aliasArn.getBytes(StandardCharsets.UTF_8), + new byte[10]); + + AwsKmsMrkAwareMasterKeyProvider mkp = AwsKmsMrkAwareMasterKeyProvider + .builder() + .buildStrict(identifier); + + final IllegalStateException test = assertThrows( + "Invalid provider info in message.", + IllegalStateException.class, () -> mkp + .decryptDataKey( + ALGORITHM_SUITE, + Arrays.asList(edk), + ENCRYPTION_CONTEXT)); + assertEquals(0, test.getSuppressed().length); + } + + @Test + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.9 + //= type=test + //# If this attempt results in an error, then + //# these errors MUST be collected. + // + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.9 + //= type=test + //# If all the input encrypted data keys have been processed then this + //# function MUST yield an error that includes all the collected errors. + public void exception_wrapped() { + final String identifier = "arn:aws:kms:us-west-2:111122223333:key/mrk-edb7fe6942894d32ac46dbb1c922d574"; + final CryptoAlgorithm ALGORITHM_SUITE = CryptoAlgorithm.ALG_AES_256_GCM_IV12_TAG16_HKDF_SHA256; + final Map ENCRYPTION_CONTEXT = Collections.singletonMap("myKey", "myValue"); + final EncryptedDataKey edk = new KeyBlob( + "aws-kms", + identifier.getBytes(StandardCharsets.UTF_8), + new byte[10]); + + final RegionalClientSupplier supplier = mock(RegionalClientSupplier.class); + final AWSKMS client = mock(AWSKMS.class); + final String clientErrMsg = "asdf"; + when(client.decrypt(any())).thenThrow(new AmazonServiceException(clientErrMsg)); + when(supplier.getClient(any())).thenReturn(client); + + AwsKmsMrkAwareMasterKeyProvider mkp = AwsKmsMrkAwareMasterKeyProvider + .builder() + .withCustomClientFactory(supplier) + .buildStrict(identifier); + + CannotUnwrapDataKeyException test = assertThrows( + "Unable to decrypt any data keys", + CannotUnwrapDataKeyException.class, () -> mkp + .decryptDataKey( + ALGORITHM_SUITE, + Arrays.asList(edk), + ENCRYPTION_CONTEXT)); + assertEquals(1, test.getSuppressed().length); + Throwable fromMasterKey = Arrays.stream(test.getSuppressed()).findFirst().get(); + assertTrue(fromMasterKey instanceof CannotUnwrapDataKeyException); + assertEquals(1, fromMasterKey.getSuppressed().length); + Throwable fromClient = Arrays.stream(fromMasterKey.getSuppressed()).findFirst().get(); + assertTrue(fromClient instanceof AmazonServiceException); + assertTrue(fromClient.getMessage().startsWith(clientErrMsg)); + } + } + + static public class clientFactory { + @Test + public void basic_use() { + final ConcurrentHashMap cache = spy(new ConcurrentHashMap<>()); + + final AWSKMS test = AwsKmsMrkAwareMasterKeyProvider + .Builder + .clientFactory(cache, null) + .getClient("asdf"); + assertNotEquals(null, test); + verify(cache, times(1)).containsKey("asdf"); + } + + @Test + @DisplayName("Check for early return (Postcondition): If a client already exists, use that.") + public void use_clients_that_exist() { + final String region = "asdf"; + final ConcurrentHashMap cache = spy(new ConcurrentHashMap<>()); + // Add something so we can verify that we get it + final AWSKMS client = mock(AWSKMS.class); + cache.put(region, client); + + final AWSKMS test = AwsKmsMrkAwareMasterKeyProvider + .Builder + .clientFactory(cache, null) + .getClient(region); + + assertEquals(client, test); + } + } + + static public class getMasterKeysForEncryption { + @Test + public void basic_use() { + final String identifier = "arn:aws:kms:us-west-2:111122223333:key/mrk-edb7fe6942894d32ac46dbb1c922d574"; + final AWSKMS client = spy(new MockKMSClient()); + final RegionalClientSupplier supplier = mock(RegionalClientSupplier.class); + when(supplier.getClient("us-west-2")).thenReturn(client); + final MasterKeyRequest request = MasterKeyRequest.newBuilder().build(); + + final AwsKmsMrkAwareMasterKeyProvider mkp = AwsKmsMrkAwareMasterKeyProvider + .builder() + .withCustomClientFactory(supplier) + .buildStrict(identifier); + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.8 + //= type=test + //# The input MUST be the same as the Master Key Provider Get Master Keys + //# For Encryption (../master-key-provider-interface.md#get-master-keys- + //# for-encryption) interface. + // + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.8 + //= type=test + //# The output MUST be the same as the Master Key Provider Get Master + //# Keys For Encryption (../master-key-provider-interface.md#get-master- + //# keys-for-encryption) interface. + final List test = mkp.getMasterKeysForEncryption(request); + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.8 + //= type=test + //# If the configured mode is strict this function MUST return a + //# list of master keys obtained by calling Get Master Key (aws-kms-mrk- + //# aware-master-key-provider.md#get-master-key) for each AWS KMS key + //# identifier in the configured key ids + assertEquals(1, test.size()); + assertEquals(identifier, test.get(0).getKeyId()); + } + + @Test + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.8 + //= type=test + //# If the configured mode is discovery the function MUST return an empty + //# list. + public void no_keys_is_empty_list() { + final AwsKmsMrkAwareMasterKeyProvider mkp = AwsKmsMrkAwareMasterKeyProvider + .builder() + .buildDiscovery(); + + final List test = mkp.getMasterKeysForEncryption(MasterKeyRequest.newBuilder().build()); + assertEquals(0, test.size()); + } + } +} diff --git a/src/test/java/com/amazonaws/encryptionsdk/kms/AwsKmsMrkAwareMasterKeyTest.java b/src/test/java/com/amazonaws/encryptionsdk/kms/AwsKmsMrkAwareMasterKeyTest.java new file mode 100644 index 000000000..b87527b8b --- /dev/null +++ b/src/test/java/com/amazonaws/encryptionsdk/kms/AwsKmsMrkAwareMasterKeyTest.java @@ -0,0 +1,913 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.amazonaws.encryptionsdk.kms; + +import com.amazonaws.AmazonServiceException; +import com.amazonaws.RequestClientOptions; +import com.amazonaws.encryptionsdk.*; +import com.amazonaws.encryptionsdk.exception.CannotUnwrapDataKeyException; +import com.amazonaws.encryptionsdk.internal.AwsKmsCmkArnInfo; +import com.amazonaws.encryptionsdk.internal.VersionInfo; +import com.amazonaws.encryptionsdk.model.KeyBlob; +import com.amazonaws.services.kms.AWSKMS; +import com.amazonaws.services.kms.model.*; +import org.junit.Test; +import org.junit.experimental.runners.Enclosed; +import org.junit.jupiter.api.DisplayName; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; + +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import java.lang.reflect.Method; +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 static com.amazonaws.encryptionsdk.internal.RandomBytesGenerator.generate; +import static org.junit.Assert.*; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.*; +import static org.mockito.Mockito.mock; + +@RunWith(Enclosed.class) +public class AwsKmsMrkAwareMasterKeyTest { + + public static class getInstance { + + @Test + public void basic_use() { + AWSKMS client = spy(new MockKMSClient()); + MasterKeyProvider mkp = mock(MasterKeyProvider.class); + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.6 + //= type=test + //# On initialization, the caller MUST provide: + final AwsKmsMrkAwareMasterKey test = AwsKmsMrkAwareMasterKey + .getInstance( + client, + "arn:aws:kms:us-west-2:658956600833:key/b3537ef1-d8dc-4780-9f5a-55776cbb2f7f", + mkp); + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.5 + //= type=test + //# MUST implement the Master Key Interface (../master-key- + //# interface.md#interface) + assertTrue(MasterKey.class.isInstance(test)); + } + + @Test + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.6 + //= type=test + //# The AWS KMS key identifier MUST NOT be null or empty. + public void requires_valid_identifiers() { + AWSKMS client = spy(new MockKMSClient()); + MasterKeyProvider mkp = mock(MasterKeyProvider.class); + + assertThrows(IllegalArgumentException.class, () -> AwsKmsMrkAwareMasterKey + .getInstance( + client, + "", + mkp)); + assertThrows(IllegalArgumentException.class, () -> AwsKmsMrkAwareMasterKey + .getInstance( + client, + null, + mkp)); + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.6 + //= type=test + //# The AWS KMS + //# key identifier MUST be a valid identifier (aws-kms-key-arn.md#a- + //# valid-aws-kms-identifier). + assertThrows(IllegalArgumentException.class, () -> AwsKmsMrkAwareMasterKey + .getInstance( + client, + "arn:aws:dynamodb:us-east-2:123456789012:table/myDynamoDBTable", + mkp)); + } + + @Test + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.6 + //= type=test + //# The AWS KMS SDK client MUST not be null. + public void requires_valid_client() { + MasterKeyProvider mkp = mock(MasterKeyProvider.class); + + assertThrows(IllegalArgumentException.class, () -> AwsKmsMrkAwareMasterKey + .getInstance( + null, + "arn:aws:kms:us-west-2:658956600833:key/b3537ef1-d8dc-4780-9f5a-55776cbb2f7f", + mkp)); + } + + @Test + @DisplayName("Precondition: A provider is required.") + public void requires_valid_provider() { + AWSKMS client = spy(new MockKMSClient()); + + assertThrows(IllegalArgumentException.class, () -> AwsKmsMrkAwareMasterKey + .getInstance( + client, + "arn:aws:kms:us-west-2:658956600833:key/b3537ef1-d8dc-4780-9f5a-55776cbb2f7f", + null)); + } + } + + public static class generateDataKey { + + @Test + public void basic_use() { + final CryptoAlgorithm ALGORITHM_SUITE = CryptoAlgorithm.ALG_AES_256_GCM_IV12_TAG16_HKDF_SHA256; + final List GRANT_TOKENS = Collections.singletonList("testGrantToken"); + final Map ENCRYPTION_CONTEXT = Collections.singletonMap("myKey", "myValue"); + final String keyIdentifier = "arn:aws:kms:us-west-2:658956600833:key/b3537ef1-d8dc-4780-9f5a-55776cbb2f7f"; + final ByteBuffer udk = ByteBuffer.allocate(ALGORITHM_SUITE.getDataKeyLength()); + final ByteBuffer ciphertext = ByteBuffer.allocate(10); + + final AWSKMS client = mock(AWSKMS.class); + when(client.generateDataKey(any())) + .thenReturn(new GenerateDataKeyResult() + .withPlaintext(udk) + .withKeyId(keyIdentifier) + .withCiphertextBlob(ciphertext)); + final MasterKeyProvider mkp = mock(MasterKeyProvider.class); + when(mkp.getDefaultProviderId()).thenReturn("aws-kms"); + AwsKmsMrkAwareMasterKey masterKey = AwsKmsMrkAwareMasterKey + .getInstance( + client, + keyIdentifier, + mkp); + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.6 + //= type=test + //# The master key MUST be able to be configured with an optional list of + //# Grant Tokens. + masterKey.setGrantTokens(GRANT_TOKENS); + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.10 + //= type=test + //# The inputs MUST be the same as the Master Key Generate Data Key + //# (../master-key-interface.md#generate-data-key) interface. + DataKey test = masterKey.generateDataKey(ALGORITHM_SUITE, ENCRYPTION_CONTEXT); + ArgumentCaptor gr = ArgumentCaptor.forClass(GenerateDataKeyRequest.class); + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.10 + //= type=test + //# This + //# master key MUST use the configured AWS KMS client to make an AWS KMS + //# GenerateDatakey (https://docs.aws.amazon.com/kms/latest/APIReference/ + //# API_GenerateDataKey.html) request constructed as follows: + verify(client, times(1)).generateDataKey(gr.capture()); + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.10 + //= type=test + //# The output MUST be the same as the Master Key Generate Data Key + //# (../master-key-interface.md#generate-data-key) interface. + assertTrue(DataKey.class.isInstance(test)); + + GenerateDataKeyRequest actualRequest = gr.getValue(); + + assertEquals(keyIdentifier, actualRequest.getKeyId()); + assertEquals(GRANT_TOKENS, actualRequest.getGrantTokens()); + assertEquals(ENCRYPTION_CONTEXT, actualRequest.getEncryptionContext()); + assertEquals(ALGORITHM_SUITE.getDataKeyLength(), actualRequest.getNumberOfBytes().longValue()); + assertTrue(actualRequest.getRequestClientOptions().getClientMarker(RequestClientOptions.Marker.USER_AGENT) + .contains(VersionInfo.loadUserAgent())); + + assertNotNull(test.getKey()); + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.10 + //= type=test + //# The response's "Plaintext" MUST be the plaintext in + //# the output. + assertEquals(ALGORITHM_SUITE.getDataKeyLength(), test.getKey().getEncoded().length); + assertEquals(ALGORITHM_SUITE.getDataKeyAlgo(), test.getKey().getAlgorithm()); + assertNotNull(test.getEncryptedDataKey()); + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.10 + //= type=test + //# The response's cipher text blob MUST be used as the + //# returned as the ciphertext for the encrypted data key in the output. + assertEquals(10, test.getEncryptedDataKey().length); + + } + + @Test + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.10 + //= type=test + //# If the call succeeds the AWS KMS Generate Data Key response's + //# "Plaintext" MUST match the key derivation input length specified by + //# the algorithm suite included in the input. + public void length_must_match() { + final CryptoAlgorithm ALGORITHM_SUITE = CryptoAlgorithm.ALG_AES_256_GCM_IV12_TAG16_HKDF_SHA256; + final Map ENCRYPTION_CONTEXT = Collections.singletonMap("myKey", "myValue"); + final String keyIdentifier = "arn:aws:kms:us-west-2:658956600833:key/b3537ef1-d8dc-4780-9f5a-55776cbb2f7f"; + // I use more, because less _should_ trigger an underflow... but the condition should _always_ fail + final int wrongLength = ALGORITHM_SUITE.getDataKeyLength() + 1; + + final AWSKMS client = mock(AWSKMS.class); + when(client.generateDataKey(any())) + .thenReturn(new GenerateDataKeyResult() + .withPlaintext(ByteBuffer.allocate(wrongLength)) + .withKeyId(keyIdentifier) + .withCiphertextBlob(ByteBuffer.allocate(10))); + final MasterKeyProvider mkp = mock(MasterKeyProvider.class); + when(mkp.getDefaultProviderId()).thenReturn("aws-kms"); + AwsKmsMrkAwareMasterKey masterKey = AwsKmsMrkAwareMasterKey + .getInstance( + client, + keyIdentifier, + mkp); + + assertThrows(IllegalStateException.class, + () -> masterKey.generateDataKey(ALGORITHM_SUITE, ENCRYPTION_CONTEXT)); + } + + @Test + @DisplayName("Exceptional Postcondition: Must have an AWS KMS ARN from AWS KMS generateDataKey.") + public void need_an_arn() { + final CryptoAlgorithm ALGORITHM_SUITE = CryptoAlgorithm.ALG_AES_256_GCM_IV12_TAG16_HKDF_SHA256; + final Map ENCRYPTION_CONTEXT = Collections.singletonMap("myKey", "myValue"); + final String keyIdentifier = "arn:aws:kms:us-west-2:658956600833:key/b3537ef1-d8dc-4780-9f5a-55776cbb2f7f"; + + final AWSKMS client = mock(AWSKMS.class); + when(client.generateDataKey(any())) + .thenReturn(new GenerateDataKeyResult() + .withPlaintext(ByteBuffer.allocate(ALGORITHM_SUITE.getDataKeyLength())) + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.10 + //= type=test + //# The response's "KeyId" + //# MUST be valid. + .withKeyId("b3537ef1-d8dc-4780-9f5a-55776cbb2f7f") + .withCiphertextBlob(ByteBuffer.allocate(10))); + final MasterKeyProvider mkp = mock(MasterKeyProvider.class); + when(mkp.getDefaultProviderId()).thenReturn("aws-kms"); + AwsKmsMrkAwareMasterKey masterKey = AwsKmsMrkAwareMasterKey + .getInstance( + client, + keyIdentifier, + mkp); + + assertThrows(IllegalStateException.class, + () -> masterKey.generateDataKey(ALGORITHM_SUITE, ENCRYPTION_CONTEXT)); + } + } + + public static class encryptDataKey { + @Test + public void basic_use() { + final CryptoAlgorithm ALGORITHM_SUITE = CryptoAlgorithm.ALG_AES_256_GCM_IV12_TAG16_HKDF_SHA256; + final List GRANT_TOKENS = Collections.singletonList("testGrantToken"); + final Map ENCRYPTION_CONTEXT = Collections.singletonMap("myKey", "myValue"); + final String keyIdentifier = "arn:aws:kms:us-west-2:658956600833:key/b3537ef1-d8dc-4780-9f5a-55776cbb2f7f"; + final SecretKey SECRET_KEY = new SecretKeySpec(generate(ALGORITHM_SUITE.getDataKeyLength()), ALGORITHM_SUITE.getDataKeyAlgo()); + + final MasterKeyProvider mkp = mock(MasterKeyProvider.class); + when(mkp.getDefaultProviderId()).thenReturn("aws-kms"); + + final DataKey dataKey = new DataKey(SECRET_KEY, new byte[0], + "aws-kms".getBytes(StandardCharsets.UTF_8), mock(MasterKey.class)); + + final AWSKMS client = mock(AWSKMS.class); + when(client.encrypt(any())) + .thenReturn(new EncryptResult() + .withKeyId(keyIdentifier) + .withCiphertextBlob(ByteBuffer.allocate(10))); + + AwsKmsMrkAwareMasterKey masterKey = AwsKmsMrkAwareMasterKey + .getInstance( + client, + keyIdentifier, + mkp); + masterKey.setGrantTokens(GRANT_TOKENS); + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.11 + //= type=test + //# The inputs MUST be the same as the Master Key Encrypt Data Key + //# (../master-key-interface.md#encrypt-data-key) interface. + DataKey test = masterKey.encryptDataKey(ALGORITHM_SUITE, ENCRYPTION_CONTEXT, dataKey); + + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.11 + //= type=test + //# The output MUST be the same as the Master Key Encrypt Data Key + //# (../master-key-interface.md#encrypt-data-key) interface. + assertTrue(DataKey.class.isInstance(test)); + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.11 + //= type=test + //# The master + //# key MUST use the configured AWS KMS client to make an AWS KMS Encrypt + //# (https://docs.aws.amazon.com/kms/latest/APIReference/ + //# API_Encrypt.html) request constructed as follows: + verify(client, times(1)).encrypt(any()); + ArgumentCaptor gr = ArgumentCaptor.forClass(EncryptRequest.class); + verify(client, times(1)).encrypt(gr.capture()); + + final EncryptRequest actualRequest = gr.getValue(); + + assertEquals(keyIdentifier, actualRequest.getKeyId()); + assertEquals(GRANT_TOKENS, actualRequest.getGrantTokens()); + assertEquals(ENCRYPTION_CONTEXT, actualRequest.getEncryptionContext()); + assertTrue(actualRequest.getRequestClientOptions().getClientMarker(RequestClientOptions.Marker.USER_AGENT) + .contains(VersionInfo.loadUserAgent())); + + assertNotNull(test.getKey()); + assertEquals(ALGORITHM_SUITE.getDataKeyLength(), test.getKey().getEncoded().length); + assertEquals(ALGORITHM_SUITE.getDataKeyAlgo(), test.getKey().getAlgorithm()); + assertNotNull(test.getEncryptedDataKey()); + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.11 + //= type=test + //# The + //# response's cipher text blob MUST be used as the "ciphertext" for the + //# encrypted data key. + assertEquals(10, test.getEncryptedDataKey().length); + } + + @Test + @DisplayName("Precondition: The key format MUST be RAW.") + public void secret_key_must_be_raw() { + final CryptoAlgorithm ALGORITHM_SUITE = CryptoAlgorithm.ALG_AES_256_GCM_IV12_TAG16_HKDF_SHA256; + final List GRANT_TOKENS = Collections.singletonList("testGrantToken"); + final Map ENCRYPTION_CONTEXT = Collections.singletonMap("myKey", "myValue"); + final String keyIdentifier = "arn:aws:kms:us-west-2:658956600833:key/b3537ef1-d8dc-4780-9f5a-55776cbb2f7f"; + final MasterKeyProvider mkp = mock(MasterKeyProvider.class); + when(mkp.getDefaultProviderId()).thenReturn("aws-kms"); + + // Test "stuff" here + final SecretKey SECRET_KEY = mock(SecretKeySpec.class); + when(SECRET_KEY.getFormat()).thenReturn("NOT-RAW"); + + final DataKey dataKey = new DataKey(SECRET_KEY, new byte[0], + "aws-kms".getBytes(StandardCharsets.UTF_8), mock(MasterKey.class)); + + final AWSKMS client = mock(AWSKMS.class); + when(client.encrypt(any())) + .thenReturn(new EncryptResult() + .withKeyId(keyIdentifier) + .withCiphertextBlob(ByteBuffer.allocate(10))); + + AwsKmsMrkAwareMasterKey masterKey = AwsKmsMrkAwareMasterKey + .getInstance( + client, + keyIdentifier, + mkp); + masterKey.setGrantTokens(GRANT_TOKENS); + + assertThrows( + "Only RAW encoded keys are supported", + IllegalArgumentException.class, + () -> masterKey.encryptDataKey(ALGORITHM_SUITE, ENCRYPTION_CONTEXT, dataKey)); + } + + @Test + @DisplayName("Postcondition: Must have an AWS KMS ARN from AWS KMS encrypt.") + public void need_an_arn() { + final CryptoAlgorithm ALGORITHM_SUITE = CryptoAlgorithm.ALG_AES_256_GCM_IV12_TAG16_HKDF_SHA256; + final List GRANT_TOKENS = Collections.singletonList("testGrantToken"); + final Map ENCRYPTION_CONTEXT = Collections.singletonMap("myKey", "myValue"); + final String keyIdentifier = "arn:aws:kms:us-west-2:658956600833:key/b3537ef1-d8dc-4780-9f5a-55776cbb2f7f"; + final SecretKey SECRET_KEY = new SecretKeySpec(generate(ALGORITHM_SUITE.getDataKeyLength()), ALGORITHM_SUITE.getDataKeyAlgo()); + + final MasterKeyProvider mkp = mock(MasterKeyProvider.class); + when(mkp.getDefaultProviderId()).thenReturn("aws-kms"); + + final DataKey dataKey = new DataKey(SECRET_KEY, new byte[0], + "aws-kms".getBytes(StandardCharsets.UTF_8), mock(MasterKey.class)); + + final AWSKMS client = mock(AWSKMS.class); + when(client.encrypt(any())) + .thenReturn(new EncryptResult() + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.11 + //= type=test + //# The AWS KMS Encrypt response MUST contain a valid "KeyId". + .withKeyId("b3537ef1-d8dc-4780-9f5a-55776cbb2f7f") + .withCiphertextBlob(ByteBuffer.allocate(10))); + + AwsKmsMrkAwareMasterKey masterKey = AwsKmsMrkAwareMasterKey + .getInstance( + client, + keyIdentifier, + mkp); + masterKey.setGrantTokens(GRANT_TOKENS); + + assertThrows(IllegalStateException.class, + () -> masterKey.encryptDataKey(ALGORITHM_SUITE, ENCRYPTION_CONTEXT, dataKey)); + } + } + + public static class filterEncryptedDataKeys { + @Test + public void basic_use() { + final String keyIdentifier = "arn:aws:kms:us-west-2:658956600833:key/b3537ef1-d8dc-4780-9f5a-55776cbb2f7f"; + final String providerId = "aws-kms"; + final EncryptedDataKey edk = new KeyBlob( + providerId, + keyIdentifier.getBytes(StandardCharsets.UTF_8), + new byte[10]); + + assertTrue(AwsKmsMrkAwareMasterKey.filterEncryptedDataKeys( + providerId, + keyIdentifier, + edk)); + + } + + @Test + public void mrk_specific() { + /* This may be overkill, + * but the whole point + * of multi-region optimization + * is this fuzzy match. + */ + final String configuredIdentifier = "arn:aws:kms:us-west-2:111122223333:key/mrk-edb7fe6942894d32ac46dbb1c922d574"; + final String ekdIdentifier = "arn:aws:kms:us-east-2:111122223333:key/mrk-edb7fe6942894d32ac46dbb1c922d574"; + + final String providerId = "aws-kms"; + final EncryptedDataKey edk = new KeyBlob( + providerId, + ekdIdentifier.getBytes(StandardCharsets.UTF_8), + new byte[10]); + + assertTrue(AwsKmsMrkAwareMasterKey.filterEncryptedDataKeys( + providerId, + configuredIdentifier, + edk)); + + } + + @Test + public void provider_info_must_be_arn() { + final String configuredIdentifier = "arn:aws:kms:us-west-2:111122223333:key/mrk-edb7fe6942894d32ac46dbb1c922d574"; + final String rawKeyId = "mrk-edb7fe6942894d32ac46dbb1c922d574"; + final String alias = "arn:aws:kms:us-west-2:111122223333:alias/mrk-edb7fe6942894d32ac46dbb1c922d574"; + + final String providerId = "aws-kms"; + final EncryptedDataKey edkNotArn = new KeyBlob( + providerId, + rawKeyId.getBytes(StandardCharsets.UTF_8), + new byte[10]); + + final EncryptedDataKey edkAliasArn = new KeyBlob( + providerId, + rawKeyId.getBytes(StandardCharsets.UTF_8), + new byte[10]); + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.9 + //= type=test + //# Additionally each provider info MUST be a valid AWS KMS ARN + //# (aws-kms-key-arn.md#a-valid-aws-kms-arn) with a resource type of + //# "key". + assertThrows( + IllegalStateException.class, + () -> AwsKmsMrkAwareMasterKey.filterEncryptedDataKeys( + providerId, + configuredIdentifier, + edkNotArn)); + assertThrows( + IllegalStateException.class, + () -> AwsKmsMrkAwareMasterKey.filterEncryptedDataKeys( + providerId, + configuredIdentifier, + edkAliasArn)); + + } + + @Test + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.9 + //= type=test + //# To match the encrypted data key's + //# provider ID MUST exactly match the value "aws-kms" and the the + //# function AWS KMS MRK Match for Decrypt (aws-kms-mrk-match-for- + //# decrypt.md#implementation) called with the configured AWS KMS key + //# identifier and the encrypted data key's provider info MUST return + //# "true". + public void may_not_match() { + final String keyIdentifier = "arn:aws:kms:us-west-2:658956600833:key/b3537ef1-d8dc-4780-9f5a-55776cbb2f7f"; + final String providerId = "aws-kms"; + final EncryptedDataKey edk = new KeyBlob( + providerId, + keyIdentifier.getBytes(StandardCharsets.UTF_8), + new byte[10]); + + assertFalse(AwsKmsMrkAwareMasterKey.filterEncryptedDataKeys( + "not-aws-kms", + keyIdentifier, + edk)); + + assertFalse(AwsKmsMrkAwareMasterKey.filterEncryptedDataKeys( + providerId, + "arn:aws:kms:us-west-2:111122223333:key/mrk-edb7fe6942894d32ac46dbb1c922d574", + edk)); + } + } + + public static class decryptSingleEncryptedDataKey { + @Test + public void basic_use() { + final CryptoAlgorithm ALGORITHM_SUITE = CryptoAlgorithm.ALG_AES_256_GCM_IV12_TAG16_HKDF_SHA256; + final List GRANT_TOKENS = Collections.singletonList("testGrantToken"); + final Map ENCRYPTION_CONTEXT = Collections.singletonMap("myKey", "myValue"); + final String keyIdentifier = "arn:aws:kms:us-west-2:658956600833:key/b3537ef1-d8dc-4780-9f5a-55776cbb2f7f"; + final String providerId = "aws-kms"; + final EncryptedDataKey edk = new KeyBlob( + providerId, + keyIdentifier.getBytes(StandardCharsets.UTF_8), + new byte[10]); + + final MasterKeyProvider mkp = mock(MasterKeyProvider.class); + when(mkp.getDefaultProviderId()).thenReturn(providerId); + + final AWSKMS client = mock(AWSKMS.class); + when(client.decrypt(any())) + .thenReturn(new DecryptResult() + .withKeyId(keyIdentifier) + .withPlaintext(ByteBuffer.allocate(ALGORITHM_SUITE.getDataKeyLength()))); + + AwsKmsMrkAwareMasterKey masterKey = AwsKmsMrkAwareMasterKey + .getInstance( + client, + keyIdentifier, + mkp); + masterKey.setGrantTokens(GRANT_TOKENS); + + DataKey test = AwsKmsMrkAwareMasterKey.decryptSingleEncryptedDataKey( + any(), + client, + keyIdentifier, + GRANT_TOKENS, + ALGORITHM_SUITE, + edk, + ENCRYPTION_CONTEXT + ); + + verify(client, times(1)).decrypt(any()); + ArgumentCaptor gr = ArgumentCaptor.forClass(DecryptRequest.class); + verify(client, times(1)).decrypt(gr.capture()); + + final DecryptRequest actualRequest = gr.getValue(); + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.9 + //= type=test + //# To decrypt the encrypted data key this master key MUST use the + //# configured AWS KMS client to make an AWS KMS Decrypt + //# (https://docs.aws.amazon.com/kms/latest/APIReference/ + //# API_Decrypt.html) request constructed as follows: + assertEquals(keyIdentifier, actualRequest.getKeyId()); + assertEquals(GRANT_TOKENS, actualRequest.getGrantTokens()); + assertEquals(ENCRYPTION_CONTEXT, actualRequest.getEncryptionContext()); + assertTrue(actualRequest.getRequestClientOptions().getClientMarker(RequestClientOptions.Marker.USER_AGENT) + .contains(VersionInfo.loadUserAgent())); + + assertNotNull(test.getKey()); + assertEquals(ALGORITHM_SUITE.getDataKeyLength(), test.getKey().getEncoded().length); + assertEquals(ALGORITHM_SUITE.getDataKeyAlgo(), test.getKey().getAlgorithm()); + } + + + @Test + @DisplayName("Exceptional Postcondition: Must have a CMK ARN from AWS KMS to match.") + public void expect_key_arn() { + final CryptoAlgorithm ALGORITHM_SUITE = CryptoAlgorithm.ALG_AES_256_GCM_IV12_TAG16_HKDF_SHA256; + final List GRANT_TOKENS = Collections.singletonList("testGrantToken"); + final Map ENCRYPTION_CONTEXT = Collections.singletonMap("myKey", "myValue"); + final String keyIdentifier = "arn:aws:kms:us-west-2:658956600833:key/b3537ef1-d8dc-4780-9f5a-55776cbb2f7f"; + final String providerId = "aws-kms"; + final EncryptedDataKey edk = new KeyBlob( + providerId, + keyIdentifier.getBytes(StandardCharsets.UTF_8), + new byte[10]); + + final MasterKeyProvider mkp = mock(MasterKeyProvider.class); + when(mkp.getDefaultProviderId()).thenReturn(providerId); + + final AWSKMS client = mock(AWSKMS.class); + when(client.decrypt(any())) + .thenReturn(new DecryptResult() + .withKeyId(null) + .withPlaintext(ByteBuffer.allocate(ALGORITHM_SUITE.getDataKeyLength()))); + + AwsKmsMrkAwareMasterKey masterKey = AwsKmsMrkAwareMasterKey + .getInstance( + client, + keyIdentifier, + mkp); + masterKey.setGrantTokens(GRANT_TOKENS); + + assertThrows(IllegalStateException.class, () -> AwsKmsMrkAwareMasterKey.decryptSingleEncryptedDataKey( + any(), + client, + keyIdentifier, + GRANT_TOKENS, + ALGORITHM_SUITE, + edk, + ENCRYPTION_CONTEXT + )); + } + + @Test + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.9 + //= type=test + //# If the call succeeds then the response's "KeyId" MUST be equal to the + //# configured AWS KMS key identifier otherwise the function MUST collect + //# an error. + public void returned_arn_must_match() { + final CryptoAlgorithm ALGORITHM_SUITE = CryptoAlgorithm.ALG_AES_256_GCM_IV12_TAG16_HKDF_SHA256; + final List GRANT_TOKENS = Collections.singletonList("testGrantToken"); + final Map ENCRYPTION_CONTEXT = Collections.singletonMap("myKey", "myValue"); + final String keyIdentifier = "arn:aws:kms:us-west-2:658956600833:key/b3537ef1-d8dc-4780-9f5a-55776cbb2f7f"; + final String providerId = "aws-kms"; + final EncryptedDataKey edk = new KeyBlob( + providerId, + keyIdentifier.getBytes(StandardCharsets.UTF_8), + new byte[10]); + + final MasterKeyProvider mkp = mock(MasterKeyProvider.class); + when(mkp.getDefaultProviderId()).thenReturn(providerId); + + final AWSKMS client = mock(AWSKMS.class); + when(client.decrypt(any())) + .thenReturn(new DecryptResult() + .withKeyId("arn:aws:kms:us-west-2:658956600833:key/something-else") + .withPlaintext(ByteBuffer.allocate(ALGORITHM_SUITE.getDataKeyLength()))); + + AwsKmsMrkAwareMasterKey masterKey = AwsKmsMrkAwareMasterKey + .getInstance( + client, + keyIdentifier, + mkp); + masterKey.setGrantTokens(GRANT_TOKENS); + + assertThrows(IllegalStateException.class, () -> AwsKmsMrkAwareMasterKey.decryptSingleEncryptedDataKey( + any(), + client, + keyIdentifier, + GRANT_TOKENS, + ALGORITHM_SUITE, + edk, + ENCRYPTION_CONTEXT + )); + } + + @Test + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.9 + //= type=test + //# The response's "Plaintext"'s length MUST equal the length + //# required by the requested algorithm suite otherwise the function MUST + //# collect an error. + public void key_length_must_match() { + final CryptoAlgorithm ALGORITHM_SUITE = CryptoAlgorithm.ALG_AES_256_GCM_IV12_TAG16_HKDF_SHA256; + final List GRANT_TOKENS = Collections.singletonList("testGrantToken"); + final Map ENCRYPTION_CONTEXT = Collections.singletonMap("myKey", "myValue"); + final String keyIdentifier = "arn:aws:kms:us-west-2:658956600833:key/b3537ef1-d8dc-4780-9f5a-55776cbb2f7f"; + final String providerId = "aws-kms"; + // I use more, because less _should_ trigger an underflow... but the condition should _always_ fail + final int wrongLength = ALGORITHM_SUITE.getDataKeyLength() + 1; + final EncryptedDataKey edk = new KeyBlob( + providerId, + keyIdentifier.getBytes(StandardCharsets.UTF_8), + new byte[10]); + + final MasterKeyProvider mkp = mock(MasterKeyProvider.class); + when(mkp.getDefaultProviderId()).thenReturn(providerId); + + final AWSKMS client = mock(AWSKMS.class); + when(client.decrypt(any())) + .thenReturn(new DecryptResult() + .withKeyId(keyIdentifier) + .withPlaintext(ByteBuffer.allocate(wrongLength))); + + AwsKmsMrkAwareMasterKey masterKey = AwsKmsMrkAwareMasterKey + .getInstance( + client, + keyIdentifier, + mkp); + masterKey.setGrantTokens(GRANT_TOKENS); + + assertThrows(IllegalStateException.class, () -> AwsKmsMrkAwareMasterKey.decryptSingleEncryptedDataKey( + any(), + client, + keyIdentifier, + GRANT_TOKENS, + ALGORITHM_SUITE, + edk, + ENCRYPTION_CONTEXT + )); + } + } + + public static class decryptDataKey { + + + @Test + public void basic_use() { + final String keyIdentifier = "arn:aws:kms:us-west-2:111122223333:key/mrk-edb7fe6942894d32ac46dbb1c922d574"; + final CryptoAlgorithm ALGORITHM_SUITE = CryptoAlgorithm.ALG_AES_256_GCM_IV12_TAG16_HKDF_SHA256; + final List GRANT_TOKENS = Collections.singletonList("testGrantToken"); + final Map ENCRYPTION_CONTEXT = Collections.singletonMap("myKey", "myValue"); + final byte[] cipherText = new byte[10]; + final String providerId = "aws-kms"; + final MasterKeyProvider mkp = mock(MasterKeyProvider.class); + when(mkp.getDefaultProviderId()).thenReturn(providerId); + + final EncryptedDataKey edk1 = new KeyBlob( + "aws-kms", + keyIdentifier.getBytes(StandardCharsets.UTF_8), + cipherText); + final EncryptedDataKey edk2 = new KeyBlob( + "aws-kms", + keyIdentifier.getBytes(StandardCharsets.UTF_8), + cipherText); + + final AWSKMS client = mock(AWSKMS.class); + when(client.decrypt(any())) + .thenReturn(new DecryptResult() + .withKeyId(keyIdentifier) + .withPlaintext(ByteBuffer.allocate(ALGORITHM_SUITE.getDataKeyLength()))); + + final AwsKmsMrkAwareMasterKey mk = AwsKmsMrkAwareMasterKey + .getInstance(client, + keyIdentifier, + mkp); + mk.setGrantTokens(GRANT_TOKENS); + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.9 + //= type=test + //# The inputs MUST be the same as the Master Key Decrypt Data Key + //# (../master-key-interface.md#decrypt-data-key) interface. + final DataKey test = mk + .decryptDataKey( + ALGORITHM_SUITE, + Arrays.asList(edk1, edk2), + ENCRYPTION_CONTEXT); + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.9 + //= type=test + //# For each encrypted data key in the filtered set, one at a time, the + //# master key MUST attempt to decrypt the data key. + // + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.9 + //= type=test + //# If the AWS KMS response satisfies the requirements then it MUST be + //# use and this function MUST return and not attempt to decrypt any more + //# encrypted data keys. + verify(client, times((1))).decrypt(new DecryptRequest() + .withGrantTokens(GRANT_TOKENS) + .withEncryptionContext(ENCRYPTION_CONTEXT) + .withKeyId(keyIdentifier) + .withCiphertextBlob(ByteBuffer.wrap(cipherText)) + ); + + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.9 + //= type=test + //# The output MUST be the same as the Master Key Decrypt Data Key + //# (../master-key-interface.md#decrypt-data-key) interface. + assertTrue(DataKey.class.isInstance(test)); + + } + + @Test + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.9 + //= type=test + //# The set of encrypted data keys MUST first be filtered to match this + //# master key's configuration. + public void edk_match() { + final CryptoAlgorithm ALGORITHM_SUITE = CryptoAlgorithm.ALG_AES_256_GCM_IV12_TAG16_HKDF_SHA256; + final List GRANT_TOKENS = Collections.singletonList("testGrantToken"); + final Map ENCRYPTION_CONTEXT = Collections.singletonMap("myKey", "myValue"); + final String keyIdentifier = "arn:aws:kms:us-west-2:658956600833:key/b3537ef1-d8dc-4780-9f5a-55776cbb2f7f"; + final String providerId = "aws-kms"; + final String clientErrMsg = "asdf"; + final EncryptedDataKey edk1 = new KeyBlob( + "not-aws-kms", + keyIdentifier.getBytes(StandardCharsets.UTF_8), + new byte[10]); + + final EncryptedDataKey edk2 = new KeyBlob( + providerId, + "not-key-identifier".getBytes(StandardCharsets.UTF_8), + new byte[10]); + + final MasterKeyProvider mkp = mock(MasterKeyProvider.class); + when(mkp.getDefaultProviderId()).thenReturn(providerId); + + final AWSKMS client = mock(AWSKMS.class); + when(client.decrypt(any())).thenThrow(new AmazonServiceException(clientErrMsg)); + final KmsMasterKeyProvider.RegionalClientSupplier supplier = mock(KmsMasterKeyProvider.RegionalClientSupplier.class); + when(supplier.getClient(any())).thenReturn(client); + + final AwsKmsMrkAwareMasterKey masterKey = AwsKmsMrkAwareMasterKey + .getInstance( + client, + keyIdentifier, + mkp); + masterKey.setGrantTokens(GRANT_TOKENS); + + final CannotUnwrapDataKeyException testProviderNotMatch = assertThrows( + "Unable to decrypt any data keys", + CannotUnwrapDataKeyException.class, () -> masterKey.decryptDataKey( + ALGORITHM_SUITE, + Arrays.asList(edk1), + ENCRYPTION_CONTEXT + )); + assertEquals(0, testProviderNotMatch.getSuppressed().length); + + final IllegalStateException testArnNotMatch = assertThrows( + "Unable to decrypt any data keys", + IllegalStateException.class, () -> masterKey.decryptDataKey( + ALGORITHM_SUITE, + Arrays.asList(edk2), + ENCRYPTION_CONTEXT + )); + assertEquals(0, testArnNotMatch.getSuppressed().length); + } + + @Test + @DisplayName("Exceptional Postcondition: Master key was unable to decrypt.") + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.9 + //= type=test + //# If this attempt + //# results in an error, then these errors MUST be collected. + // + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.9 + //= type=test + //# If all the input encrypted data keys have been processed then this + //# function MUST yield an error that includes all the collected errors. + public void exception_wrapped() { + final CryptoAlgorithm ALGORITHM_SUITE = CryptoAlgorithm.ALG_AES_256_GCM_IV12_TAG16_HKDF_SHA256; + final List GRANT_TOKENS = Collections.singletonList("testGrantToken"); + final Map ENCRYPTION_CONTEXT = Collections.singletonMap("myKey", "myValue"); + final String keyIdentifier = "arn:aws:kms:us-west-2:658956600833:key/b3537ef1-d8dc-4780-9f5a-55776cbb2f7f"; + final String providerId = "aws-kms"; + final String clientErrMsg = "asdf"; + final EncryptedDataKey edk = new KeyBlob( + providerId, + keyIdentifier.getBytes(StandardCharsets.UTF_8), + new byte[10]); + + final MasterKeyProvider mkp = mock(MasterKeyProvider.class); + when(mkp.getDefaultProviderId()).thenReturn(providerId); + + final AWSKMS client = mock(AWSKMS.class); + when(client.decrypt(any())).thenThrow(new AmazonServiceException(clientErrMsg)); + + KmsMasterKeyProvider.RegionalClientSupplier supplier = mock(KmsMasterKeyProvider.RegionalClientSupplier.class); + when(supplier.getClient(any())).thenReturn(client); + + AwsKmsMrkAwareMasterKey masterKey = AwsKmsMrkAwareMasterKey + .getInstance( + client, + keyIdentifier, + mkp); + + masterKey.setGrantTokens(GRANT_TOKENS); + + final CannotUnwrapDataKeyException test = assertThrows( + "Unable to decrypt any data keys", + CannotUnwrapDataKeyException.class, () -> masterKey.decryptDataKey( + ALGORITHM_SUITE, + Arrays.asList(edk), + ENCRYPTION_CONTEXT + )); + assertEquals(1, test.getSuppressed().length); + Throwable fromClient = Arrays.stream(test.getSuppressed()).findFirst().get(); + assertTrue(fromClient instanceof AmazonServiceException); + assertTrue(fromClient.getMessage().startsWith(clientErrMsg)); + } + } + + public static class getMasterKey { + @Test + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.7 + //= type=test + //# MUST be unchanged from the Master Key interface. + public void test_get_master_key() throws NoSuchMethodException { + String methodName = "getMasterKey"; + Class[] parameterTypes = new Class[]{ String.class, String.class }; + // Make sure the signature is correct by fetching the base method + Method baseMethod = MasterKey.class.getDeclaredMethod(methodName, parameterTypes); + assertNotNull(baseMethod); + // Assert AwsKmsMrkAwareMasterKey does not declare the same method directly + assertThrows(NoSuchMethodException.class, () -> AwsKmsMrkAwareMasterKey.class.getDeclaredMethod(methodName, parameterTypes)); + } + } + + + public static class getMasterKeysForEncryption { + @Test + //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.8 + //= type=test + //# MUST be unchanged from the Master Key interface. + public void test_getMasterKeysForEncryption() throws NoSuchMethodException { + String methodName = "getMasterKeysForEncryption"; + Class[] parameterTypes = new Class[]{ MasterKeyRequest.class }; + + // Make sure the signature is correct by fetching the base method + Method baseMethod = MasterKey.class.getDeclaredMethod(methodName, parameterTypes); + assertNotNull(baseMethod); + // Assert AwsKmsMrkAwareMasterKey does no declare the same method directly + assertThrows(NoSuchMethodException.class, () -> AwsKmsMrkAwareMasterKey.class.getDeclaredMethod(methodName, parameterTypes)); + } + } +} diff --git a/src/test/java/com/amazonaws/encryptionsdk/kms/KMSProviderBuilderIntegrationTests.java b/src/test/java/com/amazonaws/encryptionsdk/kms/KMSProviderBuilderIntegrationTests.java index d67ec9429..a76ce2137 100644 --- a/src/test/java/com/amazonaws/encryptionsdk/kms/KMSProviderBuilderIntegrationTests.java +++ b/src/test/java/com/amazonaws/encryptionsdk/kms/KMSProviderBuilderIntegrationTests.java @@ -422,7 +422,7 @@ public void whenUserAgentsOverridden_originalUAsPreserved() throws Exception { assertTrue(ua.contains("TEST-UA-PREFIX")); assertTrue(ua.contains("TEST-UA-SUFFIX")); - assertTrue(ua.contains(VersionInfo.USER_AGENT)); + assertTrue(ua.contains(VersionInfo.loadUserAgent())); } @Test diff --git a/src/test/java/com/amazonaws/encryptionsdk/kms/KMSProviderBuilderMockTests.java b/src/test/java/com/amazonaws/encryptionsdk/kms/KMSProviderBuilderMockTests.java index 4dcbdaf15..c3f7bb74c 100644 --- a/src/test/java/com/amazonaws/encryptionsdk/kms/KMSProviderBuilderMockTests.java +++ b/src/test/java/com/amazonaws/encryptionsdk/kms/KMSProviderBuilderMockTests.java @@ -169,15 +169,15 @@ public void testUserAgentPassthrough() throws Exception { ArgumentCaptor gdkr = ArgumentCaptor.forClass(GenerateDataKeyRequest.class); verify(client, times(1)).generateDataKey(gdkr.capture()); - assertTrue(getUA(gdkr.getValue()).contains(VersionInfo.USER_AGENT)); + assertTrue(getUA(gdkr.getValue()).contains(VersionInfo.loadUserAgent())); ArgumentCaptor encr = ArgumentCaptor.forClass(EncryptRequest.class); verify(client, times(1)).encrypt(encr.capture()); - assertTrue(getUA(encr.getValue()).contains(VersionInfo.USER_AGENT)); + assertTrue(getUA(encr.getValue()).contains(VersionInfo.loadUserAgent())); ArgumentCaptor decr = ArgumentCaptor.forClass(DecryptRequest.class); verify(client, times(1)).decrypt(decr.capture()); - assertTrue(getUA(decr.getValue()).contains(VersionInfo.USER_AGENT)); + assertTrue(getUA(decr.getValue()).contains(VersionInfo.loadUserAgent())); } private String getUA(AmazonWebServiceRequest request) { diff --git a/src/test/java/com/amazonaws/encryptionsdk/kms/KMSTestFixtures.java b/src/test/java/com/amazonaws/encryptionsdk/kms/KMSTestFixtures.java index 93eb55de5..0484397d7 100644 --- a/src/test/java/com/amazonaws/encryptionsdk/kms/KMSTestFixtures.java +++ b/src/test/java/com/amazonaws/encryptionsdk/kms/KMSTestFixtures.java @@ -19,6 +19,8 @@ private KMSTestFixtures() { */ public static final String US_WEST_2_KEY_ID = "arn:aws:kms:us-west-2:658956600833:key/b3537ef1-d8dc-4780-9f5a-55776cbb2f7f"; public static final String EU_CENTRAL_1_KEY_ID = "arn:aws:kms:eu-central-1:658956600833:key/75414c93-5285-4b57-99c9-30c1cf0a22c2"; + public static final String US_EAST_1_MULTI_REGION_KEY_ID = "arn:aws:kms:us-east-1:658956600833:key/mrk-80bd8ecdcd4342aebd84b7dc9da498a7"; + public static final String US_WEST_2_MULTI_REGION_KEY_ID = "arn:aws:kms:us-west-2:658956600833:key/mrk-80bd8ecdcd4342aebd84b7dc9da498a7"; public static final String ACCOUNT_ID = "658956600833"; public static final String PARTITION = "aws"; public static final String US_WEST_2 = "us-west-2"; diff --git a/src/test/java/com/amazonaws/encryptionsdk/kms/KmsMasterKeyProviderTest.java b/src/test/java/com/amazonaws/encryptionsdk/kms/KmsMasterKeyProviderTest.java index 13bc1956f..89eac92ff 100644 --- a/src/test/java/com/amazonaws/encryptionsdk/kms/KmsMasterKeyProviderTest.java +++ b/src/test/java/com/amazonaws/encryptionsdk/kms/KmsMasterKeyProviderTest.java @@ -302,7 +302,7 @@ public void testDecryptKMSFailsOnce() throws Exception { private void assertUserAgent(AmazonWebServiceRequest request) { assertTrue(request.getRequestClientOptions().getClientMarker(RequestClientOptions.Marker.USER_AGENT) - .contains(VersionInfo.USER_AGENT)); + .contains(VersionInfo.loadUserAgent())); } } diff --git a/src/test/java/com/amazonaws/encryptionsdk/kms/KmsMasterKeyTest.java b/src/test/java/com/amazonaws/encryptionsdk/kms/KmsMasterKeyTest.java index 9795d0de8..eaab97464 100644 --- a/src/test/java/com/amazonaws/encryptionsdk/kms/KmsMasterKeyTest.java +++ b/src/test/java/com/amazonaws/encryptionsdk/kms/KmsMasterKeyTest.java @@ -360,6 +360,6 @@ public void testDecryptSkipsMismatchedIdEDK() { private void assertUserAgent(AmazonWebServiceRequest request) { assertTrue(request.getRequestClientOptions().getClientMarker(RequestClientOptions.Marker.USER_AGENT) - .contains(VersionInfo.USER_AGENT)); + .contains(VersionInfo.loadUserAgent())); } } diff --git a/src/test/resources/aws-encryption-sdk-test-vectors b/src/test/resources/aws-encryption-sdk-test-vectors index 3fdc7c682..5cb6870e3 160000 --- a/src/test/resources/aws-encryption-sdk-test-vectors +++ b/src/test/resources/aws-encryption-sdk-test-vectors @@ -1 +1 @@ -Subproject commit 3fdc7c682184a3c7214686550fafb490c14930c4 +Subproject commit 5cb6870e3d9e0d7117220c0f0033451818f25e85 diff --git a/src/test/resources/commitment-test-vectors.json b/src/test/resources/commitment-test-vectors.json index e61b52f69..fa6a84d6f 100644 --- a/src/test/resources/commitment-test-vectors.json +++ b/src/test/resources/commitment-test-vectors.json @@ -1023,4 +1023,4 @@ "comment": "46. [Java ESDK] alg=ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY_ECDSA_P384; unframed" } ] -} \ No newline at end of file +} diff --git a/util/duvet-report.sh b/util/duvet-report.sh new file mode 100755 index 000000000..cc8b9256e --- /dev/null +++ b/util/duvet-report.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +./aws-encryption-sdk-specification/util/report.js \ + 'src/main/**/*.java' \ + 'src/test/**/*.java' \ + 'compliance_exceptions/*.java' diff --git a/util/test-conditions.sh b/util/test-conditions.sh new file mode 100755 index 000000000..f1d2a260b --- /dev/null +++ b/util/test-conditions.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +./aws-encryption-sdk-specification/util/test_conditions \ + -s '-r src/main/ --include *.java' \ + -t '-r src/test/ --include *.java' \ + -s 'compliance_exceptions/*.java'