diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0c28e211..dac50f7e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,21 @@
# Changelog
+## 1.15.0 -- 2021-02-04
+Adds the CachingMostRecentProvider and deprecates MostRecentProvider.
+
+Time-based key reauthorization logic in MostRecentProvider did not re-authorize the use of the key
+after key usage permissions were changed at the key provider
+(for example AWS Key Management Service).
+This created the potential for keys to be used in the DynamoDB Encryption Client after permissions
+to do so were revoked.
+
+CachingMostRecentProvider replaces MostRecentProvider and provides a cache entry TTL to reauthorize
+the key with the key provider.
+
+MostRecentProvider is now deprecated, and is removed in 2.0.0.
+See https://docs.aws.amazon.com/dynamodb-encryption-client/latest/devguide/most-recent-provider.html#mrp-versions for more details.
+
+1.15.0 also fixes interoperability issues between the Python and Java implementations of DynamoDB Encryption Client.
+
## 1.14.1 -- 2019-10-14
Fixes `com.amazonaws:aws-dynamodb-encryption-java` so that it may be consumed
in mavenCentral.
diff --git a/README.md b/README.md
index 3d8f478f..f7630405 100644
--- a/README.md
+++ b/README.md
@@ -119,7 +119,7 @@ You can download the [latest snapshot release][download] or pick it up from Mave
com.amazonawsaws-dynamodb-encryption-java
- 1.14.1
+ 1.15.0
```
@@ -177,4 +177,4 @@ For signing, the user specified signing key can be either symmetric or asymmetri
[materialprovider]: sdk1/src/main/java/com/amazonaws/services/dynamodbv2/datamodeling/encryption/providers/EncryptionMaterialsProvider.java
[privatekey]: http://docs.oracle.com/javase/7/docs/api/java/security/PrivateKey.html
[secretkey]: http://docs.oracle.com/javase/7/docs/api/javax/crypto/SecretKey.html
-[download]: https://github.com/aws/aws-dynamodb-encryption-java/releases/tag/1.13.0
+[download]: https://github.com/aws/aws-dynamodb-encryption-java/releases
diff --git a/common/pom.xml b/common/pom.xml
deleted file mode 100644
index c4e59171..00000000
--- a/common/pom.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
- 4.0.0
-
-
- software.amazon.cryptools
- dynamodbencryptionclient-pom
- 0.1.0-SNAPSHOT
-
-
- dynamodbencryptionclient-common
- jar
- 0.1.0-SNAPSHOT
- aws-dynamodb-encryption-java :: Common
- AWS DynamoDB Encryption Client Common Library
-
diff --git a/ddej-build-tools/pom.xml b/ddej-build-tools/pom.xml
deleted file mode 100644
index d4f82afb..00000000
--- a/ddej-build-tools/pom.xml
+++ /dev/null
@@ -1,39 +0,0 @@
-
-
- 4.0.0
- software.amazon.cryptools
- ddej-build-tools
- 0.1.0
- aws-dynamodb-encryption-java :: Build Tools
- Houses configuration for checkstyle
-
-
- UTF-8
-
-
-
-
-
- org.apache.maven.plugins
- maven-compiler-plugin
- 3.8.0
-
- 1.8
- 1.8
-
-
-
-
- org.apache.maven.plugins
- maven-deploy-plugin
- 3.0.0-M1
-
- true
-
-
-
-
-
diff --git a/examples/pom.xml b/examples/pom.xml
index 86c82683..695f9534 100644
--- a/examples/pom.xml
+++ b/examples/pom.xml
@@ -8,24 +8,152 @@
software.amazon.cryptoolsdynamodbencryptionclient-pom
- 0.1.0-SNAPSHOT
+ 1.15.0dynamodbencryptionclient-sdk1examplesjar
- 0.1.0-SNAPSHOT
+ 1.15.0aws-dynamodb-encryption-java :: SDK1 ExamplesExamples for AWS DynamoDB Encryption Client for AWS Java SDK v1
+
+ 1.0.392
+ 3.0.0-M3
+ 3.0.0-M3
+
+
com.amazonawsaws-dynamodb-encryption-java
- 1.13.0
+ 1.15.0
+
+
+
+ org.testng
+ testng
+ 6.10
+ test
+
+
+
+ com.amazonaws
+ DynamoDBLocal
+ 1.10.5.1
+ test
+
+
+
+ com.almworks.sqlite4java
+ sqlite4java
+ ${sqlite4java.version}
+ test
+
+
+
+ com.almworks.sqlite4java
+ libsqlite4java-osx
+ ${sqlite4java.version}
+ dylib
+ test
+
+
+
+ com.almworks.sqlite4java
+ sqlite4java-win32-x64
+ ${sqlite4java.version}
+ dll
+ test
+
+
+
+ com.almworks.sqlite4java
+ libsqlite4java-linux-amd64
+ so
+ ${sqlite4java.version}
+ test
+
+
+
+ dynamodb-local
+ DynamoDB Local Release Repository
+ https://s3-us-west-2.amazonaws.com/dynamodb-local/release
+
+
+
- .
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.8.0
+
+ 1.8
+ 1.8
+
+
+
+
+ org.apache.maven.plugins
+ maven-dependency-plugin
+ 3.1.1
+
+
+ copy
+ test-compile
+
+ copy-dependencies
+
+
+ test
+ so,dll,dylib
+ ${project.build.directory}/test-lib
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ ${maven-surefire-plugin.version}
+
+ false
+
+
+ sqlite4java.library.path
+ ${project.build.directory}/test-lib
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-failsafe-plugin
+ ${maven-failsafe-plugin.version}
+
+ false
+
+
+ sqlite4java.library.path
+ ${project.build.directory}/test-lib
+
+
+
+
+
+
+ integration-test
+ verify
+
+
+
+
+
diff --git a/examples/com/amazonaws/examples/AsymmetricEncryptedItem.java b/examples/src/main/java/com/amazonaws/examples/AsymmetricEncryptedItem.java
similarity index 73%
rename from examples/com/amazonaws/examples/AsymmetricEncryptedItem.java
rename to examples/src/main/java/com/amazonaws/examples/AsymmetricEncryptedItem.java
index 6de2ed8a..2bf245f9 100644
--- a/examples/com/amazonaws/examples/AsymmetricEncryptedItem.java
+++ b/examples/src/main/java/com/amazonaws/examples/AsymmetricEncryptedItem.java
@@ -34,7 +34,11 @@
* For ease of the example, we create new random ones every time.
*/
public class AsymmetricEncryptedItem {
-
+ private static final String STRING_FIELD_NAME = "example";
+ private static final String BINARY_FIELD_NAME = "and some binary";
+ private static final String NUMBER_FIELD_NAME = "some numbers";
+ private static final String IGNORED_FIELD_NAME = "leave me";
+
public static void main(String[] args) throws GeneralSecurityException {
final String tableName = args[0];
final KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
@@ -42,21 +46,21 @@ public static void main(String[] args) throws GeneralSecurityException {
// You should never use the same key for encryption and signing
final KeyPair wrappingKeys = keyGen.generateKeyPair();
final KeyPair signingKeys = keyGen.generateKeyPair();
-
+
encryptRecord(tableName, wrappingKeys, signingKeys);
}
- private static void encryptRecord(String tableName, KeyPair wrappingKeys, KeyPair signingKeys) throws GeneralSecurityException {
+ public static void encryptRecord(String tableName, KeyPair wrappingKeys, KeyPair signingKeys) throws GeneralSecurityException {
// Sample record to be encrypted
final String partitionKeyName = "partition_attribute";
final String sortKeyName = "sort_attribute";
final Map record = new HashMap<>();
record.put(partitionKeyName, new AttributeValue().withS("is this"));
record.put(sortKeyName, new AttributeValue().withN("55"));
- record.put("example", new AttributeValue().withS("data"));
- record.put("some numbers", new AttributeValue().withN("99"));
- record.put("and some binary", new AttributeValue().withB(ByteBuffer.wrap(new byte[]{0x00, 0x01, 0x02})));
- record.put("leave me", new AttributeValue().withS("alone")); // We want to ignore this attribute
+ record.put(STRING_FIELD_NAME, new AttributeValue().withS("data"));
+ record.put(NUMBER_FIELD_NAME, new AttributeValue().withN("99"));
+ record.put(BINARY_FIELD_NAME, new AttributeValue().withB(ByteBuffer.wrap(new byte[]{0x00, 0x01, 0x02})));
+ record.put(IGNORED_FIELD_NAME, new AttributeValue().withS("alone")); // We want to ignore this attribute
// Set up our configuration and clients. All of this is thread-safe and can be reused across calls.
// Provider Configuration
@@ -82,7 +86,7 @@ private static void encryptRecord(String tableName, KeyPair wrappingKeys, KeyPai
// Partition and sort keys must not be encrypted but should be signed
actions.put(attributeName, signOnly);
break;
- case "leave me":
+ case IGNORED_FIELD_NAME:
// For this example, we are neither signing nor encrypting this field
break;
default:
@@ -96,6 +100,12 @@ private static void encryptRecord(String tableName, KeyPair wrappingKeys, KeyPai
// Encrypt the plaintext record directly
final Map encrypted_record = encryptor.encryptRecord(record, actions, encryptionContext);
+ // Encrypted record fields change as expected
+ assert encrypted_record.get(STRING_FIELD_NAME).getB() != null; // the encrypted string is stored as bytes
+ assert encrypted_record.get(NUMBER_FIELD_NAME).getB() != null; // the encrypted number is stored as bytes
+ assert !record.get(BINARY_FIELD_NAME).getB().equals(encrypted_record.get(BINARY_FIELD_NAME).getB()); // the encrypted bytes have updated
+ assert record.get(IGNORED_FIELD_NAME).getS().equals(encrypted_record.get(IGNORED_FIELD_NAME).getS()); // ignored field is left as is
+
// We could now put the encrypted item to DynamoDB just as we would any other item.
// We're skipping it to to keep the example simpler.
@@ -105,5 +115,10 @@ private static void encryptRecord(String tableName, KeyPair wrappingKeys, KeyPai
// Decryption is identical. We'll pretend that we retrieved the record from DynamoDB.
final Map decrypted_record = encryptor.decryptRecord(encrypted_record, actions, encryptionContext);
System.out.println("Decrypted Record: " + decrypted_record);
- }
+
+ // The decrypted fields match the original fields before encryption
+ assert record.get(STRING_FIELD_NAME).getS().equals(decrypted_record.get(STRING_FIELD_NAME).getS());
+ assert record.get(NUMBER_FIELD_NAME).getN().equals(decrypted_record.get(NUMBER_FIELD_NAME).getN());
+ assert record.get(BINARY_FIELD_NAME).getB().equals(decrypted_record.get(BINARY_FIELD_NAME).getB());
+ }
}
diff --git a/examples/com/amazonaws/examples/AwsKmsEncryptedItem.java b/examples/src/main/java/com/amazonaws/examples/AwsKmsEncryptedItem.java
similarity index 67%
rename from examples/com/amazonaws/examples/AwsKmsEncryptedItem.java
rename to examples/src/main/java/com/amazonaws/examples/AwsKmsEncryptedItem.java
index 848c1363..7c9f8431 100644
--- a/examples/com/amazonaws/examples/AwsKmsEncryptedItem.java
+++ b/examples/src/main/java/com/amazonaws/examples/AwsKmsEncryptedItem.java
@@ -33,31 +33,43 @@
* Example showing use of AWS KMS CMP with record encryption functions directly.
*/
public class AwsKmsEncryptedItem {
-
+ private static final String STRING_FIELD_NAME = "example";
+ private static final String BINARY_FIELD_NAME = "and some binary";
+ private static final String NUMBER_FIELD_NAME = "some numbers";
+ private static final String IGNORED_FIELD_NAME = "leave me";
+
public static void main(String[] args) throws GeneralSecurityException {
final String tableName = args[0];
final String cmkArn = args[1];
final String region = args[2];
-
- encryptRecord(tableName, cmkArn, region);
+
+ AWSKMS kms = null;
+ try {
+ kms = AWSKMSClientBuilder.standard().withRegion(region).build();
+ encryptRecord(tableName, cmkArn, kms);
+ } finally {
+ if (kms != null) {
+ kms.shutdown();
+ }
+ }
}
- private static void encryptRecord(String tableName, String cmkArn, String region) throws GeneralSecurityException {
+ public static void encryptRecord(final String tableName, final String cmkArn, final AWSKMS kmsClient) throws GeneralSecurityException {
// Sample record to be encrypted
final String partitionKeyName = "partition_attribute";
final String sortKeyName = "sort_attribute";
final Map record = new HashMap<>();
record.put(partitionKeyName, new AttributeValue().withS("is this"));
record.put(sortKeyName, new AttributeValue().withN("55"));
- record.put("example", new AttributeValue().withS("data"));
- record.put("some numbers", new AttributeValue().withN("99"));
- record.put("and some binary", new AttributeValue().withB(ByteBuffer.wrap(new byte[]{0x00, 0x01, 0x02})));
- record.put("leave me", new AttributeValue().withS("alone")); // We want to ignore this attribute
+ record.put(STRING_FIELD_NAME, new AttributeValue().withS("data"));
+ record.put(NUMBER_FIELD_NAME, new AttributeValue().withN("99"));
+ record.put(BINARY_FIELD_NAME, new AttributeValue().withB(ByteBuffer.wrap(new byte[]{0x00, 0x01, 0x02})));
+ record.put(IGNORED_FIELD_NAME, new AttributeValue().withS("alone")); // We want to ignore this attribute
// Set up our configuration and clients. All of this is thread-safe and can be reused across calls.
+ // This example assumes we already have a AWS KMS client `kmsClient`
// Provider Configuration
- final AWSKMS kms = AWSKMSClientBuilder.standard().withRegion(region).build();
- final DirectKmsMaterialProvider cmp = new DirectKmsMaterialProvider(kms, cmkArn);
+ final DirectKmsMaterialProvider cmp = new DirectKmsMaterialProvider(kmsClient, cmkArn);
// Encryptor creation
final DynamoDBEncryptor encryptor = DynamoDBEncryptor.getInstance(cmp);
@@ -79,7 +91,7 @@ private static void encryptRecord(String tableName, String cmkArn, String region
// Partition and sort keys must not be encrypted but should be signed
actions.put(attributeName, signOnly);
break;
- case "leave me":
+ case IGNORED_FIELD_NAME:
// For this example, we are neither signing nor encrypting this field
break;
default:
@@ -93,6 +105,12 @@ private static void encryptRecord(String tableName, String cmkArn, String region
// Encrypt the plaintext record directly
final Map encrypted_record = encryptor.encryptRecord(record, actions, encryptionContext);
+ // Encrypted record fields change as expected
+ assert encrypted_record.get(STRING_FIELD_NAME).getB() != null; // the encrypted string is stored as bytes
+ assert encrypted_record.get(NUMBER_FIELD_NAME).getB() != null; // the encrypted number is stored as bytes
+ assert !record.get(BINARY_FIELD_NAME).getB().equals(encrypted_record.get(BINARY_FIELD_NAME).getB()); // the encrypted bytes have updated
+ assert record.get(IGNORED_FIELD_NAME).getS().equals(encrypted_record.get(IGNORED_FIELD_NAME).getS()); // ignored field is left as is
+
// We could now put the encrypted item to DynamoDB just as we would any other item.
// We're skipping it to to keep the example simpler.
@@ -102,5 +120,10 @@ private static void encryptRecord(String tableName, String cmkArn, String region
// Decryption is identical. We'll pretend that we retrieved the record from DynamoDB.
final Map decrypted_record = encryptor.decryptRecord(encrypted_record, actions, encryptionContext);
System.out.println("Decrypted Record: " + decrypted_record);
- }
+
+ // The decrypted fields match the original fields before encryption
+ assert record.get(STRING_FIELD_NAME).getS().equals(decrypted_record.get(STRING_FIELD_NAME).getS());
+ assert record.get(NUMBER_FIELD_NAME).getN().equals(decrypted_record.get(NUMBER_FIELD_NAME).getN());
+ assert record.get(BINARY_FIELD_NAME).getB().equals(decrypted_record.get(BINARY_FIELD_NAME).getB());
+ }
}
diff --git a/examples/com/amazonaws/examples/AwsKmsEncryptedObject.java b/examples/src/main/java/com/amazonaws/examples/AwsKmsEncryptedObject.java
similarity index 63%
rename from examples/com/amazonaws/examples/AwsKmsEncryptedObject.java
rename to examples/src/main/java/com/amazonaws/examples/AwsKmsEncryptedObject.java
index 29742730..b148fed5 100644
--- a/examples/com/amazonaws/examples/AwsKmsEncryptedObject.java
+++ b/examples/src/main/java/com/amazonaws/examples/AwsKmsEncryptedObject.java
@@ -14,6 +14,7 @@
*/
package com.amazonaws.examples;
+import java.nio.ByteBuffer;
import java.security.GeneralSecurityException;
import java.util.Arrays;
import java.util.HashMap;
@@ -38,18 +39,42 @@
/**
* This demonstrates how to use the {@link DynamoDBMapper} with the {@link AttributeEncryptor}
- * to encrypt your data. Before you can use this you need to set up a table called "ExampleTable"
+ * to encrypt your data. Before you can use this you need to set up a DynamoDB table called "ExampleTable"
* to hold the encrypted data.
+ * "ExampleTable" should have a partition key named "partition_attribute" for Strings
+ * and a sort (range) key named "sort_attribute" for numbers.
*/
public class AwsKmsEncryptedObject {
+ public static final String EXAMPLE_TABLE_NAME = "ExampleTable";
+ public static final String PARTITION_ATTRIBUTE = "partition_attribute";
+ public static final String SORT_ATTRIBUTE = "sort_attribute";
+
+ private static final String STRING_FIELD_NAME = "example";
+ private static final String BINARY_FIELD_NAME = "and some binary";
+ private static final String NUMBER_FIELD_NAME = "some numbers";
+ private static final String IGNORED_FIELD_NAME = "leave me";
+
public static void main(String[] args) throws GeneralSecurityException {
final String cmkArn = args[0];
final String region = args[1];
-
- encryptRecord(cmkArn, region);
+
+ AmazonDynamoDB ddb = null;
+ AWSKMS kms = null;
+ try {
+ ddb = AmazonDynamoDBClientBuilder.standard().withRegion(region).build();
+ kms = AWSKMSClientBuilder.standard().withRegion(region).build();
+ encryptRecord(cmkArn, ddb, kms);
+ } finally {
+ if (ddb != null) {
+ ddb.shutdown();
+ }
+ if (kms != null) {
+ kms.shutdown();
+ }
+ }
}
- public static void encryptRecord(final String cmkArn, final String region) {
+ public static void encryptRecord(final String cmkArn, final AmazonDynamoDB ddbClient, final AWSKMS kmsClient) {
// Sample object to be encrypted
DataPoJo record = new DataPoJo();
record.setPartitionAttribute("is this");
@@ -60,16 +85,15 @@ public static void encryptRecord(final String cmkArn, final String region) {
record.setLeaveMe("alone");
// 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);
+ // This example assumes we already have a DynamoDB client `ddbClient` and AWS KMS client `kmsClient`
+ final DirectKmsMaterialProvider cmp = new DirectKmsMaterialProvider(kmsClient, cmkArn);
// Encryptor creation
final DynamoDBEncryptor encryptor = DynamoDBEncryptor.getInstance(cmp);
// Mapper Creation
// 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.PUT).build();
- DynamoDBMapper mapper = new DynamoDBMapper(ddb, mapperConfig, new AttributeEncryptor(encryptor));
+ DynamoDBMapper mapper = new DynamoDBMapper(ddbClient, mapperConfig, new AttributeEncryptor(encryptor));
System.out.println("Plaintext Record: " + record);
// Save the item to the DynamoDB table
@@ -77,16 +101,28 @@ public static void encryptRecord(final String cmkArn, final String region) {
// Retrieve the encrypted item (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("ExampleTable", itemKey).getItem());
-
+ itemKey.put(PARTITION_ATTRIBUTE, new AttributeValue().withS("is this"));
+ itemKey.put(SORT_ATTRIBUTE, new AttributeValue().withN("55"));
+ final Map encrypted_record = ddbClient.getItem(EXAMPLE_TABLE_NAME, itemKey).getItem();
+ System.out.println("Encrypted Record: " + encrypted_record);
+
+ // Encrypted record fields change as expected
+ assert encrypted_record.get(STRING_FIELD_NAME).getB() != null; // the encrypted string is stored as bytes
+ assert encrypted_record.get(NUMBER_FIELD_NAME).getB() != null; // the encrypted number is stored as bytes
+ assert !ByteBuffer.wrap(record.getSomeBinary()).equals(encrypted_record.get(BINARY_FIELD_NAME).getB()); // the encrypted bytes have updated
+ assert record.getLeaveMe().equals(encrypted_record.get(IGNORED_FIELD_NAME).getS()); // ignored field is left as is
+
// Retrieve (and decrypt) it from DynamoDB
DataPoJo decrypted_record = mapper.load(DataPoJo.class, "is this", 55);
System.out.println("Decrypted Record: " + decrypted_record);
+
+ // The decrypted fields match the original fields before encryption
+ assert record.getExample().equals(decrypted_record.getExample());
+ assert record.getSomeNumbers() == decrypted_record.getSomeNumbers();
+ assert Arrays.equals(record.getSomeBinary(), decrypted_record.getSomeBinary());
}
- @DynamoDBTable(tableName = "ExampleTable")
+ @DynamoDBTable(tableName = EXAMPLE_TABLE_NAME)
public static final class DataPoJo {
private String partitionAttribute;
private int sortAttribute;
@@ -95,7 +131,7 @@ public static final class DataPoJo {
private byte[] someBinary;
private String leaveMe;
- @DynamoDBHashKey(attributeName = "partition_attribute")
+ @DynamoDBHashKey(attributeName = PARTITION_ATTRIBUTE)
public String getPartitionAttribute() {
return partitionAttribute;
}
@@ -104,7 +140,7 @@ public void setPartitionAttribute(String partitionAttribute) {
this.partitionAttribute = partitionAttribute;
}
- @DynamoDBRangeKey(attributeName = "sort_attribute")
+ @DynamoDBRangeKey(attributeName = SORT_ATTRIBUTE)
public int getSortAttribute() {
return sortAttribute;
}
@@ -113,7 +149,7 @@ public void setSortAttribute(int sortAttribute) {
this.sortAttribute = sortAttribute;
}
- @DynamoDBAttribute(attributeName = "example")
+ @DynamoDBAttribute(attributeName = STRING_FIELD_NAME)
public String getExample() {
return example;
}
@@ -122,7 +158,7 @@ public void setExample(String example) {
this.example = example;
}
- @DynamoDBAttribute(attributeName = "some numbers")
+ @DynamoDBAttribute(attributeName = NUMBER_FIELD_NAME)
public long getSomeNumbers() {
return someNumbers;
}
@@ -131,7 +167,7 @@ public void setSomeNumbers(long someNumbers) {
this.someNumbers = someNumbers;
}
- @DynamoDBAttribute(attributeName = "and some binary")
+ @DynamoDBAttribute(attributeName = BINARY_FIELD_NAME)
public byte[] getSomeBinary() {
return someBinary;
}
@@ -140,7 +176,7 @@ public void setSomeBinary(byte[] someBinary) {
this.someBinary = someBinary;
}
- @DynamoDBAttribute(attributeName = "leave me")
+ @DynamoDBAttribute(attributeName = IGNORED_FIELD_NAME)
@DoNotTouch
public String getLeaveMe() {
return leaveMe;
diff --git a/examples/com/amazonaws/examples/EncryptionContextOverridesWithDynamoDBMapper.java b/examples/src/main/java/com/amazonaws/examples/EncryptionContextOverridesWithDynamoDBMapper.java
similarity index 71%
rename from examples/com/amazonaws/examples/EncryptionContextOverridesWithDynamoDBMapper.java
rename to examples/src/main/java/com/amazonaws/examples/EncryptionContextOverridesWithDynamoDBMapper.java
index a665b997..3ea7c218 100644
--- a/examples/com/amazonaws/examples/EncryptionContextOverridesWithDynamoDBMapper.java
+++ b/examples/src/main/java/com/amazonaws/examples/EncryptionContextOverridesWithDynamoDBMapper.java
@@ -39,7 +39,23 @@
import static com.amazonaws.services.dynamodbv2.datamodeling.encryption.utils.EncryptionContextOperators.overrideEncryptionContextTableNameUsingMap;
+/**
+ * This demonstrates how to use an operator to override the table name used in the encryption context.
+ * Before you can use this you need to set up a DynamoDB table called "ExampleTableForEncryptionContextOverrides"
+ * to hold the encrypted data.
+ * "ExampleTableForEncryptionContextOverrides" should have a partition key named "partition_attribute" for Strings
+ * and a sort (range) key named "sort_attribute" for numbers.
+ */
public class EncryptionContextOverridesWithDynamoDBMapper {
+ public static final String TABLE_NAME_TO_OVERRIDE = "ExampleTableForEncryptionContextOverrides";
+ public static final String PARTITION_ATTRIBUTE = "partition_attribute";
+ public static final String SORT_ATTRIBUTE = "sort_attribute";
+
+ private static final String STRING_FIELD_NAME = "example";
+ private static final String BINARY_FIELD_NAME = "and some binary";
+ private static final String NUMBER_FIELD_NAME = "some numbers";
+ private static final String IGNORED_FIELD_NAME = "leave me";
+
public static void main(String[] args) throws GeneralSecurityException {
final String cmkArn = args[0];
final String region = args[1];
@@ -63,8 +79,8 @@ public static void main(String[] args) throws GeneralSecurityException {
public static void encryptRecord(final String cmkArn,
final String newEncryptionContextTableName,
- AmazonDynamoDB ddb,
- AWSKMS kms) throws GeneralSecurityException {
+ AmazonDynamoDB ddbClient,
+ AWSKMS kmsClient) throws GeneralSecurityException {
// Sample object to be encrypted
ExampleItem record = new ExampleItem();
record.setPartitionAttribute("is this");
@@ -72,11 +88,12 @@ public static void encryptRecord(final String cmkArn,
record.setExample("my data");
// Set up our configuration and clients
- final DirectKmsMaterialProvider cmp = new DirectKmsMaterialProvider(kms, cmkArn);
+ // This example assumes we already have a DynamoDB client `ddbClient` and AWS KMS client `kmsClient`
+ final DirectKmsMaterialProvider cmp = new DirectKmsMaterialProvider(kmsClient, cmkArn);
final DynamoDBEncryptor encryptor = DynamoDBEncryptor.getInstance(cmp);
Map tableNameEncryptionContextOverrides = new HashMap<>();
- tableNameEncryptionContextOverrides.put("ExampleTableForEncryptionContextOverrides", newEncryptionContextTableName);
+ tableNameEncryptionContextOverrides.put(TABLE_NAME_TO_OVERRIDE, newEncryptionContextTableName);
tableNameEncryptionContextOverrides.put("AnotherExampleTableForEncryptionContextOverrides", "this table doesn't exist");
// Supply an operator to override the table name used in the encryption context
@@ -89,7 +106,7 @@ public static void encryptRecord(final String cmkArn,
// Omitting this can result in data-corruption.
DynamoDBMapperConfig mapperConfig = DynamoDBMapperConfig.builder()
.withSaveBehavior(DynamoDBMapperConfig.SaveBehavior.PUT).build();
- DynamoDBMapper mapper = new DynamoDBMapper(ddb, mapperConfig, new AttributeEncryptor(encryptor));
+ DynamoDBMapper mapper = new DynamoDBMapper(ddbClient, mapperConfig, new AttributeEncryptor(encryptor));
System.out.println("Plaintext Record: " + record.toString());
// Save the record to the DynamoDB table
@@ -99,41 +116,47 @@ public static void encryptRecord(final String cmkArn,
ExampleItem decrypted_record = mapper.load(ExampleItem.class, "is this", 55);
System.out.println("Decrypted Record: " + decrypted_record.toString());
+ // The decrypted field matches the original field before encryption
+ assert record.getExample().equals(decrypted_record.getExample());
+
// 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"));
+ itemKey.put(PARTITION_ATTRIBUTE, new AttributeValue().withS("is this"));
+ itemKey.put(SORT_ATTRIBUTE, new AttributeValue().withN("55"));
final EnumSet signOnly = EnumSet.of(EncryptionFlags.SIGN);
final EnumSet encryptAndSign = EnumSet.of(EncryptionFlags.ENCRYPT, EncryptionFlags.SIGN);
- final Map encryptedItem = ddb.getItem("ExampleTableForEncryptionContextOverrides", itemKey)
+ final Map encryptedItem = ddbClient.getItem(TABLE_NAME_TO_OVERRIDE, 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);
+ encryptionFlags.put(PARTITION_ATTRIBUTE, signOnly);
+ encryptionFlags.put(SORT_ATTRIBUTE, signOnly);
+ encryptionFlags.put(STRING_FIELD_NAME, encryptAndSign);
final DynamoDBEncryptor encryptorWithoutOverrides = DynamoDBEncryptor.getInstance(cmp);
// Decrypt the record without using an overridden EncryptionContext
- encryptorWithoutOverrides.decryptRecord(encryptedItem,
+ Map decrypted_without_override_record = encryptorWithoutOverrides.decryptRecord(encryptedItem,
encryptionFlags,
- new EncryptionContext.Builder().withHashKeyName("partition_attribute")
- .withRangeKeyName("sort_attribute")
+ 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);
+
+ // The decrypted field matches the original field before encryption
+ assert record.getExample().equals(decrypted_without_override_record.get(STRING_FIELD_NAME).getS());
}
- @DynamoDBTable(tableName = "ExampleTableForEncryptionContextOverrides")
+ @DynamoDBTable(tableName = TABLE_NAME_TO_OVERRIDE)
public static final class ExampleItem {
private String partitionAttribute;
private int sortAttribute;
private String example;
- @DynamoDBHashKey(attributeName = "partition_attribute")
+ @DynamoDBHashKey(attributeName = PARTITION_ATTRIBUTE)
public String getPartitionAttribute() {
return partitionAttribute;
}
@@ -142,7 +165,7 @@ public void setPartitionAttribute(String partitionAttribute) {
this.partitionAttribute = partitionAttribute;
}
- @DynamoDBRangeKey(attributeName = "sort_attribute")
+ @DynamoDBRangeKey(attributeName = SORT_ATTRIBUTE)
public int getSortAttribute() {
return sortAttribute;
}
@@ -151,7 +174,7 @@ public void setSortAttribute(int sortAttribute) {
this.sortAttribute = sortAttribute;
}
- @DynamoDBAttribute(attributeName = "example")
+ @DynamoDBAttribute(attributeName = STRING_FIELD_NAME)
public String getExample() {
return example;
}
diff --git a/examples/com/amazonaws/examples/MostRecentEncryptedItem.java b/examples/src/main/java/com/amazonaws/examples/MostRecentEncryptedItem.java
similarity index 59%
rename from examples/com/amazonaws/examples/MostRecentEncryptedItem.java
rename to examples/src/main/java/com/amazonaws/examples/MostRecentEncryptedItem.java
index 232cac73..5f6e870e 100644
--- a/examples/com/amazonaws/examples/MostRecentEncryptedItem.java
+++ b/examples/src/main/java/com/amazonaws/examples/MostRecentEncryptedItem.java
@@ -26,8 +26,8 @@
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.CachingMostRecentProvider;
import com.amazonaws.services.dynamodbv2.datamodeling.encryption.providers.DirectKmsMaterialProvider;
-import com.amazonaws.services.dynamodbv2.datamodeling.encryption.providers.MostRecentProvider;
import com.amazonaws.services.dynamodbv2.datamodeling.encryption.providers.store.MetaStore;
import com.amazonaws.services.dynamodbv2.model.AttributeValue;
import com.amazonaws.services.dynamodbv2.model.ProvisionedThroughput;
@@ -35,13 +35,21 @@
import com.amazonaws.services.kms.AWSKMSClientBuilder;
/**
- * This demonstrates how to use the {@link MostRecentProvider} backed by a
+ * This demonstrates how to use the {@link CachingMostRecentProvider} backed by a
* {@link MetaStore} and the {@link DirectKmsMaterialProvider} to encrypt
* your data. Before you can use this, you need to set up a table to hold the
- * intermediate keys.
+ * intermediate keys or use --setup mode to construct the table once
+ * and then re-run the example without the --setup mode
*/
public class MostRecentEncryptedItem {
-
+ public static final String PARTITION_ATTRIBUTE = "partition_attribute";
+ public static final String SORT_ATTRIBUTE = "sort_attribute";
+
+ private static final String STRING_FIELD_NAME = "example";
+ private static final String BINARY_FIELD_NAME = "and some binary";
+ private static final String NUMBER_FIELD_NAME = "some numbers";
+ private static final String IGNORED_FIELD_NAME = "leave me";
+
public static void main(String[] args) throws GeneralSecurityException {
final String mode = args[0];
final String region = args[1];
@@ -49,37 +57,48 @@ public static void main(String[] args) throws GeneralSecurityException {
final String keyTableName = args[3];
final String cmkArn = args[4];
final String materialName = args[5];
-
+
if (mode.equalsIgnoreCase("--setup")) {
AmazonDynamoDB ddb = AmazonDynamoDBClientBuilder.standard().withRegion(region).build();
MetaStore.createTable(ddb, keyTableName, new ProvisionedThroughput(1L, 1L));
return;
}
-
- encryptRecord(tableName, keyTableName, region, cmkArn, materialName);
+
+ AmazonDynamoDB ddb = null;
+ AWSKMS kms = null;
+ try {
+ ddb = AmazonDynamoDBClientBuilder.standard().withRegion(region).build();
+ kms = AWSKMSClientBuilder.standard().withRegion(region).build();
+ encryptRecord(tableName, keyTableName, cmkArn, materialName, ddb, kms);
+ } finally {
+ if (ddb != null) {
+ ddb.shutdown();
+ }
+ if (kms != null) {
+ kms.shutdown();
+ }
+ }
}
- private static void encryptRecord(String tableName, String keyTableName, String region, String cmkArn, String materialName) throws GeneralSecurityException {
+ public static void encryptRecord(String tableName, String keyTableName, String cmkArn, String materialName,
+ AmazonDynamoDB ddbClient, AWSKMS kmsClient) throws GeneralSecurityException {
// Sample record to be encrypted
- final String partitionKeyName = "partition_attribute";
- final String sortKeyName = "sort_attribute";
final Map record = new HashMap<>();
- record.put(partitionKeyName, new AttributeValue().withS("is this"));
- record.put(sortKeyName, new AttributeValue().withN("55"));
- record.put("example", new AttributeValue().withS("data"));
- record.put("some numbers", new AttributeValue().withN("99"));
- record.put("and some binary", new AttributeValue().withB(ByteBuffer.wrap(new byte[]{0x00, 0x01, 0x02})));
- record.put("leave me", new AttributeValue().withS("alone")); // We want to ignore this attribute
+ record.put(PARTITION_ATTRIBUTE, new AttributeValue().withS("is this"));
+ record.put(SORT_ATTRIBUTE, new AttributeValue().withN("55"));
+ record.put(STRING_FIELD_NAME, new AttributeValue().withS("data"));
+ record.put(NUMBER_FIELD_NAME, new AttributeValue().withN("99"));
+ record.put(BINARY_FIELD_NAME, new AttributeValue().withB(ByteBuffer.wrap(new byte[]{0x00, 0x01, 0x02})));
+ record.put(IGNORED_FIELD_NAME, new AttributeValue().withS("alone")); // We want to ignore this attribute
// Set up our configuration and clients. All of this is thread-safe and can be reused across calls.
// Provider Configuration to protect the data keys
- final AmazonDynamoDB ddb = AmazonDynamoDBClientBuilder.standard().withRegion(region).build();
- final AWSKMS kms = AWSKMSClientBuilder.standard().withRegion(region).build();
- final DirectKmsMaterialProvider kmsProv = new DirectKmsMaterialProvider(kms, cmkArn);
+ // This example assumes we already have a DynamoDB client `ddbClient` and AWS KMS client `kmsClient`
+ final DirectKmsMaterialProvider kmsProv = new DirectKmsMaterialProvider(kmsClient, cmkArn);
final DynamoDBEncryptor keyEncryptor = DynamoDBEncryptor.getInstance(kmsProv);
- final MetaStore metaStore = new MetaStore(ddb, keyTableName, keyEncryptor);
+ final MetaStore metaStore = new MetaStore(ddbClient, keyTableName, keyEncryptor);
//Provider configuration to protect the data
- final MostRecentProvider cmp = new MostRecentProvider(metaStore, materialName, 60_000);
+ final CachingMostRecentProvider cmp = new CachingMostRecentProvider(metaStore, materialName, 60_000);
// Encryptor creation
final DynamoDBEncryptor encryptor = DynamoDBEncryptor.getInstance(cmp);
@@ -87,8 +106,8 @@ private static void encryptRecord(String tableName, String keyTableName, String
// Information about the context of our data (normally just Table information)
final EncryptionContext encryptionContext = new EncryptionContext.Builder()
.withTableName(tableName)
- .withHashKeyName(partitionKeyName)
- .withRangeKeyName(sortKeyName)
+ .withHashKeyName(PARTITION_ATTRIBUTE)
+ .withRangeKeyName(SORT_ATTRIBUTE)
.build();
// Describe what actions need to be taken for each attribute
@@ -97,12 +116,12 @@ private static void encryptRecord(String tableName, String keyTableName, String
final Map> actions = new HashMap<>();
for (final String attributeName : record.keySet()) {
switch (attributeName) {
- case partitionKeyName: // fall through
- case sortKeyName:
+ case PARTITION_ATTRIBUTE: // fall through
+ case SORT_ATTRIBUTE:
// Partition and sort keys must not be encrypted but should be signed
actions.put(attributeName, signOnly);
break;
- case "leave me":
+ case IGNORED_FIELD_NAME:
// For this example, we are neither signing nor encrypting this field
break;
default:
@@ -116,6 +135,12 @@ private static void encryptRecord(String tableName, String keyTableName, String
// Encrypt the plaintext record directly
final Map encrypted_record = encryptor.encryptRecord(record, actions, encryptionContext);
+ // Encrypted record fields change as expected
+ assert encrypted_record.get(STRING_FIELD_NAME).getB() != null; // the encrypted string is stored as bytes
+ assert encrypted_record.get(NUMBER_FIELD_NAME).getB() != null; // the encrypted number is stored as bytes
+ assert !record.get(BINARY_FIELD_NAME).getB().equals(encrypted_record.get(BINARY_FIELD_NAME).getB()); // the encrypted bytes have updated
+ assert record.get(IGNORED_FIELD_NAME).getS().equals(encrypted_record.get(IGNORED_FIELD_NAME).getS()); // ignored field is left as is
+
// We could now put the encrypted item to DynamoDB just as we would any other item.
// We're skipping it to to keep the example simpler.
@@ -125,5 +150,10 @@ private static void encryptRecord(String tableName, String keyTableName, String
// Decryption is identical. We'll pretend that we retrieved the record from DynamoDB.
final Map decrypted_record = encryptor.decryptRecord(encrypted_record, actions, encryptionContext);
System.out.println("Decrypted Record: " + decrypted_record);
- }
+
+ // The decrypted fields match the original fields before encryption
+ assert record.get(STRING_FIELD_NAME).getS().equals(decrypted_record.get(STRING_FIELD_NAME).getS());
+ assert record.get(NUMBER_FIELD_NAME).getN().equals(decrypted_record.get(NUMBER_FIELD_NAME).getN());
+ assert record.get(BINARY_FIELD_NAME).getB().equals(decrypted_record.get(BINARY_FIELD_NAME).getB());
+ }
}
diff --git a/examples/com/amazonaws/examples/SymmetricEncryptedItem.java b/examples/src/main/java/com/amazonaws/examples/SymmetricEncryptedItem.java
similarity index 75%
rename from examples/com/amazonaws/examples/SymmetricEncryptedItem.java
rename to examples/src/main/java/com/amazonaws/examples/SymmetricEncryptedItem.java
index e54c4e75..e5f54645 100644
--- a/examples/com/amazonaws/examples/SymmetricEncryptedItem.java
+++ b/examples/src/main/java/com/amazonaws/examples/SymmetricEncryptedItem.java
@@ -36,7 +36,12 @@
* For ease of the example, we create new random ones every time.
*/
public class SymmetricEncryptedItem {
-
+
+ private static final String STRING_FIELD_NAME = "example";
+ private static final String BINARY_FIELD_NAME = "and some binary";
+ private static final String NUMBER_FIELD_NAME = "some numbers";
+ private static final String IGNORED_FIELD_NAME = "leave me";
+
public static void main(String[] args) throws GeneralSecurityException {
final String tableName = args[0];
// Both AES and HMAC keys are just random bytes.
@@ -48,21 +53,21 @@ public static void main(String[] args) throws GeneralSecurityException {
secureRandom.nextBytes(rawHmac);
final SecretKey wrappingKey = new SecretKeySpec(rawAes, "AES");
final SecretKey signingKey = new SecretKeySpec(rawHmac, "HmacSHA256");
-
+
encryptRecord(tableName, wrappingKey, signingKey);
}
- private static void encryptRecord(String tableName, SecretKey wrappingKey, SecretKey signingKey) throws GeneralSecurityException {
+ public static void encryptRecord(String tableName, SecretKey wrappingKey, SecretKey signingKey) throws GeneralSecurityException {
// Sample record to be encrypted
final String partitionKeyName = "partition_attribute";
final String sortKeyName = "sort_attribute";
final Map record = new HashMap<>();
record.put(partitionKeyName, new AttributeValue().withS("is this"));
record.put(sortKeyName, new AttributeValue().withN("55"));
- record.put("example", new AttributeValue().withS("data"));
- record.put("some numbers", new AttributeValue().withN("99"));
- record.put("and some binary", new AttributeValue().withB(ByteBuffer.wrap(new byte[]{0x00, 0x01, 0x02})));
- record.put("leave me", new AttributeValue().withS("alone")); // We want to ignore this attribute
+ record.put(STRING_FIELD_NAME, new AttributeValue().withS("data"));
+ record.put(NUMBER_FIELD_NAME, new AttributeValue().withN("99"));
+ record.put(BINARY_FIELD_NAME, new AttributeValue().withB(ByteBuffer.wrap(new byte[]{0x00, 0x01, 0x02})));
+ record.put(IGNORED_FIELD_NAME, new AttributeValue().withS("alone")); // We want to ignore this attribute
// Set up our configuration and clients. All of this is thread-safe and can be reused across calls.
// Provider Configuration
@@ -91,7 +96,7 @@ private static void encryptRecord(String tableName, SecretKey wrappingKey, Secre
// Partition and sort keys must not be encrypted but should be signed
actions.put(attributeName, signOnly);
break;
- case "leave me":
+ case IGNORED_FIELD_NAME:
// For this example, we are neither signing nor encrypting this field
break;
default:
@@ -105,6 +110,12 @@ private static void encryptRecord(String tableName, SecretKey wrappingKey, Secre
// Encrypt the plaintext record directly
final Map encrypted_record = encryptor.encryptRecord(record, actions, encryptionContext);
+ // Encrypted record fields change as expected
+ assert encrypted_record.get(STRING_FIELD_NAME).getB() != null; // the encrypted string is stored as bytes
+ assert encrypted_record.get(NUMBER_FIELD_NAME).getB() != null; // the encrypted number is stored as bytes
+ assert !record.get(BINARY_FIELD_NAME).getB().equals(encrypted_record.get(BINARY_FIELD_NAME).getB()); // the encrypted bytes have updated
+ assert record.get(IGNORED_FIELD_NAME).getS().equals(encrypted_record.get(IGNORED_FIELD_NAME).getS()); // ignored field is left as is
+
// We could now put the encrypted item to DynamoDB just as we would any other item.
// We're skipping it to to keep the example simpler.
@@ -114,5 +125,10 @@ private static void encryptRecord(String tableName, SecretKey wrappingKey, Secre
// Decryption is identical. We'll pretend that we retrieved the record from DynamoDB.
final Map decrypted_record = encryptor.decryptRecord(encrypted_record, actions, encryptionContext);
System.out.println("Decrypted Record: " + decrypted_record);
- }
+
+ // The decrypted fields match the original fields before encryption
+ assert record.get(STRING_FIELD_NAME).getS().equals(decrypted_record.get(STRING_FIELD_NAME).getS());
+ assert record.get(NUMBER_FIELD_NAME).getN().equals(decrypted_record.get(NUMBER_FIELD_NAME).getN());
+ assert record.get(BINARY_FIELD_NAME).getB().equals(decrypted_record.get(BINARY_FIELD_NAME).getB());
+ }
}
diff --git a/examples/src/test/java/com/amazonaws/examples/AsymmetricEncryptedItemTest.java b/examples/src/test/java/com/amazonaws/examples/AsymmetricEncryptedItemTest.java
new file mode 100644
index 00000000..3ead80f5
--- /dev/null
+++ b/examples/src/test/java/com/amazonaws/examples/AsymmetricEncryptedItemTest.java
@@ -0,0 +1,24 @@
+// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package com.amazonaws.examples;
+
+import org.testng.annotations.Test;
+
+import java.security.GeneralSecurityException;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+
+public class AsymmetricEncryptedItemTest {
+ private static final String TABLE_NAME = "java-ddbec-test-table-asym-example";
+
+ @Test
+ public void testEncryptAndDecrypt() throws GeneralSecurityException {
+ final KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
+ keyGen.initialize(2048);
+ final KeyPair wrappingKeys = keyGen.generateKeyPair();
+ final KeyPair signingKeys = keyGen.generateKeyPair();
+
+ AsymmetricEncryptedItem.encryptRecord(TABLE_NAME, wrappingKeys, signingKeys);
+ }
+}
diff --git a/examples/src/test/java/com/amazonaws/examples/AwsKmsEncryptedItemIT.java b/examples/src/test/java/com/amazonaws/examples/AwsKmsEncryptedItemIT.java
new file mode 100644
index 00000000..43dc5acf
--- /dev/null
+++ b/examples/src/test/java/com/amazonaws/examples/AwsKmsEncryptedItemIT.java
@@ -0,0 +1,23 @@
+// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package com.amazonaws.examples;
+
+import com.amazonaws.services.kms.AWSKMS;
+import com.amazonaws.services.kms.AWSKMSClientBuilder;
+import org.testng.annotations.Test;
+
+import java.security.GeneralSecurityException;
+
+import static com.amazonaws.examples.TestUtils.US_WEST_2;
+import static com.amazonaws.examples.TestUtils.US_WEST_2_KEY_ID;
+
+public class AwsKmsEncryptedItemIT {
+ private static final String TABLE_NAME = "java-ddbec-test-table-kms-item-example";
+
+ @Test
+ public void testEncryptAndDecrypt() throws GeneralSecurityException {
+ final AWSKMS kms = AWSKMSClientBuilder.standard().withRegion(US_WEST_2).build();
+ AwsKmsEncryptedItem.encryptRecord(TABLE_NAME, US_WEST_2_KEY_ID, kms);
+ }
+}
diff --git a/examples/src/test/java/com/amazonaws/examples/AwsKmsEncryptedObjectIT.java b/examples/src/test/java/com/amazonaws/examples/AwsKmsEncryptedObjectIT.java
new file mode 100644
index 00000000..c2e31831
--- /dev/null
+++ b/examples/src/test/java/com/amazonaws/examples/AwsKmsEncryptedObjectIT.java
@@ -0,0 +1,31 @@
+// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package com.amazonaws.examples;
+
+import com.amazonaws.services.dynamodbv2.AmazonDynamoDB;
+import com.amazonaws.services.dynamodbv2.local.embedded.DynamoDBEmbedded;
+import com.amazonaws.services.kms.AWSKMS;
+import com.amazonaws.services.kms.AWSKMSClientBuilder;
+import org.testng.annotations.Test;
+
+import static com.amazonaws.examples.AwsKmsEncryptedObject.EXAMPLE_TABLE_NAME;
+import static com.amazonaws.examples.AwsKmsEncryptedObject.PARTITION_ATTRIBUTE;
+import static com.amazonaws.examples.AwsKmsEncryptedObject.SORT_ATTRIBUTE;
+import static com.amazonaws.examples.TestUtils.US_WEST_2;
+import static com.amazonaws.examples.TestUtils.US_WEST_2_KEY_ID;
+import static com.amazonaws.examples.TestUtils.createDDBTable;
+
+public class AwsKmsEncryptedObjectIT {
+
+ @Test
+ public void testEncryptAndDecrypt() {
+ final AWSKMS kms = AWSKMSClientBuilder.standard().withRegion(US_WEST_2).build();
+ final AmazonDynamoDB ddb = DynamoDBEmbedded.create();
+
+ // Create the table under test
+ createDDBTable(ddb, EXAMPLE_TABLE_NAME, PARTITION_ATTRIBUTE, SORT_ATTRIBUTE);
+
+ AwsKmsEncryptedObject.encryptRecord(US_WEST_2_KEY_ID, ddb, kms);
+ }
+}
diff --git a/examples/src/test/java/com/amazonaws/examples/EncryptionContextOverridesWithDynamoDBMapperIT.java b/examples/src/test/java/com/amazonaws/examples/EncryptionContextOverridesWithDynamoDBMapperIT.java
new file mode 100644
index 00000000..fa32dd88
--- /dev/null
+++ b/examples/src/test/java/com/amazonaws/examples/EncryptionContextOverridesWithDynamoDBMapperIT.java
@@ -0,0 +1,35 @@
+// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package com.amazonaws.examples;
+
+import com.amazonaws.services.dynamodbv2.AmazonDynamoDB;
+import com.amazonaws.services.dynamodbv2.local.embedded.DynamoDBEmbedded;
+
+import com.amazonaws.services.kms.AWSKMS;
+import com.amazonaws.services.kms.AWSKMSClientBuilder;
+import org.testng.annotations.Test;
+
+import java.security.GeneralSecurityException;
+
+import static com.amazonaws.examples.EncryptionContextOverridesWithDynamoDBMapper.PARTITION_ATTRIBUTE;
+import static com.amazonaws.examples.EncryptionContextOverridesWithDynamoDBMapper.SORT_ATTRIBUTE;
+import static com.amazonaws.examples.EncryptionContextOverridesWithDynamoDBMapper.TABLE_NAME_TO_OVERRIDE;
+import static com.amazonaws.examples.TestUtils.US_WEST_2;
+import static com.amazonaws.examples.TestUtils.US_WEST_2_KEY_ID;
+import static com.amazonaws.examples.TestUtils.createDDBTable;
+
+public class EncryptionContextOverridesWithDynamoDBMapperIT {
+ private static final String OVERRIDE_TABLE_NAME = "java-ddbec-test-table-encctx-override-example";
+
+ @Test
+ public void testEncryptAndDecrypt() throws GeneralSecurityException {
+ final AWSKMS kms = AWSKMSClientBuilder.standard().withRegion(US_WEST_2).build();
+ final AmazonDynamoDB ddb = DynamoDBEmbedded.create();
+
+ // Create the table under test
+ createDDBTable(ddb, TABLE_NAME_TO_OVERRIDE, PARTITION_ATTRIBUTE, SORT_ATTRIBUTE);
+
+ EncryptionContextOverridesWithDynamoDBMapper.encryptRecord(US_WEST_2_KEY_ID, OVERRIDE_TABLE_NAME, ddb, kms);
+ }
+}
diff --git a/examples/src/test/java/com/amazonaws/examples/MostRecentEncryptedItemIT.java b/examples/src/test/java/com/amazonaws/examples/MostRecentEncryptedItemIT.java
new file mode 100644
index 00000000..420f0a7b
--- /dev/null
+++ b/examples/src/test/java/com/amazonaws/examples/MostRecentEncryptedItemIT.java
@@ -0,0 +1,38 @@
+// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package com.amazonaws.examples;
+
+import com.amazonaws.services.dynamodbv2.AmazonDynamoDB;
+import com.amazonaws.services.dynamodbv2.datamodeling.encryption.providers.store.MetaStore;
+import com.amazonaws.services.dynamodbv2.local.embedded.DynamoDBEmbedded;
+import com.amazonaws.services.dynamodbv2.model.ProvisionedThroughput;
+import com.amazonaws.services.kms.AWSKMS;
+import com.amazonaws.services.kms.AWSKMSClientBuilder;
+import org.testng.annotations.Test;
+
+import java.security.GeneralSecurityException;
+
+import static com.amazonaws.examples.MostRecentEncryptedItem.PARTITION_ATTRIBUTE;
+import static com.amazonaws.examples.MostRecentEncryptedItem.SORT_ATTRIBUTE;
+import static com.amazonaws.examples.TestUtils.*;
+
+public class MostRecentEncryptedItemIT {
+ private static final String TABLE_NAME = "java-ddbec-test-table-mostrecent-example";
+ private static final String KEY_TABLE_NAME = "java-ddbec-test-table-mostrecent-example-keys";
+ private static final String MATERIAL_NAME = "testMaterial";
+
+ @Test
+ public void testEncryptAndDecrypt() throws GeneralSecurityException {
+ final AWSKMS kms = AWSKMSClientBuilder.standard().withRegion(US_WEST_2).build();
+ final AmazonDynamoDB ddb = DynamoDBEmbedded.create();
+
+ // Create the key table under test
+ MetaStore.createTable(ddb, KEY_TABLE_NAME, new ProvisionedThroughput(1L, 1L));
+
+ // Create the table under test
+ createDDBTable(ddb, TABLE_NAME, PARTITION_ATTRIBUTE, SORT_ATTRIBUTE);
+
+ MostRecentEncryptedItem.encryptRecord(TABLE_NAME, KEY_TABLE_NAME, US_WEST_2_KEY_ID, MATERIAL_NAME, ddb, kms);
+ }
+}
diff --git a/examples/src/test/java/com/amazonaws/examples/SymmetricEncryptedItemTest.java b/examples/src/test/java/com/amazonaws/examples/SymmetricEncryptedItemTest.java
new file mode 100644
index 00000000..54c14353
--- /dev/null
+++ b/examples/src/test/java/com/amazonaws/examples/SymmetricEncryptedItemTest.java
@@ -0,0 +1,28 @@
+// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package com.amazonaws.examples;
+
+import org.testng.annotations.Test;
+
+import javax.crypto.SecretKey;
+import javax.crypto.spec.SecretKeySpec;
+import java.security.GeneralSecurityException;
+import java.security.SecureRandom;
+
+public class SymmetricEncryptedItemTest {
+ private static final String TABLE_NAME = "java-ddbec-test-table-sym-example";
+
+ @Test
+ public void testEncryptAndDecrypt() throws GeneralSecurityException {
+ final SecureRandom secureRandom = new SecureRandom();
+ byte[] rawAes = new byte[32];
+ byte[] rawHmac = new byte[32];
+ secureRandom.nextBytes(rawAes);
+ secureRandom.nextBytes(rawHmac);
+ final SecretKey wrappingKey = new SecretKeySpec(rawAes, "AES");
+ final SecretKey signingKey = new SecretKeySpec(rawHmac, "HmacSHA256");
+
+ SymmetricEncryptedItem.encryptRecord(TABLE_NAME, wrappingKey, signingKey);
+ }
+}
diff --git a/examples/src/test/java/com/amazonaws/examples/TestUtils.java b/examples/src/test/java/com/amazonaws/examples/TestUtils.java
new file mode 100644
index 00000000..15030b50
--- /dev/null
+++ b/examples/src/test/java/com/amazonaws/examples/TestUtils.java
@@ -0,0 +1,41 @@
+package com.amazonaws.examples;
+
+import com.amazonaws.services.dynamodbv2.AmazonDynamoDB;
+import com.amazonaws.services.dynamodbv2.model.*;
+
+import java.util.ArrayList;
+
+import static com.amazonaws.examples.AwsKmsEncryptedObject.*;
+
+public class TestUtils {
+ private TestUtils() {
+ throw new UnsupportedOperationException(
+ "This class exists to hold static resources and cannot be instantiated."
+ );
+ }
+
+ /**
+ * These special test keys have been configured to allow Encrypt, Decrypt, and GenerateDataKey operations from any
+ * AWS principal and should be used when adding new KMS tests.
+ *
+ * This should go without saying, but never use these keys for production purposes (as anyone in the world can
+ * decrypt data encrypted using them).
+ */
+ public static final String US_WEST_2_KEY_ID = "arn:aws:kms:us-west-2:658956600833:key/b3537ef1-d8dc-4780-9f5a-55776cbb2f7f";
+ public static final String US_WEST_2 = "us-west-2";
+
+ public static void createDDBTable(AmazonDynamoDB ddb, String tableName, String partitionName, String sortName) {
+ ArrayList attrDef = new ArrayList();
+ attrDef.add(new AttributeDefinition().withAttributeName(partitionName).withAttributeType(ScalarAttributeType.S));
+ attrDef.add(new AttributeDefinition().withAttributeName(sortName).withAttributeType(ScalarAttributeType.N));
+
+ ArrayList keySchema = new ArrayList();
+ keySchema.add(new KeySchemaElement().withAttributeName(partitionName).withKeyType(KeyType.HASH));
+ keySchema.add(new KeySchemaElement().withAttributeName(sortName).withKeyType(KeyType.RANGE));
+
+ ddb.createTable(new CreateTableRequest().withTableName(tableName)
+ .withAttributeDefinitions(attrDef)
+ .withKeySchema(keySchema)
+ .withProvisionedThroughput(new ProvisionedThroughput(100L, 100L)));
+ }
+}
diff --git a/pom.xml b/pom.xml
index 240ded06..79ce1b75 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,7 +6,7 @@
4.0.0software.amazon.cryptoolsdynamodbencryptionclient-pom
- 0.1.0-SNAPSHOT
+ 1.15.0pomaws-dynamodb-encryption-java :: POM
@@ -20,8 +20,6 @@
- ddej-build-tools
- commonsdk1examples
@@ -34,20 +32,6 @@
-
- UTF-8
- 1.0.392
- 8.29
- 3.1.0
- 0.1.0
- 0.8.3
- 3.0.1
- 3.0.0
- 3.0.0-M3
- 3.0.0-M3
- 3.1.1
-
-
amazonwebservices
@@ -59,244 +43,16 @@
-
-
- org.testng
- testng
- 6.10
- test
-
-
-
- org.quicktheories
- quicktheories
- 0.25
- test
-
-
-
- org.hamcrest
- hamcrest-all
- 1.3
- test
-
-
-
- org.bouncycastle
- bcprov-ext-jdk15on
- 1.65
- test
-
-
-
- com.amazonaws
- DynamoDBLocal
- 1.10.5.1
- test
-
-
-
- com.almworks.sqlite4java
- sqlite4java
- ${sqlite4java.version}
- test
-
-
-
- com.almworks.sqlite4java
- libsqlite4java-osx
- ${sqlite4java.version}
- dylib
- test
-
-
-
- com.almworks.sqlite4java
- sqlite4java-win32-x64
- ${sqlite4java.version}
- dll
- test
-
-
-
- com.almworks.sqlite4java
- libsqlite4java-linux-amd64
- so
- ${sqlite4java.version}
- test
-
-
-
-
-
-
- dynamodb-local
- DynamoDB Local Release Repository
- https://s3-us-west-2.amazonaws.com/dynamodb-local/release
-
-
-
-
-
- org.apache.maven.plugins
- maven-compiler-plugin
- 3.8.0
-
- 1.8
- 1.8
-
-
-
-
- org.apache.maven.plugins
- maven-source-plugin
- 3.0.1
-
-
-
- org.apache.maven.plugins
- maven-javadoc-plugin
- 3.0.1
-
- *.internal:*.transform
- 128m
- 1024m
-
-
-
-
- org.apache.maven.plugins
- maven-dependency-plugin
- 3.1.1
-
-
- copy
- test-compile
-
- copy-dependencies
-
-
- test
- so,dll,dylib
- ${project.build.directory}/test-lib
-
-
-
-
-
-
- org.apache.maven.plugins
- maven-surefire-plugin
- ${maven-surefire-plugin.version}
-
- false
-
- **/Test*.java
- **/*Test.java
- **/*TestCase.java
- **/*Tests.java
- **/*TestCases.java
-
-
-
- sqlite4java.library.path
- ${project.build.directory}/test-lib
-
-
-
-
-
-
- org.apache.maven.plugins
- maven-checkstyle-plugin
- ${maven-checkstyle-plugin.version}
-
-
- com.puppycrawl.tools
- checkstyle
- ${checkstyle.version}
-
-
- software.amazon.cryptools
- ddej-build-tools
- ${ddej-build-tools.version}
-
-
-
-
- checkstyle
- validate
-
- check
-
-
-
-
- software/amazon/cryptools/ddej-build-tools/checkstyle/checkstyle.xml
- software/amazon/cryptools/ddej-build-tools/checkstyle/checkstyle-suppressions.xml
- true
- true
- true
- true
-
-
-
-
- org.apache.maven.plugins
- maven-failsafe-plugin
- ${maven-failsafe-plugin.version}
-
- false
-
-
- sqlite4java.library.path
- ${project.build.directory}/test-lib
-
-
-
- **/*ITCase.java
-
-
-
-
-
- integration-test
- verify
-
-
-
-
-
-
- org.jacoco
- jacoco-maven-plugin
- ${jacoco-maven-plugin.version}
-
-
-
- prepare-agent
-
-
-
- report
- test
-
- report
-
-
-
-
-
-
-
- org.apache.maven.plugins
- maven-jxr-plugin
- ${maven-jxr-plugin.version}
+ maven-deploy-plugin
+ 2.8.2
+
+ true
+
-
+
diff --git a/ddej-build-tools/src/main/resources/software/amazon/cryptools/ddej-build-tools/checkstyle/checkstyle-suppressions.xml b/sdk1/checkstyle/checkstyle-suppressions.xml
similarity index 100%
rename from ddej-build-tools/src/main/resources/software/amazon/cryptools/ddej-build-tools/checkstyle/checkstyle-suppressions.xml
rename to sdk1/checkstyle/checkstyle-suppressions.xml
diff --git a/ddej-build-tools/src/main/resources/software/amazon/cryptools/ddej-build-tools/checkstyle/checkstyle.xml b/sdk1/checkstyle/checkstyle.xml
similarity index 68%
rename from ddej-build-tools/src/main/resources/software/amazon/cryptools/ddej-build-tools/checkstyle/checkstyle.xml
rename to sdk1/checkstyle/checkstyle.xml
index 3b20f72d..bb85ec5b 100644
--- a/ddej-build-tools/src/main/resources/software/amazon/cryptools/ddej-build-tools/checkstyle/checkstyle.xml
+++ b/sdk1/checkstyle/checkstyle.xml
@@ -8,6 +8,6 @@
+ value="^(/*|// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.)\n( * Copyright \d{4}([-]\d{4})? Amazon\.com, Inc\. or its affiliates\. All Rights Reserved\.)?$"/>
diff --git a/sdk1/pom.xml b/sdk1/pom.xml
index d72a09e6..36d9325e 100644
--- a/sdk1/pom.xml
+++ b/sdk1/pom.xml
@@ -4,16 +4,60 @@
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/maven-v4_0_0.xsd">
4.0.0
-
com.amazonawsaws-dynamodb-encryption-java
- 1.14.1
+ 1.15.0jaraws-dynamodb-encryption-java :: SDK1AWS DynamoDB Encryption Client for AWS Java SDK v1https://github.com/aws/aws-dynamodb-encryption-java
+
+ publishingCodeArtifact
+
+
+
+
+ codeartifact
+ codeartifact
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-gpg-plugin
+ 1.6
+
+
+ sign-artifacts
+ verify
+
+ sign
+
+
+
+ --pinentry-mode
+ loopback
+
+
+
+
+
+
+
+
+
publishing
@@ -81,7 +125,6 @@
1.0.3928.293.1.0
- 0.1.00.8.33.0.13.0.0
@@ -148,7 +191,7 @@
org.bouncycastlebcprov-ext-jdk15on
- 1.60
+ 1.68test
@@ -189,6 +232,34 @@
${sqlite4java.version}test
+
+
+ com.fasterxml.jackson.core
+ jackson-annotations
+ 2.9.0
+ test
+
+
+
+ com.fasterxml.jackson.core
+ jackson-core
+ 2.9.8
+ test
+
+
+
+ com.googlecode.multithreadedtc
+ multithreadedtc
+ 1.01
+ test
+
+
+
+ junit
+ junit
+ 4.13.1
+ test
+
@@ -281,11 +352,6 @@
checkstyle${checkstyle.version}
-
- software.amazon.cryptools
- ddej-build-tools
- ${ddej-build-tools.version}
-
@@ -297,8 +363,8 @@
- software/amazon/cryptools/ddej-build-tools/checkstyle/checkstyle.xml
- software/amazon/cryptools/ddej-build-tools/checkstyle/checkstyle-suppressions.xml
+ checkstyle/checkstyle.xml
+ checkstyle/checkstyle-suppressions.xmltruetruetrue
@@ -320,6 +386,7 @@
**/*ITCase.java
+ **/*HolisticIT.java
@@ -351,6 +418,14 @@
+
+ org.apache.maven.plugins
+ maven-deploy-plugin
+ 2.8.2
+
+ false
+
+
diff --git a/sdk1/src/main/java/com/amazonaws/services/dynamodbv2/datamodeling/encryption/materials/WrappedRawMaterials.java b/sdk1/src/main/java/com/amazonaws/services/dynamodbv2/datamodeling/encryption/materials/WrappedRawMaterials.java
index bd809947..d70b5bd5 100644
--- a/sdk1/src/main/java/com/amazonaws/services/dynamodbv2/datamodeling/encryption/materials/WrappedRawMaterials.java
+++ b/sdk1/src/main/java/com/amazonaws/services/dynamodbv2/datamodeling/encryption/materials/WrappedRawMaterials.java
@@ -162,9 +162,15 @@ protected SecretKey unwrapKey(Map description, byte[] encryptedK
description.get(CONTENT_KEY_ALGORITHM), Cipher.SECRET_KEY, null, wrappingAlgorithm);
} else {
Cipher cipher = Cipher.getInstance(wrappingAlgorithm);
+
+ // This can be of the form "AES/256" as well as "AES" e.g.,
+ // but we want to set the SecretKey with just "AES" in either case
+ String[] algPieces = description.get(CONTENT_KEY_ALGORITHM).split("/", 2);
+ String contentKeyAlgorithm = algPieces[0];
+
cipher.init(Cipher.UNWRAP_MODE, unwrappingKey, Utils.getRng());
return (SecretKey) cipher.unwrap(encryptedKey,
- description.get(CONTENT_KEY_ALGORITHM), Cipher.SECRET_KEY);
+ contentKeyAlgorithm, Cipher.SECRET_KEY);
}
}
diff --git a/sdk1/src/main/java/com/amazonaws/services/dynamodbv2/datamodeling/encryption/providers/CachingMostRecentProvider.java b/sdk1/src/main/java/com/amazonaws/services/dynamodbv2/datamodeling/encryption/providers/CachingMostRecentProvider.java
new file mode 100644
index 00000000..1a575f8a
--- /dev/null
+++ b/sdk1/src/main/java/com/amazonaws/services/dynamodbv2/datamodeling/encryption/providers/CachingMostRecentProvider.java
@@ -0,0 +1,183 @@
+// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+package com.amazonaws.services.dynamodbv2.datamodeling.encryption.providers;
+
+import com.amazonaws.services.dynamodbv2.datamodeling.encryption.EncryptionContext;
+import com.amazonaws.services.dynamodbv2.datamodeling.encryption.materials.DecryptionMaterials;
+import com.amazonaws.services.dynamodbv2.datamodeling.encryption.materials.EncryptionMaterials;
+import com.amazonaws.services.dynamodbv2.datamodeling.encryption.providers.store.ProviderStore;
+import com.amazonaws.services.dynamodbv2.datamodeling.internal.TTLCache;
+import com.amazonaws.services.dynamodbv2.datamodeling.internal.TTLCache.EntryLoader;
+
+import java.util.concurrent.TimeUnit;
+
+import static com.amazonaws.services.dynamodbv2.datamodeling.internal.Utils.checkNotNull;
+
+/**
+ * This meta-Provider encrypts data with the most recent version of keying materials from a
+ * {@link ProviderStore} and decrypts using whichever version is appropriate. It also caches the
+ * results from the {@link ProviderStore} to avoid excessive load on the backing systems.
+ */
+public class CachingMostRecentProvider implements EncryptionMaterialsProvider {
+ private static final long INITIAL_VERSION = 0;
+ private static final String PROVIDER_CACHE_KEY_DELIM = "#";
+ private static final int DEFAULT_CACHE_MAX_SIZE = 1000;
+
+ private final long ttlInNanos;
+ private final ProviderStore keystore;
+ protected final String defaultMaterialName;
+ private final TTLCache providerCache;
+ private final TTLCache versionCache;
+
+ private final EntryLoader versionLoader = new EntryLoader() {
+ @Override
+ public Long load(String entryKey) {
+ return keystore.getMaxVersion(entryKey);
+ }
+ };
+
+ private final EntryLoader providerLoader = new EntryLoader() {
+ @Override
+ public EncryptionMaterialsProvider load(String entryKey) {
+ final String[] parts = entryKey.split(PROVIDER_CACHE_KEY_DELIM, 2);
+ if (parts.length != 2) {
+ throw new IllegalStateException("Invalid cache key for provider cache: " + entryKey);
+ }
+ return keystore.getProvider(parts[0], Long.parseLong(parts[1]));
+ }
+ };
+
+
+ /**
+ * Creates a new {@link CachingMostRecentProvider}.
+ *
+ * @param keystore
+ * The key store that this provider will use to determine which material and which version of material to use
+ * @param materialName
+ * The name of the materials associated with this provider
+ * @param ttlInMillis
+ * The length of time in milliseconds to cache the most recent provider
+ */
+ public CachingMostRecentProvider(final ProviderStore keystore, final String materialName, final long ttlInMillis) {
+ this(keystore, materialName, ttlInMillis, DEFAULT_CACHE_MAX_SIZE);
+ }
+
+ /**
+ * Creates a new {@link CachingMostRecentProvider}.
+ *
+ * @param keystore
+ * The key store that this provider will use to determine which material and which version of material to use
+ * @param materialName
+ * The name of the materials associated with this provider
+ * @param ttlInMillis
+ * The length of time in milliseconds to cache the most recent provider
+ * @param maxCacheSize
+ * The maximum size of the underlying caches this provider uses. Entries will be evicted from the cache
+ * once this size is exceeded.
+ */
+ public CachingMostRecentProvider(final ProviderStore keystore, final String materialName, final long ttlInMillis, final int maxCacheSize) {
+ this.keystore = checkNotNull(keystore, "keystore must not be null");
+ this.defaultMaterialName = materialName;
+ this.ttlInNanos = TimeUnit.MILLISECONDS.toNanos(ttlInMillis);
+
+ this.providerCache = new TTLCache<>(maxCacheSize, ttlInMillis, providerLoader);
+ this.versionCache = new TTLCache<>(maxCacheSize, ttlInMillis, versionLoader);
+ }
+
+ @Override
+ public EncryptionMaterials getEncryptionMaterials(EncryptionContext context) {
+ final String materialName = getMaterialName(context);
+ final long currentVersion = versionCache.load(materialName);
+
+ if (currentVersion < 0) {
+ // The material hasn't been created yet, so specify a loading function
+ // to create the first version of materials and update both caches.
+ // We want this to be done as part of the cache load to ensure that this logic
+ // only happens once in a multithreaded environment,
+ // in order to limit calls to the keystore's dependencies.
+ final String cacheKey = buildCacheKey(materialName, INITIAL_VERSION);
+ EncryptionMaterialsProvider newProvider = providerCache.load(
+ cacheKey,
+ s -> {
+ // Create the new material in the keystore
+ final String[] parts = s.split(PROVIDER_CACHE_KEY_DELIM, 2);
+ if (parts.length != 2) {
+ throw new IllegalStateException("Invalid cache key for provider cache: " + s);
+ }
+ EncryptionMaterialsProvider provider = keystore.getOrCreate(parts[0], Long.parseLong(parts[1]));
+
+ // We now should have version 0 in our keystore.
+ // Update the version cache for this material as a side effect
+ versionCache.put(materialName, INITIAL_VERSION);
+
+ // Return the new materials to be put into the cache
+ return provider;
+ }
+ );
+
+ return newProvider.getEncryptionMaterials(context);
+ } else {
+ final String cacheKey = buildCacheKey(materialName, currentVersion);
+ return providerCache.load(cacheKey).getEncryptionMaterials(context);
+ }
+ }
+
+ public DecryptionMaterials getDecryptionMaterials(EncryptionContext context) {
+ final long version = keystore.getVersionFromMaterialDescription(
+ context.getMaterialDescription());
+ final String materialName = getMaterialName(context);
+ final String cacheKey = buildCacheKey(materialName, version);
+
+ EncryptionMaterialsProvider provider = providerCache.load(cacheKey);
+ return provider.getDecryptionMaterials(context);
+ }
+
+ /**
+ * Completely empties the cache of both the current and old versions.
+ */
+ @Override
+ public void refresh() {
+ versionCache.clear();
+ providerCache.clear();
+ }
+
+ public String getMaterialName() {
+ return defaultMaterialName;
+ }
+
+ public long getTtlInMills() {
+ return TimeUnit.NANOSECONDS.toMillis(ttlInNanos);
+ }
+
+ /**
+ * The current version of the materials being used for encryption. Returns -1 if we do not
+ * currently have a current version.
+ */
+ public long getCurrentVersion() {
+ return versionCache.load(getMaterialName());
+ }
+
+ /**
+ * The last time the current version was updated. Returns 0 if we do not currently have a
+ * current version.
+ */
+ public long getLastUpdated() {
+ // We cache a version of -1 to mean that there is not a current version
+ if (versionCache.load(getMaterialName()) < 0) {
+ return 0;
+ }
+ // Otherwise, return the last update time of that entry
+ return TimeUnit.NANOSECONDS.toMillis(versionCache.getLastUpdated(getMaterialName()));
+ }
+
+ protected String getMaterialName(final EncryptionContext context) {
+ return defaultMaterialName;
+ }
+
+ private static String buildCacheKey(final String materialName, final long version) {
+ StringBuilder result = new StringBuilder(materialName);
+ result.append(PROVIDER_CACHE_KEY_DELIM);
+ result.append(version);
+ return result.toString();
+ }
+}
diff --git a/sdk1/src/main/java/com/amazonaws/services/dynamodbv2/datamodeling/encryption/providers/DirectKmsMaterialProvider.java b/sdk1/src/main/java/com/amazonaws/services/dynamodbv2/datamodeling/encryption/providers/DirectKmsMaterialProvider.java
index b7a7865e..bee842ba 100644
--- a/sdk1/src/main/java/com/amazonaws/services/dynamodbv2/datamodeling/encryption/providers/DirectKmsMaterialProvider.java
+++ b/sdk1/src/main/java/com/amazonaws/services/dynamodbv2/datamodeling/encryption/providers/DirectKmsMaterialProvider.java
@@ -263,6 +263,7 @@ protected GenerateDataKeyResult generateDataKey(final GenerateDataKeyRequest req
*
{@code RangeKeyValue}
*
{@link #TABLE_NAME_EC_KEY}
*
{@code TableName}
+ *
*/
protected void populateKmsEcFromEc(EncryptionContext context, Map kmsEc) {
final String hashKeyName = context.getHashKeyName();
diff --git a/sdk1/src/main/java/com/amazonaws/services/dynamodbv2/datamodeling/encryption/providers/MostRecentProvider.java b/sdk1/src/main/java/com/amazonaws/services/dynamodbv2/datamodeling/encryption/providers/MostRecentProvider.java
index 9edf116b..ce926f16 100644
--- a/sdk1/src/main/java/com/amazonaws/services/dynamodbv2/datamodeling/encryption/providers/MostRecentProvider.java
+++ b/sdk1/src/main/java/com/amazonaws/services/dynamodbv2/datamodeling/encryption/providers/MostRecentProvider.java
@@ -1,15 +1,5 @@
-/*
- * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except
- * in compliance with the License. A copy of the License is located at
- *
- * http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations under the License.
- */
+// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
package com.amazonaws.services.dynamodbv2.datamodeling.encryption.providers;
import java.util.concurrent.atomic.AtomicReference;
@@ -21,12 +11,24 @@
import com.amazonaws.services.dynamodbv2.datamodeling.encryption.providers.store.ProviderStore;
import com.amazonaws.services.dynamodbv2.datamodeling.internal.LRUCache;
+import static com.amazonaws.services.dynamodbv2.datamodeling.internal.Utils.checkNotNull;
+
/**
* This meta-Provider encrypts data with the most recent version of keying materials from a
* {@link ProviderStore} and decrypts using whichever version is appropriate. It also caches the
* results from the {@link ProviderStore} to avoid excessive load on the backing systems. The cache
* is not currently configurable.
+ *
+ * @deprecated This provider uses a TTL value to determine when to ping the keystore
+ * to get the current materials version, instead of using the TTL value to determine
+ * when to expire cached materials. This is unintuitive behavior for users of this provider
+ * who may wish to use a TTL to force the keystore to re-obtain materials.
+ *
+ * Use the CachingMostRecentProvider, which uses a user defined TTL value to
+ * also expire the cached materials themselves, forcing
+ * the keystore to regularly re-obtain materials.
*/
+@Deprecated
public class MostRecentProvider implements EncryptionMaterialsProvider {
private static final long MILLI_TO_NANO = 1000000L;
private static final long TTL_GRACE_IN_NANO = 500 * MILLI_TO_NANO;
@@ -165,14 +167,6 @@ private static String buildCacheKey(final String materialName, final long versio
return result.toString();
}
- private static V checkNotNull(final V ref, final String errMsg) {
- if (ref == null) {
- throw new NullPointerException(errMsg);
- } else {
- return ref;
- }
- }
-
private static class LockedState {
private final ReentrantLock lock = new ReentrantLock(true);
private volatile AtomicReference state = new AtomicReference<>(new State());
diff --git a/sdk1/src/main/java/com/amazonaws/services/dynamodbv2/datamodeling/internal/LRUCache.java b/sdk1/src/main/java/com/amazonaws/services/dynamodbv2/datamodeling/internal/LRUCache.java
index 00c79eb7..e4efb3b6 100644
--- a/sdk1/src/main/java/com/amazonaws/services/dynamodbv2/datamodeling/internal/LRUCache.java
+++ b/sdk1/src/main/java/com/amazonaws/services/dynamodbv2/datamodeling/internal/LRUCache.java
@@ -1,26 +1,11 @@
-/*
- * Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License").
- * You may not use this file except in compliance with the License.
- * A copy of the License is located at
- *
- * http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file is distributed
- * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
- * express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
+// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
package com.amazonaws.services.dynamodbv2.datamodeling.internal;
import com.amazonaws.annotation.ThreadSafe;
-import java.util.ArrayList;
import java.util.Collections;
-import java.util.Iterator;
import java.util.LinkedHashMap;
-import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
@@ -36,10 +21,7 @@ public final class LRUCache {
* Used for the internal cache.
*/
private final Map map;
- /**
- * Listener for cache entry eviction.
- */
- private final RemovalListener listener;
+
/**
* Maximum size of the cache.
*/
@@ -48,25 +30,13 @@ public final class LRUCache {
/**
* @param maxSize
* the maximum number of entries of the cache
- * @param listener
- * object which is notified immediately prior to the removal of
- * any objects from the cache
*/
- public LRUCache(final int maxSize, final RemovalListener listener) {
+ public LRUCache(final int maxSize) {
if (maxSize < 1) {
throw new IllegalArgumentException("maxSize " + maxSize + " must be at least 1");
}
this.maxSize = maxSize;
- this.listener = listener;
- map = Collections.synchronizedMap(new LRUHashMap(maxSize, listener));
- }
-
- /**
- * @param maxSize
- * the maximum number of entries of the cache
- */
- public LRUCache(final int maxSize) {
- this(maxSize, null);
+ map = Collections.synchronizedMap(new LRUHashMap<>(maxSize));
}
/**
@@ -96,23 +66,11 @@ public int getMaxSize() {
}
public void clear() {
- // The more complicated logic is to ensure that the listener is
- // actually called for all entries.
- if (listener != null) {
- List> removedEntries = new ArrayList>();
- synchronized (map) {
- Iterator> it = map.entrySet().iterator();
- while(it.hasNext()) {
- removedEntries.add(it.next());
- it.remove();
- }
- }
- for (Entry entry : removedEntries) {
- listener.onRemoval(entry);
- }
- } else {
- map.clear();
- }
+ map.clear();
+ }
+
+ public T remove(String key) {
+ return map.remove(key);
}
@Override
@@ -123,27 +81,15 @@ public String toString() {
@SuppressWarnings("serial")
private static class LRUHashMap extends LinkedHashMap {
private final int maxSize;
- private final RemovalListener listener;
- private LRUHashMap(final int maxSize, final RemovalListener listener) {
+ private LRUHashMap(final int maxSize) {
super(10, 0.75F, true);
this.maxSize = maxSize;
- this.listener = listener;
}
@Override
protected boolean removeEldestEntry(final Entry eldest) {
- if (size() > maxSize) {
- if (listener != null) {
- listener.onRemoval(eldest);
- }
- return true;
- }
- return false;
+ return size() > maxSize;
}
}
-
- public static interface RemovalListener {
- public void onRemoval(Entry entry);
- }
}
diff --git a/sdk1/src/main/java/com/amazonaws/services/dynamodbv2/datamodeling/internal/MsClock.java b/sdk1/src/main/java/com/amazonaws/services/dynamodbv2/datamodeling/internal/MsClock.java
new file mode 100644
index 00000000..f0f77b48
--- /dev/null
+++ b/sdk1/src/main/java/com/amazonaws/services/dynamodbv2/datamodeling/internal/MsClock.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except
+ * in compliance with the License. A copy of the License is located at
+ *
+ * http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+
+package com.amazonaws.services.dynamodbv2.datamodeling.internal;
+
+interface MsClock {
+ MsClock WALLCLOCK = System::nanoTime;
+
+ public long timestampNano();
+}
diff --git a/sdk1/src/main/java/com/amazonaws/services/dynamodbv2/datamodeling/internal/TTLCache.java b/sdk1/src/main/java/com/amazonaws/services/dynamodbv2/datamodeling/internal/TTLCache.java
new file mode 100644
index 00000000..18a3af1f
--- /dev/null
+++ b/sdk1/src/main/java/com/amazonaws/services/dynamodbv2/datamodeling/internal/TTLCache.java
@@ -0,0 +1,259 @@
+// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+package com.amazonaws.services.dynamodbv2.datamodeling.internal;
+
+import com.amazonaws.annotation.ThreadSafe;
+
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.concurrent.locks.ReentrantLock;
+import java.util.function.Function;
+
+import static com.amazonaws.services.dynamodbv2.datamodeling.internal.Utils.checkNotNull;
+
+/**
+ * A cache, backed by an LRUCache, that uses a loader to calculate values on cache miss
+ * or expired TTL.
+ *
+ * Note that this cache does not proactively evict expired entries,
+ * however will immediately evict entries discovered to be expired on load.
+ *
+ * @param
+ * value type
+ */
+@ThreadSafe
+public final class TTLCache {
+ /**
+ * Used for the internal cache.
+ */
+ private final LRUCache> cache;
+
+ /**
+ * Time to live for entries in the cache.
+ */
+ private final long ttlInNanos;
+
+ /**
+ * Used for loading new values into the cache on cache miss or expiration.
+ */
+ private final EntryLoader defaultLoader;
+
+ // Mockable time source, to allow us to test TTL behavior.
+ // package access for tests
+ MsClock clock = MsClock.WALLCLOCK;
+
+ private static final long TTL_GRACE_IN_NANO = TimeUnit.MILLISECONDS.toNanos(500);
+
+ /**
+ * @param maxSize
+ * the maximum number of entries of the cache
+ * @param ttlInMillis
+ * the time to live value for entries of the cache, in milliseconds
+ */
+ public TTLCache(final int maxSize, final long ttlInMillis, final EntryLoader loader) {
+ if (maxSize < 1) {
+ throw new IllegalArgumentException("maxSize " + maxSize + " must be at least 1");
+ }
+ if (ttlInMillis < 1) {
+ throw new IllegalArgumentException("ttlInMillis " + maxSize + " must be at least 1");
+ }
+ this.ttlInNanos = TimeUnit.MILLISECONDS.toNanos(ttlInMillis);
+ this.cache = new LRUCache<>(maxSize);
+ this.defaultLoader = checkNotNull(loader, "loader must not be null");
+ }
+
+ /**
+ * Uses the default loader to calculate the value at key and insert it into the cache,
+ * if it doesn't already exist or is expired according to the TTL.
+ *
+ * This immediately evicts entries past the TTL such that a load failure results
+ * in the removal of the entry.
+ *
+ * Entries that are not expired according to the TTL are returned without recalculating the value.
+ *
+ * Within a grace period past the TTL, the cache may either return the cached value without recalculating
+ * or use the loader to recalculate the value. This is implemented such that, in a multi-threaded environment,
+ * only one thread per cache key uses the loader to recalculate the value at one time.
+ *
+ * @param key
+ * The cache key to load the value at
+ * @return
+ * The value of the given value (already existing or re-calculated).
+ */
+ public T load(final String key) {
+ return load(key, defaultLoader::load);
+ }
+
+ /**
+ * Uses the inputted function to calculate the value at key and insert it into the cache,
+ * if it doesn't already exist or is expired according to the TTL.
+ *
+ * This immediately evicts entries past the TTL such that a load failure results
+ * in the removal of the entry.
+ *
+ * Entries that are not expired according to the TTL are returned without recalculating the value.
+ *
+ * Within a grace period past the TTL, the cache may either return the cached value without recalculating
+ * or use the loader to recalculate the value. This is implemented such that, in a multi-threaded environment,
+ * only one thread per cache key uses the loader to recalculate the value at one time.
+ *
+ * Returns the value of the given key (already existing or re-calculated).
+ *
+ * @param key
+ * The cache key to load the value at
+ * @param f
+ * The function to use to load the value, given key as input
+ * @return
+ * The value of the given value (already existing or re-calculated).
+ */
+ public T load(final String key, Function f) {
+ final LockedState ls = cache.get(key);
+
+ if (ls == null) {
+ // The entry doesn't exist yet, so load a new one.
+ return loadNewEntryIfAbsent(key, f);
+ } else if (clock.timestampNano() - ls.getState().lastUpdatedNano > ttlInNanos + TTL_GRACE_IN_NANO) {
+ // The data has expired past the grace period.
+ // Evict the old entry and load a new entry.
+ cache.remove(key);
+ return loadNewEntryIfAbsent(key, f);
+ } else if (clock.timestampNano() - ls.getState().lastUpdatedNano <= ttlInNanos) {
+ // The data hasn't expired. Return as-is from the cache.
+ return ls.getState().data;
+ } else if (!ls.tryLock()) {
+ // We are in the TTL grace period. If we couldn't grab the lock, then some other
+ // thread is currently loading the new value. Because we are in the grace period,
+ // use the cached data instead of waiting for the lock.
+ return ls.getState().data;
+ }
+
+ // We are in the grace period and have acquired a lock.
+ // Update the cache with the value determined by the loading function.
+ try {
+ T loadedData = f.apply(key);
+ ls.update(loadedData, clock.timestampNano());
+ return ls.getState().data;
+ } finally {
+ ls.unlock();
+ }
+ }
+
+ // Synchronously calculate the value for a new entry in the cache if it doesn't already exist.
+ // Otherwise return the cached value.
+ // It is important that this is the only place where we use the loader for a new entry,
+ // given that we don't have the entry yet to lock on.
+ // This ensures that the loading function is only called once if multiple threads
+ // attempt to add a new entry for the same key at the same time.
+ private synchronized T loadNewEntryIfAbsent(final String key, Function f) {
+ // If the entry already exists in the cache, return it
+ final LockedState cachedState = cache.get(key);
+ if (cachedState != null) {
+ return cachedState.getState().data;
+ }
+
+ // Otherwise, load the data and create a new entry
+ T loadedData = f.apply(key);
+ LockedState ls = new LockedState<>(loadedData, clock.timestampNano());
+ cache.add(key, ls);
+ return loadedData;
+ }
+
+ /**
+ * Put a new entry in the cache.
+ * Returns the value previously at that key in the cache,
+ * or null if the entry previously didn't exist or
+ * is expired.
+ */
+ public synchronized T put(final String key, final T value) {
+ LockedState ls = new LockedState<>(value, clock.timestampNano());
+ LockedState oldLockedState = cache.add(key, ls);
+ if (oldLockedState == null || clock.timestampNano() - oldLockedState.getState().lastUpdatedNano > ttlInNanos + TTL_GRACE_IN_NANO) {
+ return null;
+ }
+ return oldLockedState.getState().data;
+ }
+
+ /**
+ * Get when the entry at this key was last updated.
+ * Returns 0 if the entry doesn't exist at key.
+ */
+ public long getLastUpdated(String key) {
+ LockedState ls = cache.get(key);
+ if (ls == null) {
+ return 0;
+ }
+ return ls.getState().lastUpdatedNano;
+ }
+
+ /**
+ * Returns the current size of the cache.
+ */
+ public int size() {
+ return cache.size();
+ }
+
+ /**
+ * Returns the maximum size of the cache.
+ */
+ public int getMaxSize() {
+ return cache.getMaxSize();
+ }
+
+ /**
+ * Clears all entries from the cache.
+ */
+ public void clear() {
+ cache.clear();
+ }
+
+ @Override
+ public String toString() {
+ return cache.toString();
+ }
+
+ public interface EntryLoader {
+ T load(String entryKey);
+ }
+
+ // An object which stores a state alongside a lock,
+ // and performs updates to that state atomically.
+ // The state may only be updated if the lock is acquired by the current thread.
+ private static class LockedState {
+ private final ReentrantLock lock = new ReentrantLock(true);
+ private final AtomicReference> state;
+
+ public LockedState(T data, long createTimeNano) {
+ state = new AtomicReference<>(new State<>(data, createTimeNano));
+ }
+
+ public State getState() {
+ return state.get();
+ }
+
+ public void unlock() {
+ lock.unlock();
+ }
+
+ public boolean tryLock() {
+ return lock.tryLock();
+ }
+
+ public void update(T data, long createTimeNano) {
+ if (!lock.isHeldByCurrentThread()) {
+ throw new IllegalStateException("Lock not held by current thread");
+ }
+ state.set(new State<>(data, createTimeNano));
+ }
+ }
+
+ // An object that holds some data and the time at which this object was created
+ private static class State {
+ public final T data;
+ public final long lastUpdatedNano;
+
+ public State(T data, long lastUpdatedNano) {
+ this.data = data;
+ this.lastUpdatedNano = lastUpdatedNano;
+ }
+ }
+}
diff --git a/sdk1/src/main/java/com/amazonaws/services/dynamodbv2/datamodeling/internal/Utils.java b/sdk1/src/main/java/com/amazonaws/services/dynamodbv2/datamodeling/internal/Utils.java
index a4beb8ac..5ee04895 100644
--- a/sdk1/src/main/java/com/amazonaws/services/dynamodbv2/datamodeling/internal/Utils.java
+++ b/sdk1/src/main/java/com/amazonaws/services/dynamodbv2/datamodeling/internal/Utils.java
@@ -39,4 +39,12 @@ public static byte[] getRandom(int len) {
getRng().nextBytes(result);
return result;
}
+
+ public static V checkNotNull(final V ref, final String errMsg) {
+ if (ref == null) {
+ throw new NullPointerException(errMsg);
+ } else {
+ return ref;
+ }
+ }
}
diff --git a/sdk1/src/test/java/com/amazonaws/services/dynamodbv2/datamodeling/TransformerHolisticIT.java b/sdk1/src/test/java/com/amazonaws/services/dynamodbv2/datamodeling/TransformerHolisticIT.java
new file mode 100644
index 00000000..e982121f
--- /dev/null
+++ b/sdk1/src/test/java/com/amazonaws/services/dynamodbv2/datamodeling/TransformerHolisticIT.java
@@ -0,0 +1,840 @@
+// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+package com.amazonaws.services.dynamodbv2.datamodeling;
+
+import com.amazonaws.services.dynamodbv2.AmazonDynamoDB;
+import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapperConfig.SaveBehavior;
+import com.amazonaws.services.dynamodbv2.datamodeling.encryption.DynamoDBEncryptor;
+import com.amazonaws.services.dynamodbv2.datamodeling.encryption.providers.AsymmetricStaticProvider;
+import com.amazonaws.services.dynamodbv2.datamodeling.encryption.providers.CachingMostRecentProvider;
+import com.amazonaws.services.dynamodbv2.datamodeling.encryption.providers.DirectKmsMaterialProvider;
+import com.amazonaws.services.dynamodbv2.datamodeling.encryption.providers.EncryptionMaterialsProvider;
+import com.amazonaws.services.dynamodbv2.datamodeling.encryption.providers.SymmetricStaticProvider;
+import com.amazonaws.services.dynamodbv2.datamodeling.encryption.providers.WrappedMaterialsProvider;
+import com.amazonaws.services.dynamodbv2.datamodeling.encryption.providers.store.MetaStore;
+import com.amazonaws.services.dynamodbv2.datamodeling.encryption.providers.store.ProviderStore;
+import com.amazonaws.services.dynamodbv2.local.embedded.DynamoDBEmbedded;
+import com.amazonaws.services.dynamodbv2.model.AttributeAction;
+import com.amazonaws.services.dynamodbv2.model.AttributeDefinition;
+import com.amazonaws.services.dynamodbv2.model.AttributeValue;
+import com.amazonaws.services.dynamodbv2.model.AttributeValueUpdate;
+import com.amazonaws.services.dynamodbv2.model.ConditionalCheckFailedException;
+import com.amazonaws.services.dynamodbv2.model.CreateTableRequest;
+import com.amazonaws.services.dynamodbv2.model.KeySchemaElement;
+import com.amazonaws.services.dynamodbv2.model.KeyType;
+import com.amazonaws.services.dynamodbv2.model.ProvisionedThroughput;
+import com.amazonaws.services.dynamodbv2.model.PutItemRequest;
+import com.amazonaws.services.dynamodbv2.model.ScalarAttributeType;
+import com.amazonaws.services.dynamodbv2.model.ScanRequest;
+import com.amazonaws.services.dynamodbv2.model.ScanResult;
+import com.amazonaws.services.dynamodbv2.model.UpdateItemRequest;
+import com.amazonaws.services.dynamodbv2.testing.AttributeValueDeserializer;
+import com.amazonaws.services.dynamodbv2.testing.AttributeValueSerializer;
+import com.amazonaws.services.dynamodbv2.testing.ScenarioManifest;
+import com.amazonaws.services.dynamodbv2.testing.ScenarioManifest.KeyData;
+import com.amazonaws.services.dynamodbv2.testing.ScenarioManifest.Keys;
+import com.amazonaws.services.dynamodbv2.testing.ScenarioManifest.Scenario;
+import com.amazonaws.services.dynamodbv2.testing.types.BaseClass;
+import com.amazonaws.services.dynamodbv2.testing.types.HashKeyOnly;
+import com.amazonaws.services.dynamodbv2.testing.types.KeysOnly;
+import com.amazonaws.services.dynamodbv2.testing.types.Mixed;
+import com.amazonaws.services.dynamodbv2.testing.types.SignOnly;
+import com.amazonaws.services.dynamodbv2.testing.types.Untouched;
+import com.amazonaws.services.kms.AWSKMS;
+import com.amazonaws.services.kms.AWSKMSClientBuilder;
+import com.amazonaws.util.Base64;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.fasterxml.jackson.databind.module.SimpleModule;
+import org.junit.Before;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import javax.crypto.SecretKey;
+import javax.crypto.spec.SecretKeySpec;
+import java.io.File;
+import java.io.IOException;
+import java.net.URL;
+import java.security.GeneralSecurityException;
+import java.security.KeyFactory;
+import java.security.KeyPair;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.security.spec.X509EncodedKeySpec;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import static org.testng.AssertJUnit.assertEquals;
+import static org.testng.AssertJUnit.assertFalse;
+import static org.testng.AssertJUnit.assertNull;
+import static org.testng.AssertJUnit.assertTrue;
+import static org.testng.AssertJUnit.fail;
+
+public class TransformerHolisticIT {
+ private static final SecretKey aesKey = new SecretKeySpec(new byte[]{0,
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}, "AES");
+ private static final SecretKey hmacKey = new SecretKeySpec(new byte[]{0,
+ 1, 2, 3, 4, 5, 6, 7}, "HmacSHA256");
+ private static final String rsaEncPub = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtiNSLSvT9cExXOcD0dGZ"
+ + "9DFEMHw8895gAZcCdSppDrxbD7XgZiQYTlgt058i5fS+l11guAUJtKt5sZ2u8Fx0"
+ + "K9pxMdlczGtvQJdx/LQETEnLnfzAijvHisJ8h6dQOVczM7t01KIkS24QZElyO+kY"
+ + "qMWLytUV4RSHnrnIuUtPHCe6LieDWT2+1UBguxgtFt1xdXlquACLVv/Em3wp40Xc"
+ + "bIwzhqLitb98rTY/wqSiGTz1uvvBX46n+f2j3geZKCEDGkWcXYw3dH4lRtDWTbqw"
+ + "eRcaNDT/MJswQlBk/Up9KCyN7gjX67gttiCO6jMoTNDejGeJhG4Dd2o0vmn8WJlr"
+ + "5wIDAQAB";
+ private static final String rsaEncPriv = "MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC2I1ItK9P1wTFc"
+ + "5wPR0Zn0MUQwfDzz3mABlwJ1KmkOvFsPteBmJBhOWC3TnyLl9L6XXWC4BQm0q3mx"
+ + "na7wXHQr2nEx2VzMa29Al3H8tARMScud/MCKO8eKwnyHp1A5VzMzu3TUoiRLbhBk"
+ + "SXI76RioxYvK1RXhFIeeuci5S08cJ7ouJ4NZPb7VQGC7GC0W3XF1eWq4AItW/8Sb"
+ + "fCnjRdxsjDOGouK1v3ytNj/CpKIZPPW6+8Ffjqf5/aPeB5koIQMaRZxdjDd0fiVG"
+ + "0NZNurB5Fxo0NP8wmzBCUGT9Sn0oLI3uCNfruC22II7qMyhM0N6MZ4mEbgN3ajS+"
+ + "afxYmWvnAgMBAAECggEBAIIU293zDWDZZ73oJ+w0fHXQsdjHAmlRitPX3CN99KZX"
+ + "k9m2ldudL9bUV3Zqk2wUzgIg6LDEuFfWmAVojsaP4VBopKtriEFfAYfqIbjPgLpT"
+ + "gh8FoyWW6D6MBJCFyGALjUAHQ7uRScathvt5ESMEqV3wKJTmdsfX97w/B8J+rLN3"
+ + "3fT3ZJUck5duZ8XKD+UtX1Y3UE1hTWo3Ae2MFND964XyUqy+HaYXjH0x6dhZzqyJ"
+ + "/OJ/MPGeMJgxp+nUbMWerwxrLQceNFVgnQgHj8e8k4fd04rkowkkPua912gNtmz7"
+ + "DuIEvcMnY64z585cn+cnXUPJwtu3JbAmn/AyLsV9FLECgYEA798Ut/r+vORB16JD"
+ + "KFu38pQCgIbdCPkXeI0DC6u1cW8JFhgRqi+AqSrEy5SzY3IY7NVMSRsBI9Y026Bl"
+ + "R9OQwTrOzLRAw26NPSDvbTkeYXlY9+hX7IovHjGkho/OxyTJ7bKRDYLoNCz56BC1"
+ + "khIWvECpcf/fZU0nqOFVFqF3H/UCgYEAwmJ4rjl5fksTNtNRL6ivkqkHIPKXzk5w"
+ + "C+L90HKNicic9bqyX8K4JRkGKSNYN3mkjrguAzUlEld390qNBw5Lu7PwATv0e2i+"
+ + "6hdwJsjTKNpj7Nh4Mieq6d7lWe1L8FLyHEhxgIeQ4BgqrVtPPOH8IBGpuzVZdWwI"
+ + "dgOvEvAi/usCgYBdfk3NB/+SEEW5jn0uldE0s4vmHKq6fJwxWIT/X4XxGJ4qBmec"
+ + "NbeoOAtMbkEdWbNtXBXHyMbA+RTRJctUG5ooNou0Le2wPr6+PMAVilXVGD8dIWpj"
+ + "v9htpFvENvkZlbU++IKhCY0ICR++3ARpUrOZ3Hou/NRN36y9nlZT48tSoQKBgES2"
+ + "Bi6fxmBsLUiN/f64xAc1lH2DA0I728N343xRYdK4hTMfYXoUHH+QjurvwXkqmI6S"
+ + "cEFWAdqv7IoPYjaCSSb6ffYRuWP+LK4WxuAO0QV53SSViDdCalntHmlhRhyXVVnG"
+ + "CckDIqT0JfHNev7savDzDWpNe2fUXlFJEBPDqrstAoGBAOpd5+QBHF/tP5oPILH4"
+ + "aD/zmqMH7VtB+b/fOPwtIM+B/WnU7hHLO5t2lJYu18Be3amPkfoQIB7bpkM3Cer2"
+ + "G7Jw+TcHrY+EtIziDB5vwau1fl4VcbA9SfWpBojJ5Ifo9ELVxGiK95WxeQNSmLUy"
+ + "7AJzhK1Gwey8a/v+xfqiu9sE";
+ private static final PrivateKey rsaPriv;
+ private static final PublicKey rsaPub;
+ private static final KeyPair rsaPair;
+ private static final EncryptionMaterialsProvider symProv;
+ private static final EncryptionMaterialsProvider asymProv;
+ private static final EncryptionMaterialsProvider symWrappedProv;
+ private static final String HASH_KEY = "hashKey";
+ private static final String RANGE_KEY = "rangeKey";
+ private static final String RSA = "RSA";
+
+ private AmazonDynamoDB client;
+ private static AWSKMS kmsClient = AWSKMSClientBuilder.standard().build();
+
+ private static Map keyDataMap = new HashMap<>();
+
+ // AttributeEncryptor *must* be used with SaveBehavior.CLOBBER to avoid the risk of data corruption.
+ private static final DynamoDBMapperConfig CLOBBER_CONFIG =
+ DynamoDBMapperConfig.builder().withSaveBehavior(SaveBehavior.CLOBBER).build();
+ private static final BaseClass ENCRYPTED_TEST_VALUE = new BaseClass();
+ private static final Mixed MIXED_TEST_VALUE = new Mixed();
+ private static final SignOnly SIGNED_TEST_VALUE = new SignOnly();
+ private static final Untouched UNTOUCHED_TEST_VALUE = new Untouched();
+
+ private static final BaseClass ENCRYPTED_TEST_VALUE_2 = new BaseClass();
+ private static final Mixed MIXED_TEST_VALUE_2 = new Mixed();
+ private static final SignOnly SIGNED_TEST_VALUE_2 = new SignOnly();
+ private static final Untouched UNTOUCHED_TEST_VALUE_2 = new Untouched();
+
+ private static final String TEST_VECTOR_MANIFEST_DIR = "/vectors/encrypted_item/";
+ private static final String SCENARIO_MANIFEST_PATH = TEST_VECTOR_MANIFEST_DIR + "scenarios.json";
+ private static final String JAVA_DIR = "java";
+
+ static {
+ try {
+ KeyFactory rsaFact = KeyFactory.getInstance("RSA");
+ rsaPub = rsaFact.generatePublic(new X509EncodedKeySpec(Base64
+ .decode(rsaEncPub)));
+ rsaPriv = rsaFact.generatePrivate(new PKCS8EncodedKeySpec(Base64
+ .decode(rsaEncPriv)));
+ rsaPair = new KeyPair(rsaPub, rsaPriv);
+ } catch (GeneralSecurityException ex) {
+ throw new RuntimeException(ex);
+ }
+ symProv = new SymmetricStaticProvider(aesKey, hmacKey);
+ asymProv = new AsymmetricStaticProvider(rsaPair, rsaPair);
+ symWrappedProv = new WrappedMaterialsProvider(aesKey, aesKey, hmacKey);
+
+ ENCRYPTED_TEST_VALUE.setHashKey(5);
+ ENCRYPTED_TEST_VALUE.setRangeKey(7);
+ ENCRYPTED_TEST_VALUE.setVersion(0);
+ ENCRYPTED_TEST_VALUE.setIntValue(123);
+ ENCRYPTED_TEST_VALUE.setStringValue("Hello world!");
+ ENCRYPTED_TEST_VALUE.setByteArrayValue(new byte[]{0, 1, 2, 3, 4, 5});
+ ENCRYPTED_TEST_VALUE.setStringSet(new HashSet(Arrays.asList(
+ "Goodbye", "Cruel", "World", "?")));
+ ENCRYPTED_TEST_VALUE.setIntSet(new HashSet(Arrays.asList(1,
+ 200, 10, 15, 0)));
+
+ MIXED_TEST_VALUE.setHashKey(6);
+ MIXED_TEST_VALUE.setRangeKey(8);
+ MIXED_TEST_VALUE.setVersion(0);
+ MIXED_TEST_VALUE.setIntValue(123);
+ MIXED_TEST_VALUE.setStringValue("Hello world!");
+ MIXED_TEST_VALUE.setByteArrayValue(new byte[]{0, 1, 2, 3, 4, 5});
+ MIXED_TEST_VALUE.setStringSet(new HashSet(Arrays.asList(
+ "Goodbye", "Cruel", "World", "?")));
+ MIXED_TEST_VALUE.setIntSet(new HashSet(Arrays.asList(1, 200,
+ 10, 15, 0)));
+
+ SIGNED_TEST_VALUE.setHashKey(8);
+ SIGNED_TEST_VALUE.setRangeKey(10);
+ SIGNED_TEST_VALUE.setVersion(0);
+ SIGNED_TEST_VALUE.setIntValue(123);
+ SIGNED_TEST_VALUE.setStringValue("Hello world!");
+ SIGNED_TEST_VALUE.setByteArrayValue(new byte[]{0, 1, 2, 3, 4, 5});
+ SIGNED_TEST_VALUE.setStringSet(new HashSet(Arrays.asList(
+ "Goodbye", "Cruel", "World", "?")));
+ SIGNED_TEST_VALUE.setIntSet(new HashSet(Arrays.asList(1, 200,
+ 10, 15, 0)));
+
+ UNTOUCHED_TEST_VALUE.setHashKey(7);
+ UNTOUCHED_TEST_VALUE.setRangeKey(9);
+ UNTOUCHED_TEST_VALUE.setVersion(0);
+ UNTOUCHED_TEST_VALUE.setIntValue(123);
+ UNTOUCHED_TEST_VALUE.setStringValue("Hello world!");
+ UNTOUCHED_TEST_VALUE.setByteArrayValue(new byte[]{0, 1, 2, 3, 4, 5});
+ UNTOUCHED_TEST_VALUE.setStringSet(new HashSet(Arrays.asList(
+ "Goodbye", "Cruel", "World", "?")));
+ UNTOUCHED_TEST_VALUE.setIntSet(new HashSet(Arrays.asList(1,
+ 200, 10, 15, 0)));
+
+ // Now storing doubles
+ ENCRYPTED_TEST_VALUE_2.setHashKey(5);
+ ENCRYPTED_TEST_VALUE_2.setRangeKey(7);
+ ENCRYPTED_TEST_VALUE_2.setVersion(0);
+ ENCRYPTED_TEST_VALUE_2.setIntValue(123);
+ ENCRYPTED_TEST_VALUE_2.setStringValue("Hello world!");
+ ENCRYPTED_TEST_VALUE_2.setByteArrayValue(new byte[]{0, 1, 2, 3, 4, 5});
+ ENCRYPTED_TEST_VALUE_2.setStringSet(new HashSet(Arrays.asList(
+ "Goodbye", "Cruel", "World", "?")));
+ ENCRYPTED_TEST_VALUE_2.setIntSet(new HashSet(Arrays.asList(1,
+ 200, 10, 15, 0)));
+ ENCRYPTED_TEST_VALUE_2.setDoubleValue(15);
+ ENCRYPTED_TEST_VALUE_2.setDoubleSet(
+ new HashSet(Arrays.asList(15.0D, 7.6D, -3D, -34.2D, 0.0D)));
+
+ MIXED_TEST_VALUE_2.setHashKey(6);
+ MIXED_TEST_VALUE_2.setRangeKey(8);
+ MIXED_TEST_VALUE_2.setVersion(0);
+ MIXED_TEST_VALUE_2.setIntValue(123);
+ MIXED_TEST_VALUE_2.setStringValue("Hello world!");
+ MIXED_TEST_VALUE_2.setByteArrayValue(new byte[]{0, 1, 2, 3, 4, 5});
+ MIXED_TEST_VALUE_2.setStringSet(new HashSet(Arrays.asList(
+ "Goodbye", "Cruel", "World", "?")));
+ MIXED_TEST_VALUE_2.setIntSet(new HashSet(Arrays.asList(1, 200,
+ 10, 15, 0)));
+ MIXED_TEST_VALUE_2.setDoubleValue(15);
+ MIXED_TEST_VALUE_2.setDoubleSet(
+ new HashSet(Arrays.asList(15.0D, 7.6D, -3D, -34.2D, 0.0D)));
+
+ SIGNED_TEST_VALUE_2.setHashKey(8);
+ SIGNED_TEST_VALUE_2.setRangeKey(10);
+ SIGNED_TEST_VALUE_2.setVersion(0);
+ SIGNED_TEST_VALUE_2.setIntValue(123);
+ SIGNED_TEST_VALUE_2.setStringValue("Hello world!");
+ SIGNED_TEST_VALUE_2.setByteArrayValue(new byte[]{0, 1, 2, 3, 4, 5});
+ SIGNED_TEST_VALUE_2.setStringSet(new HashSet(Arrays.asList(
+ "Goodbye", "Cruel", "World", "?")));
+ SIGNED_TEST_VALUE_2.setIntSet(new HashSet(Arrays.asList(1, 200,
+ 10, 15, 0)));
+ SIGNED_TEST_VALUE_2.setDoubleValue(15);
+ SIGNED_TEST_VALUE_2.setDoubleSet(
+ new HashSet(Arrays.asList(15.0D, 7.6D, -3D, -34.2D, 0.0D)));
+
+ UNTOUCHED_TEST_VALUE_2.setHashKey(7);
+ UNTOUCHED_TEST_VALUE_2.setRangeKey(9);
+ UNTOUCHED_TEST_VALUE_2.setVersion(0);
+ UNTOUCHED_TEST_VALUE_2.setIntValue(123);
+ UNTOUCHED_TEST_VALUE_2.setStringValue("Hello world!");
+ UNTOUCHED_TEST_VALUE_2.setByteArrayValue(new byte[]{0, 1, 2, 3, 4, 5});
+ UNTOUCHED_TEST_VALUE_2.setStringSet(new HashSet(Arrays.asList(
+ "Goodbye", "Cruel", "World", "?")));
+ UNTOUCHED_TEST_VALUE_2.setIntSet(new HashSet(Arrays.asList(1,
+ 200, 10, 15, 0)));
+ UNTOUCHED_TEST_VALUE_2.setDoubleValue(15);
+ UNTOUCHED_TEST_VALUE_2.setDoubleSet(
+ new HashSet(Arrays.asList(15.0D, 7.6D, -3D, -34.2D, 0.0D)));
+
+ }
+
+ @DataProvider(name = "getEncryptTestVectors")
+ public static Object[][] getEncryptTestVectors() throws IOException {
+ ScenarioManifest scenarioManifest = getManifestFromFile(SCENARIO_MANIFEST_PATH,
+ new TypeReference() {});
+ loadKeyData(scenarioManifest.keyDataPath);
+
+ // Only use Java generated test vectors to dedupe the scenarios for encrypt,
+ // we only care that we are able to generate data using the different provider configurations
+ List