From 051a8e1d80ec0e1c3bfd3a853477a7804a5f4b9e Mon Sep 17 00:00:00 2001 From: Valerie Lambert Date: Wed, 1 Nov 2023 15:30:26 -0700 Subject: [PATCH] fix: Decrypt attributes returned by all DDB APIs --- CHANGELOG.md | 8 + .../runtimes/java/build.gradle.kts | 2 +- .../src/DeleteItemTransform.dfy | 75 +++- .../src/DynamoDbMiddlewareSupport.dfy | 9 + .../src/PutItemTransform.dfy | 75 +++- .../src/UpdateItemTransform.dfy | 138 ++++++- .../runtimes/java/build.gradle.kts | 2 +- .../DynamoDbEncryptionInterceptor.java | 11 + .../datamodeling/encryption/DoNotEncrypt.java | 4 + .../datamodeling/encryption/DoNotTouch.java | 4 + ...EncryptionInterceptorIntegrationTests.java | 155 ++++++- ...ryptionEnhancedClientIntegrationTests.java | 179 +++++--- .../validdatamodels/AllTypesClass.java | 386 ++++++++++++++++++ .../java/DynamoDbEncryption/build.gradle.kts | 2 +- .../Migration/DDBECToAWSDBE/build.gradle.kts | 2 +- .../PlaintextToAWSDBE/build.gradle.kts | 2 +- TestVectors/runtimes/java/build.gradle.kts | 2 +- .../ddb-sdk-integration.md | 89 ++++ 18 files changed, 1082 insertions(+), 63 deletions(-) create mode 100644 DynamoDbEncryption/runtimes/java/src/test/java/software/amazon/cryptography/dbencryptionsdk/dynamodb/enhancedclient/validdatamodels/AllTypesClass.java diff --git a/CHANGELOG.md b/CHANGELOG.md index e993f2e15..ecda8d9be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 3.1.2 2023-11-13 + +### Fix + +Fixed an issue where, when using the DynamoDbEncryptionInterceptor, +an encrypted item in the Attributes field of a DeleteItem, PutItem, or UpdateItem +response was passed through unmodified instead of being decrypted. + ## 3.1.1 2023-11-07 ### Fix diff --git a/DecryptWithPermute/runtimes/java/build.gradle.kts b/DecryptWithPermute/runtimes/java/build.gradle.kts index 9163bc53f..d5e464531 100644 --- a/DecryptWithPermute/runtimes/java/build.gradle.kts +++ b/DecryptWithPermute/runtimes/java/build.gradle.kts @@ -68,7 +68,7 @@ repositories { val dynamodb by configurations.creating dependencies { - implementation("software.amazon.cryptography:aws-database-encryption-sdk-dynamodb:3.1.1") + implementation("software.amazon.cryptography:aws-database-encryption-sdk-dynamodb:3.1.2") implementation("org.dafny:DafnyRuntime:4.1.0") implementation("software.amazon.smithy.dafny:conversion:0.1") implementation("software.amazon.cryptography:aws-cryptographic-material-providers:1.0.0") diff --git a/DynamoDbEncryption/dafny/DynamoDbEncryptionTransforms/src/DeleteItemTransform.dfy b/DynamoDbEncryption/dafny/DynamoDbEncryptionTransforms/src/DeleteItemTransform.dfy index 2042879e7..ee971d997 100644 --- a/DynamoDbEncryption/dafny/DynamoDbEncryptionTransforms/src/DeleteItemTransform.dfy +++ b/DynamoDbEncryption/dafny/DynamoDbEncryptionTransforms/src/DeleteItemTransform.dfy @@ -66,8 +66,79 @@ module DeleteItemTransform { method Output(config: Config, input: DeleteItemOutputTransformInput) returns (output: Result) - ensures output.Success? && output.value.transformedOutput == input.sdkOutput + + //= specification/dynamodb-encryption-client/ddb-sdk-integration.md#decrypt-after-deleteitem + //= type=implication + //# After a [DeleteItem](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_DeleteItem.html) + //# call is made to DynamoDB, + //# the resulting response MUST be modified before + //# being returned to the caller if: + // - there exists an Item Encryptor specified within the + // [DynamoDB Encryption Client Config](#dynamodb-encryption-client-configuration) + // with a [DynamoDB Table Name](./ddb-item-encryptor.md#dynamodb-table-name) + // equal to the `TableName` on the DeleteItem request. + // - the response contains [Attributes](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_DeleteItem.html#DDB-DeleteItem-response-Attributes). + // The response will contain Attributes if the related DeleteItem request's + // [ReturnValues](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_DeleteItem.html#DDB-DeleteItem-request-ReturnValues) + // had a value of `ALL_OLD` and an item was deleted. + ensures ( + && output.Success? + && input.originalInput.TableName in config.tableEncryptionConfigs + && input.sdkOutput.Attributes.Some? + ) ==> + && var tableConfig := config.tableEncryptionConfigs[input.originalInput.TableName]; + && var oldHistory := old(tableConfig.itemEncryptor.History.DecryptItem); + && var newHistory := tableConfig.itemEncryptor.History.DecryptItem; + + && |newHistory| == |oldHistory|+1 + && Seq.Last(newHistory).output.Success? + + //= specification/dynamodb-encryption-client/ddb-sdk-integration.md#decrypt-after-deleteitem + //= type=implication + //# In this case, the [Item Encryptor](./ddb-item-encryptor.md) MUST perform + //# [Decrypt Item](./decrypt-item.md) where the input + //# [DynamoDB Item](./decrypt-item.md#dynamodb-item) + //# is the `Attributes` field in the original response + && Seq.Last(newHistory).input.encryptedItem == input.sdkOutput.Attributes.value + + //= specification/dynamodb-encryption-client/ddb-sdk-integration.md#decrypt-after-deleteitem + //= type=implication + //# Beacons MUST be [removed](ddb-support.md#removebeacons) from the result. + && RemoveBeacons(tableConfig, Seq.Last(newHistory).output.value.plaintextItem).Success? + && var item := RemoveBeacons(tableConfig, Seq.Last(newHistory).output.value.plaintextItem).value; + + //= specification/dynamodb-encryption-client/ddb-sdk-integration.md#decrypt-after-deleteitem + //= type=implication + //# The DeleteItem response's `Attributes` field MUST be + //# replaced by the encrypted DynamoDb Item outputted above. + && output.value.transformedOutput.Attributes.Some? + && (item == output.value.transformedOutput.Attributes.value) + + // Passthrough the response if the above specification is not met + ensures ( + && output.Success? + && ( + || input.originalInput.TableName !in config.tableEncryptionConfigs + || input.sdkOutput.Attributes.None? + ) + ) ==> + output.value.transformedOutput == input.sdkOutput + + requires ValidConfig?(config) + ensures ValidConfig?(config) + modifies ModifiesConfig(config) { - return Success(DeleteItemOutputTransformOutput(transformedOutput := input.sdkOutput)); + var tableName := input.originalInput.TableName; + if tableName !in config.tableEncryptionConfigs || input.sdkOutput.Attributes.None? + { + return Success(DeleteItemOutputTransformOutput(transformedOutput := input.sdkOutput)); + } + var tableConfig := config.tableEncryptionConfigs[tableName]; + var decryptRes := tableConfig.itemEncryptor.DecryptItem( + EncTypes.DecryptItemInput(encryptedItem:=input.sdkOutput.Attributes.value) + ); + var decrypted :- MapError(decryptRes); + var item :- RemoveBeacons(tableConfig, decrypted.plaintextItem); + return Success(DeleteItemOutputTransformOutput(transformedOutput := input.sdkOutput.(Attributes := Some(item)))); } } diff --git a/DynamoDbEncryption/dafny/DynamoDbEncryptionTransforms/src/DynamoDbMiddlewareSupport.dfy b/DynamoDbEncryption/dafny/DynamoDbEncryptionTransforms/src/DynamoDbMiddlewareSupport.dfy index ed85df9ab..6ab4b6f1c 100644 --- a/DynamoDbEncryption/dafny/DynamoDbEncryptionTransforms/src/DynamoDbMiddlewareSupport.dfy +++ b/DynamoDbEncryption/dafny/DynamoDbEncryptionTransforms/src/DynamoDbMiddlewareSupport.dfy @@ -30,6 +30,15 @@ module DynamoDbMiddlewareSupport { .MapFailure(e => E(e)) } + // IsSigned returned whether this attribute is signed according to this config + predicate method {:opaque} IsSigned( + config : ValidTableConfig, + attr : string + ) + { + BS.IsSigned(config.itemEncryptor.config.attributeActionsOnEncrypt, attr) + } + // TestConditionExpression fails if a condition expression is not suitable for the // given encryption schema. // Generally this means no encrypted attribute is referenced. diff --git a/DynamoDbEncryption/dafny/DynamoDbEncryptionTransforms/src/PutItemTransform.dfy b/DynamoDbEncryption/dafny/DynamoDbEncryptionTransforms/src/PutItemTransform.dfy index 7cec499cf..5b2e96ff9 100644 --- a/DynamoDbEncryption/dafny/DynamoDbEncryptionTransforms/src/PutItemTransform.dfy +++ b/DynamoDbEncryption/dafny/DynamoDbEncryptionTransforms/src/PutItemTransform.dfy @@ -83,8 +83,79 @@ module PutItemTransform { method Output(config: Config, input: PutItemOutputTransformInput) returns (output: Result) - ensures output.Success? && output.value.transformedOutput == input.sdkOutput + + //= specification/dynamodb-encryption-client/ddb-sdk-integration.md#decrypt-after-putitem + //= type=implication + //# After a [PutItem](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_PutItem.html) + //# call is made to DynamoDB, + //# the resulting response MUST be modified before + //# being returned to the caller if: + // - there exists an Item Encryptor specified within the + // [DynamoDB Encryption Client Config](#dynamodb-encryption-client-configuration) + // with a [DynamoDB Table Name](./ddb-item-encryptor.md#dynamodb-table-name) + // equal to the `TableName` on the PutItem request. + // - the response contains [Attributes](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_PutItem.html#DDB-PutItem-response-Attributes). + // The response will contain Attributes if the related PutItem request's + // [ReturnValues](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_PutItem.html#DDB-PutItem-request-ReturnValues) + // had a value of `ALL_OLD` and the PutItem call replaced a pre-existing item. + ensures ( + && output.Success? + && input.originalInput.TableName in config.tableEncryptionConfigs + && input.sdkOutput.Attributes.Some? + ) ==> + && var tableConfig := config.tableEncryptionConfigs[input.originalInput.TableName]; + && var oldHistory := old(tableConfig.itemEncryptor.History.DecryptItem); + && var newHistory := tableConfig.itemEncryptor.History.DecryptItem; + + && |newHistory| == |oldHistory|+1 + && Seq.Last(newHistory).output.Success? + + //= specification/dynamodb-encryption-client/ddb-sdk-integration.md#decrypt-after-putitem + //= type=implication + //# In this case, the [Item Encryptor](./ddb-item-encryptor.md) MUST perform + //# [Decrypt Item](./decrypt-item.md) where the input + //# [DynamoDB Item](./decrypt-item.md#dynamodb-item) + //# is the `Attributes` field in the original response + && Seq.Last(newHistory).input.encryptedItem == input.sdkOutput.Attributes.value + + //= specification/dynamodb-encryption-client/ddb-sdk-integration.md#decrypt-after-putitem + //= type=implication + //# Beacons MUST be [removed](ddb-support.md#removebeacons) from the result. + && RemoveBeacons(tableConfig, Seq.Last(newHistory).output.value.plaintextItem).Success? + && var item := RemoveBeacons(tableConfig, Seq.Last(newHistory).output.value.plaintextItem).value; + + //= specification/dynamodb-encryption-client/ddb-sdk-integration.md#decrypt-after-putitem + //= type=implication + //# The PutItem response's `Attributes` field MUST be + //# replaced by the encrypted DynamoDb Item outputted above. + && output.value.transformedOutput.Attributes.Some? + && (item == output.value.transformedOutput.Attributes.value) + + // Passthrough the response if the above specification is not met + ensures ( + && output.Success? + && ( + || input.originalInput.TableName !in config.tableEncryptionConfigs + || input.sdkOutput.Attributes.None? + ) + ) ==> + output.value.transformedOutput == input.sdkOutput + + requires ValidConfig?(config) + ensures ValidConfig?(config) + modifies ModifiesConfig(config) { - return Success(PutItemOutputTransformOutput(transformedOutput := input.sdkOutput)); + var tableName := input.originalInput.TableName; + if tableName !in config.tableEncryptionConfigs || input.sdkOutput.Attributes.None? + { + return Success(PutItemOutputTransformOutput(transformedOutput := input.sdkOutput)); + } + var tableConfig := config.tableEncryptionConfigs[tableName]; + var decryptRes := tableConfig.itemEncryptor.DecryptItem( + EncTypes.DecryptItemInput(encryptedItem:=input.sdkOutput.Attributes.value) + ); + var decrypted :- MapError(decryptRes); + var item :- RemoveBeacons(tableConfig, decrypted.plaintextItem); + return Success(PutItemOutputTransformOutput(transformedOutput := input.sdkOutput.(Attributes := Some(item)))); } } diff --git a/DynamoDbEncryption/dafny/DynamoDbEncryptionTransforms/src/UpdateItemTransform.dfy b/DynamoDbEncryption/dafny/DynamoDbEncryptionTransforms/src/UpdateItemTransform.dfy index 0e66fba1d..6e51ec902 100644 --- a/DynamoDbEncryption/dafny/DynamoDbEncryptionTransforms/src/UpdateItemTransform.dfy +++ b/DynamoDbEncryption/dafny/DynamoDbEncryptionTransforms/src/UpdateItemTransform.dfy @@ -65,8 +65,142 @@ module UpdateItemTransform { method Output(config: Config, input: UpdateItemOutputTransformInput) returns (output: Result) - ensures output.Success? && output.value.transformedOutput == input.sdkOutput + + //= specification/dynamodb-encryption-client/ddb-sdk-integration.md#decrypt-after-updateitem + //= type=implication + //# After a [UpdateItem](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_UpdateItem.html) + //# call is made to DynamoDB, + //# the resulting response MUST be modified before + //# being returned to the caller if: + // - there exists an Item Encryptor specified within the + // [DynamoDB Encryption Client Config](#dynamodb-encryption-client-configuration) + // with a [DynamoDB Table Name](./ddb-item-encryptor.md#dynamodb-table-name) + // equal to the `TableName` on the UpdateItem request. + // - the response contains [Attributes](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_UpdateItem.html#DDB-UpdateItem-response-Attributes). + // - the original UpdateItem request had a + // [ReturnValues](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_UpdateItem.html#DDB-UpdateItem-request-ReturnValues) + // with a value of `ALL_OLD` or `ALL_NEW`. + ensures ( + && output.Success? + && input.originalInput.TableName in config.tableEncryptionConfigs + && input.sdkOutput.Attributes.Some? + && input.originalInput.ReturnValues.Some? + && ( + || input.originalInput.ReturnValues.value.ALL_OLD? + || input.originalInput.ReturnValues.value.ALL_NEW? + ) + ) ==> + && var tableConfig := config.tableEncryptionConfigs[input.originalInput.TableName]; + && var oldHistory := old(tableConfig.itemEncryptor.History.DecryptItem); + && var newHistory := tableConfig.itemEncryptor.History.DecryptItem; + + && |newHistory| == |oldHistory|+1 + && Seq.Last(newHistory).output.Success? + + //= specification/dynamodb-encryption-client/ddb-sdk-integration.md#decrypt-after-updateitem + //= type=implication + //# In this case, the [Item Encryptor](./ddb-item-encryptor.md) MUST perform + //# [Decrypt Item](./decrypt-item.md) where the input + //# [DynamoDB Item](./decrypt-item.md#dynamodb-item) + //# is the `Attributes` field in the original response + && Seq.Last(newHistory).input.encryptedItem == input.sdkOutput.Attributes.value + + //= specification/dynamodb-encryption-client/ddb-sdk-integration.md#decrypt-after-updateitem + //= type=implication + //# Beacons MUST be [removed](ddb-support.md#removebeacons) from the result. + && RemoveBeacons(tableConfig, Seq.Last(newHistory).output.value.plaintextItem).Success? + && var item := RemoveBeacons(tableConfig, Seq.Last(newHistory).output.value.plaintextItem).value; + + //= specification/dynamodb-encryption-client/ddb-sdk-integration.md#decrypt-after-updateitem + //= type=implication + //# The UpdateItem response's `Attributes` field MUST be + //# replaced by the encrypted DynamoDb Item outputted above. + && output.value.transformedOutput.Attributes.Some? + && (item == output.value.transformedOutput.Attributes.value) + + //= specification/dynamodb-encryption-client/ddb-sdk-integration.md#decrypt-after-updateitem + //= type=implication + //# In all other cases, the UpdateItem response MUST NOT be modified. + ensures ( + && output.Success? + && ( + || input.originalInput.TableName !in config.tableEncryptionConfigs + || input.sdkOutput.Attributes.None? + ) + ) ==> ( + && output.value.transformedOutput == input.sdkOutput + ) + + ensures ( + && output.Success? + && input.originalInput.TableName in config.tableEncryptionConfigs + && input.sdkOutput.Attributes.Some? + && (input.originalInput.ReturnValues.Some? ==> ( + || input.originalInput.ReturnValues.value.UPDATED_NEW? + || input.originalInput.ReturnValues.value.UPDATED_OLD? + ) + ) + ) ==> ( + && var tableConfig := config.tableEncryptionConfigs[input.originalInput.TableName]; + && output.value.transformedOutput == input.sdkOutput + && forall k <- input.sdkOutput.Attributes.value.Keys :: !IsSigned(tableConfig, k) + ) + + //= specification/dynamodb-encryption-client/ddb-sdk-integration.md#decrypt-after-updateitem + //= type=implication + //# Additionally, if a value of `UPDATED_OLD` or `UPDATED_NEW` was used, + //# and any Attributes in the response are authenticated + //# per the [DynamoDB Encryption Client Config](#dynamodb-encryption-client-configuration), + //# an error MUST be raised. + ensures ( + && input.originalInput.TableName in config.tableEncryptionConfigs + && var tableConfig := config.tableEncryptionConfigs[input.originalInput.TableName]; + && input.sdkOutput.Attributes.Some? + && (input.originalInput.ReturnValues.Some? ==> ( + || input.originalInput.ReturnValues.value.UPDATED_NEW? + || input.originalInput.ReturnValues.value.UPDATED_OLD? + ) + ) + && exists k <- input.sdkOutput.Attributes.value.Keys :: IsSigned(tableConfig, k) + ) ==> + output.Failure? + + requires ValidConfig?(config) + ensures ValidConfig?(config) + modifies ModifiesConfig(config) { - return Success(UpdateItemOutputTransformOutput(transformedOutput := input.sdkOutput)); + var tableName := input.originalInput.TableName; + + if + || tableName !in config.tableEncryptionConfigs + || input.sdkOutput.Attributes.None? + { + return Success(UpdateItemOutputTransformOutput(transformedOutput := input.sdkOutput)); + } + + var tableConfig := config.tableEncryptionConfigs[tableName]; + var attributes := input.sdkOutput.Attributes.value; + + if !( + && input.originalInput.ReturnValues.Some? + && ( + || input.originalInput.ReturnValues.value.ALL_NEW? + || input.originalInput.ReturnValues.value.ALL_OLD?) + ) + { + // This error should not be possible to reach if we assume the DDB API contract is correct. + // We include this runtime check for defensive purposes. + :- Need(forall k <- attributes.Keys :: !IsSigned(tableConfig, k), + E("UpdateItems response contains signed attributes, but does not include the entire item which is required for verification.")); + + return Success(UpdateItemOutputTransformOutput(transformedOutput := input.sdkOutput)); + } + + var decryptRes := tableConfig.itemEncryptor.DecryptItem( + EncTypes.DecryptItemInput(encryptedItem:=attributes) + ); + var decrypted :- MapError(decryptRes); + var item :- RemoveBeacons(tableConfig, decrypted.plaintextItem); + return Success(UpdateItemOutputTransformOutput(transformedOutput := input.sdkOutput.(Attributes := Some(item)))); } } diff --git a/DynamoDbEncryption/runtimes/java/build.gradle.kts b/DynamoDbEncryption/runtimes/java/build.gradle.kts index 92bcb3812..10009eef3 100644 --- a/DynamoDbEncryption/runtimes/java/build.gradle.kts +++ b/DynamoDbEncryption/runtimes/java/build.gradle.kts @@ -12,7 +12,7 @@ plugins { } group = "software.amazon.cryptography" -version = "3.1.1" +version = "3.1.2" description = "Aws Database Encryption Sdk for DynamoDb Java" java { diff --git a/DynamoDbEncryption/runtimes/java/src/main/java/software/amazon/cryptography/dbencryptionsdk/dynamodb/DynamoDbEncryptionInterceptor.java b/DynamoDbEncryption/runtimes/java/src/main/java/software/amazon/cryptography/dbencryptionsdk/dynamodb/DynamoDbEncryptionInterceptor.java index 78e8848be..bdfa8306a 100644 --- a/DynamoDbEncryption/runtimes/java/src/main/java/software/amazon/cryptography/dbencryptionsdk/dynamodb/DynamoDbEncryptionInterceptor.java +++ b/DynamoDbEncryption/runtimes/java/src/main/java/software/amazon/cryptography/dbencryptionsdk/dynamodb/DynamoDbEncryptionInterceptor.java @@ -252,6 +252,17 @@ public SdkResponse modifyResponse(Context.ModifyResponse context, ExecutionAttri .sdkHttpResponse(originalResponse.sdkHttpResponse()) .build(); break; + } case "DeleteItem": { + DeleteItemResponse transformedResponse = transformer.DeleteItemOutputTransform( + DeleteItemOutputTransformInput.builder() + .sdkOutput((DeleteItemResponse) originalResponse) + .originalInput((DeleteItemRequest) originalRequest) + .build()).transformedOutput(); + outgoingResponse = transformedResponse.toBuilder() + .responseMetadata(((DeleteItemResponse) originalResponse).responseMetadata()) + .sdkHttpResponse(originalResponse.sdkHttpResponse()) + .build(); + break; } case "ExecuteStatement": { ExecuteStatementResponse transformedResponse = transformer.ExecuteStatementOutputTransform( ExecuteStatementOutputTransformInput.builder() diff --git a/DynamoDbEncryption/runtimes/java/src/main/sdkv1/com/amazonaws/services/dynamodbv2/datamodeling/encryption/DoNotEncrypt.java b/DynamoDbEncryption/runtimes/java/src/main/sdkv1/com/amazonaws/services/dynamodbv2/datamodeling/encryption/DoNotEncrypt.java index 45f490968..a6c11bdba 100644 --- a/DynamoDbEncryption/runtimes/java/src/main/sdkv1/com/amazonaws/services/dynamodbv2/datamodeling/encryption/DoNotEncrypt.java +++ b/DynamoDbEncryption/runtimes/java/src/main/sdkv1/com/amazonaws/services/dynamodbv2/datamodeling/encryption/DoNotEncrypt.java @@ -21,6 +21,9 @@ import java.lang.annotation.Target; /** + * Warning: This annotation only works with the DynamoDBMapper for AWS SDK for Java 1.x. + * If you are using the AWS SDK for Java 2.x, use @DynamoDbEncryptionSignOnly instead. + * * Prevents the associated item (class or attribute) from being encrypted. * *

