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);
+ }
+ }
+ }
+}