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..54b18bc29 --- /dev/null +++ b/src/main/java/software/amazon/encryption/s3/materials/KmsDiscoveryKeyring.java @@ -0,0 +1,175 @@ +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; + + 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; + decryptDataKeyStrategies.put(_kmsDiscoveryStrategy.keyProviderInfo(), _kmsDiscoveryStrategy); + 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"; + + @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(); + } + }; + + /** + * 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"; + 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 + // 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); + 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(); + } + }; + + 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 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/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); 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..b55f43ebc --- /dev/null +++ b/src/test/java/software/amazon/encryption/s3/materials/KmsDiscoveryKeyringTest.java @@ -0,0 +1,237 @@ +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.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.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 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; + +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"))); + + @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 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 + 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(); + + 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(); + } + + @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(); + } + + @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")); + } + } + +} 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)); } }