For guidance on performing a safe data model change procedure, please see For guidance on performing a safe data model change procedure, please see putItem = putResponse.attributes(); + assertNotNull(putItem); + assertEquals(partitionValue, putItem.get(TEST_PARTITION_NAME).s()); + assertEquals(sortValue, putItem.get(TEST_SORT_NAME).n()); + assertEquals(attrValue, putItem.get(TEST_ATTR_NAME).s()); // Get Item back from table Map keyToGet = createTestKey(partitionValue, sortValue); @@ -87,6 +99,147 @@ public void TestPutItemGetItem() { assertEquals(attrValue, returnedItem.get(TEST_ATTR_NAME).s()); } + // Test that if we update a DO_NOTHING attribute, + // we correctly decrypt the ALL_* return values + @Test + public void TestUpdateItemReturnAll() { + // Put item into table + String partitionValue = "update_ALL"; + String sortValue = randomNum; + String attrValue = "bar"; + String attrValue2 = "hello world"; + Map item = createTestItem(partitionValue, sortValue, attrValue, attrValue2); + + PutItemRequest putRequest = PutItemRequest.builder() + .tableName(TEST_TABLE_NAME) + .item(item) + .returnValues(ReturnValue.ALL_OLD) + .build(); + + ddbKmsKeyring.putItem(putRequest); + + // Update unsigned attribute, and return ALL_OLD + Map keyToGet = createTestKey(partitionValue, sortValue); + UpdateItemRequest updateRequest = UpdateItemRequest.builder() + .key(keyToGet) + .returnValues(ReturnValue.ALL_OLD) + .updateExpression("SET #D = :d") + .expressionAttributeNames(Collections.singletonMap("#D", "attr2")) + .expressionAttributeValues(Collections.singletonMap(":d", AttributeValue.fromS("updated"))) + .tableName(TEST_TABLE_NAME) + .build(); + + UpdateItemResponse updateResponse = ddbKmsKeyring.updateItem(updateRequest); + assertEquals(200, updateResponse.sdkHttpResponse().statusCode()); + Map returnedItem = updateResponse.attributes(); + assertNotNull(returnedItem); + assertEquals(partitionValue, returnedItem.get(TEST_PARTITION_NAME).s()); + assertEquals(sortValue, returnedItem.get(TEST_SORT_NAME).n()); + assertEquals(attrValue, returnedItem.get(TEST_ATTR_NAME).s()); + assertEquals("hello world", returnedItem.get(TEST_ATTR2_NAME).s()); + + // Update unsigned attribute, and return ALL_NEW + UpdateItemRequest updateRequest2 = updateRequest.toBuilder() + .returnValues(ReturnValue.ALL_NEW) + .expressionAttributeValues(Collections.singletonMap(":d", AttributeValue.fromS("updated2"))) + .build(); + + UpdateItemResponse updateResponse2 = ddbKmsKeyring.updateItem(updateRequest2); + assertEquals(200, updateResponse2.sdkHttpResponse().statusCode()); + Map returnedItem2 = updateResponse2.attributes(); + assertNotNull(returnedItem2); + assertEquals(partitionValue, returnedItem2.get(TEST_PARTITION_NAME).s()); + assertEquals(sortValue, returnedItem2.get(TEST_SORT_NAME).n()); + assertEquals(attrValue, returnedItem2.get(TEST_ATTR_NAME).s()); + assertEquals("updated2", returnedItem2.get(TEST_ATTR2_NAME).s()); + } + + // Test that if we update a DO_NOTHING attribute, + // we correctly pass through the UPDATED_* return values + @Test + public void TestUpdateItemReturnUpdated() { + // Put item into table + String partitionValue = "update_UPDATE"; + String sortValue = randomNum; + String attrValue = "bar"; + String attrValue2 = "hello world"; + Map item = createTestItem(partitionValue, sortValue, attrValue, attrValue2); + + PutItemRequest putRequest = PutItemRequest.builder() + .tableName(TEST_TABLE_NAME) + .item(item) + .returnValues(ReturnValue.ALL_OLD) + .build(); + + ddbKmsKeyring.putItem(putRequest); + + // Update unsigned attribute, and return UPDATED_OLD + Map keyToGet = createTestKey(partitionValue, sortValue); + UpdateItemRequest updateRequest = UpdateItemRequest.builder() + .key(keyToGet) + .returnValues(ReturnValue.UPDATED_OLD) + .updateExpression("SET #D = :d") + .expressionAttributeNames(Collections.singletonMap("#D", "attr2")) + .expressionAttributeValues(Collections.singletonMap(":d", AttributeValue.fromS("updated"))) + .tableName(TEST_TABLE_NAME) + .build(); + + UpdateItemResponse updateResponse = ddbKmsKeyring.updateItem(updateRequest); + assertEquals(200, updateResponse.sdkHttpResponse().statusCode()); + Map returnedItem = updateResponse.attributes(); + assertNotNull(returnedItem); + assertFalse(returnedItem.containsKey(TEST_ATTR_NAME)); + assertEquals("hello world", returnedItem.get(TEST_ATTR2_NAME).s()); + + // Update unsigned attribute, and return ALL_NEW + UpdateItemRequest updateRequest2 = updateRequest.toBuilder() + .returnValues(ReturnValue.UPDATED_NEW) + .expressionAttributeValues(Collections.singletonMap(":d", AttributeValue.fromS("updated2"))) + .build(); + + UpdateItemResponse updateResponse2 = ddbKmsKeyring.updateItem(updateRequest2); + assertEquals(200, updateResponse2.sdkHttpResponse().statusCode()); + Map returnedItem2 = updateResponse2.attributes(); + assertNotNull(returnedItem2); + assertFalse(returnedItem2.containsKey(TEST_ATTR_NAME)); + assertEquals("updated2", returnedItem2.get(TEST_ATTR2_NAME).s()); + } + + @Test + public void TestDeleteItem() { + // Put item into table + String partitionValue = "delete"; + String sortValue = randomNum; + String attrValue = "bar"; + String attrValue2 = "hello world"; + Map item = createTestItem(partitionValue, sortValue, attrValue, attrValue2); + + PutItemRequest putRequest = PutItemRequest.builder() + .tableName(TEST_TABLE_NAME) + .item(item) + .build(); + + PutItemResponse putResponse = ddbKmsKeyring.putItem(putRequest); + assertEquals(200, putResponse.sdkHttpResponse().statusCode()); + + // Delete item from table, set ReturnValues to ALL_OLD to return deleted item + Map keyToGet = createTestKey(partitionValue, sortValue); + + DeleteItemRequest deleteRequest = DeleteItemRequest.builder() + .key(keyToGet) + .tableName(TEST_TABLE_NAME) + .returnValues(ReturnValue.ALL_OLD) + .build(); + + DeleteItemResponse deleteResponse = ddbKmsKeyring.deleteItem(deleteRequest); + assertEquals(200, deleteResponse.sdkHttpResponse().statusCode()); + Map returnedItem = deleteResponse.attributes(); + assertNotNull(returnedItem); + assertEquals(partitionValue, returnedItem.get(TEST_PARTITION_NAME).s()); + assertEquals(sortValue, returnedItem.get(TEST_SORT_NAME).n()); + assertEquals(attrValue, returnedItem.get(TEST_ATTR_NAME).s()); + } + @Test public void TestBatchWriteBatchGet() { // Batch write items to table diff --git a/DynamoDbEncryption/runtimes/java/src/test/java/software/amazon/cryptography/dbencryptionsdk/dynamodb/enhancedclient/DynamoDbEncryptionEnhancedClientIntegrationTests.java b/DynamoDbEncryption/runtimes/java/src/test/java/software/amazon/cryptography/dbencryptionsdk/dynamodb/enhancedclient/DynamoDbEncryptionEnhancedClientIntegrationTests.java index 207fb41f4..ae84494ba 100644 --- a/DynamoDbEncryption/runtimes/java/src/test/java/software/amazon/cryptography/dbencryptionsdk/dynamodb/enhancedclient/DynamoDbEncryptionEnhancedClientIntegrationTests.java +++ b/DynamoDbEncryption/runtimes/java/src/test/java/software/amazon/cryptography/dbencryptionsdk/dynamodb/enhancedclient/DynamoDbEncryptionEnhancedClientIntegrationTests.java @@ -15,8 +15,12 @@ import software.amazon.awssdk.enhanced.dynamodb.Key; import software.amazon.awssdk.enhanced.dynamodb.TableSchema; import software.amazon.awssdk.enhanced.dynamodb.model.GetItemEnhancedRequest; +import software.amazon.awssdk.enhanced.dynamodb.model.PutItemEnhancedRequest; +import software.amazon.awssdk.enhanced.dynamodb.model.PutItemEnhancedResponse; +import software.amazon.awssdk.enhanced.dynamodb.model.UpdateItemEnhancedRequest; import software.amazon.awssdk.services.dynamodb.DynamoDbClient; import software.amazon.awssdk.services.dynamodb.model.DynamoDbException; +import software.amazon.awssdk.services.dynamodb.model.ReturnValue; import software.amazon.awssdk.services.kms.model.KmsException; import software.amazon.cryptography.dbencryptionsdk.dynamodb.enhancedclient.validdatamodels.*; @@ -31,6 +35,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.ThreadLocalRandom; import static org.testng.Assert.assertEquals; import static software.amazon.cryptography.dbencryptionsdk.dynamodb.TestUtils.*; @@ -41,6 +46,11 @@ public class DynamoDbEncryptionEnhancedClientIntegrationTests { + // Some integration tests MUST mutate the state of the DDB table. + // For such tests, include a random number in the primary key + // to avoid conflicts between distributed test runners sharing a table. + private int randomNum = ThreadLocalRandom.current().nextInt(Integer.MIN_VALUE, Integer.MAX_VALUE ); + private static DynamoDbEnhancedClient initEnhancedClientWithInterceptor( final TableSchema schemaOnEncrypt, final List allowedUnsignedAttributes, @@ -75,6 +85,46 @@ private static DynamoDbEnhancedClient initEnhancedClientWithInterceptor( .build(); } + private static DynamoDbEnhancedClient createEnhancedClientForLegacyClass(DynamoDBEncryptor oldEncryptor, TableSchema schemaOnEncrypt) { + Map legacyActions = new HashMap<>(); + legacyActions.put("partition_key", CryptoAction.SIGN_ONLY); + legacyActions.put("sort_key", CryptoAction.SIGN_ONLY); + legacyActions.put("encryptAndSign", CryptoAction.ENCRYPT_AND_SIGN); + legacyActions.put("signOnly", CryptoAction.SIGN_ONLY); + legacyActions.put("doNothing", CryptoAction.DO_NOTHING); + LegacyOverride legacyOverride = LegacyOverride + .builder() + .encryptor(oldEncryptor) + .policy(LegacyPolicy.FORCE_LEGACY_ENCRYPT_ALLOW_LEGACY_DECRYPT) + .attributeActionsOnEncrypt(legacyActions) + .build(); + + Map tableConfigs = new HashMap<>(); + tableConfigs.put(TEST_TABLE_NAME, + DynamoDbEnhancedTableEncryptionConfig.builder() + .logicalTableName(TEST_TABLE_NAME) + .keyring(createKmsKeyring()) + .allowedUnsignedAttributes(Arrays.asList("doNothing")) + .schemaOnEncrypt(schemaOnEncrypt) + .legacyOverride(legacyOverride) + .build()); + DynamoDbEncryptionInterceptor interceptor = + DynamoDbEnhancedClientEncryption.CreateDynamoDbEncryptionInterceptor( + CreateDynamoDbEncryptionInterceptorInput.builder() + .tableEncryptionConfigs(tableConfigs) + .build() + ); + DynamoDbClient ddb = DynamoDbClient.builder() + .overrideConfiguration( + ClientOverrideConfiguration.builder() + .addExecutionInterceptor(interceptor) + .build()) + .build(); + return DynamoDbEnhancedClient.builder() + .dynamoDbClient(ddb) + .build(); + } + @Test public void TestPutAndGet() { TableSchema schemaOnEncrypt = TableSchema.fromBean(SimpleClass.class); @@ -109,6 +159,35 @@ public void TestPutAndGet() { assertEquals(result.getDoNothing(), "fizzbuzz"); } + @Test + public void TestPutAndGetAllTypes() { + TableSchema schemaOnEncrypt = TableSchema.fromBean(AllTypesClass.class); + List allowedUnsignedAttributes = Collections.singletonList("doNothing"); + DynamoDbEnhancedClient enhancedClient = + initEnhancedClientWithInterceptor(schemaOnEncrypt, allowedUnsignedAttributes, null, null); + + DynamoDbTable table = enhancedClient.table(TEST_TABLE_NAME, schemaOnEncrypt); + + AllTypesClass record = AllTypesClass.createTestItem("EnhancedPutGetAllTypes", 1); + + // Put an item into DDB such that it also returns back the item. + PutItemEnhancedResponse putItemResp = table.putItemWithResponse( + (PutItemEnhancedRequest.Builder requestBuilder) + -> requestBuilder.item(record) + .returnValues(ReturnValue.ALL_OLD)); + assertEquals(putItemResp.attributes(), record); + + // Get the item back from the table + Key key = Key.builder() + .partitionValue("EnhancedPutGetAllTypes").sortValue(1) + .build(); + + // Get the item by using the key. + AllTypesClass result = table.getItem( + (GetItemEnhancedRequest.Builder requestBuilder) -> requestBuilder.key(key)); + assertEquals(result, record); + } + @Test public void TestPutAndGetAnnotatedFlattenedBean() { final String PARTITION = "AnnotatedFlattenedBean"; @@ -236,20 +315,6 @@ public void TestGetLegacyItem() { mapper.save(record); - // Configure EnhancedClient with Legacy behavior - Map legacyActions = new HashMap<>(); - legacyActions.put("partition_key", CryptoAction.SIGN_ONLY); - legacyActions.put("sort_key", CryptoAction.SIGN_ONLY); - legacyActions.put("encryptAndSign", CryptoAction.ENCRYPT_AND_SIGN); - legacyActions.put("signOnly", CryptoAction.SIGN_ONLY); - legacyActions.put("doNothing", CryptoAction.DO_NOTHING); - LegacyOverride legacyOverride = LegacyOverride - .builder() - .encryptor(oldEncryptor) - .policy(LegacyPolicy.FORCE_LEGACY_ENCRYPT_ALLOW_LEGACY_DECRYPT) - .attributeActionsOnEncrypt(legacyActions) - .build(); - TableSchema schemaOnEncrypt = TableSchema.fromBean(LegacyClass.class); DynamoDbEnhancedClient enhancedClient = createEnhancedClientForLegacyClass(oldEncryptor, schemaOnEncrypt); @@ -300,44 +365,58 @@ public void TestWriteLegacyItem() { assertEquals("fizzbuzz", result.getDoNothing()); } - private static DynamoDbEnhancedClient createEnhancedClientForLegacyClass(DynamoDBEncryptor oldEncryptor, TableSchema schemaOnEncrypt) { - Map legacyActions = new HashMap<>(); - legacyActions.put("partition_key", CryptoAction.SIGN_ONLY); - legacyActions.put("sort_key", CryptoAction.SIGN_ONLY); - legacyActions.put("encryptAndSign", CryptoAction.ENCRYPT_AND_SIGN); - legacyActions.put("signOnly", CryptoAction.SIGN_ONLY); - legacyActions.put("doNothing", CryptoAction.DO_NOTHING); - LegacyOverride legacyOverride = LegacyOverride - .builder() - .encryptor(oldEncryptor) - .policy(LegacyPolicy.FORCE_LEGACY_ENCRYPT_ALLOW_LEGACY_DECRYPT) - .attributeActionsOnEncrypt(legacyActions) - .build(); + @Test + public void TestDelete() { + TableSchema schemaOnEncrypt = TableSchema.fromBean(AllTypesClass.class); + List allowedUnsignedAttributes = Collections.singletonList("doNothing"); + DynamoDbEnhancedClient enhancedClient = + initEnhancedClientWithInterceptor(schemaOnEncrypt, allowedUnsignedAttributes, null, null); - Map tableConfigs = new HashMap<>(); - tableConfigs.put(TEST_TABLE_NAME, - DynamoDbEnhancedTableEncryptionConfig.builder() - .logicalTableName(TEST_TABLE_NAME) - .keyring(createKmsKeyring()) - .allowedUnsignedAttributes(Arrays.asList("doNothing")) - .schemaOnEncrypt(schemaOnEncrypt) - .legacyOverride(legacyOverride) - .build()); - DynamoDbEncryptionInterceptor interceptor = - DynamoDbEnhancedClientEncryption.CreateDynamoDbEncryptionInterceptor( - CreateDynamoDbEncryptionInterceptorInput.builder() - .tableEncryptionConfigs(tableConfigs) - .build() - ); - DynamoDbClient ddb = DynamoDbClient.builder() - .overrideConfiguration( - ClientOverrideConfiguration.builder() - .addExecutionInterceptor(interceptor) - .build()) - .build(); - return DynamoDbEnhancedClient.builder() - .dynamoDbClient(ddb) + DynamoDbTable table = enhancedClient.table(TEST_TABLE_NAME, schemaOnEncrypt); + + AllTypesClass record = AllTypesClass.createTestItem("EnhancedDelete", randomNum); + + // Put an item into an Amazon DynamoDB table. + table.putItem(record); + + // Get the item back from the table + Key key = Key.builder() + .partitionValue("EnhancedDelete").sortValue(randomNum) .build(); + + // Get the item by using the key. + AllTypesClass result = table.deleteItem(key); + assertEquals(result, record); + } + + @Test + public void TestUpdate() { + TableSchema schemaOnEncrypt = TableSchema.fromBean(AllTypesClass.class); + List allowedUnsignedAttributes = Collections.singletonList("doNothing"); + DynamoDbEnhancedClient enhancedClient = + initEnhancedClientWithInterceptor(schemaOnEncrypt, allowedUnsignedAttributes, null, null); + + DynamoDbTable table = enhancedClient.table(TEST_TABLE_NAME, schemaOnEncrypt); + + AllTypesClass record = AllTypesClass.createTestItem("EnhancedUpdate", 1); + + // Put an item into an Amazon DynamoDB table. + table.putItem(record); + + AllTypesClass doNothingValue = new AllTypesClass(); + doNothingValue.setDoNothing("updatedDoNothing"); + doNothingValue.setPartitionKey("EnhancedUpdate"); + doNothingValue.setSortKey(1); + + // Perform an update only on "doNothing" attribute + AllTypesClass result = table.updateItem( + (UpdateItemEnhancedRequest.Builder requestBuilder) + -> requestBuilder.item(doNothingValue) + .ignoreNulls(true) + ); + // EnhancedClient uses ReturnValues of ALL_NEW, so compare against put item with update + record.setDoNothing("updatedDoNothing"); + assertEquals(result, record); } @Test( diff --git a/DynamoDbEncryption/runtimes/java/src/test/java/software/amazon/cryptography/dbencryptionsdk/dynamodb/enhancedclient/validdatamodels/AllTypesClass.java b/DynamoDbEncryption/runtimes/java/src/test/java/software/amazon/cryptography/dbencryptionsdk/dynamodb/enhancedclient/validdatamodels/AllTypesClass.java new file mode 100644 index 000000000..ef50fd2fe --- /dev/null +++ b/DynamoDbEncryption/runtimes/java/src/test/java/software/amazon/cryptography/dbencryptionsdk/dynamodb/enhancedclient/validdatamodels/AllTypesClass.java @@ -0,0 +1,386 @@ +package software.amazon.cryptography.dbencryptionsdk.dynamodb.enhancedclient.validdatamodels; + +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbAttribute; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbIgnoreNulls; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; +import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSortKey; +import software.amazon.cryptography.dbencryptionsdk.dynamodb.enhancedclient.DynamoDbEncryptionDoNothing; +import software.amazon.cryptography.dbencryptionsdk.dynamodb.enhancedclient.DynamoDbEncryptionSignOnly; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +/** + * This class is used by the Enhanced Client Tests + */ + +@DynamoDbBean +public class AllTypesClass { + + private String partitionKey; + private int sortKey; + + // One attribute for every DDB attribute type and ENCRYPT_AND_SIGN/SIGN_ONLY pair + private String encryptString; + private String signString; + private Double encryptNum; + private Double signNum; + private ByteBuffer encryptBinary; + private ByteBuffer signBinary; + private Boolean encryptBool; + private Boolean signBool; + private String encryptExpectedNull; // This should always be null, define no setters + private String signExpectedNull; // This should always be null, define no setters + private List encryptList; + private List signList; + private Map encryptMap; + private Map signMap; + private Set encryptStringSet; + private Set signStringSet; + private Set encryptNumSet; + private Set signNumSet; + private Set encryptBinarySet; + private Set signBinarySet; + + // And one doNothing for good measure + private String doNothing; + + @DynamoDbPartitionKey + @DynamoDbAttribute(value = "partition_key") + public String getPartitionKey() { + return this.partitionKey; + } + + @DynamoDbSortKey + @DynamoDbAttribute(value = "sort_key") + public int getSortKey() { + return this.sortKey; + } + + @DynamoDbIgnoreNulls + public String getEncryptString() { + return this.encryptString; + } + + @DynamoDbEncryptionSignOnly + @DynamoDbIgnoreNulls + public String getSignString() { + return this.signString; + } + + @DynamoDbIgnoreNulls + public Double getEncryptNum() { + return encryptNum; + } + + @DynamoDbIgnoreNulls + @DynamoDbEncryptionSignOnly + public Double getSignNum() { + return signNum; + } + + @DynamoDbIgnoreNulls + public ByteBuffer getEncryptBinary() { + return encryptBinary; + } + + @DynamoDbIgnoreNulls + @DynamoDbEncryptionSignOnly + public ByteBuffer getSignBinary() { + return signBinary; + } + + @DynamoDbIgnoreNulls + public Boolean getEncryptBool() { + return encryptBool; + } + + @DynamoDbIgnoreNulls + @DynamoDbEncryptionSignOnly + public Boolean getSignBool() { + return signBool; + } + + // This should always return null + public String getEncryptExpectedNull() { + return encryptExpectedNull; + } + + // This should always return null + @DynamoDbEncryptionSignOnly + public String getSignExpectedNull() { + return signExpectedNull; + } + + @DynamoDbIgnoreNulls + public List getEncryptList() { + return encryptList; + } + + @DynamoDbIgnoreNulls + @DynamoDbEncryptionSignOnly + public List getSignList() { + return signList; + } + + @DynamoDbIgnoreNulls + public Map getEncryptMap() { + return encryptMap; + } + + @DynamoDbIgnoreNulls + @DynamoDbEncryptionSignOnly + public Map getSignMap() { + return signMap; + } + + @DynamoDbIgnoreNulls + public Set getEncryptStringSet() { + return encryptStringSet; + } + + @DynamoDbIgnoreNulls + @DynamoDbEncryptionSignOnly + public Set getSignStringSet() { + return signStringSet; + } + + @DynamoDbIgnoreNulls + public Set getEncryptNumSet() { + return encryptNumSet; + } + + @DynamoDbIgnoreNulls + @DynamoDbEncryptionSignOnly + public Set getSignNumSet() { + return signNumSet; + } + + @DynamoDbIgnoreNulls + public Set getEncryptBinarySet() { + return encryptBinarySet; + } + + @DynamoDbIgnoreNulls + @DynamoDbEncryptionSignOnly + public Set getSignBinarySet() { + return signBinarySet; + } + + @DynamoDbIgnoreNulls + @DynamoDbEncryptionDoNothing + public String getDoNothing() { + return this.doNothing; + } + + public void setPartitionKey(String partitionKey) { + this.partitionKey = partitionKey; + } + + public void setSortKey(int sortKey) { + this.sortKey = sortKey; + } + + public void setEncryptString(String encryptString) { + this.encryptString = encryptString; + } + + public void setSignString(String signString) { + this.signString = signString; + } + + public void setEncryptNum(Double encryptNum) { + this.encryptNum = encryptNum; + } + + public void setSignNum(Double signNum) { + this.signNum = signNum; + } + + public void setEncryptBinary(ByteBuffer encryptBinary) { + this.encryptBinary = encryptBinary; + } + + public void setSignBinary(ByteBuffer signBinary) { + this.signBinary = signBinary; + } + + public void setEncryptBool(Boolean encryptBool) { + this.encryptBool = encryptBool; + } + + public void setSignBool(Boolean signBool) { + this.signBool = signBool; + } + + public void setEncryptList(List encryptList) { + this.encryptList = encryptList; + } + + public void setSignList(List signList) { + this.signList = signList; + } + + public void setEncryptMap(Map encryptMap) { + this.encryptMap = encryptMap; + } + + public void setSignMap(Map signMap) { + this.signMap = signMap; + } + + public void setEncryptStringSet(Set encryptStringSet) { + this.encryptStringSet = encryptStringSet; + } + + public void setSignStringSet(Set signStringSet) { + this.signStringSet = signStringSet; + } + + public void setEncryptNumSet(Set encryptNumSet) { + this.encryptNumSet = encryptNumSet; + } + + public void setSignNumSet(Set signNumSet) { + this.signNumSet = signNumSet; + } + + public void setEncryptBinarySet(Set encryptBinarySet) { + this.encryptBinarySet = encryptBinarySet; + } + + public void setSignBinarySet(Set signBinarySet) { + this.signBinarySet = signBinarySet; + } + + public void setDoNothing(String doNothing) { + this.doNothing = doNothing; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + + if (obj.getClass() != this.getClass()) { + return false; + } + + final AllTypesClass other = (AllTypesClass) obj; + + if (!(Objects.equals(other.getPartitionKey(), this.partitionKey))) { + return false; + } + if (other.getSortKey() != this.sortKey) { + return false; + } + if (!(Objects.equals(other.getEncryptString(), this.encryptString))) { + return false; + } + if (!(Objects.equals(other.getSignString(), this.signString))) { + return false; + } + if (!(Objects.equals(other.getEncryptNum(), this.encryptNum))) { + return false; + } + if (!(Objects.equals(other.getSignNum(), this.signNum))) { + return false; + } + if (!(Objects.equals(other.getEncryptBinary(), this.encryptBinary))) { + return false; + } + if (!(Objects.equals(other.getSignBinary(), this.signBinary))) { + return false; + } + if (!(Objects.equals(other.getEncryptBool(), this.encryptBool))) { + return false; + } + if (!(Objects.equals(other.getSignBool(), this.signBool))) { + return false; + } + if (other.getEncryptExpectedNull() != null) { + return false; + } + if (other.getSignExpectedNull() != null) { + return false; + } + if (!(Objects.equals(other.getEncryptList(), this.encryptList))) { + return false; + } + if (!(Objects.equals(other.getSignList(), this.signList))) { + return false; + } + if (!(Objects.equals(other.getEncryptMap(), this.encryptMap))) { + return false; + } + if (!(Objects.equals(other.getSignMap(), this.signMap))) { + return false; + } + if (!(Objects.equals(other.getEncryptStringSet(), this.encryptStringSet))) { + return false; + } + if (!(Objects.equals(other.getSignStringSet(), this.signStringSet))) { + return false; + } + if (!(Objects.equals(other.getEncryptNumSet(), this.encryptNumSet))) { + return false; + } + if (!(Objects.equals(other.getSignNumSet(), this.signNumSet))) { + return false; + } + if (!(Objects.equals(other.getEncryptBinarySet(), this.encryptBinarySet))) { + return false; + } + if (!(Objects.equals(other.getSignBinarySet(), this.signBinarySet))) { + return false; + } + if (!(Objects.equals(other.getDoNothing(), this.doNothing))) { + return false; + } + + return true; + } + + public static AllTypesClass createTestItem(String partitionValue, int sortValue) { + AllTypesClass testItem = new AllTypesClass(); + testItem.setPartitionKey(partitionValue); + testItem.setSortKey(sortValue); + testItem.setEncryptString("encryptString"); + testItem.setSignString("signString"); + testItem.setEncryptNum(111.111); + testItem.setSignNum(999.999); + testItem.setEncryptBinary(StandardCharsets.UTF_8.encode("encryptBinary")); + testItem.setSignBinary(StandardCharsets.UTF_8.encode("sortBinary")); + testItem.setEncryptBool(true); + testItem.setSignBool(false); + testItem.setEncryptList(Arrays.asList("encrypt1", "encrypt2", "encrypt3")); + testItem.setSignList(Arrays.asList("sort1", "sort2", "sort3")); + testItem.setEncryptMap(Collections.singletonMap("encryptMap", 1)); + testItem.setSignMap(Collections.singletonMap("sortMap", 2)); + testItem.setEncryptStringSet(new HashSet<>(Arrays.asList("encrypt1", "encrypt2", "encrypt3"))); + testItem.setSignStringSet(new HashSet<>(Arrays.asList("sort1", "sort2", "sort3"))); + testItem.setEncryptNumSet(new HashSet<>(Arrays.asList(1, 2, 3))); + testItem.setSignNumSet(new HashSet<>(Arrays.asList(4, 5, 6))); + testItem.setEncryptBinarySet(new HashSet<>(Arrays.asList( + StandardCharsets.UTF_8.encode("encrypt1"), + StandardCharsets.UTF_8.encode("encrypt2"), + StandardCharsets.UTF_8.encode("encrypt3") + ))); + testItem.setSignBinarySet(new HashSet<>(Arrays.asList( + StandardCharsets.UTF_8.encode("sort1"), + StandardCharsets.UTF_8.encode("sort2"), + StandardCharsets.UTF_8.encode("sort3") + ))); + testItem.setDoNothing("doNothing"); + return testItem; + } +} diff --git a/Examples/runtimes/java/DynamoDbEncryption/build.gradle.kts b/Examples/runtimes/java/DynamoDbEncryption/build.gradle.kts index d2a870df9..a901e27c7 100644 --- a/Examples/runtimes/java/DynamoDbEncryption/build.gradle.kts +++ b/Examples/runtimes/java/DynamoDbEncryption/build.gradle.kts @@ -57,7 +57,7 @@ repositories { } dependencies { - implementation("software.amazon.cryptography:aws-database-encryption-sdk-dynamodb:3.1.1") + implementation("software.amazon.cryptography:aws-database-encryption-sdk-dynamodb:3.1.2") implementation("software.amazon.cryptography:aws-cryptographic-material-providers:1.0.0") implementation(platform("software.amazon.awssdk:bom:2.19.1")) diff --git a/Examples/runtimes/java/Migration/DDBECToAWSDBE/build.gradle.kts b/Examples/runtimes/java/Migration/DDBECToAWSDBE/build.gradle.kts index dba71bfa3..11e4a65dc 100644 --- a/Examples/runtimes/java/Migration/DDBECToAWSDBE/build.gradle.kts +++ b/Examples/runtimes/java/Migration/DDBECToAWSDBE/build.gradle.kts @@ -56,7 +56,7 @@ repositories { } dependencies { - implementation("software.amazon.cryptography:aws-database-encryption-sdk-dynamodb:3.1.1") + implementation("software.amazon.cryptography:aws-database-encryption-sdk-dynamodb:3.1.2") implementation("software.amazon.cryptography:aws-cryptographic-material-providers:1.0.0") implementation(platform("software.amazon.awssdk:bom:2.19.1")) diff --git a/Examples/runtimes/java/Migration/PlaintextToAWSDBE/build.gradle.kts b/Examples/runtimes/java/Migration/PlaintextToAWSDBE/build.gradle.kts index c98b992b0..26ba74824 100644 --- a/Examples/runtimes/java/Migration/PlaintextToAWSDBE/build.gradle.kts +++ b/Examples/runtimes/java/Migration/PlaintextToAWSDBE/build.gradle.kts @@ -56,7 +56,7 @@ repositories { } dependencies { - implementation("software.amazon.cryptography:aws-database-encryption-sdk-dynamodb:3.1.1") + implementation("software.amazon.cryptography:aws-database-encryption-sdk-dynamodb:3.1.2") implementation("software.amazon.cryptography:aws-cryptographic-material-providers:1.0.0") implementation(platform("software.amazon.awssdk:bom:2.19.1")) diff --git a/TestVectors/runtimes/java/build.gradle.kts b/TestVectors/runtimes/java/build.gradle.kts index d5e379c77..e8dcefa67 100644 --- a/TestVectors/runtimes/java/build.gradle.kts +++ b/TestVectors/runtimes/java/build.gradle.kts @@ -76,7 +76,7 @@ dependencies { implementation("org.dafny:DafnyRuntime:4.1.0") implementation("software.amazon.smithy.dafny:conversion:0.1") implementation("software.amazon.cryptography:aws-cryptographic-material-providers:1.0.0") - implementation("software.amazon.cryptography:aws-database-encryption-sdk-dynamodb:3.1.1") + implementation("software.amazon.cryptography:aws-database-encryption-sdk-dynamodb:3.1.2") implementation("software.amazon.cryptography:TestAwsCryptographicMaterialProviders:1.0-SNAPSHOT") implementation(platform("software.amazon.awssdk:bom:2.20.138")) diff --git a/specification/dynamodb-encryption-client/ddb-sdk-integration.md b/specification/dynamodb-encryption-client/ddb-sdk-integration.md index 0cb2716fb..3f48a692a 100644 --- a/specification/dynamodb-encryption-client/ddb-sdk-integration.md +++ b/specification/dynamodb-encryption-client/ddb-sdk-integration.md @@ -124,6 +124,9 @@ MUST have the following modified behavior: - [Encrypt before BatchWriteItem](#encrypt-before-batchwriteitem) - [Encrypt before TransactWriteItems](#encrypt-before-transactwriteitems) - [Decrypt after GetItem](#decrypt-after-getitem) +- [Decrypt after PutItem](#decrypt-after-putitem) +- [Decrypt after UpdateItem](#decrypt-after-updateitem) +- [Decrypt after DeleteItem](#decrypt-after-deleteitem) - [Decrypt after BatchGetItem](#decrypt-after-batchgetitem) - [Decrypt after Scan](#decrypt-after-scan) - [Decrypt after Query](#decrypt-after-query) @@ -334,6 +337,92 @@ Beacons MUST be [removed](ddb-support.md#removebeacons) from the result. The GetItem response's `Item` field MUST be replaced by the encrypted DynamoDb Item outputted above. +### Decrypt after PutItem + +After a [PutItem](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_PutItem.html) +call is made to DynamoDB, +the resulting response MUST be modified before +being returned to the caller if: +- there exists an Item Encryptor specified within the + [DynamoDB Encryption Client Config](#dynamodb-encryption-client-configuration) + with a [DynamoDB Table Name](./ddb-item-encryptor.md#dynamodb-table-name) + equal to the `TableName` on the PutItem request. +- the response contains [Attributes](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_PutItem.html#DDB-PutItem-response-Attributes). + The response will contain Attributes if the related PutItem request's + [ReturnValues](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_PutItem.html#DDB-PutItem-request-ReturnValues) + had a value of `ALL_OLD` and the PutItem call replaced a pre-existing item. + +In this case, the [Item Encryptor](./ddb-item-encryptor.md) MUST perform +[Decrypt Item](./decrypt-item.md) where the input +[DynamoDB Item](./decrypt-item.md#dynamodb-item) +is the `Attributes` field in the original response + +Beacons MUST be [removed](ddb-support.md#removebeacons) from the result. + +The PutItem response's `Attributes` field MUST be +replaced by the encrypted DynamoDb Item outputted above. + +### Decrypt after DeleteItem + +After a [DeleteItem](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_DeleteItem.html) +call is made to DynamoDB, +the resulting response MUST be modified before +being returned to the caller if: +- there exists an Item Encryptor specified within the + [DynamoDB Encryption Client Config](#dynamodb-encryption-client-configuration) + with a [DynamoDB Table Name](./ddb-item-encryptor.md#dynamodb-table-name) + equal to the `TableName` on the DeleteItem request. +- the response contains [Attributes](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_DeleteItem.html#DDB-DeleteItem-response-Attributes). + The response will contain Attributes if the related DeleteItem request's + [ReturnValues](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_DeleteItem.html#DDB-DeleteItem-request-ReturnValues) + had a value of `ALL_OLD` and an item was deleted. + +In this case, the [Item Encryptor](./ddb-item-encryptor.md) MUST perform +[Decrypt Item](./decrypt-item.md) where the input +[DynamoDB Item](./decrypt-item.md#dynamodb-item) +is the `Attributes` field in the original response + +Beacons MUST be [removed](ddb-support.md#removebeacons) from the result. + +The DeleteItem response's `Attributes` field MUST be +replaced by the encrypted DynamoDb Item outputted above. + +### Decrypt after UpdateItem + +After a [UpdateItem](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_UpdateItem.html) +call is made to DynamoDB, +the resulting response MUST be modified before +being returned to the caller if: +- there exists an Item Encryptor specified within the + [DynamoDB Encryption Client Config](#dynamodb-encryption-client-configuration) + with a [DynamoDB Table Name](./ddb-item-encryptor.md#dynamodb-table-name) + equal to the `TableName` on the UpdateItem request. +- the response contains [Attributes](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_UpdateItem.html#DDB-UpdateItem-response-Attributes). +- the original UpdateItem request had a + [ReturnValues](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_UpdateItem.html#DDB-UpdateItem-request-ReturnValues) + with a value of `ALL_OLD` or `ALL_NEW`. + +In this case, the [Item Encryptor](./ddb-item-encryptor.md) MUST perform +[Decrypt Item](./decrypt-item.md) where the input +[DynamoDB Item](./decrypt-item.md#dynamodb-item) +is the `Attributes` field in the original response + +Beacons MUST be [removed](ddb-support.md#removebeacons) from the result. + +The UpdateItem response's `Attributes` field MUST be +replaced by the encrypted DynamoDb Item outputted above. + +In all other cases, the UpdateItem response MUST NOT be modified. + +Additionally, if a value of `UPDATED_OLD` or `UPDATED_NEW` was used, +and any Attributes in the response are authenticated +per the [DynamoDB Encryption Client Config](#dynamodb-encryption-client-configuration), +an error MUST be raised. +Given that we [validate UpdateItem requests](#validate-before-updateitem), +and thus updates will not modify any signed field, +an error here would indicate a bug in +our library or a bug within DynamoDB. + ### Decrypt after BatchGetItem After a [BatchGetItem](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_BatchGetItem.html)