Skip to content

Commit 4d9b3b2

Browse files
committed
type-hint
beacon examples
1 parent 5ca6fba commit 4d9b3b2

10 files changed

+1611
-0
lines changed
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
"""Example demonstrating how to get encrypted data key descriptions from DynamoDB items."""
4+
5+
import boto3
6+
from aws_dbesdk_dynamodb.smithygenerated.aws_cryptography_dbencryptionsdk_dynamodb.client import DynamoDbEncryption
7+
from aws_dbesdk_dynamodb.smithygenerated.aws_cryptography_dbencryptionsdk_dynamodb.config import (
8+
DynamoDbEncryptionConfig,
9+
)
10+
from aws_dbesdk_dynamodb.structures.dynamodb import (
11+
GetEncryptedDataKeyDescriptionInput,
12+
GetEncryptedDataKeyDescriptionUnionItem,
13+
)
14+
15+
16+
def get_encrypted_data_key_description(
17+
table_name: str,
18+
partition_key: str,
19+
partition_key_val: str,
20+
sort_key_name: str,
21+
sort_key_value: str,
22+
expected_key_provider_id: str,
23+
expected_key_provider_info: str,
24+
expected_branch_key_id: str,
25+
expected_branch_key_version: str,
26+
):
27+
"""
28+
Get encrypted data key description from a DynamoDB item.
29+
30+
:param table_name: The name of the DynamoDB table
31+
:param partition_key: The name of the partition key
32+
:param partition_key_val: The value of the partition key
33+
:param sort_key_name: The name of the sort key
34+
:param sort_key_value: The value of the sort key
35+
:param expected_key_provider_id: The expected key provider ID
36+
:param expected_key_provider_info: The expected key provider info (optional)
37+
:param expected_branch_key_id: The expected branch key ID (optional)
38+
:param expected_branch_key_version: The expected branch key version (optional)
39+
"""
40+
# 1. Create a new AWS SDK DynamoDb client. This client will be used to get item from the DynamoDB table
41+
ddb = boto3.client("dynamodb")
42+
43+
# 2. Get item from the DynamoDB table. This item will be used to Get Encrypted DataKey Description
44+
key_to_get = {partition_key: {"S": partition_key_val}, sort_key_name: {"N": sort_key_value}}
45+
46+
response = ddb.get_item(TableName=table_name, Key=key_to_get)
47+
48+
returned_item = response.get("Item", {})
49+
if not returned_item:
50+
print(f"No item found with the key {partition_key}!")
51+
return
52+
53+
# 3. Prepare the input for GetEncryptedDataKeyDescription method.
54+
# This input can be a DynamoDB item or a header. For now, we are giving input as a DynamoDB item
55+
# but users can also extract the header from the attribute "aws_dbe_head" in the DynamoDB table
56+
# and use it for GetEncryptedDataKeyDescription method.
57+
ddb_enc = DynamoDbEncryption(config=DynamoDbEncryptionConfig())
58+
59+
input_union = GetEncryptedDataKeyDescriptionUnionItem(returned_item)
60+
61+
input_obj = GetEncryptedDataKeyDescriptionInput(input=input_union)
62+
63+
output = ddb_enc.get_encrypted_data_key_description(input=input_obj)
64+
65+
# In the following code, we are giving input as header instead of a complete DynamoDB item
66+
# This code is provided solely to demo how the alternative approach works. So, it is commented.
67+
68+
# header_attribute = "aws_dbe_head"
69+
# header = returned_item[header_attribute]["B"]
70+
# input_union = GetEncryptedDataKeyDescriptionUnion(
71+
# header=header
72+
# )
73+
74+
# Assert everything
75+
assert output.encrypted_data_key_description_output[0].key_provider_id == expected_key_provider_id
76+
77+
if expected_key_provider_id.startswith("aws-kms"):
78+
assert output.encrypted_data_key_description_output[0].key_provider_info == expected_key_provider_info
79+
80+
if output.encrypted_data_key_description_output[0].key_provider_id == "aws-kms-hierarchy":
81+
assert output.encrypted_data_key_description_output[0].branch_key_id == expected_branch_key_id
82+
assert output.encrypted_data_key_description_output[0].branch_key_version == expected_branch_key_version

Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/basic_searchable_encryption_example.py

