Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package software.amazon.encryption.s3.materials;

import software.amazon.encryption.s3.S3EncryptionClientException;
import software.amazon.encryption.s3.algorithms.AlgorithmSuite;

/**
* Content-encryption algorithm handling shared by the Kms+Context decrypt paths of
* {@link KmsKeyring} and {@link KmsDiscoveryKeyring}.
*/
final class KmsContentAlgorithm {

private static final String ENCRYPTION_CONTEXT_ALGORITHM_KEY = "aws:x-amz-cek-alg";

private KmsContentAlgorithm() {
}

/**
* The stored content-encryption algorithm (KMS-authenticated) MUST match the algorithm suite
* selected from content metadata (unauthenticated).
*/
static void validate(DecryptionMaterials materials) {
String storedContentAlgorithm = materials.encryptionContext().get(ENCRYPTION_CONTEXT_ALGORITHM_KEY);
if (storedContentAlgorithm != null
&& !storedContentAlgorithm.equals(forSuite(materials.algorithmSuite()))) {
throw new S3EncryptionClientException(String.format(
"The content encryption algorithm in the KMS-authenticated encryption context (%s) does not "
+ "match the algorithm suite selected for decryption (%s).",
storedContentAlgorithm, materials.algorithmSuite().name()));
}
}

/**
* Return content-encryption algorithm for a given AlgorithmSuite. A CTR suite returns the value of its
* GCM counterpart, because ranged gets decrypt a GCM object with the CTR suite.
*/
static String forSuite(AlgorithmSuite algorithmSuite) {
switch (algorithmSuite) {
case ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY:
case ALG_AES_256_CTR_HKDF_SHA512_COMMIT_KEY:
return AlgorithmSuite.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY.idAsString();
case ALG_AES_256_GCM_IV12_TAG16_NO_KDF:
case ALG_AES_256_CTR_IV16_TAG16_NO_KDF:
return AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF.cipherName();
default:
return algorithmSuite.cipherName();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@ public byte[] decryptDataKey(DecryptionMaterials materials, byte[] encryptedData
throw new S3EncryptionClientException("Provided encryption context does not match information retrieved from S3");
}

KmsContentAlgorithm.validate(materials);

DecryptRequest request = DecryptRequest.builder()
.encryptionContext(materials.encryptionContext())
.ciphertextBlob(SdkBytes.fromByteArray(encryptedDataKey))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,8 @@ public byte[] decryptDataKey(DecryptionMaterials materials, byte[] encryptedData
throw new S3EncryptionClientException("Provided encryption context does not match information retrieved from S3");
}

KmsContentAlgorithm.validate(materials);

//= specification/s3-encryption/materials/s3-kms-keyring.md#kms-context
//# To attempt to decrypt a particular [encrypted data key](../structures.md#encrypted-data-key), the KmsKeyring
//# MUST call [AWS KMS Decrypt](https://docs.aws.amazon.com/kms/latest/APIReference/API_Decrypt.html) with the configured AWS KMS client.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package software.amazon.encryption.s3.materials;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.EnumSource;
import org.junit.jupiter.params.provider.MethodSource;
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
import software.amazon.encryption.s3.algorithms.AlgorithmSuite;

import java.util.Collections;
import java.util.Map;
import java.util.stream.Stream;

import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;

public class KmsContentAlgorithmTest {

private static Stream<Arguments> contentAlgorithmBySuite() {
return Stream.of(
Arguments.of(AlgorithmSuite.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY, "115"),
Arguments.of(AlgorithmSuite.ALG_AES_256_CTR_HKDF_SHA512_COMMIT_KEY, "115"),
Arguments.of(AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF, "AES/GCM/NoPadding"),
Arguments.of(AlgorithmSuite.ALG_AES_256_CTR_IV16_TAG16_NO_KDF, "AES/GCM/NoPadding"),
Arguments.of(AlgorithmSuite.ALG_AES_256_CBC_IV16_NO_KDF, "AES/CBC/PKCS5Padding"));
}

@ParameterizedTest
@MethodSource("contentAlgorithmBySuite")
public void forSuiteReturnsStoredContentAlgorithm(AlgorithmSuite suite, String expected) {
assertEquals(expected, KmsContentAlgorithm.forSuite(suite));
}

@ParameterizedTest
@MethodSource("contentAlgorithmBySuite")
public void validateAcceptsMatchingContentAlgorithm(AlgorithmSuite suite, String storedContentAlgorithm) {
DecryptionMaterials materials = materials(suite,
Collections.singletonMap("aws:x-amz-cek-alg", storedContentAlgorithm));
assertDoesNotThrow(() -> KmsContentAlgorithm.validate(materials));
}

@ParameterizedTest
@EnumSource(AlgorithmSuite.class)
public void validateSkipsWhenContentAlgorithmIsAbsent(AlgorithmSuite suite) {
DecryptionMaterials materials = materials(suite,
Collections.singletonMap("kms_cmk_id", "arn:aws:kms:us-west-2:111122223333:key/example"));
assertDoesNotThrow(() -> KmsContentAlgorithm.validate(materials));
}

@Test
public void forSuiteCoversEveryAlgorithmSuite() {
assertEquals(AlgorithmSuite.values().length, contentAlgorithmBySuite().count());
}

private static DecryptionMaterials materials(AlgorithmSuite suite, Map<String, String> encryptionContext) {
return DecryptionMaterials.builder()
.s3Request(GetObjectRequest.builder().bucket("bucket").key("key").build())
.algorithmSuite(suite)
.encryptionContext(encryptionContext)
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,24 +11,35 @@
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 org.junitpioneer.jupiter.RetryingTest;
import software.amazon.awssdk.core.ResponseBytes;
import software.amazon.awssdk.core.SdkBytes;
import software.amazon.awssdk.core.sync.RequestBody;
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.S3Client;
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
import software.amazon.awssdk.services.s3.model.GetObjectResponse;
import software.amazon.encryption.s3.CommitmentPolicy;
import software.amazon.encryption.s3.S3EncryptionClient;
import software.amazon.encryption.s3.S3EncryptionClientException;
import software.amazon.encryption.s3.algorithms.AlgorithmSuite;

import java.io.ByteArrayInputStream;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
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 org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
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;
Expand Down Expand Up @@ -237,4 +248,39 @@ public void testKmsContextV3DiscoveryEncryptFails() {
}
}

// The data key was wrapped for a GCM object, so the KMS-authenticated encryption context carries
// aws:x-amz-cek-alg = AES/GCM/NoPadding, while the decryption materials select the CBC suite.
@Test
public void kmsContextDiscoveryDecryptFailsWhenAuthenticatedCekAlgDoesNotMatchSelectedSuite() {
Map<String, String> storedEncryptionContext = new HashMap<>();
storedEncryptionContext.put("kms_cmk_id", "arn:aws:kms:us-west-2:111122223333:key/example");
storedEncryptionContext.put("aws:x-amz-cek-alg", AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF.cipherName());

KmsClient kmsClient = mock(KmsClient.class);
when(kmsClient.decrypt(argThat((DecryptRequest request) ->
storedEncryptionContext.equals(request.encryptionContext()))))
.thenReturn(DecryptResponse.builder()
.plaintext(SdkBytes.fromByteArray(new byte[32]))
.build());

KmsDiscoveryKeyring keyring = KmsDiscoveryKeyring.builder()
.kmsClient(kmsClient)
.build();

DecryptionMaterials materials = DecryptionMaterials.builder()
.s3Request(GetObjectRequest.builder().bucket("bucket").key("key").build())
.algorithmSuite(AlgorithmSuite.ALG_AES_256_CBC_IV16_NO_KDF)
.encryptionContext(storedEncryptionContext)
.build();

EncryptedDataKey edk = EncryptedDataKey.builder()
.keyProviderId(S3Keyring.KEY_PROVIDER_ID)
.keyProviderInfo("kms+context")
.encryptedDataKey(new byte[]{1, 2, 3})
.build();

assertThrows(S3EncryptionClientException.class,
() -> keyring.onDecrypt(materials, Collections.singletonList(edk)));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,31 @@
// SPDX-License-Identifier: Apache-2.0
package software.amazon.encryption.s3.materials;

import org.junit.jupiter.api.Test;
import org.junitpioneer.jupiter.RetryingTest;
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.S3EncryptionClientException;
import software.amazon.encryption.s3.algorithms.AlgorithmSuite;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

public class KmsKeyringTest {

private static final String KMS_KEY_ID = "arn:aws:kms:us-west-2:111122223333:key/example";

@RetryingTest(3)
public void buildKmsKeyringWithNullSecureRandomFails() {
assertThrows(S3EncryptionClientException.class, () -> KmsKeyring.builder().secureRandom(null));
Expand All @@ -19,4 +37,83 @@ public void buildKmsKeyringWithNullDataKeyGeneratorFails() {
assertThrows(S3EncryptionClientException.class, () -> KmsKeyring.builder().dataKeyGenerator(null));
}

// The data key was wrapped for a GCM object, so the KMS-authenticated encryption context carries
// aws:x-amz-cek-alg = AES/GCM/NoPadding. The decryption materials select the CBC suite (as they would
// if the content-algorithm metadata read CBC). The authenticated content algorithm and the selected
// suite disagree, so onDecrypt must fail rather than return the data key.
@Test
public void kmsContextDecryptFailsWhenAuthenticatedCekAlgDoesNotMatchSelectedSuite() {
Map<String, String> storedEncryptionContext = new HashMap<>();
storedEncryptionContext.put("kms_cmk_id", KMS_KEY_ID);
storedEncryptionContext.put("aws:x-amz-cek-alg", AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF.cipherName());

// Releases the data key only for the encryption context it was wrapped under.
KmsClient kmsClient = mock(KmsClient.class);
when(kmsClient.decrypt(argThat((DecryptRequest request) ->
storedEncryptionContext.equals(request.encryptionContext()))))
.thenReturn(DecryptResponse.builder()
.keyId(KMS_KEY_ID)
.plaintext(SdkBytes.fromByteArray(new byte[32]))
.build());

KmsKeyring keyring = KmsKeyring.builder()
.kmsClient(kmsClient)
.wrappingKeyId(KMS_KEY_ID)
.build();

DecryptionMaterials materials = DecryptionMaterials.builder()
.s3Request(GetObjectRequest.builder().bucket("bucket").key("key").build())
.algorithmSuite(AlgorithmSuite.ALG_AES_256_CBC_IV16_NO_KDF)
.encryptionContext(storedEncryptionContext)
.build();

EncryptedDataKey edk = EncryptedDataKey.builder()
.keyProviderId(S3Keyring.KEY_PROVIDER_ID)
.keyProviderInfo("kms+context")
.encryptedDataKey(new byte[]{1, 2, 3})
.build();

S3EncryptionClientException exception = assertThrows(S3EncryptionClientException.class,
() -> keyring.onDecrypt(materials, Collections.singletonList(edk)));
assertTrue(exception.getMessage().contains("AES/GCM/NoPadding"));
assertTrue(exception.getMessage().contains("ALG_AES_256_CBC_IV16_NO_KDF"));
}

// A ranged get selects the CTR suite for an object sealed under GCM, so the stored GCM content
// algorithm must be accepted for the CTR suite.
@Test
public void kmsContextDecryptSucceedsForCtrSuiteOfGcmObject() {
Map<String, String> storedEncryptionContext = new HashMap<>();
storedEncryptionContext.put("kms_cmk_id", KMS_KEY_ID);
storedEncryptionContext.put("aws:x-amz-cek-alg", AlgorithmSuite.ALG_AES_256_GCM_IV12_TAG16_NO_KDF.cipherName());

KmsClient kmsClient = mock(KmsClient.class);
when(kmsClient.decrypt(argThat((DecryptRequest request) ->
storedEncryptionContext.equals(request.encryptionContext()))))
.thenReturn(DecryptResponse.builder()
.keyId(KMS_KEY_ID)
.plaintext(SdkBytes.fromByteArray(new byte[32]))
.build());

KmsKeyring keyring = KmsKeyring.builder()
.kmsClient(kmsClient)
.wrappingKeyId(KMS_KEY_ID)
.build();

DecryptionMaterials materials = DecryptionMaterials.builder()
.s3Request(GetObjectRequest.builder().bucket("bucket").key("key").build())
.algorithmSuite(AlgorithmSuite.ALG_AES_256_CTR_IV16_TAG16_NO_KDF)
.encryptionContext(storedEncryptionContext)
.build();

EncryptedDataKey edk = EncryptedDataKey.builder()
.keyProviderId(S3Keyring.KEY_PROVIDER_ID)
.keyProviderInfo("kms+context")
.encryptedDataKey(new byte[]{1, 2, 3})
.build();

assertArrayEquals(new byte[32],
keyring.onDecrypt(materials, Collections.singletonList(edk)).plaintextDataKey());
}

}
Loading