Skip to content

Commit ea42115

Browse files
authored
fix: Decrypt attributes returned by all DDB APIs (#578)
1 parent 5401b04 commit ea42115

File tree

18 files changed

+1082
-63
lines changed

18 files changed

+1082
-63
lines changed

CHANGELOG.md

+8
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# Changelog
22

3+
## 3.1.2 2023-11-13
4+
5+
### Fix
6+
7+
Fixed an issue where, when using the DynamoDbEncryptionInterceptor,
8+
an encrypted item in the Attributes field of a DeleteItem, PutItem, or UpdateItem
9+
response was passed through unmodified instead of being decrypted.
10+
311
## 3.1.1 2023-11-07
412

513
### Fix

DecryptWithPermute/runtimes/java/build.gradle.kts

+1-1
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ repositories {
6868
val dynamodb by configurations.creating
6969

7070
dependencies {
71-
implementation("software.amazon.cryptography:aws-database-encryption-sdk-dynamodb:3.1.1")
71+
implementation("software.amazon.cryptography:aws-database-encryption-sdk-dynamodb:3.1.2")
7272
implementation("org.dafny:DafnyRuntime:4.1.0")
7373
implementation("software.amazon.smithy.dafny:conversion:0.1")
7474
implementation("software.amazon.cryptography:aws-cryptographic-material-providers:1.0.0")

DynamoDbEncryption/dafny/DynamoDbEncryptionTransforms/src/DeleteItemTransform.dfy

+73-2
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,79 @@ module DeleteItemTransform {
6666

6767
method Output(config: Config, input: DeleteItemOutputTransformInput)
6868
returns (output: Result<DeleteItemOutputTransformOutput, Error>)
69-
ensures output.Success? && output.value.transformedOutput == input.sdkOutput
69+
70+
//= specification/dynamodb-encryption-client/ddb-sdk-integration.md#decrypt-after-deleteitem
71+
//= type=implication
72+
//# After a [DeleteItem](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_DeleteItem.html)
73+
//# call is made to DynamoDB,
74+
//# the resulting response MUST be modified before
75+
//# being returned to the caller if:
76+
// - there exists an Item Encryptor specified within the
77+
// [DynamoDB Encryption Client Config](#dynamodb-encryption-client-configuration)
78+
// with a [DynamoDB Table Name](./ddb-item-encryptor.md#dynamodb-table-name)
79+
// equal to the `TableName` on the DeleteItem request.
80+
// - the response contains [Attributes](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_DeleteItem.html#DDB-DeleteItem-response-Attributes).
81+
// The response will contain Attributes if the related DeleteItem request's
82+
// [ReturnValues](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_DeleteItem.html#DDB-DeleteItem-request-ReturnValues)
83+
// had a value of `ALL_OLD` and an item was deleted.
84+
ensures (
85+
&& output.Success?
86+
&& input.originalInput.TableName in config.tableEncryptionConfigs
87+
&& input.sdkOutput.Attributes.Some?
88+
) ==>
89+
&& var tableConfig := config.tableEncryptionConfigs[input.originalInput.TableName];
90+
&& var oldHistory := old(tableConfig.itemEncryptor.History.DecryptItem);
91+
&& var newHistory := tableConfig.itemEncryptor.History.DecryptItem;
92+
93+
&& |newHistory| == |oldHistory|+1
94+
&& Seq.Last(newHistory).output.Success?
95+
96+
//= specification/dynamodb-encryption-client/ddb-sdk-integration.md#decrypt-after-deleteitem
97+
//= type=implication
98+
//# In this case, the [Item Encryptor](./ddb-item-encryptor.md) MUST perform
99+
//# [Decrypt Item](./decrypt-item.md) where the input
100+
//# [DynamoDB Item](./decrypt-item.md#dynamodb-item)
101+
//# is the `Attributes` field in the original response
102+
&& Seq.Last(newHistory).input.encryptedItem == input.sdkOutput.Attributes.value
103+
104+
//= specification/dynamodb-encryption-client/ddb-sdk-integration.md#decrypt-after-deleteitem
105+
//= type=implication
106+
//# Beacons MUST be [removed](ddb-support.md#removebeacons) from the result.
107+
&& RemoveBeacons(tableConfig, Seq.Last(newHistory).output.value.plaintextItem).Success?
108+
&& var item := RemoveBeacons(tableConfig, Seq.Last(newHistory).output.value.plaintextItem).value;
109+
110+
//= specification/dynamodb-encryption-client/ddb-sdk-integration.md#decrypt-after-deleteitem
111+
//= type=implication
112+
//# The DeleteItem response's `Attributes` field MUST be
113+
//# replaced by the encrypted DynamoDb Item outputted above.
114+
&& output.value.transformedOutput.Attributes.Some?
115+
&& (item == output.value.transformedOutput.Attributes.value)
116+
117+
// Passthrough the response if the above specification is not met
118+
ensures (
119+
&& output.Success?
120+
&& (
121+
|| input.originalInput.TableName !in config.tableEncryptionConfigs
122+
|| input.sdkOutput.Attributes.None?
123+
)
124+
) ==>
125+
output.value.transformedOutput == input.sdkOutput
126+
127+
requires ValidConfig?(config)
128+
ensures ValidConfig?(config)
129+
modifies ModifiesConfig(config)
70130
{
71-
return Success(DeleteItemOutputTransformOutput(transformedOutput := input.sdkOutput));
131+
var tableName := input.originalInput.TableName;
132+
if tableName !in config.tableEncryptionConfigs || input.sdkOutput.Attributes.None?
133+
{
134+
return Success(DeleteItemOutputTransformOutput(transformedOutput := input.sdkOutput));
135+
}
136+
var tableConfig := config.tableEncryptionConfigs[tableName];
137+
var decryptRes := tableConfig.itemEncryptor.DecryptItem(
138+
EncTypes.DecryptItemInput(encryptedItem:=input.sdkOutput.Attributes.value)
139+
);
140+
var decrypted :- MapError(decryptRes);
141+
var item :- RemoveBeacons(tableConfig, decrypted.plaintextItem);
142+
return Success(DeleteItemOutputTransformOutput(transformedOutput := input.sdkOutput.(Attributes := Some(item))));
72143
}
73144
}

DynamoDbEncryption/dafny/DynamoDbEncryptionTransforms/src/DynamoDbMiddlewareSupport.dfy

+9
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,15 @@ module DynamoDbMiddlewareSupport {
3030
.MapFailure(e => E(e))
3131
}
3232

33+
// IsSigned returned whether this attribute is signed according to this config
34+
predicate method {:opaque} IsSigned(
35+
config : ValidTableConfig,
36+
attr : string
37+
)
38+
{
39+
BS.IsSigned(config.itemEncryptor.config.attributeActionsOnEncrypt, attr)
40+
}
41+
3342
// TestConditionExpression fails if a condition expression is not suitable for the
3443
// given encryption schema.
3544
// Generally this means no encrypted attribute is referenced.

DynamoDbEncryption/dafny/DynamoDbEncryptionTransforms/src/PutItemTransform.dfy

+73-2
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,79 @@ module PutItemTransform {
8383

8484
method Output(config: Config, input: PutItemOutputTransformInput)
8585
returns (output: Result<PutItemOutputTransformOutput, Error>)
86-
ensures output.Success? && output.value.transformedOutput == input.sdkOutput
86+
87+
//= specification/dynamodb-encryption-client/ddb-sdk-integration.md#decrypt-after-putitem
88+
//= type=implication
89+
//# After a [PutItem](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_PutItem.html)
90+
//# call is made to DynamoDB,
91+
//# the resulting response MUST be modified before
92+
//# being returned to the caller if:
93+
// - there exists an Item Encryptor specified within the
94+
// [DynamoDB Encryption Client Config](#dynamodb-encryption-client-configuration)
95+
// with a [DynamoDB Table Name](./ddb-item-encryptor.md#dynamodb-table-name)
96+
// equal to the `TableName` on the PutItem request.
97+
// - the response contains [Attributes](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_PutItem.html#DDB-PutItem-response-Attributes).
98+
// The response will contain Attributes if the related PutItem request's
99+
// [ReturnValues](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_PutItem.html#DDB-PutItem-request-ReturnValues)
100+
// had a value of `ALL_OLD` and the PutItem call replaced a pre-existing item.
101+
ensures (
102+
&& output.Success?
103+
&& input.originalInput.TableName in config.tableEncryptionConfigs
104+
&& input.sdkOutput.Attributes.Some?
105+
) ==>
106+
&& var tableConfig := config.tableEncryptionConfigs[input.originalInput.TableName];
107+
&& var oldHistory := old(tableConfig.itemEncryptor.History.DecryptItem);
108+
&& var newHistory := tableConfig.itemEncryptor.History.DecryptItem;
109+
110+
&& |newHistory| == |oldHistory|+1
111+
&& Seq.Last(newHistory).output.Success?
112+
113+
//= specification/dynamodb-encryption-client/ddb-sdk-integration.md#decrypt-after-putitem
114+
//= type=implication
115+
//# In this case, the [Item Encryptor](./ddb-item-encryptor.md) MUST perform
116+
//# [Decrypt Item](./decrypt-item.md) where the input
117+
//# [DynamoDB Item](./decrypt-item.md#dynamodb-item)
118+
//# is the `Attributes` field in the original response
119+
&& Seq.Last(newHistory).input.encryptedItem == input.sdkOutput.Attributes.value
120+
121+
//= specification/dynamodb-encryption-client/ddb-sdk-integration.md#decrypt-after-putitem
122+
//= type=implication
123+
//# Beacons MUST be [removed](ddb-support.md#removebeacons) from the result.
124+
&& RemoveBeacons(tableConfig, Seq.Last(newHistory).output.value.plaintextItem).Success?
125+
&& var item := RemoveBeacons(tableConfig, Seq.Last(newHistory).output.value.plaintextItem).value;
126+
127+
//= specification/dynamodb-encryption-client/ddb-sdk-integration.md#decrypt-after-putitem
128+
//= type=implication
129+
//# The PutItem response's `Attributes` field MUST be
130+
//# replaced by the encrypted DynamoDb Item outputted above.
131+
&& output.value.transformedOutput.Attributes.Some?
132+
&& (item == output.value.transformedOutput.Attributes.value)
133+
134+
// Passthrough the response if the above specification is not met
135+
ensures (
136+
&& output.Success?
137+
&& (
138+
|| input.originalInput.TableName !in config.tableEncryptionConfigs
139+
|| input.sdkOutput.Attributes.None?
140+
)
141+
) ==>
142+
output.value.transformedOutput == input.sdkOutput
143+
144+
requires ValidConfig?(config)
145+
ensures ValidConfig?(config)
146+
modifies ModifiesConfig(config)
87147
{
88-
return Success(PutItemOutputTransformOutput(transformedOutput := input.sdkOutput));
148+
var tableName := input.originalInput.TableName;
149+
if tableName !in config.tableEncryptionConfigs || input.sdkOutput.Attributes.None?
150+
{
151+
return Success(PutItemOutputTransformOutput(transformedOutput := input.sdkOutput));
152+
}
153+
var tableConfig := config.tableEncryptionConfigs[tableName];
154+
var decryptRes := tableConfig.itemEncryptor.DecryptItem(
155+
EncTypes.DecryptItemInput(encryptedItem:=input.sdkOutput.Attributes.value)
156+
);
157+
var decrypted :- MapError(decryptRes);
158+
var item :- RemoveBeacons(tableConfig, decrypted.plaintextItem);
159+
return Success(PutItemOutputTransformOutput(transformedOutput := input.sdkOutput.(Attributes := Some(item))));
89160
}
90161
}

DynamoDbEncryption/dafny/DynamoDbEncryptionTransforms/src/UpdateItemTransform.dfy

+136-2
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,142 @@ module UpdateItemTransform {
6565

6666
method Output(config: Config, input: UpdateItemOutputTransformInput)
6767
returns (output: Result<UpdateItemOutputTransformOutput, Error>)
68-
ensures output.Success? && output.value.transformedOutput == input.sdkOutput
68+
69+
//= specification/dynamodb-encryption-client/ddb-sdk-integration.md#decrypt-after-updateitem
70+
//= type=implication
71+
//# After a [UpdateItem](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_UpdateItem.html)
72+
//# call is made to DynamoDB,
73+
//# the resulting response MUST be modified before
74+
//# being returned to the caller if:
75+
// - there exists an Item Encryptor specified within the
76+
// [DynamoDB Encryption Client Config](#dynamodb-encryption-client-configuration)
77+
// with a [DynamoDB Table Name](./ddb-item-encryptor.md#dynamodb-table-name)
78+
// equal to the `TableName` on the UpdateItem request.
79+
// - the response contains [Attributes](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_UpdateItem.html#DDB-UpdateItem-response-Attributes).
80+
// - the original UpdateItem request had a
81+
// [ReturnValues](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_UpdateItem.html#DDB-UpdateItem-request-ReturnValues)
82+
// with a value of `ALL_OLD` or `ALL_NEW`.
83+
ensures (
84+
&& output.Success?
85+
&& input.originalInput.TableName in config.tableEncryptionConfigs
86+
&& input.sdkOutput.Attributes.Some?
87+
&& input.originalInput.ReturnValues.Some?
88+
&& (
89+
|| input.originalInput.ReturnValues.value.ALL_OLD?
90+
|| input.originalInput.ReturnValues.value.ALL_NEW?
91+
)
92+
) ==>
93+
&& var tableConfig := config.tableEncryptionConfigs[input.originalInput.TableName];
94+
&& var oldHistory := old(tableConfig.itemEncryptor.History.DecryptItem);
95+
&& var newHistory := tableConfig.itemEncryptor.History.DecryptItem;
96+
97+
&& |newHistory| == |oldHistory|+1
98+
&& Seq.Last(newHistory).output.Success?
99+
100+
//= specification/dynamodb-encryption-client/ddb-sdk-integration.md#decrypt-after-updateitem
101+
//= type=implication
102+
//# In this case, the [Item Encryptor](./ddb-item-encryptor.md) MUST perform
103+
//# [Decrypt Item](./decrypt-item.md) where the input
104+
//# [DynamoDB Item](./decrypt-item.md#dynamodb-item)
105+
//# is the `Attributes` field in the original response
106+
&& Seq.Last(newHistory).input.encryptedItem == input.sdkOutput.Attributes.value
107+
108+
//= specification/dynamodb-encryption-client/ddb-sdk-integration.md#decrypt-after-updateitem
109+
//= type=implication
110+
//# Beacons MUST be [removed](ddb-support.md#removebeacons) from the result.
111+
&& RemoveBeacons(tableConfig, Seq.Last(newHistory).output.value.plaintextItem).Success?
112+
&& var item := RemoveBeacons(tableConfig, Seq.Last(newHistory).output.value.plaintextItem).value;
113+
114+
//= specification/dynamodb-encryption-client/ddb-sdk-integration.md#decrypt-after-updateitem
115+
//= type=implication
116+
//# The UpdateItem response's `Attributes` field MUST be
117+
//# replaced by the encrypted DynamoDb Item outputted above.
118+
&& output.value.transformedOutput.Attributes.Some?
119+
&& (item == output.value.transformedOutput.Attributes.value)
120+
121+
//= specification/dynamodb-encryption-client/ddb-sdk-integration.md#decrypt-after-updateitem
122+
//= type=implication
123+
//# In all other cases, the UpdateItem response MUST NOT be modified.
124+
ensures (
125+
&& output.Success?
126+
&& (
127+
|| input.originalInput.TableName !in config.tableEncryptionConfigs
128+
|| input.sdkOutput.Attributes.None?
129+
)
130+
) ==> (
131+
&& output.value.transformedOutput == input.sdkOutput
132+
)
133+
134+
ensures (
135+
&& output.Success?
136+
&& input.originalInput.TableName in config.tableEncryptionConfigs
137+
&& input.sdkOutput.Attributes.Some?
138+
&& (input.originalInput.ReturnValues.Some? ==> (
139+
|| input.originalInput.ReturnValues.value.UPDATED_NEW?
140+
|| input.originalInput.ReturnValues.value.UPDATED_OLD?
141+
)
142+
)
143+
) ==> (
144+
&& var tableConfig := config.tableEncryptionConfigs[input.originalInput.TableName];
145+
&& output.value.transformedOutput == input.sdkOutput
146+
&& forall k <- input.sdkOutput.Attributes.value.Keys :: !IsSigned(tableConfig, k)
147+
)
148+
149+
//= specification/dynamodb-encryption-client/ddb-sdk-integration.md#decrypt-after-updateitem
150+
//= type=implication
151+
//# Additionally, if a value of `UPDATED_OLD` or `UPDATED_NEW` was used,
152+
//# and any Attributes in the response are authenticated
153+
//# per the [DynamoDB Encryption Client Config](#dynamodb-encryption-client-configuration),
154+
//# an error MUST be raised.
155+
ensures (
156+
&& input.originalInput.TableName in config.tableEncryptionConfigs
157+
&& var tableConfig := config.tableEncryptionConfigs[input.originalInput.TableName];
158+
&& input.sdkOutput.Attributes.Some?
159+
&& (input.originalInput.ReturnValues.Some? ==> (
160+
|| input.originalInput.ReturnValues.value.UPDATED_NEW?
161+
|| input.originalInput.ReturnValues.value.UPDATED_OLD?
162+
)
163+
)
164+
&& exists k <- input.sdkOutput.Attributes.value.Keys :: IsSigned(tableConfig, k)
165+
) ==>
166+
output.Failure?
167+
168+
requires ValidConfig?(config)
169+
ensures ValidConfig?(config)
170+
modifies ModifiesConfig(config)
69171
{
70-
return Success(UpdateItemOutputTransformOutput(transformedOutput := input.sdkOutput));
172+
var tableName := input.originalInput.TableName;
173+
174+
if
175+
|| tableName !in config.tableEncryptionConfigs
176+
|| input.sdkOutput.Attributes.None?
177+
{
178+
return Success(UpdateItemOutputTransformOutput(transformedOutput := input.sdkOutput));
179+
}
180+
181+
var tableConfig := config.tableEncryptionConfigs[tableName];
182+
var attributes := input.sdkOutput.Attributes.value;
183+
184+
if !(
185+
&& input.originalInput.ReturnValues.Some?
186+
&& (
187+
|| input.originalInput.ReturnValues.value.ALL_NEW?
188+
|| input.originalInput.ReturnValues.value.ALL_OLD?)
189+
)
190+
{
191+
// This error should not be possible to reach if we assume the DDB API contract is correct.
192+
// We include this runtime check for defensive purposes.
193+
:- Need(forall k <- attributes.Keys :: !IsSigned(tableConfig, k),
194+
E("UpdateItems response contains signed attributes, but does not include the entire item which is required for verification."));
195+
196+
return Success(UpdateItemOutputTransformOutput(transformedOutput := input.sdkOutput));
197+
}
198+
199+
var decryptRes := tableConfig.itemEncryptor.DecryptItem(
200+
EncTypes.DecryptItemInput(encryptedItem:=attributes)
201+
);
202+
var decrypted :- MapError(decryptRes);
203+
var item :- RemoveBeacons(tableConfig, decrypted.plaintextItem);
204+
return Success(UpdateItemOutputTransformOutput(transformedOutput := input.sdkOutput.(Attributes := Some(item))));
71205
}
72206
}

DynamoDbEncryption/runtimes/java/build.gradle.kts

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ plugins {
1212
}
1313

1414
group = "software.amazon.cryptography"
15-
version = "3.1.1"
15+
version = "3.1.2"
1616
description = "Aws Database Encryption Sdk for DynamoDb Java"
1717

1818
java {

DynamoDbEncryption/runtimes/java/src/main/java/software/amazon/cryptography/dbencryptionsdk/dynamodb/DynamoDbEncryptionInterceptor.java

+11
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,17 @@ public SdkResponse modifyResponse(Context.ModifyResponse context, ExecutionAttri
252252
.sdkHttpResponse(originalResponse.sdkHttpResponse())
253253
.build();
254254
break;
255+
} case "DeleteItem": {
256+
DeleteItemResponse transformedResponse = transformer.DeleteItemOutputTransform(
257+
DeleteItemOutputTransformInput.builder()
258+
.sdkOutput((DeleteItemResponse) originalResponse)
259+
.originalInput((DeleteItemRequest) originalRequest)
260+
.build()).transformedOutput();
261+
outgoingResponse = transformedResponse.toBuilder()
262+
.responseMetadata(((DeleteItemResponse) originalResponse).responseMetadata())
263+
.sdkHttpResponse(originalResponse.sdkHttpResponse())
264+
.build();
265+
break;
255266
} case "ExecuteStatement": {
256267
ExecuteStatementResponse transformedResponse = transformer.ExecuteStatementOutputTransform(
257268
ExecuteStatementOutputTransformInput.builder()

DynamoDbEncryption/runtimes/java/src/main/sdkv1/com/amazonaws/services/dynamodbv2/datamodeling/encryption/DoNotEncrypt.java

+4
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@
2121
import java.lang.annotation.Target;
2222

2323
/**
24+
* Warning: This annotation only works with the DynamoDBMapper for AWS SDK for Java 1.x.
25+
* If you are using the AWS SDK for Java 2.x, use @DynamoDbEncryptionSignOnly instead.
26+
*
2427
* Prevents the associated item (class or attribute) from being encrypted.
2528
*
2629
* <p>For guidance on performing a safe data model change procedure, please see <a
@@ -32,4 +35,5 @@
3235
@DynamoDB
3336
@Target(value = {ElementType.TYPE, ElementType.METHOD, ElementType.FIELD})
3437
@Retention(value = RetentionPolicy.RUNTIME)
38+
@Deprecated
3539
public @interface DoNotEncrypt {}

0 commit comments

Comments
 (0)