From 43b3e9c9a74fc686b732c2b7ef25d48ded19ccc3 Mon Sep 17 00:00:00 2001 From: J Plasmeier Date: Mon, 22 Jul 2024 16:37:58 -0700 Subject: [PATCH 1/6] feat: add KmsDiscoveryKeyring --- .../s3/materials/KmsDiscoveryKeyring.java | 160 ++++++++++++++++++ .../s3/materials/KmsDiscoveryKeyringTest.java | 137 +++++++++++++++ .../s3/materials/KmsKeyringTest.java | 8 +- 3 files changed, 301 insertions(+), 4 deletions(-) create mode 100644 src/main/java/software/amazon/encryption/s3/materials/KmsDiscoveryKeyring.java create mode 100644 src/test/java/software/amazon/encryption/s3/materials/KmsDiscoveryKeyringTest.java diff --git a/src/main/java/software/amazon/encryption/s3/materials/KmsDiscoveryKeyring.java b/src/main/java/software/amazon/encryption/s3/materials/KmsDiscoveryKeyring.java new file mode 100644 index 000000000..4eafc9538 --- /dev/null +++ b/src/main/java/software/amazon/encryption/s3/materials/KmsDiscoveryKeyring.java @@ -0,0 +1,160 @@ +package software.amazon.encryption.s3.materials; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import software.amazon.awssdk.awscore.AwsRequestOverrideConfiguration; +import software.amazon.awssdk.core.ApiName; +import software.amazon.awssdk.core.SdkBytes; +import software.amazon.awssdk.services.kms.KmsClient; +import software.amazon.awssdk.services.kms.model.DecryptRequest; +import software.amazon.awssdk.services.kms.model.DecryptResponse; +import software.amazon.awssdk.services.s3.model.GetObjectRequest; +import software.amazon.encryption.s3.S3EncryptionClient; +import software.amazon.encryption.s3.S3EncryptionClientException; +import software.amazon.encryption.s3.internal.ApiNameVersion; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +public class KmsDiscoveryKeyring extends S3Keyring { + private static final ApiName API_NAME = ApiNameVersion.apiNameWithVersion(); + private static final String KEY_ID_CONTEXT_KEY = "kms_cmk_id"; + + private final KmsClient _kmsClient; + + public KmsDiscoveryKeyring(Builder builder) { + super(builder); + + _kmsClient = builder._kmsClient; + decryptDataKeyStrategies.put(_kmsDiscoveryStrategy.keyProviderInfo(), _kmsDiscoveryStrategy); + decryptDataKeyStrategies.put(_kmsContextDiscoveryStrategy.keyProviderInfo(), _kmsContextDiscoveryStrategy); + } + + private final DecryptDataKeyStrategy _kmsDiscoveryStrategy = new DecryptDataKeyStrategy() { + + private static final String KEY_PROVIDER_INFO = "kms"; + + @Override + public boolean isLegacy() { + return true; + } + + @Override + public String keyProviderInfo() { + return KEY_PROVIDER_INFO; + } + + @Override + public byte[] decryptDataKey(DecryptionMaterials materials, byte[] encryptedDataKey) { + DecryptRequest request = DecryptRequest.builder() + .encryptionContext(materials.encryptionContext()) + .ciphertextBlob(SdkBytes.fromByteArray(encryptedDataKey)) + .overrideConfiguration(builder -> builder.addApiName(API_NAME)) + .build(); + + DecryptResponse response = _kmsClient.decrypt(request); + return response.plaintext().asByteArray(); + } + }; + + private final DecryptDataKeyStrategy _kmsContextDiscoveryStrategy = new DecryptDataKeyStrategy() { + + private static final String KEY_PROVIDER_INFO = "kms+context"; + private static final String ENCRYPTION_CONTEXT_ALGORITHM_KEY = "aws:x-amz-cek-alg"; + + @Override + public boolean isLegacy() { + return false; + } + + @Override + public String keyProviderInfo() { + return KEY_PROVIDER_INFO; + } + + @Override + public byte[] decryptDataKey(DecryptionMaterials materials, byte[] encryptedDataKey) { + Map requestEncryptionContext = new HashMap<>(); + GetObjectRequest s3Request = materials.s3Request(); + if (s3Request.overrideConfiguration().isPresent()) { + AwsRequestOverrideConfiguration overrideConfig = s3Request.overrideConfiguration().get(); + Optional> optEncryptionContext = overrideConfig + .executionAttributes() + .getOptionalAttribute(S3EncryptionClient.ENCRYPTION_CONTEXT); + if (optEncryptionContext.isPresent()) { + requestEncryptionContext = new HashMap<>(optEncryptionContext.get()); + } + } + + // We are validating the encryption context to match S3EC V2 behavior + Map materialsEncryptionContextCopy = new HashMap<>(materials.encryptionContext()); + materialsEncryptionContextCopy.remove(KEY_ID_CONTEXT_KEY); + materialsEncryptionContextCopy.remove(ENCRYPTION_CONTEXT_ALGORITHM_KEY); + if (!materialsEncryptionContextCopy.equals(requestEncryptionContext)) { + throw new S3EncryptionClientException("Provided encryption context does not match information retrieved from S3"); + } + + DecryptRequest request = DecryptRequest.builder() + .encryptionContext(materials.encryptionContext()) + .ciphertextBlob(SdkBytes.fromByteArray(encryptedDataKey)) + .overrideConfiguration(builder -> builder.addApiName(API_NAME)) + .build(); + + DecryptResponse response = _kmsClient.decrypt(request); + return response.plaintext().asByteArray(); + } + }; + + private final Map decryptDataKeyStrategies = new HashMap<>(); + + public static Builder builder() { + return new Builder(); + } + + @Override + protected GenerateDataKeyStrategy generateDataKeyStrategy() { + throw new S3EncryptionClientException("KmsDiscoveryKeyring does not support GenerateDataKey"); + } + + @Override + protected EncryptDataKeyStrategy encryptDataKeyStrategy() { + throw new S3EncryptionClientException("KmsDiscoveryKeyring does not support EncryptDataKey"); + } + + @Override + protected Map decryptDataKeyStrategies() { + return decryptDataKeyStrategies; + } + + public static class Builder extends S3Keyring.Builder { + private KmsClient _kmsClient; + private String _wrappingKeyId; + + private Builder() { + super(); + } + + @Override + protected Builder builder() { + return this; + } + + /** + * Note that this does NOT create a defensive clone of KmsClient. Any modifications made to the wrapped + * client will be reflected in this Builder. + */ + @SuppressFBWarnings(value = "EI_EXPOSE_REP2", justification = "Pass mutability into wrapping client") + public Builder kmsClient(KmsClient kmsClient) { + _kmsClient = kmsClient; + return this; + } + + public KmsDiscoveryKeyring build() { + if (_kmsClient == null) { + _kmsClient = KmsClient.create(); + } + + return new KmsDiscoveryKeyring(this); + } + } +} diff --git a/src/test/java/software/amazon/encryption/s3/materials/KmsDiscoveryKeyringTest.java b/src/test/java/software/amazon/encryption/s3/materials/KmsDiscoveryKeyringTest.java new file mode 100644 index 000000000..3d19eae05 --- /dev/null +++ b/src/test/java/software/amazon/encryption/s3/materials/KmsDiscoveryKeyringTest.java @@ -0,0 +1,137 @@ +package software.amazon.encryption.s3.materials; + +import com.amazonaws.regions.Region; +import com.amazonaws.regions.Regions; +import com.amazonaws.services.s3.AmazonS3Encryption; +import com.amazonaws.services.s3.AmazonS3EncryptionClient; +import com.amazonaws.services.s3.model.CryptoConfiguration; +import com.amazonaws.services.s3.model.CryptoMode; +import com.amazonaws.services.s3.model.EncryptionMaterialsProvider; +import com.amazonaws.services.s3.model.KMSEncryptionMaterialsProvider; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.core.ResponseBytes; +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.GetObjectResponse; +import software.amazon.encryption.s3.S3EncryptionClient; +import software.amazon.encryption.s3.S3EncryptionClientException; + +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static software.amazon.encryption.s3.S3EncryptionClient.withAdditionalConfiguration; +import static software.amazon.encryption.s3.utils.S3EncryptionClientTestResources.appendTestSuffix; +import static software.amazon.encryption.s3.utils.S3EncryptionClientTestResources.deleteObject; + +public class KmsDiscoveryKeyringTest { + private static final String BUCKET = System.getenv("AWS_S3EC_TEST_BUCKET"); + private static final String KMS_KEY_ID = System.getenv("AWS_S3EC_TEST_KMS_KEY_ID"); + private static final Region KMS_REGION = Region.getRegion(Regions.fromName(System.getenv("AWS_REGION"))); + + private static SecretKey AES_KEY; + private static KeyPair RSA_KEY_PAIR; + + @BeforeAll + public static void setUp() throws NoSuchAlgorithmException { + KeyGenerator keyGen = KeyGenerator.getInstance("AES"); + keyGen.init(256); + AES_KEY = keyGen.generateKey(); + + KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA"); + keyPairGen.initialize(2048); + RSA_KEY_PAIR = keyPairGen.generateKeyPair(); + } + + @Test + public void buildKmsDiscoveryKeyringWithNullSecureRandomFails() { + assertThrows(S3EncryptionClientException.class, () -> KmsDiscoveryKeyring.builder().secureRandom(null)); + } + + @Test + public void buildDiscoveryKeyringWithNullDataKeyGeneratorFails() { + assertThrows(S3EncryptionClientException.class, () -> KmsDiscoveryKeyring.builder().dataKeyGenerator(null)); + } + + @Test + public void testKmsDiscovery() { + final String objectKey = appendTestSuffix("kms-v1-to-v3-discovery"); + + // V1 Client + EncryptionMaterialsProvider materialsProvider = new KMSEncryptionMaterialsProvider(KMS_KEY_ID); + + CryptoConfiguration v1Config = + new CryptoConfiguration(CryptoMode.AuthenticatedEncryption) + .withAwsKmsRegion(KMS_REGION); + + AmazonS3Encryption v1Client = AmazonS3EncryptionClient.encryptionBuilder() + .withCryptoConfiguration(v1Config) + .withEncryptionMaterials(materialsProvider) + .build(); + + // V3 Client + KmsDiscoveryKeyring kmsDiscoveryKeyring = KmsDiscoveryKeyring + .builder().enableLegacyWrappingAlgorithms(true).build(); + S3Client v3Client = S3EncryptionClient.builder() + .keyring(kmsDiscoveryKeyring) + .build(); + + // Asserts + final String input = "KMS Discovery Keyring"; + v1Client.putObject(BUCKET, objectKey, input); + + ResponseBytes objectResponse = v3Client.getObjectAsBytes(builder -> builder + .bucket(BUCKET) + .key(objectKey)); + String output = objectResponse.asUtf8String(); + assertEquals(input, output); + + // Cleanup + deleteObject(BUCKET, objectKey, v3Client); + v3Client.close(); + } + + @Test + public void testKmsContextDiscovery() { + final String objectKey = appendTestSuffix("kms-v3-to-v3-discovery-context"); + + // V3 Client - KmsKeyring + S3Client v3Client = S3EncryptionClient.builder() + .kmsKeyId(KMS_KEY_ID) + .build(); + + final String input = "KmsContextV3toV1Discovery"; + Map encryptionContext = new HashMap<>(); + encryptionContext.put("user-metadata-key", "user-metadata-value-v3-to-v3-context"); + + v3Client.putObject(builder -> builder + .bucket(BUCKET) + .key(objectKey) + .overrideConfiguration(withAdditionalConfiguration(encryptionContext)), RequestBody.fromString(input)); + + // V3 Client - KmsDiscoveryContext + KmsDiscoveryKeyring kmsDiscoveryKeyring = KmsDiscoveryKeyring + .builder().enableLegacyWrappingAlgorithms(true).build(); + S3Client v3ClientDiscovery = S3EncryptionClient.builder() + .keyring(kmsDiscoveryKeyring) + .build(); + + ResponseBytes objectResponse = v3ClientDiscovery.getObjectAsBytes(builder -> builder + .bucket(BUCKET) + .key(objectKey) + .overrideConfiguration(withAdditionalConfiguration(encryptionContext))); + String output = objectResponse.asUtf8String(); + assertEquals(input, output); + + // Cleanup + deleteObject(BUCKET, objectKey, v3Client); + v3Client.close(); + } +} diff --git a/src/test/java/software/amazon/encryption/s3/materials/KmsKeyringTest.java b/src/test/java/software/amazon/encryption/s3/materials/KmsKeyringTest.java index 8892d780d..b9cca6815 100644 --- a/src/test/java/software/amazon/encryption/s3/materials/KmsKeyringTest.java +++ b/src/test/java/software/amazon/encryption/s3/materials/KmsKeyringTest.java @@ -10,13 +10,13 @@ public class KmsKeyringTest { @Test - public void buildAesKeyringWithNullSecureRandomFails() { - assertThrows(S3EncryptionClientException.class, () -> AesKeyring.builder().secureRandom(null)); + public void buildKmsKeyringWithNullSecureRandomFails() { + assertThrows(S3EncryptionClientException.class, () -> KmsKeyring.builder().secureRandom(null)); } @Test - public void buildAesKeyringWithNullDataKeyGeneratorFails() { - assertThrows(S3EncryptionClientException.class, () -> AesKeyring.builder().dataKeyGenerator(null)); + public void buildKmsKeyringWithNullDataKeyGeneratorFails() { + assertThrows(S3EncryptionClientException.class, () -> KmsKeyring.builder().dataKeyGenerator(null)); } } From ee161593bc2e3e282632f47a869175bd47295b02 Mon Sep 17 00:00:00 2001 From: J Plasmeier Date: Tue, 23 Jul 2024 12:16:34 -0700 Subject: [PATCH 2/6] wip - discovery filter --- .../s3/materials/DiscoveryFilter.java | 51 +++++++++++++++++++ .../s3/materials/KmsDiscoveryKeyring.java | 27 +++++++++- 2 files changed, 76 insertions(+), 2 deletions(-) create mode 100644 src/main/java/software/amazon/encryption/s3/materials/DiscoveryFilter.java diff --git a/src/main/java/software/amazon/encryption/s3/materials/DiscoveryFilter.java b/src/main/java/software/amazon/encryption/s3/materials/DiscoveryFilter.java new file mode 100644 index 000000000..e9ab96747 --- /dev/null +++ b/src/main/java/software/amazon/encryption/s3/materials/DiscoveryFilter.java @@ -0,0 +1,51 @@ +package software.amazon.encryption.s3.materials; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; + +/** + * This class stores the configuration for filtering AWS KMS CMK ARNs by AWS account ID and + * partition. + * + *

The filter allows a KMS CMK if its partition matches {@code partition} and its accountId is + * included in {@code accountIds}. + */ +public class DiscoveryFilter { + + private final String partition_; + private final Collection accountIds_; + + public DiscoveryFilter(String partition, String... accountIds) { + this(partition, Arrays.asList(accountIds)); + } + + public DiscoveryFilter(String partition, Collection accountIds) { + if (partition == null || partition.isEmpty()) { + throw new IllegalArgumentException( + "Discovery filter cannot be configured without " + "a partition."); + } else if (accountIds == null || accountIds.isEmpty()) { + throw new IllegalArgumentException( + "Discovery filter cannot be configured without " + "account IDs."); + } else if (accountIds.contains(null) || accountIds.contains("")) { + throw new IllegalArgumentException( + "Discovery filter cannot be configured with " + "null or empty account IDs."); + } + + partition_ = partition; + accountIds_ = new HashSet(accountIds); + } + + public String getPartition() { + return partition_; + } + + public Collection getAccountIds() { + return Collections.unmodifiableSet(new HashSet<>(accountIds_)); + } + + public boolean allowsPartitionAndAccount(String partition, String accountId) { + return (partition_.equals(partition) && accountIds_.contains(accountId)); + } +} diff --git a/src/main/java/software/amazon/encryption/s3/materials/KmsDiscoveryKeyring.java b/src/main/java/software/amazon/encryption/s3/materials/KmsDiscoveryKeyring.java index 4eafc9538..44e77730e 100644 --- a/src/main/java/software/amazon/encryption/s3/materials/KmsDiscoveryKeyring.java +++ b/src/main/java/software/amazon/encryption/s3/materials/KmsDiscoveryKeyring.java @@ -21,11 +21,15 @@ public class KmsDiscoveryKeyring extends S3Keyring { private static final String KEY_ID_CONTEXT_KEY = "kms_cmk_id"; private final KmsClient _kmsClient; + private final DiscoveryFilter _discoveryFilter; + + private final Map decryptDataKeyStrategies = new HashMap<>(); public KmsDiscoveryKeyring(Builder builder) { super(builder); _kmsClient = builder._kmsClient; + _discoveryFilter = builder._discoveryFilter; decryptDataKeyStrategies.put(_kmsDiscoveryStrategy.keyProviderInfo(), _kmsDiscoveryStrategy); decryptDataKeyStrategies.put(_kmsContextDiscoveryStrategy.keyProviderInfo(), _kmsContextDiscoveryStrategy); } @@ -105,7 +109,21 @@ public byte[] decryptDataKey(DecryptionMaterials materials, byte[] encryptedData } }; - private final Map decryptDataKeyStrategies = new HashMap<>(); + /** + * Checks the given materials to see if the KMS key passes through the keyring's + * discovery filter. + * @param materials + * @return + */ + private boolean checkDiscoveryFilter(final DecryptionMaterials materials) { + // this is not trivial, + // i THINK that in v1, there was NO key id in the metadata. + // in v2 (kms+context), there is either in metadata and/or EC + // so always fail v1, and pull v2 key out of EC? + // related: ciphertext library + // todo implement + return true; + } public static Builder builder() { return new Builder(); @@ -128,7 +146,7 @@ protected Map decryptDataKeyStrategies() { public static class Builder extends S3Keyring.Builder { private KmsClient _kmsClient; - private String _wrappingKeyId; + private DiscoveryFilter _discoveryFilter; private Builder() { super(); @@ -149,6 +167,11 @@ public Builder kmsClient(KmsClient kmsClient) { return this; } + public Builder discoveryFilter(DiscoveryFilter discoveryFilter) { + _discoveryFilter = discoveryFilter; + return this; + } + public KmsDiscoveryKeyring build() { if (_kmsClient == null) { _kmsClient = KmsClient.create(); From dca56104b9daaf5b6925dea636238ade69dec4ce Mon Sep 17 00:00:00 2001 From: J Plasmeier Date: Tue, 23 Jul 2024 14:50:35 -0700 Subject: [PATCH 3/6] remove discovery filter, add more tests --- .../s3/materials/DiscoveryFilter.java | 51 -------- .../s3/materials/KmsDiscoveryKeyring.java | 31 ++--- .../s3/materials/KmsDiscoveryKeyringTest.java | 116 ++++++++++++++---- 3 files changed, 101 insertions(+), 97 deletions(-) delete mode 100644 src/main/java/software/amazon/encryption/s3/materials/DiscoveryFilter.java diff --git a/src/main/java/software/amazon/encryption/s3/materials/DiscoveryFilter.java b/src/main/java/software/amazon/encryption/s3/materials/DiscoveryFilter.java deleted file mode 100644 index e9ab96747..000000000 --- a/src/main/java/software/amazon/encryption/s3/materials/DiscoveryFilter.java +++ /dev/null @@ -1,51 +0,0 @@ -package software.amazon.encryption.s3.materials; - -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashSet; - -/** - * This class stores the configuration for filtering AWS KMS CMK ARNs by AWS account ID and - * partition. - * - *

The filter allows a KMS CMK if its partition matches {@code partition} and its accountId is - * included in {@code accountIds}. - */ -public class DiscoveryFilter { - - private final String partition_; - private final Collection accountIds_; - - public DiscoveryFilter(String partition, String... accountIds) { - this(partition, Arrays.asList(accountIds)); - } - - public DiscoveryFilter(String partition, Collection accountIds) { - if (partition == null || partition.isEmpty()) { - throw new IllegalArgumentException( - "Discovery filter cannot be configured without " + "a partition."); - } else if (accountIds == null || accountIds.isEmpty()) { - throw new IllegalArgumentException( - "Discovery filter cannot be configured without " + "account IDs."); - } else if (accountIds.contains(null) || accountIds.contains("")) { - throw new IllegalArgumentException( - "Discovery filter cannot be configured with " + "null or empty account IDs."); - } - - partition_ = partition; - accountIds_ = new HashSet(accountIds); - } - - public String getPartition() { - return partition_; - } - - public Collection getAccountIds() { - return Collections.unmodifiableSet(new HashSet<>(accountIds_)); - } - - public boolean allowsPartitionAndAccount(String partition, String accountId) { - return (partition_.equals(partition) && accountIds_.contains(accountId)); - } -} diff --git a/src/main/java/software/amazon/encryption/s3/materials/KmsDiscoveryKeyring.java b/src/main/java/software/amazon/encryption/s3/materials/KmsDiscoveryKeyring.java index 44e77730e..a7de4da80 100644 --- a/src/main/java/software/amazon/encryption/s3/materials/KmsDiscoveryKeyring.java +++ b/src/main/java/software/amazon/encryption/s3/materials/KmsDiscoveryKeyring.java @@ -21,15 +21,20 @@ public class KmsDiscoveryKeyring extends S3Keyring { private static final String KEY_ID_CONTEXT_KEY = "kms_cmk_id"; private final KmsClient _kmsClient; - private final DiscoveryFilter _discoveryFilter; private final Map decryptDataKeyStrategies = new HashMap<>(); + /** + * This keyring will decrypt the object without specifying the KMS key + * in its configuration. This is similar to the Encryption SDK's Discovery Keyring. + * NOTE: There is no Discovery Filter, as kms+context mode used in v2/v3 does not persist the KMS key ID + * to the object metadata, so it is not possible to safely filter on this attribute. + * @param builder + */ public KmsDiscoveryKeyring(Builder builder) { super(builder); _kmsClient = builder._kmsClient; - _discoveryFilter = builder._discoveryFilter; decryptDataKeyStrategies.put(_kmsDiscoveryStrategy.keyProviderInfo(), _kmsDiscoveryStrategy); decryptDataKeyStrategies.put(_kmsContextDiscoveryStrategy.keyProviderInfo(), _kmsContextDiscoveryStrategy); } @@ -109,22 +114,6 @@ public byte[] decryptDataKey(DecryptionMaterials materials, byte[] encryptedData } }; - /** - * Checks the given materials to see if the KMS key passes through the keyring's - * discovery filter. - * @param materials - * @return - */ - private boolean checkDiscoveryFilter(final DecryptionMaterials materials) { - // this is not trivial, - // i THINK that in v1, there was NO key id in the metadata. - // in v2 (kms+context), there is either in metadata and/or EC - // so always fail v1, and pull v2 key out of EC? - // related: ciphertext library - // todo implement - return true; - } - public static Builder builder() { return new Builder(); } @@ -146,7 +135,6 @@ protected Map decryptDataKeyStrategies() { public static class Builder extends S3Keyring.Builder { private KmsClient _kmsClient; - private DiscoveryFilter _discoveryFilter; private Builder() { super(); @@ -167,11 +155,6 @@ public Builder kmsClient(KmsClient kmsClient) { return this; } - public Builder discoveryFilter(DiscoveryFilter discoveryFilter) { - _discoveryFilter = discoveryFilter; - return this; - } - public KmsDiscoveryKeyring build() { if (_kmsClient == null) { _kmsClient = KmsClient.create(); diff --git a/src/test/java/software/amazon/encryption/s3/materials/KmsDiscoveryKeyringTest.java b/src/test/java/software/amazon/encryption/s3/materials/KmsDiscoveryKeyringTest.java index 3d19eae05..6320aea51 100644 --- a/src/test/java/software/amazon/encryption/s3/materials/KmsDiscoveryKeyringTest.java +++ b/src/test/java/software/amazon/encryption/s3/materials/KmsDiscoveryKeyringTest.java @@ -4,11 +4,13 @@ import com.amazonaws.regions.Regions; import com.amazonaws.services.s3.AmazonS3Encryption; import com.amazonaws.services.s3.AmazonS3EncryptionClient; +import com.amazonaws.services.s3.AmazonS3EncryptionClientV2; +import com.amazonaws.services.s3.AmazonS3EncryptionV2; import com.amazonaws.services.s3.model.CryptoConfiguration; import com.amazonaws.services.s3.model.CryptoMode; +import com.amazonaws.services.s3.model.EncryptedPutObjectRequest; import com.amazonaws.services.s3.model.EncryptionMaterialsProvider; import com.amazonaws.services.s3.model.KMSEncryptionMaterialsProvider; -import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import software.amazon.awssdk.core.ResponseBytes; import software.amazon.awssdk.core.sync.RequestBody; @@ -17,16 +19,15 @@ import software.amazon.encryption.s3.S3EncryptionClient; import software.amazon.encryption.s3.S3EncryptionClientException; -import javax.crypto.KeyGenerator; -import javax.crypto.SecretKey; -import java.security.KeyPair; -import java.security.KeyPairGenerator; -import java.security.NoSuchAlgorithmException; +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; import static software.amazon.encryption.s3.S3EncryptionClient.withAdditionalConfiguration; import static software.amazon.encryption.s3.utils.S3EncryptionClientTestResources.appendTestSuffix; import static software.amazon.encryption.s3.utils.S3EncryptionClientTestResources.deleteObject; @@ -36,20 +37,6 @@ public class KmsDiscoveryKeyringTest { private static final String KMS_KEY_ID = System.getenv("AWS_S3EC_TEST_KMS_KEY_ID"); private static final Region KMS_REGION = Region.getRegion(Regions.fromName(System.getenv("AWS_REGION"))); - private static SecretKey AES_KEY; - private static KeyPair RSA_KEY_PAIR; - - @BeforeAll - public static void setUp() throws NoSuchAlgorithmException { - KeyGenerator keyGen = KeyGenerator.getInstance("AES"); - keyGen.init(256); - AES_KEY = keyGen.generateKey(); - - KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA"); - keyPairGen.initialize(2048); - RSA_KEY_PAIR = keyPairGen.generateKeyPair(); - } - @Test public void buildKmsDiscoveryKeyringWithNullSecureRandomFails() { assertThrows(S3EncryptionClientException.class, () -> KmsDiscoveryKeyring.builder().secureRandom(null)); @@ -99,7 +86,49 @@ public void testKmsDiscovery() { } @Test - public void testKmsContextDiscovery() { + public void testKmsContextV2Discovery() { + final String objectKey = appendTestSuffix("kms-context-v2-to-v3-discovery"); + + // V2 Client + EncryptionMaterialsProvider materialsProvider = new KMSEncryptionMaterialsProvider(KMS_KEY_ID); + + AmazonS3EncryptionV2 v2Client = AmazonS3EncryptionClientV2.encryptionBuilder() + .withEncryptionMaterialsProvider(materialsProvider) + .build(); + + // V3 Client + KmsDiscoveryKeyring kmsDiscoveryKeyring = KmsDiscoveryKeyring + .builder().enableLegacyWrappingAlgorithms(true).build(); + S3Client v3Client = S3EncryptionClient.builder() + .keyring(kmsDiscoveryKeyring) + .build(); + + // Asserts + final String input = "KmsContextV2toV3Discovery"; + Map encryptionContext = new HashMap<>(); + encryptionContext.put("user-metadata-key", "user-metadata-value"); + EncryptedPutObjectRequest putObjectRequest = new EncryptedPutObjectRequest( + BUCKET, + objectKey, + new ByteArrayInputStream(input.getBytes(StandardCharsets.UTF_8)), + null + ).withMaterialsDescription(encryptionContext); + v2Client.putObject(putObjectRequest); + + ResponseBytes objectResponse = v3Client.getObjectAsBytes(builder -> builder + .bucket(BUCKET) + .key(objectKey) + .overrideConfiguration(withAdditionalConfiguration(encryptionContext))); + String output = objectResponse.asUtf8String(); + assertEquals(input, output); + + // Cleanup + deleteObject(BUCKET, objectKey, v3Client); + v3Client.close(); + } + + @Test + public void testKmsContextV3Discovery() { final String objectKey = appendTestSuffix("kms-v3-to-v3-discovery-context"); // V3 Client - KmsKeyring @@ -107,7 +136,7 @@ public void testKmsContextDiscovery() { .kmsKeyId(KMS_KEY_ID) .build(); - final String input = "KmsContextV3toV1Discovery"; + final String input = "KmsContextV3toV3Discovery"; Map encryptionContext = new HashMap<>(); encryptionContext.put("user-metadata-key", "user-metadata-value-v3-to-v3-context"); @@ -134,4 +163,47 @@ public void testKmsContextDiscovery() { deleteObject(BUCKET, objectKey, v3Client); v3Client.close(); } + + @Test + public void testKmsContextV3DiscoveryWrongECFails() { + final String objectKey = appendTestSuffix("kms-v3-to-v3-discovery-context-wrong-ec"); + + // V3 Client - KmsKeyring + S3Client v3Client = S3EncryptionClient.builder() + .kmsKeyId(KMS_KEY_ID) + .build(); + + final String input = "KmsContextV3toV3Discovery"; + Map encryptionContext = new HashMap<>(); + encryptionContext.put("user-metadata-key", "user-metadata-value-v3-to-v3-context"); + + v3Client.putObject(builder -> builder + .bucket(BUCKET) + .key(objectKey) + .overrideConfiguration(withAdditionalConfiguration(encryptionContext)), RequestBody.fromString(input)); + + // V3 Client - KmsDiscoveryContext + KmsDiscoveryKeyring kmsDiscoveryKeyring = KmsDiscoveryKeyring + .builder().enableLegacyWrappingAlgorithms(true).build(); + S3Client v3ClientDiscovery = S3EncryptionClient.builder() + .keyring(kmsDiscoveryKeyring) + .build(); + + Map wrongEncryptionContext = new HashMap<>(); + encryptionContext.put("user-metadata-key", "user-metadata-value-v3-to-v3-wrong"); + try { + ResponseBytes objectResponse = v3ClientDiscovery.getObjectAsBytes(builder -> builder + .bucket(BUCKET) + .key(objectKey) + .overrideConfiguration(withAdditionalConfiguration(wrongEncryptionContext))); + fail(); + } catch (S3EncryptionClientException ex) { + // expected + assertTrue(ex.getMessage().contains("Provided encryption context does not match information retrieved from S3")); + } + + // Cleanup + deleteObject(BUCKET, objectKey, v3Client); + v3Client.close(); + } } From b2e639fda8d31626cf3887dfdcc0c4b4bea33fa9 Mon Sep 17 00:00:00 2001 From: J Plasmeier Date: Tue, 23 Jul 2024 15:34:34 -0700 Subject: [PATCH 4/6] add negative test for encrypt --- .../s3/materials/KmsDiscoveryKeyringTest.java | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/test/java/software/amazon/encryption/s3/materials/KmsDiscoveryKeyringTest.java b/src/test/java/software/amazon/encryption/s3/materials/KmsDiscoveryKeyringTest.java index 6320aea51..b55f43ebc 100644 --- a/src/test/java/software/amazon/encryption/s3/materials/KmsDiscoveryKeyringTest.java +++ b/src/test/java/software/amazon/encryption/s3/materials/KmsDiscoveryKeyringTest.java @@ -206,4 +206,32 @@ public void testKmsContextV3DiscoveryWrongECFails() { deleteObject(BUCKET, objectKey, v3Client); v3Client.close(); } + + @Test + public void testKmsContextV3DiscoveryEncryptFails() { + final String objectKey = appendTestSuffix("kms-v3-to-v3-discovery-context-encrypt-fails"); + + // V3 Client - KmsDiscoveryKeyring + KmsDiscoveryKeyring kmsDiscoveryKeyring = KmsDiscoveryKeyring + .builder().enableLegacyWrappingAlgorithms(true).build(); + S3Client v3ClientDiscovery = S3EncryptionClient.builder() + .keyring(kmsDiscoveryKeyring) + .build(); + + final String input = "KmsContextV3toV3Discovery"; + Map encryptionContext = new HashMap<>(); + encryptionContext.put("user-metadata-key", "user-metadata-value-v3-to-v3-context"); + + try { + v3ClientDiscovery.putObject(builder -> builder + .bucket(BUCKET) + .key(objectKey) + .overrideConfiguration(withAdditionalConfiguration(encryptionContext)), RequestBody.fromString(input)); + fail("expected exception"); + } catch (S3EncryptionClientException exception) { + // expected + assertTrue(exception.getMessage().contains("KmsDiscoveryKeyring does not support EncryptDataKey")); + } + } + } From 8798fe9cad83ef06c49d18fc0a7501d690676282 Mon Sep 17 00:00:00 2001 From: J Plasmeier Date: Fri, 26 Jul 2024 10:33:44 -0700 Subject: [PATCH 5/6] add some comments --- .../encryption/s3/materials/KmsDiscoveryKeyring.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main/java/software/amazon/encryption/s3/materials/KmsDiscoveryKeyring.java b/src/main/java/software/amazon/encryption/s3/materials/KmsDiscoveryKeyring.java index a7de4da80..4a89011dd 100644 --- a/src/main/java/software/amazon/encryption/s3/materials/KmsDiscoveryKeyring.java +++ b/src/main/java/software/amazon/encryption/s3/materials/KmsDiscoveryKeyring.java @@ -39,6 +39,10 @@ public KmsDiscoveryKeyring(Builder builder) { decryptDataKeyStrategies.put(_kmsContextDiscoveryStrategy.keyProviderInfo(), _kmsContextDiscoveryStrategy); } + /** + * This DecryptDataKeyStrategy decrypts objects encrypted using the legacy kms v1 mode + * using whichever key the object was encrypted with. + */ private final DecryptDataKeyStrategy _kmsDiscoveryStrategy = new DecryptDataKeyStrategy() { private static final String KEY_PROVIDER_INFO = "kms"; @@ -66,6 +70,10 @@ public byte[] decryptDataKey(DecryptionMaterials materials, byte[] encryptedData } }; + /** + * This DecryptDataKeyStrategy decrypts objects encrypted using the kms+context v2 mode + * using whichever key the object was encrypted with. + */ private final DecryptDataKeyStrategy _kmsContextDiscoveryStrategy = new DecryptDataKeyStrategy() { private static final String KEY_PROVIDER_INFO = "kms+context"; From 190b776532a38393da7265f483069ed8daf94a9d Mon Sep 17 00:00:00 2001 From: J Plasmeier Date: Fri, 26 Jul 2024 10:38:34 -0700 Subject: [PATCH 6/6] more comments --- .../amazon/encryption/s3/materials/KmsDiscoveryKeyring.java | 1 + .../java/software/amazon/encryption/s3/materials/KmsKeyring.java | 1 + 2 files changed, 2 insertions(+) diff --git a/src/main/java/software/amazon/encryption/s3/materials/KmsDiscoveryKeyring.java b/src/main/java/software/amazon/encryption/s3/materials/KmsDiscoveryKeyring.java index 4a89011dd..54b18bc29 100644 --- a/src/main/java/software/amazon/encryption/s3/materials/KmsDiscoveryKeyring.java +++ b/src/main/java/software/amazon/encryption/s3/materials/KmsDiscoveryKeyring.java @@ -104,6 +104,7 @@ public byte[] decryptDataKey(DecryptionMaterials materials, byte[] encryptedData } // We are validating the encryption context to match S3EC V2 behavior + // Refer to KMSMaterialsHandler in the V2 client for details Map materialsEncryptionContextCopy = new HashMap<>(materials.encryptionContext()); materialsEncryptionContextCopy.remove(KEY_ID_CONTEXT_KEY); materialsEncryptionContextCopy.remove(ENCRYPTION_CONTEXT_ALGORITHM_KEY); diff --git a/src/main/java/software/amazon/encryption/s3/materials/KmsKeyring.java b/src/main/java/software/amazon/encryption/s3/materials/KmsKeyring.java index e436815b7..6088811b3 100644 --- a/src/main/java/software/amazon/encryption/s3/materials/KmsKeyring.java +++ b/src/main/java/software/amazon/encryption/s3/materials/KmsKeyring.java @@ -178,6 +178,7 @@ public byte[] decryptDataKey(DecryptionMaterials materials, byte[] encryptedData } // We are validating the encryption context to match S3EC V2 behavior + // Refer to KMSMaterialsHandler in the V2 client for details Map materialsEncryptionContextCopy = new HashMap<>(materials.encryptionContext()); materialsEncryptionContextCopy.remove(KEY_ID_CONTEXT_KEY); materialsEncryptionContextCopy.remove(ENCRYPTION_CONTEXT_ALGORITHM_KEY);