From cfb0fa0ea4946916f9de34f0de945e984f5c9457 Mon Sep 17 00:00:00 2001 From: John Walker Date: Fri, 16 Nov 2018 12:24:09 -0800 Subject: [PATCH 1/3] Add support for EncryptionContext overrides to the DynamoDBEncryptor There are people asking for overrides, and it would be better if they didn't need to wait for longer term, internal refactors before they are able to use overrides. This adds optional operators that give the client the last say on the EncryptionContext's value. Note: I haven't tested the example, since I didn't figure out a way to run them. How are we running example code? If we don't have a suggested way yet, I can figure something out and document it. --- .../examples/AwsKmsEncryptedObject.java | 3 +- ...ionContextOverridesWithDynamoDBMapper.java | 115 +++++++++++++ .../encryption/DynamoDBEncryptor.java | 34 +++- .../utils/EncryptionContextOperators.java | 65 ++++++++ .../encryption/DynamoDBEncryptorTest.java | 69 +++++++- .../utils/EncryptionContextOperatorsTest.java | 152 ++++++++++++++++++ 6 files changed, 433 insertions(+), 5 deletions(-) create mode 100644 examples/com/amazonaws/examples/EncryptionContextOverridesWithDynamoDBMapper.java create mode 100644 src/main/java/com/amazonaws/services/dynamodbv2/datamodeling/encryption/utils/EncryptionContextOperators.java create mode 100644 src/test/java/com/amazonaws/services/dynamodbv2/datamodeling/encryption/utils/EncryptionContextOperatorsTest.java diff --git a/examples/com/amazonaws/examples/AwsKmsEncryptedObject.java b/examples/com/amazonaws/examples/AwsKmsEncryptedObject.java index d2fc9d36..041739a7 100644 --- a/examples/com/amazonaws/examples/AwsKmsEncryptedObject.java +++ b/examples/com/amazonaws/examples/AwsKmsEncryptedObject.java @@ -52,7 +52,8 @@ public static void encryptRecord(final String cmkArn, final String region) { // Encryptor creation final DynamoDBEncryptor encryptor = DynamoDBEncryptor.getInstance(cmp); // Mapper Creation - // Please note the use of SaveBehavior.CLOBBER. Omitting this can result in data-corruption. + // Please note the use of SaveBehavior.CLOBBER (SaveBehavior.PUT works as well). + // Omitting this can result in data-corruption. DynamoDBMapperConfig mapperConfig = DynamoDBMapperConfig.builder().withSaveBehavior(SaveBehavior.CLOBBER).build(); DynamoDBMapper mapper = new DynamoDBMapper(ddb, mapperConfig, new AttributeEncryptor(encryptor)); diff --git a/examples/com/amazonaws/examples/EncryptionContextOverridesWithDynamoDBMapper.java b/examples/com/amazonaws/examples/EncryptionContextOverridesWithDynamoDBMapper.java new file mode 100644 index 00000000..b7943976 --- /dev/null +++ b/examples/com/amazonaws/examples/EncryptionContextOverridesWithDynamoDBMapper.java @@ -0,0 +1,115 @@ +package com.amazonaws.examples; + +import com.amazonaws.services.dynamodbv2.AmazonDynamoDB; +import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClientBuilder; +import com.amazonaws.services.dynamodbv2.datamodeling.AttributeEncryptor; +import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBAttribute; +import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBHashKey; +import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapper; +import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapperConfig; +import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBRangeKey; +import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBTable; +import com.amazonaws.services.dynamodbv2.datamodeling.encryption.DynamoDBEncryptor; +import com.amazonaws.services.dynamodbv2.datamodeling.encryption.providers.DirectKmsMaterialProvider; +import com.amazonaws.services.dynamodbv2.model.AttributeValue; +import com.amazonaws.services.kms.AWSKMS; +import com.amazonaws.services.kms.AWSKMSClientBuilder; + +import java.security.GeneralSecurityException; +import java.util.HashMap; +import java.util.Map; + +import static com.amazonaws.services.dynamodbv2.datamodeling.encryption.utils.EncryptionContextOperators.overrideEncryptionContextTableNameUsingMap; + +public class EncryptionContextOverridesWithDynamoDBMapper { + public static void main(String[] args) throws GeneralSecurityException { + final String cmkArn = args[0]; + final String region = args[1]; + final String encryptionContextTableName = args[2]; + + encryptRecord(cmkArn, region, encryptionContextTableName); + } + + public static void encryptRecord(final String cmkArn, + final String region, + final String newEncryptionContextTableName) { + // Sample object to be encrypted + ExampleItem record = new ExampleItem(); + record.setPartitionAttribute("is this"); + record.setSortAttribute(55); + record.setExample("my data"); + + // Set up our configuration and clients + final AmazonDynamoDB ddb = AmazonDynamoDBClientBuilder.standard().withRegion(region).build(); + final AWSKMS kms = AWSKMSClientBuilder.standard().withRegion(region).build(); + final DirectKmsMaterialProvider cmp = new DirectKmsMaterialProvider(kms, cmkArn); + // Encryptor creation + final DynamoDBEncryptor encryptor = DynamoDBEncryptor.getInstance(cmp); + + Map tableNameEncryptionContextOverrides = new HashMap<>(); + tableNameEncryptionContextOverrides.put("ExampleTableForEncryptionContextOverrides", newEncryptionContextTableName); + tableNameEncryptionContextOverrides.put("AnotherExampleTableForEncryptionContextOverrides", "this table doesn't exist"); + + // Here we supply an operator to override the table name used in the encryption context + encryptor.setEncryptionContextOverrideOperator( + overrideEncryptionContextTableNameUsingMap(tableNameEncryptionContextOverrides) + ); + + // Mapper Creation + // Please note the use of SaveBehavior.CLOBBER (SaveBehavior.PUT works as well). + // Omitting this can result in data-corruption. + DynamoDBMapperConfig mapperConfig = DynamoDBMapperConfig.builder() + .withSaveBehavior(DynamoDBMapperConfig.SaveBehavior.CLOBBER).build(); + DynamoDBMapper mapper = new DynamoDBMapper(ddb, mapperConfig, new AttributeEncryptor(encryptor)); + + System.out.println("Plaintext Record: " + record); + // Save the record to the DynamoDB table + mapper.save(record); + + // Retrieve the encrypted record (directly without decrypting) from Dynamo so we can see it in our example + final Map itemKey = new HashMap<>(); + itemKey.put("partition_attribute", new AttributeValue().withS("is this")); + itemKey.put("sort_attribute", new AttributeValue().withN("55")); + System.out.println("Encrypted Record: " + ddb.getItem("ExampleTableForEncryptionContextOverrides", + itemKey).getItem()); + + // Retrieve (and decrypt) it from DynamoDB + ExampleItem decrypted_record = mapper.load(ExampleItem.class, "is this", 55); + System.out.println("Decrypted Record: " + decrypted_record); + } + + @DynamoDBTable(tableName = "ExampleTableForEncryptionContextOverrides") + public static final class ExampleItem { + private String partitionAttribute; + private int sortAttribute; + private String example; + + @DynamoDBHashKey(attributeName = "partition_attribute") + public String getPartitionAttribute() { + return partitionAttribute; + } + + public void setPartitionAttribute(String partitionAttribute) { + this.partitionAttribute = partitionAttribute; + } + + @DynamoDBRangeKey(attributeName = "sort_attribute") + public int getSortAttribute() { + return sortAttribute; + } + + public void setSortAttribute(int sortAttribute) { + this.sortAttribute = sortAttribute; + } + + @DynamoDBAttribute(attributeName = "example") + public String getExample() { + return example; + } + + public void setExample(String example) { + this.example = example; + } + } + +} diff --git a/src/main/java/com/amazonaws/services/dynamodbv2/datamodeling/encryption/DynamoDBEncryptor.java b/src/main/java/com/amazonaws/services/dynamodbv2/datamodeling/encryption/DynamoDBEncryptor.java index 76be51a9..7a70291c 100644 --- a/src/main/java/com/amazonaws/services/dynamodbv2/datamodeling/encryption/DynamoDBEncryptor.java +++ b/src/main/java/com/amazonaws/services/dynamodbv2/datamodeling/encryption/DynamoDBEncryptor.java @@ -81,7 +81,8 @@ public class DynamoDBEncryptor { private final String signingAlgorithmHeader; public static final String DEFAULT_SIGNING_ALGORITHM_HEADER = DEFAULT_DESCRIPTION_BASE + "signingAlg"; - + private Function encryptionContextOverrideOperator; + protected DynamoDBEncryptor(EncryptionMaterialsProvider provider, String descriptionBase) { this.encryptionMaterialsProvider = provider; this.descriptionBase = descriptionBase; @@ -254,6 +255,11 @@ public Map decryptRecord( .withAttributeValues(itemAttributes) .build(); + Function encryptionContextOverrideOperator = getEncryptionContextOverrideOperator(); + if (encryptionContextOverrideOperator != null) { + context = encryptionContextOverrideOperator.apply(context); + } + materials = encryptionMaterialsProvider.getDecryptionMaterials(context); decryptionKey = materials.getDecryptionKey(); if (materialDescription.containsKey(signingAlgorithmHeader)) { @@ -307,7 +313,13 @@ public Map encryptRecord( context = new EncryptionContext.Builder(context) .withAttributeValues(itemAttributes) .build(); - + + Function encryptionContextOverrideOperator = + getEncryptionContextOverrideOperator(); + if (encryptionContextOverrideOperator != null) { + context = encryptionContextOverrideOperator.apply(context); + } + EncryptionMaterials materials = encryptionMaterialsProvider.getEncryptionMaterials(context); // We need to copy this because we modify it to record other encryption details Map materialDescription = new HashMap( @@ -559,6 +571,24 @@ protected static Map unmarshallDescription(AttributeValue attrib } } + /** + * @param encryptionContextOverrideOperator the nullable operator which will be used to override + * the EncryptionContext. + * @see com.amazonaws.services.dynamodbv2.datamodeling.encryption.utils.EncryptionContextOperators + */ + public final void setEncryptionContextOverrideOperator( + Function encryptionContextOverrideOperator) { + this.encryptionContextOverrideOperator = encryptionContextOverrideOperator; + } + + /** + * @return the operator used to override the EncryptionContext + * @see #setEncryptionContextOverrideOperator(Function) + */ + public final Function getEncryptionContextOverrideOperator() { + return encryptionContextOverrideOperator; + } + private static byte[] toByteArray(ByteBuffer buffer) { buffer = buffer.duplicate(); // We can only return the array directly if: diff --git a/src/main/java/com/amazonaws/services/dynamodbv2/datamodeling/encryption/utils/EncryptionContextOperators.java b/src/main/java/com/amazonaws/services/dynamodbv2/datamodeling/encryption/utils/EncryptionContextOperators.java new file mode 100644 index 00000000..66831035 --- /dev/null +++ b/src/main/java/com/amazonaws/services/dynamodbv2/datamodeling/encryption/utils/EncryptionContextOperators.java @@ -0,0 +1,65 @@ +package com.amazonaws.services.dynamodbv2.datamodeling.encryption.utils; + +import com.amazonaws.services.dynamodbv2.datamodeling.encryption.EncryptionContext; + +import java.util.Map; +import java.util.function.UnaryOperator; + +/** + * Implementations of common operators for overriding the EncryptionContext + */ +public class EncryptionContextOperators { + /** + * An operator for overriding EncryptionContext's table name for a specific DynamoDBEncryptor. If any table names or + * the encryption context itself is null, then it returns the original EncryptionContext. + * + * @param originalTableName the name of the table that should be overridden in the Encryption Context + * @param newTableName the table name that should be used in the Encryption Context + * @return A UnaryOperator that produces a new EncryptionContext with the supplied table name + */ + public static UnaryOperator overrideEncryptionContextTableName( + String originalTableName, + String newTableName) { + return encryptionContext -> { + if (encryptionContext == null + || encryptionContext.getTableName() == null + || originalTableName == null + || newTableName == null) { + return encryptionContext; + } + if (originalTableName.equals(encryptionContext.getTableName())) { + return new EncryptionContext.Builder(encryptionContext).withTableName(newTableName).build(); + } else { + return encryptionContext; + } + }; + } + + + /** + * An operator for mapping multiple table names in the Encryption Context to a new table name. If the table name for + * a given EncryptionContext is missing, then it returns the original EncryptionContext. Similarly, it returns the + * original EncryptionContext if the value it is overridden to is null, or if the original table name is null. + * + * @param tableNameOverrideMap a map specifying the names of tables that should be overridden, + * and the values to which they should be overridden. If the given table name + * corresponds to null, or isn't in the map, then the table name won't be overridden. + * @return A UnaryOperator that produces a new EncryptionContext with the supplied table name + */ + public static UnaryOperator overrideEncryptionContextTableNameUsingMap( + Map tableNameOverrideMap) { + return encryptionContext -> { + if (tableNameOverrideMap == null || encryptionContext == null || encryptionContext.getTableName() == null) { + return encryptionContext; + } + String newTableName = tableNameOverrideMap.get(encryptionContext.getTableName()); + if (newTableName != null) { + return new EncryptionContext.Builder(encryptionContext).withTableName(newTableName).build(); + } else { + return encryptionContext; + } + }; + } + + +} diff --git a/src/test/java/com/amazonaws/services/dynamodbv2/datamodeling/encryption/DynamoDBEncryptorTest.java b/src/test/java/com/amazonaws/services/dynamodbv2/datamodeling/encryption/DynamoDBEncryptorTest.java index 0a7683b6..7d193304 100644 --- a/src/test/java/com/amazonaws/services/dynamodbv2/datamodeling/encryption/DynamoDBEncryptorTest.java +++ b/src/test/java/com/amazonaws/services/dynamodbv2/datamodeling/encryption/DynamoDBEncryptorTest.java @@ -14,8 +14,10 @@ */ package com.amazonaws.services.dynamodbv2.datamodeling.encryption; +import static com.amazonaws.services.dynamodbv2.datamodeling.encryption.utils.EncryptionContextOperators.overrideEncryptionContextTableName; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThat; @@ -31,7 +33,6 @@ import java.security.NoSuchProviderException; import java.security.Security; import java.security.SignatureException; -import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -296,7 +297,71 @@ public void RsaSignedOnlyBadSignature() throws GeneralSecurityException { encryptedAttributes.get("hashKey").setN("666"); encryptor.decryptAllFieldsExcept(encryptedAttributes, context, attribs.keySet().toArray(new String[0])); } - + + /** + * Tests that no exception is thrown when the encryption context override operator is null + * @throws GeneralSecurityException + */ + @Test + public void testNullEncryptionContextOperator() throws GeneralSecurityException { + DynamoDBEncryptor encryptor = DynamoDBEncryptor.getInstance(prov); + encryptor.setEncryptionContextOverrideOperator(null); + encryptor.encryptAllFieldsExcept(attribs, context, Collections.emptyList()); + } + + /** + * Tests decrypt and encrypt with an encryption context override operator + * @throws GeneralSecurityException + */ + @Test + public void testTableNameOverriddenEncryptionContextOperator() throws GeneralSecurityException { + // Ensure that the table name is different from what we override the table to. + assertNotEquals(context.getTableName(), "TheBestTableName"); + DynamoDBEncryptor encryptor = DynamoDBEncryptor.getInstance(prov); + encryptor.setEncryptionContextOverrideOperator(overrideEncryptionContextTableName(context.getTableName(), "TheBestTableName")); + Map encryptedItems = encryptor.encryptAllFieldsExcept(attribs, context, Collections.emptyList()); + Map decryptedItems = encryptor.decryptAllFieldsExcept(encryptedItems, context, Collections.emptyList()); + assertThat(decryptedItems, AttrMatcher.match(attribs)); + } + + + /** + * Tests encrypt with an encryption context override operator, and a second encryptor without an override + * @throws GeneralSecurityException + */ + @Test + public void testTableNameOverriddenEncryptionContextOperatorWithSecondEncryptor() throws GeneralSecurityException { + // Ensure that the table name is different from what we override the table to. + assertNotEquals(context.getTableName(), "TheBestTableName"); + DynamoDBEncryptor encryptor = DynamoDBEncryptor.getInstance(prov); + DynamoDBEncryptor encryptorWithoutOverride = DynamoDBEncryptor.getInstance(prov); + encryptor.setEncryptionContextOverrideOperator(overrideEncryptionContextTableName(context.getTableName(), "TheBestTableName")); + Map encryptedItems = encryptor.encryptAllFieldsExcept(attribs, context, Collections.emptyList()); + + EncryptionContext expectedOverriddenContext = new EncryptionContext.Builder(context).withTableName("TheBestTableName").build(); + Map decryptedItems = encryptorWithoutOverride.decryptAllFieldsExcept(encryptedItems, + expectedOverriddenContext, Collections.emptyList()); + assertThat(decryptedItems, AttrMatcher.match(attribs)); + } + + /** + * Tests encrypt with an encryption context override operator, and a second encryptor without an override + * @throws GeneralSecurityException + */ + @Test(expected = SignatureException.class) + public void testTableNameOverriddenEncryptionContextOperatorWithSecondEncryptorButTheOriginalEncryptionContext() throws GeneralSecurityException { + // Ensure that the table name is different from what we override the table to. + assertNotEquals(context.getTableName(), "TheBestTableName"); + DynamoDBEncryptor encryptor = DynamoDBEncryptor.getInstance(prov); + DynamoDBEncryptor encryptorWithoutOverride = DynamoDBEncryptor.getInstance(prov); + encryptor.setEncryptionContextOverrideOperator(overrideEncryptionContextTableName(context.getTableName(), "TheBestTableName")); + Map encryptedItems = encryptor.encryptAllFieldsExcept(attribs, context, Collections.emptyList()); + + // Use the original encryption context, and expect a signature failure + Map decryptedItems = encryptorWithoutOverride.decryptAllFieldsExcept(encryptedItems, + context, Collections.emptyList()); + } + @Test public void EcdsaSignedOnly() throws GeneralSecurityException { diff --git a/src/test/java/com/amazonaws/services/dynamodbv2/datamodeling/encryption/utils/EncryptionContextOperatorsTest.java b/src/test/java/com/amazonaws/services/dynamodbv2/datamodeling/encryption/utils/EncryptionContextOperatorsTest.java new file mode 100644 index 00000000..9749060a --- /dev/null +++ b/src/test/java/com/amazonaws/services/dynamodbv2/datamodeling/encryption/utils/EncryptionContextOperatorsTest.java @@ -0,0 +1,152 @@ +package com.amazonaws.services.dynamodbv2.datamodeling.encryption.utils; + +import com.amazonaws.services.dynamodbv2.datamodeling.encryption.EncryptionContext; +import org.junit.Test; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.UnaryOperator; + +import static com.amazonaws.services.dynamodbv2.datamodeling.encryption.utils.EncryptionContextOperators.overrideEncryptionContextTableName; +import static com.amazonaws.services.dynamodbv2.datamodeling.encryption.utils.EncryptionContextOperators.overrideEncryptionContextTableNameUsingMap; +import static org.junit.Assert.*; + +public class EncryptionContextOperatorsTest { + + @Test + public void testCreateEncryptionContextTableNameOverride_expectedOverride() { + UnaryOperator myNewTableName = overrideEncryptionContextTableName("OriginalTableName", "MyNewTableName"); + + EncryptionContext context = new EncryptionContext.Builder().withTableName("OriginalTableName").build(); + + EncryptionContext newContext = myNewTableName.apply(context); + + assertEquals("OriginalTableName", context.getTableName()); + assertEquals("MyNewTableName", newContext.getTableName()); + } + + /** + * Some pretty clear repetition in null cases. May make sense to replace with data providers or parameterized + * classes for null cases + */ + @Test + public void testNullCasesCreateEncryptionContextTableNameOverride_nullOriginalTableName() { + assertEncryptionContextUnchanged(new EncryptionContext.Builder().withTableName("example").build(), + null, + "MyNewTableName"); + } + + @Test + public void testCreateEncryptionContextTableNameOverride_differentOriginalTableName() { + assertEncryptionContextUnchanged(new EncryptionContext.Builder().withTableName("example").build(), + "DifferentTableName", + "MyNewTableName"); + } + + @Test + public void testNullCasesCreateEncryptionContextTableNameOverride_nullEncryptionContext() { + assertEncryptionContextUnchanged(null, + "DifferentTableName", + "MyNewTableName"); + } + + @Test + public void testCreateEncryptionContextTableNameOverrideMap_expectedOverride() { + Map tableNameOverrides = new HashMap<>(); + tableNameOverrides.put("OriginalTableName", "MyNewTableName"); + + + UnaryOperator nameOverrideMap = + overrideEncryptionContextTableNameUsingMap(tableNameOverrides); + + EncryptionContext context = new EncryptionContext.Builder().withTableName("OriginalTableName").build(); + + EncryptionContext newContext = nameOverrideMap.apply(context); + + assertEquals("OriginalTableName", context.getTableName()); + assertEquals("MyNewTableName", newContext.getTableName()); + } + + @Test + public void testCreateEncryptionContextTableNameOverrideMap_multipleOverrides() { + Map tableNameOverrides = new HashMap<>(); + tableNameOverrides.put("OriginalTableName1", "MyNewTableName1"); + tableNameOverrides.put("OriginalTableName2", "MyNewTableName2"); + + + UnaryOperator overrideOperator = + overrideEncryptionContextTableNameUsingMap(tableNameOverrides); + + EncryptionContext context = new EncryptionContext.Builder().withTableName("OriginalTableName1").build(); + + EncryptionContext newContext = overrideOperator.apply(context); + + assertEquals("OriginalTableName1", context.getTableName()); + assertEquals("MyNewTableName1", newContext.getTableName()); + + EncryptionContext context2 = new EncryptionContext.Builder().withTableName("OriginalTableName2").build(); + + EncryptionContext newContext2 = overrideOperator.apply(context2); + + assertEquals("OriginalTableName2", context2.getTableName()); + assertEquals("MyNewTableName2", newContext2.getTableName()); + + } + + + @Test + public void testNullCasesCreateEncryptionContextTableNameOverrideFromMap_nullEncryptionContextTableName() { + Map tableNameOverrides = new HashMap<>(); + tableNameOverrides.put("DifferentTableName", "MyNewTableName"); + assertEncryptionContextUnchangedFromMap(new EncryptionContext.Builder().build(), + tableNameOverrides); + } + + @Test + public void testNullCasesCreateEncryptionContextTableNameOverrideFromMap_nullEncryptionContext() { + Map tableNameOverrides = new HashMap<>(); + tableNameOverrides.put("DifferentTableName", "MyNewTableName"); + assertEncryptionContextUnchangedFromMap(null, + tableNameOverrides); + } + + + @Test + public void testNullCasesCreateEncryptionContextTableNameOverrideFromMap_nullOriginalTableName() { + Map tableNameOverrides = new HashMap<>(); + tableNameOverrides.put(null, "MyNewTableName"); + assertEncryptionContextUnchangedFromMap(new EncryptionContext.Builder().withTableName("example").build(), + tableNameOverrides); + } + + @Test + public void testNullCasesCreateEncryptionContextTableNameOverrideFromMap_nullNewTableName() { + Map tableNameOverrides = new HashMap<>(); + tableNameOverrides.put("MyOriginalTableName", null); + assertEncryptionContextUnchangedFromMap(new EncryptionContext.Builder().withTableName("MyOriginalTableName").build(), + tableNameOverrides); + } + + + @Test + public void testNullCasesCreateEncryptionContextTableNameOverrideFromMap_nullMap() { + assertEncryptionContextUnchangedFromMap(new EncryptionContext.Builder().withTableName("MyOriginalTableName").build(), + null); + } + + + private void assertEncryptionContextUnchanged(EncryptionContext encryptionContext, String originalTableName, String newTableName) { + UnaryOperator encryptionContextTableNameOverride = overrideEncryptionContextTableName(originalTableName, newTableName); + EncryptionContext newEncryptionContext = encryptionContextTableNameOverride.apply(encryptionContext); + assertEquals(encryptionContext, newEncryptionContext); + } + + + private void assertEncryptionContextUnchangedFromMap(EncryptionContext encryptionContext, Map overrideMap) { + UnaryOperator encryptionContextTableNameOverrideFromMap = overrideEncryptionContextTableNameUsingMap(overrideMap); + EncryptionContext newEncryptionContext = encryptionContextTableNameOverrideFromMap.apply(encryptionContext); + assertEquals(encryptionContext, newEncryptionContext); + } + + +} \ No newline at end of file From 7ab7fd9ae6da185ec5fda0c3164f617b524ac77d Mon Sep 17 00:00:00 2001 From: John Walker Date: Tue, 27 Nov 2018 15:05:55 -0800 Subject: [PATCH 2/3] Rework EncryptionContextOverride example to cleanup properly shutdown must be called on ddb and kms to threads to exit when using mvn exec:java to run commands, otherwise maven will pause for a configurable (by default 15s) timeout to join daemon threads. --- ...ionContextOverridesWithDynamoDBMapper.java | 68 +++++++++++++++---- 1 file changed, 54 insertions(+), 14 deletions(-) diff --git a/examples/com/amazonaws/examples/EncryptionContextOverridesWithDynamoDBMapper.java b/examples/com/amazonaws/examples/EncryptionContextOverridesWithDynamoDBMapper.java index b7943976..e25989a2 100644 --- a/examples/com/amazonaws/examples/EncryptionContextOverridesWithDynamoDBMapper.java +++ b/examples/com/amazonaws/examples/EncryptionContextOverridesWithDynamoDBMapper.java @@ -10,14 +10,18 @@ import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBRangeKey; import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBTable; import com.amazonaws.services.dynamodbv2.datamodeling.encryption.DynamoDBEncryptor; +import com.amazonaws.services.dynamodbv2.datamodeling.encryption.EncryptionContext; +import com.amazonaws.services.dynamodbv2.datamodeling.encryption.EncryptionFlags; import com.amazonaws.services.dynamodbv2.datamodeling.encryption.providers.DirectKmsMaterialProvider; import com.amazonaws.services.dynamodbv2.model.AttributeValue; import com.amazonaws.services.kms.AWSKMS; import com.amazonaws.services.kms.AWSKMSClientBuilder; import java.security.GeneralSecurityException; +import java.util.EnumSet; import java.util.HashMap; import java.util.Map; +import java.util.Set; import static com.amazonaws.services.dynamodbv2.datamodeling.encryption.utils.EncryptionContextOperators.overrideEncryptionContextTableNameUsingMap; @@ -27,12 +31,26 @@ public static void main(String[] args) throws GeneralSecurityException { final String region = args[1]; final String encryptionContextTableName = args[2]; - encryptRecord(cmkArn, region, encryptionContextTableName); + AmazonDynamoDB ddb = null; + AWSKMS kms = null; + try { + ddb = AmazonDynamoDBClientBuilder.standard().withRegion(region).build(); + kms = AWSKMSClientBuilder.standard().withRegion(region).build(); + encryptRecord(cmkArn, encryptionContextTableName, ddb, kms); + } finally { + if (ddb != null) { + ddb.shutdown(); + } + if (kms != null) { + kms.shutdown(); + } + } } public static void encryptRecord(final String cmkArn, - final String region, - final String newEncryptionContextTableName) { + final String newEncryptionContextTableName, + AmazonDynamoDB ddb, + AWSKMS kms) throws GeneralSecurityException { // Sample object to be encrypted ExampleItem record = new ExampleItem(); record.setPartitionAttribute("is this"); @@ -40,17 +58,14 @@ public static void encryptRecord(final String cmkArn, record.setExample("my data"); // Set up our configuration and clients - final AmazonDynamoDB ddb = AmazonDynamoDBClientBuilder.standard().withRegion(region).build(); - final AWSKMS kms = AWSKMSClientBuilder.standard().withRegion(region).build(); final DirectKmsMaterialProvider cmp = new DirectKmsMaterialProvider(kms, cmkArn); - // Encryptor creation final DynamoDBEncryptor encryptor = DynamoDBEncryptor.getInstance(cmp); Map tableNameEncryptionContextOverrides = new HashMap<>(); tableNameEncryptionContextOverrides.put("ExampleTableForEncryptionContextOverrides", newEncryptionContextTableName); tableNameEncryptionContextOverrides.put("AnotherExampleTableForEncryptionContextOverrides", "this table doesn't exist"); - // Here we supply an operator to override the table name used in the encryption context + // Supply an operator to override the table name used in the encryption context encryptor.setEncryptionContextOverrideOperator( overrideEncryptionContextTableNameUsingMap(tableNameEncryptionContextOverrides) ); @@ -62,20 +77,40 @@ public static void encryptRecord(final String cmkArn, .withSaveBehavior(DynamoDBMapperConfig.SaveBehavior.CLOBBER).build(); DynamoDBMapper mapper = new DynamoDBMapper(ddb, mapperConfig, new AttributeEncryptor(encryptor)); - System.out.println("Plaintext Record: " + record); + System.out.println("Plaintext Record: " + record.toString()); // Save the record to the DynamoDB table mapper.save(record); - // Retrieve the encrypted record (directly without decrypting) from Dynamo so we can see it in our example + // Retrieve (and decrypt) it from DynamoDB + ExampleItem decrypted_record = mapper.load(ExampleItem.class, "is this", 55); + System.out.println("Decrypted Record: " + decrypted_record.toString()); + + // Setup new configuration to decrypt without using an overridden EncryptionContext final Map itemKey = new HashMap<>(); itemKey.put("partition_attribute", new AttributeValue().withS("is this")); itemKey.put("sort_attribute", new AttributeValue().withN("55")); - System.out.println("Encrypted Record: " + ddb.getItem("ExampleTableForEncryptionContextOverrides", - itemKey).getItem()); - // Retrieve (and decrypt) it from DynamoDB - ExampleItem decrypted_record = mapper.load(ExampleItem.class, "is this", 55); - System.out.println("Decrypted Record: " + decrypted_record); + final EnumSet signOnly = EnumSet.of(EncryptionFlags.SIGN); + final EnumSet encryptAndSign = EnumSet.of(EncryptionFlags.ENCRYPT, EncryptionFlags.SIGN); + final Map encryptedItem = ddb.getItem("ExampleTableForEncryptionContextOverrides", itemKey) + .getItem(); + System.out.println("Encrypted Record: " + encryptedItem); + + Map> encryptionFlags = new HashMap<>(); + encryptionFlags.put("partition_attribute", signOnly); + encryptionFlags.put("sort_attribute", signOnly); + encryptionFlags.put("example", encryptAndSign); + + final DynamoDBEncryptor encryptorWithoutOverrides = DynamoDBEncryptor.getInstance(cmp); + + // Decrypt the record without using an overridden EncryptionContext + encryptorWithoutOverrides.decryptRecord(encryptedItem, + encryptionFlags, + new EncryptionContext.Builder().withHashKeyName("partition_attribute") + .withRangeKeyName("sort_attribute") + .withTableName(newEncryptionContextTableName) + .build()); + System.out.printf("The example item was encrypted using the table name '%s' in the EncryptionContext%n", newEncryptionContextTableName); } @DynamoDBTable(tableName = "ExampleTableForEncryptionContextOverrides") @@ -110,6 +145,11 @@ public String getExample() { public void setExample(String example) { this.example = example; } + + public String toString() { + return String.format("{partition_attribute: %s, sort_attribute: %s, example: %s}", + partitionAttribute, sortAttribute, example); + } } } From 9cedf08ae4a66f85757bb2964f2690fbf10e4b30 Mon Sep 17 00:00:00 2001 From: John Walker Date: Thu, 29 Nov 2018 17:59:40 -0800 Subject: [PATCH 3/3] Update AWS SDK and remaining examples to use PUT instead of CLOBBER Update the AWS SDK to latest in order to get access to SaveBehavior.PUT for examples. Update tests to use the more general Function instead of UnaryOperator. Make it so EncryptionContextOperators can't be instantiated by adding a private constructor. --- .../examples/AwsKmsEncryptedObject.java | 4 ++-- ...yptionContextOverridesWithDynamoDBMapper.java | 4 ++-- pom.xml | 2 +- .../utils/EncryptionContextOperators.java | 8 +++++--- .../utils/EncryptionContextOperatorsTest.java | 16 +++++++--------- 5 files changed, 17 insertions(+), 17 deletions(-) diff --git a/examples/com/amazonaws/examples/AwsKmsEncryptedObject.java b/examples/com/amazonaws/examples/AwsKmsEncryptedObject.java index 041739a7..7dc1d5d5 100644 --- a/examples/com/amazonaws/examples/AwsKmsEncryptedObject.java +++ b/examples/com/amazonaws/examples/AwsKmsEncryptedObject.java @@ -52,9 +52,9 @@ public static void encryptRecord(final String cmkArn, final String region) { // Encryptor creation final DynamoDBEncryptor encryptor = DynamoDBEncryptor.getInstance(cmp); // Mapper Creation - // Please note the use of SaveBehavior.CLOBBER (SaveBehavior.PUT works as well). + // Please note the use of SaveBehavior.PUT (SaveBehavior.CLOBBER works as well). // Omitting this can result in data-corruption. - DynamoDBMapperConfig mapperConfig = DynamoDBMapperConfig.builder().withSaveBehavior(SaveBehavior.CLOBBER).build(); + DynamoDBMapperConfig mapperConfig = DynamoDBMapperConfig.builder().withSaveBehavior(SaveBehavior.PUT).build(); DynamoDBMapper mapper = new DynamoDBMapper(ddb, mapperConfig, new AttributeEncryptor(encryptor)); System.out.println("Plaintext Record: " + record); diff --git a/examples/com/amazonaws/examples/EncryptionContextOverridesWithDynamoDBMapper.java b/examples/com/amazonaws/examples/EncryptionContextOverridesWithDynamoDBMapper.java index e25989a2..bf097656 100644 --- a/examples/com/amazonaws/examples/EncryptionContextOverridesWithDynamoDBMapper.java +++ b/examples/com/amazonaws/examples/EncryptionContextOverridesWithDynamoDBMapper.java @@ -71,10 +71,10 @@ public static void encryptRecord(final String cmkArn, ); // Mapper Creation - // Please note the use of SaveBehavior.CLOBBER (SaveBehavior.PUT works as well). + // Please note the use of SaveBehavior.PUT (SaveBehavior.CLOBBER works as well). // Omitting this can result in data-corruption. DynamoDBMapperConfig mapperConfig = DynamoDBMapperConfig.builder() - .withSaveBehavior(DynamoDBMapperConfig.SaveBehavior.CLOBBER).build(); + .withSaveBehavior(DynamoDBMapperConfig.SaveBehavior.PUT).build(); DynamoDBMapper mapper = new DynamoDBMapper(ddb, mapperConfig, new AttributeEncryptor(encryptor)); System.out.println("Plaintext Record: " + record.toString()); diff --git a/pom.xml b/pom.xml index 37db5b52..34947992 100644 --- a/pom.xml +++ b/pom.xml @@ -43,7 +43,7 @@ com.amazonaws aws-java-sdk-bom - 1.11.434 + 1.11.460 pom import diff --git a/src/main/java/com/amazonaws/services/dynamodbv2/datamodeling/encryption/utils/EncryptionContextOperators.java b/src/main/java/com/amazonaws/services/dynamodbv2/datamodeling/encryption/utils/EncryptionContextOperators.java index 66831035..d61c0d0f 100644 --- a/src/main/java/com/amazonaws/services/dynamodbv2/datamodeling/encryption/utils/EncryptionContextOperators.java +++ b/src/main/java/com/amazonaws/services/dynamodbv2/datamodeling/encryption/utils/EncryptionContextOperators.java @@ -9,6 +9,11 @@ * Implementations of common operators for overriding the EncryptionContext */ public class EncryptionContextOperators { + + // Prevent instantiation + private EncryptionContextOperators() { + } + /** * An operator for overriding EncryptionContext's table name for a specific DynamoDBEncryptor. If any table names or * the encryption context itself is null, then it returns the original EncryptionContext. @@ -35,7 +40,6 @@ public static UnaryOperator overrideEncryptionContextTableNam }; } - /** * An operator for mapping multiple table names in the Encryption Context to a new table name. If the table name for * a given EncryptionContext is missing, then it returns the original EncryptionContext. Similarly, it returns the @@ -60,6 +64,4 @@ public static UnaryOperator overrideEncryptionContextTableNam } }; } - - } diff --git a/src/test/java/com/amazonaws/services/dynamodbv2/datamodeling/encryption/utils/EncryptionContextOperatorsTest.java b/src/test/java/com/amazonaws/services/dynamodbv2/datamodeling/encryption/utils/EncryptionContextOperatorsTest.java index 9749060a..1e4323bc 100644 --- a/src/test/java/com/amazonaws/services/dynamodbv2/datamodeling/encryption/utils/EncryptionContextOperatorsTest.java +++ b/src/test/java/com/amazonaws/services/dynamodbv2/datamodeling/encryption/utils/EncryptionContextOperatorsTest.java @@ -5,7 +5,7 @@ import java.util.HashMap; import java.util.Map; -import java.util.function.UnaryOperator; +import java.util.function.Function; import static com.amazonaws.services.dynamodbv2.datamodeling.encryption.utils.EncryptionContextOperators.overrideEncryptionContextTableName; import static com.amazonaws.services.dynamodbv2.datamodeling.encryption.utils.EncryptionContextOperators.overrideEncryptionContextTableNameUsingMap; @@ -15,7 +15,7 @@ public class EncryptionContextOperatorsTest { @Test public void testCreateEncryptionContextTableNameOverride_expectedOverride() { - UnaryOperator myNewTableName = overrideEncryptionContextTableName("OriginalTableName", "MyNewTableName"); + Function myNewTableName = overrideEncryptionContextTableName("OriginalTableName", "MyNewTableName"); EncryptionContext context = new EncryptionContext.Builder().withTableName("OriginalTableName").build(); @@ -56,7 +56,7 @@ public void testCreateEncryptionContextTableNameOverrideMap_expectedOverride() { tableNameOverrides.put("OriginalTableName", "MyNewTableName"); - UnaryOperator nameOverrideMap = + Function nameOverrideMap = overrideEncryptionContextTableNameUsingMap(tableNameOverrides); EncryptionContext context = new EncryptionContext.Builder().withTableName("OriginalTableName").build(); @@ -74,7 +74,7 @@ public void testCreateEncryptionContextTableNameOverrideMap_multipleOverrides() tableNameOverrides.put("OriginalTableName2", "MyNewTableName2"); - UnaryOperator overrideOperator = + Function overrideOperator = overrideEncryptionContextTableNameUsingMap(tableNameOverrides); EncryptionContext context = new EncryptionContext.Builder().withTableName("OriginalTableName1").build(); @@ -136,17 +136,15 @@ public void testNullCasesCreateEncryptionContextTableNameOverrideFromMap_nullMap private void assertEncryptionContextUnchanged(EncryptionContext encryptionContext, String originalTableName, String newTableName) { - UnaryOperator encryptionContextTableNameOverride = overrideEncryptionContextTableName(originalTableName, newTableName); + Function encryptionContextTableNameOverride = overrideEncryptionContextTableName(originalTableName, newTableName); EncryptionContext newEncryptionContext = encryptionContextTableNameOverride.apply(encryptionContext); assertEquals(encryptionContext, newEncryptionContext); } private void assertEncryptionContextUnchangedFromMap(EncryptionContext encryptionContext, Map overrideMap) { - UnaryOperator encryptionContextTableNameOverrideFromMap = overrideEncryptionContextTableNameUsingMap(overrideMap); + Function encryptionContextTableNameOverrideFromMap = overrideEncryptionContextTableNameUsingMap(overrideMap); EncryptionContext newEncryptionContext = encryptionContextTableNameOverrideFromMap.apply(encryptionContext); assertEquals(encryptionContext, newEncryptionContext); } - - -} \ No newline at end of file +}