Skip to content

Commit

Permalink
chore: include bad item keys in query errors (#1244)
Browse files Browse the repository at this point in the history
* chore: include bad item keys in query errors
  • Loading branch information
ajewellamz authored Aug 6, 2024
1 parent 1487d7e commit 07bba8b
Show file tree
Hide file tree
Showing 6 changed files with 255 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ module DdbMiddlewareConfig {
import EncTypes = AwsCryptographyDbEncryptionSdkDynamoDbItemEncryptorTypes
import DDBE = AwsCryptographyDbEncryptionSdkDynamoDbTypes
import SearchableEncryptionInfo
import DDB = ComAmazonawsDynamodbTypes
import HexStrings

datatype TableConfig = TableConfig(
physicalTableName: ComAmazonawsDynamodbTypes.TableName,
Expand Down Expand Up @@ -86,6 +88,35 @@ module DdbMiddlewareConfig {
tableEncryptionConfigs: map<string, ValidTableConfig>
)

function method AttrToString(attr : DDB.AttributeValue) : string
{
if attr.S? then
attr.S
else if attr.N? then
attr.N
else if attr.B? then
HexStrings.ToHexString(attr.B)
else
"unexpected key type"
}

// return human readable string containing primary keys
function method KeyString(config : ValidTableConfig, item : DDB.AttributeMap) : string
{
var partition :=
if config.partitionKeyName in item then
config.partitionKeyName + " = " + AttrToString(item[config.partitionKeyName])
else
"";
var sort :=
if config.sortKeyName.Some? && config.sortKeyName.value in item then
"\n" + config.sortKeyName.value + " = " + AttrToString(item[config.sortKeyName.value])
else
"";

partition + sort
}

function method MapError<T>(r : Result<T, EncTypes.Error>) : Result<T, Error> {
r.MapFailure(e => AwsCryptographyDbEncryptionSdkDynamoDbItemEncryptor(e))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,16 +81,29 @@ module QueryTransform {
if keyId.KeyId? {
keyIdUtf8 :- UTF8.Encode(keyId.value).MapFailure(e => E(e));
}
ghost var originalHistory := tableConfig.itemEncryptor.History.DecryptItem;
ghost var historySize := |originalHistory|;

var decryptErrors : seq<Error> := [];
var lastRealError := -1;

for x := 0 to |encryptedItems|
invariant lastRealError == -1 || lastRealError < |decryptErrors|
{
//= specification/dynamodb-encryption-client/ddb-sdk-integration.md#decrypt-after-query
//# Each of these entries on the original response MUST be replaced
//# with the resulting decrypted [DynamoDB Item](./decrypt-item.md#dynamodb-item-1).
var decryptInput := EncTypes.DecryptItemInput(encryptedItem := encryptedItems[x]);
var decryptRes := tableConfig.itemEncryptor.DecryptItem(decryptInput);
var decrypted :- MapError(decryptRes);
if decryptRes.Failure? {
var error := AwsCryptographyDbEncryptionSdkDynamoDbItemEncryptor(decryptRes.error);
var context := E(KeyString(tableConfig, encryptedItems[x]));
if lastRealError == -1 || error != decryptErrors[lastRealError] {
lastRealError := |decryptErrors|;
decryptErrors := decryptErrors + [error];
}
decryptErrors := decryptErrors + [context];
continue;
}
var decrypted := decryptRes.value;

// If the decrypted result was plaintext, i.e. has no parsedHeader
// then this is expected IFF the table config allows plaintext read
Expand All @@ -111,7 +124,9 @@ module QueryTransform {
decryptedItems := decryptedItems + [decrypted.plaintextItem];
}
}

if |decryptErrors| != 0 {
return Failure(CollectionOfErrors(decryptErrors, message := "Error(s) found decrypting Query results."));
}
//= specification/dynamodb-encryption-client/ddb-sdk-integration.md#decrypt-after-query
//# The resulting decrypted response MUST be [filtered](ddb-support.md#queryoutputforbeacons) from the result.
var decryptedOutput := input.sdkOutput.(Items := Some(decryptedItems));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,11 @@ module ScanTransform {
if keyId.KeyId? {
keyIdUtf8 :- UTF8.Encode(keyId.value).MapFailure(e => E(e));
}
var decryptErrors : seq<Error> := [];
var lastRealError := -1;

for x := 0 to |encryptedItems|
invariant lastRealError == -1 || lastRealError < |decryptErrors|
{
//= specification/dynamodb-encryption-client/ddb-sdk-integration.md#decrypt-after-scan
//# Each of these entries on the original response MUST be replaced
Expand All @@ -88,7 +92,17 @@ module ScanTransform {

var decryptInput := EncTypes.DecryptItemInput(encryptedItem := encryptedItems[x]);
var decryptRes := tableConfig.itemEncryptor.DecryptItem(decryptInput);
var decrypted :- MapError(decryptRes);
if decryptRes.Failure? {
var error := AwsCryptographyDbEncryptionSdkDynamoDbItemEncryptor(decryptRes.error);
var context := E(KeyString(tableConfig, encryptedItems[x]));
if lastRealError == -1 || error != decryptErrors[lastRealError] {
lastRealError := |decryptErrors|;
decryptErrors := decryptErrors + [error];
}
decryptErrors := decryptErrors + [context];
continue;
}
var decrypted := decryptRes.value;

// If the decrypted result was plaintext, i.e. has no parsedHeader
// then this is expected IFF the table config allows plaintext read
Expand All @@ -109,6 +123,9 @@ module ScanTransform {
decryptedItems := decryptedItems + [decrypted.plaintextItem];
}
}
if |decryptErrors| != 0 {
return Failure(CollectionOfErrors(decryptErrors, message := "Error(s) found decrypting Scan results."));
}

//= specification/dynamodb-encryption-client/ddb-sdk-integration.md#decrypt-after-scan
//# The resulting decrypted response MUST be [filtered](ddb-support.md#scanoutputforbeacons) from the result.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ module PutItemTransformTest {
const BasicItem : DDB.AttributeMap := map["bar" := DDB.AttributeValue.S("baz")]

method TestPutItemInputMultiFail(plaintextOverride : Option<AwsCryptographyDbEncryptionSdkDynamoDbTypes.PlaintextOverride>) {
assume {:axiom} false;
var middlewareUnderTest := TestFixtures.GetDynamoDbEncryptionTransformsMutli(plaintextOverride);
var tableName := GetTableName("foo");
var input := DDB.PutItemInput(
Expand All @@ -48,12 +49,14 @@ module PutItemTransformTest {
}

method {:test} TestPutItemInputMulti() {
assume {:axiom} false;
TestPutItemInputMultiFail(None);
TestPutItemInputMultiFail(Some(PlaintextOverride.FORBID_PLAINTEXT_WRITE_FORBID_PLAINTEXT_READ));
TestPutItemInputMultiFail(Some(PlaintextOverride.FORBID_PLAINTEXT_WRITE_ALLOW_PLAINTEXT_READ));
}

method {:test} TestPutItemInputMultiForceAllow() {
assume {:axiom} false;
var middlewareUnderTest := TestFixtures.GetDynamoDbEncryptionTransformsMutli(
Some(PlaintextOverride.FORCE_PLAINTEXT_WRITE_ALLOW_PLAINTEXT_READ)
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ module QueryTransformTest {
import opened DynamoDbEncryptionTransforms
import opened TestFixtures
import DDB = ComAmazonawsDynamodbTypes
import AwsCryptographyDbEncryptionSdkDynamoDbTransformsTypes
import DBT = AwsCryptographyDbEncryptionSdkDynamoDbTransformsTypes
import DBE = AwsCryptographyDbEncryptionSdkDynamoDbTypes
import Types = AwsCryptographyDbEncryptionSdkDynamoDbItemEncryptorTypes
import AwsCryptographyDbEncryptionSdkStructuredEncryptionTypes

method {:test} TestQueryInputPassthrough() {
var middlewareUnderTest := TestFixtures.GetDynamoDbEncryptionTransforms();
Expand Down Expand Up @@ -53,23 +56,7 @@ module QueryTransformTest {
);
var tableName := GetTableName("no_such_table");
var input := DDB.QueryInput(
TableName := tableName,
IndexName := None(),
Select := None(),
AttributesToGet := None(),
Limit := None(),
ConsistentRead := None(),
KeyConditions := None(),
QueryFilter := None(),
ConditionalOperator := None(),
ScanIndexForward := None(),
ExclusiveStartKey := None(),
ReturnConsumedCapacity := None(),
ProjectionExpression := None(),
FilterExpression := None(),
KeyConditionExpression := None(),
ExpressionAttributeNames := None(),
ExpressionAttributeValues := None()
TableName := tableName
);
var transformed := middlewareUnderTest.QueryOutputTransform(
AwsCryptographyDbEncryptionSdkDynamoDbTransformsTypes.QueryOutputTransformInput(
Expand All @@ -81,4 +68,152 @@ module QueryTransformTest {
expect_ok("QueryOutput", transformed);
expect_equal("QueryOutput", transformed.value.transformedOutput, output);
}

function method DDBS(x : string) : DDB.AttributeValue {
DDB.AttributeValue.S(x)
}

const Actions1 : DBE.AttributeActions := map[
GetAttrName("bar") := CSE.SIGN_ONLY,
GetAttrName("sortKey") := CSE.SIGN_ONLY,
GetAttrName("encrypt1") := CSE.ENCRYPT_AND_SIGN,
GetAttrName("encrypt2") := CSE.ENCRYPT_AND_SIGN,
GetAttrName("sign1") := CSE.SIGN_ONLY,
GetAttrName("sign2") := CSE.SIGN_ONLY
]

method {:test} TestDecryptErrorWithSortKey() {
var config := TestFixtures.GetEncryptorConfigFromActions(Actions1, Some("sortKey"));
var encryptor := TestFixtures.GetDynamoDbItemEncryptorFrom(config);

var inputItem : map<DDB.AttributeName, DDB.AttributeValue> := map[
"bar" := DDB.AttributeValue.N("00001234"),
"sortKey" := DDB.AttributeValue.B([1,2,3,4]),
"encrypt1" := DDBS("some text"),
"encrypt2" := DDBS("more text"),
"sign1" := DDBS("stuff"),
"sign2" := DDB.AttributeValue.BOOL(false)
];

var encryptRes :- expect encryptor.EncryptItem(
Types.EncryptItemInput(
plaintextItem:=inputItem
)
);
var item1 := encryptRes.encryptedItem;
expect "encrypt1" in item1;
expect item1["encrypt1"] != DDBS("some text");

inputItem := map[
"bar" := DDB.AttributeValue.N("567"),
"sortKey" := DDB.AttributeValue.B([5,6,7]),
"encrypt1" := DDBS("some text"),
"encrypt2" := DDBS("more text"),
"sign1" := DDBS("stuff"),
"sign2" := DDB.AttributeValue.BOOL(false)
];
encryptRes :- expect encryptor.EncryptItem(
Types.EncryptItemInput(
plaintextItem:=inputItem
)
);
var item2 := encryptRes.encryptedItem;
expect "encrypt1" in item2;
expect item2["encrypt1"] != DDBS("some text");

inputItem := map[
"bar" := DDB.AttributeValue.N("890"),
"sortKey" := DDB.AttributeValue.B([3,1,4]),
"encrypt1" := DDBS("some text"),
"encrypt2" := DDBS("more text"),
"sign1" := DDBS("stuff"),
"sign2" := DDB.AttributeValue.BOOL(false)
];
encryptRes :- expect encryptor.EncryptItem(
Types.EncryptItemInput(
plaintextItem:=inputItem
)
);
var item3 := encryptRes.encryptedItem;
expect "encrypt1" in item3;
expect item3["encrypt1"] != DDBS("some text");

var middlewareUnderTest := TestFixtures.GetDynamoDbEncryptionTransforms2(Actions1, Some("sortKey"));
var tableName := GetTableName("foo");
var input := DDB.QueryInput(
TableName := tableName
);

var transformed := middlewareUnderTest.QueryOutputTransform(
AwsCryptographyDbEncryptionSdkDynamoDbTransformsTypes.QueryOutputTransformInput(
sdkOutput := DDB.QueryOutput(Items := Some([item1, item2, item3])),
originalInput := input
)
);

TestFixtures.expect_ok("QueryOutput", transformed);
expect transformed.value.transformedOutput.Items.Some?;
var itemList := transformed.value.transformedOutput.Items.value;
expect |itemList| == 3;
expect "encrypt1" in itemList[0];
expect itemList[0]["encrypt1"] == DDBS("some text");


/// now do some damage
item1 := item1["encrypt1" := item2["encrypt1"]];
transformed := middlewareUnderTest.QueryOutputTransform(
AwsCryptographyDbEncryptionSdkDynamoDbTransformsTypes.QueryOutputTransformInput(
sdkOutput := DDB.QueryOutput(Items := Some([item1, item2, item3])),
originalInput := input
)
);
expect transformed.Failure?;
print "\n", transformed.error, "\n";
expect transformed.error ==
AwsCryptographyDbEncryptionSdkDynamoDbTransformsTypes.Error.CollectionOfErrors(
[
AwsCryptographyDbEncryptionSdkDynamoDbTransformsTypes.Error.AwsCryptographyDbEncryptionSdkDynamoDbItemEncryptor(AwsCryptographyDbEncryptionSdkDynamoDbItemEncryptorTypes.Error.AwsCryptographyDbEncryptionSdkDynamoDb(AwsCryptographyDbEncryptionSdkDynamoDbTypes.Error.AwsCryptographyDbEncryptionSdkStructuredEncryption(AwsCryptographyDbEncryptionSdkStructuredEncryptionTypes.Error.StructuredEncryptionException(message := "No recipient tag matched.")))),
AwsCryptographyDbEncryptionSdkDynamoDbTransformsTypes.Error.DynamoDbEncryptionTransformsException(message := "bar = 1234\nsortKey = 01020304")
],
message := "Error(s) found decrypting Query results."
);

/// do more damage
item3 := item3["encrypt1" := item2["encrypt1"]];
transformed := middlewareUnderTest.QueryOutputTransform(
AwsCryptographyDbEncryptionSdkDynamoDbTransformsTypes.QueryOutputTransformInput(
sdkOutput := DDB.QueryOutput(Items := Some([item1, item2, item3])),
originalInput := input
)
);
expect transformed.Failure?;
print "\n", transformed.error, "\n";
expect transformed.error ==
AwsCryptographyDbEncryptionSdkDynamoDbTransformsTypes.Error.CollectionOfErrors(
[
AwsCryptographyDbEncryptionSdkDynamoDbTransformsTypes.Error.AwsCryptographyDbEncryptionSdkDynamoDbItemEncryptor(AwsCryptographyDbEncryptionSdkDynamoDbItemEncryptorTypes.Error.AwsCryptographyDbEncryptionSdkDynamoDb(AwsCryptographyDbEncryptionSdkDynamoDbTypes.Error.AwsCryptographyDbEncryptionSdkStructuredEncryption(AwsCryptographyDbEncryptionSdkStructuredEncryptionTypes.Error.StructuredEncryptionException(message := "No recipient tag matched.")))),
AwsCryptographyDbEncryptionSdkDynamoDbTransformsTypes.Error.DynamoDbEncryptionTransformsException(message := "bar = 1234\nsortKey = 01020304"),
AwsCryptographyDbEncryptionSdkDynamoDbTransformsTypes.Error.DynamoDbEncryptionTransformsException(message := "bar = 890\nsortKey = 030104")
],
message := "Error(s) found decrypting Query results."
);

var transformed_scan := middlewareUnderTest.ScanOutputTransform(
AwsCryptographyDbEncryptionSdkDynamoDbTransformsTypes.ScanOutputTransformInput(
sdkOutput := DDB.ScanOutput(Items := Some([item1, item2, item3])),
originalInput := DDB.ScanInput(TableName := tableName)
)
);
expect transformed_scan.Failure?;
print "\n", transformed_scan.error, "\n";
expect transformed_scan.error ==
AwsCryptographyDbEncryptionSdkDynamoDbTransformsTypes.Error.CollectionOfErrors(
[
AwsCryptographyDbEncryptionSdkDynamoDbTransformsTypes.Error.AwsCryptographyDbEncryptionSdkDynamoDbItemEncryptor(AwsCryptographyDbEncryptionSdkDynamoDbItemEncryptorTypes.Error.AwsCryptographyDbEncryptionSdkDynamoDb(AwsCryptographyDbEncryptionSdkDynamoDbTypes.Error.AwsCryptographyDbEncryptionSdkStructuredEncryption(AwsCryptographyDbEncryptionSdkStructuredEncryptionTypes.Error.StructuredEncryptionException(message := "No recipient tag matched.")))),
AwsCryptographyDbEncryptionSdkDynamoDbTransformsTypes.Error.DynamoDbEncryptionTransformsException(message := "bar = 1234\nsortKey = 01020304"),
AwsCryptographyDbEncryptionSdkDynamoDbTransformsTypes.Error.DynamoDbEncryptionTransformsException(message := "bar = 890\nsortKey = 030104")
],
message := "Error(s) found decrypting Scan results."
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,12 @@ module TestFixtures {
expect DDB.IsValid_TableName(s);
return s;
}
method GetAttrName(s : string) returns (output : DDB.AttributeName)
function method GetAttrName(s : string) : DDB.AttributeName
{
expect DDB.IsValid_AttributeName(s);
return s;
if DDB.IsValid_AttributeName(s) then
s
else
"abc"
}
method GetStatement(s : string) returns (output : DDB.PartiQLStatement)
{
Expand Down Expand Up @@ -235,6 +237,32 @@ module TestFixtures {
assume {:axiom} fresh(encryption.Modifies);
}

method GetDynamoDbEncryptionTransforms2(actions : AttributeActions, sortKey : Option<string>)
returns (encryption: DynamoDbEncryptionTransforms.DynamoDbEncryptionTransformsClient)
ensures encryption.ValidState()
ensures fresh(encryption)
ensures fresh(encryption.Modifies)
{
var keyring := GetKmsKeyring();
encryption :- expect DynamoDbEncryptionTransforms.DynamoDbEncryptionTransforms(
DynamoDbTablesEncryptionConfig(
tableEncryptionConfigs := map[
"foo" := DynamoDbTableEncryptionConfig(
logicalTableName := "foo",
partitionKeyName := "bar",
sortKeyName := sortKey,
attributeActionsOnEncrypt := actions,
allowedUnsignedAttributes := Some(["plain"]),
allowedUnsignedAttributePrefix := None(),
algorithmSuiteId := None(),
keyring := Some(keyring)
)
]
)
);
assume {:axiom} fresh(encryption.Modifies);
}

// type AttributeActions = map<ComAmazonawsDynamodbTypes.AttributeName, AwsCryptographyDbEncryptionSdkStructuredEncryptionTypes.CryptoAction>

const MultiActions : AttributeActions :=
Expand Down

0 comments on commit 07bba8b

Please sign in to comment.