Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: include bad item keys in query errors #1244

Merged
merged 4 commits into from
Aug 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading