Skip to content

Commit 5286619

Browse files
chore(rust): add plaintext to encrypted table migration examples (#1977)
1 parent c71fc82 commit 5286619

File tree

14 files changed

+996
-0
lines changed

14 files changed

+996
-0
lines changed

.github/workflows/library_rust_tests.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,3 +146,4 @@ jobs:
146146
shell: bash
147147
run: |
148148
cargo run --release --example main
149+
cargo test --release --example main

DynamoDbEncryption/runtimes/rust/examples/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ pub mod keyring;
1414
pub mod multi_get_put_example;
1515
pub mod searchableencryption;
1616
pub mod test_utils;
17+
pub mod migration;
1718

1819
use std::convert::From;
1920

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
pub mod plaintext_to_awsdbe;
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: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
use aws_db_esdk::material_providers::client;
5+
use aws_db_esdk::material_providers::types::material_providers_config::MaterialProvidersConfig;
6+
use aws_db_esdk::CryptoAction;
7+
use aws_db_esdk::dynamodb::types::DynamoDbTableEncryptionConfig;
8+
use aws_db_esdk::types::dynamo_db_tables_encryption_config::DynamoDbTablesEncryptionConfig;
9+
use aws_db_esdk::dynamodb::types::PlaintextOverride;
10+
use std::collections::HashMap;
11+
12+
pub async fn create_table_configs(
13+
kms_key_id: &str,
14+
ddb_table_name: &str,
15+
plaintext_override: PlaintextOverride,
16+
) -> Result<DynamoDbTablesEncryptionConfig, Box<dyn std::error::Error>> {
17+
// Create a Keyring. This Keyring will be responsible for protecting the data keys that protect your data.
18+
// For this example, we will create a AWS KMS Keyring with the AWS KMS Key we want to use.
19+
// We will use the `CreateMrkMultiKeyring` method to create this keyring,
20+
// as it will correctly handle both single region and Multi-Region KMS Keys.
21+
let provider_config = MaterialProvidersConfig::builder().build()?;
22+
let mat_prov = client::Client::from_conf(provider_config)?;
23+
let kms_keyring = mat_prov
24+
.create_aws_kms_mrk_multi_keyring()
25+
.generator(kms_key_id)
26+
.send()
27+
.await?;
28+
29+
// Configure which attributes are encrypted and/or signed when writing new items.
30+
// For each attribute that may exist on the items we plan to write to our DynamoDbTable,
31+
// we must explicitly configure how they should be treated during item encryption:
32+
// - ENCRYPT_AND_SIGN: The attribute is encrypted and included in the signature
33+
// - SIGN_ONLY: The attribute not encrypted, but is still included in the signature
34+
// - DO_NOTHING: The attribute is not encrypted and not included in the signature
35+
let partition_key_name = "partition_key";
36+
let sort_key_name = "sort_key";
37+
let attribute_actions_on_encrypt = HashMap::from([
38+
(partition_key_name.to_string(), CryptoAction::SignOnly),
39+
(sort_key_name.to_string(), CryptoAction::SignOnly),
40+
("attribute1".to_string(), CryptoAction::EncryptAndSign),
41+
("attribute2".to_string(), CryptoAction::SignOnly),
42+
("attribute3".to_string(), CryptoAction::DoNothing),
43+
]);
44+
45+
// Configure which attributes we expect to be excluded in the signature
46+
// when reading items. There are two options for configuring this:
47+
//
48+
// - (Recommended) Configure `allowedUnsignedAttributesPrefix`:
49+
// When defining your DynamoDb schema and deciding on attribute names,
50+
// choose a distinguishing prefix (such as ":") for all attributes that
51+
// you do not want to include in the signature.
52+
// This has two main benefits:
53+
// - It is easier to reason about the security and authenticity of data within your item
54+
// when all unauthenticated data is easily distinguishable by their attribute name.
55+
// - If you need to add new unauthenticated attributes in the future,
56+
// you can easily make the corresponding update to your `attributeActionsOnEncrypt`
57+
// and immediately start writing to that new attribute, without
58+
// any other configuration update needed.
59+
// Once you configure this field, it is not safe to update it.
60+
//
61+
// - Configure `allowedUnsignedAttributes`: You may also explicitly list
62+
// a set of attributes that should be considered unauthenticated when encountered
63+
// on read. Be careful if you use this configuration. Do not remove an attribute
64+
// name from this configuration, even if you are no longer writing with that attribute,
65+
// as old items may still include this attribute, and our configuration needs to know
66+
// to continue to exclude this attribute from the signature scope.
67+
// If you add new attribute names to this field, you must first deploy the update to this
68+
// field to all readers in your host fleet before deploying the update to start writing
69+
// with that new attribute.
70+
//
71+
// For this example, we will explicitly list the attributes that are not signed.
72+
let unsigned_attributes = vec!["attribute3".to_string()];
73+
74+
// Create the DynamoDb Encryption configuration for the table we will be writing to.
75+
let table_config = DynamoDbTableEncryptionConfig::builder()
76+
.logical_table_name(ddb_table_name)
77+
.partition_key_name(partition_key_name)
78+
.sort_key_name(sort_key_name)
79+
.attribute_actions_on_encrypt(attribute_actions_on_encrypt)
80+
.keyring(kms_keyring)
81+
.allowed_unsigned_attributes(unsigned_attributes)
82+
.plaintext_override(plaintext_override)
83+
.build()?;
84+
85+
let table_configs = DynamoDbTablesEncryptionConfig::builder()
86+
.table_encryption_configs(HashMap::from([(ddb_table_name.to_string(), table_config)]))
87+
.build()?;
88+
89+
Ok(table_configs)
90+
}
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
use aws_sdk_dynamodb::types::AttributeValue;
5+
use std::collections::HashMap;
6+
use aws_db_esdk::intercept::DbEsdkInterceptor;
7+
use aws_db_esdk::dynamodb::types::PlaintextOverride;
8+
use crate::migration::plaintext_to_awsdbe::migration_utils::{
9+
verify_returned_item, ENCRYPTED_AND_SIGNED_VALUE, SIGN_ONLY_VALUE, DO_NOTHING_VALUE,
10+
};
11+
use crate::migration::plaintext_to_awsdbe::awsdbe::common::create_table_configs;
12+
13+
/*
14+
Migration Step 1: This is the first step in the migration process from
15+
plaintext to encrypted DynamoDB using the AWS Database Encryption SDK.
16+
17+
In this example, we configure a DynamoDB Encryption client to do the following:
18+
1. Write items only in plaintext
19+
2. Read items in plaintext or, if the item is encrypted, decrypt with our encryption configuration
20+
21+
While this step configures your client to be ready to start reading encrypted items,
22+
we do not yet expect to be reading any encrypted items,
23+
as our client still writes plaintext items.
24+
Before you move on to step 2, ensure that these changes have successfully been deployed
25+
to all of your readers.
26+
27+
Running this example requires access to the DDB Table whose name
28+
is provided in the function parameter.
29+
This table must be configured with the following
30+
primary key configuration:
31+
- Partition key is named "partition_key" with type (S)
32+
- Sort key is named "sort_key" with type (N)
33+
*/
34+
pub async fn migration_step_1_example(
35+
kms_key_id: &str,
36+
ddb_table_name: &str,
37+
partition_key_value: &str,
38+
sort_key_write_value: &str,
39+
sort_key_read_value: &str,
40+
) -> Result<bool, Box<dyn std::error::Error>> {
41+
// 1. Create table configurations
42+
// In this step of migration we will use PlaintextOverride::ForcePlaintextWriteAllowPlaintextRead
43+
// which means:
44+
// - Write: Items are forced to be written as plaintext.
45+
// Items may not be written as encrypted items.
46+
// - Read: Items are allowed to be read as plaintext.
47+
// Items are allowed to be read as encrypted items.
48+
let table_configs = create_table_configs(
49+
kms_key_id,
50+
ddb_table_name,
51+
PlaintextOverride::ForcePlaintextWriteAllowPlaintextRead,
52+
)
53+
.await?;
54+
55+
// 2. Create a new AWS SDK DynamoDb client using the TableEncryptionConfigs
56+
let sdk_config = aws_config::load_defaults(aws_config::BehaviorVersion::latest()).await;
57+
let dynamo_config = aws_sdk_dynamodb::config::Builder::from(&sdk_config)
58+
.interceptor(DbEsdkInterceptor::new(table_configs)?)
59+
.build();
60+
let ddb = aws_sdk_dynamodb::Client::from_conf(dynamo_config);
61+
62+
// 3. Put an item into our table using the above client.
63+
// This item will be stored in plaintext due to our PlaintextOverride configuration.
64+
let partition_key_name = "partition_key";
65+
let sort_key_name = "sort_key";
66+
let encrypted_and_signed_value = ENCRYPTED_AND_SIGNED_VALUE;
67+
let sign_only_value = SIGN_ONLY_VALUE;
68+
let do_nothing_value = DO_NOTHING_VALUE;
69+
let item = HashMap::from([
70+
(
71+
partition_key_name.to_string(),
72+
AttributeValue::S(partition_key_value.to_string()),
73+
),
74+
(
75+
sort_key_name.to_string(),
76+
AttributeValue::N(sort_key_write_value.to_string()),
77+
),
78+
(
79+
"attribute1".to_string(),
80+
AttributeValue::S(encrypted_and_signed_value.to_string()),
81+
),
82+
(
83+
"attribute2".to_string(),
84+
AttributeValue::S(sign_only_value.to_string()),
85+
),
86+
(
87+
"attribute3".to_string(),
88+
AttributeValue::S(do_nothing_value.to_string()),
89+
),
90+
]);
91+
92+
ddb.put_item()
93+
.table_name(ddb_table_name)
94+
.set_item(Some(item))
95+
.send()
96+
.await?;
97+
98+
// 4. Get an item back from the table using the same client.
99+
// If this is an item written in plaintext (i.e. any item written
100+
// during Step 0 or 1), then the item will still be in plaintext.
101+
// If this is an item that was encrypted client-side (i.e. any item written
102+
// during Step 2 or after), then the item will be decrypted client-side
103+
// and surfaced as a plaintext item.
104+
let key = HashMap::from([
105+
(
106+
partition_key_name.to_string(),
107+
AttributeValue::S(partition_key_value.to_string()),
108+
),
109+
(
110+
sort_key_name.to_string(),
111+
AttributeValue::N(sort_key_read_value.to_string()),
112+
),
113+
]);
114+
115+
let response = ddb
116+
.get_item()
117+
.table_name(ddb_table_name)
118+
.set_key(Some(key))
119+
// In this example we configure a strongly consistent read
120+
// because we perform a read immediately after a write (for demonstrative purposes).
121+
// By default, reads are only eventually consistent.
122+
.consistent_read(true)
123+
.send()
124+
.await?;
125+
126+
// 5. Verify we get the expected item back
127+
if let Some(item) = response.item {
128+
let success = verify_returned_item(&item, partition_key_value, sort_key_read_value)?;
129+
if success {
130+
println!("MigrationStep1 completed successfully");
131+
}
132+
Ok(success)
133+
} else {
134+
Err("No item found".into())
135+
}
136+
}
137+
138+
#[tokio::test(flavor = "multi_thread")]
139+
async fn test_migration_step_1() -> Result<(), Box<dyn std::error::Error>> {
140+
use crate::migration::plaintext_to_awsdbe::plaintext::migration_step_0::migration_step_0_example;
141+
use crate::migration::plaintext_to_awsdbe::awsdbe::migration_step_2::migration_step_2_example;
142+
use crate::migration::plaintext_to_awsdbe::awsdbe::migration_step_3::migration_step_3_example;
143+
use crate::test_utils;
144+
use uuid::Uuid;
145+
146+
let kms_key_id = test_utils::TEST_KMS_KEY_ID;
147+
let table_name = test_utils::TEST_DDB_TABLE_NAME;
148+
let partition_key = Uuid::new_v4().to_string();
149+
let sort_keys = ["0", "1", "2", "3"];
150+
151+
// Successfully executes step 1
152+
let success = migration_step_1_example(kms_key_id, table_name, &partition_key, sort_keys[1], sort_keys[1]).await?;
153+
assert!(success, "MigrationStep1 should complete successfully");
154+
155+
// Given: Step 0 has succeeded
156+
let success = migration_step_0_example(table_name, &partition_key, sort_keys[0], sort_keys[0]).await?;
157+
assert!(success, "MigrationStep0 should complete successfully");
158+
159+
// When: Execute Step 1 with sortReadValue=0, Then: Success (i.e. can read plaintext values from Step 0)
160+
let success = migration_step_1_example(kms_key_id, table_name, &partition_key, sort_keys[1], sort_keys[0]).await?;
161+
assert!(success, "MigrationStep1 should be able to read items written by Step 0");
162+
163+
// Given: Step 2 has succeeded
164+
let success = migration_step_2_example(kms_key_id, table_name, &partition_key, sort_keys[2], sort_keys[2]).await?;
165+
assert!(success, "MigrationStep2 should complete successfully");
166+
167+
// When: Execute Step 1 with sortReadValue=2, Then: Success (i.e. can read encrypted values from Step 2)
168+
let success = migration_step_1_example(kms_key_id, table_name, &partition_key, sort_keys[1], sort_keys[2]).await?;
169+
assert!(success, "MigrationStep1 should be able to read items written by Step 2");
170+
171+
// Given: Step 3 has succeeded
172+
let success = migration_step_3_example(kms_key_id, table_name, &partition_key, sort_keys[3], sort_keys[3]).await?;
173+
assert!(success, "MigrationStep3 should complete successfully");
174+
175+
// When: Execute Step 1 with sortReadValue=3, Then: Success (i.e. can read encrypted values from Step 3)
176+
let success = migration_step_1_example(kms_key_id, table_name, &partition_key, sort_keys[1], sort_keys[3]).await?;
177+
assert!(success, "MigrationStep1 should be able to read items written by Step 3");
178+
179+
// Cleanup
180+
for sort_key in &sort_keys {
181+
test_utils::cleanup_items(table_name, &partition_key, sort_key).await?;
182+
}
183+
184+
Ok(())
185+
}

0 commit comments

Comments
 (0)