diff --git a/DynamoDbEncryption/dafny/DynamoDbEncryptionTransforms/src/DdbMiddlewareConfig.dfy b/DynamoDbEncryption/dafny/DynamoDbEncryptionTransforms/src/DdbMiddlewareConfig.dfy index 12ce8fa04..e2c4036f7 100644 --- a/DynamoDbEncryption/dafny/DynamoDbEncryptionTransforms/src/DdbMiddlewareConfig.dfy +++ b/DynamoDbEncryption/dafny/DynamoDbEncryptionTransforms/src/DdbMiddlewareConfig.dfy @@ -110,7 +110,7 @@ module DdbMiddlewareConfig { ""; var sort := if config.sortKeyName.Some? && config.sortKeyName.value in item then - "\n" + config.sortKeyName.value + " = " + AttrToString(item[config.sortKeyName.value]) + "; " + config.sortKeyName.value + " = " + AttrToString(item[config.sortKeyName.value]) else ""; diff --git a/DynamoDbEncryption/dafny/DynamoDbEncryptionTransforms/test/QueryTransform.dfy b/DynamoDbEncryption/dafny/DynamoDbEncryptionTransforms/test/QueryTransform.dfy index 04a1a4539..72f474bb1 100644 --- a/DynamoDbEncryption/dafny/DynamoDbEncryptionTransforms/test/QueryTransform.dfy +++ b/DynamoDbEncryption/dafny/DynamoDbEncryptionTransforms/test/QueryTransform.dfy @@ -173,7 +173,7 @@ module QueryTransformTest { 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 = 1234; sortKey = 01020304") ], message := "Error(s) found decrypting Query results." ); @@ -192,8 +192,8 @@ module QueryTransformTest { 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") + AwsCryptographyDbEncryptionSdkDynamoDbTransformsTypes.Error.DynamoDbEncryptionTransformsException(message := "bar = 1234; sortKey = 01020304"), + AwsCryptographyDbEncryptionSdkDynamoDbTransformsTypes.Error.DynamoDbEncryptionTransformsException(message := "bar = 890; sortKey = 030104") ], message := "Error(s) found decrypting Query results." ); @@ -210,8 +210,8 @@ module QueryTransformTest { 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") + AwsCryptographyDbEncryptionSdkDynamoDbTransformsTypes.Error.DynamoDbEncryptionTransformsException(message := "bar = 1234; sortKey = 01020304"), + AwsCryptographyDbEncryptionSdkDynamoDbTransformsTypes.Error.DynamoDbEncryptionTransformsException(message := "bar = 890; sortKey = 030104") ], message := "Error(s) found decrypting Scan results." ); diff --git a/Examples/runtimes/java/DynamoDbEncryption/src/main/java/software/amazon/cryptography/examples/ScanErrorExample.java b/Examples/runtimes/java/DynamoDbEncryption/src/main/java/software/amazon/cryptography/examples/ScanErrorExample.java new file mode 100644 index 000000000..46fa125a7 --- /dev/null +++ b/Examples/runtimes/java/DynamoDbEncryption/src/main/java/software/amazon/cryptography/examples/ScanErrorExample.java @@ -0,0 +1,191 @@ +package software.amazon.cryptography.examples; + +import java.util.HashMap; +import java.util.Map; +import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.services.dynamodb.model.*; +import software.amazon.awssdk.services.dynamodb.model.ScanRequest; +import software.amazon.awssdk.services.dynamodb.model.ScanResponse; +import software.amazon.cryptography.dbencryptionsdk.dynamodb.DynamoDbEncryptionInterceptor; +import software.amazon.cryptography.dbencryptionsdk.dynamodb.model.DynamoDbTableEncryptionConfig; +import software.amazon.cryptography.dbencryptionsdk.dynamodb.model.DynamoDbTablesEncryptionConfig; +import software.amazon.cryptography.dbencryptionsdk.dynamodb.transforms.model.CollectionOfErrors; +import software.amazon.cryptography.dbencryptionsdk.structuredencryption.model.CryptoAction; +import software.amazon.cryptography.materialproviders.IKeyring; +import software.amazon.cryptography.materialproviders.MaterialProviders; +import software.amazon.cryptography.materialproviders.model.CreateAwsKmsMrkMultiKeyringInput; +import software.amazon.cryptography.materialproviders.model.DBEAlgorithmSuiteId; +import software.amazon.cryptography.materialproviders.model.MaterialProvidersConfig; + +/* + This example sets up DynamoDb Encryption for the AWS SDK client + and uses the low level Scan operation to demonstrate + retrieving the error messages from the returned CollectionOfErrors + when some of the Scan results do not decrypt successfully. + + Running this example requires access to the DDB Table whose name + is provided in CLI arguments. + This table must be configured with the following + primary key configuration: + - Partition key is named "partition_key" with type (S) + - Sort key is named "sort_key" with type (N) + */ +public class ScanErrorExample { + + public static void ScanError(String kmsKeyId, String ddbTableName) { + // 1. Create a Keyring. This Keyring will be responsible for protecting the data keys that protect your data. + // For this example, we will create a AWS KMS Keyring with the AWS KMS Key we want to use. + // We will use the `CreateMrkMultiKeyring` method to create this keyring, + // as it will correctly handle both single region and Multi-Region KMS Keys. + final MaterialProviders matProv = MaterialProviders + .builder() + .MaterialProvidersConfig(MaterialProvidersConfig.builder().build()) + .build(); + final CreateAwsKmsMrkMultiKeyringInput keyringInput = + CreateAwsKmsMrkMultiKeyringInput.builder().generator(kmsKeyId).build(); + final IKeyring kmsKeyring = matProv.CreateAwsKmsMrkMultiKeyring( + keyringInput + ); + + // 2. Configure which attributes are encrypted and/or signed when writing new items. + // For each attribute that may exist on the items we plan to write to our DynamoDbTable, + // we must explicitly configure how they should be treated during item encryption: + // - ENCRYPT_AND_SIGN: The attribute is encrypted and included in the signature + // - SIGN_ONLY: The attribute not encrypted, but is still included in the signature + // - DO_NOTHING: The attribute is not encrypted and not included in the signature + final Map attributeActionsOnEncrypt = new HashMap<>(); + attributeActionsOnEncrypt.put("partition_key", CryptoAction.SIGN_ONLY); // Our partition attribute must be SIGN_ONLY + attributeActionsOnEncrypt.put("sort_key", CryptoAction.SIGN_ONLY); // Our sort attribute must be SIGN_ONLY + attributeActionsOnEncrypt.put("attribute1", CryptoAction.ENCRYPT_AND_SIGN); + attributeActionsOnEncrypt.put("attribute2", CryptoAction.SIGN_ONLY); + attributeActionsOnEncrypt.put(":attribute3", CryptoAction.DO_NOTHING); + + // 3. Configure which attributes we expect to be included in the signature + // when reading items. There are two options for configuring this: + // + // - (Recommended) Configure `allowedUnsignedAttributesPrefix`: + // When defining your DynamoDb schema and deciding on attribute names, + // choose a distinguishing prefix (such as ":") for all attributes that + // you do not want to include in the signature. + // This has two main benefits: + // - It is easier to reason about the security and authenticity of data within your item + // when all unauthenticated data is easily distinguishable by their attribute name. + // - If you need to add new unauthenticated attributes in the future, + // you can easily make the corresponding update to your `attributeActionsOnEncrypt` + // and immediately start writing to that new attribute, without + // any other configuration update needed. + // Once you configure this field, it is not safe to update it. + // + // - Configure `allowedUnsignedAttributes`: You may also explicitly list + // a set of attributes that should be considered unauthenticated when encountered + // on read. Be careful if you use this configuration. Do not remove an attribute + // name from this configuration, even if you are no longer writing with that attribute, + // as old items may still include this attribute, and our configuration needs to know + // to continue to exclude this attribute from the signature scope. + // If you add new attribute names to this field, you must first deploy the update to this + // field to all readers in your host fleet before deploying the update to start writing + // with that new attribute. + // + // For this example, we have designed our DynamoDb table such that any attribute name with + // the ":" prefix should be considered unauthenticated. + final String unsignAttrPrefix = ":"; + + // 4. Create the DynamoDb Encryption configuration for the table we will be writing to. + final Map tableConfigs = + new HashMap<>(); + final DynamoDbTableEncryptionConfig config = DynamoDbTableEncryptionConfig + .builder() + .logicalTableName(ddbTableName) + .partitionKeyName("partition_key") + .sortKeyName("sort_key") + .attributeActionsOnEncrypt(attributeActionsOnEncrypt) + .keyring(kmsKeyring) + .allowedUnsignedAttributePrefix(unsignAttrPrefix) + // Specifying an algorithm suite is not required, + // but is done here to demonstrate how to do so. + // We suggest using the + // `ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY_ECDSA_P384_SYMSIG_HMAC_SHA384` suite, + // which includes AES-GCM with key derivation, signing, and key commitment. + // This is also the default algorithm suite if one is not specified in this config. + // For more information on supported algorithm suites, see: + // https://docs.aws.amazon.com/database-encryption-sdk/latest/devguide/supported-algorithms.html + .algorithmSuiteId( + DBEAlgorithmSuiteId.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY_ECDSA_P384_SYMSIG_HMAC_SHA384 + ) + .build(); + tableConfigs.put(ddbTableName, config); + + // 5. Create the DynamoDb Encryption Interceptor + DynamoDbEncryptionInterceptor encryptionInterceptor = + DynamoDbEncryptionInterceptor + .builder() + .config( + DynamoDbTablesEncryptionConfig + .builder() + .tableEncryptionConfigs(tableConfigs) + .build() + ) + .build(); + + // 6. Create a new AWS SDK DynamoDb client using the DynamoDb Encryption Interceptor above + final DynamoDbClient ddb = DynamoDbClient + .builder() + .overrideConfiguration( + ClientOverrideConfiguration + .builder() + .addExecutionInterceptor(encryptionInterceptor) + .build() + ) + .build(); + + // 7. Perform a Scan for which some records will not decrypt + Map expressionAttributeValues = new HashMap<>(); + expressionAttributeValues.put( + ":prefix", + AttributeValue.builder().s("Broken").build() + ); + + ScanRequest scanRequest = ScanRequest + .builder() + .tableName(ddbTableName) + .filterExpression("begins_with(partition_key, :prefix)") + .expressionAttributeValues(expressionAttributeValues) + .build(); + + try { + final ScanResponse scanResponse = ddb.scan(scanRequest); + assert false; + } catch (Exception e) { + print_exception(e, ""); + } + } + + public static void print_exception(Exception e, String indent) { + System.err.println(indent + e.getMessage()); + if (e.getCause() instanceof CollectionOfErrors) { + System.err.println(indent + e.getCause().getMessage()); + for (RuntimeException err : ((CollectionOfErrors) e.getCause()).list()) { + print_exception(err, indent + " "); + } + } else if ( + e instanceof + software.amazon.cryptography.materialproviders.model.CollectionOfErrors + ) { + for (RuntimeException err : ((software.amazon.cryptography.materialproviders.model.CollectionOfErrors) e).list()) { + print_exception(err, indent + " "); + } + } + } + + public static void main(final String[] args) { + if (args.length < 2) { + throw new IllegalArgumentException( + "To run this example, include the kmsKeyId as args[0] and ddbTableName as args[1]" + ); + } + final String kmsKeyId = args[0]; + final String ddbTableName = args[1]; + ScanError(kmsKeyId, ddbTableName); + } +} diff --git a/Examples/runtimes/java/DynamoDbEncryption/src/test/java/software/amazon/cryptography/examples/TestScanErrorExample.java b/Examples/runtimes/java/DynamoDbEncryption/src/test/java/software/amazon/cryptography/examples/TestScanErrorExample.java new file mode 100644 index 000000000..45a4a16c1 --- /dev/null +++ b/Examples/runtimes/java/DynamoDbEncryption/src/test/java/software/amazon/cryptography/examples/TestScanErrorExample.java @@ -0,0 +1,14 @@ +package software.amazon.cryptography.examples; + +import org.testng.annotations.Test; + +public class TestScanErrorExample { + + @Test + public void ScanError() { + ScanErrorExample.ScanError( + TestUtils.TEST_KMS_KEY_ID, + TestUtils.TEST_DDB_TABLE_NAME + ); + } +} diff --git a/Examples/runtimes/net/src/Examples.cs b/Examples/runtimes/net/src/Examples.cs index 64ed261b4..c7f5b15f3 100644 --- a/Examples/runtimes/net/src/Examples.cs +++ b/Examples/runtimes/net/src/Examples.cs @@ -12,6 +12,7 @@ static async Task Main() ItemEncryptDecryptExample.PutItemGetItem(); await BasicPutGetExample.PutItemGetItem(); + await ScanErrorExample.ScanError(); await GetEncryptedDataKeyDescriptionExample.GetEncryptedDataKeyDescription(); await MultiPutGetExample.MultiPutGet(); await ClientSupplierExample.ClientSupplierPutItemGetItem(); diff --git a/Examples/runtimes/net/src/ScanErrorExample.cs b/Examples/runtimes/net/src/ScanErrorExample.cs new file mode 100644 index 000000000..40c5cf74f --- /dev/null +++ b/Examples/runtimes/net/src/ScanErrorExample.cs @@ -0,0 +1,154 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Net; +using System.Threading.Tasks; +using Amazon.DynamoDBv2.Model; +using AWS.Cryptography.DbEncryptionSDK.DynamoDb; +using AWS.Cryptography.DbEncryptionSDK.StructuredEncryption; +using AWS.Cryptography.MaterialProviders; + +/* + This example sets up DynamoDb Encryption for the AWS SDK client + and uses the low level Scan operation to demonstrate + retrieving the error messages from the returned CollectionOfErrors + when some of the Scan results do not decrypt successfully. + + Running this example requires access to the DDB Table whose name + is provided in CLI arguments. + This table must be configured with the following + primary key configuration: + - Partition key is named "partition_key" with type (S) + - Sort key is named "sort_key" with type (N) + */ +public class ScanErrorExample +{ + public static async Task ScanError() + { + var kmsKeyId = TestUtils.TEST_KMS_KEY_ID; + var ddbTableName = TestUtils.TEST_DDB_TABLE_NAME; + // 1. Create a Keyring. This Keyring will be responsible for protecting the data keys that protect your data. + // For this example, we will create a AWS KMS Keyring with the AWS KMS Key we want to use. + // We will use the `CreateMrkMultiKeyring` method to create this keyring, + // as it will correctly handle both single region and Multi-Region KMS Keys. + var matProv = new MaterialProviders(new MaterialProvidersConfig()); + var keyringInput = new CreateAwsKmsMrkMultiKeyringInput { Generator = kmsKeyId }; + var kmsKeyring = matProv.CreateAwsKmsMrkMultiKeyring(keyringInput); + + // 2. Configure which attributes are encrypted and/or signed when writing new items. + // For each attribute that may exist on the items we plan to write to our DynamoDbTable, + // we must explicitly configure how they should be treated during item encryption: + // - ENCRYPT_AND_SIGN: The attribute is encrypted and included in the signature + // - SIGN_ONLY: The attribute not encrypted, but is still included in the signature + // - DO_NOTHING: The attribute is not encrypted and not included in the signature + var attributeActionsOnEncrypt = new Dictionary + { + ["partition_key"] = CryptoAction.SIGN_ONLY, // Our partition attribute must be SIGN_ONLY + ["sort_key"] = CryptoAction.SIGN_ONLY, // Our sort attribute must be SIGN_ONLY + ["attribute1"] = CryptoAction.ENCRYPT_AND_SIGN, + ["attribute2"] = CryptoAction.SIGN_ONLY, + [":attribute3"] = CryptoAction.DO_NOTHING + }; + + // 3. Configure which attributes we expect to be included in the signature + // when reading items. There are two options for configuring this: + // + // - (Recommended) Configure `allowedUnsignedAttributesPrefix`: + // When defining your DynamoDb schema and deciding on attribute names, + // choose a distinguishing prefix (such as ":") for all attributes that + // you do not want to include in the signature. + // This has two main benefits: + // - It is easier to reason about the security and authenticity of data within your item + // when all unauthenticated data is easily distinguishable by their attribute name. + // - If you need to add new unauthenticated attributes in the future, + // you can easily make the corresponding update to your `attributeActionsOnEncrypt` + // and immediately start writing to that new attribute, without + // any other configuration update needed. + // Once you configure this field, it is not safe to update it. + // + // - Configure `allowedUnsignedAttributes`: You may also explicitly list + // a set of attributes that should be considered unauthenticated when encountered + // on read. Be careful if you use this configuration. Do not remove an attribute + // name from this configuration, even if you are no longer writing with that attribute, + // as old items may still include this attribute, and our configuration needs to know + // to continue to exclude this attribute from the signature scope. + // If you add new attribute names to this field, you must first deploy the update to this + // field to all readers in your host fleet before deploying the update to start writing + // with that new attribute. + // + // For this example, we have designed our DynamoDb table such that any attribute name with + // the ":" prefix should be considered unauthenticated. + const String unsignAttrPrefix = ":"; + + // 4. Create the DynamoDb Encryption configuration for the table we will be writing to. + Dictionary tableConfigs = + new Dictionary(); + DynamoDbTableEncryptionConfig config = new DynamoDbTableEncryptionConfig + { + LogicalTableName = ddbTableName, + PartitionKeyName = "partition_key", + SortKeyName = "sort_key", + AttributeActionsOnEncrypt = attributeActionsOnEncrypt, + Keyring = kmsKeyring, + AllowedUnsignedAttributePrefix = unsignAttrPrefix, + // Specifying an algorithm suite is not required, + // but is done here to demonstrate how to do so. + // We suggest using the + // `ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY_ECDSA_P384_SYMSIG_HMAC_SHA384` suite, + // which includes AES-GCM with key derivation, signing, and key commitment. + // This is also the default algorithm suite if one is not specified in this config. + // For more information on supported algorithm suites, see: + // https://docs.aws.amazon.com/database-encryption-sdk/latest/devguide/supported-algorithms.html + AlgorithmSuiteId = DBEAlgorithmSuiteId.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY_ECDSA_P384_SYMSIG_HMAC_SHA384 + }; + tableConfigs.Add(ddbTableName, config); + + // 5. Create a new AWS SDK DynamoDb client using the TableEncryptionConfigs + var ddb = new Client.DynamoDbClient( + new DynamoDbTablesEncryptionConfig { TableEncryptionConfigs = tableConfigs }); + + var expressionAttributeValues = new Dictionary + { + [":prefix"] = new AttributeValue("Broken"), + }; + + // 6. Perform a Scan for which some records will not decrypt + var scanRequest = new ScanRequest + { + TableName = ddbTableName, + FilterExpression = "begins_with(partition_key, :prefix)", + ExpressionAttributeValues = expressionAttributeValues + }; + + try + { + var scanResponse = await ddb.ScanAsync(scanRequest); + Debug.Assert(false); + } + catch (Exception e) + { + PrintException(e, ""); + } + } + + public static void PrintException(Exception e, String indent) + { + Console.Error.WriteLine(indent + e.Message); + if (e is AWS.Cryptography.DbEncryptionSDK.DynamoDb.Transforms.CollectionOfErrors) + { + var ee = e as AWS.Cryptography.DbEncryptionSDK.DynamoDb.Transforms.CollectionOfErrors; + foreach (Exception element in ee.list) + { + PrintException(element, " " + indent); + } + } + else if (e is AWS.Cryptography.MaterialProviders.CollectionOfErrors) + { + var ee = e as AWS.Cryptography.MaterialProviders.CollectionOfErrors; + foreach (Exception element in ee.list) + { + PrintException(element, " " + indent); + } + } + } +}