Skip to content

Commit 814acbf

Browse files
chore(net): Add plaintext to encrypted table migration example (#1976)
1 parent 516fd3d commit 814acbf

File tree

14 files changed

+916
-0
lines changed

14 files changed

+916
-0
lines changed

.github/workflows/ci_examples_net.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,3 +94,4 @@ jobs:
9494
shell: bash
9595
run: |
9696
dotnet run
97+
dotnet test

Examples/runtimes/net/Examples.csproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111
<ItemGroup>
1212
<ProjectReference Include="../../../DynamoDbEncryption/runtimes/net/DynamoDbEncryption.csproj" />
1313
<PackageReference Include="AWSSDK.SecurityToken" Version="3.7.202.5"/>
14+
<PackageReference Include="xunit" Version="2.4.0" />
15+
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.0" />
16+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
1417
</ItemGroup>
1518

1619
</Project>

Examples/runtimes/net/src/TestUtils.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Linq;
3+
using System.Collections.Generic;
34
using Amazon.DynamoDBv2.Model;
45

56
public class TestUtils
@@ -84,4 +85,21 @@ public static void PrintAttributeValue(AttributeValue value)
8485
if (value.IsBOOLSet) Console.Write($"BOOL {value.BOOL}\n");
8586
Console.Write("UNKNOWN\n");
8687
}
88+
89+
// Helper method to clean up test items
90+
public static async System.Threading.Tasks.Task CleanupItems(string tableName, string partitionKey, string sortKey)
91+
{
92+
var ddb = new Amazon.DynamoDBv2.AmazonDynamoDBClient();
93+
var key = new Dictionary<string, AttributeValue>
94+
{
95+
["partition_key"] = new AttributeValue { S = partitionKey },
96+
["sort_key"] = new AttributeValue { N = sortKey }
97+
};
98+
var deleteRequest = new DeleteItemRequest
99+
{
100+
TableName = tableName,
101+
Key = key
102+
};
103+
await ddb.DeleteItemAsync(deleteRequest);
104+
}
87105
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using Amazon.DynamoDBv2.Model;
4+
5+
namespace Examples.migration.PlaintextToAWSDBE
6+
{
7+
/*
8+
Utility class for the PlaintextToAWSDBE migration examples.
9+
This class contains shared functionality used by all migration steps.
10+
*/
11+
public class MigrationUtils
12+
{
13+
// Common attribute values used across all migration steps
14+
public static readonly string ENCRYPTED_AND_SIGNED_VALUE = "this will be encrypted and signed";
15+
public static readonly string SIGN_ONLY_VALUE = "this will never be encrypted, but it will be signed";
16+
public static readonly string DO_NOTHING_VALUE = "this will never be encrypted nor signed";
17+
18+
// Verify that a returned item matches the expected values
19+
public static bool VerifyReturnedItem(GetItemResponse response, string partitionKeyValue, string sortKeyValue)
20+
{
21+
var item = response.Item;
22+
23+
if (!item.ContainsKey("partition_key") || item["partition_key"].S != partitionKeyValue)
24+
{
25+
throw new Exception($"partition_key mismatch: expected {partitionKeyValue}, got {(item.ContainsKey("partition_key") ? item["partition_key"].S : "null")}");
26+
}
27+
28+
if (!item.ContainsKey("sort_key") || item["sort_key"].N != sortKeyValue)
29+
{
30+
throw new Exception($"sort_key mismatch: expected {sortKeyValue}, got {(item.ContainsKey("sort_key") ? item["sort_key"].N : "null")}");
31+
}
32+
33+
if (!item.ContainsKey("attribute1") || item["attribute1"].S != ENCRYPTED_AND_SIGNED_VALUE)
34+
{
35+
throw new Exception($"attribute1 mismatch: expected {ENCRYPTED_AND_SIGNED_VALUE}, got {(item.ContainsKey("attribute1") ? item["attribute1"].S : "null")}");
36+
}
37+
38+
if (!item.ContainsKey("attribute2") || item["attribute2"].S != SIGN_ONLY_VALUE)
39+
{
40+
throw new Exception($"attribute2 mismatch: expected {SIGN_ONLY_VALUE}, got {(item.ContainsKey("attribute2") ? item["attribute2"].S : "null")}");
41+
}
42+
43+
if (!item.ContainsKey("attribute3") || item["attribute3"].S != DO_NOTHING_VALUE)
44+
{
45+
throw new Exception($"attribute3 mismatch: expected {DO_NOTHING_VALUE}, got {(item.ContainsKey("attribute3") ? item["attribute3"].S : "null")}");
46+
}
47+
48+
return true;
49+
}
50+
}
51+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# Plaintext DynamoDB Table to AWS Database Encryption SDK Encrypted Table Migration
2+
3+
This projects demonstrates the steps necessary
4+
to migrate to the AWS Database Encryption SDK for DynamoDb
5+
from a plaintext database.
6+
7+
[Step 0](plaintext/step0.go) demonstrates the starting state for your system.
8+
9+
## Step 1
10+
11+
In Step 1, you update your system to do the following:
12+
13+
- continue to read plaintext items
14+
- continue to write plaintext items
15+
- prepare to read encrypted items
16+
17+
When you deploy changes in Step 1,
18+
you should not expect any behavior change in your system,
19+
and your dataset still consists of plaintext data.
20+
21+
You must ensure that the changes in Step 1 make it to all your readers before you proceed to Step 2.
22+
23+
## Step 2
24+
25+
In Step 2, you update your system to do the following:
26+
27+
- continue to read plaintext items
28+
- start writing encrypted items
29+
- continue to read encrypted items
30+
31+
When you deploy changes in Step 2,
32+
you are introducing encrypted items to your system,
33+
and must make sure that all your readers are updated with the changes from Step 1.
34+
35+
Before you move onto the next step, you will need to encrypt all plaintext items in your dataset.
36+
Once you have completed this step,
37+
while new items are being encrypted using the new format and will be authenticated on read,
38+
your system will still accept reading plaintext, unauthenticated items.
39+
In order to complete migration to a system where you always authenticate your items,
40+
you should prioritize moving on to Step 3.
41+
42+
## Step 3
43+
44+
Once all old items are encrypted,
45+
update your system to do the following:
46+
47+
- continue to write encrypted items
48+
- continue to read encrypted items
49+
- do not accept reading plaintext items
50+
51+
Once you have deployed these changes to your system, you have completed migration.
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
using System.Collections.Generic;
2+
using AWS.Cryptography.DbEncryptionSDK.DynamoDb;
3+
using AWS.Cryptography.DbEncryptionSDK.StructuredEncryption;
4+
using AWS.Cryptography.MaterialProviders;
5+
6+
namespace Examples.migration.PlaintextToAWSDBE
7+
{
8+
public static class Common
9+
{
10+
public static Dictionary<string, DynamoDbTableEncryptionConfig> CreateTableConfigs(string kmsKeyId, string ddbTableName, PlaintextOverride PlaintextOverride)
11+
{
12+
// Create a Keyring. This Keyring will be responsible for protecting the data keys that protect your data.
13+
// For this example, we will create a AWS KMS Keyring with the AWS KMS Key we want to use.
14+
// We will use the `CreateMrkMultiKeyring` method to create this keyring,
15+
// as it will correctly handle both single region and Multi-Region KMS Keys.
16+
var matProv = new MaterialProviders(new MaterialProvidersConfig());
17+
var keyringInput = new CreateAwsKmsMrkMultiKeyringInput { Generator = kmsKeyId };
18+
var kmsKeyring = matProv.CreateAwsKmsMrkMultiKeyring(keyringInput);
19+
20+
// Configure which attributes are encrypted and/or signed when writing new items.
21+
// For each attribute that may exist on the items we plan to write to our DynamoDbTable,
22+
// we must explicitly configure how they should be treated during item encryption:
23+
// - ENCRYPT_AND_SIGN: The attribute is encrypted and included in the signature
24+
// - SIGN_ONLY: The attribute not encrypted, but is still included in the signature
25+
// - DO_NOTHING: The attribute is not encrypted and not included in the signature
26+
string partitionKeyName = "partition_key";
27+
string sortKeyName = "sort_key";
28+
var attributeActionsOnEncrypt = new Dictionary<string, CryptoAction>
29+
{
30+
[partitionKeyName] = CryptoAction.SIGN_ONLY,
31+
[sortKeyName] = CryptoAction.SIGN_ONLY,
32+
["attribute1"] = CryptoAction.ENCRYPT_AND_SIGN,
33+
["attribute2"] = CryptoAction.SIGN_ONLY,
34+
["attribute3"] = CryptoAction.DO_NOTHING
35+
};
36+
37+
// Configure which attributes we expect to be excluded in the signature
38+
// when reading items. There are two options for configuring this:
39+
//
40+
// - (Recommended) Configure `allowedUnsignedAttributesPrefix`:
41+
// When defining your DynamoDb schema and deciding on attribute names,
42+
// choose a distinguishing prefix (such as ":") for all attributes that
43+
// you do not want to include in the signature.
44+
// This has two main benefits:
45+
// - It is easier to reason about the security and authenticity of data within your item
46+
// when all unauthenticated data is easily distinguishable by their attribute name.
47+
// - If you need to add new unauthenticated attributes in the future,
48+
// you can easily make the corresponding update to your `attributeActionsOnEncrypt`
49+
// and immediately start writing to that new attribute, without
50+
// any other configuration update needed.
51+
// Once you configure this field, it is not safe to update it.
52+
//
53+
// - Configure `allowedUnsignedAttributes`: You may also explicitly list
54+
// a set of attributes that should be considered unauthenticated when encountered
55+
// on read. Be careful if you use this configuration. Do not remove an attribute
56+
// name from this configuration, even if you are no longer writing with that attribute,
57+
// as old items may still include this attribute, and our configuration needs to know
58+
// to continue to exclude this attribute from the signature scope.
59+
// If you add new attribute names to this field, you must first deploy the update to this
60+
// field to all readers in your host fleet before deploying the update to start writing
61+
// with that new attribute.
62+
//
63+
// For this example, we will explicitly list the attributes that are not signed.
64+
var unsignedAttributes = new List<string> { "attribute3" };
65+
66+
// Create the DynamoDb Encryption configuration for the table we will be writing to.
67+
var tableConfig = new DynamoDbTableEncryptionConfig
68+
{
69+
LogicalTableName = ddbTableName,
70+
PartitionKeyName = partitionKeyName,
71+
SortKeyName = sortKeyName,
72+
AttributeActionsOnEncrypt = attributeActionsOnEncrypt,
73+
Keyring = kmsKeyring,
74+
AllowedUnsignedAttributes = unsignedAttributes,
75+
PlaintextOverride = PlaintextOverride
76+
};
77+
78+
return new Dictionary<string, DynamoDbTableEncryptionConfig>
79+
{
80+
[ddbTableName] = tableConfig
81+
};
82+
}
83+
}
84+
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Threading.Tasks;
4+
using Amazon.DynamoDBv2;
5+
using Amazon.DynamoDBv2.Model;
6+
using System.Diagnostics;
7+
using System.Net;
8+
using AWS.Cryptography.DbEncryptionSDK.DynamoDb;
9+
using AWS.Cryptography.DbEncryptionSDK.StructuredEncryption;
10+
using AWS.Cryptography.MaterialProviders;
11+
using Examples.migration.PlaintextToAWSDBE;
12+
13+
namespace Examples.migration.PlaintextToAWSDBE.awsdbe
14+
{
15+
/*
16+
Migration Step 1: This is the first step in the migration process from
17+
plaintext to encrypted DynamoDB using the AWS Database Encryption SDK.
18+
19+
In this example, we configure a DynamoDB Encryption client to do the following:
20+
1. Write items only in plaintext
21+
2. Read items in plaintext or, if the item is encrypted, decrypt with our encryption configuration
22+
23+
While this step configures your client to be ready to start reading encrypted items,
24+
we do not yet expect to be reading any encrypted items,
25+
as our client still writes plaintext items.
26+
Before you move on to step 2, ensure that these changes have successfully been deployed
27+
to all of your readers.
28+
29+
Running this example requires access to the DDB Table whose name
30+
is provided in the function parameter.
31+
This table must be configured with the following
32+
primary key configuration:
33+
- Partition key is named "partition_key" with type (S)
34+
- Sort key is named "sort_key" with type (N)
35+
*/
36+
public class MigrationStep1
37+
{
38+
public static async Task<bool> MigrationStep1Example(string kmsKeyId, string ddbTableName, string partitionKeyValue, string sortKeyWriteValue, string sortKeyReadValue)
39+
{
40+
// 1. Create table configurations
41+
// In this of migration we will use PlaintextOverride.FORCE_PLAINTEXT_WRITE_ALLOW_PLAINTEXT_READ
42+
// which means:
43+
// - Write: Items are forced to be written as plaintext.
44+
// Items may not be written as encrypted items.
45+
// - Read: Items are allowed to be read as plaintext.
46+
// Items are allowed to be read as encrypted items.
47+
var tableConfigs = Common.CreateTableConfigs(kmsKeyId, ddbTableName, PlaintextOverride.FORCE_PLAINTEXT_WRITE_ALLOW_PLAINTEXT_READ);
48+
49+
// 1. Create a new AWS SDK DynamoDb client using the TableEncryptionConfigs
50+
var ddb = new Client.DynamoDbClient(
51+
new DynamoDbTablesEncryptionConfig { TableEncryptionConfigs = tableConfigs });
52+
53+
// 2. Put an item into our table using the above client.
54+
// This item will be stored in plaintext due to our PlaintextOverride configuration.
55+
string partitionKeyName = "partition_key";
56+
string sortKeyName = "sort_key";
57+
string encryptedAndSignedValue = MigrationUtils.ENCRYPTED_AND_SIGNED_VALUE;
58+
string signOnlyValue = MigrationUtils.SIGN_ONLY_VALUE;
59+
string doNothingValue = MigrationUtils.DO_NOTHING_VALUE;
60+
var item = new Dictionary<string, AttributeValue>
61+
{
62+
[partitionKeyName] = new AttributeValue { S = partitionKeyValue },
63+
[sortKeyName] = new AttributeValue { N = sortKeyWriteValue },
64+
["attribute1"] = new AttributeValue { S = encryptedAndSignedValue },
65+
["attribute2"] = new AttributeValue { S = signOnlyValue },
66+
["attribute3"] = new AttributeValue { S = doNothingValue }
67+
};
68+
69+
var putRequest = new PutItemRequest
70+
{
71+
TableName = ddbTableName,
72+
Item = item
73+
};
74+
75+
var putResponse = await ddb.PutItemAsync(putRequest);
76+
Debug.Assert(putResponse.HttpStatusCode == HttpStatusCode.OK);
77+
78+
// 3. Get an item back from the table using the same client.
79+
// If this is an item written in plaintext (i.e. any item written
80+
// during Step 0 or 1), then the item will still be in plaintext.
81+
// If this is an item that was encrypted client-side (i.e. any item written
82+
// during Step 2 or after), then the item will be decrypted client-side
83+
// and surfaced as a plaintext item.
84+
var key = new Dictionary<string, AttributeValue>
85+
{
86+
[partitionKeyName] = new AttributeValue { S = partitionKeyValue },
87+
[sortKeyName] = new AttributeValue { N = sortKeyReadValue }
88+
};
89+
90+
var getRequest = new GetItemRequest
91+
{
92+
TableName = ddbTableName,
93+
Key = key,
94+
// In this example we configure a strongly consistent read
95+
// because we perform a read immediately after a write (for demonstrative purposes).
96+
// By default, reads are only eventually consistent.
97+
ConsistentRead = true
98+
};
99+
100+
var getResponse = await ddb.GetItemAsync(getRequest);
101+
Debug.Assert(getResponse.HttpStatusCode == HttpStatusCode.OK);
102+
103+
// 4. Verify we get the expected item back
104+
if (getResponse.Item == null)
105+
{
106+
throw new Exception("No item found");
107+
}
108+
109+
bool success = MigrationUtils.VerifyReturnedItem(getResponse, partitionKeyValue, sortKeyReadValue);
110+
if (success)
111+
{
112+
Console.WriteLine("MigrationStep1 completed successfully");
113+
}
114+
return success;
115+
}
116+
}
117+
}

0 commit comments

Comments
 (0)