main/searchableencryption/compound_beacon_searchable_encryption.rs
1// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4use crate::test_utils;
5use aws_db_esdk::client as transform_client;
6use aws_db_esdk::dynamodb::types::BeaconKeySource;
7use aws_db_esdk::dynamodb::types::BeaconVersion;
8use aws_db_esdk::dynamodb::types::CompoundBeacon;
9use aws_db_esdk::dynamodb::types::DynamoDbTableEncryptionConfig;
10use aws_db_esdk::dynamodb::types::EncryptedPart;
11use aws_db_esdk::dynamodb::types::SearchConfig;
12use aws_db_esdk::dynamodb::types::SingleKeyStore;
13use aws_db_esdk::dynamodb::types::StandardBeacon;
14use aws_db_esdk::intercept::DbEsdkInterceptor;
15use aws_db_esdk::key_store::client as keystore_client;
16use aws_db_esdk::key_store::types::key_store_config::KeyStoreConfig;
17use aws_db_esdk::key_store::types::KmsConfiguration;
18use aws_db_esdk::material_providers::client as mpl_client;
19use aws_db_esdk::material_providers::types::material_providers_config::MaterialProvidersConfig;
20use aws_db_esdk::CryptoAction;
21use aws_db_esdk::DynamoDbTablesEncryptionConfig;
22use aws_sdk_dynamodb::types::AttributeValue;
23use std::collections::HashMap;
24
25/*
26 This example demonstrates how to set up a compound beacon on encrypted attributes,
27 put an item with the beacon, and query against that beacon.
28 This example follows a use case of a database that stores unit inspection information.
29 This is an extension of the "BasicSearchableEncryptionExample" in this directory.
30 This example uses the same situation (storing unit inspection information)
31 and the same table schema.
32 However, this example uses a different Global Secondary Index (GSI)
33 that is based on a compound beacon configuration composed of
34 the `last4` and `unit` attributes.
35
36 Running this example requires access to a DDB table with the
37 following key configuration:
38 - Partition key is named "work_id" with type (S)
39 - Sort key is named "inspection_time" with type (S)
40 This table must have a Global Secondary Index (GSI) configured named "last4UnitCompound-index":
41 - Partition key is named "aws_dbe_b_last4UnitCompound" with type (S)
42
43 In this example for storing unit inspection information, this schema is utilized for the data:
44 - "work_id" stores a unique identifier for a unit inspection work order (v4 UUID)
45 - "inspection_date" stores an ISO 8601 date for the inspection (YYYY-MM-DD)
46 - "inspector_id_last4" stores the last 4 digits of the ID of the inspector performing the work
47 - "unit" stores a 12-digit serial number for the unit being inspected
48
49 The example requires the following ordered input command line parameters:
50 1. DDB table name for table to put/query data from
51 2. Branch key ID for a branch key that was previously created in your key store. See the
52 CreateKeyStoreKeyExample.
53 3. Branch key wrapping KMS key ARN for the KMS key used to create the branch key with ID
54 provided in arg 2
55 4. Branch key DDB table ARN for the DDB table representing the branch key store
56*/
57
58const GSI_NAME: &str = "last4UnitCompound-index";
59
60pub async fn put_and_query_with_beacon(branch_key_id: &str) -> Result<(), crate::BoxError> {
61 let ddb_table_name = test_utils::UNIT_INSPECTION_TEST_DDB_TABLE_NAME;
62 let branch_key_wrapping_kms_key_arn = test_utils::TEST_BRANCH_KEY_WRAPPING_KMS_KEY_ARN;
63 let branch_key_ddb_table_name = test_utils::TEST_BRANCH_KEYSTORE_DDB_TABLE_NAME;
64
65 // 1. Create Beacons.
66 // These are the same beacons as in the "BasicSearchableEncryptionExample" in this directory.
67 // See that file to see details on beacon construction and parameters.
68 // While we will not directly query against these beacons,
69 // you must create standard beacons on encrypted fields
70 // that we wish to use in compound beacons.
71 let last4_beacon = StandardBeacon::builder()
72 .name("inspector_id_last4")
73 .length(10)
74 .build()?;
75
76 let unit_beacon = StandardBeacon::builder().name("unit").length(30).build()?;
77
78 let standard_beacon_list = vec![last4_beacon, unit_beacon];
79
80 // 2. Define encrypted parts.
81 // Encrypted parts define the beacons that can be used to construct a compound beacon,
82 // and how the compound beacon prefixes those beacon values.
83
84 // A encrypted part must receive:
85 // - name: Name of a standard beacon
86 // - prefix: Any string. This is plaintext that prefixes the beaconized value in the compound beacon.
87 // Prefixes must be unique across the configuration, and must not be a prefix of another prefix;
88 // i.e. for all configured prefixes, the first N characters of a prefix must not equal another prefix.
89 // In practice, it is suggested to have a short value distinguishable from other parts served on the prefix.
90 // For this example, we will choose "L-" as the prefix for "Last 4 digits of inspector ID".
91 // With this prefix and the standard beacon's bit length definition (10), the beaconized
92 // version of the inspector ID's last 4 digits will appear as
93 // `L-000` to `L-3ff` inside a compound beacon.
94
95 // For this example, we will choose "U-" as the prefix for "unit".
96 // With this prefix and the standard beacon's bit length definition (30), a unit beacon will appear
97 // as `U-00000000` to `U-3fffffff` inside a compound beacon.
98 let encrypted_parts_list = vec![
99 EncryptedPart::builder()
100 .name("inspector_id_last4")
101 .prefix("L-")
102 .build()?,
103 EncryptedPart::builder().name("unit").prefix("U-").build()?,
104 ];
105
106 // 3. Define compound beacon.
107 // A compound beacon allows one to serve multiple beacons or attributes from a single index.
108 // A compound beacon must receive:
109 // - name: The name of the beacon. Compound beacon values will be written to `aws_ddb_e_[name]`.
110 // - split: A character separating parts in a compound beacon
111 // A compound beacon may also receive:
112 // - encrypted: A list of encrypted parts. This is effectively a list of beacons. We provide the list
113 // that we created above.
114 // - constructors: A list of constructors. This is an ordered list of possible ways to create a beacon.
115 // We have not defined any constructors here; see the complex example for how to do this.
116 // The client will provide a default constructor, which will write a compound beacon as:
117 // all signed parts in the order they are added to the signed list;
118 // all encrypted parts in order they are added to the encrypted list; all parts required.
119 // In this example, we expect compound beacons to be written as
120 // `L-XXX.U-YYYYYYYY`, since our encrypted list looks like
121 // [last4EncryptedPart, unitEncryptedPart].
122 // - signed: A list of signed parts, i.e. plaintext attributes. This would be provided if we
123 // wanted to use plaintext values as part of constructing our compound beacon. We do not
124 // provide this here; see the Complex example for an example.
125 let compound_beacon_list = vec![CompoundBeacon::builder()
126 .name("last4UnitCompound")
127 .split(".")
128 .encrypted(encrypted_parts_list)
129 .build()?];
130
131 // 4. Configure the Keystore
132 // These are the same constructions as in the Basic example, which describes these in more detail.
133
134 let sdk_config = aws_config::load_defaults(aws_config::BehaviorVersion::latest()).await;
135 let key_store_config = KeyStoreConfig::builder()
136 .kms_client(aws_sdk_kms::Client::new(&sdk_config))
137 .ddb_client(aws_sdk_dynamodb::Client::new(&sdk_config))
138 .ddb_table_name(branch_key_ddb_table_name)
139 .logical_key_store_name(branch_key_ddb_table_name)
140 .kms_configuration(KmsConfiguration::KmsKeyArn(
141 branch_key_wrapping_kms_key_arn.to_string(),
142 ))
143 .build()?;
144
145 let key_store = keystore_client::Client::from_conf(key_store_config)?;
146
147 // 5. Create BeaconVersion.
148 // This is similar to the Basic example, except we have also provided a compoundBeaconList.
149 // We must also continue to provide all of the standard beacons that compose a compound beacon list.
150 let beacon_version = BeaconVersion::builder()
151 .standard_beacons(standard_beacon_list)
152 .compound_beacons(compound_beacon_list)
153 .version(1) // MUST be 1
154 .key_store(key_store.clone())
155 .key_source(BeaconKeySource::Single(
156 SingleKeyStore::builder()
157 // `keyId` references a beacon key.
158 // For every branch key we create in the keystore,
159 // we also create a beacon key.
160 // This beacon key is not the same as the branch key,
161 // but is created with the same ID as the branch key.
162 .key_id(branch_key_id)
163 .cache_ttl(6000)
164 .build()?,
165 ))
166 .build()?;
167 let beacon_versions = vec![beacon_version];
168
169 // 6. Create a Hierarchical Keyring
170 // This is the same configuration as in the Basic example.
171
172 let mpl_config = MaterialProvidersConfig::builder().build()?;
173 let mpl = mpl_client::Client::from_conf(mpl_config)?;
174 let kms_keyring = mpl
175 .create_aws_kms_hierarchical_keyring()
176 .branch_key_id(branch_key_id)
177 .key_store(key_store)
178 .ttl_seconds(6000)
179 .send()
180 .await?;
181
182 // 7. Configure which attributes are encrypted and/or signed when writing new items.
183 let attribute_actions_on_encrypt = HashMap::from([
184 ("work_id".to_string(), CryptoAction::SignOnly), // Our partition attribute must be SIGN_ONLY
185 ("inspection_date".to_string(), CryptoAction::SignOnly), // Our sort attribute must be SIGN_ONLY
186 (
187 "inspector_id_last4".to_string(),
188 CryptoAction::EncryptAndSign,
189 ), // Beaconized attributes must be encrypted
190 ("unit".to_string(), CryptoAction::EncryptAndSign), // Beaconized attributes must be encrypted
191 ]);
192
193 // We do not need to define a crypto action on last4UnitCompound.
194 // We only need to define crypto actions on attributes that we pass to PutItem.
195
196 // 8. Create the DynamoDb Encryption configuration for the table we will be writing to.
197 // The beaconVersions are added to the search configuration.
198 let table_config = DynamoDbTableEncryptionConfig::builder()
199 .logical_table_name(ddb_table_name)
200 .partition_key_name("work_id")
201 .sort_key_name("inspection_date")
202 .attribute_actions_on_encrypt(attribute_actions_on_encrypt)
203 .keyring(kms_keyring)
204 .search(
205 SearchConfig::builder()
206 .write_version(1) // MUST be 1
207 .versions(beacon_versions)
208 .build()?,
209 )
210 .build()?;
211
212 // 9. Create config
213 let encryption_config = DynamoDbTablesEncryptionConfig::builder()
214 .table_encryption_configs(HashMap::from([(ddb_table_name.to_string(), table_config)]))
215 .build()?;
216
217 // 10. Create an item with both attributes used in the compound beacon.
218 let item = HashMap::from([
219 (
220 "work_id".to_string(),
221 AttributeValue::S("9ce39272-8068-4efd-a211-cd162ad65d4c".to_string()),
222 ),
223 (
224 "inspection_date".to_string(),
225 AttributeValue::S("2023-06-13".to_string()),
226 ),
227 (
228 "inspector_id_last4".to_string(),
229 AttributeValue::S("5678".to_string()),
230 ),
231 (
232 "unit".to_string(),
233 AttributeValue::S("011899988199".to_string()),
234 ),
235 ]);
236
237 // 11. If developing or debugging, verify config by checking compound beacon values directly
238 let trans = transform_client::Client::from_conf(encryption_config.clone())?;
239 let resolve_output = trans
240 .resolve_attributes()
241 .table_name(ddb_table_name)
242 .item(item.clone())
243 .version(1)
244 .send()
245 .await?;
246
247 // Verify that there are no virtual fields
248 assert_eq!(resolve_output.virtual_fields.unwrap().len(), 0);
249
250 // Verify that CompoundBeacons has the expected value
251 let compound_beacons = resolve_output.compound_beacons.unwrap();
252 assert_eq!(compound_beacons.len(), 1);
253 assert_eq!(
254 compound_beacons["last4UnitCompound"],
255 "L-5678.U-011899988199"
256 );
257 // Note : the compound beacon actually stored in the table is not "L-5678.U-011899988199"
258 // but rather something like "L-abc.U-123", as both parts are EncryptedParts
259 // and therefore the text is replaced by the associated beacon
260
261 // 12. Create a new AWS SDK DynamoDb client using the DynamoDb Encryption Interceptor above
262 let dynamo_config = aws_sdk_dynamodb::config::Builder::from(&sdk_config)
263 .interceptor(DbEsdkInterceptor::new(encryption_config)?)
264 .build();
265 let ddb = aws_sdk_dynamodb::Client::from_conf(dynamo_config);
266
267 // 13. Write the item to the table
268 ddb.put_item()
269 .table_name(ddb_table_name)
270 .set_item(Some(item.clone()))
271 .send()
272 .await?;
273
274 // 14. Query for the item we just put.
275 let expression_attribute_values = HashMap::from([
276 // This query expression takes a few factors into consideration:
277 // - The configured prefix for the last 4 digits of an inspector ID is "L-";
278 // the prefix for the unit is "U-"
279 // - The configured split character, separating component parts, is "."
280 // - The default constructor adds encrypted parts in the order they are in the encrypted list, which
281 // configures `last4` to come before `unit``
282 // NOTE: We did not need to create a compound beacon for this query. This query could have also been
283 // done by querying on the partition and sort key, as was done in the Basic example.
284 // This is intended to be a simple example to demonstrate how one might set up a compound beacon.
285 // For examples where compound beacons are required, see the Complex example.
286 // The most basic extension to this example that would require a compound beacon would add a third
287 // part to the compound beacon, then query against three parts.
288 (
289 ":value".to_string(),
290 AttributeValue::S("L-5678.U-011899988199".to_string()),
291 ),
292 ]);
293
294 // GSIs are sometimes a little bit delayed, so we retry if the query comes up empty.
295 for _i in 0..10 {
296 let query_response = ddb
297 .query()
298 .table_name(ddb_table_name)
299 .index_name(GSI_NAME)
300 .key_condition_expression("last4UnitCompound = :value")
301 .set_expression_attribute_values(Some(expression_attribute_values.clone()))
302 .send()
303 .await?;
304
305 // if no results, sleep and try again
306 if query_response.items.is_none() || query_response.items.as_ref().unwrap().is_empty() {
307 std::thread::sleep(std::time::Duration::from_millis(20));
308 continue;
309 }
310
311 let attribute_values = query_response.items.unwrap();
312 // Validate only 1 item was returned: the item we just put
313 assert_eq!(attribute_values.len(), 1);
314 let returned_item = &attribute_values[0];
315 // Validate the item has the expected attributes
316 assert_eq!(
317 returned_item["inspector_id_last4"],
318 AttributeValue::S("5678".to_string())
319 );
320 assert_eq!(
321 returned_item["unit"],
322 AttributeValue::S("011899988199".to_string())
323 );
324 break;
325 }
326 println!("compound_beacon_searchable_encryption successful.");
327 Ok(())
328}