Lines changed: 312 additions & 0 deletions
Large diffs are not rendered by default.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,298 @@
1+
# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
"""
4+
Example demonstrating DynamoDB encryption using beacon styles.
5+
6+
This example demonstrates how to use Beacons Styles on Standard Beacons on encrypted attributes,
7+
put an item with the beacon, and query against that beacon.
8+
This example follows a use case of a database that stores food information.
9+
This is an extension of the "BasicSearchableEncryptionExample" in this directory
10+
and uses the same table schema.
11+
12+
Running this example requires access to a DDB table with the
13+
following key configuration:
14+
- Partition key is named "work_id" with type (S)
15+
- Sort key is named "inspection_time" with type (S)
16+
17+
In this example for storing food information, this schema is utilized for the data:
18+
- "work_id" stores a unique identifier for a unit inspection work order (v4 UUID)
19+
- "inspection_date" stores an ISO 8601 date for the inspection (YYYY-MM-DD)
20+
- "fruit" stores one type of fruit
21+
- "basket" stores a set of types of fruit
22+
- "dessert" stores one type of dessert
23+
- "veggies" stores a set of types of vegetable
24+
- "work_type" stores a unit inspection category
25+
26+
The example requires the following ordered input command line parameters:
27+
1. DDB table name for table to put/query data from
28+
2. Branch key ID for a branch key that was previously created in your key store. See the
29+
CreateKeyStoreKeyExample.
30+
3. Branch key wrapping KMS key ARN for the KMS key used to create the branch key with ID
31+
provided in arg 2
32+
4. Branch key DDB table name for the DDB table representing the branch key store
33+
"""
34+
from typing import List
35+
36+
import boto3
37+
from aws_cryptographic_material_providers.keystore.client import KeyStore
38+
from aws_cryptographic_material_providers.keystore.config import KeyStoreConfig
39+
from aws_cryptographic_material_providers.keystore.models import KMSConfigurationKmsKeyArn
40+
from aws_cryptographic_material_providers.mpl import AwsCryptographicMaterialProviders
41+
from aws_cryptographic_material_providers.mpl.config import MaterialProvidersConfig
42+
from aws_cryptographic_material_providers.mpl.models import CreateAwsKmsHierarchicalKeyringInput
43+
from aws_dbesdk_dynamodb.encrypted.client import EncryptedClient
44+
from aws_dbesdk_dynamodb.structures.dynamodb import (
45+
AsSet,
46+
BeaconKeySourceSingle,
47+
BeaconStyleAsSet,
48+
BeaconStylePartOnly,
49+
BeaconStyleShared,
50+
BeaconStyleSharedSet,
51+
BeaconVersion,
52+
CompoundBeacon,
53+
DynamoDbTableEncryptionConfig,
54+
DynamoDbTablesEncryptionConfig,
55+
EncryptedPart,
56+
PartOnly,
57+
SearchConfig,
58+
Shared,
59+
SharedSet,
60+
SignedPart,
61+
SingleKeyStore,
62+
StandardBeacon,
63+
)
64+
from aws_dbesdk_dynamodb.structures.structured_encryption import CryptoAction
65+
66+
67+
def put_item_query_item_with_beacon_styles(
68+
ddb_table_name: str, branch_key_id: str, branch_key_wrapping_kms_key_arn: str, branch_key_ddb_table_name: str
69+
):
70+
"""
71+
Demonstrate using beacon styles with DynamoDB encryption.
72+
73+
:param ddb_table_name: The name of the DynamoDB table
74+
:param branch_key_id: Branch key ID for a branch key previously created in key store
75+
:param branch_key_wrapping_kms_key_arn: ARN of KMS key used to create the branch key
76+
:param branch_key_ddb_table_name: Name of DDB table representing the branch key store
77+
"""
78+
# 1. Create Beacons.
79+
standard_beacon_list: List[StandardBeacon] = []
80+
81+
# The fruit beacon allows searching on the encrypted fruit attribute
82+
# We have selected 30 as an example beacon length, but you should go to
83+
# https://docs.aws.amazon.com/database-encryption-sdk/latest/devguide/choosing-beacon-length.html
84+
# when creating your beacons.
85+
fruit_beacon = StandardBeacon(name="fruit", length=30)
86+
standard_beacon_list.append(fruit_beacon)
87+
88+
# The basket beacon allows searching on the encrypted basket attribute
89+
# basket is used as a Set, and therefore needs a beacon style to reflect that.
90+
# Further, we need to be able to compare the items in basket to the fruit attribute
91+
# so we `share` this beacon with `fruit`.
92+
# Since we need both of these things, we use the SharedSet style.
93+
basket_beacon = StandardBeacon(name="basket", length=30, style=BeaconStyleSharedSet(SharedSet(other="fruit")))
94+
standard_beacon_list.append(basket_beacon)
95+
96+
# The dessert beacon allows searching on the encrypted dessert attribute
97+
# We need to be able to compare the dessert attribute to the fruit attribute
98+
# so we `share` this beacon with `fruit`.
99+
dessert_beacon = StandardBeacon(name="dessert", length=30, style=BeaconStyleShared(Shared(other="fruit")))
100+
standard_beacon_list.append(dessert_beacon)
101+
102+
# The veggie_beacon allows searching on the encrypted veggies attribute
103+
# veggies is used as a Set, and therefore needs a beacon style to reflect that.
104+
veggie_beacon = StandardBeacon(name="veggies", length=30, style=BeaconStyleAsSet(AsSet()))
105+
standard_beacon_list.append(veggie_beacon)
106+
107+
# The work_type_beacon allows searching on the encrypted work_type attribute
108+
# We only use it as part of the compound work_unit beacon,
109+
# so we disable its use as a standalone beacon
110+
work_type_beacon = StandardBeacon(name="work_type", length=30, style=BeaconStylePartOnly(PartOnly()))
111+
standard_beacon_list.append(work_type_beacon)
112+
113+
# Here we build a compound beacon from work_id and work_type
114+
# If we had tried to make a StandardBeacon from work_type, we would have seen an error
115+
# because work_type is "PartOnly"
116+
encrypted_part_list = [EncryptedPart(name="work_type", prefix="T-")]
117+
118+
signed_part_list = [SignedPart(name="work_id", prefix="I-")]
119+
120+
compound_beacon_list = [
121+
CompoundBeacon(name="work_unit", split=".", encrypted=encrypted_part_list, signed=signed_part_list)
122+
]
123+
124+
# 2. Configure the Keystore
125+
# These are the same constructions as in the Basic example, which describes these in more detail.
126+
keystore = KeyStore(
127+
config=KeyStoreConfig(
128+
ddb_client=boto3.client("dynamodb"),
129+
ddb_table_name=branch_key_ddb_table_name,
130+
logical_key_store_name=branch_key_ddb_table_name,
131+
kms_client=boto3.client("kms"),
132+
kms_configuration=KMSConfigurationKmsKeyArn(value=branch_key_wrapping_kms_key_arn),
133+
)
134+
)
135+
136+
# 3. Create BeaconVersion.
137+
# This is similar to the Basic example
138+
beacon_versions = [
139+
BeaconVersion(
140+
standard_beacons=standard_beacon_list,
141+
compound_beacons=compound_beacon_list,
142+
version=1, # MUST be 1
143+
key_store=keystore,
144+
key_source=BeaconKeySourceSingle(SingleKeyStore(key_id=branch_key_id, cache_ttl=6000)),
145+
)
146+
]
147+
148+
# 4. Create a Hierarchical Keyring
149+
# This is the same configuration as in the Basic example.
150+
mat_prov = AwsCryptographicMaterialProviders(config=MaterialProvidersConfig())
151+
152+
keyring_input = CreateAwsKmsHierarchicalKeyringInput(
153+
branch_key_id=branch_key_id, key_store=keystore, ttl_seconds=6000
154+
)
155+
156+
kms_keyring = mat_prov.create_aws_kms_hierarchical_keyring(input=keyring_input)
157+
158+
# 5. Configure which attributes are encrypted and/or signed when writing new items.
159+
attribute_actions = {
160+
"work_id": CryptoAction.SIGN_ONLY, # Our partition attribute must be SIGN_ONLY
161+
"inspection_date": CryptoAction.SIGN_ONLY, # Our sort attribute must be SIGN_ONLY
162+
"dessert": CryptoAction.ENCRYPT_AND_SIGN, # Beaconized attributes must be encrypted
163+
"fruit": CryptoAction.ENCRYPT_AND_SIGN, # Beaconized attributes must be encrypted
164+
"basket": CryptoAction.ENCRYPT_AND_SIGN, # Beaconized attributes must be encrypted
165+
"veggies": CryptoAction.ENCRYPT_AND_SIGN, # Beaconized attributes must be encrypted
166+
"work_type": CryptoAction.ENCRYPT_AND_SIGN, # Beaconized attributes must be encrypted
167+
}
168+
169+
# 6. Create the DynamoDb Encryption configuration for the table we will be writing to.
170+
# The beaconVersions are added to the search configuration.
171+
table_config = DynamoDbTableEncryptionConfig(
172+
logical_table_name=ddb_table_name,
173+
partition_key_name="work_id",
174+
sort_key_name="inspection_date",
175+
attribute_actions_on_encrypt=attribute_actions,
176+
keyring=kms_keyring,
177+
search=SearchConfig(write_version=1, versions=beacon_versions), # MUST be 1
178+
)
179+
180+
table_configs = {ddb_table_name: table_config}
181+
tables_config = DynamoDbTablesEncryptionConfig(table_encryption_configs=table_configs)
182+
183+
# 7. Create the EncryptedClient
184+
ddb_client = boto3.client("dynamodb")
185+
encrypted_ddb_client = EncryptedClient(client=ddb_client, encryption_config=tables_config)
186+
187+
# 8. Create item one, specifically with "dessert != fruit", and "fruit in basket".
188+
item1 = {
189+
"work_id": {"S": "1"},
190+
"inspection_date": {"S": "2023-06-13"},
191+
"dessert": {"S": "cake"},
192+
"fruit": {"S": "banana"},
193+
"basket": {"SS": ["apple", "banana", "pear"]},
194+
"veggies": {"SS": ["beans", "carrots", "celery"]},
195+
"work_type": {"S": "small"},
196+
}
197+
198+
# 9. Create item two, specifically with "dessert == fruit", and "fruit not in basket".
199+
item2 = {
200+
"work_id": {"S": "2"},
201+
"inspection_date": {"S": "2023-06-13"},
202+
"fruit": {"S": "orange"},
203+
"dessert": {"S": "orange"},
204+
"basket": {"SS": ["blackberry", "blueberry", "strawberry"]},
205+
"veggies": {"SS": ["beans", "carrots", "peas"]},
206+
"work_type": {"S": "large"},
207+
}
208+
209+
# 10. Add the two items
210+
put_response = encrypted_ddb_client.put_item(TableName=ddb_table_name, Item=item1)
211+
# Validate object put successfully
212+
assert put_response["ResponseMetadata"]["HTTPStatusCode"] == 200
213+
214+
put_response = encrypted_ddb_client.put_item(TableName=ddb_table_name, Item=item2)
215+
# Validate object put successfully
216+
assert put_response["ResponseMetadata"]["HTTPStatusCode"] == 200
217+
218+
# 11. Test the first type of Set operation:
219+
# Select records where the basket attribute holds a particular value
220+
expression_attribute_values = {":value": {"S": "banana"}}
221+
222+
scan_response = encrypted_ddb_client.scan(
223+
TableName=ddb_table_name,
224+
FilterExpression="contains(basket, :value)",
225+
ExpressionAttributeValues=expression_attribute_values,
226+
)
227+
# Validate query was returned successfully
228+
assert scan_response["ResponseMetadata"]["HTTPStatusCode"] == 200
229+
230+
# Validate only 1 item was returned: item1
231+
assert len(scan_response["Items"]) == 1
232+
assert scan_response["Items"][0] == item1
233+
234+
# 12. Test the second type of Set operation:
235+
# Select records where the basket attribute holds the fruit attribute
236+
scan_response = encrypted_ddb_client.scan(TableName=ddb_table_name, FilterExpression="contains(basket, fruit)")
237+
# Validate query was returned successfully
238+
assert scan_response["ResponseMetadata"]["HTTPStatusCode"] == 200
239+
240+
# Validate only 1 item was returned: item1
241+
assert len(scan_response["Items"]) == 1
242+
assert scan_response["Items"][0] == item1
243+
244+
# 13. Test the third type of Set operation:
245+
# Select records where the fruit attribute exists in a particular set
246+
expression_attribute_values = {":value": {"SS": ["boysenberry", "grape", "orange"]}}
247+
248+
scan_response = encrypted_ddb_client.scan(
249+
TableName=ddb_table_name,
250+
FilterExpression="contains(:value, fruit)",
251+
ExpressionAttributeValues=expression_attribute_values,
252+
)
253+
# Validate query was returned successfully
254+
assert scan_response["ResponseMetadata"]["HTTPStatusCode"] == 200
255+
256+
# Validate only 1 item was returned: item2
257+
assert len(scan_response["Items"]) == 1
258+
assert scan_response["Items"][0] == item2
259+
260+
# 14. Test a Shared search. Select records where the dessert attribute matches the fruit attribute
261+
scan_response = encrypted_ddb_client.scan(TableName=ddb_table_name, FilterExpression="dessert = fruit")
262+
# Validate query was returned successfully
263+
assert scan_response["ResponseMetadata"]["HTTPStatusCode"] == 200
264+
265+
# Validate only 1 item was returned: item2
266+
assert len(scan_response["Items"]) == 1
267+
assert scan_response["Items"][0] == item2
268+
269+
# 15. Test the AsSet attribute 'veggies':
270+
# Select records where the veggies attribute holds a particular value
271+
expression_attribute_values = {":value": {"S": "peas"}}
272+
273+
scan_response = encrypted_ddb_client.scan(
274+
TableName=ddb_table_name,
275+
FilterExpression="contains(veggies, :value)",
276+
ExpressionAttributeValues=expression_attribute_values,
277+
)
278+
# Validate query was returned successfully
279+
assert scan_response["ResponseMetadata"]["HTTPStatusCode"] == 200
280+
281+
# Validate only 1 item was returned: item2
282+
assert len(scan_response["Items"]) == 1
283+
assert scan_response["Items"][0] == item2
284+
285+
# 16. Test the compound beacon 'work_unit':
286+
expression_attribute_values = {":value": {"S": "I-1.T-small"}}
287+
288+
scan_response = encrypted_ddb_client.scan(
289+
TableName=ddb_table_name,
290+
FilterExpression="work_unit = :value",
291+
ExpressionAttributeValues=expression_attribute_values,
292+
)
293+
# Validate query was returned successfully
294+
assert scan_response["ResponseMetadata"]["HTTPStatusCode"] == 200
295+
296+
# Validate only 1 item was returned: item1
297+
assert len(scan_response["Items"]) == 1
298+
assert scan_response["Items"][0] == item1

0 commit comments

Comments
 (0)