diff --git a/.github/workflows/ci_examples_net.yml b/.github/workflows/ci_examples_net.yml index f9d85e050..41d90d49c 100644 --- a/.github/workflows/ci_examples_net.yml +++ b/.github/workflows/ci_examples_net.yml @@ -94,3 +94,4 @@ jobs: shell: bash run: | dotnet run + dotnet test diff --git a/Examples/runtimes/net/Examples.csproj b/Examples/runtimes/net/Examples.csproj index 573a10eb6..f57bdb388 100644 --- a/Examples/runtimes/net/Examples.csproj +++ b/Examples/runtimes/net/Examples.csproj @@ -11,6 +11,9 @@ + + + diff --git a/Examples/runtimes/net/src/TestUtils.cs b/Examples/runtimes/net/src/TestUtils.cs index 746c50560..2ad5c4a26 100644 --- a/Examples/runtimes/net/src/TestUtils.cs +++ b/Examples/runtimes/net/src/TestUtils.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using System.Collections.Generic; using Amazon.DynamoDBv2.Model; public class TestUtils @@ -84,4 +85,21 @@ public static void PrintAttributeValue(AttributeValue value) if (value.IsBOOLSet) Console.Write($"BOOL {value.BOOL}\n"); Console.Write("UNKNOWN\n"); } + + // Helper method to clean up test items + public static async System.Threading.Tasks.Task CleanupItems(string tableName, string partitionKey, string sortKey) + { + var ddb = new Amazon.DynamoDBv2.AmazonDynamoDBClient(); + var key = new Dictionary + { + ["partition_key"] = new AttributeValue { S = partitionKey }, + ["sort_key"] = new AttributeValue { N = sortKey } + }; + var deleteRequest = new DeleteItemRequest + { + TableName = tableName, + Key = key + }; + await ddb.DeleteItemAsync(deleteRequest); + } } \ No newline at end of file diff --git a/Examples/runtimes/net/src/migration/PlaintextToAWSDBE/MigrationUtils.cs b/Examples/runtimes/net/src/migration/PlaintextToAWSDBE/MigrationUtils.cs new file mode 100644 index 000000000..891c0a4df --- /dev/null +++ b/Examples/runtimes/net/src/migration/PlaintextToAWSDBE/MigrationUtils.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using Amazon.DynamoDBv2.Model; + +namespace Examples.migration.PlaintextToAWSDBE +{ + /* + Utility class for the PlaintextToAWSDBE migration examples. + This class contains shared functionality used by all migration steps. + */ + public class MigrationUtils + { + // Common attribute values used across all migration steps + public static readonly string ENCRYPTED_AND_SIGNED_VALUE = "this will be encrypted and signed"; + public static readonly string SIGN_ONLY_VALUE = "this will never be encrypted, but it will be signed"; + public static readonly string DO_NOTHING_VALUE = "this will never be encrypted nor signed"; + + // Verify that a returned item matches the expected values + public static bool VerifyReturnedItem(GetItemResponse response, string partitionKeyValue, string sortKeyValue) + { + var item = response.Item; + + if (!item.ContainsKey("partition_key") || item["partition_key"].S != partitionKeyValue) + { + throw new Exception($"partition_key mismatch: expected {partitionKeyValue}, got {(item.ContainsKey("partition_key") ? item["partition_key"].S : "null")}"); + } + + if (!item.ContainsKey("sort_key") || item["sort_key"].N != sortKeyValue) + { + throw new Exception($"sort_key mismatch: expected {sortKeyValue}, got {(item.ContainsKey("sort_key") ? item["sort_key"].N : "null")}"); + } + + if (!item.ContainsKey("attribute1") || item["attribute1"].S != ENCRYPTED_AND_SIGNED_VALUE) + { + throw new Exception($"attribute1 mismatch: expected {ENCRYPTED_AND_SIGNED_VALUE}, got {(item.ContainsKey("attribute1") ? item["attribute1"].S : "null")}"); + } + + if (!item.ContainsKey("attribute2") || item["attribute2"].S != SIGN_ONLY_VALUE) + { + throw new Exception($"attribute2 mismatch: expected {SIGN_ONLY_VALUE}, got {(item.ContainsKey("attribute2") ? item["attribute2"].S : "null")}"); + } + + if (!item.ContainsKey("attribute3") || item["attribute3"].S != DO_NOTHING_VALUE) + { + throw new Exception($"attribute3 mismatch: expected {DO_NOTHING_VALUE}, got {(item.ContainsKey("attribute3") ? item["attribute3"].S : "null")}"); + } + + return true; + } + } +} diff --git a/Examples/runtimes/net/src/migration/PlaintextToAWSDBE/README.md b/Examples/runtimes/net/src/migration/PlaintextToAWSDBE/README.md new file mode 100644 index 000000000..31170c101 --- /dev/null +++ b/Examples/runtimes/net/src/migration/PlaintextToAWSDBE/README.md @@ -0,0 +1,51 @@ +# Plaintext DynamoDB Table to AWS Database Encryption SDK Encrypted Table Migration + +This projects demonstrates the steps necessary +to migrate to the AWS Database Encryption SDK for DynamoDb +from a plaintext database. + +[Step 0](plaintext/step0.go) demonstrates the starting state for your system. + +## Step 1 + +In Step 1, you update your system to do the following: + +- continue to read plaintext items +- continue to write plaintext items +- prepare to read encrypted items + +When you deploy changes in Step 1, +you should not expect any behavior change in your system, +and your dataset still consists of plaintext data. + +You must ensure that the changes in Step 1 make it to all your readers before you proceed to Step 2. + +## Step 2 + +In Step 2, you update your system to do the following: + +- continue to read plaintext items +- start writing encrypted items +- continue to read encrypted items + +When you deploy changes in Step 2, +you are introducing encrypted items to your system, +and must make sure that all your readers are updated with the changes from Step 1. + +Before you move onto the next step, you will need to encrypt all plaintext items in your dataset. +Once you have completed this step, +while new items are being encrypted using the new format and will be authenticated on read, +your system will still accept reading plaintext, unauthenticated items. +In order to complete migration to a system where you always authenticate your items, +you should prioritize moving on to Step 3. + +## Step 3 + +Once all old items are encrypted, +update your system to do the following: + +- continue to write encrypted items +- continue to read encrypted items +- do not accept reading plaintext items + +Once you have deployed these changes to your system, you have completed migration. diff --git a/Examples/runtimes/net/src/migration/PlaintextToAWSDBE/awsdbe/Common.cs b/Examples/runtimes/net/src/migration/PlaintextToAWSDBE/awsdbe/Common.cs new file mode 100644 index 000000000..912116d39 --- /dev/null +++ b/Examples/runtimes/net/src/migration/PlaintextToAWSDBE/awsdbe/Common.cs @@ -0,0 +1,84 @@ +using System.Collections.Generic; +using AWS.Cryptography.DbEncryptionSDK.DynamoDb; +using AWS.Cryptography.DbEncryptionSDK.StructuredEncryption; +using AWS.Cryptography.MaterialProviders; + +namespace Examples.migration.PlaintextToAWSDBE +{ + public static class Common + { + public static Dictionary CreateTableConfigs(string kmsKeyId, string ddbTableName, PlaintextOverride PlaintextOverride) + { + // 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); + + // 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 + string partitionKeyName = "partition_key"; + string sortKeyName = "sort_key"; + var attributeActionsOnEncrypt = new Dictionary + { + [partitionKeyName] = CryptoAction.SIGN_ONLY, + [sortKeyName] = CryptoAction.SIGN_ONLY, + ["attribute1"] = CryptoAction.ENCRYPT_AND_SIGN, + ["attribute2"] = CryptoAction.SIGN_ONLY, + ["attribute3"] = CryptoAction.DO_NOTHING + }; + + // Configure which attributes we expect to be excluded 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 will explicitly list the attributes that are not signed. + var unsignedAttributes = new List { "attribute3" }; + + // Create the DynamoDb Encryption configuration for the table we will be writing to. + var tableConfig = new DynamoDbTableEncryptionConfig + { + LogicalTableName = ddbTableName, + PartitionKeyName = partitionKeyName, + SortKeyName = sortKeyName, + AttributeActionsOnEncrypt = attributeActionsOnEncrypt, + Keyring = kmsKeyring, + AllowedUnsignedAttributes = unsignedAttributes, + PlaintextOverride = PlaintextOverride + }; + + return new Dictionary + { + [ddbTableName] = tableConfig + }; + } + } +} diff --git a/Examples/runtimes/net/src/migration/PlaintextToAWSDBE/awsdbe/MigrationStep1.cs b/Examples/runtimes/net/src/migration/PlaintextToAWSDBE/awsdbe/MigrationStep1.cs new file mode 100644 index 000000000..bcd78d5bd --- /dev/null +++ b/Examples/runtimes/net/src/migration/PlaintextToAWSDBE/awsdbe/MigrationStep1.cs @@ -0,0 +1,117 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Amazon.DynamoDBv2; +using Amazon.DynamoDBv2.Model; +using System.Diagnostics; +using System.Net; +using AWS.Cryptography.DbEncryptionSDK.DynamoDb; +using AWS.Cryptography.DbEncryptionSDK.StructuredEncryption; +using AWS.Cryptography.MaterialProviders; +using Examples.migration.PlaintextToAWSDBE; + +namespace Examples.migration.PlaintextToAWSDBE.awsdbe +{ + /* + Migration Step 1: This is the first step in the migration process from + plaintext to encrypted DynamoDB using the AWS Database Encryption SDK. + + In this example, we configure a DynamoDB Encryption client to do the following: + 1. Write items only in plaintext + 2. Read items in plaintext or, if the item is encrypted, decrypt with our encryption configuration + + While this step configures your client to be ready to start reading encrypted items, + we do not yet expect to be reading any encrypted items, + as our client still writes plaintext items. + Before you move on to step 2, ensure that these changes have successfully been deployed + to all of your readers. + + Running this example requires access to the DDB Table whose name + is provided in the function parameter. + 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 MigrationStep1 + { + public static async Task MigrationStep1Example(string kmsKeyId, string ddbTableName, string partitionKeyValue, string sortKeyWriteValue, string sortKeyReadValue) + { + // 1. Create table configurations + // In this of migration we will use PlaintextOverride.FORCE_PLAINTEXT_WRITE_ALLOW_PLAINTEXT_READ + // which means: + // - Write: Items are forced to be written as plaintext. + // Items may not be written as encrypted items. + // - Read: Items are allowed to be read as plaintext. + // Items are allowed to be read as encrypted items. + var tableConfigs = Common.CreateTableConfigs(kmsKeyId, ddbTableName, PlaintextOverride.FORCE_PLAINTEXT_WRITE_ALLOW_PLAINTEXT_READ); + + // 1. Create a new AWS SDK DynamoDb client using the TableEncryptionConfigs + var ddb = new Client.DynamoDbClient( + new DynamoDbTablesEncryptionConfig { TableEncryptionConfigs = tableConfigs }); + + // 2. Put an item into our table using the above client. + // This item will be stored in plaintext due to our PlaintextOverride configuration. + string partitionKeyName = "partition_key"; + string sortKeyName = "sort_key"; + string encryptedAndSignedValue = MigrationUtils.ENCRYPTED_AND_SIGNED_VALUE; + string signOnlyValue = MigrationUtils.SIGN_ONLY_VALUE; + string doNothingValue = MigrationUtils.DO_NOTHING_VALUE; + var item = new Dictionary + { + [partitionKeyName] = new AttributeValue { S = partitionKeyValue }, + [sortKeyName] = new AttributeValue { N = sortKeyWriteValue }, + ["attribute1"] = new AttributeValue { S = encryptedAndSignedValue }, + ["attribute2"] = new AttributeValue { S = signOnlyValue }, + ["attribute3"] = new AttributeValue { S = doNothingValue } + }; + + var putRequest = new PutItemRequest + { + TableName = ddbTableName, + Item = item + }; + + var putResponse = await ddb.PutItemAsync(putRequest); + Debug.Assert(putResponse.HttpStatusCode == HttpStatusCode.OK); + + // 3. Get an item back from the table using the same client. + // If this is an item written in plaintext (i.e. any item written + // during Step 0 or 1), then the item will still be in plaintext. + // If this is an item that was encrypted client-side (i.e. any item written + // during Step 2 or after), then the item will be decrypted client-side + // and surfaced as a plaintext item. + var key = new Dictionary + { + [partitionKeyName] = new AttributeValue { S = partitionKeyValue }, + [sortKeyName] = new AttributeValue { N = sortKeyReadValue } + }; + + var getRequest = new GetItemRequest + { + TableName = ddbTableName, + Key = key, + // In this example we configure a strongly consistent read + // because we perform a read immediately after a write (for demonstrative purposes). + // By default, reads are only eventually consistent. + ConsistentRead = true + }; + + var getResponse = await ddb.GetItemAsync(getRequest); + Debug.Assert(getResponse.HttpStatusCode == HttpStatusCode.OK); + + // 4. Verify we get the expected item back + if (getResponse.Item == null) + { + throw new Exception("No item found"); + } + + bool success = MigrationUtils.VerifyReturnedItem(getResponse, partitionKeyValue, sortKeyReadValue); + if (success) + { + Console.WriteLine("MigrationStep1 completed successfully"); + } + return success; + } + } +} diff --git a/Examples/runtimes/net/src/migration/PlaintextToAWSDBE/awsdbe/MigrationStep1Test.cs b/Examples/runtimes/net/src/migration/PlaintextToAWSDBE/awsdbe/MigrationStep1Test.cs new file mode 100644 index 000000000..7822bd6c6 --- /dev/null +++ b/Examples/runtimes/net/src/migration/PlaintextToAWSDBE/awsdbe/MigrationStep1Test.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Amazon.DynamoDBv2; +using Amazon.DynamoDBv2.Model; +using System.Diagnostics; +using Xunit; +using Examples.migration.PlaintextToAWSDBE; +using Examples.migration.PlaintextToAWSDBE.plaintext; + +namespace Examples.migration.PlaintextToAWSDBE.awsdbe +{ + public class MigrationStep1Test + { + [Fact] + public async Task TestMigrationStep1() + { + string kmsKeyID = TestUtils.TEST_KMS_KEY_ID; + string tableName = TestUtils.TEST_DDB_TABLE_NAME; + string partitionKey = Guid.NewGuid().ToString(); + string[] sortKeys = { "0", "1", "2", "3" }; + + // Successfully executes step 1 + bool success = await MigrationStep1.MigrationStep1Example(kmsKeyID, tableName, partitionKey, sortKeys[1], sortKeys[1]); + Assert.True(success, "MigrationStep1 should complete successfully"); + + // Given: Step 0 has succeeded + success = await MigrationStep0.MigrationStep0Example(tableName, partitionKey, sortKeys[0], sortKeys[0]); + Assert.True(success, "MigrationStep0 should complete successfully"); + + // When: Execute Step 1 with sortReadValue=0, Then: Success (i.e. can read plaintext values from Step 0) + success = await MigrationStep1.MigrationStep1Example(kmsKeyID, tableName, partitionKey, sortKeys[1], sortKeys[0]); + Assert.True(success, "MigrationStep1 should be able to read items written by Step 0"); + + // Given: Step 2 has succeeded + success = await MigrationStep2.MigrationStep2Example(kmsKeyID, tableName, partitionKey, sortKeys[2], sortKeys[2]); + Assert.True(success, "MigrationStep2 should complete successfully"); + + // When: Execute Step 1 with sortReadValue=2, Then: Success (i.e. can read encrypted values from Step 2) + success = await MigrationStep1.MigrationStep1Example(kmsKeyID, tableName, partitionKey, sortKeys[1], sortKeys[2]); + Assert.True(success, "MigrationStep1 should be able to read items written by Step 2"); + + // Given: Step 3 has succeeded + success = await MigrationStep3.MigrationStep3Example(kmsKeyID, tableName, partitionKey, sortKeys[3], sortKeys[3]); + Assert.True(success, "MigrationStep3 should complete successfully"); + + // When: Execute Step 1 with sortReadValue=3, Then: Success (i.e. can read encrypted values from Step 3) + success = await MigrationStep1.MigrationStep1Example(kmsKeyID, tableName, partitionKey, sortKeys[1], sortKeys[3]); + Assert.True(success, "MigrationStep1 should be able to read items written by Step 3"); + + // Cleanup + foreach (var sortKey in sortKeys) + { + await TestUtils.CleanupItems(tableName, partitionKey, sortKey); + } + } + } +} diff --git a/Examples/runtimes/net/src/migration/PlaintextToAWSDBE/awsdbe/MigrationStep2.cs b/Examples/runtimes/net/src/migration/PlaintextToAWSDBE/awsdbe/MigrationStep2.cs new file mode 100644 index 000000000..ffc345dde --- /dev/null +++ b/Examples/runtimes/net/src/migration/PlaintextToAWSDBE/awsdbe/MigrationStep2.cs @@ -0,0 +1,119 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Amazon.DynamoDBv2; +using Amazon.DynamoDBv2.Model; +using System.Diagnostics; +using System.Net; +using AWS.Cryptography.DbEncryptionSDK.DynamoDb; +using AWS.Cryptography.DbEncryptionSDK.StructuredEncryption; +using AWS.Cryptography.MaterialProviders; +using Examples.migration.PlaintextToAWSDBE; + +namespace Examples.migration.PlaintextToAWSDBE.awsdbe +{ + /* + Migration Step 2: This is the second step in the migration process from + plaintext to encrypted DynamoDB using the AWS Database Encryption SDK. + + In this example, we configure a DynamoDB Encryption client to do the following: + 1. Write items with encryption (no longer writing plaintext) + 2. Read both plaintext items and encrypted items + + Once you deploy this change to your system, you will have a dataset + containing both encrypted and plaintext items. + Because the changes in Step 1 have been deployed to all readers, + we can be sure that our entire system is ready to read this new data. + + Before you move onto the next step, you will need to encrypt all plaintext items in your dataset. + How you will want to do this depends on your system. + + Running this example requires access to the DDB Table whose name + is provided in the function parameter. + 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 MigrationStep2 + { + public static async Task MigrationStep2Example(string kmsKeyId, string ddbTableName, string partitionKeyValue, string sortKeyWriteValue, string sortKeyReadValue) + { + // 1. Create table configurations + // In this of migration we will use PlaintextOverride.FORBID_PLAINTEXT_WRITE_ALLOW_PLAINTEXT_READ + // which means: + // - Write: Items are forbidden to be written as plaintext. + // Items will be written as encrypted items. + // - Read: Items are allowed to be read as plaintext. + // Items are allowed to be read as encrypted items. + var tableConfigs = Common.CreateTableConfigs(kmsKeyId, ddbTableName, PlaintextOverride.FORBID_PLAINTEXT_WRITE_ALLOW_PLAINTEXT_READ); + + // 2. Create a new AWS SDK DynamoDb client using the TableEncryptionConfigs + var ddb = new Client.DynamoDbClient( + new DynamoDbTablesEncryptionConfig { TableEncryptionConfigs = tableConfigs }); + + // 3. Put an item into our table using the above client. + // This item will be encrypted due to our PlaintextOverride configuration. + string partitionKeyName = "partition_key"; + string sortKeyName = "sort_key"; + string encryptedAndSignedValue = MigrationUtils.ENCRYPTED_AND_SIGNED_VALUE; + string signOnlyValue = MigrationUtils.SIGN_ONLY_VALUE; + string doNothingValue = MigrationUtils.DO_NOTHING_VALUE; + var item = new Dictionary + { + [partitionKeyName] = new AttributeValue { S = partitionKeyValue }, + [sortKeyName] = new AttributeValue { N = sortKeyWriteValue }, + ["attribute1"] = new AttributeValue { S = encryptedAndSignedValue }, + ["attribute2"] = new AttributeValue { S = signOnlyValue }, + ["attribute3"] = new AttributeValue { S = doNothingValue } + }; + + var putRequest = new PutItemRequest + { + TableName = ddbTableName, + Item = item + }; + + var putResponse = await ddb.PutItemAsync(putRequest); + Debug.Assert(putResponse.HttpStatusCode == HttpStatusCode.OK); + + // 4. Get an item back from the table using the same client. + // If this is an item written in plaintext (i.e. any item written + // during Step 0 or 1), then the item will still be in plaintext. + // If this is an item that was encrypted client-side (i.e. any item written + // during Step 2 or after), then the item will be decrypted client-side + // and surfaced as a plaintext item. + var key = new Dictionary + { + [partitionKeyName] = new AttributeValue { S = partitionKeyValue }, + [sortKeyName] = new AttributeValue { N = sortKeyReadValue } + }; + + var getRequest = new GetItemRequest + { + TableName = ddbTableName, + Key = key, + // In this example we configure a strongly consistent read + // because we perform a read immediately after a write (for demonstrative purposes). + // By default, reads are only eventually consistent. + ConsistentRead = true + }; + + var getResponse = await ddb.GetItemAsync(getRequest); + Debug.Assert(getResponse.HttpStatusCode == HttpStatusCode.OK); + + // 5. Verify we get the expected item back + if (getResponse.Item == null) + { + throw new Exception("No item found"); + } + + bool success = MigrationUtils.VerifyReturnedItem(getResponse, partitionKeyValue, sortKeyReadValue); + if (success) + { + Console.WriteLine("MigrationStep2 completed successfully"); + } + return success; + } + } +} diff --git a/Examples/runtimes/net/src/migration/PlaintextToAWSDBE/awsdbe/MigrationStep2Test.cs b/Examples/runtimes/net/src/migration/PlaintextToAWSDBE/awsdbe/MigrationStep2Test.cs new file mode 100644 index 000000000..a1eb7075d --- /dev/null +++ b/Examples/runtimes/net/src/migration/PlaintextToAWSDBE/awsdbe/MigrationStep2Test.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Amazon.DynamoDBv2; +using Amazon.DynamoDBv2.Model; +using System.Diagnostics; +using Xunit; +using Examples.migration.PlaintextToAWSDBE; +using Examples.migration.PlaintextToAWSDBE.plaintext; + +namespace Examples.migration.PlaintextToAWSDBE.awsdbe +{ + public class MigrationStep2Test + { + [Fact] + public async Task TestMigrationStep2() + { + string kmsKeyID = TestUtils.TEST_KMS_KEY_ID; + string tableName = TestUtils.TEST_DDB_TABLE_NAME; + string partitionKey = Guid.NewGuid().ToString(); + string[] sortKeys = { "0", "1", "2", "3" }; + + // Successfully executes step 2 + bool success = await MigrationStep2.MigrationStep2Example(kmsKeyID, tableName, partitionKey, sortKeys[2], sortKeys[2]); + Assert.True(success, "MigrationStep2 should complete successfully"); + + // Given: Step 0 has succeeded + success = await MigrationStep0.MigrationStep0Example(tableName, partitionKey, sortKeys[0], sortKeys[0]); + Assert.True(success, "MigrationStep0 should complete successfully"); + + // When: Execute Step 2 with sortReadValue=0, Then: Success (i.e. can read plaintext values from Step 0) + success = await MigrationStep2.MigrationStep2Example(kmsKeyID, tableName, partitionKey, sortKeys[2], sortKeys[0]); + Assert.True(success, "MigrationStep2 should be able to read items written by Step 0"); + + // Given: Step 1 has succeeded + success = await MigrationStep1.MigrationStep1Example(kmsKeyID, tableName, partitionKey, sortKeys[1], sortKeys[1]); + Assert.True(success, "MigrationStep1 should complete successfully"); + + // When: Execute Step 2 with sortReadValue=1, Then: Success (i.e. can read plaintext values from Step 1) + success = await MigrationStep2.MigrationStep2Example(kmsKeyID, tableName, partitionKey, sortKeys[2], sortKeys[1]); + Assert.True(success, "MigrationStep2 should be able to read items written by Step 1"); + + // Given: Step 3 has succeeded + success = await MigrationStep3.MigrationStep3Example(kmsKeyID, tableName, partitionKey, sortKeys[3], sortKeys[3]); + Assert.True(success, "MigrationStep3 should complete successfully"); + + // When: Execute Step 2 with sortReadValue=3, Then: Success (i.e. can read encrypted values from Step 3) + success = await MigrationStep2.MigrationStep2Example(kmsKeyID, tableName, partitionKey, sortKeys[2], sortKeys[3]); + Assert.True(success, "MigrationStep2 should be able to read items written by Step 3"); + + // Cleanup + foreach (var sortKey in sortKeys) + { + await TestUtils.CleanupItems(tableName, partitionKey, sortKey); + } + } + } +} diff --git a/Examples/runtimes/net/src/migration/PlaintextToAWSDBE/awsdbe/MigrationStep3.cs b/Examples/runtimes/net/src/migration/PlaintextToAWSDBE/awsdbe/MigrationStep3.cs new file mode 100644 index 000000000..95cbd947f --- /dev/null +++ b/Examples/runtimes/net/src/migration/PlaintextToAWSDBE/awsdbe/MigrationStep3.cs @@ -0,0 +1,120 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Amazon.DynamoDBv2; +using Amazon.DynamoDBv2.Model; +using System.Diagnostics; +using System.Net; +using AWS.Cryptography.DbEncryptionSDK.DynamoDb; +using AWS.Cryptography.DbEncryptionSDK.StructuredEncryption; +using AWS.Cryptography.MaterialProviders; +using Examples.migration.PlaintextToAWSDBE; + +namespace Examples.migration.PlaintextToAWSDBE.awsdbe +{ + /* + Migration Step 3: This is the final step in the migration process from + plaintext to encrypted DynamoDB using the AWS Database Encryption SDK. + + In this example, we configure a DynamoDB Encryption client to do the following: + 1. Write items with encryption (no longer writing plaintext) + 2. Read only encrypted items (no longer reading plaintext) + + Once you complete Step 3, all items being read by your system are encrypted. + + Before you move onto this step, you will need to encrypt all plaintext items in your dataset. + How you will want to do this depends on your system. + + Running this example requires access to the DDB Table whose name + is provided in the function parameter. + 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 MigrationStep3 + { + public static async Task MigrationStep3Example(string kmsKeyId, string ddbTableName, string partitionKeyValue, string sortKeyWriteValue, string sortKeyReadValue) + { + // 1. Create table configurations + // In this of migration we will use PlaintextOverride.FORBID_PLAINTEXT_WRITE_FORBID_PLAINTEXT_READ + // which means: + // - Write: Items are forbidden to be written as plaintext. + // Items will be written as encrypted items. + // - Read: Items are forbidden to be read as plaintext. + // Items will be read as encrypted items. + // Note: If you do not specify a PlaintextOverride, it defaults to + // FORBID_PLAINTEXT_WRITE_FORBID_PLAINTEXT_READ, which is the desired + // behavior for a client interacting with a fully encrypted database. + var tableConfigs = Common.CreateTableConfigs(kmsKeyId, ddbTableName, PlaintextOverride.FORBID_PLAINTEXT_WRITE_FORBID_PLAINTEXT_READ); + + // 2. Create a new AWS SDK DynamoDb client using the TableEncryptionConfigs + var ddb = new Client.DynamoDbClient( + new DynamoDbTablesEncryptionConfig { TableEncryptionConfigs = tableConfigs }); + + // 3. Put an item into our table using the above client. + // This item will be encrypted due to our PlaintextOverride configuration. + string partitionKeyName = "partition_key"; + string sortKeyName = "sort_key"; + string encryptedAndSignedValue = MigrationUtils.ENCRYPTED_AND_SIGNED_VALUE; + string signOnlyValue = MigrationUtils.SIGN_ONLY_VALUE; + string doNothingValue = MigrationUtils.DO_NOTHING_VALUE; + var item = new Dictionary + { + ["partition_key"] = new AttributeValue { S = partitionKeyValue }, + ["sort_key"] = new AttributeValue { N = sortKeyWriteValue }, + ["attribute1"] = new AttributeValue { S = encryptedAndSignedValue }, + ["attribute2"] = new AttributeValue { S = signOnlyValue }, + ["attribute3"] = new AttributeValue { S = doNothingValue } + }; + + var putRequest = new PutItemRequest + { + TableName = ddbTableName, + Item = item + }; + + var putResponse = await ddb.PutItemAsync(putRequest); + Debug.Assert(putResponse.HttpStatusCode == HttpStatusCode.OK); + + // 4. Get an item back from the table using the same client. + // If this is an item written in plaintext (i.e. any item written + // during Step 0 or 1), then the read will fail, as we have + // configured our client to forbid reading plaintext items. + // If this is an item that was encrypted client-side (i.e. any item written + // during Step 2 or after), then the item will be decrypted client-side + // and surfaced as a plaintext item. + var key = new Dictionary + { + ["partition_key"] = new AttributeValue { S = partitionKeyValue }, + ["sort_key"] = new AttributeValue { N = sortKeyReadValue } + }; + + var getRequest = new GetItemRequest + { + TableName = ddbTableName, + Key = key, + // In this example we configure a strongly consistent read + // because we perform a read immediately after a write (for demonstrative purposes). + // By default, reads are only eventually consistent. + ConsistentRead = true + }; + + var getResponse = await ddb.GetItemAsync(getRequest); + Debug.Assert(getResponse.HttpStatusCode == HttpStatusCode.OK); + + // Verify we get the expected item back + if (getResponse.Item == null) + { + throw new Exception("No item found"); + } + + bool success = MigrationUtils.VerifyReturnedItem(getResponse, partitionKeyValue, sortKeyReadValue); + if (success) + { + Console.WriteLine("MigrationStep3 completed successfully"); + } + return success; + } + } +} diff --git a/Examples/runtimes/net/src/migration/PlaintextToAWSDBE/awsdbe/MigrationStep3Test.cs b/Examples/runtimes/net/src/migration/PlaintextToAWSDBE/awsdbe/MigrationStep3Test.cs new file mode 100644 index 000000000..52c3f9838 --- /dev/null +++ b/Examples/runtimes/net/src/migration/PlaintextToAWSDBE/awsdbe/MigrationStep3Test.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Amazon.DynamoDBv2; +using Amazon.DynamoDBv2.Model; +using System.Diagnostics; +using Xunit; +using Examples.migration.PlaintextToAWSDBE; +using Examples.migration.PlaintextToAWSDBE.plaintext; +using AWS.Cryptography.DbEncryptionSDK.DynamoDb.ItemEncryptor; + +namespace Examples.migration.PlaintextToAWSDBE.awsdbe +{ + public class MigrationStep3Test + { + [Fact] + public async Task TestMigrationStep3() + { + string kmsKeyID = TestUtils.TEST_KMS_KEY_ID; + string tableName = TestUtils.TEST_DDB_TABLE_NAME; + string partitionKey = Guid.NewGuid().ToString(); + string[] sortKeys = { "0", "1", "2", "3" }; + + // Successfully executes step 3 + bool success = await MigrationStep3.MigrationStep3Example(kmsKeyID, tableName, partitionKey, sortKeys[3], sortKeys[3]); + Assert.True(success, "MigrationStep3 should complete successfully"); + + // Given: Step 0 has succeeded + success = await MigrationStep0.MigrationStep0Example(tableName, partitionKey, sortKeys[0], sortKeys[0]); + Assert.True(success, "MigrationStep0 should complete successfully"); + + // When: Execute Step 3 with sortReadValue=0, Then: should error out when reading plaintext items from Step 0 + var exception = await Assert.ThrowsAsync(() => + MigrationStep3.MigrationStep3Example(kmsKeyID, tableName, partitionKey, sortKeys[3], sortKeys[0])); + + // Given: Step 1 has succeeded + success = await MigrationStep1.MigrationStep1Example(kmsKeyID, tableName, partitionKey, sortKeys[1], sortKeys[1]); + Assert.True(success, "MigrationStep1 should complete successfully"); + + // When: Execute Step 3 with sortReadValue=1, Then: should error out when reading plaintext items from Step 1 + exception = await Assert.ThrowsAsync(() => + MigrationStep3.MigrationStep3Example(kmsKeyID, tableName, partitionKey, sortKeys[3], sortKeys[1])); + Assert.Contains("encrypted item missing expected header and footer attributes", exception.Message.ToLower()); + + // Given: Step 2 has succeeded + success = await MigrationStep2.MigrationStep2Example(kmsKeyID, tableName, partitionKey, sortKeys[2], sortKeys[2]); + Assert.True(success, "MigrationStep2 should complete successfully"); + + Assert.Contains("encrypted item missing expected header and footer attributes", exception.Message.ToLower()); + + // When: Execute Step 3 with sortReadValue=2, Then: Success (i.e. can read encrypted values from Step 2) + success = await MigrationStep3.MigrationStep3Example(kmsKeyID, tableName, partitionKey, sortKeys[3], sortKeys[2]); + Assert.True(success, "MigrationStep3 should be able to read items written by Step 2"); + + // Cleanup + foreach (var sortKey in sortKeys) + { + await TestUtils.CleanupItems(tableName, partitionKey, sortKey); + } + } + } +} diff --git a/Examples/runtimes/net/src/migration/PlaintextToAWSDBE/plaintext/MigrationStep0.cs b/Examples/runtimes/net/src/migration/PlaintextToAWSDBE/plaintext/MigrationStep0.cs new file mode 100644 index 000000000..5e09c6b3e --- /dev/null +++ b/Examples/runtimes/net/src/migration/PlaintextToAWSDBE/plaintext/MigrationStep0.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Amazon.DynamoDBv2; +using Amazon.DynamoDBv2.Model; +using System.Diagnostics; +using System.Net; +using Examples.migration.PlaintextToAWSDBE; + +namespace Examples.migration.PlaintextToAWSDBE.plaintext +{ + /* + Migration Step 0: This is the pre-migration step for the + plaintext-to-encrypted database migration, and is the starting + state for our migration from a plaintext database to a + client-side encrypted database encrypted using the + AWS Database Encryption SDK for DynamoDb. + + In this example, we configure a DynamoDbClient to + write a plaintext record to a table and read that record. + This emulates the starting state of a plaintext-to-encrypted + database migration; i.e. a plaintext database you can + read and write to with the DynamoDbClient. + + Running this example requires access to the DDB Table whose name + is provided in the function parameter. + 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 MigrationStep0 + { + public static async Task MigrationStep0Example(string ddbTableName, string partitionKeyValue, string sortKeyWriteValue, string sortKeyReadValue) + { + // 1. Create a standard DynamoDB client + var ddb = new AmazonDynamoDBClient(); + + // 2. Put an example item into DynamoDB table + // This item will be stored in plaintext. + string encryptedAndSignedValue = MigrationUtils.ENCRYPTED_AND_SIGNED_VALUE; + string signOnlyValue = MigrationUtils.SIGN_ONLY_VALUE; + string doNothingValue = MigrationUtils.DO_NOTHING_VALUE; + var item = new Dictionary + { + ["partition_key"] = new AttributeValue { S = partitionKeyValue }, + ["sort_key"] = new AttributeValue { N = sortKeyWriteValue }, + ["attribute1"] = new AttributeValue { S = encryptedAndSignedValue }, + ["attribute2"] = new AttributeValue { S = signOnlyValue }, + ["attribute3"] = new AttributeValue { S = doNothingValue } + }; + + var putRequest = new PutItemRequest + { + TableName = ddbTableName, + Item = item + }; + + var putResponse = await ddb.PutItemAsync(putRequest); + Debug.Assert(putResponse.HttpStatusCode == HttpStatusCode.OK); + + // 3. Get an item back from the table as it was written. + // If this is an item written in plaintext (i.e. any item written + // during Step 0 or 1), then the item will still be in plaintext + // and will be able to be processed. + // If this is an item that was encrypted client-side (i.e. any item written + // during Step 2 or after), then the item will still be encrypted client-side + // and will be unable to be processed in your code. To decrypt and process + // client-side encrypted items, you will need to configure encrypted reads on + // your dynamodb client (this is configured from Step 1 onwards). + var key = new Dictionary + { + ["partition_key"] = new AttributeValue { S = partitionKeyValue }, + ["sort_key"] = new AttributeValue { N = sortKeyReadValue } + }; + + var getRequest = new GetItemRequest + { + TableName = ddbTableName, + Key = key + }; + + var getResponse = await ddb.GetItemAsync(getRequest); + Debug.Assert(getResponse.HttpStatusCode == HttpStatusCode.OK); + + // 4. Verify we get the expected item back + if (getResponse.Item == null) + { + throw new Exception("No item found"); + } + + bool success = MigrationUtils.VerifyReturnedItem(getResponse, partitionKeyValue, sortKeyReadValue); + if (success) + { + Console.WriteLine("MigrationStep0 completed successfully"); + } + return success; + } + + } +} diff --git a/Examples/runtimes/net/src/migration/PlaintextToAWSDBE/plaintext/MigrationStep0Test.cs b/Examples/runtimes/net/src/migration/PlaintextToAWSDBE/plaintext/MigrationStep0Test.cs new file mode 100644 index 000000000..4a06c646c --- /dev/null +++ b/Examples/runtimes/net/src/migration/PlaintextToAWSDBE/plaintext/MigrationStep0Test.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Amazon.DynamoDBv2; +using Amazon.DynamoDBv2.Model; +using System.Diagnostics; +using Xunit; +using Examples.migration.PlaintextToAWSDBE; + +namespace Examples.migration.PlaintextToAWSDBE.plaintext +{ + /* + Test for Migration Step 0: This tests the pre-migration step for the + plaintext-to-encrypted database migration. + + This test verifies that: + 1. Step 0 can successfully write and read plaintext items + 2. Step 0 can read items written by Step 1 (which are also plaintext) + 3. Step 0 cannot read items written by Steps 2 and 3 (which are encrypted) + + Running this test requires access to the DDB Table whose name + is provided by TestUtils.TEST_DDB_TABLE_NAME. + 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 MigrationStep0Test + { + [Fact] + public async Task TestMigrationStep0() + { + string kmsKeyID = TestUtils.TEST_KMS_KEY_ID; + string tableName = TestUtils.TEST_DDB_TABLE_NAME; + string partitionKey = Guid.NewGuid().ToString(); + string[] sortKeys = { "0", "1", "2", "3" }; + + // Successfully executes step 0 + bool success = await MigrationStep0.MigrationStep0Example(tableName, partitionKey, sortKeys[0], sortKeys[0]); + Assert.True(success, "MigrationStep0 should complete successfully"); + + // Given: Step 1 has succeeded + await awsdbe.MigrationStep1.MigrationStep1Example(kmsKeyID, tableName, partitionKey, sortKeys[1], sortKeys[1]); + + // When: Execute Step 0 with sortReadValue=1, Then: Success (i.e. can read plaintext values) + success = await MigrationStep0.MigrationStep0Example(tableName, partitionKey, sortKeys[0], sortKeys[1]); + Assert.True(success, "MigrationStep0 should be able to read items written by Step 1"); + + // Given: Step 2 has succeeded + await awsdbe.MigrationStep2.MigrationStep2Example(kmsKeyID, tableName, partitionKey, sortKeys[2], sortKeys[2]); + + // When: Execute Step 0 with sortReadValue=2, Then: should error out when reading encrypted items. + var exception = await Assert.ThrowsAsync(() => + MigrationStep0.MigrationStep0Example(tableName, partitionKey, sortKeys[0], sortKeys[2])); + + Assert.Contains("attribute1 mismatch", exception.Message); + + // Given: Step 3 has succeeded + await awsdbe.MigrationStep3.MigrationStep3Example(kmsKeyID, tableName, partitionKey, sortKeys[3], sortKeys[3]); + + // When: Execute Step 0 with sortReadValue=3, Then: should error out + exception = await Assert.ThrowsAsync(() => + MigrationStep0.MigrationStep0Example(tableName, partitionKey, sortKeys[0], sortKeys[3])); + Assert.Contains("attribute1 mismatch", exception.Message); + + // Cleanup + foreach (var sortKey in sortKeys) + { + await TestUtils.CleanupItems(tableName, partitionKey, sortKey); + } + } + } +}